在论坛上看到一个贴子
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;
形式也完全可以。
另外本分析也可作为缓冲区溢出攻击学习的一个基础。
本人汇编很弱,不对之处,欢迎拍砖! ^_^