我先简单介绍一下Nt系列函数与Zw系列函数的区别 以ReadFile为例 ntdll.dll导出了ZwReadFile和NtReadFile 以下是反汇编代码
.text:7C92E27C ; __stdcall NtReadFile(x, x, x, x, x, x, x, x, x)
.text:7C92E27C public _NtReadFile@36
.text:7C92E27C _NtReadFile@36 proc near ; CODE XREF: RtlGetSetBootStatusData(x,x,x,x,x,x)+5Fp
.text:7C92E27C ; RtlGetSetBootStatusData(x,x,x,x,x,x):loc_7C95170Dp ...
.text:7C92E27C mov eax, 0B7h ; NtReadFile
.text:7C92E281 mov edx, 7FFE0300h
.text:7C92E286 call dword ptr [edx]
.text:7C92E288 retn 24h
.text:7C92E288 _NtReadFile@36 endp
.text:7C92E288
.text:7C92E288 ; ---------------------------------------------------------------------------
.text:7C92E28B db 6 dup(90h)
.text:7C92E291 ; Exported entry 274. NtReadFileScatter
.text:7C92E291 ; Exported entry 1083. ZwReadFileScatter
.text:7C92E291
我们可以看到 ZwReadFile和NtReadFile函数指向同一段代码....
也就是说在用户态 不管你调用ZwReadFile还是NtReadFile都是一样的 因为他们是同一个函数的两个不同名称而已.... 而且他们最终都会调用到ntoskrnl中的NtReadFile中去
而ntoskrnl.exe导出的ZwReadFile和NtReadFile却是不同的
.text:00406508 ; NTSTATUS __stdcall ZwReadFile(HANDLE FileHandle,HANDLE Event,PIO_APC_ROUTINE ApcRoutine,PVOID ApcContext,PIO_STATUS_BLOCK IoStatusBlock,PVOID Buffer,ULONG Length,PLARGE_INTEGER ByteOffset,PULONG Key)
.text:00406508 mov eax, 0B7h
.text:0040650D lea edx, [esp+FileHandle]
.text:00406511 pushf
.text:00406512 push 8
.text:00406514 call _KiSystemService
.text:00406519 retn 24h
.text:00406519 _ZwReadFile@36 endp
PAGE:004990D8 ; NTSTATUS __stdcall NtReadFile(HANDLE FileHandle,HANDLE Event,PIO_APC_ROUTINE ApcRoutine,PVOID ApcContext,PIO_STATUS_BLOCK IoStatusBlock,PVOID Buffer,ULONG Length,PLARGE_INTEGER ByteOffset,PULONG Key)
PAGE:004990D8 push 68h
PAGE:004990DA push offset stru_418748
PAGE:004990DF call __SEH_prolog
PAGE:004990E4 xor esi, esi
PAGE:004990E6 mov [ebp+var_20], esi
PAGE:004990E9 mov [ebp+var_34], esi
PAGE:004990EC mov [ebp+var_70], esi
PAGE:004990EF mov [ebp+var_6C], esi
PAGE:004990F2 mov eax, large fs:124h
PAGE:004990F8 mov [ebp+var_58], eax
PAGE:004990FB mov al, [eax+140h]
PAGE:00499101 mov [ebp+WaitMode], al
PAGE:00499104 push esi ; HandleInformation
PAGE:00499105 lea eax, [ebp+FileObject]
PAGE:00499108 push eax ; Object
PAGE:00499109 push dword ptr [ebp+WaitMode] ; AccessMode
PAGE:0049910C push _IoFileObjectType ; ObjectType
PAGE:00499112 push 1 ; DesiredAccess
PAGE:00499114 push [ebp+Handle] ; Handle
PAGE:00499117 call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x)
PAGE:0049911C cmp eax, esi
PAGE:0049911E jl loc_49928C
...
...
...
Ntoskrnl导出的NtReadFile是真正的执行函数 而ZwReadFile仍然是一个stub函数
内核态调用ZwReadFile 会将Previous Mode设置为Kernel Mode 然后再调用到NtReadFile中 而在内核态直接调用NtReadFile 不会改变Previous Mode . 而在NtReadFile中 会检测当前调用来自用户态还是内核态 如果是来自内核态 不会检测参数 而如果是来自用户态 就会做一系列的参数检测 而我们知道 内核组件可能运行在任意进程的上下文中 当它调用NtReadFile时 因为Previous Mode很可能是User Mode .... 而我们的参数请求的内核态的地址 这时通常就会产STATUS_ACCESS_VIOLATION
错误 ..
所以内核态一般用Zw*系列的函数
关于Nt*与Zw*更为详细的介绍请参考
Nt vs. Zw - Clearing Confusion On The Native API
那我们为什么还要在内核态直接调用Nt*函数呢?
大家都知道很多杀毒软件hook了ssdt 对系统进行监控.... 现在比如我们已经进入了Ring0(这个不在本文介绍的范围内) 然后想干一些猥琐的事情 那么怎样绕过监控呢 ? 一种方法先恢复被ssdt
然后再调用我们要用到的Zw*函数 ,不过这种方法不太好 不够隐藏 而且有很多监控程序 会定时检测ssdt表的
那么我们可不可以直接调用 Nt函数呢? 如果可以的话 那么监控对我们就失去作用了 .
直接调用Nt*函数要解决几个问题 第一个就是Nt*函数在内存中的地址 只有有了地址我们才能调用啊 而ssdt表已经被hook了 所以我们不能直接从ssdt中获得 而要从磁盘文件ntoskrnl.exe中得到它
第二个问题是系统会在Nt*函数中检查当前线程结构的PreviousMode 来确定调用是来自用户态还是内核态 如果是来自用户态 函数会检测参数地址是否小于_MmUserProbeAddress.如果不是 将会产生一个错误.. 所以我们的目标就是把PreviousMode改为Kernel Mode
下面看NtReadFile的代码
PAGE:004990D8 ; NTSTATUS __stdcall NtReadFile(HANDLE FileHandle,HANDLE Event,PIO_APC_ROUTINE ApcRoutine,PVOID ApcContext,PIO_STATUS_BLOCK IoStatusBlock,PVOID Buffer,ULONG Length,PLARGE_INTEGER ByteOffset,PULONG Key)
PAGE:004990D8 push 68h
PAGE:004990DA push offset stru_418748
PAGE:004990DF call __SEH_prolog
PAGE:004990E4 xor esi, esi
PAGE:004990E6 mov [ebp+var_20], esi
PAGE:004990E9 mov [ebp+var_34], esi
PAGE:004990EC mov [ebp+var_70], esi
PAGE:004990EF mov [ebp+var_6C], esi
PAGE:004990F2 mov eax, large fs:124h ;内核态fs指向KPCR
/* ;fs:120为KPRCB
lkd> dt _kprcb
nt!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD ;fs:124
+0x008 NextThread
*/
PAGE:004990F8 mov [ebp+var_58], eax
PAGE:004990FB mov al, [eax+140h]
/*
lkd> dt _kthread
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void
+0x01c StackLimit : Ptr32 Void
........
+0x140 PreviousMode : Char ;al =PreviousMode
......................................
*/
PAGE:00499101 mov [ebp+WaitMode], al
PAGE:00499104 push esi ; HandleInformation
PAGE:00499105 lea eax, [ebp+FileObject]
PAGE:00499108 push eax ; Object
PAGE:00499109 push dword ptr [ebp+WaitMode] ; AccessMode
PAGE:0049910C push _IoFileObjectType ; ObjectType
PAGE:00499112 push 1 ; DesiredAccess
PAGE:00499114 push [ebp+Handle] ; Handle
PAGE:00499117 call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x)
PAGE:0049911C cmp eax, esi
PAGE:0049911E jl loc_49928C
PAGE:00499124 mov ebx, [ebp+FileObject]
PAGE:00499127 push ebx ; FileObject
PAGE:00499128 call _IoGetRelatedDeviceObject@4 ; IoGetRelatedDeviceObject(x)
PAGE:0049912D mov [ebp+var_28], eax
PAGE:00499130 cmp [ebp+WaitMode], 0 ;如果调用来处内核态
;则不作后面的检查
PAGE:00499134 jz loc_4A9553
PAGE:0049913A mov [ebp-4], esi
PAGE:0049913D mov edi, [ebp+IoStatusBlock]
PAGE:00499140 mov eax, _MmUserProbeAddress
PAGE:00499145 cmp edi, eax
PAGE:00499147 jnb loc_50EBDC
PAGE:0049914D
PAGE:0049914D loc_49914D: ; CODE XREF: NtReadFile(x,x,x,x,x,x,x,x,x)+75B06j
PAGE:0049914D mov eax, [edi]
PAGE:0049914F mov [edi], eax
PAGE:00499151 mov eax, [edi+4]
PAGE:00499154 mov [edi+4], eax
..............
我们只要把kthread中的PreviousMode值设为0就可以了
而PreviousMode在kthread中的偏移在各个系统版本中是不一样的 我们可以硬编码之
也可以在NtAdjustPrivilegesToken函数中得到这个偏移
PAGE:004B7BB2 loc_4B7BB2: ; CODE XREF: NtAdjustPrivilegesToken(x,x,x,x,x,x)+23j
PAGE:004B7BB2 mov eax, large fs:124h
PAGE:004B7BB8 mov al, [eax+140h] ;这个140h就是偏移量
PAGE:004B7BBE mov [ebp+AccessMode], al
PAGE:004B7BC1 test al, al
PAGE:004B7BC3 jz loc_4B8F67
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课