一、本文大纲
- 系统调用的两种方式:中断门和快速调用
- _KUSER_SHARED_DATA 结构
- 使用 cpuid 指令判断当前CPU是否支持快速调用
- 3环进0环需要更改的4个寄存器
- 以 ReadProcessMemory 为例说明系统调用全过程
- 重写 ReadProcessMemory 和 WriteProcessMemory
- int 0x2e 和 sysenter 都做了什么工作?
二、中断门和快速调用
以我的理解,系统调用,即从调用操作系统提供的3环API开始,到进0环,再到返回结果到3环的全过程。
系统调用有中断调用和快速调用两种方式,中断调用是通过中断门进0环,此过程需要查IDT表和TSS表;
快速调用则是使用 sysenter 指令进0环,这种方式不需要查内存,而是直接从CPU的MSR寄存器中获取所需数据,所以称为快速调用
三、_KUSER_SHARED_DATA 结构
7ffe0000
ffdf0000
此结构体由操作系统负责初始化,其偏移 0x300 处有一个 SystemCall 属性,是个函数指针。
nt!_KUSER_SHARED_DATA +0x000 TickCountLow : Uint4B +0x004 TickCountMultiplier : Uint4B +0x008 InterruptTime : _KSYSTEM_TIME +0x014 SystemTime : _KSYSTEM_TIME +0x020 TimeZoneBias : _KSYSTEM_TIME +0x02c ImageNumberLow : Uint2B +0x02e ImageNumberHigh : Uint2B +0x030 NtSystemRoot : [260] Uint2B +0x238 MaxStackTraceDepth : Uint4B +0x23c CryptoExponent : Uint4B +0x240 TimeZoneId : Uint4B +0x244 Reserved2 : [8] Uint4B +0x264 NtProductType : _NT_PRODUCT_TYPE +0x268 ProductTypeIsValid : UChar +0x26c NtMajorVersion : Uint4B +0x270 NtMinorVersion : Uint4B +0x274 ProcessorFeatures : [64] UChar +0x2b4 Reserved1 : Uint4B +0x2b8 Reserved3 : Uint4B +0x2bc TimeSlip : Uint4B +0x2c0 AlternativeArchitecture : _ALTERNATIVE_ARCHITECTURE_TYPE +0x2c8 SystemExpirationDate : _LARGE_INTEGER +0x2d0 SuiteMask : Uint4B +0x2d4 KdDebuggerEnabled : UChar +0x2d5 NXSupportPolicy : UChar +0x2d8 ActiveConsoleId : Uint4B +0x2dc DismountCount : Uint4B +0x2e0 ComPlusPackage : Uint4B +0x2e4 LastSystemRITEventTickCount : Uint4B +0x2e8 NumberOfPhysicalPages : Uint4B +0x2ec SafeBootMode : UChar +0x2f0 TraceLogging : Uint4B +0x2f8 TestRetInstruction : Uint8B +0x300 SystemCall : Uint4B +0x304 SystemCallReturn : Uint4B +0x308 SystemCallPad : [3] Uint8B +0x320 TickCount : _KSYSTEM_TIME +0x320 TickCountQuad : Uint8B +0x330 Cookie : Uint4B
操作系统启动时,通过CPUID指令,判断CPU是否支持快速调用,根据判断结果,在 +0x300 SystemCall 处填写不同的函数指针。
当CPU支持快读调用,SystemCall 指向 ntdll.dll!KiFastSystemCall()
当CPU不支持快速调用,SystemCall 指向 ntdll.dll!KiIntSystemCall()
观察该结构体的名字,意思为“内核-用户共享内存”。
3环通过地址 0x7ffe0000 可以访问到这个结构体,3环PTE属性是只读;
0环通过地址 0xffdf0000 可以访问到这个结构体,0环PTE属性是可读写。
这两个线性地址映射的是同一个物理页。
四、CPUID 指令
通过CPUID指令查看当前CPU是否支持快速调用,方法是将EAX值设置为1,然后调用CPUID指令,指令执行结果存储在ECX和EDX中,其中EDX的SEP位(11位)表明CPU是否支持快速调用指令 sysenter / sysexit。
可以看到,在我的电脑中执行CPUID指令后,EDX(…BFF)的11位是1。
五、3环进0环需要更改的4个寄存器
-
CS的权限由3变为0 意味着需要新的CS
-
SS与CS的权限永远一致 需要新的SS
-
权限发生切换的时候,堆栈也一定会切换,需要新的ESP
-
进0环后代码的位置,需要EIP
简单复习一下,中断门进0环时,我们在IDT表里填的中断门描述符,包含了0环的CS和EIP,而SS和0环的ESP是在TSS里存储的,当时我们还有一个结论,windows里不使用任务,所以TSS的唯一作用就是提权时提供ESP0和SS0。
现在,我们知道了进0环需要更改的4个寄存器,接下来分析 KiFastSystemCall 和 KiIntSystemCall 时,只要明白一点,这两个函数做的事情就是更改这4个寄存器。
六、以 ReadProcessMemory 为例说明系统调用全过程
大家可以看 kernel32.dll 里 ReadProcessMemory 的反汇编,我这里抠出最关键的一条指令:
call ds:__imp__NtReadVirtualMemory@20 ; NtReadVirtualMemory(x,x,x,x,x)
ReadProcessMemory 啥也没干,只是调用了 ntdll.dll 的导出函数 NtReadVirtualMemory 函数。
看看 NtReadVirtualMemory 干了啥?
_NtReadVirtualMemory@20 proc near mov eax, 0BAh ; NtReadVirtualMemory mov edx, 7FFE0300h call dword ptr [edx] retn 14h _NtReadVirtualMemory@20 endp
NtReadVirtualMemory 把系统调用号(服务号?)存到EAX,然后 call [7FFE0300h],实际上就是调用了 KiFastSystemCall 函数(因为我的CPU支持快速调用的,所以 7FFE0300h 存的是 KiFastSystemCall)
再看看 KiFastSystemCall 干了啥?
_KiFastSystemCall@0 proc near mov edx, esp sysenter _KiFastSystemCall@0 endp ;
把3环栈顶地址存储到edx中,然后调用sysenter指令,然后就进0环了。
假设,我的CPU不支持快速调用,那么 NtReadVirtualMemory 就会调用另一个函数 KiIntSystemCall
_KiIntSystemCall@0 proc near arg_4= byte ptr 8 lea edx, [esp+arg_4] ; edx是第一个参数的指针,eax存的是系统调用号 int 2Eh ; DOS 2+ internal - EXECUTE COMMAND ; DS:SI -> counted CR-terminated command string retn _KiIntSystemCall@0 endp
这个和sysenter稍有不同,它把第一个参数(或者说最后一个压栈的参数)的指针存到edx中,然后触发2E中断进0环。
七、重写 ReadProcessMemory 和 WriteProcessMemory
通过上面的分析,我们已经了解了系统调用3环部分的过程,下面我重写了 ReadProcessMemory 和 WriteProcessMemory 函数。重写3环API的意义在于,可以防3环HOOK API的检测。
注意,vs 内联汇编不支持 sysenter 指令,可以用 _emit 代替。
我的代码是在vs2010编译的,实测vc6编译 push NtWriteVirtualMemoryReturn 这条指令时会出错,你可以看一下vc6生成的是什么代码,挺坑的。
// 读写内存_中断门和快速调用实现.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <Windows.h> // 读进程内存(中断门调用) BOOL WINAPI HbgReadProcessMemory_INT(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead) {
LONG NtStatus; __asm {
// 直接模拟 KiIntSystemCall lea edx,hProcess; // 要求 edx 存储最后入栈的参数 mov eax, 0xBA; int 0x2E; mov NtStatus, eax; } if (lpNumberOfBytesRead != NULL) {
*lpNumberOfBytesRead = nSize; } // 错误检查 if (NtStatus < 0) {
return FALSE; } return TRUE; } // 读进程内存(快速调用) BOOL WINAPI HbgReadProcessMemory_FAST(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead) {
LONG NtStatus; __asm {
// 模拟 ReadProcessMemory lea eax,nSize; push eax; push nSize; push lpBuffer; push lpBaseAddress; push hProcess; sub esp, 0x04; // 模拟 ReadProcessMemory 里的 CALL NtReadVirtualMemory // 模拟 NtReadVirtualMemory mov eax, 0xBA; push NtReadVirtualMemoryReturn; // 模拟 NtReadVirtualMemory 函数里的 CALL [0x7FFE0300] // 模拟 KiFastSystemCall mov edx, esp; _emit 0x0F; // sysenter _emit 0x34; NtReadVirtualMemoryReturn: add esp, 0x18; // 模拟 NtReadVirtualMemory 返回到 ReadProcessMemory 时的 RETN 0x14 mov NtStatus, eax; } if (lpNumberOfBytesRead != NULL) {
*lpNumberOfBytesRead = nSize; } // 错误检查 if (NtStatus < 0) {
return FALSE; } return TRUE; } // 写进程内存(中断门调用) BOOL WINAPI HbgWriteProcessMemory_INT(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesWritten) {
LONG NtStatus; __asm {
lea edx,hProcess; mov eax, 0x115; int 0x2E; mov NtStatus, eax; } if (lpNumberOfBytesWritten != NULL) {
*lpNumberOfBytesWritten = nSize; } // 错误检查 if (NtStatus < 0) {
return FALSE; } return TRUE; } // 写进程内存(快速调用) BOOL WINAPI HbgWriteProcessMemory_FAST(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesWritten) {
LONG NtStatus; __asm {
// 模拟 WriteProcessMemory lea eax,nSize; push eax; push nSize; push lpBuffer; push lpBaseAddress; push hProcess; sub esp, 0x04; // 模拟 WriteProcessMemory 里的 CALL NtWriteVirtualMemory // 模拟 NtWriteVirtualMemory mov eax, 0x115; push NtWriteVirtualMemoryReturn; // 模拟 NtWriteVirtualMemory 函数里的 CALL [0x7FFE0300] // 模拟 KiFastSystemCall mov edx, esp; _emit 0x0F; // sysenter _emit 0x34; NtWriteVirtualMemoryReturn: add esp, 0x18; // 模拟 NtWriteVirtualMemory 返回到 WriteProcessMemory 时的 RETN 0x14 mov NtStatus, eax; } if (lpNumberOfBytesWritten != NULL) {
*lpNumberOfBytesWritten = nSize; } // 错误检查 if (NtStatus < 0) {
return FALSE; } return TRUE; } // 提权函数:提升为DEBUG权限 BOOL EnableDebugPrivilege() {
HANDLE hToken; BOOL fOk=FALSE; if(OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES,&hToken)) {
TOKEN_PRIVILEGES tp; tp.PrivilegeCount=1; LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&tp.Privileges[0].Luid); tp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(tp),NULL,NULL); fOk=(GetLastError()==ERROR_SUCCESS); CloseHandle(hToken); } return fOk; } int _tmain(int argc, _TCHAR* argv[]) {
EnableDebugPrivilege(); DWORD pid,addr,dwRead,dwWritten; char buff[20] = {
0}; printf("依次输入PID和要读的线性地址(均为16进制)...\n"); scanf("%x %x", &pid, &addr); getchar(); // 测试两个版本的 ReadProcessMemory HbgReadProcessMemory_INT(OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid),(LPCVOID)addr,buff,4,&dwRead); printf("读取了%d个字节,内容是: \"%s\"\n", dwRead, buff); HbgReadProcessMemory_FAST(OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid),(LPCVOID)(addr+4),buff,4,&dwRead); printf("读取了%d个字节,内容是: \"%s\"\n", dwRead, buff); // 测试两个版本的 WriteProcessMemory HbgWriteProcessMemory_INT(OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid),(LPCVOID)addr,"##",2,&dwWritten); printf("写入了%d字节.\n", dwWritten); HbgWriteProcessMemory_FAST(OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid),(LPCVOID)(addr+4),"**",2,&dwWritten); printf("写入了%d字节.\n", dwWritten); // 再次读取,验证写入是否成功 HbgReadProcessMemory_INT(OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid),(LPCVOID)addr,buff,4,&dwRead); printf("读取了%d个字节,内容是: \"%s\"\n", dwRead, buff); HbgReadProcessMemory_FAST(OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid),(LPCVOID)(addr+4),buff,4,&dwRead); printf("读取了%d个字节,内容是: \"%s\"\n", dwRead, buff); printf("bye!\n"); getchar(); return 0; }