Zw函数与Nt函数的分别与联系
in ring3:
lkd> ? ntdll!ZwOpenProcess
Evaluate expression: 2089999739 = 7c92dd7b
lkd> ?ntdll!NtOpenProcess
Evaluate expression: 2089999739 = 7c92dd7b
可以看到,在ntdll中,ZwOpenProcess和NtOpenProcess其实是同一个函数,只不过拥有两个名称而已。
也就是说,在ring3环境中,Zw***系列函数和Nt***系列函数无区别。
in ring0:
lkd> u nt!ZwOpenProcess
nt!ZwOpenProcess:
804de044 b87a000000 mov eax,7Ah
804de049 8d542404 lea edx,[esp+4]
804de04d 9c pushfd
804de04e 6a08 push 8
804de050 e8dc150000 call nt!KiSystemService (804df631)
804de055 c21000 ret 10h
lkd> u nt!NtOpenProcess
nt!NtOpenProcess:
80573d06 68c4000000 push 0C4h
80573d0b 6810b44e80 push offset nt!ObWatchHandles+0x25c (804eb410)
80573d10 e826f7f6ff call nt!_SEH_prolog (804e343b)
80573d15 33f6 xor esi,esi
80573d17 8975d4 mov dword ptr [ebp-2Ch],esi
80573d1a 33c0 xor eax,eax
80573d1c 8d7dd8 lea edi,[ebp-28h]
80573d1f ab stos dword ptr es:[edi]
可以看得出,ZwOpenProcess函数很短,首先把0x7a(NtOpenProcess的服务号)存入eax,然后做一些保存现场的工作即KiSystemService——这个函数根据eax中的service id在SSDT中查找相应的系统服务,然后调用之。
而NtOpenProcess函数很长(反汇编结果只是一部分),事实上,NtOpenProcess便是真正执行打开进程操作的函数(在r0中通常称为服务,或例程),所以,若在驱动中直接调用Nt系列函数,是不会经过SSDT的,也就不会被SSDT HOOK所拦截。
简单总结如下:
r3下无论如何调用,均无法绕过SSDT HOOK,r0下调用Nt***可以绕过SSDT HOOK。
这里先说明下2000和XP系统用户模式到内核模式是通过软中断的方式进入的。2000下通过"int 2e",而XP下通过"sysenter"进入。
这里用WINDBG工具来说明区别:
1.在用户模式下ntdll.dll的Nt和Zw的区别
0:005> u ntdll!ZwCreateFile
ntdll!ZwCreateFile:
7c92d0ae b825000000 mov eax,25h
7c92d0b3 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d0b8 ff12 call dword ptr [edx]
7c92d0ba c22c00 ret 2Ch
0:005> u ntdll!NtCreateFile
ntdll!ZwCreateFile:
7c92d0ae b825000000 mov eax,25h
7c92d0b3 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d0b8 ff12 call dword ptr [edx]
7c92d0ba c22c00 ret 2Ch
函数先将0x25号放入eax,再将函数地址的指针放入edx,可以查看函数地址在7FFE03000为7C92E510
7FFE0300 : 10 E5 92 7C
进入7C92E510可以发现调用了sysenter,从用户态进入内核态。
7C92E510 > 8BD4 mov edx, esp
7C92E512 0F34 sysenter
纵观ntdll.dll里的Nt和Zw发现就是同一个函数,也就是ntdll.dll的Nt和Zw其实是一个函数的两个别名而已。
2.进入内核态查看:
lkd> u nt!ZwCreateFile
nt!ZwCreateFile:
80501020 b825000000 mov eax,25h
80501025 8d542404 lea edx,[esp+4]
80501029 9c pushfd
8050102a 6a08 push 8
8050102c e860140400 call nt!KiSystemService (80542491)
80501031 c22c00 ret 2Ch
lkd> u nt!NtCreateFile
nt!NtCreateFile:
8057a084 8bff mov edi,edi
8057a086 55 push ebp
8057a087 8bec mov ebp,esp
8057a089 33c0 xor eax,eax
8057a08b 50 push eax
8057a08c 50 push eax
8057a08d 50 push eax
8057a08e ff7530 push dword ptr [ebp+30h]
lkd> u
nt!NtCreateFile+0xd:
8057a091 ff752c push dword ptr [ebp+2Ch]
8057a094 ff7528 push dword ptr [ebp+28h]
8057a097 ff7524 push dword ptr [ebp+24h]
8057a09a ff7520 push dword ptr [ebp+20h]
8057a09d ff751c push dword ptr [ebp+1Ch]
8057a0a0 ff7518 push dword ptr [ebp+18h]
8057a0a3 ff7514 push dword ptr [ebp+14h]
8057a0a6 ff7510 push dword ptr [ebp+10h]
lkd> u
nt!NtCreateFile+0x25:
8057a0a9 ff750c push dword ptr [ebp+0Ch]
8057a0ac ff7508 push dword ptr [ebp+8]
8057a0af e868d8ffff call nt!IoCreateFile (8057791c)
8057a0b4 5d pop ebp
8057a0b5 c22c00 ret 2Ch
可以发现Zw内核态下调用了nt!KiSystemService (80542491) 调用SSDT
而Nt内核态下直接调用了nt!IoCreateFile (8057791c) 此函数直接实现了功能
所以不管是否在用户或者内核态Zw函数都是要经过SSDT 而Nt用户态经过,内核态就可以绕过SSDT
附:
操作系统根据是否是多处理器平台和是否支持PAE(Physical Address Extension)来选择合适的系统:
ntoskrnl.exe
单x86处理器,使用不超过4GB的物理内存。
ntkrnlpa.exe
单x86处理器,支持PAE。
ntkrnlmp.exe
多处理器,使用不超过4GB的物理内存。
ntkrpamp.exe
多处理器,支持PAE。
以NtOpenProcess和ZwOpenProcess为例,结合Windbg的lkd调试来说明
1、Q:ntdll.dll中的Nt*和Zw*区别?
lkd> u ntdll!zwopenprocess l4
ntdll!ZwOpenProcess:
7c92d5fe b87a000000 mov eax,7Ah //函数服务号
7c92d603 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300) //函数地址
7c92d608 ff12 call dword ptr [edx]
7c92d60a c21000 ret 10h
lkd> u ntdll!ntopenprocess l4
ntdll!ZwOpenProcess:
7c92d5fe b87a000000 mov eax,7Ah
7c92d603 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d608 ff12 call dword ptr [edx]
7c92d60a c21000 ret 10h
A:从反汇编代码来看,两者别无区别,都是通过SSDT服务表,通过系统服务调度程序KiSystemService调用ntoskrnl.exe中的中断处理程序Nt*函数。
2、Q:ntoskrnl.exe中的Nt*和Zw*有什么区别?
lkd> u nt!zwopenprocess l6
nt!ZwOpenProcess:
804e4c0a b87a000000 mov eax,7Ah
804e4c0f 8d542404 lea edx,[esp+4]
804e4c13 9c pushfd
804e4c14 6a08 push 8
804e4c16 e8b69bffff call nt!KiSystemService (804de7d1)
804e4c1b c21000 ret 10h
lkd> u nt!ntopenprocess l20
nt!NtOpenProcess:
80582702 68c4000000 push 0C4h
80582707 68e8524f80 push offset nt!ObWatchHandles+0x25c (804f52e8)
8058270c e87217f6ff call nt!_SEH_prolog (804e3e83)
80582711 33f6 xor esi,esi
80582713 8975d4 mov dword ptr [ebp-2Ch],esi
80582716 33c0 xor eax,eax
80582718 8d7dd8 lea edi,[ebp-28h]
8058271b ab stos dword ptr es:[edi]
8058271c 64a124010000 mov eax,dword ptr fs:[00000124h]
80582722 8a8040010000 mov al,byte ptr [eax+140h]
80582728 8845cc mov byte ptr [ebp-34h],al
8058272b 84c0 test al,al
8058272d 0f840e7b0100 je nt!NtOpenProcess+0xc0 (8059a241)
80582733 8975fc mov dword ptr [ebp-4],esi
80582736 a1d48e5680 mov eax,dword ptr [nt!MmUserProbeAddress (80568ed4)]
8058273b 8b4d08 mov ecx,dword ptr [ebp+8]
8058273e 3bc8 cmp ecx,eax
80582740 0f8315170800 jae nt!NtOpenProcess+0x40 (80603e5b)
80582746 8b01 mov eax,dword ptr [ecx]
80582748 8901 mov dword ptr [ecx],eax
8058274a 8b5d10 mov ebx,dword ptr [ebp+10h]
8058274d f6c303 test bl,3
80582750 0f850c170800 jne nt!NtOpenProcess+0x4e (80603e62)
80582756 a1d48e5680 mov eax,dword ptr [nt!MmUserProbeAddress (80568ed4)]
8058275b 3bd8 cmp ebx,eax
8058275d 0f8309170800 jae nt!NtOpenProcess+0x5c (80603e6c)
80582763 397308 cmp dword ptr [ebx+8],esi
80582766 0f9545e6 setne byte ptr [ebp-1Ah]
8058276a 8b4b0c mov ecx,dword ptr [ebx+0Ch]
8058276d 894dc8 mov dword ptr [ebp-38h],ecx
80582770 8b4d14 mov ecx,dword ptr [ebp+14h]
80582773 3bce cmp ecx,esi
A:从反汇编代码可知NT*是实现函数具体过程,而ZW*函数是在ring 0下通过SSDT服务表,KiSystemService调用ntoskrnl.exe中的中断处理程序Nt*函数。
3、Q:ntdll.dll中的ZW*和ntoskrnl.exe中的ZW*有什么区别呢?
A:
从发汇编代码来看:ntdll.dll:传入服务号到eax——>调用SharedUserData!SystemCallStub (7ffe0300)函数->KiSystemService---->Nt*(ntoskrnl)
ntoskrnl.exe:传入服务号eax——>KiSystemService--->Nt*(ntoshrnl)
S:从用户模式调用Nt和Zw API,如NtReadFile和ZwReadFile,连接ntdll.lib:
二者没有任何区别,通过设置系统服务表中的索引和在堆栈中设置参数,经由SYSENTER指令进入内核态(而不是象w2k中通过int 0x2e中断),并最终由KiSystemService跳转到KiServiceTable对应的系统服务例程中。由于是从用户模式进入内核模式,因此代码会严格检查用户空间传入的参数。
从内核模式调用Nt和Zw API,连接nooskrnl.lib:
Nt系列API将直接调用对应的函数代码,而Zw系列API则通过KiSystemService,最终跳转到对应的函数代码。
重要的是两种不同的调用对内核中previous mode的改变,如果是从用户模式调用Native API则previous mode是用户态,如果从内核模式调用Native API则previous mode是内核态。previous为用户态时Native API将对传递的参数进行严格的检查,而为内核态时则不会。
调用用户模式Nt API时不会改变previous mode的状态,调用Zw API时会将previous mode改为内核态,因此在进行Kernel Mode Driver开发时可以使用Zw系列API可以避免额外的参数列表检查,提高效率。