首页 > 代码库 > 从ZwProtectVirtualMemory到NtProtectVirtualMemory-内核函数调用

从ZwProtectVirtualMemory到NtProtectVirtualMemory-内核函数调用

0x01 前言

  我们知道R3层中,Zw系列函数和Nt系列函数函数是一样的,但是在内核Zw系列函数调用了Nt系列函数,但是为什么要在内核设置一个Zw系列函数而不是直接调用Nt函数呢?Zw系列函数又是怎么调用Nt系列函数的呢?我们利用IDA分析NtosKrnl.exe文件。

 

0x02 ZwProtectVirtualMemory

  我们先看看ZwProtectVirtualMemory的实现

.text:00406170 ; NTSTATUS __stdcall ZwProtectVirtualMemory(HANDLE ProcessHandle, PVOID *BaseAddress, PULONG ProtectSize, ULONG NewProtect, PULONG OldProtect).text:00406170 _ZwProtectVirtualMemory@20 proc near    ; CODE XREF: RtlpCreateStack(x,x,x,x,x)+FAp.text:00406170.text:00406170 ProcessHandle   = dword ptr  4.text:00406170 BaseAddress     = dword ptr  8.text:00406170 ProtectSize     = dword ptr  0Ch.text:00406170 NewProtect      = dword ptr  10h.text:00406170 OldProtect      = dword ptr  14h.text:00406170.text:00406170                 mov     eax, 89h  //Nt函数的系统调用号.text:00406175                 lea     edx, [esp+ProcessHandle] //使用EDX指向堆栈上的参数块.text:00406179                 pushf        //EFLAGS.text:0040617A                 push    8    //CS   KGDT_R0_CODE.text:0040617C                 call    _KiSystemService.text:00406181                 retn    14h    //5个参数,20字节.text:00406181 _ZwProtectVirtualMemory@20 endp

  这里89h为NtProtectVirtualMemory函数在SSDT函数中的调用号,CS寄存器,最后位为0表示当前处于内核态,然后调用KiSystemService函数

 

0x03 KiSystemService

  我们接着看KiSystemService的函数实现

.text:00407631 _KiSystemService proc near              ; CODE XREF: ZwAcceptConnectPort(x,x,x,x,x,x)+Cp.text:00407631                                         ; ZwAccessCheck(x,x,x,x,x,x,x,x)+Cp ....text:00407631.text:00407631 arg_0           = dword ptr  4.text:00407631.text:00407631                 push    0.text:00407633                 push    ebp.text:00407634                 push    ebx.text:00407635                 push    esi.text:00407636                 push    edi.text:00407637                 push    fs              ; 保存用户空间的fs.text:00407639                 mov     ebx, 30h        ; KGDT_R0_PCR.text:0040763E                 mov     fs, ebx         ; 使FS段的起点与KPCR数据结构对齐.text:00407640                 push    dword ptr ds:0FFDFF000h.text:00407646                 mov     dword ptr ds:0FFDFF000h, 0FFFFFFFFh.text:00407650                 mov     esi, ds:0FFDFF124h ; #define KPCR_CURRENT_THREAD 0x124.text:00407650                                         ; 指向当前cpu正在运行的线程.text:00407650                                         ; FS:0x124.text:00407650                                         ; PCR的大小只有0x54,这里偏移到了KPRCB中的CurrentThread.text:00407656                 push    dword ptr [esi+140h].text:0040765C                 sub     esp, 48h.text:0040765F                 mov     ebx, [esp+68h+arg_0] ; 系统调用前夕的CS映像.text:00407663                 and     ebx, 1          ; 0环的最低位为0,3环的最低位为1.text:00407666                 mov     [esi+140h], bl  ; 新的"先前模式"  [esi+KTHREAD_PREVIOUS_MODE].text:0040766C                 mov     ebp, esp.text:0040766E                 mov     ebx, [esi+134h] ; KTHREAD结构中的指针TrapFrame [esi+KTHREAD_TRAP_FRAME].text:00407674                 mov     [ebp+3Ch], ebx  ; 暂时保存在这里 [ebp+KTRAP_FRAME_EDX].text:00407677                 mov     [esi+134h], ebp ; 新的TrapFrame,指向堆栈上的框架 [esi+KTHREAD_TRAP_FRAME]....text:0040769E                 sti.text:0040769F                 jmp     loc_407781.text:0040769F _KiSystemService endp

  这里首先要在系统态堆栈上构建一个系统调用"框架Frame",或称为"自陷框架",其作用主要是用来保存发生自陷时CPU中各寄存器的"现场",或者说"上下文",以备返回用户空间时予以恢复。
  Windows内核有个特殊的基本要求,只要CPU在内核运行,FS寄存器就指向一个KPCR的数据结构,FS的值为0x30,其0-1位为0,表示0环,第2位为0,表示GDT表,为1则表示LDT表,3-15位为6,表示在GDT的下标为6的表项中的地址即为KPCR的地址。KPCR是处理器控制块,在单处理器中只有一个KPCR,在多CPU的系统中,每个CPU都有自己的KPCR结构。
  CPU从用户空间进入系统空间时会将当时寄存器CS的内容压入系统态堆栈,CS的最低位就可以说明当时运行于何种模式的标志位。这里取出CS最低位保存在ETHREAD的PREVIOUS_MODE上。
  更新ETHREAD中的TrapFrame框架,保存旧的框架。

 

0x04 KiFastCallEntry

  KiSystemService中的jmp loc_407781跳转到KiFastCallEntry函数中,代码如下:

.text:004076F0 _KiFastCallEntry proc near              ; DATA XREF: _KiTrap01+6Fo.text:004076F0                                         ; KiLoadFastSyscallMachineSpecificRegisters(x)+24o.text:004076F0.text:004076F0 var_B           = byte ptr -0Bh.text:004076F0.text:004076F0 ; FUNCTION CHUNK AT .text:004076C8 SIZE 00000023 BYTES.text:004076F0 ; FUNCTION CHUNK AT .text:00407990 SIZE 00000014 BYTES.text:004076F0.text:004076F0                 mov     ecx, 23h.text:004076F5                 push    30h.text:004076F7                 pop     fs.text:004076F9                 mov     ds, ecx....text:00407781 loc_407781:                             ; CODE XREF: _KiBBTUnexpectedRange+18j.text:00407781                                         ; _KiSystemService+6Ej.text:00407781                 mov     edi, eax        ; 系统调用号.text:00407783                 shr     edi, 8          ; NtProtectVirtualMemory 89h = 10001001.text:00407783                                         ; shr右移8位为0.text:00407786                 and     edi, 30h.text:00407789                 mov     ecx, edi        ; 将ECX变成0x00(SSDT)或者0x10(ShadowSSDT).text:0040778B                 add     edi, [esi+0E0h] ; 本线程的系统调用表.text:0040778B                                         ; EDI指向描述块0或描述块1.text:00407791                 mov     ebx, eax        ; 将eax中的索引值,赋值给ebx.text:00407793                 and     eax, 0FFFh      ; SERVICE_NUMBER_MASK定义为0xFFF.text:00407798                 cmp     eax, [edi+8]    ; 检查系统调用号是否越界.text:00407798                                         ; SERVICE_DESCRIPTOR_LIMIT定义为8.text:0040779B                 jnb     _KiBBTUnexpectedRange ; 系统调用号越界,有可能是大于0x1000

  这里eax保存着系统调用号,在KTHREAD中有一个指针ServiceTable,如果是gui线程则指向KeServiceDescriptorTableShadow[],如果不是则指向KeServiceDescriptor[]。这里检查了系统调用号是否越界。多数情况下不会越界,我们继续往下看:

