一、本文大綱
- 系統調用的兩種方式:中斷門和快速調用
- _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 :
作業系統啟動時,通過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
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
這個和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;
}