Windows系統調用中API的3環部分

原文鏈接:https://www.cnblogs.com/onetrainee/p/11675224.html

一、R3環API分析的重要性

  1. Windows所提供給R3環的API,實質就是對操作系統接口的封裝,其實現部分都是在R0實現的。
  2. 很多惡意程序會利用鉤子來鉤取這些API,從而達到截取內容,修改數據的意圖。
  3. 現在我們使用olldbg對ReadProcessMemory進行跟蹤分析,查看其在R3的實現,並根據我們的分析來重寫一個ReadProcessMemory。
  4. 重寫ReadProcessMemory之後,這就會加大惡意代碼截獲的難度。
  5. 當然,對於自己來說也有很多弊端,比如只能在指定的操作系統中運行(32位與64位操作系統,其運行ReadProcessMemory的執行動作是不一樣的,在64位運行32位程序,其中間會調用wow64cpu.dll來進行轉換)

二、調試代碼

#include "pch.h"
#include <iostream>
#include <algorithm>
#include <Windows.h>

int main() {
    getchar();
    getchar();
    int a[4],t;
    printf("hello world!");
    getchar();
    getchar();
    // 依次往 p 指針中寫入數據,再用ReadProcessMemory讀取數據
    for (int i = 0; i < 4; i++) {
        WriteProcessMemory(INVALID_HANDLE_VALUE, &a[i], &i, sizeof(int),NULL);
        
    }
    for (int i = 0; i < 4; i++) {
        ReadProcessMemory(INVALID_HANDLE_VALUE, &a[i], &t, sizeof(int), NULL);
        printf("%d\n", t);
    }
    getchar();
    getchar();
    
}

三、調試中的關鍵彙編代碼(系統環境:在Windows7 32位操作系統 / 調試器:olldbg)

1. 在exe 中 調用 kernel32.ReadProcessMemroy函數
  01314E3E    8BF4         mov esi,esp
  01314E40    6A 00        push 0x0
  01314E42    6A 04        push 0x4
  01314E44    8D45 DC      lea eax,dword ptr ss:[ebp-0x24]
  01314E47    50           push eax
  01314E48    8B4D C4      mov ecx,dword ptr ss:[ebp-0x3C]
  01314E4B    8D548D E8    lea edx,dword ptr ss:[ebp+ecx*4-0x18]
  01314E4F    52           push edx
  01314E50    6A FF        push -0x1
  01314E52    FF15 64B0310>call dword ptr ds:[<&KERNEL32.ReadProcessMemory>]; kernel32.ReadProcessMemory
  01314E58    3BF4         cmp esi,esp

2. 在 kernel32.ReadProcessMemroy函數 中調用 jmp.&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory> 函數
  // 該函數相當於什麼也沒做...
  7622C1CE >  8BFF             mov edi,edi
  7622C1D0    55               push ebp
  7622C1D1    8BEC             mov ebp,esp
  7622C1D3    5D               pop ebp                                                           ;
  7622C1D4  ^ E9 F45EFCFF      jmp <jmp.&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory>