.text:004077A1                 cmp     ecx, 10h        ; 检查ECX的内容是否0x10.text:004077A4                 jnz     short NotWin32K...//使用Win32k系统调用表.text:004077C0 NotWin32K:                              ; CODE XREF: _KiFastCallEntry+B4j.text:004077C0                                         ; _KiFastCallEntry+C4j.text:004077C0                 inc     dword ptr ds:0FFDFF638h.text:004077C6                 mov     esi, edx        ; 使ESI指向用户空间堆栈上的参数块.text:004077C8                 mov     ebx, [edi+0Ch]  ; [edi+SERVICE_DESCRIPTOR_NUMBER].text:004077CB                 xor     ecx, ecx.text:004077CD                 mov     cl, [eax+ebx]   ; 寄存器ECX.text:004077D0                 mov     edi, [edi]      ; EDI指向具体的系统调用表.text:004077D0                                         ; [edi+SERVICE_DESCRIPTOR_BASE].text:004077D2                 mov     ebx, [edi+eax*4] ; 函数指针.text:004077D5                 sub     esp, ecx        ; 系统堆栈上留出空间.text:004077D7                 shr     ecx, 2          ; 右移2位.text:004077DA                 mov     edi, esp        ; 目标在系统空间堆栈上.text:004077DC                 cmp     esi, ds:_MmUserProbeAddress ; 参数块的位置不得高于MmSystemRangeStart-0x10000.text:004077E2                 jnb     AccessViolation.text:004077E8.text:004077E8 loc_4077E8:                             ; CODE XREF: _KiFastCallEntry+2A4j.text:004077E8                                         ; DATA XREF: _KiTrap0E+106o.text:004077E8                 rep movsd               ; 复制参数,以ESI为源,EDI为目标,ECX为循环次数.text:004077EA                 call    ebx             ; 调用目标函数 

  这里将ECX与0x10比较,如果不是0x10则为基本调用表(SSDT函数),转到NotWin32K处。这里ecx的cl保存着KSERVICE_TABLE_DESCRIPTOR结构体中的Number,将cl右移2位就是参数的个数,后面重复执行的movsd的次数就是参数的个数,不过复制之前要调整堆栈指针,将ESP与移位前的ECX相减,在系统空间堆栈上 空出相应的字节数。注意movsd指令以ESI所指处为源,以EDI所指处为目标,另一方面,指令获得函数的指针赋值为ebx,最后call ebx实现了对目标函数的调用。
  一些安全软件对KiFastCallEntry通过Hook实现过滤SSDT框架的时候,通常是在ebx完成赋值之后,在call ebx之前,替换这中间的地方,进入fake1函数,将保存好的参数push,比如edi保存的SSDT表地址,ebx保存函数地址,eax保存调用号,ecx保存参数个数,在这中间hook,可以直接利用系统初始化好的寄存器,然后调用filter函数,通过寄存器的值,过滤指定的SSDT函数,替换ebx的值,然后继续执行KiFastCallEntry中的call ebx,这样就可以过滤整个SSDT系统调用了。

  当执行完成call ebx,从目标函数返回时我们继续看下面的指令:

.text:004077EC loc_4077EC:                             ; CODE XREF: _KiFastCallEntry+2AFj.text:004077EC                                         ; DATA XREF: _KiTrap0E+126o ....text:004077EC                 mov     esp, ebp        ; 回到自陷(系统调用框架).text:004077EE.text:004077EE KeReturnFromSystemCall:                 ; CODE XREF: _KiBBTUnexpectedRange+38j.text:004077EE                                         ; _KiBBTUnexpectedRange+43j.text:004077EE                 mov     ecx, ds:0FFDFF124h ; 使ECX只想当前线程的KTHREAD.text:004077F4                 mov     edx, [ebp+3Ch]  ; 从堆栈中取出保存着的框架指针.text:004077F4                                         ; [ebp+KTRAP_FRAME_EDX].text:004077F7                 mov     [ecx+134h], edx ; 恢复KTHREAD结构中的框架指针.text:004077F7 _KiFastCallEntry endp ; sp-analysis failed ; [exc+KTHREAD_TRAP_FRAME]

  首先将堆栈指针恢复指向系统调用框架即自陷框架的底部,因为这些参数已经失去意义,然后把原先保存在堆栈上的先前自陷框架指针恢复到当前线程的控制块中。

 

