逆向——C语言的汇编表示之堆栈图 手把手示例 可以考虑在函数内部加一个局部变量来综合理解

发布时间 2023-04-02 20:51:43作者: bonelee

课程概要

来自:https://gh0st.cn/Binary-Learning/C%E8%AF%AD%E8%A8%80.html 写得非常详细

本章课程需要具备汇编语言基础,若无汇编语言基础是无法去理解课程中所讲的一些知识点和技术细节的;同时也表示本课程是以汇编语言来理解C语言,透过本质理解高级语言。

关于本节课的环境:VC6,VC6是一个集成开发环境,使用VC6而不去使用较新的VS是因为VS会自己优化代码,而我们想要直接了解真正的本质就应该选择无添加的VC6。

C语言的汇编表示&函数的定义与调用

在了解C语言的汇编表示之前,我们要弄清楚C、C++、VC6、VS之间的关系,C和C++都属于编程语言,VC6、VS属于集成开发环境。

我们创建第一个C程序的顺序为(以下键盘快捷方式基于VC6):

1.创建项目(选择Win32 Console Application)

images/download/attachments/12714021/image2021-2-5_18-34-38.png

2.创建文件(Source File)

images/download/attachments/12714021/image2021-2-3_19-20-31.png

3.编写入口程序

images/download/attachments/12714021/image2021-2-3_19-39-45.png

34.构建(F7)

images/download/attachments/12714021/image2021-2-3_19-40-15.png

5.运行(F5)

images/download/attachments/12714021/image2021-2-3_19-40-40.png

如下代码就是入口函数:

void main() {
return;
}

在C语言中约定俗成的入口函数名称为main(),函数的格式是这样的:

返回类型 函数名(参数列表) {
函数体;
return 返回类型对应的数据; // 执行结束
}

定义一个函数,其返回类型、函数名是必须要有的,参数列表是可有可无的,定义函数在函数体的最后一定需要使用return返回对应数据类型的数据。

关于函数名、参数名的命名也是有要求的,如下所示:

  1. 只能以字母、数字、下划线组成;且第一个字母必须是字母或下划线。

  2. 命名严格区分大小写

  3. 不能使用C语言的关键字(例如:void、return之类)

定义好函数之后,我们需要知道如何调用函数(使用函数),假设现在我们需要做一个加减法的程序,可以这样写:

int plus(int x, int y) {
return x+y;
}
 
void main() {
plus(1,2);
return;
}

如上所示,调用函数的格式为:函数名(传入参数);,这是C语言调用函数的方法,我们之前也了解过汇编如何调用函数

push 0x1
push 0x2
call address

那么C语言其调用函数的本质是什么呢?我们可以来具体看看其编译后的反汇编代码。

单击plus(1,2);那一行,按一下F9,下一个断点,然后F7构建,F5运行。

images/download/attachments/12714021/image2021-2-10_21-36-33.png

再右击这行代码,选择如下图所示的按钮,来查看反汇编代码:

images/download/attachments/12714021/image2021-2-10_21-37-26.png

通过查看反汇编代码我们发现C语言调用函数实际上跟我们之前所学的汇编是一样的:

images/download/attachments/12714021/image2021-2-10_21-41-15.png

但需要注意的是,这里我们看见的反汇编代码是Debug版本,也就是方便我们调试的,而实际上程序编译应该是以Release版本,两个版本对应的汇编代码也是不一样的,另外VC6在展示反汇编代码的时,适当的做了一些优化,也就是便于阅读理解,例如上图所示的函数调用的汇编call指令,实际上就是call 0040100a

总结:函数名本质上就是编译器给内存地址起的名字。

 

以下汇编代码需要熟悉了解(plus函数的汇编代码实现):

1:
2: int plus(int x, int y) {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
3: return x+y;
00401028 mov eax,dword ptr [ebp+8]
0040102B add eax,dword ptr [ebp+0Ch]
4: }
0040102E pop edi
0040102F pop esi
00401030 pop ebx
00401031 mov esp,ebp
00401033 pop ebp
00401034 ret

 

VC6的快捷键:

下断点:F9

运行:F5

构建:F7

编译:Ctrl+F7

构建执行:Ctrl + F5

执行下一条:F10

执行下一条(步入内部):F11

停止调试:Shift + F5

参数传递与返回值

在上一节中我们了解到了函数,函数的本质就是一堆指令,我们可以重复调用;函数的定义在上节中我们也已经了解了,我们举一个函数的例子:

int plus(int x, int y) {
return x+y;
}

在这个函数中,其参数列表有x和y,它们我们可以理解为是一个占位符,当我们想要调用函数的时候,可以使用真正的数据替换这两个占位符。(:占位符也需要指定其数据大小,也就是数据宽度;不可以直接写作x, y)

该函数plus前面有一个int,这就表示plus函数返回类型int类型,而int类型也是表示数据宽度,其为4个字节,除此之外还有short(2个字节)char(1个字节)

