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 [email protected]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 [email protected]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:[email protected]@4 ; 这是快速调用模式的函数,通过寄存器传递参数 .text:00407849 push eax ; 将返回值(老的运行级别)压入堆栈 .text:0040784A sti .text:0040784B push ebx .text:0040784C push 0 .text:0040784E push 1 .text:00407850 call [email protected]12 ; 执行内核APC,并未用户空间APC的执行进行准备 .text:00407855 pop ecx ; 从堆栈恢复老的运行级别 .text:00407856 call ds:[email protected]@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的流程中就有这样一个框架。