3. 在 API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemo 中調用 KernelBa.ReadProcessMemory 函數
  761F20CD  - FF25 0C191F7>jmp dword ptr ds:[<&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemo>; KernelBa.ReadProcessMemory

4. 在KernelBa.ReadProcessMemory 調用 <&ntdll.NtReadVirtualMemory> 函數
  75DA9A0A >  8BFF         mov edi,edi
  // 這兩部分在編寫函數時就會使用
  75DA9A0C    55           push ebp
  75DA9A0D    8BEC         mov ebp,esp
  75DA9A0F    8D45 14      lea eax,dword ptr ss:[ebp+0x14]
  75DA9A12    50           push eax
  75DA9A13    FF75 14      push dword ptr ss:[ebp+0x14]
  75DA9A16    FF75 10      push dword ptr ss:[ebp+0x10]
  75DA9A19    FF75 0C      push dword ptr ss:[ebp+0xC]
  75DA9A1C    FF75 08      push dword ptr ss:[ebp+0x8]
  75DA9A1F    FF15 C411DA7>call dword ptr ds:[<&ntdll.NtReadVirtualMemory>] ; ntdll.ZwReadVirtualMemory

5. 在 <&ntdll.NtReadVirtualMemory> 中調用 ntdll.KiFastSystemCall 函數
  77A162F8 >  B8 15010000  mov eax,0x115  // 對應操作系統內核中某一函數的編號。
  77A162FD    BA 0003FE7F  mov edx,0x7FFE0300  // 該地方是一個函數,該函數決定了什麼方式進零環。
  77A16302    FF12         call dword ptr ds:[edx]  ; ntdll.KiFastSystemCall

6. 在 ntdll.KiFastSystemCall 中 調用sysenter
  77A170B0 >  8BD4         mov edx,esp
  77A170B2    0F34         sysenter
  77A170B4 >  C3           retn

四、彙編代碼分析解讀(根據三中的序號依次解讀)

  1. 這部分是我們程序中調用ReadProcessMemory後編譯器直接編譯後的彙編代碼,傳入參數與API調用
  2. 在kenel32.dll中,mov edi,edi 是用於熱補丁技術所保留的(函數開始處的MOV EDI, EDI的作用),這段代碼仔細看其實除了jmp什麼也沒幹。
  3. 轉到kernelBase.dll中實現ReadProcessMemory。
  4. 這段彙編代碼,將ReadProcessMemory中傳入的參數再次入棧,調用ntdll.ZwReadVirtualMemory函數。
  5. 這段彙編代碼看註釋,eax中存放了一個編號,其就是在內核中的ReadProcessMemory實現;在 0x7FFE0300 處存放了一個函數指針,該函數指針決定了以什麼方式進入0環(中斷/快速調用)。
  6. 在ntdll.KiFastSystemCall調用sysenter。

五、重寫ReadProcessMemory函數的思路

  我們所看到的彙編代碼,本質就是Windows所執行的步驟,我們依據上面的分析,完全可以重新寫一個該函數,只需要關鍵部分。

  1) 退而求其次

    我們希望可以在自己的代碼中直接使用 "sysenter",但經過編寫發現其並沒有提供這種指令。

    因此在"sysenter"無法直接使用的情況下,只能退而求其次,調用ntdll.KiFastSystemCall函數。

  2)傳遞參數,模擬call指令

    ntdll.KiFastSystemCall函數需要藉助ntdll.NtReadVirtualMemory傳遞過來的參數,然後執行call指令。

    我們並不希望執行call指令執行,因爲執行call指令意味着又上了一層。(多一層被鉤取的風險)

    我們希望自己的代碼中直接傳遞參數,並且直接調用調用ntdll.KiFastSystemCall函數。

    因此我們需要模擬call指令。call指令的本質就是將返回地址入棧,並跳轉。我們不需要跳轉,只需要將返回地址入棧(四個字節 使用 sub esp,4 模擬)

  3)手動實現棧平衡

    我們內嵌彙編代碼後,需要手動平衡棧,我們只需要分析esp改變了多少(push、pop以及直接對esp的計算)。

    經過分析共減少了24字節,所以代碼最後應該有 add esp,24 來平衡棧。

六、ReadProcessMemory函數重寫的實現(重點看彙編代碼)

該代碼是使用快速調用所編寫的,如果想使用中斷實現調用內核函數,請移步這裏:Windows系統調用中API從三環到零環(下)

(執行結果)

#include "pch.h"
#include <iostream>
#include <algorithm>
#include <Windows.h>
void  ReadMemory(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD  *dwSizeRet)
{

    _asm
    {
        lea     eax, [ebp + 0x14]
        push    eax
        push[ebp + 0x14]
        push[ebp + 0x10]
        push[ebp + 0xc]
        push[ebp + 8]
        sub esp, 4
        mov eax, 0x115
        mov edx, 0X7FFE0300   //sysenter不能直接調用,我間接call的
        CALL DWORD PTR[EDX]
        add esp, 24

    }
}
int main()
{
    HANDLE hProcess = 0;
    int t = 123;
    DWORD pBuffer;
    //hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0,a);
    ReadMemory((HANDLE)-1, (PVOID)&t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);
    ReadProcessMemory((HANDLE)-1, &t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);

    getchar();
    return 0;
}

