一、API調用過程(3環部分)
什么是API?
API,Application Programming Interface 應用程序接口,操作系統的API主要放在C:\WINDOWS\system32 。絕大多數的API都是在Ring0實現的,極少數是在3環,3環只是提供一個接口。
幾個重要的DLL
1、Kernel32.dll
最核心的功能模塊:如管理內存、進程、線程相關函數
2、User3.dll
是Windows用戶界面相關應用程序接口,如創建窗口和發送消息
3、GDI32.dll
Graphical Device Interface,圖形設備接口,包含用於畫圖和顯示文本的函數:如要顯示一個程序窗口
4、Ntdil.dll
大多數API都會通過這個DLL進入內核
IDA分析ReadProcessMemory API
1、Kernel32.dll中搜索ReadProcessMemory
//5個參數壓棧
.text:7C8021D0 ProcessHandle = dword ptr 8
.text:7C8021D0 BaseAddress = dword ptr 0Ch
.text:7C8021D0 Buffer = dword ptr 10h
.text:7C8021D0 BufferLength = dword ptr 14h
.text:7C8021D0 lpNumberOfBytesRead= dword ptr 18h
.text:7C8021D0
.text:7C8021D0 mov edi, edi
.text:7C8021D2 push ebp
.text:7C8021D3 mov ebp, esp
.text:7C8021D5 lea eax, [ebp+BufferLength]
.text:7C8021D8 push eax ; ReturnLength
.text:7C8021D9 push [ebp+BufferLength] ; BufferLength
.text:7C8021DC push [ebp+Buffer] ; Buffer
.text:7C8021DF push [ebp+BaseAddress] ; BaseAddress
.text:7C8021E2 push [ebp+ProcessHandle] ; ProcessHandle
.text:7C8021E5 call ds:NtReadVirtualMemory ;核心功能在這里
.text:7C8021EB mov ecx, [ebp+lpNumberOfBytesRead]
.text:7C8021EE test ecx, ecx
.text:7C8021F0 jnz short loc_7C8021FD
.text:7C8021F2
.text:7C8021F2 loc_7C8021F2: ; CODE XREF: ReadProcessMemory+32j
.text:7C8021F2 test eax, eax
.text:7C8021F4 jl short loc_7C802204 ;小於0則跳轉
.text:7C8021F6 xor eax, eax
.text:7C8021F8 inc eax
.text:7C8021F9
.text:7C8021F9 loc_7C8021F9: ; CODE XREF: ReadProcessMemory+3Cj
.text:7C8021F9 pop ebp
.text:7C8021FA retn 14h
.text:7C8021FD ; ---------------------------------------------------------------------------
.text:7C8021FD
.text:7C8021FD loc_7C8021FD: ; CODE XREF: ReadProcessMemory+20j
.text:7C8021FD mov edx, [ebp+BufferLength]
.text:7C802200 mov [ecx], edx
.text:7C802202 jmp short loc_7C8021F2
.text:7C802204 ; ---------------------------------------------------------------------------
.text:7C802204
.text:7C802204 loc_7C802204: ; CODE XREF: ReadProcessMemory+24j
.text:7C802204 push eax ; NtStatus
.text:7C802205 call sub_7C8093FD
.text:7C80220A xor eax, eax
.text:7C80220C jmp short loc_7C8021F9
2、參數壓棧后,調用了另外一個函數,返回結果在eax中,小於0的話就跳到這里
ReadProcessMemory+24j
.text:7C802204 push eax
.text:7C802205 call sub_7C8093FD ;這是一個API用於設置錯誤號
.text:7C80220A xor eax, eax
.text:7C80220C jmp short loc_7C8021F9
3、Call一個用於設置錯誤號(BaseSetLastNTError )函數,將eax清0后,又跳回去。
.text:7C8021F9
.text:7C8021F9 loc_7C8021F9: ; CODE XREF: ReadProcessMemory+3Cj
.text:7C8021F9 pop ebp
.text:7C8021FA retn 14h
跳到這,直接返回,所以失敗返回的結果是0.
4、如果eax>0,eax清0,eax+1,返回1
分析完這個函數,發現:主要的功能實現全面在NtReadVirtualMemory 這個API里了。
我們先來看看NtReadVirtualMemory 在哪個DLL中。
打開導入表,發現在ntdll.dll里。
7C801418 NtReadVirtualMemory ntdll
在這個dll里搜索一下NtReadVirtualMemory
發現這個函數里也沒什么實現細節,只是提供一個操作碼,通過這種方式實現從3環進入0環,而真正的函數實現是在0環。
.text:7C92D9E0 mov eax, 0BAh ; NtReadVirtualMemory
.text:7C92D9E5 mov edx, 7FFE0300h ;決定了什么方式進0環
.text:7C92D9EA call dword ptr [edx]
.text:7C92D9EC retn 14h
我們自己在3環實現一個ReadProcessMemory ,自己實現的好處就在於 能夠防止被人來通過API斷點來分析我們的程序,對我們的程序進行hook檢測。
具體的實現如下:
#include<windows.h>
#include<stdio.h>
void __declspec(naked) read(HANDLE hProcess,LPVOID addr,LPVOID buffer,DWORD len)
{
_asm
{
mov eax, 0BAh
mov edx, 7FFE0300h
call dword ptr[edx]
ret
}
}
int main(int argc, char* argv[])
{
HWND hwnd = FindWindow("#32770","xxx");
DWORD pid;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
DWORD val = 0;
read(hProcess,(LPVOID)0x001D6EBC,&val,4);
printf("val: %d", val);
getchar();
return 0;
}
(二)API調用過程(3環進0環)
1、_KUSER_SHARED_DATA 結構
在用戶層和核心層分別定義了一個 _KUSER_SHARED_DATA 結構區域,用於用戶層和內核層共享某些數據。
它們使用固定的地址值映射,_KUSER_SHARED_DATA 結構區域在用戶層和內核層的線性地址分別為:
用戶層: 0x7ffe0000
內核層: 0xffdf0000
這兩個線性地址對應的物理頁是一樣的。如圖在windbg里查看這兩個地址的數據:
備注:雖然指向的是同一個物理頁,但在User層是只讀的,在Kernel層是可讀可寫的。
再來看看_KUSER_SHARED_DATA 這個結構,windbg 中輸入 dt _KUSER_SHARED_DATA
0: kd> dt _KUSER_SHARED_DATA
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
2、分析7FFE0300h進Ring0的方式
在之前分析的NtReadVirtualMemory中的代碼如下:
.text:7C92D9E0 mov eax, 0BAh ; NtReadVirtualMemory
.text:7C92D9E5 mov edx, 7FFE0300h ;決定了什么方式進0環
.text:7C92D9EA call dword ptr [edx]
.text:7C92D9EC retn 14h
當通過eax=1來執行cpuid指令時,處理器的特征信息被放在ecx和edx寄存器中,其中edx包含了一個SEP位,該位處於第11位,指明了當前CPU是否支持 sysenter、sysexit 指令,如果支持說明支持快速調用。
當SEP=1時,表明支持快速調用 , ntdll.dll!KiFastSystemCall()
當SEP=0時,表明不支持快速調用,ntdll.dll!KiIntSystemCall()。
7FFE0300h決定了以什么樣的方式進入Ring0,那么7FFE0300h到底是如何決定的呢?
接下來我們來拆解一下7FFE0300h:
7FFE0300
0111 1111 1111 1110 0000 0011 0000 0000
第11位是0,所以不支持快速調用。
進入0環需要修改的寄存器
1、CS的權限由Ring3->Ring0,CS會發生改變
2、SS與CS的權限一致
3、權限發生切換的時候,堆棧也一定會切換,需要新的ESP
4、進Ring0后的代碼位置
3、兩種進Ring0的方式
3.1 API通過中斷門進Ring0
如上所示的7FFE0300h就不支持快速調用,那么這種情況該如何進內核呢?
我們可以通過中斷門進Ring0:
.text:7C92E500 public KiIntSystemCall
.text:7C92E500 KiIntSystemCall proc near ; DATA XREF: .text:off_7C923428o
.text:7C92E500
.text:7C92E500 arg_4 = byte ptr 8
.text:7C92E500
.text:7C92E500 lea edx, [esp+arg_4] ;edx是參數指針,系統調用號在eax寄存器
.text:7C92E504 int 2Eh ; DOS 2+ internal - EXECUTE COMMAND
.text:7C92E504 ; DS:SI -> counted CR-terminated command string
.text:7C92E506 retn
.text:7C92E506 KiIntSystemCall endp
eax里的值是內核操作碼,edx存的是參數的指針,api統一中斷號0x2e,因為idt表在2e的位置。
3.2 通過int 0x2e進Ring0
具體步驟如下:
1、在IDT表中找到0x2e號的描述符
2、分析CS/SS/ESP/EIP的來源
3、分析EIP是什么
通過 >dq idtr L30 找到0x2e處的描述符,如圖所示:
0: kd> dq idtr L30
8003f400 80548e00`000831a0 80548e00`0008331c
8003f410 00008500`0058113e 8054ee00`00083730
8003f420 8054ee00`000838b0 80548e00`00083a10
8003f430 80548e00`00083b84 80548e00`000841fc
8003f440 00008500`00501198 80548e00`00084600
8003f450 80548e00`00084720 80548e00`00084860
8003f460 80548e00`00084ac0 80548e00`00084dac
8003f470 80548e00`000854a8 80548e00`000857e0
8003f480 80548e00`00085900 80548e00`00085a3c
8003f490 80548500`00a057e0 80548e00`00085ba4
8003f4a0 80548e00`000857e0 80548e00`000857e0
8003f4b0 80548e00`000857e0 80548e00`000857e0
8003f4c0 80548e00`000857e0 80548e00`000857e0
8003f4d0 80548e00`000857e0 80548e00`000857e0
8003f4e0 80548e00`000857e0 80548e00`000857e0
8003f4f0 80548e00`000857e0 806e8e00`0008710c
8003f500 00000000`00080000 00000000`00080000
8003f510 00000000`00080000 00000000`00080000
8003f520 00000000`00080000 00000000`00080000
8003f530 00000000`00080000 00000000`00080000
8003f540 00000000`00080000 00000000`00080000
8003f550 8054ee00`000829ce 8054ee00`00082ad0
8003f560 8054ee00`00082c80 8054ee00`0008360c
8003f570 8054ee00`00082451 80548e00`000857e0
如圖所示的最后一行的位置:8054ee00`00082451
0008就是CS,高4字節+低4字節就是EIP:80542451 ,SS和ESP是TSS提供的,如圖所示:
通過 >u 80542451 查看反匯編,如圖:
0: kd> u 80542451
nt!KiSystemService:
EIP:KiSystemService(),這個函數在內核模塊里:
1、固定中斷號為0x2e
2、CS/EIP由門描述符提供,ESP/SS由TSS提供
3、進入Ring0后執行內核函數: NT!KiSystemService
4、通過iret/iretd指令返回到用戶模式
3.3 sysenter進Ring0
通過快速調用進Ring0,分析KiFastSystemCall()函數
.text:7C92E4F0 public KiFastSystemCall
.text:7C92E4F0 KiFastSystemCall proc near ; DATA XREF: .text:off_7C923428o
.text:7C92E4F0 mov edx, esp ;edx 3環棧頂,系統調用號在eax寄存器
.text:7C92E4F2 sysenter ;寄存器數據傳遞
.text:7C92E4F2 KiFastSystemCall endp ; sp-analysis failed
eax是操作碼,edx是返回地址以及參數的指針。
1、為什么叫快速調用
中斷門進Ring0需要CS、EIP在IDT表中,需要查內存。
假設CPU支持sysenter指令時,OS會提前將CS、SS、EIP、ESP的值存儲在MSR寄存器中,sysenter指令執行時,CPU會將MSR寄存器中的值直接寫入相關的寄存器,沒有讀取內存的過程,所以叫快速調用。
2、在執行sysenter指令前,OS必須指定0環的CS段、SS段、EIP以及ESP。
MSR寄存器 | MSR地址 | 含義 |
---|---|---|
IA32_SYSENTER_CS | 174h | 低16位值指定了特權級0的代碼段和棧段的段選擇符 |
IA32_SYSENTER_ESP | 175h | 內核棧指針的32位偏移 |
IA32_SYSENTER_EIP | 176h | 目前例程的32位偏移 |
如上表所示:0x174位置存放的是新的CS,0x175位置存放的是新的ESP,0x176位置存放的是新的EIP。SS並沒有存在MSR寄存器里,執行sysenter后,CS+8就是SS。
可以通過RDMSR/WRMST來進行讀寫。
windbg查看這幾個值:
rdmsr 174 //查看CS
rdmsr 175 //查看ESP
rdmsr 176 //查看EIP
0: kd> rdmsr 174
msr[174] = 00000000`00000008
0: kd> rdmsr 175
msr[175] = 00000000`bacd0000
0: kd> rdmsr 176
msr[176] = 00000000`80542520
EIP:KiFastCallEntry()
0: kd> u 80542520
nt!KiFastCallEntry:
80542520 b923000000 mov ecx,23h
80542525 6a30 push 30h
80542527 0fa1 pop fs
80542529 8ed9 mov ds,cx
8054252b 8ec1 mov es,cx
8054252d 648b0d40000000 mov ecx,dword ptr fs:[40h]
80542534 8b6104 mov esp,dword ptr [ecx+4]
80542537 6a23 push 23h
用戶代碼調用sysenter指令以前,必須將要返回的指令地址和棧指針保存到edx和ecx寄存器中,否則,內核模式代碼將來無法設置正確的值,以使sysexit還能返回到用戶模式代碼原來的位置。
API通過sysenter指令進0環:
1、CS、ESP、EIP由MSR寄存器提供
2、進0環后執行的內核函數: NT!KiCallEntry
3、通過sysexit指令返回用戶模式
sysexit:
將IA32_SYSENTER_CS+16裝載到CS寄存器;將edx寄存器中的指針裝載到eip寄存器中;(指定用戶模式代碼段)
將IA32_SYSENTER_CS+24裝載到SS寄存器;將ecx寄存器中的指針裝載到esp寄存器中;(指定用戶模式棧段)
切換到特權級3,執行eip寄存器中指定的用戶代碼。
(三)API調用過程(保護現場)
為什么要保存現場?
要理解保存現場的必要性,首先得明白:
1、API調用過程中發生了什么?
內核API調用的過程,會涉及到進程的切換,如從3環切換到0環。
2、這些東西在調用前后有什么變化?
3、保存現場在這其中的作用?
如何保存現場?
先來熟悉一個結構:_KTrap_Frame 結構
0: kd> dt _KTrap_Frame
nt!_KTRAP_FRAME
//調試系統服務
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
//當需要調整棧時,以下作為臨時變量
+0x010 TempSegCs : Uint4B
+0x014 TempEsp : Uint4B
//調試寄存器
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
//段寄存器
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
//易失寄存器
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B
//非易失寄存器需要在中斷歷程中先保存
+0x048 PreviousPreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
//非易失寄存器
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
//硬件填充,通過中斷門進入的話,這個值就是NULL
+0x064 ErrCode : Uint4B
//中斷發生時保存被中斷的代碼段和地址,iret返回到此地址
+0x068 Eip : Uint4B //硬件填充
+0x06c SegCs : Uint4B //硬件填充
+0x070 EFlags : Uint4B //硬件填充
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B
+0x07c V86Es : Uint4B
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
無論是通過中斷門進入0環,還是通過快速調用,3環所有的寄存器都會保存在這個結構里。
通過快速調用是不需要68~78號寄存器的。
IDA打開ntkrnlpa.exe, 查看KiSystemService函數細節如下:
(如果找不到這個函數,那可能就是符號庫沒加載成功,可以將調試機上的pdb文件拷到對應的被調試機的ntkrnlpa.exe目錄)
.text:0046A451 _KiSystemService proc near ; CODE XREF: ZwAcceptConnectPort(x,x,x,x,x,x)+Cp
.text:0046A451 ; ZwAccessCheck(x,x,x,x,x,x,x,x)+Cp ...
.text:0046A451
.text:0046A451 arg_0 = dword ptr 4
.text:0046A451
.text:0046A451 push 0 ;保存ErrCode到esp0
.text:0046A453 push ebp ;保存ebp到esp0
.text:0046A454 push ebx ;保存ebx到esp0
.text:0046A455 push esi ;保存esi到esp0
.text:0046A456 push edi ;保存edi到esp0
.text:0046A457 push fs ;保存fs到esp
.text:0046A459 mov ebx, 30h ;FS寄存器,指向KPCR
.text:0046A45E mov fs, bx
權限發生切換時,堆棧也會發生改變。
內核層的ESP=TSS+4
Windows操作系統在每個處理器初始化時,會在GDT中為它構造一個TSS段,然后利用ltr指令設置處理器的任務環境段。
另外,Windows每次切換線程時,總會設置好TSS中0環的ESP,使其指向當前線程的內核棧。
保存3環寄存器值到_KTrap_Frame的0x50~0x64 的位置
將fs:0030做拆分,0030轉化成二進制 0000 0000 0011 0000,RPL=0,index=6,即查GDT表的第6組:【ffc093df`f0000001 】
ffdff000這個地址剛好指向的就是CPU的KPCR結構,fs在3環指向的事PEB結構,在0環指向的就是KPCR。
.text:0046A461 assume fs:nothing
.text:0046A461 push large dword ptr fs:0 ;保存老的ExceptionList,KPCR+0x00-> _NT_TIB -> ExceptionList
.text:0046A468 mov large dword ptr fs:0, 0FFFFFFFFh ;新的ExceptionList是空白的
.text:0046A473 mov esi, large fs:124h ;得到當前正在執行的線程信息:KPCR
.text:0046A47A push dword ptr [esi+140h] ;保存老的“先前模式”到堆棧
;KTHREAD
;+0x140 PreviousMode
.text:0046A480 sub esp, 48h ;ESP _KTRAP_FRAME 結構指針
.text:0046A483 mov ebx, [esp+68h+arg_0] ;取出3環壓入的參數CS _KTRAP_FRAME + 0x6C
.text:0046A487 and ebx, 1 ;0環最低為0, 3環最低為1
.text:0046A48A mov [esi+140h], bl ;新的先前模式
.text:0046A490 mov ebp, esp ;ESP==EDP _KTRAP_FRAME結構指針
.text:0046A492 mov ebx, [esi+134h] ;_KTHREAD中TrapFrame
.text:0046A498 mov [ebp+3Ch], ebx ;將_KTHREAD中的TrapFrame暫存在這個位置,后面會將這個值取出來,重新恢復給_KTHREAD中TrapFrame
Exceptionlist是異常鏈表
把老的Exceptionlist保存到_KTRAP_FRAME的 +0x4c位置,然后把它清0.
將KPCR+0x124放在esi中,也就是說將CurrentThread放在esi中。
將[esi+140]保存先前模式(_KTHREAD.PreviousMode )到_KTRAP_FRAME 的+0x48的位置。
再將堆棧提升0x48.
取出3環的CS放到ebx,然后將 esi+0x140=ebx &1,此時esi+0x140存儲的就是新的先前模式。
那么這里反復提到的先前模式到底有什么用呢?
當我們調用這段代碼的時候,如果是0環程序的先前模式就存0,3環就存1。OS通過先前模式就可以知道到底是哪一環的程序在調用自己。
提升棧底ebp與esp指向同一個位置 _KTRAP_FRAME 最開始的位置。
取出esi+0x134的位置是_KTHREAD.TrapFrame ,放在ebp+3c ???
.text:0046A49B mov [esi+134h], ebp ;將堆棧中形成的_KTRAP_FRAME的結構指針賦值給_KTHREAD中的TrapFrame
.text:0046A4A1 cld
.text:0046A4A2 mov ebx, [ebp+60h] ;3環的EBP
.text:0046A4A5 mov edi, [ebp+68h] ;3環的EIP
.text:0046A4A8 mov [ebp+0Ch], edx ;edx存儲的是3環參數的指針
_KTRAP_FRAME 放到 _KTHREAD.TrapFrame
把3環的ebp放到ebx
把3環的eip放到edi
把3環的參數指針放到 _KTRAP_FRAME.DbgArgPointer
.text:0046A4AB mov dword ptr [ebp+8], 0BADB0D00h
.text:0046A4B2 mov [ebp+0], ebx ;3環的ebp存儲到_KTRAP_FRAME +0x00 DbgEbp 的位置
.text:0046A4B5 mov [ebp+4], edi ;3環的eip存儲到_KTRAP_FRAME+0x004 DbgEip的位置
.text:0046A4B8 test byte ptr [esi+2Ch], 0FFh ;判斷_KTHREAD的+0x2c DebugActive 是否為-1
.text:0046A4BC jnz Dr_kss_a ;如果處於調試狀態,跳轉
.text:0046A4C2
.text:0046A4C2 loc_46A4C2: ; CODE XREF: Dr_kss_a+10j
.text:0046A4C2 ; Dr_kss_a+7Cj
.text:0046A4C2 sti ;關閉中斷
.text:0046A4C3 jmp loc_46A5AF ;取出從3環傳進來的系統調用號
.text:0046A4C3 _KiSystemService endp
0BADB0D00h 這是操作系統用到的一個標志,將它放到 _KTRAP_FRAME.DbgArgMark 。
ebp+0x00 = _KTRAP_FRAME.DbgEbp
ebp+0x04 = _KTRAP_FRAME.DbgEip
比較 _KTHREAD.DebugActive ,判斷當前線程是否處於調試狀態,如果是這個值就不是0ff。處於調試狀態中的代碼就會跳轉到下面的代碼。
.text:0046A34C Dr_kss_a proc near ; CODE XREF: _KiSystemService+6Bj
.text:0046A34C test dword ptr [ebp+70h], 20000h ;是否是虛擬8086模式(Eflags標志寄存器的VM位)
.text:0046A353 jnz short loc_46A362 ;不是跳轉
.text:0046A355 test dword ptr [ebp+6Ch], 1
.text:0046A35C jz loc_46A4C2 ;關閉中斷
.text:0046A362
.text:0046A362 loc_46A362: ; CODE XREF: Dr_kss_a+7j
.text:0046A362 mov ebx, dr0
.text:0046A365 mov ecx, dr1
.text:0046A368 mov edi, dr2
.text:0046A36B mov [ebp+18h], ebx ;存儲Dr0寄存器到 _KTRAP_FRAME+0x18
.text:0046A36E mov [ebp+1Ch], ecx ;存儲Dr1寄存器到 _KTRAP_FRAME+0x1C
.text:0046A371 mov [ebp+20h], edi ;存儲Dr0寄存器到 _KTRAP_FRAME+0x20
.text:0046A374 mov ebx, dr3
.text:0046A377 mov ecx, dr6
.text:0046A37A mov edi, dr7
.text:0046A37D mov [ebp+24h], ebx ;存儲Dr3寄存器到_KTRAP_FRAME
.text:0046A380 mov [ebp+28h], ecx ;存儲Dr6寄存器到_KTRAP_FRAME
.text:0046A383 xor ebx, ebx
.text:0046A385 mov [ebp+2Ch], edi ;存儲Dr7寄存器到_KTRAP_FRAME
.text:0046A388 mov dr7, ebx ;將Dr7寄存器清零
.text:0046A38B mov edi, large fs:20h ;得到_KPRCD指針
.text:0046A392 mov ebx, [edi+2F8h]
.text:0046A398 mov ecx, [edi+2FCh]
.text:0046A39E mov dr0, ebx
.text:0046A3A1 mov dr1, ecx
就是dr0~dr7存到_KTRAP_FRAME +0x18~+0x2c 。
如果當前線程的狀態不處於調試的狀態,就會轉到下面這段代碼:
.text:0046A5AF loc_46A5AF: ; CODE XREF: _KiBBTUnexpectedRange+18j
.text:0046A5AF ; _KiSystemService+72j
.text:0046A5AF mov edi, eax
.text:0046A5B1 shr edi, 8
.text:0046A5B4 and edi, 30h
.text:0046A5B7 mov ecx, edi
.text:0046A5B9 add edi, [esi+0E0h]
.text:0046A5BF mov ebx, eax
.text:0046A5C1 and eax, 0FFFh
.text:0046A5C6 cmp eax, [edi+8]
.text:0046A5C9 jnb _KiBBTUnexpectedRange
.text:0046A5CF cmp ecx, 10h
.text:0046A5D2 jnz short loc_46A5EF
.text:0046A5D4 mov ecx, large fs:18h
.text:0046A5DB xor ebx, ebx
.text:0046A5DD
.text:0046A5DD loc_46A5DD: ; DATA XREF: _KiTrap0E+117o
.text:0046A5DD or ebx, [ecx+0F70h]
.text:0046A5E3 jz short loc_46A5EF
.text:0046A5E5 push edx
.text:0046A5E6 push eax
.text:0046A5E7 call ds:_KeGdiFlushUserBatch
.text:0046A5ED pop eax
.text:0046A5EE pop edx
.text:0046A5EF
.text:0046A5EF loc_46A5EF: ; CODE XREF: _KiFastCallEntry+B2j
.text:0046A5EF ; _KiFastCallEntry+C3j
我們跟蹤代碼發現,無論是KiFastCallEntry 還是 KisystenService ,最終都會調用一下這個代碼。
kiSystenService:
進入0環后原來3環的寄存器保存在_KTrap_Frame的0x50~0x64
把3環的參數指針放到_KTRAP_FRAME.DbgArgPointer
以上過程是kiSystenService從0環進入3環的填表過程。
KiFastCallEntry的填表過程
.text:0046A520 mov ecx, 23h
.text:0046A525 push 30h
.text:0046A527 pop fs ;修改fs寄存器為30
.text:0046A529 mov ds, ecx
.text:0046A52B mov es, ecx
.text:0046A52D mov ecx, large fs:40h ;獲取當前TSS
.text:0046A534 mov esp, [ecx+4] ;TSS中得到ESP
.text:0046A537 push 23h ;原SS壓棧
.text:0046A539 push edx ;原ESP壓棧
.text:0046A53A pushf ;EFLAGS壓棧
TSS = KPCR+40,ESP0=TSS+4(0表示0環)
push到 _KTrap_Frame
+0x078 HardwareSegSs : Uint4B
+0x074 HardwareEsp : Uint4B
+0x070 EFlags : Uint4B
完整的IDA分析代碼如下:
.text:0046A520
.text:0046A520 mov ecx, 23h
.text:0046A525 push 30h
.text:0046A527 pop fs ;修改fs寄存器為30
.text:0046A529 mov ds, ecx
.text:0046A52B mov es, ecx
.text:0046A52D mov ecx, large fs:40h ;獲取當前TSS
.text:0046A534 mov esp, [ecx+4] ;TSS中得到ESP
.text:0046A537 push 23h ;原ss壓棧
.text:0046A539 push edx ;原esp壓棧
.text:0046A53A pushf ;EFLAGS壓棧
.text:0046A53B
.text:0046A53B loc_46A53B: ; CODE XREF: _KiFastCallEntry2+23j
.text:0046A53B push 2
.text:0046A53D add edx, 8 ;當前保存着systener進入前的esp的值,esp+8=參數指針
.text:0046A540 popf
.text:0046A541 or byte ptr [esp+1], 2 ;KtrapFrame->Eflags.if = 1
.text:0046A546 push 1Bh ; KtrapFrame->CS=0x1b 保存r3的cs
.text:0046A548 push dword ptr ds:0FFDF0304h ; KtrapFrame->EIP =返回地址
.text:0046A54E push 0 ; KtrapFrame->Error = 0
.text:0046A550 push ebp ; KtrapFrame->ebp = ebp
.text:0046A551 push ebx ; KtrapFrame->ebx = ebx
.text:0046A552 push esi ; KtrapFrame->esi = esi
.text:0046A553 push edi ; KtrapFrame->edi = edi
.text:0046A554 mov ebx, large fs:1Ch
.text:0046A55B push 3Bh ; KtrapFrame->SegFs = 0x3b 保存r3的fs
.text:0046A55D mov esi, [ebx+124h] ; 得到當前線程結構
.text:0046A563 push dword ptr [ebx] ; KtrapFrame->0x4c 保存原異常鏈
.text:0046A565 mov dword ptr [ebx], 0FFFFFFFFh ; 設置為空的異常鏈
.text:0046A56B mov ebp, [esi+18h] ; 得到初始堆棧KtrapFrame->0x48
.text:0046A56E push 1 ; KtrapFrame->PreviousPreviousMode = 1
.text:0046A570 sub esp, 48h ; 提升棧頂指針到_Ktrap_Frame
.text:0046A573 sub ebp, 29Ch ; Ktrap_Frame
.text:0046A579 mov byte ptr [esi+140h], 1 ; 先前模式
.text:0046A580 cmp ebp, esp
.text:0046A582 jnz short loc_46A511
.text:0046A584 and dword ptr [ebp+2Ch], 0 ; 清零dr7
.text:0046A588 test byte ptr [esi+2Ch], 0FFh ; 檢查是否處於調試狀態
.text:0046A58C mov [esi+134h], ebp
.text:0046A592 jnz Dr_FastCallDrSave
.text:0046A598
.text:0046A598 loc_46A598: ; CODE XREF: Dr_FastCallDrSave+10j
.text:0046A598 ; Dr_FastCallDrSave+7Cj
.text:0046A598 mov ebx, [ebp+60h] ; ebp = KtrapFrame->ebp
.text:0046A59B mov edi, [ebp+68h] ; edi = KtrapFrame->eip
.text:0046A59E mov [ebp+0Ch], edx ; KtrapFrame->DbgArgPointer = 參數指針
.text:0046A5A1 mov dword ptr [ebp+8], 0BADB0D00h
.text:0046A5A8 mov [ebp+0], ebx ; KtrapFrame->DbgEbp = ebx
.text:0046A5AB mov [ebp+4], edi ; KtrapFrame->DbgEip = edi
.text:0046A5AE sti
(四)API調用(系統服務表)
1、前言
API從Ring3到Ring0需要帶兩個寄存器:eax,edx。其中eax保存的是系統的服務號,edx保存的事Ring3的esp,我們可以通過esp找到三環的參數。
那我們該如何找到Ring0的參數呢?
本篇主要解決的是,如何通過eax找到Ring0的函數,Ring0的函數是如何被調用的,並且在這個過程中,Ring0的函數是如何使用Ring3的參數的?
2、系統服務表
在OS內核里,有一張系統服務表(SystemServiceTable),如下圖:
在這里有4個成員分別為:
-
ServiceTable 這個成員是個地址,通過這個地址可以找到一個函數地址表,從三環進0環的eax服務號就是函數地址表的索引。
-
count當前的系統服務表被調用了多少次。
-
ServiceLimit 保存的是函數地址表的大小,即服務函數的個數。
-
ArgmentTable 函數參數表,里面保存的是函數地址表對應的參數個數。
-
總共有兩份系統服務表,其中一份函數來自於Ntoskrl.exe 內核模塊的導出函數,里面保存常用的系統服務;另一份來自Win32k.sys的導出函數,里面是與圖形和用戶界面相關的系統服務。它們向三環提供的內核函數全在這兩張表里。
3、如何找到系統服務表
系統服務表位於_KTHREAD結構體0xE0的位置。
4、判斷調用的函數在哪個表
-
要找到兩張表取決於eax系統服務號,這個系統服務號總共有32位,但是真正只使用了13位。
-
系統服務號在使用的時候分為兩部分,低12位表示的是函數地址表的索引,下標為12的位置的值,決定使用哪張表。
-
如果第12位為0,則找第一張表(圖中的白色區域);如果第12位為1,則找第二張表(圖中的灰色區域)。
5、分析API函數的調用過程
通過以上的系統服務表分析,思考一下0環代碼是如何通過服務號找到0環的函數的?0環的函數是如何使用在三環的參數的?
用IDA打開 ntkrnlpa.exe ,找到 KiFastCallEntry 、KiSystemService 函數,KiFastCallEntry 和 KiSystemService前面的代碼都是用於保存現場,代碼如下:
保存現場后,取出3環傳進來的系統調用號:
.text:0046A5AF mov edi, eax ;取出3環傳進來的系統調用號
首先將3環傳遞過來的系統調用號保存到edi里:
.text:0046A5B1 shr edi, 8 ;系統調用號右移8位
.text:0046A5B4 and edi, 30h ;判斷調用號的第12位是0還是1
.text:0046A5B7 mov ecx, edi ;ecx存儲的值是00 或 0x10
然后將系統調用號右移8位,然后再和0x30做與運算。此時,edi的結果只能有兩種,要么是0x0,要么是0x10。如果是0的話,就說明調用號下標為12的位置是0,如果edi的結果是0x10,那么調用號下標為12的位置是1。
.text:0046A5B9 add edi, [esi+0E0h] ;edi指向KTHREAD-->ServiceTable
esi指向的是ETHREAD線程結構體,ETHREAD+0xE位置存的就是ServiceTable。
當edi=0時,ServiceTable+edi 指向的就是第一張ServiceTable;
當edi=0x10時,ServiceTable+edi 指向的就是第二張ServiceTable。
.text:0046A5BF mov ebx, eax ;把三環的系統服務號存到ebx備份
.text:0046A5C1 and eax, 0FFFh ;系統調用號,只保留后12位
接着把系統調用號備份到ebx,然后 &0FFFh,保留后12位。
.text:0046A5C6 cmp eax, [edi+8]
.text:0046A5C9 jnb _KiBBTUnexpectedRange
[edi+8]存的是ServiceLimit(服務函數個數),如果傳入的調用號的低12位>ServiceLimit,就jmp到_KiBBTUnexpectedRange。 這里是判斷是否越界,即要找的函數地址有沒有超過函數地址表的范圍,如果沒有越界就繼續往下走。
.text:0046A5CF cmp ecx, 10h ;判斷是否是查第二張系統服務表
.text:0046A5D2 jnz short loc_46A5EF ;如果是查第一張系統服務表,則jmp
這里將ecx和0x10進行比較,ecx存儲的是 0x00或0x10,如果ecx是0x10,就說明要查第二張系統服務表。
如果要查第一張服務表,就會jmp;如果查第一張表則繼續往下執行。
.text:0046A5D4 mov ecx, large fs:18h
.text:0046A5DB xor ebx, ebx
.text:0046A5DD
.text:0046A5DD loc_46A5DD: ; DATA XREF: _KiTrap0E+117o
.text:0046A5DD or ebx, [ecx+0F70h]
.text:0046A5E3 jz short loc_46A5EF
.text:0046A5E5 push edx
.text:0046A5E6 push eax
.text:0046A5E7 call ds:_KeGdiFlushUserBatch
.text:0046A5ED pop eax
.text:0046A5EE pop edx
調用_KeGdiFlushUserBatch函數后,假設我們查找的是第一張系統服務表
.text:0046A5EF ; _KiFastCallEntry+C3j
.text:0046A5EF inc large dword ptr fs:638h ; _KPCRB->0x518 KeSystemCall增加1
.text:0046A5F6 mov esi, edx ; edx存儲的三環的參數指針
這里將edx保存到esi,edx存儲的是三環的參數指針。
.text:0046A5F8 mov ebx, [edi+0Ch] ;ebx-->參數起始位置
edi->系統服務表的起始位置,[edi+0Ch]存的是ParamTableBase 參數表的基址 ,ebx-->參數起始位置。
.text:0046A5FD mov cl, [eax+ebx] ;eax->函數地址表索引,ebx->參數表的起始位置 ,cl->參數的個數
參數表的基址+函數地址表的索引,再取得到的值就是要調用的函數的參數個數。
.text:0046A600 mov edi, [edi] ;edi->系統服務表,第一個成員是函數地址表
取出函數地址表,放到edi。
.text:0046A602 mov ebx, [edi+eax*4] ;ebx->零環的函數地址
edi為函數地址表+eax索引*4,實現將0環的函數地址存到ebx。
.text:0046A605 sub esp, ecx ;提升堆棧高度到cl
為了能夠存儲3環的參數,提升堆棧到cl(參數個數)。
.text:0046A607 shr ecx, 2 ;參數總長度/4 = 參數的個數
.text:0046A60A mov edi, esp
.text:0046A60C cmp esi, ds:_MmUserProbeAddress
.text:0046A612 jnb loc_46A7C0
.text:0046A618
.text:0046A618 loc_46A618: ; CODE XREF: _KiFastCallEntry+2A4j
.text:0046A618 ; DATA XREF: _KiTrap0E+10Do
.text:0046A618 rep movsd ;開始拷貝參數
.text:0046A61A call ebx
將ecx右移2位,即將ecx/4=參數的個數。我們知道rep的次數,取決於ecx,而movsd每次復制4個字節,所以需要將ecx/4。
.text:0046A60C cmp esi, ds:_MmUserProbeAddress ;判斷3環參數的地址范圍是否越界
.text:0046A612 jnb loc_46A7C0 ;越界跳轉到錯誤處理模塊
_MmUserProbeAddress是一個全局變量,是用戶程序能訪問地址的最大范圍。esi指向的是3環的函數指針,將esi與_MmUserProbeAddress比較,是為了判斷3環參數的地址范圍是否越界,如果越界則跳轉到錯誤處理模塊。
.text:0046A61A call ebx ;調用函數
最后將3環的參數賦值到0環,開始真正的內核函數。
(五)API函數的調用過程(SSDT)
在前一章,我們逆向分析得出:KTHREAD +0xE0 -->系統服務表。實際上,Windows提供了一個到處的全局變量,通過這個變量,就可以直接訪問系統服務表。
1、分析KeServiceDescriptorTable
- IDA打開ntkrnlpa.exe ,在export中搜索KeServiceDescriptorTable
- 在Windbg中查看這個變量
2
這就是所謂的SSDT了。
2、系統服務描述表,SSDT
SSDT(System Service Despcriptor Table,系統服務描述表),SSDT結構包含4個成員,這4個成員都是一個系統服務表的結構體。
接下來,在windbg里看一下每個成員:
3
用SSDT查看只有一張表,這個表就是Ntoskrl.exe![]
導出的,后面的3個成員都是空的。
3、SSDT Shadow
通過SSSDT就能同時看到Ntoskrl.exe與win32k.sys導出的表 。
4
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; //這個指向系統服務函數地址表
PULONG ServiceCounterTableBase; //系統這個服務表調用了幾次
ULONG NumberOfService; //服務函數的個數
PULONG ParamTableBase; //參數表
}KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
PSYSTEM_SERVICE_TABLE Ntoskr_ssdt;
//這4個參數就是對應第一個系統服務表導出的值
Ntoskr_ssdt->ServiceTableBase = 80505450;
Ntoskr_ssdt->ServiceCounterTableBase = 00000000;
Ntoskr_ssdt->NumberOfService = 0000011c;
Ntoskr_ssdt->ParamTableBase = 805058c4;
系統服務表基址+0xBA--->ReadVirtualMemory:
查看ReadVirtualMemory函數反匯編:
查看參數表
0x14的十進制是20,它當前所有的參數加起來有20個字節,每個參數4字節,所以它有5個參數。