阅读:1812回复:0
转贴一篇很有趣的文章。
伪造返回地址绕过CallStack检测以及检测伪造返回地址的实践笔记
创建时间:2007-11-10 文章属性:原创 文章提交:KiSSinGGer (kyller_clemens_at_hotmail.com) 伪造返回地址绕过CallStack检测以及检测伪造返回地址的实践笔记 Author:[CISRG]KiSSinGGer E-mail:kissingger@gmail.com MSN:kyller_clemens@hotmail.com 题目有点搞......Anti-CallStack Check and Anti-Anti-CallStack Check...(;- -) 发现最近MJ0011的“基于CallStack的Anti-Rootkit HOOK检测思路”和gyzy的“基于栈指纹检测缓冲区溢出的一点思路”两篇文章有异曲同工之妙。 两者都通过检测CallStack中的返回地址来做文章。 最近在初步学习一些AntiRootkit技术,这两个不得不吸引我的眼球。 按照MJ0011大侠的逻辑,从Rootkit Detector的Hook点向上检测CallStack. 但是CallStack里面都是些DWORDs,怎么判断哪儿是参数,哪儿是返回地址呢? 我Goo了两把...普遍是用EBP回溯的方式. 考虑大部分的__stdcall的形式: mov edi edi push ebp mov ebp esp ... ... 我们从dword ptr [EBP]里面可以获得上个call的EBP,dword ptr [EBP+4]里面获得需要检测的返回地址,然后EBP = dword ptr [EBP],继续找下去.找到栈基址为止. 每次得到的返回地址,判断一下它是否在一个合法的模块中. 但是,根据gyzy大侠的<编写绕过卡巴主动防御的Shellcode>一文启示,我们可以知道如下一种方式,可使这样的检测方式失效. 1.在合法的系统模块里(e.g. ntoskrnl.exe),找到一个'C3'(ret Opcode)字节,它的指针是K. 2.使用如下方式的Hook函数 HookedZwXxx(...) { // // 一些参数处理操作 // jmp __pushrealretaddr __trickstage: push Arg[N] push Arg[N-1] ... push Arg[0] push K jmp ZwXxx; //调用原始函数 __pushrealretaddr: call __trickstage realretaddr: // // 另一些结果处理操作 // } 这样,在ZwXxx深处检查调用栈,dword ptr [EBP+4]是一个处于合法模块中的地址K. 我写了一个如下的ring3示例程序. 定义如下一些函数: int __stdcall Call_C(int a, int b) { check_callstack(); return a+b; } int __stdcall Call_B(int a, int b) { return Call_C(a,b); } int __stdcall Call_A(int a, int b) { return Call_B(a,b); } 调用次序是A->B->C,其中C里面执行check_callstack()来检测是否有非法的返回地址. void __stdcall check_callstack( void ) { int saved_ebp; int retaddr; printf("Check Call Stack Methord 1:\n"); __asm { mov eax, dword ptr [ebp+4] mov retaddr, eax mov eax, dword ptr [ebp] mov saved_ebp, eax } printf("retaddr = 0x%08X\n",retaddr); while(saved_ebp < StackBase && saved_ebp > 0) { if(saved_ebp != 0) { retaddr = *(int*)(saved_ebp+4); printf("retaddr = 0x%08X\n",retaddr); saved_ebp = *(int*)saved_ebp; } } } 在没有Hook的情况下,我们执行Call_A(1,2),得到正常返回为3. check_callstack输出: retaddr = 0x00401008 retaddr = 0x00401030 retaddr = 0x00401050 retaddr = 0x00401126 retaddr = 0x0040149D retaddr = 0x7C816FD7 我们现在使用一个函数Hooked_Call_B来在Call_A中把Call_B给Hook掉. Hook掉的Call_B做的只是把的返回值改成4. __declspec( naked ) int Hooked_Call_B(int a, int b) { __asm { push ebp mov ebp, esp jmp __a __trickstage: mov eax, b push eax mov eax, a push eax //为了方便这里使用一个OD得到的硬编码:P push 0x004011AD //这个地址指向一个'C3' jmp Call_B __a: call __trickstage mov eax, 4 //这里,改返回值,使得1+2的结果为4. pop ebp ret 8 } } 用来改写Call_A的函数,这个函数在2003编译出来的EXE中会导致异常 因为.text段没有写权限.实际测试中我用StudPE改了段属性.在内核态 的话...这个修改代码段段属性问题...应该很简单把... int __stdcall SetHook( int Hook_Call ) { int Original_Call = 0; int hook_pos = (int)Call_A; // // 以下丑陋代码是在Call_A中找到"call Call_B"指令的位置 // __asm { __again: mov eax,hook_pos xor ecx,ecx mov cl,byte ptr ds:[eax] cmp cl,0xE8 je __finish mov edx,hook_pos add edx,1 mov hook_pos,edx jmp __again } __finish: // // 用Hook_Call patch掉call后面的地址 // Hook_Call = Hook_Call - hook_pos - 5; __asm { mov eax, Hook_Call mov edi, hook_pos mov dword ptr [edi+1], eax } return hook_pos; } 我们之后将调用SetHook( Hooked_Call_B )将Call_A中的"call Call_B"改掉. 我们的Hooked_Call_B,在调试器中看到是[0x004010B0,0x004010D2]这段地址. 那么,如果我们根据EBP回溯CallStack的方法有效,在Hooked_Call_B生效以后应该成功的找到一个retaddr属于[0x004010B0,0x004010D2]区间. 遗憾的是,没有... check_callstack输出: retaddr = 0x00401008 retaddr = 0x00401030 retaddr = 0x004011AD <--注意这里 retaddr = 0x00401050 retaddr = 0x0040114D retaddr = 0x0040149D retaddr = 0x7C816FD7 我们可以看到,我们正常的返回地址被一个貌似合法的0x004011AD给偷梁换柱了. 于是,我们在这里断定...根据EBP的回溯,被这种方式(叫做Detour Ret? :P)给愚弄了. 另想辙. 我们来OD里面看看实际的堆栈,这是停在Call_C里面的时候. 0012FEA0 0012FEB4 <--当前EBP 0012FEA4 004011AD <--伪造的返回地址,指向C3 0012FEA8 00000001 <- 0012FEAC 00000002 <-两个参数 0012FEB0 004010CC <--真正的返回地址! 0012FEB4 /0012FEC4 0012FEB8 |00401050 0012FEBC |00000001 0012FEC0 |00000002 当Call_C退出时,执行: pop ebp ret 8 此后寄存器状态: ebp = 0012FEB4 esp = 0012FEB0 eip = 004011AD 这时就执行到004011AD了,004011AD处的ret将使得eip = dword ptr [esp],这样就顺利的返回到004010CC了. 呃?这么看来,004010CC这个恶意的返回地址确确实实是存在于CallStack中的.关键就是怎么确定它的. EBP回溯不行,也许ESP回溯...这个具体方式我这个愚人就不知了.MJ0011就是说使用ESP回溯的.这样得考虑经过的每个call的参数个数问题. 这样我就有了一个思路: 对每一个返回地址判断一下,是否指向一个'C3'. 若是,则retaddr = 第一个参数位置 + 参数个数*4 若否,则retaddr = dword ptr [EBP + 4] 改一下check_callstack: void __stdcall check_callstack( void ) { int saved_ebp; int retaddr; //[参数个数]x4,对于内核例程,参数一般是固定的. int stack_fix = 0x8; printf("Check Call Stack Methord 2:\n"); __asm { mov eax, dword ptr [ebp+4] mov retaddr, eax mov eax, dword ptr [ebp] mov saved_ebp, eax } printf("retaddr = 0x%08X\n",retaddr); while(saved_ebp < StackBase && saved_ebp > 0) { if(saved_ebp != 0) { retaddr = *(int*)(saved_ebp+4); printf("retaddr = 0x%08X\n",retaddr); if(retaddr != 0) { if(*(unsigned char*)retaddr == 0xC3) { // // 若返回指令指向一个'C3',我们得检查在参数push之后的返回地址 // Sorry for my 丑陋的表达式 :( retaddr = *(int*)(saved_ebp+8+stack_fix); printf("Suspicious retaddr found : 0x%08x\n",retaddr); } } saved_ebp = *(int*)saved_ebp; } } } 我们来运行程序来验证一下: 没Hook的情况: retaddr = 0x0040100D retaddr = 0x00401030 retaddr = 0x00401050 retaddr = 0x00401126 retaddr = 0x0040149D retaddr = 0x7C816FD7 有Hook的情况: retaddr = 0x0040100D retaddr = 0x00401030 retaddr = 0x004011AD Suspecious retaddr found : 0x004010cc retaddr = 0x00401050 retaddr = 0x0040114D retaddr = 0x0040149D retaddr = 0x7C816FD7 比较顺利的找到属于[0x004010B0,0x004010D2]的0x004010cc 那么我们是否可用就此断定,这种堆栈回溯检测有效了? 还不可妄下结论... 如果,伪造的返回地址指向一个"C2 XXXX"? 比如,我们在Hooked_Call_B里面这么写: push xxx //这里随便push两个,与ret 8配合平衡堆栈 push xxx mov eax, b push eax mov eax, a push eax push K //这个地址指向一个'C2 08 00'(ret 8) jmp Call_B 那么,我们还得检测返回地址为C2的情况,并取得C2后面的一个WORD,通过这个WORD判断真正的返回地址在Arg[N]栈位置后面的第3个DWORD处. 更进一步,如果,伪造的返回地址K指向一个如下的指令序列: pop eax pop ebx pop ebp ret 8 我们还得对这个返回地址做一些语义(pop+ret)上的分析,才能确定真正的返回地址...它在Arg[N]栈位置后面的第6个DWORD的处... 还有 如果返回地址里还有对esp的add,sub..这些东西,呵呵,需要做检测工作的就多了去了. 虽然我在实践中实现了一个比较简单的'C3'检测,但我还是觉得这个Callstack回溯,并不是想象中好搞. 我不想和自己下棋了,没完没了......这篇陋文权当抛砖引玉了. 搞来搞去...我发现各Rootkit Coders以及ARK Coders都进入了一种Code Tricks的较量. 想象各种伎俩的RK/ARK代码在内核中堆积...他进我退他退我追他疲我生... 祸邪?福邪? 最后 感谢有人看完冗长的文章以及丑陋的代码 向以下达人及其共享的文档及其共享的精神致敬: gyzy <编写绕过卡巴主动防御的Shellcode> gyzy <基于栈指纹检测缓冲区溢出的一点思路> MJ0011 <基于CallStack的Anti-Rootkit HOOK检测思路> l0pht <点评"基于栈指纹检测缓冲区溢出的一点思路> Matt Conover (Show me his trick "without put anything extra on the callstack" :0) |
|