一、結構體 _KUSER_SHARED_DATA

  該結構體看名字可知是用於內核層與用戶層來共享數據所使用的結構體。

   kd > dt _KUSER_SHARED_DATA
          ntdll!_KUSER_SHARED_DATA
          + 0x000 TickCountLowDeprecated : Uint4B
          + 0x004 TickCountMultiplier : Uint4B
          + 0x008 InterruptTime : _KSYSTEM_TIME
          + 0x014 SystemTime : _KSYSTEM_TIME
          + 0x020 TimeZoneBias : _KSYSTEM_TIME
          + 0x02c ImageNumberLow : Uint2B
          + 0x02e ImageNumberHigh : Uint2B
          ·······

   1)在User層和KerNel層分別定義了一個_KUSER_SHARED_DATA結構區域,用於User層和Kernel層共享某些數據。

   2)它們使用同一段頁,只是映射位置不同。雖然同一頁,但User只讀,Kernnel層可寫。

   3)它們使用固定的地址值映射,_KUSER_SHARED_DATA結構在User爲:0x7ffe0000,在Kernel層爲:0xffdf0000。

   通過windbg可以查看其中兩塊內存完全一樣(實質查看的是同一物理頁,掛在兩塊頁表PTT中。

    

二、分析 0X7FFE0300  這個地址

在 <&ntdll.NtReadVirtualMemory> 中調用 ntdll.KiFastSystemCall 函數 ,實質就是調用 0X7FFE0300 這個地址。
    77A162F8 >  B8 15010000  mov eax,0x115  // 對應操作系統內核中某一函數的編號。
    77A162FD    BA 0003FE7F  mov edx,0x7FFE0300  // 該地方是一個函數,該函數決定了什麼方式進零環。
    77A16302    FF12         call dword ptr ds:[edx]  ; ntdll.KiFastSystemCall

  1)_KUSER_SHARED_DATA 在用戶層的位置爲 0x7FFE0000,該地址爲其+0x300位置

     +0x300 SystemCall       : Uint4B

  2)該成員保存着系統調用的函數入口,如果當前CPU支持快速調用。

    則存儲着ntdll.dll!KiFastSystemCall()函數地址;

    如果不支持快速調用,則存儲着ntdll.dll!KiIntSystemCall()函數地址。

  3)通過實驗驗證當前CPU是否支持快速調用:

    當通過eax=1來執行cupid指令時,處理器特徵信息被存放在ecx和edx寄存器中,

    其中edx包含了SEP位(11位),該位指明瞭當前處理器是否支持sysenter/sysexit指令。

    

    如下圖,我們執行cupid指令,獲取edx 178BFBFF,拆分 11-8位 B 1011,故其11位爲1,支持快速調用。

    這也驗證了我們上一篇文章中的分析結果。(最後是快速調用並非使用中斷門)

三、從3環進0環需要哪些寄存器改變

  1. CS的權限由3變爲0,意味着需要新的CS
  2. SS與CS的權限永遠一致,需要新的SS
  3. 權限發生切換的時候,堆棧也一定會改變,需要新的ESP
  4. 進0環後的代碼位置,需要EIP

四、ntdll.dll!KiIntSystemCall() 分析

  我們使用ida來分析ntdll.dll!KiIntSystemCall()

  .text:77F070C0
  .text : 77F070C0                 public KiIntSystemCall
  .text : 77F070C0 KiIntSystemCall proc near; DATA XREF : .text : off_77EF61B8↑o
  .text : 77F070C0
  .text : 77F070C0 arg_4 = byte ptr  8
  .text : 77F070C0            // 之前調用該函數時 mov eax, 0x115,向eax傳入一個函數號
  .text : 77F070C0                 lea     edx, [esp + arg_4] // 當前參數的指針存儲在 edx中
  .text : 77F070C4                 int     2Eh; // 通過中斷門的形式進入到內核中
  .text:77F070C4; DS:SI->counted CR - terminated command string
  .text : 77F070C6                 retn
  .text : 77F070C6 KiIntSystemCall endp

  其在觸發 int 2eh中斷前用到兩個寄存器,一個是內核中調用函數的函數號,另外一個就是傳入參數的指針。