我们想要了解程序的本质,就需要追踪每一行到底是如何运作的,如下代码我们来进行跟踪分析plus函数是如何运行的:

int plus(int x, int y) {
return x+y;
}
 
void main() {
plus(1,2);
return;
}

老规矩我们基于VC6的环境下,在调用plus函数那一行下断点(F9),然后(F7)构建,(F5)运行,右键进入汇编界面。

images/download/attachments/12714021/image2021-2-12_23-47-35.png

在这里,我们需要观察堆栈来观察程序的本质,这里可以借助Excel工具堆栈图便于理解,我们可以选中一列然后将其边框都填上:

images/download/attachments/12714021/image2021-2-12_23-51-12.png

记住堆栈在执行前后的变化,画堆栈图要记住两个寄存器,一个是栈顶(ESP),一个是栈底(EBP)。

在我们代码(调用plus)函数还没开始执行时候要先记住这两个寄存器的值:

images/download/attachments/12714021/image2021-2-12_23-54-14.png

将两个值填入我们的Excel表格中,再将其用颜色标记一下即可:

images/download/attachments/12714021/image2021-2-12_23-55-52.png

接下来我们就可以按照程序执行顺序来进行跟进了,我们来看一下汇编代码:

images/download/attachments/12714021/image2021-2-13_0-1-8.png

可以看见从右到左,依次压入我们调用函数传入的参数,然后再使用call指令去调用函数,在这里我们可以使用F10跟进执行

连续压入堆栈2个数据,堆栈也会根据数据宽度提升,此时我们要在堆栈图中根据变化进行修改:

images/download/attachments/12714021/image2021-2-13_0-3-46.png

而我们想要跟进call指令需要使用F11跟进,就如同学习汇编时「使用DTDebug 跟进CALL指令不能使用F8要是用F7」。

而跟进call指令之后,我们的堆栈也会发生变化,call指令下一行执行的地址会压入堆栈,栈顶也随之提升,需要注意的是在VC6中F11跟进会先过渡到一个jmp指令,然后再通过其跳到真正的函数执行地址。

images/download/attachments/12714021/image2021-2-13_0-28-42.png

images/download/attachments/12714021/image2021-2-13_0-48-50.png

images/download/attachments/12714021/image2021-2-13_0-50-49.png

接着我们再来看一下跟进的函数对应的汇编代码:

images/download/attachments/12714021/image2021-2-13_0-52-39.png

在这里看到汇编代码,我们就应该知道它要干什么了,就要通过ebp进行寻址,关于这一块,在学习汇编时也有了解到,所以还是建议各位在学习本课程时候先去学习汇编

我们先来看一下return之前的汇编代码:首先压入ebp到堆栈中,然后提升栈底(ebp)到栈顶(esp)的位置,再将栈顶(esp)提升0x40(十进制则表示64,堆栈图中也就是16个格子,这一块区域我们称之为缓冲区),后将ebx、esi、edi分别压入堆栈(此处是保存现场,为了函数执行完后恢复),而后lea指令是将ebp-0x40的地址(也就是esp提升0x40后的地址)给到edi,再将0x10(十进制则表示16)给到ecx(这里ecx是循环计数器),接着将0xCCCCCCCC给到eax,然后rep stosd(简写)就是将eax的值储存到edi指定的内存地址,默认情况下标志寄存器的 DF位为0,所以edi的值也就随循环每次递增4(dword为4字节所以是4)在这里实际上就是将哪一块缓冲区填充CC,此时堆栈图变成如下所示:

images/download/attachments/12714021/image2021-2-13_1-23-20.png

为什么缓冲区填充的数据是0xCCCCCCCC?因为CC可以起到断点的作用,填充CC就是以防程序使用缓冲区时用过了,如果用过了可以及时断点;这一块包含调试器的一些知识,这里不过多阐述。

return的汇编代码则很简单:通过ebp寻址获得传递的参数,ebp+8则是1,ebp+0xC则是2,最终结果在eax中。

当函数执行完成之后我们需要将之前压入堆栈的寄存器还原,分别pop edi → esi → ebp(堆栈遵循先入后出),而后就是恢复堆栈到函数执行之前的样子,将esp下降到ebp的位置,而后再pop ebp,还原栈底,最后ret也就是将当前栈顶的值赋给eip,然后让栈顶加4(注:这里之前使用过的数据都不会清空,如果程序运行时敏感数据存储在堆栈内则会被黑客恶意利用),但此时结束了吗?并没有,我们F10继续跟进:

images/download/attachments/12714021/image2021-2-13_1-37-20.png

可以清晰的看见esp的值加0x8,此时才是遵循了堆栈平衡,还原了堆栈在函数执行前的样子。

images/download/attachments/12714021/image2021-2-13_1-38-42.png

最后,我们来总结一下:

  1. 在C语言中参数传递是通过堆栈的,传递的顺序是从右到左

  2. 在C语言中函数返回值是存储在寄存器eax中