这两天研究了下windows的shellcode写法, 发现挺多值得记录的东西. 顺便整理一下之前打chrome利用链时写shellcode用到的技巧.
linux/windows shellcode差别 最重要的差别是两者调用shellcode的方式有很大不同. linux上系统调用号是不随版本而改变的, 只可能新增而不可能改变原有的调用号, 每个数字的坑位被占用就是被占用了, 即使弃用也会保留.
而windows的系统调用号可能会随着版本被变更, 所以windows上不能简单通过设置rax然后调用syscall来稳定地调用任意系统调用, 常见的办法是使用ntdll.dll, kernel32.dll, KERNELBASE.dll对外暴露的函数来间接调用系统调用或所需功能. 而这三个dll在常规进程都会被加载, 所以问题就是如何获取其中导出函数地址.
获取 ntdll, kernel32, KERNELBASE 导出函数地址 获取一个已加载dll上的导出函数地址分为两步, 首先获取该dll基地址, 其次根据dll基地址在导出表上查找指定导出函数地址
dll基地址->导出函数地址 像ntdll, kernel32, KERNELBASE这种需要导出符号给外部函数的dll, 会在dll头部有函数导出表的结构. 其中有一个表是有函数名信息, 可以在该表上遍历比对函数名, 找到后根据函数表idx查找到函数的偏移, 再加上dll的基地址就是函数真实地址
有可能该函数的实现在别的dll中, 这时候找到的不是函数地址, 而是类似KERNELBASE.Sleep这样的转发字符串. 这时就需要继续查找
下面是通过dll基地址和函数名在dll中动态查找该函数的c实现. 尽管GetProcAddress可以达到同样甚至更好的效果, 但可能更容易被分析和发现. 而且获取GetProcAddress本身也可能需要类似的动态查找函数办法.
通常我们使用该函数的时候已经知道要的函数在哪个dll中了, 这里的FUNC_FORWARDER只是严谨考虑, 并不一定真的会用到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 typedef enum { NOT_FOUND, FUNC_DIRECT, FUNC_FORWARDER } func_result; static uint32_t search_func (uint64_t dll_base, char *func_name, PIMAGE_EXPORT_DIRECTORY export_table) { uint32_t func_name_len = my_strlen (func_name); uint32_t name_table_len = export_table->NumberOfNames; uint32_t *name_table = (uint32_t *)(dll_base + export_table->AddressOfNames); uint32_t *func_table = (uint32_t *)(dll_base + export_table->AddressOfFunctions); uint16_t *name_2_idx = (uint16_t *)(dll_base + export_table->AddressOfNameOrdinals); for (uint32_t i = 0 ; i < name_table_len; i++) { char *name = (char *)(dll_base + name_table[i]); if (my_memcmp (name, func_name, func_name_len + 1 )) continue ; uint32_t idx = name_2_idx[i]; return func_table[idx]; } return 0 ; } static func_resultget_func (uint64_t dll_base, char *func_name, uint64_t *result) { PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)dll_base; PIMAGE_NT_HEADERS nt_headers = (PIMAGE_NT_HEADERS)(dll_base + dos_header->e_lfanew); IMAGE_DATA_DIRECTORY export_datadir = nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; PIMAGE_EXPORT_DIRECTORY export_table = (PIMAGE_EXPORT_DIRECTORY)(dll_base + export_datadir.VirtualAddress); uint32_t export_size = export_datadir.Size; if (export_datadir.VirtualAddress == 0 || export_size == 0 ) return NOT_FOUND; uint32_t func_rva = search_func (dll_base, func_name, export_table); *result = dll_base + func_rva; if (func_rva == 0 ) return NOT_FOUND; if (func_rva >= export_datadir.VirtualAddress && func_rva < export_datadir.VirtualAddress + export_size) return FUNC_FORWARDER; return FUNC_DIRECT; }
获取dll基地址 ntdll.dll kernel32.dll KERNELBASE.dll通常都会被加载. 而windows进程有一个结构体_PEB, 在x64用户态时固定存在gs:60的位置, 这个结构体最终可以给我们一个该进程已加载的dll信息链表, 其中就有dll的基地址和dll名字.
完整的结构体链子是PEB peb->struct _PEB_LDR_DATA *ldr->LIST_ENTRY *InLoadOrderLinks, 随后的链表结构有点绕, 但示意图应该很清晰了. 这里InMemoryOrderModuleLinks/InLoadOrderLinks/InInitializationOrderLinks都可以, 三者只是顺序不同. (图中把links写成list了, 但是懒得改了…)
下面是从peb获取dll基地址的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 typedef struct { uint64_t user32; uint64_t ntdll; uint64_t kernel32; uint64_t kernelbase; } dll_base; typedef struct { struct _LIST_ENTRY InLoadOrderLinks ; struct _LIST_ENTRY InMemoryOrderLinks ; struct _LIST_ENTRY InInitializationOrderLinks ; VOID *DllBase; VOID *EntryPoint; ULONG SizeOfImage; struct _UNICODE_STRING FullDllName ; struct _UNICODE_STRING BaseDllName ; } my_LDR_DATA_TABLE_ENTRY; #define IF_MEMCMP_DLL_NAME(dll_name) \ if (dll->Length == (sizeof (dll_name) - sizeof (L"" )) \ && !my_memicmp (dll->Buffer, (void *)dll_name, dll->Length)) \ bases->dll_name = (uint64_t)data->DllBase; WCHAR user32[] = L"user32.dll" ; WCHAR ntdll[] = L"ntdll.dll" ; WCHAR kernel32[] = L"kernel32.dll" ; WCHAR kernelbase[] = L"KERNELBASE.dll" ; static void get_dll_base (struct _PEB_LDR_DATA *ldr, dll_base *bases) { LIST_ENTRY *head = &ldr->InMemoryOrderModuleList; LIST_ENTRY *link = head->Flink; while (link != head) { my_LDR_DATA_TABLE_ENTRY *data = CONTAINING_RECORD ( link, my_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); UNICODE_STRING *dll = &data->BaseDllName; IF_MEMCMP_DLL_NAME (ntdll) else IF_MEMCMP_DLL_NAME (kernel32) else IF_MEMCMP_DLL_NAME (kernelbase) else IF_MEMCMP_DLL_NAME (user32) if (bases->user32 && bases->kernel32 && bases->ntdll && bases->kernelbase) break ; link = link->Flink; } } int main () { PEB *peb = (PEB *)__readgsqword (0x60 ); struct _PEB_LDR_DATA *ldr = peb->Ldr; dll_base bases; bases.kernelbase = 0 ; bases.kernel32 = 0 ; bases.ntdll = 0 ; bases.user32 = 0 ; get_dll_base (ldr, &bases); }
手写shellcode常见技巧 写shellcode常见的问题是如何处理字符串, 普通的c程序中字符串会被放在data或rdata节, 与text节中间有padding. 要是将这个padding也当作shellcode的一部分就太浪费空间了, 而如果手动去调整text中对于data的引用又很麻烦.
最好的办法是在编译时就将data节紧邻在text节后面, 编译器自然就会处理好text中对于data的引用. 但之前我不知道可以这样做, 所以采取的办法是在代码中用uint64_t去一次次拼接出完整字符串, 导致shellcode大小太过膨胀.
这次重新学习就尝试了一下, 使用的是clang. 注意不同编译器语法会有差别
$A $B是在节内的顺序, 这里让main的顺序位于helper之前, 就可以让shellcode开头就是main的指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #pragma clang section data = "payload_data" WCHAR user32[] = L"user32.dll" ; WCHAR ntdll[] = L"ntdll.dll" ; WCHAR kernel32[] = L"kernel32.dll" ; WCHAR kernelbase[] = L"KERNELBASE.dll" ; #pragma clang section data = "" #pragma clang section text = "code$A" int main () {}#pragma clang section text = "code$B" void helper_function () {}#pragma clang section text = ""
仅仅在源码层面标记还不够, 需要在编译器参数里将payload_data节合并进code节. 并且我声明了code节权限为E(execute)R(read). 话说执行权限居然是E而不是X….
1 2 3 4 5 "clang" ,"-Xlinker" ,"/merge:payload_data=code" ,"-Xlinker" ,"/section:code,ER" ,
跳MessageBox的参考 了解这些信息, 就来试试写shellcode跳个MessageBox. 这些代码编译成a.exe后并不是只能单独执行a.exe, 其中的code节数据可以放到shellcode执行器中执行, 一样可以跳MessageBox
MessageBoxA和MessageBoxW在User32.dll, 这个dll尽管常见, 但并不是默认加载的. 所以shellcode里还多了一步先获取LoadLibraryW加载User32.dll的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 #include "test.h" #include <Windows.h> #include <stdint.h> #include <winternl.h> #pragma clang section data = "payload_data" WCHAR user32[] = L"user32.dll" ; WCHAR ntdll[] = L"ntdll.dll" ; WCHAR kernel32[] = L"kernel32.dll" ; WCHAR kernelbase[] = L"KERNELBASE.dll" ; char message_box[] = "MessageBoxA" ;char load_library[] = "LoadLibraryW" ;char text[] = "hello" ;char hint[] = "hint" ;#pragma clang section data = "" #define IF_MEMCMP_DLL_NAME(dll_name) \ if (dll->Length == (sizeof (dll_name) - sizeof (L"" )) \ && !my_memicmp (dll->Buffer, (void *)dll_name, dll->Length)) \ bases->dll_name = (uint64_t)data->DllBase; #pragma clang section text = "code$A" int main () { PEB *peb = (PEB *)__readgsqword (0x60 ); struct _PEB_LDR_DATA *ldr = peb->Ldr; dll_base bases; bases.kernelbase = 0 ; bases.kernel32 = 0 ; bases.ntdll = 0 ; bases.user32 = 0 ; void (*MessageBoxA) (HWND, LPCSTR, LPCSTR, UINT); HWND (*LoadLibraryW) (WCHAR *); get_dll_base (ldr, &bases); func_result result = get_func (bases.kernelbase, (char *)load_library, (uint64_t *)&LoadLibraryW); if (result != FUNC_DIRECT) return 0 ; LoadLibraryW ((WCHAR *)user32); get_dll_base (ldr, &bases); result = get_func (bases.user32, (char *)message_box, (uint64_t *)&MessageBoxA); if (result != FUNC_DIRECT) return 0 ; MessageBoxA (NULL , text, hint, MB_OK); return 0 ; } #pragma clang section text = "code$B" static bool my_memicmp (void *dst, void *src, size_t size) { uint8_t *left = (uint8_t *)dst; uint8_t *right = (uint8_t *)src; for (uint64_t i = 0 ; i < size; i++) { uint8_t cmp_l = left[i]; uint8_t cmp_r = right[i]; if (left[i] >= 'a' && left[i] <= 'z' ) cmp_l = left[i] - 0x20 ; if (right[i] >= 'a' && right[i] <= 'z' ) cmp_r = right[i] - 0x20 ; if (cmp_l != cmp_r) return true ; } return false ; } static bool my_memcmp (void *dst, void *src, size_t size) { for (uint64_t i = 0 ; i < size; i++) { if (((uint8_t *)dst)[i] != ((uint8_t *)src)[i]) return true ; } return false ; } static uint64_t my_strlen (void *dst) { uint64_t i = 0 ; for (; ((uint8_t *)dst)[i] != '\0' ; i++) continue ; return i; } static uint32_t search_func (uint64_t dll_base, char *func_name, PIMAGE_EXPORT_DIRECTORY export_table) { uint32_t func_name_len = my_strlen (func_name); uint32_t name_table_len = export_table->NumberOfNames; uint32_t *name_table = (uint32_t *)(dll_base + export_table->AddressOfNames); uint32_t *func_table = (uint32_t *)(dll_base + export_table->AddressOfFunctions); uint16_t *name_2_idx = (uint16_t *)(dll_base + export_table->AddressOfNameOrdinals); for (uint32_t i = 0 ; i < name_table_len; i++) { char *name = (char *)(dll_base + name_table[i]); if (my_memcmp (name, func_name, func_name_len + 1 )) continue ; uint32_t idx = name_2_idx[i]; return func_table[idx]; } return 0 ; } static func_resultget_func (uint64_t dll_base, char *func_name, uint64_t *result) { PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)dll_base; PIMAGE_NT_HEADERS nt_headers = (PIMAGE_NT_HEADERS)(dll_base + dos_header->e_lfanew); IMAGE_DATA_DIRECTORY export_datadir = nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; PIMAGE_EXPORT_DIRECTORY export_table = (PIMAGE_EXPORT_DIRECTORY)(dll_base + export_datadir.VirtualAddress); uint32_t export_size = export_datadir.Size; if (export_datadir.VirtualAddress == 0 || export_size == 0 ) return NOT_FOUND; uint32_t func_rva = search_func (dll_base, func_name, export_table); *result = dll_base + func_rva; if (func_rva == 0 ) return NOT_FOUND; if (func_rva >= export_datadir.VirtualAddress && func_rva < export_datadir.VirtualAddress + export_size) return FUNC_FORWARDER; return FUNC_DIRECT; } static void get_dll_base (struct _PEB_LDR_DATA *ldr, dll_base *bases) { LIST_ENTRY *head = &ldr->InMemoryOrderModuleList; LIST_ENTRY *link = head->Flink; while (link != head) { my_LDR_DATA_TABLE_ENTRY *data = CONTAINING_RECORD ( link, my_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); UNICODE_STRING *dll = &data->BaseDllName; IF_MEMCMP_DLL_NAME (ntdll) else IF_MEMCMP_DLL_NAME (kernel32) else IF_MEMCMP_DLL_NAME (kernelbase) else IF_MEMCMP_DLL_NAME (user32) if (bases->user32 && bases->kernel32 && bases->ntdll && bases->kernelbase) break ; link = link->Flink; } } #pragma clang section text = ""
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pathlib import Pathimport subprocessimport pefiledef main (): root = Path.cwd() source_path = root / "test.c" exe_path = root / "a.exe" payload_path = root / "payload.bin" if not source_path.is_file(): raise FileNotFoundError(f"can't found source code: {source_path} " ) subprocess.run( [ "clang" , "-Wl,/subsystem:console,/entry:main" , "-Xlinker" , "/merge:payload_data=code" , "-Xlinker" , "/section:code,ER" , "-masm=intel" , "-nostdlib" , "-O0" , "-g" , str (source_path), ], check=True , ) if not exe_path.is_file(): raise FileNotFoundError(f"can't find compile exe: {exe_path} " ) pe = pefile.PE(str (exe_path)) sections = [ section for section in pe.sections if section.Name.rstrip(b"\0" ) == b"code" ] sections.sort(key=lambda s: s.VirtualAddress) if not sections: raise ValueError("can't find code section" ) payload = b"" .join(s.get_data(length=s.Misc_VirtualSize) for s in sections) payload_path.write_bytes(payload) if __name__ == "__main__" : main()
… 之前打chrome的时候思路感觉还是很稚嫩, 字符串用uint64_t硬凑, 从chrome.dll上找GetProcAddress和各种dll基地址(笑), 也是chrome.dll刚好量大管饱.
参考 PEB及其武器化