0x05 KiServiceExit

  然后继续执行KiServiceExit函数

.text:004077FD _KiServiceExit  proc near               ; CODE XREF: _KiSetLowWaitHighThread+7Cj.text:004077FD                                         ; NtContinue(x,x)+42j ....text:004077FD.text:004077FD arg_C           = dword ptr  10h.text:004077FD arg_10          = dword ptr  14h.text:004077FD arg_40          = dword ptr  44h.text:004077FD arg_44          = dword ptr  48h.text:004077FD arg_48          = dword ptr  4Ch.text:004077FD arg_60          = dword ptr  64h.text:004077FD arg_64          = dword ptr  68h.text:004077FD arg_68          = dword ptr  6Ch.text:004077FD arg_6C          = dword ptr  70h.text:004077FD.text:004077FD ; FUNCTION CHUNK AT .text:00407908 SIZE 00000088 BYTES.text:004077FD.text:004077FD                 cli                     ; 关中断.text:004077FE                 test    dword ptr [ebp+70h], 20000h.text:00407805                 jnz     short CHECK_FOR_APC_DELIVER.text:00407807                 test    byte ptr [ebp+6Ch], 1.text:0040780B                 jz      short loc_407864.text:0040780D.text:0040780D CHECK_FOR_APC_DELIVER:                  ; CODE XREF: _KiServiceExit+8j.text:0040780D                                         ; _KiServiceExit+63j.text:0040780D                 mov     ebx, ds:0FFDFF124h.text:00407813                 mov     byte ptr [ebx+2Eh], 0.text:00407817                 cmp     byte ptr [ebx+4Ah], 0.text:0040781B                 jz      short loc_407864 ; 如果先前模式是内核模式,就往前跳转到下面,不递交APC请求.text:0040781D                 mov     ebx, ebp.text:0040781F                 mov     [ebx+44h], eax.text:00407822                 mov     dword ptr [ebx+50h], 3Bh.text:00407829                 mov     dword ptr [ebx+38h], 23h.text:00407830                 mov     dword ptr [ebx+34h], 23h.text:00407837                 mov     dword ptr [ebx+30h], 0.text:0040783E                 mov     ecx, 1          ; APC_LEVEL,参数.text:00407843                 call    ds:__imp_@KfRaiseIrql@4 ; 这是快速调用模式的函数,通过寄存器传递参数.text:00407849                 push    eax             ; 将返回值(老的运行级别)压入堆栈.text:0040784A                 sti.text:0040784B                 push    ebx.text:0040784C                 push    0.text:0040784E                 push    1.text:00407850                 call    _KiDeliverApc@12 ; 执行内核APC,并未用户空间APC的执行进行准备.text:00407855                 pop     ecx             ; 从堆栈恢复老的运行级别.text:00407856                 call    ds:__imp_@KfLowerIrql@4 ; 恢复原来的运行级别,在这里应该是PASSIVE_LEVEL.text:0040785C                 mov     eax, [ebx+44h].text:0040785F                 cli.text:00407860                 jmp     short CHECK_FOR_APC_DELIVER.text:00407860 ; ---------------------------------------------------------------------------.text:00407862                 align 4.text:00407864.text:00407864 loc_407864:                             ; CODE XREF: _KiServiceExit+Ej.text:00407864                                         ; _KiServiceExit+1Ej...

在KiServiceExit执行的时候,首先关闭中断,然后检查是否有APC请求,如果有就通过KiDeliverApc递交APC请求(插入线程apc队列)。

最后会通过TrapFrame返回r3或者返回内核调用Zw函数的地方。

 

0x06 总结

一般而言在内核里面不能直接调用Nt函数,一方面是内核并不导出,只能通过ssdt表获得,另一方面,这些函数往往要求在系统空间堆栈上有个属于本次调用的自陷框架,而Zw->KiSystemService->KiFastCallEntry->Nt->KiSystemExit的流程中就有这样一个框架。

从ZwProtectVirtualMemory到NtProtectVirtualMemory-内核函数调用