# 前言
上一篇我们分析了 KiSystemService 和 KiFastCallEntry 如何保存 3 环现场。本篇将分析它们的共同代码部分 —— 如何根据系统调用号找到并调用内核函数。
# SST(System Service Table,系统服务表)
- 系统服务表共有两张,第一张表后紧接第二张表。
- 第一张表的函数来自
ntoskrnl.exe (基本系统调用),第二张表的函数来自 win32k.sys (图形 / 窗口相关调用)。
- 它并不包含内核文件导出的所有函数,而是 3 环最常用的内核函数。
- 系统服务表指针位于
_KTHREAD + 0xE0 ( ServiceTable 成员)。
1 2 3 4 5 6 7 8 9
| typedef struct _SERVICE_DESCRIPTOR_TABLE { PULONG ServiceTableBase; PULONG ServiceCounterTableBase; ULONG NumberOfService; PUCHAR ParamTableBase; } SSDTEntry, *PSSDTEntry;
|
# 实验 1:分析共同代码
KiSystemService 和 KiFastCallEntry 最终都跳转到 loc_406922 执行以下共同代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| ; 从 eax 寄存器取出 3 环传入的系统调用号 .text:00406922 mov edi, eax
; 系统调用号右移 8 位 .text:00406924 shr edi, 8
; 与 0x30 做与运算,检测第 12 位的值 ; WindowsNT 基本的系统调用编号小于 0x1000 ; 编号大于 0x1000 的系统调用来自 win32k.sys(图形/窗口相关) ; 若第 12 位为 0(结果为 0x00)→ 在 ntoskrnl.exe 中查找 ; 若第 12 位为 1(结果为 0x10)→ 在 win32k.sys 中查找 .text:00406927 and edi, 30h
; 将运算结果赋值给 ecx .text:0040692A mov ecx, edi
; KTHREAD + 0xE0 = ServiceTable(系统服务表指针) ; 将系统服务表地址加上 edi 的运算结果 ; 巧妙地选择使用哪张表(两张表连续,每张占 16 字节) .text:0040692C add edi, [esi+0E0h]
; 将系统调用号赋值给 ebx .text:00406932 mov ebx, eax
; 保留调用号的低 12 位,作为函数地址的下标 .text:00406934 and eax, 0FFFh
; 校验系统调用号是否越界 ; ServiceTable + 0x8 = NumberOfService(服务函数总数) .text:00406939 cmp eax, [edi+8] .text:0040693C jnb _KiBBTUnexpectedRange
; 若系统调用号 < 0x1000,跳过 GUI 批处理 .text:00406942 cmp ecx, 10h .text:00406945 jnz short loc_406962
; 只有 ecx == 0x10(win32k.sys 调用)才会执行以下代码 ; 作用是动态加载 GUI 等图形相关函数 .text:00406947 mov ecx, large fs:18h .text:0040694E xor ebx, ebx .text:00406950 or ebx, [ecx+0F70h] .text:00406956 jz short loc_406962 .text:00406958 push edx .text:00406959 push eax .text:0040695A call ds:_KeGdiFlushUserBatch .text:00406960 pop eax .text:00406961 pop edx
.text:00406962 loc_406962: ; 系统调用计数 +1 ; KPRCB + 0x518 = KeSystemCalls .text:00406962 inc large dword ptr fs:638h
; edx = 3 环参数指针(esi 保存) .text:00406969 mov esi, edx
; 取出参数表指针 ; ServiceTable + 0xC = ParamTableBase .text:0040696B mov ebx, [edi+0Ch]
; ecx 清零 .text:0040696E xor ecx, ecx
; 取出当前内核函数的参数总长度(单位:字节) ; ParamTableBase[eax] → cl .text:00406970 mov cl, [eax+ebx]
; 取出函数地址表指针 ; ServiceTable + 0x0 = ServiceTableBase .text:00406973 mov edi, [edi]
; 函数地址 = ServiceTableBase[eax * 4] .text:00406975 mov ebx, [edi+eax*4]
; 提升堆栈,为参数腾出空间 .text:00406978 sub esp, ecx
; 参数总长度 / 4 = 参数个数 .text:0040697A shr ecx, 2
; 设置复制的目的地 .text:0040697D mov edi, esp
; 检查 3 环参数是否越界 .text:0040697F cmp esi, ds:_MmUserProbeAddress .text:00406985 jnb loc_406B33
.text:0040698B loc_40698B: ; 将 3 环参数复制到 0 环堆栈 .text:0040698B rep movsd
; 调用内核函数 .text:0040698D call ebx
|
总结:
- 解析系统调用号:通过检查第 12 位决定查找
ntoskrnl.exe (基本系统调用)还是 win32k.sys (GUI 扩展调用)。
- 校验调用号合法性:与
NumberOfService 比较,防止越界。
- 复制用户态参数到内核栈:通过
rep movsd 指令将 3 环参数拷贝到 0 环堆栈。
- 跳转执行:
call ebx 调用对应的内核函数。
# SSDT(System Service Descriptor Table,系统服务描述符表)
- SSDT 的每个成员叫做系统服务表(SST)。
- SSDT 的第一个成员是导出的,声明一下即可使用。
- SSDT 的第二个成员是未导出的,需要通过其他方式查找。
- 在 Windows 中,SSDT 的第三个和第四个成员未被使用。
在 WinDbg 中查看已导出成员:
1
| kd> dd KeServiceDescriptorTable
|
在 WinDbg 中查看未导出成员:
1
| kd> dd KeServiceDescriptorTableShadow
|
# 实验 2:在 SSDT 中查找内核函数信息
说明:
- 在之前的实验中,我们知道
ReadProcessMemory 即将进入内核时传递了一个系统调用号 0xBA 。
- 本次实验在 SSDT 中查找编号
0xBA 对应的内核函数信息。
一、查看函数地址。
函数地址表:
[函数地址表 + 系统调用号 × 4] = 内核函数地址:
二、查看参数个数。
参数表:
[参数表 + 系统调用号] = 内核函数参数长度(单位:字节):
三、查看内核函数反汇编。
# 练习:在 SSDT 中新增函数并调用
要求:在 SSDT 表中追加一个函数地址( NtReadVirtualMemory ),自己编写 API 的 3 环部分调用这个新增的函数。
一、在 ServiceTableBase 末尾新增一个函数。
查看 SSDT 表:
查看 NtReadVirtualMemory 地址:
查看 ServiceTableBase 的最后一项:
将 NtReadVirtualMemory 的地址写入 ServiceTableBase 的末尾:
二、函数个数加 1。
三、在参数表末尾加入新增函数的参数长度(单位:字节)。
四、编写 3 环代码,通过新的调用号调用 NtReadVirtualMemory。
进程 1(运行后不要关闭):
1 2 3 4 5 6 7 8 9
| #include <stdio.h>
int main() { int num = 0x12345678; printf("&num = %x \n", &num); getchar(); return 0; }
|
进程 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <stdio.h> #include <windows.h>
void MyReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead) { __asm { lea eax, [ebp+0x14] push eax ; ReturnLength push [ebp+0x14] ; BufferLength push [ebp+0x10] ; Buffer push [ebp+0x0C] ; BaseAddress push [ebp+0x08] ; ProcessHandle
mov eax, 11ch ; 新的系统调用号 mov edx, esp ; 参数指针 int 02eh ; 通过 IDT[0x2E] 进行内核调用
add esp, 20 ; 平衡堆栈(5 × 4 = 20 字节) } }
int main() { DWORD pBuffer; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, ); MyReadProcessMemory(hProcess, (PVOID)0x12ff7c, &pBuffer, 4, 0);
printf("pBuffer = %x \n", pBuffer);
getchar(); return 0; }
|
运行结果:
进程 2 通过新的调用号 0x11C 成功读出了进程 1 的内存,说明 SSDT 中新增的函数调用成功。
上一篇:系统调用(三)| 保存现场