gloomy的介绍int 2e的文章

系统调用接口
===========================
                              Я смотрел на снег весь день... Падающий...
                              Всегда вниз. Падающий весь день.
                              И тогда я закричал "Это жизнь?"
                                             (c) by My Dying Bride

系统调用是线程由用户模式转向内核模式的接口。自然,如果讲到系统的安全性和可靠性,研究系统调用的实现机制是非常有益的。系统服务实现上的错误就是系统安全上的漏洞,因为任何用户模式下的线程都能利用这个错误来访问内核模式。

因此,用户模式下的线程需要调用系统服务并转入内核模式。系统服务的调用是通过中断2Eh进行的。用户模式模块NTDLL.DLL将调用转向内核中的许多函数。例如,导出函数NtQuerySection的代码形式如下:

7F67CDC                 public _NtQuerySection@20
7F67CDC _NtQuerySection@20 proc near
7F67CDC
7F67CDC arg_0           = byte ptr  4
7F67CDC
7F67CDC                 mov     eax, 77h      ; NtQuerySection
7F67CE1                 lea     edx, [esp+arg_0]
7F67CE5                 int     2Eh
7F67CE7                 retn    14h
7F67CE7 _NtQuerySection@20 endp

实际上,NTDLL.DLL中其它所有的对内核服务的调用都是这个样子。从代码中可以看到,调用中断2Eh时,EAX寄存器为服务的功能号,EDX寄存器为堆栈中参数的地址。现在我们来看NTOSKRNL.EXE中_KiSystemService(中断2Eh的处理程序)的部分代码。有意思的是下面这一段:

[skipped]

8013CB20 _KiEndUnexpectedRange proc near
8013CB20                 cmp     ecx, 10h ; if call to win32k.sys
8013CB23                 jnz     short Kss_LimitError
8013CB25                 push    edx
8013CB26                 push    ebx
8013CB27                 call    _PsConvertToGuiThread@0
8013CB2C                 or      eax, eax
8013CB2E                 pop     eax
8013CB2F                 pop     edx

[skipped]

8013CBD0 ;           S u b r o u t i n e
8013CBD0
8013CBD0        public   _KiSystemService
8013CBD0 _KiSystemService proc near           ; DATA   XREF: INIT:801C7A50 o

[skipped]

8013CBD8        mov  ebx, 30h
8013CBDD        db   66h
8013CBDD        mov  fs, bx            ; set fs to 30 (processor contol region)
8013CBE0        push      dword ptr ds:0FFDFF000h
8013CBE6        mov  dword ptr ds:0FFDFF000h, 0FFFFFFFFh
8013CBF0        mov  esi, ds:0FFDFF124h   ; Current Kernel Thread Pointer
8013CBF6        push      dword ptr [esi+137h] ; previous mode: Kernel/user

[skipped]

8013CC29 _KiSystemServiceRepeat:
8013CC29        mov  edi, eax  ; function number
8013CC2B        shr  edi, 8
8013CC2E        and  edi, 30h
8013CC31        mov  ecx, edi
8013CC33        add  edi, [esi+0DCh] ; got service tables address
8013CC39        mov  ebx, eax
8013CC3B        and  eax, 0FFFh
8013CC40        cmp  eax, [edi+8]    ; num of services
8013CC43        jnb  _KiEndYnexpectedRange

[skipped]

8013CC6E        mov  esi, edx    ; parameters addres
8013CC70        mov  ebx, [edi+0Ch]   ; table with sizes
8013CC73        xor  ecx, ecx
8013CC75        mov  cl, [eax+ebx]    ; size of parameters
8013CC78        mov  edi, [edi]       ; handler's table
8013CC7A        mov  ebx, [edi+eax*4] ; got function address
8013CC7D        sub  esp, ecx    ; clear stack
8013CC7F        shr  ecx, 2
8013CC82        mov  edi, esp
8013CC84        cmp  esi, ds:_MmUserProbeAddress ; 7fff0000
8013CC8A        jnb  kss80
8013CC90 KiSystemServiceCopyArguments:
8013CC90        repe movsd          ; copy   to ring0 stack
8013CC92 kssdoit:
8013CC92        call      ebx
8013CC94 kss60:
8013CC94        mov  esp, ebp
8013CC96 kss70:
8013CC96        mov  ecx, ds:0FFDFF124h
8013CC9C        mov  edx, [ebp+3Ch]
8013CC9F        mov  [ecx+128h], edx
8013CC9F _KiSystemService endp
8013CC9F
8013CCA5 _KiServiceExit   proc near

[skipped]

8013CE34 kss80:
8013CE34        test      byte ptr [ebp+6Ch], 1 ; kernel/user
8013CE38        jz   KiSystemServiceCopyArguments
8013CE3E        mov  eax, 0C0000005h ; STATUS_ACCESS_VIOLATION
8013CE43        jmp  kss60

