1. 程式人生 > WINDOWS開發 >API呼叫過程

API呼叫過程

目錄

一、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,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)+Cp
.text:0046A451                                         ; ZwAccessCheck(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,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,[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個引數。