原题如下: 求这个函数的输出结果:
#pragma pack(1)
struct Node
{
char a[10];
short b;
};
void func()
{
int i;
struct Node c[2];
for(i=0;i<2;i++)
{
c[i].b = i;
sprintf(c[i].a,"This is a %d\n",i);
printf("%d, %s",c[i].b,c[i].a);
}
}
近日学习了一点反汇编知识,看到蓬蓬发的此题,涉及到数据的内存对齐以及利用溢出(exploit)来改变
原程序的执行流程,下面是我对该程序反汇编结果以及部分注释。
注意:为了调试方便 我把循环20次改为了2次,但效果是一样的
1: // define.cpp : Defines the entry point for the console application.
2: //
3:
4: #include "stdafx.h"
5: #include <stdio.h>
6: #include <stdlib.h>
7:
8:
9:
10: #pragma pack(1)
;设置数据按1字节对齐,如果不设则按照编译器设定的值处理(编译器相关)
11: struct Node
12: {
13: char a[10];
14: short b;
15: };
16:
17: void func()
18: {
00401020 push ebp
00401021 mov ebp,esp ;这两行保存堆栈信息,函数结束时进行恢复 这是Windows调用规范
00401023 sub esp,5Ch ;创建该函数的内部堆栈(用于存储局部变量)
00401026 push ebx
00401027 push esi
00401028 push edi ;这三行保存三个寄存器:ebx,esi,edi,这是C调用规范
00401029 lea edi,[ebp-5Ch] ;将ebp-5ch,也就是sp的值放入edi
0040102C mov ecx,17h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
;这三行是对堆栈内容进行初始化,0xCC是INT3中断,这是debug模式下的初始化内存模式
19: int i;
20: struct Node c[2];
21:
22: for(i=0;i<2;i++)
00401038 mov dword ptr [ebp-4],0 ;ebp-4即为i在堆栈的地址,以后要访问i就通过ebp-4
0040103F jmp func+2Ah (0040104a) ;跳转至循环条件判断处
00401041 mov eax,dword ptr [ebp-4]
00401044 add eax,1
00401047 mov dword ptr [ebp-4],eax ;这三行是控制循环变量也就是实现i++
0040104A cmp dword ptr [ebp-4],2 ;判断i<2
0040104E jge func+81h (004010a1) ;如果>=2则跳出循环
23: {
24: c[i].b = i;
00401050 mov ecx,dword ptr [ebp-4]
00401053 imul ecx,ecx,0Ch
00401056 mov dx,word ptr [ebp-4]
0040105A mov word ptr [ebp+ecx-12h],dx
;这四行是给结构体成员赋值,需要注意的结构体数组中成员访问的方式,先去i,然后i*sizeof(node),
;然后用数组的基地址+i*sizeof(node),然后再取结构体内的c[i].b成员(ebp+ecx-12h)
25: sprintf(c[i].a,"This is a %d\n",i);
0040105F mov eax,dword ptr [ebp-4]
00401062 push eax
;函数调用时的压栈: 从右->左 先压 i
00401063 push offset string "This is a %d\n" (0041ca58)
;压入 字符串的地址
00401068 mov ecx,dword ptr [ebp-4]
0040106B imul ecx,ecx,0Ch
0040106E lea edx,[ebp+ecx-1Ch]
00401072 push edx
;再压入c[i].a,注意取c[i].a的方式 和上面取c[i].b的方式是一样的
00401073 call sprintf (004011f4) ;Call 函数调用
00401078 add esp,0Ch ;恢复调用后的堆栈指针
26: printf("%d, %s",c[i].b,c[i].a);
0040107B mov eax,dword ptr [ebp-4]
0040107E imul eax,eax,0Ch
00401081 lea ecx,[ebp+eax-1Ch]
00401085 push ecx ;调用前的压栈c[i].a
00401086 mov edx,dword ptr [ebp-4]
00401089 imul edx,edx,0Ch
0040108C movsx eax,word ptr [ebp+edx-12h]
00401091 push eax ;调用前的压栈c[i].b
00401092 push offset string "%d, %s" (0041ca4c) ;压栈
00401097 call printf (0040119e) ;调用
0040109C add esp,0Ch ;恢复调用栈
27: }
0040109F jmp func+21h (00401041) ;跳转至循环开始处0x00401041
28: }
004010A1 pop edi ;函数结束时的 恢复工作
004010A2 pop esi
004010A3 pop ebx ;函数调用入口时压栈,结束时退栈,恢复原始状态的寄存器
004010A4 add esp,5Ch ;恢复esp
004010A7 cmp ebp,esp
004010A9 call _chkesp (004012e6) ;校验是否ebp==esp,如果不等则表示函数调用有错,会报异常
004010AE mov esp,ebp
004010B0 pop ebp
004010B1 ret
大家可以把原程序在VC编译运行下,会发现是一个死循环的打印,从源程序看次数应该是2次才对,
为什么会是死循环了?原因就在于 sprintf 这个函数上,在func()入口初始化 i 和 Node c[2]时,
它们在堆栈中的地址是连续在一起的,sprintf函数在调用后导致的结果就是把i(ebp-4)的值清0了,
也就是说 每次i++完后,调用sprintf又会把i给置0,因此i永远是0或1。
还有一点就是被sprintf同时被覆盖的c[i].b这个值,这个值也将永远是2608或2609,为什么会这样?
原因就在于c[i].b(ebp+ecx-12h)被 30 0A 或者 31 0A,Intel的x86机器是little-ending的,
其原始值就是 2608 或 2609 了。 下面是调试期间堆栈的内存情况 此时ebp=0012FF18 i=ebp-4=0012FF14
0012FEF8 CC CC CC CC 烫烫
0012FEFC 54 68 69 73 This
0012FF00 20 69 73 20 is
0012FF04 61 20 30 0A a 0.
0012FF08 54 68 69 73 This
0012FF0C 20 69 73 20 is
0012FF10 61 20 31 0A a 1.
0012FF14 00 00 00 00 ....
0012FF18 78 FF 12 00 x...
好了,我的分析也就到此了,如果有感兴趣的可以更进一步交流。
我也是刚刚学习此方面内容,有些地方可能理解的不够到位或者错误的地方,还请大牛们给与指正。
本文来源于栀子博客-栀子花驿站 http://www.zhizihua.com/blog/ , 原文地址:http://www.zhizihua.com/blog/post/反汇编学习笔记--溢出初探.html
让我搞不明白的是
sprintf(c[i].a,"This is a %d\n",i);
会把 c[i].b的两个字节给覆盖掉 就像堆栈中所显示的那样
0012FF10 61 20 31 0A a 1. 中的 31 0A 是
c[i].b的值 被 1(0x31) 和 \n (0x0A) 覆盖
但是0012FF14 00 00 00 00 .... 也就是 i 所在的位置 为什么也被覆盖成0x00了?
其中的原理是什么?
难道是返回值吗?但是从MSDN里查到的结果是sprintf 函数返回的是 成功拷贝的字符数
我跟踪了 返回值也不是 0x00......
这彻底让我迷惑了。。。。。
我在debugman的公开群里求助无果,因此发此贴来向大家请教,请巨牛们帮看看,谢谢先!
[课程]FART 脱壳王!加量不加价!FART作者讲授!