这样,如果调用产生于ring0,则处理程序检查参数是否位于用户地址区域中(见8013CC84)。之后,处理程序检查传递给它的参数(向ring0堆栈中拷贝参数起始于标号KiSystemServiceCopyArguments)。如果没有错误,则按照预先从服务地址表中选出的地址进行CALL EBX。接着,注意到两个有趣的地方。第一个是,所有的核心线程都能够取得服务地址表的地址(参照8013CBF0和8013CC33处的代码)。第二个有趣的地方是,服务表可以有四个(对于每个线程来说)。标号_KiSystemServiceRepeat处代码的调用依赖于位0x3000的值,从四个描述服务表地址的描述符中选择一个。描述符占据16字节并连续排列。这四个描述符总称服务描述符表。对于每一个线程,在内核线程结构体中都有其自己的指向服务描述符表的指针。这个指针可以从线程结构体的0DCh偏移处取得(Windows NT 4.0下)。线程结构体的地址可以在内核模式下从PCRB的偏移124h处取得。(MOV EAX, FS:[124h])。每个线程都有自己指向服务描述符表的指针,实际上,所有线程中的指针都指向两个描述符表中的一个。这两个表位于NTOSKERNEL.EXE,分别叫做KeServiceDescriptorTable和KeServiceDescriptorTableShadow。表中的描述符的格式如下:

typedef struct _ServiceDescriptor{
     DWORD* ServiceTable; /* 指向服务地址表的指针 */
     DWORD Reserved;      /* 在checked build下使用 */
     DWORD ServiceLimit;  /* 表中服务的数目 */
     BYTE* ArgumentTable; /* 指向服务堆栈中参数表大小的指针 */
                          /* 实际上等于 (4*参数数目) */
     }ServiceDescriptor;

在系统初始化(KiInitSystem)时,表KeServiceDescriptorTable和KeServiceDescriptorShadow的描述符0被初始化为以下这个样子(伪代码):

KeServiceDescriptorTable [0].ServiceTable = KiServiceTable;
KeServiceDescriptorTable [0].ServiceLimit = KiServiceLimit;
KeServiceDescriptorTable [0].ArgumentTable = KiArgumentTable;
memcpy (&KeServiceDescriptorTableShadow[0], &KeServiceDescriptorTable[0],0x10);

其余的描述符都为0。KiServiceTable是NTOSKRNL.EXE中函数的偏移表。KiArgumentTable为服务参数数目乘以4(参数堆栈frame的大小)。KiServiceLimit为KiServiceTable表中服务的数目。KeServiceDescriptorTableShadow表的描述符0,为创建的描述符的副本。因此,描述符0是在内核初始化时填充的,并描述了内核的基本服务。这个描述符对所有线程都是相同的。那剩下的描述符是做什么的?在驱动WIN32K.SYS初始化的时候会调用内核函数KeAddSystemServiceTable。其伪代码如下:

BOOL KeAddSystemServiceTable (
          PVOID* ServiceTable,
          ULONG Reserved,
          ULONG Limit,
          BYTE* Arguments,
          ULONG NumOfDesc)
{
     if (NumOfDesc>3) return 0;
     Descriptor= &KeServiceDescriptorTable [NumOfDesc*16];
     if (Descriptor->ServiceTable)return 0;
     ShadowDescriptor= &KeServiceDescriptorTableShadow[NumOfDesc*16];
     if (ShadowDescriptor->ServiceTable) return 0;

     ShadowDescriptor->ServiceTable=ServiceTable;
     ShadowDescriptor->Reserved=Reserved;
     ShadowDescriptor->ServiceLimit=Limit;
     ShadowDescriptor->ArgumentTable=Arguments;

     if (NumOfDesc!=1){
          Descriptor->ServiceTable=ServiceTable;
          Descriptor->Reserved=Reserved;
          Descriptor->ServiceLimit=Limit;
          Descriptor->ArgumentTable=Arguments;
          }
return 1;
}

函数很简单,但可从中获取不少信息。这个函数填充四个描述符中的一个,一般来说,可能填充shadow table,也有可能填充主表(若描述符为0,则未使用)。但有一个特殊之处很有意思——如果添加描述符1,则该描述符只会被添加到shadow table中。初始化WIN32K.SYS时,恰好添加的就是描述符1。此时,其余的描述符并未使用。我们知道,为了提高效率,在Windows NT v4.0下,Win32子系统的USER和GDI函数都是在内核中实现的。Win32k是内核模式驱动程序,它实现了Win32函数,描述符1描述了这些服务。现在,我们来看一下,这些表都为线程提供了什么。函数KeInitializeThread有两行:

80119344    mov     esi, [ebp+lpThread]
[skipped]
80119394    mov     dword ptr [esi+0DCh], offset _KeServiceDescriptorTable

下面又有两行,但是在PsConvertToGuiThread函数中的:

80192919    mov     ecx, [ebp+lpServiceDescriptorTable] ; thread struct + 0dch
[skipped]
80192926    mov     dword ptr [ecx], offset _KeServiceDescriptorTableShadow

