以一个demo测试为例,本测试查看OpenProcess
在R3-R0
下的调用。
// test.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
int main(int argc, char* argv[])
{
// __asm int 3
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS,FALSE,2548);
printf("handle=%08x",handle);
return 0;
}
载入OD具体分析调用。
ring3 application space
00401028 |. 8BF4 mov esi, esp
0040102A |. 68 F4090000 push 0x9F4 ; /ProcessId = 9F4
0040102F |. 6A 00 push 0x0 ; |Inheritable = FALSE
00401031 |. 68 FF0F1F00 push 0x1F0FFF ; |Access = PROCESS_ALL_ACCESS
00401036 |. FF15 4CA14200 call dword ptr ds:[<&KERNEL32.OpenProcess>] ; \OpenProcess
此时由测试进程模块 进入到kernel32
模块
--kernel32.dll (OpenProcess)
7C830A0B 8975 E8 mov dword ptr ss:[ebp-0x18], esi
7C830A0E 8975 F0 mov dword ptr ss:[ebp-0x10], esi
7C830A11 8975 F4 mov dword ptr ss:[ebp-0xC], esi
7C830A14 FF15 1C11807C call dword ptr ds:[<&ntdll.NtOpenProcess>]; ntdll.ZwOpenProcess
----ntdll.dll(NtOpenProcess (or ZWOpenProcess))
7C92D5E0 > B8 7A000000 mov eax, 0x7A
7C92D5E5 BA 0003FE7F mov edx, 0x7FFE0300
7C92D5EA FF12 call dword ptr ds:[edx] ; ntdll.KiFastSystemCall
7C92E4F0 > 8BD4 mov edx, esp
7C92E4F2 0F34 sysenter ; ring3->ring0
7C92E4F4 > C3 retn
在R3下的函数调用过程:本地模块->kernel32(openprocess)->ntdll(zwOpenProcess)->ntdll(sysenter)
。
ring3级别的函数调用栈回朔
地址 堆栈 函数过程 / 参数 调用来自 结构
........ ........ sysenter ntdll.KiFastSystemCall+0xC(7C92E4F2) ...
0012FEE0 7C92D5EC 包含ntdll.KiFastSystemCall ntdll.7C92D5EA 0012FF1C
0012FEE4 7C830A1A ntdll.ZwOpenProcess kernel32.7C830A14 0012FF1C
0012FF20 0040103C kernel32.OpenProcess test.00401036 0012FF1C
0012FF24 001F0FFF Access = PROCESS_ALL_ACCESS
0012FF28 00000000 Inheritable = FALSE
0012FF2C 000009F4 ProcessId = 9F4
0012FF84 00401239 test.00401005 test.<模块入口点>+0E4 0012FF80
也可以用windbg
去查看,更简单,但是调试就不能是local kernel
模式了,往windbg
中拖拽一个可执行文件后,查看结构
0:000> u kernel32!openprocess l30 //l30表示取30条指令,只求多,不求少
kernel32!OpenProcess:
7c8309d1 8bff mov edi,edi
7c8309d3 55 push ebp
7c8309d4 8bec mov ebp,esp
7c8309d6 83ec20 sub esp,20h
7c8309d9 8b4510 mov eax,dword ptr [ebp+10h]
7c8309dc 8945f8 mov dword ptr [ebp-8],eax
7c8309df 8b450c mov eax,dword ptr [ebp+0Ch]
7c8309e2 56 push esi
7c8309e3 33f6 xor esi,esi
7c8309e5 f7d8 neg eax
7c8309e7 1bc0 sbb eax,eax
7c8309e9 83e002 and eax,2
7c8309ec 8945ec mov dword ptr [ebp-14h],eax
7c8309ef 8d45f8 lea eax,[ebp-8]
7c8309f2 50 push eax
7c8309f3 8d45e0 lea eax,[ebp-20h]
7c8309f6 50 push eax
7c8309f7 ff7508 push dword ptr [ebp+8]
7c8309fa 8d4510 lea eax,[ebp+10h]
7c8309fd 50 push eax
7c8309fe 8975fc mov dword ptr [ebp-4],esi
7c830a01 c745e018000000 mov dword ptr [ebp-20h],18h
7c830a08 8975e4 mov dword ptr [ebp-1Ch],esi
7c830a0b 8975e8 mov dword ptr [ebp-18h],esi
7c830a0e 8975f0 mov dword ptr [ebp-10h],esi
7c830a11 8975f4 mov dword ptr [ebp-0Ch],esi
7c830a14 ff151c11807c call dword ptr [kernel32!_imp__NtOpenProcess (7c80111c)]
7c830a1a 3bc6 cmp eax,esi
7c830a1c 5e pop esi
7c830a1d 0f8cdf710000 jl kernel32!OpenProcess+0x53 (7c837c02)
7c830a23 8b4510 mov eax,dword ptr [ebp+10h]
7c830a26 c9 leave
7c830a27 c20c00 ret 0Ch
可以看到,从导入表中调用了NtOpenProcess
。
7c830a14 ff151c11807c call dword ptr [kernel32!_imp__NtOpenProcess (7c80111c)]
很遗憾,这里没有告诉NtOpenProcess
属于哪个模块(其实是ntdll
模块),没关系,如果符号唯一的话,可以windbg可以识别出来
再次跟进NtOpenProcess
:
0:000> u ntopenprocess l4
ntdll!ZwOpenProcess:
7c92d5e0 b87a000000 mov eax,7Ah
7c92d5e5 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d5ea ff12 call dword ptr [edx]
7c92d5ec c21000 ret 10h
这里告诉我们是ntdll
模块。同时可以注意到ntdll下NtOpenProcess 和ZwOpenProcess
是同一个函数。当然,不确定的话可以尝试一下:
0:000> u zwopenprocess l4
ntdll!ZwOpenProcess:
7c92d5e0 b87a000000 mov eax,7Ah
7c92d5e5 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d5ea ff12 call dword ptr [edx]
7c92d5ec c21000 ret 10h
可以看到,它们两个是同一个函数。
跟进 [edx]=[7ffe0300]=?
0:000> dd 7ffe0300
7ffe0300 7c92e4f0 7c92e4f4 00000000 00000000
7ffe0310 00000000 00000000 00000000 00000000
7ffe0320 00000000 00000000 00000000 00000000
7ffe0330 32c5a0a6 00000000 00000000 00000000
7ffe0340 00000000 00000000 00000000 00000000
7ffe0350 00000000 00000000 00000000 00000000
7ffe0360 00000000 00000000 00000000 00000000
7ffe0370 00000000 00000000 00000000 00000000
0:000> u 7c92e4f0 l3
*****ntdll!KiFastSystemCall:*****
7c92e4f0 8bd4 mov edx,esp
7c92e4f2 0f34 sysenter
ntdll!KiFastSystemCallRet:
7c92e4f4 c3 ret
可以看到7c92e4f2 0f34 sysenter
,实现r3到r0的转化。个人感觉用windbg
更快速一点,更适合查看数据结构,而不适合ring3
下的动态调试(可能也是因为我不熟吧)。
执行sysenter
之后,就与ring3
没有关系了,此时进入ring0
,调试器只能选择windbg
,用local kernel
调试分析。
有一点是默认的,进入内核之后,会自动调用对应的nt!ZwOpenProcess
.(保留意见-update:5.14)
ring0 kernel system space
nt!ZwOpenProcess:
804ff720 b87a000000 mov eax,7Ah
804ff725 8d542404 lea edx,[esp+4]
804ff729 9c pushfd
804ff72a 6a08 push 8
804ff72c e850ed0300 call nt!KiSystemService (8053e481)
804ff731 c21000 ret 10h
nt!ZwOpenProcessToken:
804ff734 b87b000000 mov eax,7Bh
804ff739 8d542404 lea edx,[esp+4]
804ff73d 9c pushfd
804ff73e 6a08 push 8
804ff740 e83ced0300 call nt!KiSystemService (8053e481)
804ff745 c20c00 ret 0Ch
nt!ZwOpenProcessTokenEx:
804ff748 b87c000000 mov eax,7Ch
804ff74d 8d542404 lea edx,[esp+4]
804ff751 9c pushfd
804ff752 6a08 push 8
804ff754 e828ed0300 call nt!KiSystemService (8053e481)
804ff759 c21000 ret 10h
call nt!KiSystemService (8053e481)
做了什么:从eax中获得ssdt数组索引,访问ssdt找到nt!NtOpenProcess地址,执行最终的内核函数nt!NtOpenProcess
nt!NtOpenProcess:
805c2296 68c4000000 push 0C4h
805c229b 68a8aa4d80 push offset nt!ObWatchHandles+0x25c (804daaa8)
805c22a0 e86b6cf7ff call nt!_SEH_prolog (80538f10)
.......
805c250d eb05 jmp nt!NtOpenProcess+0x27e (805c2514)
805c250f b8300000c0 mov eax,0C0000030h
805c2514 e8326af7ff call nt!_SEH_epilog (80538f4b)
805c2519 c21000 ret 10h
因此可以得到以下流程顺序:
OpenProcess
ring3: our module
->kernel32.dll(OpenProcess)
->ntdll(zw(nt)OpenProcess) (ssdt 索引号在eax中)
->system call
ring0:
->ntoskrnl.exe(nt!ZWOpenProcess) (同样存放ssdt索引号在eax中)
-> ntoskrnl.exe(nt!KiSystemService)
->ntoskrnl.exe(nt!NtOpenProccess)
update2:5.14
这幅图是我查找资料时得到的,大致说的还行。
但是还有些问题,这个图的画法就不说了。记得我前面说了:
有一点是默认的,进入内核之后,会自动调用对应的
nt!ZwOpenProcess
.(保留意见-update:5.14)
这幅图作者的意思是说,切换时触发中断后,一定是先执行nt!KiSystemService
,也就是一定不是先执行nt!ZwOpenProcess(内部其实也是在调用KiSystemService)
,这和我之前的说法有些出入 。还有说nt!zw*
不触发中断,什么意思。下个断点去研究了一下。
kd> bc *
kd> bp nt!zwOpenProcess
kd> bp nt!ntOpenProcess
kd> bl
0 e 804ff720 0001 (0001) nt!ZwOpenProcess
1 e 805c2296 0001 (0001) nt!NtOpenProcess
kd> g
Breakpoint 1 hit
nt!NtOpenProcess:
805c2296 68c4000000 push 0C4h
***********************
可以看到,打开一个程序后,断点在Nt*
上,没有在ZW*
上,所以我之前的说法是错误的。在本例中使用ntdll.KiFastSystemCall
,并没有进入到nt!ZWOpenProcess
中。
其实最好的方式是在KiSystemService
中下断点。但是这个符号基本上随时都在调用,根本调试不了。我想到的方法是添加条件断点,可惜现在还不会啊。。。
***********************
或者说,要研究的问题是:
既然r3下已经有了
nt!nt*
函数在ssdt
中的索引号,通过nt!KiSystemservice
检索就可以直接执行目标内核函数。那么nt!ZW*
再包装一次索引号的价值是什么。优势在哪里。
ntoskrnl.exe(nt!KiSystemService)
我没有研究过,汇编有好多行,不过现在知道主要功能如下。
通过eax(ssdt索引号)找到对应的nt!nt*函数地址,执行此函数,此函数为最终的内核函数。
上面概括性的讲了可以通过ssdt索引号
找到原始API函数地址,现在具体分析下SSDT
的数据结构和查找过程。
首先需要了解如何找到SSDT:
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase; //ssdt的指针
PULONG ServiceCounterTableBase;
ULONG NumberOfService; //SSDT中存放的NATIVE API 地址数据,即可索引数目。
PUCHAR ParamTableBase;
}SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;
ntoskrnl.exe
中导出了PSERVICE_DESCRPITOR_TABLE
类型指针,变量为KeServiceDescriptorTable
,在编程中导出即可。
来看下这个结构,使用windbg
选择local kernel
。
lkd> dd KeServiceDescriptorTable l4
80553fa0 80502b8c 00000000 0000011c 80503000
从上面的数据结构可以看出,ssdt的地址为80502b8c
,可索引的函数有0x11c
个。
SSDT是个数组结构,里面存放了所有的nt!nt*
函数地址,我们来查看下SSDT数组。
lkd> dd 80502b8c l0x11c
80502b8c 8059a948 805e7db6 805eb5fc 805e7de8
80502b9c 805eb636 805e7e1e 805eb67a 805eb6be
80502bac 8060cdfe 8060db50 805e31b4 805e2e0c
80502bbc 805cbde6 805cbd96 8060d424 805ac5ae
...
或者直接输入一下命令。
lkd> dd kiservicetable
80502b8c 8059a948 805e7db6 805eb5fc 805e7de8
80502b9c 805eb636 805e7e1e 805eb67a 805eb6be
80502bac 8060cdfe 8060db50 805e31b4 805e2e0c
80502bbc 805cbde6 805cbd96 8060d424 805ac5ae
...
可以看到,第一种方法是通过KeServiceDescriptorTable
得到ssdt地址,再查看。第二种是直接用符号查看。遗憾的是,编程中只能用第一种方法来获取,因为ntoskrnl.exe
没有导出这个符号变量。来验证一下。
C:\WINDOWS\system32>dumpbin /exports ntoskrnl.exe |findstr "KeService"
609 252 00083220 KeServiceDescriptorTable
C:\WINDOWS\system32>dumpbin /exports ntoskrnl.exe |findstr "KiService"
C:\WINDOWS\system32>
不管上面的小插曲,来验证下关于SSDT数组存放最终API地址的说法是否正确。
nt!ZwOpenProcess:
804ff720 b87a000000 mov eax,7Ah
804ff725 8d542404 lea edx,[esp+4]
804ff729 9c pushfd
804ff72a 6a08 push 8
804ff72c e850ed0300 call nt!KiSystemService (8053e481)
从上方可以得到nt!NTOpenProcess
地址在SSDT
表中的索引为7Ah。接下来查找一下其地址。
lkd> dd kiservicetable +0x7A*4 l1
80502d74 805c2296
lkd> u 805c2296
nt!NtOpenProcess:
805c2296 68c4000000 push 0C4h
805c229b 68a8aa4d80 push offset nt!ObWatchHandles+0x25c (804daaa8)
805c22a0 e86b6cf7ff call nt!_SEH_prolog (80538f10)
805c22a5 33f6 xor esi,esi
805c22a7 8975d4 mov dword ptr [ebp-2Ch],esi
805c22aa 33c0 xor eax,eax
805c22ac 8d7dd8 lea edi,[ebp-28h]
805c22af ab stos dword ptr es:[edi]
可见,猜想是正确的。
这时候再谈论SSDT HOOK就很简单了。SSDT表中存放的是目标函数地址,改写这个函数地址,就可以为所欲为了。从流程上说,我们的挂钩点是在调用ring0下的nt!KiSystemService
时触发的。
#include "stdafx.h"
#ifdef __cplusplus
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);
#endif
void SSDTHookUnload(PDRIVER_OBJECT);
//这个结构不是SSDT,是服务描述表,不过能从成员中能找到SSDT而已。
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase;
PULONG ServiceCounterTableBase;
ULONG NumberOfService;
PUCHAR ParamTableBase;
}SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TALBE;
//必须extern "C" ,因为文件为CPP
//extern "C" __declspec(dllimport) PSERVICE_DESCRIPTOR_TALBE KeServiceDescriptorTable; --此句错误,会出现无法挂钩,找了很长时间的bug。。可能是声明__declspec(dllimport)的姿势不对吧。看来是资料查错了,好好去补基础。
extern "C" PSERVICE_DESCRIPTOR_TALBE KeServiceDescriptorTable;
//搬运工
typedef NTSTATUS (*NtCreateProcessEx)(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN BOOLEAN InheritObjectTable,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL );
//函数指针,本程序中存放的是原始函数地址
NtCreateProcessEx ulNtCreateProcessEx = NULL;
//SSDT中此函数地址的指针
ULONG ulNtCreateProcessExAddr = 0;
/*
xp及其以后,SSDT内核内存页为只读属性,为了改写需要修改CR0 WP位
cr0的第16位是WP位,只要将这一位置0就可写,置1则只读。
*/
void REMOVE_ONLY_READ()
{
__asm
{
push eax
mov eax,CR0
and eax,~10000h //16th bit is 0
mov CR0,eax
pop eax
}
}
void RESET_ONLY_READ()
{
__asm
{
push eax
mov eax,CR0
or eax,10000h //16th bit is 1
mov CR0,eax
pop eax
}
}
//fake function ,声明要和NtCreateProcessEx一致。
NTSTATUS MyNtCreateProcessEx(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN BOOLEAN InheritObjectTable,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL
)
{
NTSTATUS Status = STATUS_SUCCESS;
DbgPrint("have hook CreateProcess!\r\n");
Status = ulNtCreateProcessEx(
ProcessHandle,
DesiredAccess,
ObjectAttributes,
ParentProcess,
InheritObjectTable,
SectionHandle ,
DebugPort ,
ExceptionPort
);
return Status;
}
VOID HOOKCreateProcess()
{
ULONG ulSsdt = 0;
ulSsdt = (ULONG)KeServiceDescriptorTable->ServiceTableBase;
ulNtCreateProcessExAddr = ulSsdt + 0x30*4 ; //index is 0x30
ulNtCreateProcessEx = (NtCreateProcessEx)*(PULONG)ulNtCreateProcessExAddr; //real addr
REMOVE_ONLY_READ();
*(PULONG)ulNtCreateProcessExAddr = (ULONG)MyNtCreateProcessEx;
RESET_ONLY_READ();
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
// #ifdef DBG
// __asm int 3
// #endif
DbgPrint("Hello from SSDTHook!\n");
DriverObject->DriverUnload = SSDTHookUnload;
HOOKCreateProcess();
return STATUS_SUCCESS;
}
void SSDTHookUnload(IN PDRIVER_OBJECT DriverObject)
{
REMOVE_ONLY_READ();
//当时两边都写成ulNtCreateProcessExAddr 了。。。。
*(PULONG)ulNtCreateProcessExAddr = (ULONG)ulNtCreateProcessEx;
RESET_ONLY_READ();
DbgPrint("Goodbye from SSDTHook!\n");
}