Windows XP 系统调用(二)
ʕ •ᴥ•ʔ ɔ:
1 系统服务调用
上一篇分析了2种方式从3环进0环,进入0环后分别调用2个0环的函数:KiSystemService(int 0x2e)、KiFastCallEntry(sysenter),这两个函数经过一波初始化任务之后都转到相同的地址去就执行代码。以下4个问题中的1已经在上一篇解决了(保留到_Trap_Frame结构体中),接下来将会继续分析问题2和3,问题4要等到学习APC之后才能解决。
- 进0环后,原来的寄存器存在哪里?
- 如何根据系统调用号(eax中存储)找到要执行的内核函数?
- 调用时参数是存储到3环的堆栈,如何传递给内核函数?
- 2种调用方式是如何返回到3环的?
1.1 SystemServiceTable(SST)
注意:SystemServiceTable 系统服务表不是SSDT。
SystemServiceTable:SST,系统服务表。
System Services Descriptor Table:SSDT,系统服务描述符表,导出结构为KeServiceDescriptorTable(在内核ntoskrnl.exe导出表中可看到该全局变量)。
SystemServiceParameterTable:SSPT,系统服务参数表。
System Services Descriptor Table Shadow:SSDT Shadow,系统服务描述符表Shadow,导出结构为KeServiceDescriptorTableShadow,该结构没有导出。
通过上图,我们可以得知以下信息:
- 通过
_KTHREAD
偏移0xE0
可以找到系统服务表SSDT。 - 系统服务表又指向了函数地址表和函数参数表(SSPT)
- 有两张系统服务表,第一张是用来找内核函数的,第二张是找
Win32k.sys
驱动函数的。 - 两张系统服务表是线性地址连续的,每张16字节。
- SST表成员:
- ServiceTable,是一个指针,指向函数地址表(每个成员4Byte)。
- Count没有用。
- ServiceLimit,当前系统服务表函数的个数。
- ArgmentTable,指向函数参数表,参数表每个成员大小为1字节,值为参数的大小,函数参数个数(存储值 / 4 = 参数个数),每个成员和函数地址表中的函数一一对应。
3环API进0环之前,无论是中断门还是快速调用,都会在 eax 里存一个值,我们称之为系统调用号或者服务号。EAX系统服务号:
- 1~12位:函数地址表/函数参数表索引(下标)。
- 第13位(下标12):
- 0,表示找第一张系统服务表(绿色的表);
- 1,那么找第二张表(黄色的表)。
SST结构如下:
1 | typedef struct _KSYSTEM_SERVICE_TABLE |
1.2 继续分析函数
前篇博客,逆向分析了 KiSystemService 和 KiFastCallEntry 填充_KTRAP_FRAME 结构体的代码,二者大同小异,主要的区别是 sysenter 只改了eip,cs,ss,虽然esp也改了,但是windows不使用,而是从TSS里取esp0;另外sysenter并没有像中断门那样压栈,所以3环的 ss, esp, eflags, cs,eip都要在函数里依次保存到 _KTRAP_FRAME 。
他们两个函数开头的初始化工作不一样,有两个入口,初始化的工作有区别,但是往后就共用一个函数体。以下继续分析:
- 如何根据系统服务号(eax中存储)找到要执行的内核函数?
- 调用时参数是存储到3环的堆栈,如何传递给内核函数?
1 | .text:0046A5AF |
2 SSDT
SSDT(System Services Descriptor Table)系统服务描述符表,内核模块ntoskrnl.exe导出的一个全局变量KeServiceDescriptorTable,用来指向SSDT。
SSDT结构:
1 | typedef struct _KSERVICE_TABLE_DESCRIPTOR |
2.1 SST和SSDT
SST和SSDT的关系如下:
1 | SSDT |
SST结构如下:
1 | typedef struct _KSYSTEM_SERVICE_TABLE |
通过前面1.2的分析,要找到SST表可以通过_KTHREAD + 0xE0
指向的地址找到,Windows系统在内核模块中导出了一个全局变量KeServiceDescriptorTable(SSDT),也可以得到SST表。
通过_KTHREAD + 0xE0
查找SST:
fs[0] = _KPCR = 0xffdff000
。1
2
3
4
5
6
7
8kd> u _KPCR ffdff000
Couldn't resolve error at '_KPCR ffdff000'
kd> dt _KPCR ffdff000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : 0xffdff000 _KPCR
+0x020 Prcb : 0xffdff120 _KPRCB
...可以看到
_KPRCB = 0xffdff120
。1
2
3
4
5
6+0x000 MinorVersion : 1
+0x002 MajorVersion : 1
+0x004 CurrentThread : 0x8055ce60 _KTHREAD
+0x008 NextThread : (null)
+0x00c IdleThread : 0x8055ce60 _KTHREAD
...可以看到
_KTHREAD = 0x8055ce60
。1
2
3
4
5
6
7
8
9kd> dt _KTHREAD 0x8055ce60
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY [ 0x8055ce70 - 0x8055ce70 ]
...
+0x0e0 ServiceTable : 0x8055d700 Void
+0x0e4 Queue : (null)
+0x0e8 ApcQueueLock : 0
...以看到
0x8055d700
指向SSDT。1
2
3
4
5
6kd> dd 0x8055d700
8055d700 80505450 00000000 0000011c 805058c4
8055d710 00000000 00000000 00000000 00000000
8055d720 00000000 00000000 00000000 00000000
8055d730 00000000 00000000 00000000 00000000
8055d740 00000002 00002710 bf80c0b6 00000000查看内核模块导出的结构
KeServiceDescriptorTable
。1
2
3
4
5
6kd> dd KeServiceDescriptorTable
8055d700 80505450 00000000 0000011c 805058c4
8055d710 00000000 00000000 00000000 00000000
8055d720 00000000 00000000 00000000 00000000
8055d730 00000000 00000000 00000000 00000000
8055d740 00000002 00002710 bf80c0b6 00000000
可以得到结论:
- 通过
_KTHREAD + 0xE0
查找得到表是SSDT,通过ntkrnlpa.exe(2-9-9-12)导出结构KeServiceDescriptorTable
得到的也是SSDT。 - SSDT中过包涵4个SST,但是仅可以看到第一个(由ntkrnlpa.exe导出的全局变量),想要看到
win32k.sys
导出的函数还需要分析系统未导出的结构SSDT Shadow。
关于SSDT更多细节可参读:
2.2 SSDT和SSDT Shadow
- SSDT:KeServiceDescriptorTable,是由ntkrnlpa.exe导出的全局变量,可以在代码中(仅0环)使用。
- SSDT Shadow:KeServiceDescriptorTableShadow,不是导出结构,不可以在0环代码中使用。
查看SSDT和SSDT Shadow:
1 | kd> dd KeServiceDescriptorTable |
- SSDT、SSDT Shadow都包含4个SST,但从SSDT中仅可看到由ntkrnlpa.exe导出函数的地址表。
- SSDT Shadow可以看到2个地址表,由ntkrnlpa.exe和win32k.sys导出。
在驱动程序中使用SSDT:
1 | //extern "C" __declspec(dllimport) SSDT KeServiceDescriptorTable; |
内核中有两个系统服务描述符表,一个是KeServiceDescriptorTable(由ntoskrnl.exe导出),一个是KeServieDescriptorTableShadow(没有导出)。
两者的区别是:KeServiceDescriptorTable仅有ntoskrnel一项,KeServieDescriptorTableShadow包含了ntoskrnel以及win32k。一般的Native API的服务地址由KeServiceDescriptorTable分派,gdi.dll/user.dll的内核API调用服务地址由KeServieDescriptorTableShadow分派。还有要清楚一点的是win32k.sys只有在GUI线程中才加载,一般情况下是不加载的,所以要Hook KeServieDescriptorTableShadow的话,一般是用一个GUI程序通过IoControlCode来触发(或者在程序中先调用一个gdi.dll/user.dll
的API,否则会蓝屏)。
32位系统:
- XP:
KeServiceDescriptorTableShadow = KeServiceDescriptorTable - 0×40
。 - Win7:
KeServiceDescriptorTableShadow = KeServiceDescriptorTable + 0×40
。
64位系统:SSDT表是加密的。
64位系统的SSDT表可参考:HOOK技术之SSDT hook(x86/x64)。
3 SSDT HOOK
3.1 SSDT HOOK 原理
SSDT HOOK原理:用自己写好的一个函数替换SSDT中的一个函数SSDT.ntoskrnl.ServiceTableBase[0xIndex]
即可。
注意事项:
替换函数地址时必须保证SSDT表是可写入的,但自XP系统之后SSDT内存块属性仅可读,需要自己进行修改,有至少三种方法可使用:
更改注册表
恢复页面保护:HKLM\SYSTEM\CurrentControlset\Control\Session Manger\Memory Management\EnforceWriteProtection=0
去掉页面保护:HKLM\SYSTEM\CurrentControlset\Control\Session Manger\Memory Management\DisablePagingExecutive=1
改变CR0寄存器下标16的位(第17位 == 0,可写)
CR0寄存器的第17位叫做保护属性位,控制着页的读或写属性。该方法在多核情况下不稳定,核切换时CR0也就切换了。
页表基址修改页属性
通过修改要替换地址的
PDE_R/W & PTE_R/W
属性。该方法最稳定最好。
32位系统上SSDT是导出的(KeServiceDescriptorTable
),64位是不会导出的,但是可以通过PCHunter、Kernel Detective等工具查看到内核函数对应的函数下标索引。
如下是通过PCHunter查看到的SSDT表(内核钩子-SSDT-右键取消仅显示挂钩函数):
每一个版本的Windows操作系统的系统服务函数的编号都是固定的,例如所有32位的windows 7的系统服务函数的编号都是固定的,无论系统版本如何变化。这主要是因为一旦更新操作系统后,如果系统服务函数的编号发生变化会导致系统不稳定。基于以上事实,我们只需要针对win7和win10定义四份函数表即可(32位、64位)。SSDT(系统服务描述符表 system services descriptor table)
SSDT HOOK推荐几篇文章:
- SSDT Hook实现内核级的进程保护
- 进程隐藏与进程保护(SSDT Hook 实现)(一)
- 进程隐藏与进程保护(SSDT Hook 实现)(二)
- 系统调用(R3API调用过程详解)
- HOOK技术之SSDT hook(x86/x64)
3.2 SSDT HOOK模版
下面的驱动代码ssdt hook了NtOpenProcess函数,可以监视打开进程的操作。
1 |
|
3.3 SSDT HOOK 实现保护记事本进程
题目要求:
将系统服务表中某个函数改成自己的函数,使任务管理器右键无法关闭自己,只有点击自己的关闭按钮才可以正常关闭。
补充内容:
在3环的程序要想终止某个进程会调用函数TerminateProcess(HANDLE hProcess, UINT uExitCode)
,通过追码有以下调用流程:
1 | 3环:Kernel32.dll.TerminateProcess(x,x) --> NtDll.dll.NtTerminateProcess(x,x) |
前面在驱动开发部分已经通过特征码搜索未导出函数PspTerminateProcess
来结束一个进程,他和NtTerminateProcess
函数的区别:
NtTerminateProcess
:ntkrnlpa.exe中未导出该函数,存在于SST中,操作系统提供给3环API调用。PspTerminateProcess
:ntkrnlpa.exe中未导出该函数,也不存在于SST中,但是操作系统自己结束一个进程时却是调用该函数。
所以该题方法是SSDT HOOK NtTerminateProcess 函数。
1 |
|
通过任务管理器关闭记事本:
通过关闭记事本的按钮:
通过PCHunter查看:
3.4 SSDT Shadow HOOK 的 FindWindowA 监视器
先追一下FindWindowA
调用路径:
FindWindowA –> user32.dll.FindWindowA –> user32.dll.NtUserFindWindowEx(x, x, x, x, x) –> eax = 117Ah
0环驱动代码:
1 |
|
3环代码:
1 | // HOOK0316.cpp : Defines the entry point for the console application. |
⚠️注意事项:
在3环程序中,驱动名称
DRIVER_NAME
和驱动路径DRIVER_PATH
名称都要是.sys
的名称。3环的加载程序需要和
.sys
驱动程序在同一目录下。3环程序在多次调试后可能会失败卸载驱动,导致
StartService()
失败,GetLastError()
得到错误码是2,此时需要用KmdManager.exe
对驱动进行卸载。在VC直接F7、F5运行3环的加载程序可能会一闪而过,这个时候需要在该程序的目录下双击运行该程序(需要先执行步骤3进行卸载再双击)。
注意驱动的调用进程:
上述程序说明,驱动加载后,执行驱动入口代码时,所属进程是系统进程。这和 DeviceIoControl 时情况又有所不同,DeviceIoControl 通信时,所属进程是发起通信的3环程序。