奇妙的堆栈

来源:互联网 发布:ubuntu搭建nginx php7 编辑:程序博客网 时间:2024/04/30 17:40

在论坛上看到一个贴子
http://community.csdn.net/Expert/topic/4436/4436299.xml?temp=.4494287
要求猜猜下面这个程序的运行结果

#include <stdio.h>
#include <stdlib.h>

int funa(void)
{
 printf("AAAAA");
 exit(3);
 return 0;
}

int funb(int (*p)(void))
{
 char *h = (char*)&h;
 h += sizeof(char *);
 h += sizeof(char *);
 (*(int (**)(void))(h)) = p; 
 return 0;
}

int main()
{
 funb(funa);
 return 0;
}

其实要知道它的运行结果,汇编就是基础。正好现在在看汇编,对这个程序用 VC6 反汇编分析了一下:
 
一. 从 main() 开始:

19:   /* main() 由 crt0.c 中的 mainCRTStartup() 调用 */
20:   int main()
21:   {

004010E0   push        ebp ; ebp 入栈
004010E1   mov         ebp,esp ; mainCRTStartup() 的 esp 变为 main() 的新 ebp
004010E3   sub         esp,40h ; main() 的新 esp, 40h 为填充区大小
004010E6   push        ebx ; 保护 ebx
004010E7   push        esi ; 保护 esi
004010E8   push        edi ; 保护 edi
; 下 4 句开辟一块全是 CC 的安全区
004010E9   lea         edi,[ebp-40h] ; edi 其实从新 esp 处开始
004010EC   mov         ecx,10h  ; stos 进行的次数, 10h * 4 = 40h
004010F1   mov         eax,0CCCCCCCCh ; 一次要添充的数据, CC 即 int 3, 防缓冲区溢出攻击?
; 重复进行 ecx 次, 每次 stos 将 eax 中的东西添充到 edi 处, 每添充完一次 edi + 4
004010F6   rep stos    dword ptr [edi] 
22:      funb(funa);
; 将"函数指针"压栈。注意"函数指针"指向的不是函数直接的内存地址, 而是"跳转到该函数"的一个 jmp 语句的地址, 见二
; 这应该是"函数指针"和一般类型指针的区别,毕竟"函数指针"是要给 eip 用的。
004010F8   push        offset @ILT+5(_funa) (0040100a)
; 调用 funb(), 可以理解 call 语句由 2 个语句组成: push + jmp, 即
; push 00401102h  ; 当前 call 语句的下条语句地址, 用于执行 funb() 后继续在 main() 中向下执行
; jmp  0040100fh  ; 调转到 funb() 的"函数指针" 0040100fh 处, 在 0040100fh 处再次调转到才到真正的函数体, 见二
004010FD   call        @ILT+10(_funb) (0040100f) ; ---> 去三
; 用缺省 __cdecl 方式来调用 funb(), 完后需要调用者 main() 来清理参数占用的堆栈
00401102   add         esp,4 
23:      return 0;
00401105   xor         eax,eax
 ; 异或 eax 为 0
24:   }
00401107   pop         edi ; 恢复 edi
00401108   pop         esi ; 恢复 esi
00401109   pop         ebx ; 恢复 ebx
; 以下三句对 esp 进行运行时检查, 和编译参数"启用运行时调试检查" /GZ 有关,
; 加了 /GZ 且当前函数调用了其他函数后会进行这种检查
0040110A   add         esp,40h ; 见 004010E3 行
0040110D   cmp         ebp,esp ; 计算 ebp - esp 来影响 efl, ebp、esp 自身不变化
0040110F   call        __chkesp (00401380) ; 进入错误处理函数, 根据 efl 进行相应处理
00401114   mov         esp,ebp ; 恢复 mainCRTStartup() 的 esp
00401116   pop         ebp ; 恢复 mainCRTStartup() 的 ebp, 注意这时 esp 要变化的
00401117   ret   ; 返回 mainCRTStartup()

二. 看看本程序的三个函数指针,指向的其实是三个 jmp 到函数体的语句:

@ILT+0(_main):
00401005   jmp         main (004010e0)
@ILT+5(_funa):
0040100A   jmp         funa (00401030)
@ILT+10(_funb):
0040100F   jmp         funb (00401080)


三. 再看看 funb() 函数:

