Gloomy对Windows内核的分析(系统调用接口)

来源:互联网 发布:淘宝订单打印电子版 编辑:程序博客网 时间:2024/05/02 08:01

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

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

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

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寄存器为堆栈中参数的地址。现在我们来看NTO
SKRNL.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堆栈中拷贝参数起始于标号KiSystemServi
ceCopyArguments)。如果没有错误,则按照预先从服务地址表中选出的地址进行CALL EBX。
接着,注意到两个有趣的地方。第一个是,所有的核心线程都能够取得服务地址表的地址(
参照8013CBF0和8013CC33处的代码)。第二个有趣的地方是,服务表可以有四个(对于每个
线程来说)。标号_KiSystemServiceRepeat处代码的调用依赖于位0x3000的值,从四个描述
服务表地址的描述符中选择一个。描述符占据16字节并连续排列。这四个描述符总称服务描
述符表。对于每一个线程,在内核线程结构体中都有其自己的指向服务描述符表的指针。这
个指针可以从线程结构体的0DCh偏移处取得(Windows NT 4.0下)。线程结构体的地址可以
在内核模式下从PCRB的偏移124h处取得。(MOV EAX, FS:[124h])。每个线程都有自己指向
服务描述符表的指针,实际上,所有线程中的指针都指向两个描述符表中的一个。这两个表
位于NTOSKERNEL.EXE,分别叫做KeServiceDescriptorTable和KeServiceDescriptorTableSh
adow。表中的描述符的格式如下:

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

在系统初始化(KiInitSystem)时,表KeServiceDescriptorTable和KeServiceDescriptorS
hadow的描述符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初始化的时候会调用内核函数KeAddSys
temServiceTable。其伪代码如下:

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。此时,其余的描述符并未使用。我们知道,为了提高效率,在W
indows 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的界限,则可以看到它们是一样的(0xFF
FFFFFF)。选择子23(用户模式的选择子)的界限应该等于内核空间起始地址减1(0x7FFFF
FFF)。例如,在LINUX下就是这样的。如果试图强行在调试器中修改这个界限值,则Window
s 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
                                                                    
                                                                     董岩 译
                                                http://greatdong.blog.edu.cn

 

 

原创粉丝点击