系統調用篇——3環層面調用過程


寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看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中定位到這個函數:

; BOOL __stdcall ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead)
                public _ReadProcessMemory@20
_ReadProcessMemory@20 proc near         ; CODE XREF: GetProcessVersion(x)+2F12F↓p
                                        ; GetProcessVersion(x)+2F14E↓p ...

hProcess        = dword ptr  8
lpBaseAddress   = dword ptr  0Ch
lpBuffer        = dword ptr  10h
nSize           = dword ptr  14h
lpNumberOfBytesRead= dword ptr  18h

                mov     edi, edi
                push    ebp
                mov     ebp, esp
                lea     eax, [ebp+nSize]
                push    eax             ; NumberOfBytesRead
                push    [ebp+nSize]     ; NumberOfBytesToRead
                push    [ebp+lpBuffer]  ; Buffer
                push    [ebp+lpBaseAddress] ; BaseAddress
                push    [ebp+hProcess]  ; ProcessHandle
                call    ds:__imp__NtReadVirtualMemory@20 ; NtReadVirtualMemory(x,x,x,x,x)
                mov     ecx, [ebp+lpNumberOfBytesRead]
                test    ecx, ecx
                jnz     short loc_7C8021FD

loc_7C8021F2:                           ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+32↓j
                test    eax, eax
                jl      short loc_7C802204
                xor     eax, eax
                inc     eax

loc_7C8021F9:                           ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+3C↓j
                pop     ebp
                retn    14h
; ---------------------------------------------------------------------------

loc_7C8021FD:                           ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+20↑j
                mov     edx, [ebp+nSize]
                mov     [ecx], edx
                jmp     short loc_7C8021F2
; ---------------------------------------------------------------------------

loc_7C802204:                           ; CODE XREF: ReadProcessMemory(x,x,x,x,x)+24↑j
                push    eax             ; Status
                call    _BaseSetLastNTError@4 ; BaseSetLastNTError(x)
                xor     eax, eax
                jmp     short loc_7C8021F9
_ReadProcessMemory@20 endp

  從上面的代碼可知,這個函數啥也沒做,只是調用了NtReadVirtualMemory這個函數去實現讀取內存。我們跟過去看看:

NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
                extrn __imp__NtReadVirtualMemory@20:dword

  不幸的是,這個函數是人家導入的,如何查到從哪里導入的呢?我們可以按照如下圖所示的操作找到:

  我們知道NtReadVirtualMemory這個函數是來自ntdll.dll。然后我們重新定位到IDA的位置:

; __stdcall NtReadVirtualMemory(x, x, x, x, x)
                public _NtReadVirtualMemory@20
_NtReadVirtualMemory@20 proc near       ; CODE XREF: LdrFindCreateProcessManifest(x,x,x,x,x)+1CC↓p
                                        ; LdrCreateOutOfProcessImage(x,x,x,x)+7C↓p ...
                mov     eax, 0BAh       ; NtReadVirtualMemory
                mov     edx, 7FFE0300h
                call    dword ptr [edx]
                retn    14h
_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
ntdll!KiFastSystemCall:
7c92e4f0 8bd4            mov     edx,esp
7c92e4f2 0f34            sysenter
7c92e4f4 c3              ret

  可以發現,這個函數只是把esp的值交給了edx,然后調用sysenter。這個匯編就是快速調用。為什么叫快速調用?中斷門進0環,需要的CSEIPIDT表中,需要查內存(SSESPTSS提供),而CPU如果支持sysenter指令時,操作系統會提前將CS/SS/ESP/EIP的值存儲在MSR寄存器中,sysenter指令執行時,CPU會將MSR寄存器中的值直接寫入相關寄存器,沒有讀內存的過程,所以叫快速調用,但本質是一樣的。
  其實,快速調用並不是一直存在的,在比較古老的CPU是不支持快速調用的。它們進入內核的方式很簡單粗暴,就是使用中斷門。
  CPU如何知道是否支持快速調用呢?當通過eax=1來執行cpuid指令時,處理器的特征信息被放在ecxedx寄存器中,其中edx包含了一個SEP位(11位),該位指明了當前處理器知否支持sysenter/sysexit指令,具體細節可以查看白皮書。
  通過逆向匯編代碼可以看出,不管CPU是否支持快速調用,它都是調用該地址。這就說明操作系統在初始化該結構體的時候必須先判斷支不支持,然后填入適當的值。如果CPU支持快速調用,操作系統就會填入KiFastSystemCall函數的地址,我們可以看一下:

; _DWORD __stdcall KiFastSystemCall()
                public _KiFastSystemCall@0
_KiFastSystemCall@0 proc near           ; DATA XREF: .text:off_7C923428↑o
                mov     edx, esp
                sysenter
_KiFastSystemCall@0 endp

  如果CPU不支持快速調用,操作系統就會填入KiIntSystemCall函數的地址,我們可以看一下:

; _DWORD __stdcall KiIntSystemCall()
                public _KiIntSystemCall@0
_KiIntSystemCall@0 proc near            ; DATA XREF: .text:off_7C923428↑o

arg_4           = byte ptr  8

                lea     edx, [esp+arg_4] ;參數指針
                int     2Eh             ; DOS 2+ internal - EXECUTE COMMAND
                                        ; DS:SI -> counted CR-terminated command string
                retn
_KiIntSystemCall@0 endp

  本篇內容就先講解這么多,進入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環層面調用過程(上)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM