首页
社区
课程
招聘
[转帖]伪造返回地址绕过CallStack检测
发表于: 2008-4-28 09:03 13345

[转帖]伪造返回地址绕过CallStack检测

2008-4-28 09:03
13345
该文是我在以下博客地址发现的:
http://hi.baidu.com/ring3world/blog/index/6
该转帖没有征得作者的同意。
我觉得此文不错,于是发布出来。
自己对文中涉及的代码加了些注释,还有图片。

伪造返回地址绕过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  //从一开始的push  Arg[N]指令 到这里,我们实际上是自己模拟实现 call 指令的动作
     jmp      ZwXxx; // 自己模拟实现 call ZwXxx() ;返回地址 K 在合法模块 ntoskrnl.exe 中
                     //这种模拟技术还可以用于 bypass inline HOOK
     __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    //得到并保存函数的返回地址,不明白的话请看附图:_stdcall函数调用堆栈
         mov eax, dword ptr [ebp]
         mov saved_ebp, eax  //保存原始的EBP
     }
     printf("retaddr = 0x%08X\n",retaddr);
   
     while(saved_ebp < StackBase && saved_ebp > 0) //检查EBP的有效性
     {
         if(saved_ebp != 0)
         {
             retaddr = *(int*)(saved_ebp+4);    //ebp +4 为函数的返回地址 ,请看附图:_stdcall函数调用堆栈
             printf("retaddr = 0x%08X\n",retaddr);
             saved_ebp = *(int*)saved_ebp;     //(回溯操作)修改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 //★注意,0x004011AD 处的 "C3",即ret 指令,是对应这个CALL的
         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:   //将Call_A()中"call Call_B"指令的位置保存在 hook_pos 中
   
     //
     // 用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"? //但是记着,我们自己的HOOK函数内部,不能使用EBP寄存器哦。作者正是使用EBP回溯来检查函数返回地址
比如,我们在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)

附图:



[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (7)
雪    币: 709
活跃值: (2420)
能力值: ( LV12,RANK:1010 )
在线值:
发帖
回帖
粉丝
2
1243546
2008-4-28 12:36
0
雪    币: 67
活跃值: (66)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
3
值得学习
2008-4-28 19:51
0
雪    币: 197
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
好冷清哦
2008-5-3 15:32
0
雪    币: 451
活跃值: (78)
能力值: ( LV12,RANK:470 )
在线值:
发帖
回帖
粉丝
5
在第一页学习
2008-5-3 19:38
0
雪    币: 381
活跃值: (140)
能力值: ( LV13,RANK:330 )
在线值:
发帖
回帖
粉丝
6
想象各种伎俩的RK/ARK代码在内核中堆积...他进我退他退我追他疲我生...
祸邪?福邪?


的确如此, 凡是都有解决的方法,无非是花点时间构思调试而已
2008-5-4 10:05
0
雪    币: 208
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
7
ring3world,好像论坛上以前谁说过是个mm吧
2008-5-5 11:31
0
雪    币: 197
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
不知道是男是女
2008-5-5 20:10
0
游客
登录 | 注册 方可回帖
返回
//