技术 2022 年 3 月 27 日
从汇编角度看C语言 - 1
反汇编C语言程序的解读。
从汇编角度看 C 语言 - 1
写在前面
参考书目: 《加密与解密》
- 系统平台: Windows 10;
- 调试工具: OllyDBG (吾爱破解专版) ;
- 开发工具: Visual Studio 2008 Debug 模式;
- 基础要求: 了解 C 语言和汇编语言;
- 大致内容: 简述了一些汇编语言与 C 语言的关系,方便初学者更好的认识程序的底层运行机制。
开始
用 VS2008 编译如下代码:
#include <stdio.h>
int main(int argc,char* argv[])
{
printf("Hello World\n");
return 0;
}
将生成的可执行文件拖入OllyDBG。
如何寻找 C 程序入口?
明确两点:
- mainCRTStartup 和 wmainCRTStartup 是控制台环境下多字节编码和 Unicode 编码的启动函数;
- WinMainCRTStartup 和 wWinMainCRTStartup 是 windows 环境下多字节编码和 Unicode 编码的启动函数。
mainCRTStartup 做了哪些事?
如何通过 mainCRTStartup 来找到 main 函数入口?
以上述程序为例,寻找其
main
函数入口。初步调试文件,可以发现许多
jmp
指令,这是编译器生成的启动代码,往下按 F8 跟随跳转。连续按
F8
来到call tmainCRTStartuptringtionFilter
再一次按F8
后,整个程序就会返回,因此按F7
单步进入该函数。接下来连续按下
F8
,并且观察控制台输出:发现再
call hello.00241140
后控制台打出 Hello World,因此在此处设下断点。按下
Ctrl+F2
后重新启动程序,按下F9
运行到该断点,按下F7
单步进入。按几次 F8 后看到如上界面,可以看到 Hello World 字符串,程序的开头即初始化栈帧操作,显然已经成功来到了 main 函数中。
查看如上高亮的指令,该指令将“Hello World”字符串的首地址压入栈中,而后调用 printf,将字符串打印在控制台上。
事实也正是如此!
函数
C 语言程序是由具有不同功能的函数组成的,因此在逆向分析中应将重点放在函数的识别及参数的传递上,这样做可以将注意力集中在某一段代码上。
函数的识别
下面讨论函数的识别:
调用函数的代码保存了一个返回地址,该地址会与参数一起传递给被调用的函数。绝大多数情况下编译器都使用 call 和 ret 指令来调用函数和返回调用位置。
call 指令与跳转指令功能类似,但 call 指令保存返回信息,这里的返回信息实际上主要就是返回地址。
call 指令执行时会将其之后的指令地址压入栈的顶部,当遇到 ret 指令时返回这个地址。
也就是说,call 指令给出的地址就是被调用函数的起始地址,ret 指令用于结束函数的执行。
因此可以通过定位 call 指令或 ret 指令来识别函数,call 的操作数就是所调用函数的首地址。
话不多说,看一个例子。
用 vs2008 编译如下代码,使用 OllyDBG 进行调试:
#include <stdio.h>
int add(int x,int y)
{
return x+y;
}
int main(int argc,char* argv[])
{
int a = 5,b = 6;
add(a,b);
return 0;
}
进入 OD 后,依然要越过启动代码,寻找真正的
main
函数。连续按 F8。
按照以往的经验,调用
main
函数至少在调用GetCommandLine
函数之后。于是这里继续往下执行。
看到这里可以 push 了 3 个参数,发现 argv 和 argc 的字样,那么下一个 call 十有八九会到达
main
函数了。按 F7 单步进入。
来到这里就已经很明显了,可以明显的看到下图中由
mov ..., 0x5
和mov ..., 0x6
两个语句,这明显是在给变量赋值上 5 和 6,那么就可以推测call test.00DD135C
实际上在调用add
函数,将光标移动指令处,按回车键。如下图可以看到
add
字样,因此猜想是对的。那么回到之前的
main
函数,可以看到代码将 0x5 和 0x6 放入 rax 和 ecx 寄存器后,又对其进行了压栈操作,实际等价于push 0x5
和push 0x6
。push 操作就是 x86 架构下典型的压栈方式,符合**__cdecl**调用约定(C/C++程序的默认调用约定,在此不作赘述),在该约定下,可以看到压栈顺序是逆序的,右边的参数先进栈,左边的参数后进栈,栈顶指针 esp 指向栈中第 1 个可用的数据项。
在调用函数时,调用者依次将参数压入栈,然后调用函数。函数被调用以后,在栈中取得数据并进行计算,函数计算结束后,由调用者或者函数本身修改栈,使栈恢复原样(平衡栈数据)。
现在将程序运行到 call 指令之前,查看 OD 的栈区数据,可以看到显示 Arg1=5,Arg2=6,显然这两个参数已经被压栈。
进入 add 函数后,可以看到程序将 arg1 存入 eax 寄存器,再和 arg2 相加,就完成了计算。
另外还有几种调用约定,如fastcall和stdcall。
函数的返回值
再来讨论函数的返回值:
将
add
函数修改一下,如下所示:int add(int x,int y)
{
int temp;
temp = x + y;
return temp;
}
进入 OD,再次回到 add 函数中。
mov eax, [local.2]
是将计算的最后结果就保存在 eax 寄存器中,eax 就作为存放返回值的寄存器。众所周知,带回返回值的方法不只
return
,还可传引用,查看如下代码:#include <stdio.h>
void max(int* a,int* b)
{
if(*a < *b) {
*a = *b;
}
}
int main(int argc,char* argv[])
{
int a = 5,b = 6;
max(&a, &b);
printf("max: %d",a);
return 0;
}
这里定义了一个
max
函数,接收a
和b
的地址,将其中较大数放入变量a
中。使用 OD,进入main
函数。进入
max
函数,mov eax, [arg.1]
和mov ecx, [arg.2]
是将参数a
和b
的值加载到两个寄存器。可以看到
cmp
指令,这是一个用于比较大小的指令,紧跟着的是条件跳转指令,表示如果 a<b 则不跳转,继续往下执行,这里不多说。直接看到
mov dword ptr ds:[eax], edx
。dword ptr
是指明数据宽度,而这一步操作就是将结果填入变量 a 所在的内存地址处。数据结构
局部变量
现在来谈谈局部变量。
局部变量是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。从汇编角度看,局部变量分配空间时通常会使用栈和寄存器。
编译如下代码:
#include <stdio.h>
int add(int x,int y)
{
int z;
z = x+y;
return z;
}
int main(void)
{
int a=5,b=6;
add(a,b);
return 0;
}
进入
add
函数,sub esp,0xcc
即下降栈顶 0xcc 个字节,实际上是为局部变量开辟空间,同时也在预防栈溢出攻击(一种攻击手法,此处不作赘述)。这里开辟后的空间一部分是用来存放变量 z 的。
call
调用完后,会出现add esp,0x8
这步操作实际上是在平衡栈,可以理解为“回收现场”。局部变量的起始值是随机的,是其他函数执行后留在栈中的垃圾数据,因此须要对其进行初始化。
全局变量
而全局变量作用于整个程序,它一直存在,放在全局变量的内存区中。
局部变量则存在于函数的栈区中,函数调用结束后便会消失。
在大多数程序中,常数一般放在全局变量中。
全局变量通常位于数据区块(.data)的一个固定地址处,当程序要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址。
如下是示例程序,
z
是一个全局变量:#include <stdio.h>
int z;
int add(int x,int y)
{
return x+y+z;
}
int main(void)
{
int a=5,b=6;
z=7;
add(a,b);
return 0;
}
这里是对 z 赋值,直接用
mov
将 7 写入一个固定的内存地址。在
add
函数中,同样直接从固定的地址中取出z
的值到 eax 寄存器中。数组
最后看看数组:
#include <stdio.h>
int main(void)
{
static int a[3]={0x11,0x22,0x33};
int i,s=0,b[3];
for(i=0;i<3;i++)
{
s=s+a[i];
b[i]=s;
}
for(i=0;i<3;i++)
{
printf("%d\n",b[i]);
}
return 0;
}
一般对数组的访问是通过基址加变址寻址实现的。
在内存中数组可存在于栈、数据段及动态内存中,本例中
a[]
数组就保存在数据段.data 中,其寻址用“基址+偏移量”实现。b[]
数组放在栈中,这些栈在编译时分配。数组在声明时可以直接计算偏移地址,针对数组成员寻址时是采用实际的偏移量完成的。 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
作者: [object Object] 发表日期:2022 年 3 月 27 日