10:
11:   int funb(int (*p)(void))
12:   {

; 堆栈大致结构是:
; ... | 00401102(返回到main的地址) | 0040100a(funa的指针) | ... | ...
;    /|/                                                      /|/
;     |                                                        |
;    ESP (内存低处)                                           EBP (内存高处)

00401080   push        ebp ; 保存旧 ebp
00401081   mov         ebp,esp ; 原 esp 变为新 ebp
00401083   sub         esp,44h ; 为 funb 开辟 44h 大小的填充区
00401086   push        ebx
00401087   push        esi
00401088   push        edi
00401089   lea         edi,[ebp-44h]
0040108C   mov         ecx,11h
00401091   mov         eax,0CCCCCCCCh
00401096   rep stos    dword ptr [edi] 
; 同上
13:      char *h = (char*)&h;
00401098   lea         eax,[ebp-4]
 ; ebp - 4 处即为本函数的第一个变量
0040109B   mov         dword ptr [ebp-4],eax ; 在 h 处保持自己的地址

; 此时堆栈大致结构是:
; ... | ... | h(保持自己的地址) | 旧EBP | 00401102(返回到main的地址) | 0040100a(funa的指针) | ...
;    /|/                      /|/ /|/              /|/                     /|/           /|/
;     |                        |   |                |                       |             |
;    ESP (内存低)              EBP @@@             ***                     ###      (内存高)                              

14:       h += sizeof(char *);
0040109E   mov         ecx,dword ptr [ebp-4]
004010A1   add         ecx,4
004010A4   mov         dword ptr [ebp-4],ecx
 ; h 处保持 "@@@" 处地址
15:      h += sizeof(char *);
004010A7   mov         edx,dword ptr [ebp-4]
004010AA   add         edx,4
004010AD   mov         dword ptr [ebp-4],edx 
; h 处保持 "***" 处地址
         /* 关键地方了!!!*/
16:      (*(int (**)(void))(h)) = p; 
004010B0   mov         eax,dword ptr [ebp-4]
 ; h 处的值即 "***" 处地址 -> eax
004010B3   mov         ecx,dword ptr [ebp+8] ; "###" 处地址的值 -> ecx
004010B6   mov         dword ptr [eax],ecx ; "***" 处的值就是"###" 处地址的值了!!!

; 此时我们不难理解 h 实际应该是什么类型?当然是 (int (**)(void)) 型!
; 此时堆栈大致结构是: 
; ... | ... | h(保持"***"处的地址) | 旧EBP | 0040100a(funa的指针) | 0040100a(funa的指针) | ...
;    /|/                          /|/ /|/         /|/                     /|/           /|/
;     |                            |   |           |                       |             |
;    ESP (内存低)                 EBP @@@         ***                     ###         (内存高)

17:      return 0;
004010B8   xor         eax,eax
18:   }
004010BA   pop         edi
004010BB   pop         esi
004010BC   pop         ebx
004010BD   mov         esp,ebp 
; 开始恢复原 ESP, 见 00401081 处

; 此时堆栈大致结构是:
; ... | h(保持"***"处的地址) |   旧EBP | 0040100a(funa的指针) | 0040100a(funa的指针) | ...
;                           /|/   /|/       /|/                    /|/          /|/
;                            |     |         |                      |            |
;                         ESP/EBP @@@       ***                    ###        (内存高)

004010BF   pop         ebp ; 看看 ESP 指向哪里?恢复旧 EBP, 妙啊!

; 此时堆栈大致结构是:
; ... | h(保持"***"处的地址) | 旧EBP | 0040100a(funa的指针) | 0040100a(funa的指针) | ... | ...
;                                   /|/      /|/                        /|/             /|/
;                                    |        |                          |               |
;   (内存低)                        ESP      ***                        ###             EBP(内存高)

; 可以理解 ret 是两条指令组成:pop + jmp, 即
; pop eip ; 看看 ESP 指向哪里?funa的指针赋给 eip !
; jmp eip ; 调转到 funa() 的"函数指针" 0040100ah 处, 在 004010afh 处再次调转到才到真正的函数体   
004010C0   ret

; 此时堆栈大致结构是:
; ... | h(保持"***"处的地址) | 旧EBP | 0040100a(funa的指针) | 0040100a(funa的指针) | ... | ...
;                                        /|/               /|/          /|/             /|/
;                                         |                 |            |               |
;   (内存低)                             ***               ESP          ###             EBP(内存高)
;
; 可见,如果正常返回到 main() 我们将执行
; 00401102   add         esp,4
; 调整 esp 后, 一切好象什么都没发生一样!!!

四. 最后是 func() 函数, 就不做详细解释了

1:    #include <stdio.h>
2:    #include <stdlib.h>
3:
4:    int funa(void)
5:    {
00401030   push        ebp
00401031   mov         ebp,esp
00401033   sub         esp,40h
00401036   push        ebx
00401037   push        esi
00401038   push        edi
00401039   lea         edi,[ebp-40h]
0040103C   mov         ecx,10h
00401041   mov         eax,0CCCCCCCCh
00401046   rep stos    dword ptr [edi]
6:       printf("AAAAA");
00401048   push        offset string "AAAAA" (0042201c)
0040104D   call        printf (00401300)
00401052   add         esp,4
7:       exit(3); /* 再也回不去了,我们直接返回操作系统。 */
00401055   push        3
00401057   call        exit (00401170)
8:       return 0;
9:    }
0040105C   pop         edi
0040105D   pop         esi
0040105E   pop         ebx
0040105F   add         esp,40h
00401062   cmp         ebp,esp
00401064   call        __chkesp (00401380)
00401069   mov         esp,ebp
0040106B   pop         ebp
0040106C   ret

五. 需要说明的一点是本程序如果能顺利执行,看到"AAAAA", 纯是运气!不是每个编译器对堆栈都是这么处理的。比如 VC7.1 处理函数每个变量时,会在堆栈中每个变量前后都插上 "CCCCC", 应该是做一些基本的安全处理,这就不象 VC6 所有变量都是在 EBP 前紧密相连的。那么第一个

14:       h += sizeof(char *);

就根本不是旧EBP 的地址,后面就无重谈起了。不过多进行  h += sizeof(char *) 几次还是可能成功的。

六. 需要说明的第二点,关键语句

(*(int (**)(void))(h)) = p;

也不一定是唯一的形式,我们从内存的角度把其改成例如

*(char**)h = (char*)p;

形式也完全可以。

另外本分析也可作为缓冲区溢出攻击学习的一个基础。

本人汇编很弱,不对之处,欢迎拍砖! ^_^

原创粉丝点击