寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。
看此教程之前,問一個問題,你明確學系統呼叫的目的了嗎? 沒有的話就不要繼續了,請重新學習 羽夏看Win系統核心——系統呼叫篇 裡面的內容。
? 華麗的分割線 ?
Windows API
API
全稱為Application Programming Interface
,至於概念我就不多說了。下面我將介紹幾個比較重要的Dll
,我們呼叫的很多重要的函式都在這些動態連結庫裡面:
Kernel32.dll
:最核心的功能模組,比如管理記憶體、程式和執行緒相關的函式等。User32.dll
:是Windows
使用者介面相關應用程式介面,如建立視窗和傳送訊息等。GDI32.dll
:全稱是Graphical Device Interface
,即圖形裝置介面
,包含用於畫圖和顯示文字的函式.比如要顯示一個程式視窗,就呼叫了其中的函式來畫這個視窗。Ntdll.dll
:大多數API
都會通過這個DLL
進入核心(0環)。
這裡提一句,並不是所有的API
必須進0環的,可以在3環完全實現。比如Ntdll.dll
匯出的memcmp
函式,感興趣的自己可以逆向一下。有關API
在3環層面呼叫過程將以我們最常用的ReadProcessMemory
這個函式來進行講解。
函式解析
ReadProcessMemory
這個函式由Kernel32.dll
匯出,然後我們拖到IDA
進行分析。至於怎麼用IDA
分析不會的話,請參考前面的教程(我也忘了在那篇文章寫過了)。我們在IDA
中定位到這個函式:
.text:7C8021D0 ; BOOL __stdcall ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead)
.text:7C8021D0 public _ReadProcessMemory@20
.text:7C8021D0 _ReadProcessMemory@20 proc near ; CODE XREF: GetProcessVersion(x)+2F12F↓p
.text:7C8021D0 ; GetProcessVersion(x)+2F14E↓p ...
.text:7C8021D0
.text:7C8021D0 hProcess = dword ptr 8
.text:7C8021D0 lpBaseAddress = dword ptr 0Ch
.text:7C8021D0 lpBuffer = dword ptr 10h
.text:7C8021D0 nSize = 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+nSize]
.text:7C8021D8 push eax ; NumberOfBytesRead
.text:7C8021D9 push [ebp+nSize] ; NumberOfBytesToRead
.text:7C8021DC push [ebp+lpBuffer] ; Buffer
.text:7C8021DF push [ebp+lpBaseAddress] ; BaseAddress
.text:7C8021E2 push [ebp+hProcess] ; ProcessHandle
.text:7C8021E5 call ds:__imp__NtReadVirtualMemory@20 ; NtReadVirtualMemory(x,x,x,x,x)
.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(x,x,x,x,x)+32↓j
.text:7C8021F2 test eax, eax
.text:7C8021F4 jl short loc_7C802204
.text:7C8021F6 xor eax, eax
.text:7C8021F8 inc eax
.text:7C8021F9
.text:7C8021F9 loc_7C8021F9: ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+3C↓j
.text:7C8021F9 pop ebp
.text:7C8021FA retn 14h
.text:7C8021FD ; ---------------------------------------------------------------------------
.text:7C8021FD
.text:7C8021FD loc_7C8021FD: ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+20↑j
.text:7C8021FD mov edx, [ebp+nSize]
.text:7C802200 mov [ecx], edx
.text:7C802202 jmp short loc_7C8021F2
.text:7C802204 ; ---------------------------------------------------------------------------
.text:7C802204
.text:7C802204 loc_7C802204: ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+24↑j
.text:7C802204 push eax ; Status
.text:7C802205 call _BaseSetLastNTError@4 ; BaseSetLastNTError(x)
.text:7C80220A xor eax, eax
.text:7C80220C jmp short loc_7C8021F9
.text:7C80220C _ReadProcessMemory@20 endp
從上面的程式碼可知,這個函式啥也沒做,只是呼叫了NtReadVirtualMemory
這個函式去實現讀取記憶體。我們跟過去看看:
.idata:7C801418 ; NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
.idata:7C801418 extrn __imp__NtReadVirtualMemory@20:dword
不幸的是,這個函式是人家匯入的,如何查到從哪裡匯入的呢?我們可以按照如下圖所示的操作找到:
我們知道NtReadVirtualMemory
這個函式是來自ntdll.dll
。然後我們重新定位到IDA
的位置:
.text:7C92D9E0 ; __stdcall NtReadVirtualMemory(x, x, x, x, x)
.text:7C92D9E0 public _NtReadVirtualMemory@20
.text:7C92D9E0 _NtReadVirtualMemory@20 proc near ; CODE XREF: LdrFindCreateProcessManifest(x,x,x,x,x)+1CC↓p
.text:7C92D9E0 ; LdrCreateOutOfProcessImage(x,x,x,x)+7C↓p ...
.text:7C92D9E0 mov eax, 0BAh ; NtReadVirtualMemory
.text:7C92D9E5 mov edx, 7FFE0300h
.text:7C92D9EA call dword ptr [edx]
.text:7C92D9EC retn 14h
.text:7C92D9EC _NtReadVirtualMemory@20 endp
我們發現這個函式給eax
賦個值,然後給edx
個地址,然後call
一下地址的內容,然後就平棧(由於STDCALL
呼叫約定)返回了。至此,你或許就看不懂了。我們來看看這個地址到底存著什麼。
_KUSER_SHARED_DATA
當你看到這個時,你猜測這個地址儲存的是_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
在User
層和Kernel
層分別定義了一個_KUSER_SHARED_DATA
結構區域,用於User
層和Kernel
層共享某些資料。它們使用固定的地址值對映,_KUSER_SHARED_DATA
結構區域在User
層地址為0x7ffe0000
,在Kernel
層地址為0xffdf0000
。雖然它們指向的是同一個物理頁,但在User
層是隻讀的,在Kernnel
層是可寫的,通過頁的限制保證在3環的安全性。因為裡面有幾個成員是十分重要的,有一個成員就是3環API
進入核心的入口。
根據0x7FFE0300
這個地址,我們不難看出它是在呼叫SystemCall
裡面的程式碼,接下來看看這個函式到底是幹啥的。
我們先!process 0 0
遍歷一下程式:
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
(部分程式快照略……)
Failed to get VadRoot
PROCESS 896ffda0 SessionId: 0 Cid: 0a7c Peb: 7ffde000 ParentCid: 08bc
DirBase: 16840680 ObjectTable: e1ac9078 HandleCount: 36.
Image: cmd.exe
我們想要讀取0x7FFE0300
這個地址的內容,這個地址是3環應用的地址。如果讀取某個程式的記憶體,必須有它的CR3
,即和這個程式關聯起來,我們需要.process + PROCESS 的地址
進行:
kd> .process 896ffda0
ReadVirtual: 896ffdb8 not properly sign extended
Implicit process is now 896ffda0
WARNING: .cache forcedecodeuser is not enabled
然後我們dd
一下這兩個地址,看看內容是否一樣:
kd> dd 0x7ffe0000
7ffe0000 000f3594 0a03afb7 3daf17c0 00000017
7ffe0010 00000017 8b7792b3 01d7d56a 01d7d56a
7ffe0020 f1dcc000 ffffffbc ffffffbc 014c014c
7ffe0030 003a0043 0057005c 004e0049 004f0044
7ffe0040 00530057 00000000 00000000 00000000
7ffe0050 00000000 00000000 00000000 00000000
7ffe0060 00000000 00000000 00000000 00000000
7ffe0070 00000000 00000000 00000000 00000000
kd> dd 0xffdf0000
ReadVirtual: ffdf0000 not properly sign extended
ffdf0000 000f3594 0a03afb7 3daf17c0 00000017
ffdf0010 00000017 8b7792b3 01d7d56a 01d7d56a
ffdf0020 f1dcc000 ffffffbc ffffffbc 014c014c
ffdf0030 003a0043 0057005c 004e0049 004f0044
ffdf0040 00530057 00000000 00000000 00000000
ffdf0050 00000000 00000000 00000000 00000000
ffdf0060 00000000 00000000 00000000 00000000
ffdf0070 00000000 00000000 00000000 00000000
既然內容是一樣的,我們再看看它們的物理頁是不是一樣的:
kd> !vtop 16840680 0x7ffe0000
X86VtoP: Virt 000000007ffe0000, pagedir 0000000016840680
X86VtoP: PAE PDPE 0000000016840688 - 00000000823e5001
X86VtoP: PAE PDE 00000000823e5ff8 - 00000000814bf067
X86VtoP: PAE PTE 00000000814bff00 - 0000000000041025
X86VtoP: PAE Mapped phys 0000000000041000
Virtual address 7ffe0000 translates to physical address 41000.
kd> !vtop 16840680 0xffdf0000
X86VtoP: Virt 00000000ffdf0000, pagedir 0000000016840680
X86VtoP: PAE PDPE 0000000016840698 - 00000000823e3001
X86VtoP: PAE PDE 00000000823e3ff0 - 0000000000af3163
X86VtoP: PAE PTE 0000000000af3f80 - 0000000000041163
X86VtoP: PAE Mapped phys 0000000000041000
Virtual address ffdf0000 translates to physical address 41000.
!vtop
這個指令可以幫我們拆分虛擬地址到實體地址。為什麼不在段頁的部分講是因為怕你懶,缺少練習。可以驗證它們的物理頁是一樣的。
我們先看看0xffdf0300
這個地址裡面存的是什麼,先dd
一下:
kd> dd 0xffdf0300
ffdf0300 7c92e4f0 7c92e4f4 00000000 00000000
ffdf0310 00000000 00000000 00000000 00000000
ffdf0320 00000000 00000000 00000000 00000000
ffdf0330 43dc3855 00000000 00000000 00000000
ffdf0340 00000000 00000000 00000000 00000000
ffdf0350 00000000 00000000 00000000 00000000
ffdf0360 00000000 00000000 00000000 00000000
ffdf0370 00000000 00000000 00000000 00000000
然後我們uf
一下看看彙編:
kd> uf 7c92e4f0
7c92e4f0 8bd4 mov edx,esp
7c92e4f2 0f34 sysenter
7c92e4f4 c3 ret
可以發現,這個函式只是把esp
的值交給了edx
,然後呼叫sysenter
。這個彙編就是快速呼叫。為什麼叫快速呼叫?中斷門進0環,需要的CS
、EIP
在IDT
表中,需要查記憶體(SS
與ESP
由TSS
提供),而CPU
如果支援sysenter
指令時,作業系統會提前將CS
/SS
/ESP
/EIP
的值儲存在MSR
暫存器中,sysenter
指令執行時,CPU
會將MSR
暫存器中的值直接寫入相關暫存器,沒有讀記憶體的過程,所以叫快速呼叫,但本質是一樣的。
其實,快速呼叫並不是一直存在的,在比較古老的CPU
是不支援快速呼叫的。它們進入核心的方式很簡單粗暴,就是使用中斷門。
CPU
如何知道是否支援快速呼叫呢?當通過eax=1
來執行cpuid
指令時,處理器的特徵資訊被放在ecx
和edx
暫存器中,其中edx
包含了一個SEP位(11位)
,該位指明瞭當前處理器知否支援sysenter
/sysexit
指令,具體細節可以檢視白皮書。
通過逆向彙編程式碼可以看出,不管CPU是否支援快速呼叫,它都是呼叫該地址。這就說明作業系統在初始化該結構體的時候必須先判斷支不支援,然後填入適當的值。如果CPU
支援快速呼叫,作業系統就會填入KiFastSystemCall
函式的地址,我們可以看一下:
.text:7C92E4F0 ; _DWORD __stdcall KiFastSystemCall()
.text:7C92E4F0 public _KiFastSystemCall@0
.text:7C92E4F0 _KiFastSystemCall@0 proc near ; DATA XREF: .text:off_7C923428↑o
.text:7C92E4F0 mov edx, esp
.text:7C92E4F2 sysenter
.text:7C92E4F2 _KiFastSystemCall@0 endp
如果CPU
不支援快速呼叫,作業系統就會填入KiIntSystemCall
函式的地址,我們可以看一下:
.text:7C92E500 ; _DWORD __stdcall KiIntSystemCall()
.text:7C92E500 public _KiIntSystemCall@0
.text:7C92E500 _KiIntSystemCall@0 proc near ; DATA XREF: .text:off_7C923428↑o
.text:7C92E500
.text:7C92E500 arg_4 = byte ptr 8
.text:7C92E500
.text:7C92E500 lea edx, [esp+arg_4] ;引數指標
.text:7C92E504 int 2Eh ; DOS 2+ internal - EXECUTE COMMAND
.text:7C92E504 ; DS:SI -> counted CR-terminated command string
.text:7C92E506 retn
.text:7C92E506 _KiIntSystemCall@0 endp
.text:7C92E506
本篇內容就先講解這麼多,進入0環的部分將在下一篇進行講解。接下來我們將用程式碼重寫ReadProcessMemory
的3環部分,程式碼如下:
#include "stdafx.h"
#include <windows.h>
#include <iostream>
const int test=0x1234;
BOOL __declspec(naked) __stdcall ReadProcMem0(DWORD handle,DWORD addr,unsigned char* buffer,DWORD len,DWORD sizeread)
{
_asm
{
mov eax, 0BAh ;
mov edx, 7FFE0300h;
call dword ptr [edx];
retn 14h;
}
}
BOOL __declspec(naked) __stdcall ReadProcMem1(DWORD handle,DWORD addr,unsigned char* buffer,DWORD len,DWORD sizeread)
{
_asm
{
mov eax, 0BAh;
lea edx, [esp+4];
int 2Eh;
retn 14h;
}
}
int main(int argc, char* argv[])
{
int buffer = 0;
ReadProcMem0((DWORD)GetCurrentProcess(),(DWORD)&test,(unsigned char*)&buffer,4,NULL);
printf("第一次 buffer的值為:%x\n",buffer);
buffer=0;
ReadProcMem1((DWORD)GetCurrentProcess(),(DWORD)&test,(unsigned char*)&buffer,4,NULL);
printf("第二次 buffer的值為:%x\n",buffer);
system("pause");
return 0;
}
從上面的程式碼可以看出ReadProcMem0
是還通過SystemCall
進0環,ReadProcMem1
直接重寫了SystemCall
進入0環(為什麼沒用sysenter
?編譯不通過)。如下是結果:
第一次 buffer的值為:1234
第二次 buffer的值為:1234
請按任意鍵繼續. . .
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。
1️⃣ 自己編寫WriteProcessMemory
函式(不使用任何DLL
,直接呼叫0環函式)並在程式碼中使用。
下一篇
系統呼叫篇——0環層面呼叫過程