如果调用的是WIN32K.SYS的服务,但用于当前线程的描述符1并未初始化,则在2e中断处理程序中会调用函数PsConvertToGuiThread。服务描述符表有两个——主表和shadow表。在主表中只有一个偏移为0的非零描述符,其描述了基本的系统服务。在shadow表中除描述符0之外,还有WIN32K.SYS初始化的描述符1,其描述了GDI和USER服务。对于GUI线程,在线程结构体的偏移0DCh处是shadow表的地址,对于其它线程,该处为主表的地址。如果线程请求WIN32K.SYS的服务,则它要成为GUI线程。在研究了服务表的结构以及描述符1的用途之后,可以看到Win32子系统与内核非常紧密的结合在一起。描述符1的特殊性在于其嵌入到了内核代码中。KeAddSystemServiceTable函数是未公开的函数,非常简单并可以在添加新服务的驱动程序中静态的调用。我们注意到,IIS使用了两个描述符。所以最好在第三个描述符中添加自己的服务。

Windows NT调用的特殊之处在于用户模式下的大量指针。几乎每一个内核函数都是以检查指针区域参数正确性这一繁琐工作开始的。因此所有的用户地址空间都与内核空间重合,并且,在用户模式下工作的同时,内核由于页保护被隔绝开,而在内核模式下,不正确的用户指针可以寻址到内核区域。如果看一下选择子10和23的界限,则可以看到它们是一样的(0xFFFFFFFF)。选择子23(用户模式的选择子)的界限应该等于内核空间起始地址减1(0x7FFFFFFF)。例如,在LINUX下就是这样的。如果试图强行在调试器中修改这个界限值,则Windows NT会出BSOD。为什么会这样?答案令人不可思议:在内核中执行线程时竟使用选择子23。一方面,这样是很方便——驱动使用用户指针就像使用普通的指针。而另一方面,这又会引发潜在的错误。我已经讲过,在LINUX下,用户和内核空间并不重合,在内核中使用用户指针时需调用copy_from_user()之类的函数(对于i386,这些函数仅仅是一些从不同段中进行拷贝的常规代码)。这种不方便性迫使内核程序控制并最小化了对用户指针的使用。

Windows NT的内核与用户重叠的空间导致了系统最初版本代码中的许多错误。这些错误都十分隐蔽——要知道Win32经常要用到服务,这需要向内核中传递正确的参数。


Windows 2K内核中系统调用接口的变化
===========================================================

// 这里的主要内容是我看到的一篇文章里的。:( 我并不想剽窃别人的著作,但是我实在是忘了这是谁的文章以及是在哪里看见的了。

Windows 2K的内核除了通过中断2Eh的系统调用接口之外,还可以通过SYSENTER/SYSEXIT指令转入内核模式。这些指令是Pentium  II+处理器里才有的。SYSENTER的处理程序位于内核中的KiSystemService里并调用KiFastCallEntry。KiFastCallEntry开始部分的样子如下:

   MOV    ESP, SS:[0xFFDFF040]
   MOV    ESP, [ESP+4]           ;set ring-0 stack
   PUSH   0x23                   ;模拟ring3堆栈
   PUSH   EDX                    ;指向ring3堆栈参数的指针
   SUB    DWORD PTR [ESP], 4     ;在ring3的堆栈中
   PUSHFD
   OR     DWORD PTR [ESP], 0x200 ;模拟ring3的标志寄存器
   PUSH   0x1B                   ;ring3的CS选择子
   PUSH   ECX                    ;ring3的EIP
   ;..fill in KeTrapFrame
   ;..后面部分同系统调用的处理相同

显然,对于与系统调用相同的部分,处理程序完全透明的实现了系统调用——上述的代码模拟了调用中断时的堆栈。除此之外,现在可以使用Fast System Call机制来进行系统调用。

   MOV    EAX, NtCallCode ; 系统调用号
   LEA    EDX, [ESP+4]    ; 堆栈中的参数
   LEA    ECX, SYSEXIT_POINT     ; 返回点
   SYSENTER
SYSEXIT_POINT:

所有这些都好象是从NTDLL.DLL通过中断2E调用的。其它的接口与此类似。系统调用最后部分形式如下:

   TEST   KeFeatureBits, 0x1000      ;支持 fast system call
   JZ     ReturnFromInterrupt        ;非 - iret
   TEST   DWORD PTR [ESP+4], 1       ;从ring3调用?
   JZ     ReturnFromInterrupt        ; 非 - iret
   TEST   DWORD PTR [ESP+8], 0x20000 ;从v86调用?
   JNZ    ReturnFromInterrupt        ; 是 - iret
   POP    EDX                        ;返回的eip
   ADD    ESP, 8                     ;回收模拟的中断堆栈
   POP    ECX                        ;ring3的esp 3
   STI
   SYSEXIT
  ReturnFromInterrupt:
   IRETD

如此——内核支持两种系统调用接口。但是NTDLL.DLL与Windows NT 4.0下的相同,包含着对系统调用的封装。这样,Windows NT就不能使用fast system call形式的接口。看来,下一版的NTDLL.DLL将包含两种接口。或者对PII之前和之后的处理器提供两种不同的NTDLL.DLL。

---------------------------------------------------------------------------
                          (c)Gloomy aka Peter Kosyh, Melancholy Coding'2001

                                                       http://gloomy.cjb.net
                                                       mailto:gl00my@mail.ru
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值