五. ntdll.dll!KiFastSystemCall()函數分析
  當CPU支持快速調用,則使用這個函數。(我們在上篇文章中已經用到了這個來重構WriteProcessMemory函數)
  .text:77F070B0                 public KiFastSystemCall
  .text:77F070B0 KiFastSystemCall proc near              ; DATA XREF: .text:off_77EF61B8↑o
  .text:77F070B0            // 之前調用該函數時 mov eax, 0x115,向eax傳入一個函數號
  .text:77F070B0                 mov     edx, esp // 將當前堆棧放入edx,用它來存儲參數
  .text:77F070B2                 sysenter
  .text:77F070B2 KiFastSystemCall endp

  觸發sysenter指令後,也用到兩個寄存器eax,edx,作用與使用中斷一樣。

  爲什麼叫快速調用?

    中斷門進入0環,需要的CS、EIP在IDT表中,需要查內存(SS與ESP由IDT表提供);

    而CPU如果支持sysenter指令時,操作系統會提前將CS/SS/ESP/EIP的值存儲在MSR寄存器中,

    sysenter指令執行時,CPU會將MSR寄存器中的值直接寫入寄存器中,沒有讀內存過程,本質時一樣的。

一、INT 0x2E進0環

  .text : 77F070C0            // 之前調用該函數時 mov eax, 0x115,向eax傳入一個函數號
  .text : 77F070C0                 lea     edx, [esp + arg_4] // 當前參數的指針存儲在 edx中
  .text : 77F070C4                 int     2Eh; // 通過中斷門的形式進入到內核中

  1)在GDT表中查看0x2eh

    在保護模式的門這一節中,我們瞭解到當發生中斷時,操作系統會查找idt表,根據中斷號在idt表中找到中斷門描述符,從中斷門描述符中讀取CS:EIP的信息。

    之後,SS EIP 通過搜索GDT表,該表中存放着各個TSS描述符(每個進程一個TSS,內核一個TSS,TSS存放各種寄存器用於任務切換),來查找內核的 SS ESP。

    如圖:我們通過windbg來查找出該地址 gdt+2e*8

      

     根據中斷門描述符屬性將 83e8ee00`00082fee 拆分拼接之後可知SS:08 / EIP:83e82fee

      

  2)查看 EIP:83e82fee 這個函數

    kd> u 83e82fee
    nt!KiSystemService:
    83e82fee 6a00            push    0
    83e82ff0 55              push    ebp
    83e82ff1 53              push    ebx
    83e82ff2 56              push    esi
    83e82ff3 57              push    edi
    83e82ff4 0fa0            push    fs
    83e82ff6 bb30000000      mov     ebx,30h
    83e82ffb 668ee3          mov     fs,bx

    該 nt!KiSystemService函數是真正的內核函數,並不是ntdll.dll模塊下,其存在於ntoskrnl.exe / ntkrnlpa.exe中(根據分頁模式不同選用不同的程序)

二、通過 systenter進入0環

  MSR寄存器存着進入內核的 CS、ESP、EIP的寄存器的值,SS=IA32_SYSENTER_CS+8。

  

  1)windbg查看這個MSR寄存器的值

    rdmsr 174     //查看CS

    rdmsr 175    //查看ESP

    rdmsr 176    //查看EIP

     

  2)查看EIP這個函數

   kd> u 83e830c0
    nt!KiFastCallEntry:
    83e830c0 b923000000      mov     ecx,23h
    83e830c5 6a30            push    30h
    83e830c7 0fa1            pop     fs
    83e830c9 8ed9            mov     ds,cx
    83e830cb 8ec1            mov     es,cx
    83e830cd 648b0d40000000  mov     ecx,dword ptr fs:[40h]
    83e830d4 8b6104          mov     esp,dword ptr [ecx+4]
    83e830d7 6a23            push    23h
    其是調用nt!KiFastCallEntry這個函數,跟nt!KiSystemService一樣,該函數是真正的內核函數。

三、通過中斷來重寫ReadProcessMemory函數(通過快速調用時的實現可以查看這篇Windows系統調用中的API三環部分(依據分析重寫ReadProcessMemory函數))

#include "pch.h"
#include <iostream>
#include <algorithm>
#include <Windows.h>
void  ReadMemory(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD  *dwSizeRet)
{

    _asm
    {
        
        lea     eax, [ebp + 0x14]
        push    eax
        push[ebp + 0x14]
        push[ebp + 0x10]
        push[ebp + 0xc]
        push[ebp + 8]
        mov eax, 0x115
        mov edx,esp
        int 0x2e
        add esp, 20
    }
}
int main()
{
    HANDLE hProcess = 0;
    int t = 123;
    DWORD pBuffer;
    //hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0,a);
    ReadMemory((HANDLE)-1, (PVOID)&t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);
    ReadProcessMemory((HANDLE)-1, &t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);

    getchar();
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章