簡易 ShellCode 雖然可以正常被執行,但是還存在很多的問題,因為上次所編寫的 ShellCode 采用了硬編址的方式來調用相應API函數的,那么就會存在一個很大的缺陷,如果操作系統的版本不統一就會存在調用函數失敗甚至是軟件卡死的現象,下面我們通過編寫一些定位程序,讓 ShellCode 能夠動態定位我們所需要的API函數地址,從而解決上節課中 ShellCode 的通用性問題。
查找 Kernel32.dll 基址
首先我們需要通過匯編的方式來實現動態定位 Kernel32 中的基地址,你或許會有個疑問? 為什么要查找 Kernel32.dll 的地址而不是 User32.dll,這是因為我們最終的目的是調用 MessageBox 這個函數,而該函數位於 User32.dll 這個動態鏈接庫里,默認情況下是無法直接調用的,為了能夠調用這個函數,我們就需要調用 LoadLibraryA 函數來加載 User32.dll 這個模塊,而 LoadLibraryA 又位於 kernel32.dll 中。
恰巧的是 Kernel32.dll 這個模塊只要是 PE 文件都會默認被加載 ,因此我們只需要找到 LoadLibraryA 函數,即可加載任意的動態鏈接庫,並調用任意的函數啦。
由於我們需要動態獲取 LoadLibraryA() 以及 ExitProcess() 這兩個函數的地址,而這兩個函數又是存在於 kernel32.dll 中的,因此這里需要先找到 kernel32.dll 的地址,然后通過對其進行解析,從而查找那兩個函數的地址。
這里有一個公式,可以動態的查找到 Kernel32.dll 的地址,如下:
- 通過段選擇字FS在內存中找到當前的線程環境塊TEB。
- 線程環境塊偏移位置為0x30的地方存放着指向進程環境塊PEB的指針。
- 進程環境塊偏移為 0x0c 存放着指向 PEB_LDR_DATA 的結構體指針。
- PEB_LDR_DATA 偏移 0x1c 的地方存放着指向模塊初始化鏈表的頭指針。
- 初始化鏈表中,按順序存放着PE裝入運行時初始化模塊的相關信息。
既然有了固定的公式,接下我們就使用WinDBG調試器來手工完成對 Kernel32.dll 地址的定位:
1.首先我們運行 WinDBG調試器,然后按下【Ctrl + K】選擇文件(File) -> 選擇內核調試(Kernel Debug) -> 本地調試(Local) 點擊確定。打開后會看到如下界面,直接在最底部輸入兩條命令,來加載一下符號文件,否則無法進行查看。
2.通過段選擇字FS在內存中找到當前的線程環境塊TEB。這里可以利用本地調試,輸入!teb 指令:
線程環境塊偏移位置為 0x30
的地方存放着指向進程環境塊PEB的指針。結合上圖可見,PEB的地址為0x7ffd7000
。
3.進程環境塊中偏移位置為 0x0c
的地方存放着指向 PEB_LDR_DATA
結構體的指針,其中存放着已經被進程裝載的動態鏈接庫的信息。
4.接着 PEB_LDR_DATA
結構體偏移位置為 0x1c
的地方存放着指向模塊初始化鏈表的頭指針 InInitializationOrderModuleList
。
5.模塊初始化鏈表 InInitializationOrderModuleList
中按順序存放着PE裝入運行時初始化模塊的信息,第一個鏈表節點是 ntdll.dll
,第二個鏈表結點就是kernel32.dll
。比如可以先看看
InInitializationOrderModuleList
中的內容:
上圖中的 0x00191f28
保存的是第一個鏈節點的指針,解析一下這個結點,可發現如下地址:
上圖中 0x7c92000
為 ntdll.dll 的基地址,而 0x00191fd0
則是下一個模塊的指針,繼續跟隨 0x00191fd0
。
觀察發現第二個節點偏移 0x08
個字節正是 kernel32.dll的基地址,其地址為 0x7c800000
。最有我們來驗證一下:
6.通過上方的調試我們可得到公式,接着通過編寫一段匯編代碼來實現自動的遍歷出 kernel32.dl
的基址。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kerbcli.lib
assume fs:nothing
.code
main PROC
xor eax,eax
xor edx,edx
mov eax,fs:[30h] ; 得到PEB結構地址
mov eax,[eax + 0ch] ; 得到PEB_LDR_DATA結構地址
mov esi,[eax + 1ch]
lodsd ; 得到KERNEL32.DLL所在LDR_MODULE結構的
; mov eax,[eax] ; Windows 7 以上要將這里打開
mov edx,[eax + 8h] ; 得到BaseAddress,既Kernel32.dll基址
ret
main ENDP
END main
計算函數名 hash 摘要
接着需要對我們所用到的字符串進行 hash 壓縮處理,為啥要壓縮? 原因是如果直接將函數名壓棧的話,我們就需要提供更多的空間來存儲 ShellCode 代碼,為了能夠讓我們編寫的 ShellCode 代碼更加的短小精悍,所以我們將要對字符串進行hash處理,將字符串壓縮為一個十六進制數,這樣只需要比較二者hash值就能夠判斷目標函數,盡管這樣會引入額外的hash算法,但是卻可以節省出存儲函數名字的空間。
如下代碼是使用 Win32 匯編語言的實現過程,並在 MASM 上正常編譯,匯編版字符串轉換Hash值。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
include msvcrt.inc
includelib kernel32.lib
includelib msvcrt.lib
.data
data db "MessageBoxA",0h
Fomat db "0x%x",0
.code
main PROC
xor eax,eax ; 清空eax寄存器
xor edx,edx ; 清空edx寄存器
lea esi,data ; 取出字符串地址
loops:
movsx eax,byte ptr[esi] ; 每次取出一個字符放入eax中
cmp al,ah ; 驗證eax是否為0x0即結束符
jz nops ; 為0則說明計算完畢跳轉到nops
ror edx,7 ; 不為零,則進行循環右移7位
add edx,eax ; 將循環右移的值不斷累加
inc esi ; esi自增,用於讀取下一個字符
jmp loops ; 循環執行
nops:
mov eax,edx ; 結果存在eax里面
invoke crt_printf,addr Fomat,eax
ret
main ENDP
END main
當然也可以使用C語言來實現這個轉換的過程,這里使用的是VS Express 2013可正常編譯通過,如下代碼:
#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
DWORD digest = 0;
while (*fun_name)
{
digest = ((digest << 25) | (digest >> 7));
digest += *fun_name;
fun_name++;
}
return digest;
}
int main()
{
DWORD hash;
hash = GetHash("MessageBoxA");
printf("0x%.8x\n", hash);
getchar();
return 0;
}
我們通過傳入不同的函數名稱,讓其計算出我們所需要計算的三個函數的hash值,其計算結果如下所示:
MessageBoxA 0x1e380a6a
ExitProcess 0x4fd18963
LoadLibraryA 0x0c917432
解析 kernel32.dll 導出表
在開頭的部分我們通過 WinDBG
調試器已經找到了 Kernel32.dll
這個動態鏈接庫的基地址,而Dll文件本質上也是PE文件,在Dll文件中存在一個導出表,其內部記錄着該Dll的導出函數。接着我們需要對Dll文件的導出表進行遍歷,不斷地搜索,從而找到我們所需要的API函數。
同樣的,這里有一個定式,可以通過該定式獲取到指定的導出表。
- 從 kernel32.dll 加載基址算起,偏移 0x3c 的地方就是其PE文件頭。
- PE文件頭偏移 0x78 的地方存放着指向函數導出表的指針。
- 導出表偏移0x1c處的指針指向存儲導出函數偏移地址(RVA)的列表。
- 導出表偏移0x20處的指針指向存儲導出函數函數名的列表。
函數的 RVA 地址和名字按照順序存放在上述兩個列表中,我們可以在列表定位任意函數的RVA地址,通過與動態鏈接庫的基地址相加得到其真實的VA,而計算的地址就是我們最終在 ShellCode 中調用時需要的地址。
pushad ; 保護所有寄存器中的內容
mov eax,[ebp+0x3C] ; 指向PE文件頭
mov ecx,[ebp+eax+0x78] ; 導出表的指針
add ecx,ebp
mov ebx,[ecx+0x20] ; 導出函數的名字列表
add ebx,ebp
xor edi,edi ; 用edi寄存器作為索引
; ------- 循環讀取導出表函數
next_loop:
inc edi ; 不斷自增索引
mov esi,[ebx+edi*4] ; 從列表數組中讀取
add esi,ebp ; esi保存的是函數名稱所在地址
cdq
提取代碼 ShellCode
完整代碼如下,下方代碼是一個定式,這里就只做了翻譯,使用編譯器編譯如下代碼,運行后會彈出一個提示框hello lyshark
說明我們成功了。
#include <stdio.h>
#include <windows.h>
int main()
{
__asm {
// ===將索要調用的函數hash值入棧保存
CLD // 清空標志位DF
push 0x1E380A6A // 壓入MessageBoxA-->user32.dll
push 0x4FD18963 // 壓入ExitProcess-->kernel32.dll
push 0x0C917432 // 壓入LoadLibraryA-->kernel32.dll
mov esi,esp // 指向堆棧中存放LoadLibraryA的地址
lea edi,[esi-0xc] // 后面會利用edi的值來調用不同的函數
// ===開辟內存空間,這里是堆棧空間
xor ebx,ebx
mov bh,0x04 // ebx為0x400
sub esp,ebx // 開辟0x400大小的空間
// ===將user32.dll入棧
mov bx,0x3233
push ebx // 壓入字符'32'
push 0x72657375 // 壓入字符 'user'
push esp
xor edx,edx // edx=0
// ===查找kernel32.dll的基地址
mov ebx,fs:[edx+0x30] // [TEB+0x30] -> PEB
mov ecx,[ebx+0xC] // [PEB+0xC] -> PEB_LDR_DATA
mov ecx,[ecx+0x1C] // [PEB_LDR_DATA+0x1C] -> InInitializationOrderModuleList
mov ecx,[ecx] // 進入鏈表第一個就是ntdll.dll
mov ebp,[ecx+0x8] //ebp = kernel32.dll 的基地址
// === hash 的查找相關
find_lib_functions:
lodsd // eax=[ds*10H+esi],讀出來是LoadLibraryA的Hash
cmp eax,0x1E380A6A // 與MessageBoxA的Hash進行比較
jne find_functions // 如果不相等則繼續查找
xchg eax,ebp
call [edi-0x8]
xchg eax,ebp
// ===在PE文件中查找相應的API函數
find_functions:
pushad
mov eax,[ebp+0x3C] // 指向PE頭
mov ecx,[ebp+eax+0x78] // 導出表的指針
add ecx,ebp // ecx=0x78C00000+0x262c
mov ebx,[ecx+0x20] // 導出函數的名字列表
add ebx,ebp // ebx=0x78C00000+0x353C
xor edi,edi // 清空edi中的內容,用作索引
// ===循環讀取導出表函數
next_function_loop:
inc edi // edi作為索引,自動遞增
mov esi,[ebx+edi*4] // 從列表數組中讀取
add esi,ebp // esi保存的是函數名稱所在的地址
cdq
// ===hash值的運算過程
hash_loop:
movsx eax,byte ptr[esi] // 每次讀取一個字節放入eax
cmp al,ah // eax和0做比較,即結束符
jz compare_hash // hash計算完畢跳轉
ror edx,7
add edx,eax
inc esi
jmp hash_loop
// ===hash值的比較函數
compare_hash:
cmp edx,[esp+0x1C]
jnz next_function_loop // 比較不成功則查找下一個函數
mov ebx,[ecx+0x24] // ebx=序數表的相對偏移量
add ebx,ebp // ebx=序數表的絕對地址
mov di,[ebx+2*edi] // di=匹配函數的序數
mov ebx,[ecx+0x1C] // ebx=地址表的相對偏移量
add ebx,ebp // ebx=地址表的絕對地址
add ebp,[ebx+4*edi] // 添加到EBP(模塊地址庫)
xchg eax,ebp // 將func addr移到eax中
pop edi // edi是pushad中最后一個堆棧
stosd
push edi
popad
cmp eax,0x1e380a6a // 與MessageBox的hash值比較
jne find_lib_functions
// ===下方的代碼,就是我們的彈窗
function_call:
xor ebx,ebx // 清空eb寄存器
push ebx // 截斷字符串0
push 0x2020206b
push 0x72616873
push 0x796c206f
push 0x6c6c6568 // push hello lyshark
mov eax,esp
push ebx // push 0
push eax // push "hello lyshark"
push eax // push "hello lyshark"
push ebx // push 0
call [edi-0x04] // call MessageBoxA
push ebx // push 0
call [edi-0x08] // call ExitProcess
}
return 0;
}
編譯生成可執行文件以后,我們使用OD打開程序,並手工尋找到程序的 OEP 提取出 ShellCode 的機器碼,打開UltraEdit 工具,粘貼機器碼,然后按下【Alt + C】進入列模式,編輯只保留機器碼即可。