push ebp ; 提升堆栈
mov ebp,esp
sub esp,0CCh
------------------------------------------
push ebx ; 保留现场,函数在执行的时候会用到一些寄存器,但这些寄存器中
push esi ; 值很可能会被程序用到,所以要先存储到内存中
push edi
push ecx
------------------------------------------
lea edi,[ebp-0CCh] ; 向分配的空间填充数据
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
----------------------------------
pop ecx ; 函数实际实现的功能
mov dword ptr [this],ecx
mov eax,dword ptr [this]
mov dword ptr [eax+4],1
mov eax,dword ptr [nID]
push eax
push offset string "\r\nID:%d Who is your God? I am!\r\n"... (0EC6E94h)
call _printf (0E33D73h)
add esp,8
----------------------------------------
pop edi ; 恢复现场,将之前保留的寄存器的值恢复
pop esi
pop ebx
-----------------------------------------
add esp,0CCh ; 堆栈平衡
cmp ebp,esp
call __RTC_CheckEsp (0E32356h)
mov esp,ebp ; 降低堆栈
pop ebp ; 恢复栈底
ret 4 ; 函数执行完毕,返回到调用处,等同于pop eip
逆向——C语言的汇编表示之堆栈图 手把手示例 可以见后面在函数内部加一个局部变量以及嵌套调用的例子来综合理解
Posted 将者,智、信、仁、勇、严也。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向——C语言的汇编表示之堆栈图 手把手示例 可以见后面在函数内部加一个局部变量以及嵌套调用的例子来综合理解相关的知识,希望对你有一定的参考价值。
课程概要
来自: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)
2.创建文件(Source File)
3.编写入口程序
34.构建(F7)
5.运行(F5)
如下代码就是入口函数:
void
main()
return
;
在C语言中约定俗成的入口函数名称为main(),函数的格式是这样的:
返回类型 函数名(参数列表)
函数体;
return
返回类型对应的数据;
// 执行结束
定义一个函数,其返回类型、函数名是必须要有的,参数列表是可有可无的,定义函数在函数体的最后一定需要使用return返回对应数据类型的数据。
关于函数名、参数名的命名也是有要求的,如下所示:
-
只能以字母、数字、下划线组成;且第一个字母必须是字母或下划线。
-
命名严格区分大小写
-
不能使用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运行。
再右击这行代码,选择如下图所示的按钮,来查看反汇编代码:
通过查看反汇编代码我们发现C语言调用函数实际上跟我们之前所学的汇编是一样的:
但需要注意的是,这里我们看见的反汇编代码是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)运行,右键进入汇编界面。
在这里,我们需要观察堆栈来观察程序的本质,这里可以借助Excel工具画堆栈图便于理解,我们可以选中一列然后将其边框都填上:
记住堆栈在执行前后的变化,画堆栈图要记住两个寄存器,一个是栈顶(ESP),一个是栈底(EBP)。
在我们代码(调用plus)函数还没开始执行时候要先记住这两个寄存器的值:
将两个值填入我们的Excel表格中,再将其用颜色标记一下即可:
接下来我们就可以按照程序执行顺序来进行跟进了,我们来看一下汇编代码:
可以看见从右到左,依次压入我们调用函数传入的参数,然后再使用call指令去调用函数,在这里我们可以使用F10跟进执行。
连续压入堆栈2个数据,堆栈也会根据数据宽度提升,此时我们要在堆栈图中根据变化进行修改:
而我们想要跟进call指令需要使用F11跟进,就如同学习汇编时「使用DTDebug 跟进CALL指令不能使用F8要是用F7」。
而跟进call指令之后,我们的堆栈也会发生变化,call指令下一行执行的地址会压入堆栈,栈顶也随之提升,需要注意的是在VC6中F11跟进会先过渡到一个jmp指令,然后再通过其跳到真正的函数执行地址。
接着我们再来看一下跟进的函数对应的汇编代码:
在这里看到汇编代码,我们就应该知道它要干什么了,就要通过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,此时堆栈图变成如下所示:
为什么缓冲区填充的数据是0xCCCCCCCC?因为CC可以起到断点的作用,填充CC就是以防程序使用缓冲区时用过了,如果用过了可以及时断点;这一块包含调试器的一些知识,这里不过多阐述。
return的汇编代码则很简单:通过ebp寻址获得传递的参数,ebp+8则是1,ebp+0xC则是2,最终结果在eax中。
当函数执行完成之后我们需要将之前压入堆栈的寄存器还原,分别pop edi → esi → ebp(堆栈遵循先入后出),而后就是恢复堆栈到函数执行之前的样子,将esp下降到ebp的位置,而后再pop ebp,还原栈底,最后ret也就是将当前栈顶的值赋给eip,然后让栈顶加4(注:这里之前使用过的数据都不会清空,如果程序运行时敏感数据存储在堆栈内则会被黑客恶意利用),但此时结束了吗?并没有,我们F10继续跟进:
可以清晰的看见esp的值加0x8,此时才是遵循了堆栈平衡,还原了堆栈在函数执行前的样子。
最后,我们来总结一下:
-
在C语言中参数传递是通过堆栈的,传递的顺序是从右到左
-
在C语言中函数返回值是存储在寄存器eax中
变量
在编写程序的时候,经常需要存储数据,前面学习汇编时了解到了,数据可以存在寄存器中,或者内存中。在C语言中,存储数据要存在变量中,变量就是一个容器(通常就是内存);变量类型决定变量内存宽度,变量名就是内存地址的编号(别名)。
声明变量的格式:
变量类型 变量名;
注:变量类型用来说明数据宽度是多大
例:int 4字节、short 2字节、char 1字节
变量的命名规范与函数名、参数名一样:
-
只能以字母、数字、下划线组成;且第一个字母必须是字母或下划线。
-
命名严格区分大小写
-
不能使用C语言的关键字(例如:void、return之类)
在C语言中,变量有两类:全局变量、局部变量。
全局变量:
-
在函数体外定义,并且作用于全局;
-
在程序编译完成后,内存地址和宽度就已经确定下来了,变量名就是内存地址的别名;
-
只要程序启动,全局变量就已经存在,如若变量在一开始声明时没有赋值,则初始值为0;
-
如果不重新编译,全局变量的内存地址永远都不会变;
-
全局变量中的值任何程序都可以改,其最终存储的就是最后一次修改的值。
在这里我们简单的来定义一个全局变量:
int
a =
123
;
int
plus()
return
a+
1
;
void
main()
plus();
return
;
我们跟进看一下反汇编代码:
全局变量a实际上就是一个内存地址,接着来看其存储的内容(0x7B转为十进制就是123):
局部变量:
-
在函数体内定义,作用于当前所在函数;
-
局部变量的内存是在堆栈中分配的,程序执行时才分配,不执行则不会分配,我们无法预知程序何时执行,也就意味着我们无法知道局部变量的内存地址;
-
不确定局部变量的内存地址,所以其也就只能作用于当前函数内部,其他函数不能使用。
int
plus()
int
a =
123
;
return
a+
1
;
void
main()
plus();
return
;
最后结论:
-
全局变量是可以没有初始值直接使用的,因为系统默认给其0为初始值;
-
局部变量在使用前必须要赋值,因为系统不会初始化它,而只有在其赋值时才会分配内存。
变量与参数的内存布局
我们已经掌握了函数、函数调用、变量、参数、返回值等相关的一些概念,本借口我们从内存的角度来分析参数在内存中的位置、局部变量在内存中的位置、返回值是如何返回和使用的。
以下示例代码,就是一个调用函数(x+y),结果给了局部变量z,然后返回z:
int
plus(
int
x,
int
y)
int
z = x + y;
return
z;
void
main()
plus(
1
,
2
);
return
;
在这里,我们还是下断点一步一步跟进,然后画堆栈图分析即可,如下是堆栈图及其对应汇编代码(这不是整个函数执行完后的堆栈图):
如上图所示,我们可以清晰的看见参数在内存中的位置就是ebp+8、ebp+c...以此类推;局部变量则位于我们之前所说的缓冲区,也就是ebp-4、ebp-8...以此类推,这也就是为什么局部变量使用前需赋初值,不然里面是垃圾数据(CC)。
接下来我们需要知道返回值是如何返回和使用的,在C语言中使用返回值就需要一个容器来存储这个返回值,这个容器我们也称之为变量,如下示例代码:
int
plus(
int
x,
int
y)
int
z = x + y;
return
z;
void
main()
int
a;
a = plus(
1
,
2
);
return
;
我们来看下汇编代码:
可以看见这里会将eax放入到当前函数的缓冲区(main函数,ebp-4),也就是将返回值存到当前函数的缓冲区内。
函数嵌套调用的内存布局
函数嵌套调用实际上就是函数中调用另外一个函数,以下为示例代码:
int
plus1(
int
x,
int
y)
return
x+y;
int
plus(
int
x,
int
y,
int
z)
int
r;
r = plus1(x,y);
return
r+z;
void
main()
int
a;
a = plus(
1
,
2
,
3
);
return
;
老规矩我们还是在plus函数那下断点跟踪画堆栈图,如下是堆栈图:
在调用完plus1函数后,plus函数会有这样一个汇编代码:
00401094
add esp,44h
00401097
cmp ebp,esp
00401099
call __chkesp (004010b0)
需要注意的是这个代码只有Debug版本才会有,而在Release版本中堆栈的布局与这是不一样的。
这段代码的意思就是对比esp和ebp是否一样,而我们知道堆栈在使用完成之后要恢复成员来的样子(堆栈平衡),所以在add指令之后ebp与esp应该是一样的,而后的call指令实际上就是调用了一个函数(__chkesp),这个函数就是用来检查你的堆栈是否平衡的。
至此,我们就了解了函数嵌套调用的内存布局,但实际上我们在之前就已经了解过了,因为main本身也是一个函数,main调用了plus也属于函数嵌套调用,只不过我们画堆栈图是在调用plus之前画的,所以忽略了这一点。
逆向知识堆栈图-汇编中的函数
以上是关于逆向——C语言的汇编表示之堆栈图 手把手示例 可以见后面在函数内部加一个局部变量以及嵌套调用的例子来综合理解的主要内容,如果未能解决你的问题,请参考以下文章