前两天摆弄了一下windows下的反汇编工具OllyDbg, 用起来不错,于是自己写了一个C的小程序反汇编试试。程序很简单,定义了两个自定义函数,add () 和 sub (),并传入参数。因为传入参数都只是两个,而且都是int型,总共8字节,所以这样的函数不需要用传入参数块首地址的方式传参,一般这样的函数调用都是简单函数。
此次尝试使用的OllyDbg版本为2.01 ,使用的C编译器为gcc 3.4.0 (mingw special), 运行平台为win 7 x64 sp1。
C源代码:
#include <stdio.h>
int add (int, int);
int sub (int, int);
int add (int _x, int _y) {
int tmp = _x + _y;
return tmp;
}
int sub (int _x, int _y) {
int tmp = _x - _y;
return tmp;
}
int
main (int argc, char** argv) {
int x = 20;
int y = 19;
int res_add, res_sub;
res_add = add (x, y);
res_sub = sub (x, y);
printf ("add = %d, sub = %d\n", res_add, res_sub);
return 0;
}
C语言编译出的可执行文件,其函数的调用多半是遵循下面的步骤: (我的电脑用的32位机)
1) push ebp ; 将当前上下文中的ebp值压入堆栈,
2) mov ebp, esp ; 将当前的esp 值存入ebp, 之后ebp的值基本上不变,就用ebp 结合esi 得到压入栈中的函数参数,
3) 参数压栈,此时会改变esp的值,但之前上下文中的值已经压入栈中,还可以还原回来。
4) call fxn ; 调用函数, fxn用函数起始地址代替(偏移地址)
由于这次写的C程序使用了printf (),所以可以以此作为目标,查看到底在调用哪个函数后(单步执行)控制台输出了数据。
使用OllyDbg反汇编:
这里只截取了编译出的代码部分;可以看到,在反汇编后,OllyDbg会自动将cs:si指向程序起始运行位置。但目前我还说不清楚这个位置到底是在什么的起始位置,是初始化结束后,还是main 的位置?希望高人指点。
此后一直按F8 (单步执行)运行到地址为0x00401253的位置,看到代码为 call 004011e0,即是调用函数,当单步执行后,发现控制台马上输出了字符串,说明调用两个函数和printf 的就是在这个 地址为0x004011e0的函数中。重新载入exe , 步入执行这条指令。
发现函数中又调用了很多函数,有的地址有标注,有的没有,但可以通过查看它们所在的偏移地址知道到底是属于哪个模块(dll 或者 exe等 PE文件)。通过视图-> 内存映射得到所有的内存块描述:
由此可以查看函数属于哪个地址段,在kernel32.dll还是在 msvcrt.dll等。这样就好猜测函数到底是不是用户定义函数了。
接着上面说,进入调用的0x004011e0函数后,得到刚才的代码,出现大量的call函数(其中很多地址都是0x0040... 开头的可能就是引入头文件时定义的一些函数)。再使用F8 单步执行,查看什么时候控制台出现字符串输出。当运行到0x00401228处的代码后,输出了,由此可以再次进入这个函数试试。重新载入,到0x00401228 处的代码(call 004012fa),这回看到了比较熟悉的注释:printf (),在地址0x00401371处, call <jmp.&msvcrt.printf>
在调用之前还分析出了printf 格式字符串中的两个%d,都是用mov 指令从栈中转移到指定位置的(为调用printf 准备的参数栈)。我们都知道C的普通函数返回值(若有的话)都存储在eax中。这里是从eax 传入到printf 参数栈中合适的位置。说明调用的函数就是前面的call 004012e4和 call 00401200,看来这两个函数找到了!
最后分析一下函数的调用过程,只取其中一个函数0x004012d0分析,这个函数应该就是我们定义的add () 函数。
在调用之前,会有参数压栈过程,但是这里常规的push ebp 和 mov ebp,esp 却在call 00401738和 call 00401490之前,而这两个函数做什么用,目前我不清楚。C stdcall 都是参数从左到右压栈,于是可以看到0x00401324 和0x0040132B处的代码是先压入14, 再压入13,又通过eax将数据转移到esp适当的位置,等待函数调用。进入函数:
如此这里出现了一直寻找的push ebp 和 mov ebp, esp ,之后就可以通过ebp访问传入的参数了。当然,调用结束后,还要恢复调用前上下文的esp和ebp,这里就交给了leave和retn处理。32位机中,leave 同
mov esp, ebp
pop ebp