C/C++逆向:函数逆向分析-总体流程(整型指针)

server/2024/12/22 15:52:17/

函数的初始化

在逆向工程中,函数的初始化操作是函数在开始执行时,为正确运行而进行的准备工作。通常,这些操作发生在函数的序言(Prologue)阶段,具体的内容和顺序会因编译器、调用约定和目标平台(如x64x86)不同而有所差异。函数的序言(Prologue)阶段标志着函数的开始,并且包含了设置栈帧、保存上下文等关键操作。尽管在不同的体系结构和编译器优化情况下可能会有所变化,但总体上有一些通用的方式可以帮助识别它。下面分别介绍x86和x64架构下的典型序言。

序言阶段的典型操作:
①保存调用者的栈帧:通过push ebp将调用者的栈帧基址寄存器ebp压入栈中。
②建立新的栈帧:通过mov ebp, esp将当前的栈指针esp赋值给栈帧基址寄存器ebp,从而在新函数中创建一个新的栈帧。
③开辟栈空间:通过sub esp, X来减少栈指针esp的值,从而为局部变量或保存的数据开辟空间。X代表所需栈空间的大小。
④保存失性寄存器:非易失性寄存器如ebx、esi、edi等通常也在序言阶段被保存到栈上(如push ebx等操作)。
1. 标准序言的结构

在大多数情况下,函数的序言遵循一定的结构模式,特别是在有栈帧的情况下。下面分别介绍x86x64架构下的典型序言。

x86架构中的序言

x86下,使用基址指针寄存器(ebp)和栈指针寄存器(esp)来管理栈帧。典型的函数序言包括以下几个步骤:

push    ebp          ; 保存前一个栈帧的基址
mov     ebp, esp     ; 设置新的栈帧基址
sub     esp, 0xXX    ; 为局部变量分配空间

push ebp:将调用者的栈帧基址保存到栈中,以便函数返回时可以恢复。

mov ebp, esp:将当前的栈指针(esp)复制到基址指针(ebp)中,建立新的栈帧。

sub esp, XX:为函数的局部变量在栈上分配空间,XX是分配的字节数,可能会因函数复杂度而异。

esp和ebp是用于栈操作的两个重要寄存器,esp指向栈顶,栈中的每一次压栈(push)和弹栈(pop)操作都会通过esp来进行;ebp通常被用作栈帧基址寄存器,指向函数调用时栈帧的起始位置,也就是函数进入时的栈顶位置。
x64架构中的序言

类似于x86,但x64中的寄存器名称不同,例如使用rbprsp

push    rbp          ; 保存调用者的栈帧基址
mov     rbp, rsp     ; 设置当前函数的栈帧基址
sub     rsp, 0xXX    ; 为局部变量分配空间
2.保存非易失性寄存器

在某些调用约定(如stdcallfastcall)下,函数在进入时需要保存非易失性寄存器,以便在函数结束时能够恢复调用者的上下文。非易失性寄存器(Callee-saved registers)是指在函数调用过程中,如果被调用的函数修改了这些寄存器,它需要在函数结束时将其恢复到原来的状态。这样可以保证调用者在调用函数后,这些寄存器的值不会被破坏。

x86保存寄存器的典型序言
push    ebx
push    esi
push    edi
x64保存寄存器的典型序言
push    rbx
push    r12
push    r13

这些保存寄存器的指令通常出现在函数的序言阶段,他们是函数初始化的一部分,标志着对调用者上下文的保护。接着我们通过一个实际例子来进行具体分析。下面是一个 C 代码的示例:

#include <stdio.h>
​
// 传递多个整型参数
int sumIntegers(int a, int b, int c, int d, int e) {return a + b + c + d + e;
}
​
// 传递多个整型指针
void modifyIntegers(int *p1, int *p2, int *p3, int *p4, int *p5) {*p1 = 10;*p2 = 20;*p3 = 30;*p4 = 40;*p5 = 50;
}
​
int main() {int x = 1, y = 2, z = 3, w = 4, v = 5;
​int sum = sumIntegers(x, y, z, w, v);printf("Sum: %d\n", sum);
​modifyIntegers(&x, &y, &z, &w, &v);printf("Modified values: %d, %d, %d, %d, %d\n", x, y, z, w, v);
​return 0;
}

使用VS对上述代码进行编译,生成x86x64架构的exe程序。接着将这两个程序放入x96dbg中进行分析:

①x86架构

将x86架构程序载入x32dbg中

定位main函数,得到如下代码:

在图中红色方框中的代码就是main函数的序言部分:

push ebp
mov ebp,esp
sub esp,10C
push ebx
push esi
push edi

push ebp:保存调用者的栈帧基址,将调用者的ebp寄存器的值压入栈中,目的是在函数返回时恢复调用者的栈帧。

mov ebp, esp:设置当前函数的栈帧基址,将当前栈指针esp的值赋给ebp,这意味着ebp现在是当前函数的栈帧基址,用于访问局部变量和参数。

sub esp, 10C:为局部变量分配栈空间,减少栈指针esp的值,开辟0x10C(即268字节)的栈空间。这部分空间通常用于存储局部变量或其他需要在栈上分配的临时数据。

后面的三个指令则是对非易失性寄存器(ebxesiedi)进行保存。

再接下去就是初始化局部变量区域:通过rep stosd指令将0xCCCCCCCC填充到局部变量区域。这通常用于调试时帮助检测未初始化的变量或栈溢出。

lea     edi, [ebp-10C]  ; 获取局部变量空间的起始地址并存入edi
mov     ecx, 43         ; 设置计数器,表示要写入的次数为67(43h)
mov     eax, CCCCCCCC   ; 设置要写入的值为0xCCCCCCCC
rep     stosd           ; 将eax的值(0xCCCCCCCC)重复写入到edi指向的内存区域

lea edi, [ebp-10C]lea指令(Load Effective Address)将[ebp-10C](局部变量空间的起始地址)加载到edi寄存器中。此时edi指向局部变量的起始位置。

mov ecx, 43:将0x43(67)存入ecx,作为rep stosd指令的计数器,表示接下来要重复执行stosd指令67次。

mov eax, CCCCCCCC:将0xCCCCCCCC存入eax寄存器,这个值将被写入到栈空间中。0xCCCCCCCC在调试环境中通常用于填充未初始化的内存或局部变量,用于帮助识别未使用或非法使用的内存区域。

rep stosdrep是一个前缀,用于重复执行后面的stosd指令,直到ecx为0。stosdeax中的值(0xCCCCCCCC)写入edi指向的内存地址,每次写入4字节,ecx自动减1。因为ecx初始值为67,所以这个操作将0xCCCCCCCC写入[ebp-10C][ebp-4]的内存区域(268字节)。

再往后的代码中则表示函数执行过程中堆栈保护的设置,以及准备进行一次函数调用的过程。

mov eax,dword ptr ds:[<___security_cookie>]
xor eax,ebp
mov dword ptr ss:[ebp-4],eax

mov eax, dword ptr ds:[<___security_cookie>]:

作用:从数据段(ds)中的全局变量<___security_cookie>读取一个值到eax寄存器中,___security_cookie 是堆栈保护机制中常用的安全Cookie。这个值通常是一个随机生成的值,用于检测栈溢出攻击。在程序启动时,___security_cookie 会被初始化为一个随机数。它在函数开始和结束时被用来验证栈的完整性,确保没有被非法修改。

xor eax, ebp:将安全Cookieebp寄存器中的值进行异或操作(XOR),结果保存在eax中。ebp是当前函数的栈帧基址。通过异或ebp,它使得安全Cookie与当前函数的栈帧相关联,从而进一步加强了堆栈保护机制。

mov dword ptr ss:[ebp-4], eax:将eax(即异或后的安全Cookie值)存储到栈帧中[ebp-4]的位置。

后面的两行代码也则是在进行各种函数初始化时的检查,因为涉及到跳转,这边就不做过多赘述了。

再往下运行的这部分代码就开始进行参数传递,这些指令将数值压入栈中后进行函数调用,且因为在函数调用后可以看到有平栈的操作,所以基本上可以确定该函数的调用约定为cdecl

在函数的所有参数压入栈中后,此时ESPEBP寄存器的指向如下:

ESP指向栈顶,且ESP指向的地址值比EBP来的小,所以在参数压栈的过程中地址实际上是减少的。接着当我们进入函数后,程序会自动将函数的返回地址压入栈中;

此时栈中的情况为

进入函数后程序做的第一件事就是将原来的ebp压入栈中,用于保存调用者栈空间。

此时栈中的内容为:

紧接着就是mov ebp,esp将栈顶寄存器的值赋给基址寄存器,然后sub esp,C0开辟新的栈帧空间。

此时栈中的内容如下:

后续的代码就是在进行函数的相关初始化,与上述main函数的初始化相同,这边就不再赘述了。接着我们来说一下在函数内的参数引用,也就是函数如何使用栈中的参数值:

mov eax,dword ptr ss:[ebp+8]
add eax,dword ptr ss:[ebp+C]
add eax,dword ptr ss:[ebp+10]
add eax,dword ptr ss:[ebp+14]
add eax,dword ptr ss:[ebp+18]

这个时候dword ptr ss:[ebp+8]实际上就表示栈中的第一个参数,dword ptr ss:[ebp+C]就是第二个参数以此类推。

最后得到5个参数的和放在eax寄存器中。

再往后就是各种还原操作如还原非易失性寄存器、还原开辟的栈帧空间、还原ESPESP原来的位置等。

最后ret,根据栈中的返回地址回到原本的执行地址中。

由于是cdecl调用约定,所以需要进行平栈操作(add esp,14这边的14为16进制,转化为十进制为20,在32位架构中正好就是代码中5个参数占用的空间),至此函数执行完毕。

参数传递-指针

上面的例子是所有参数传递方式为整数的情况,那么如果传入的参数为指针呢?我们就接着看下面的代码。

可以看到如果传入的参数为指针的情况下,这个时候压入栈中的就是参数的地址,这边使用lea命令进行取地址,接着我们进入函数中进行查看;

可以看到此时函数中的初始化操作和还原操作都是一样的,这里我们来关注一下不同点:

mov eax,dword ptr ss:[ebp+8]
mov dword ptr ds:[eax],A
mov eax,dword ptr ss:[ebp+C]
mov dword ptr ds:[eax],14
mov eax,dword ptr ss:[ebp+10]
mov dword ptr ds:[eax],1E
mov eax,dword ptr ss:[ebp+14]
mov dword ptr ds:[eax],28
mov eax,dword ptr ss:[ebp+18]
mov dword ptr ds:[eax],32

mov eax, dword ptr ss:[ebp+8] 将第一个参数的值(即一个指针)从栈中加载到 eax 寄存器。

mov dword ptr ds:[eax], A将常量 A(十六进制数,等于 10)存储到 eax 指向的地址。这意味着将值 10 存入该指针指向的内存位置。

后面的情况也是类似的,总结一下该代码实际上就是根据传入的指针参数,将一系列特定值(10、20、30、40、50)写入这些指针所指向的内存地址。

执行完毕后也是一样根据栈中的返回地址回到原来的指令执行地址处,进行平栈操作。

至此函数执行完毕。后续的printf函数的调用这边就不做过多赘述了,如果对这个比较不熟悉的话,可以看看笔者前面的文章。

至此x86架构的函数执行的全部过程刨析完了,接着我们来看一下x64架构中函数执行的全部过程。

②x64架构

首先我们将x64架构的程序载入x64dbg中进行分析调试。

紧接着定位到main函数得到如下代码:

我们先来看一下

push rbp
push rdi
sub rsp,1B8
lea rbp,qword ptr ss:[rsp+30]  ;与x86不同,x64将rbp指向了rsp+30的地址
mov rdi,rsp
mov ecx,6E
mov eax,CCCCCCCC
rep stosd 

这段代码可以被视为一个序言阶段(prologue)。具体来说,这段代码所做的事情符合序言的特征:

①保存寄存器值:push rbppush rdi 保存了旧的基址指针和 rdi 寄存器的值。

②为局部变量分配空间:sub rsp, 1B8 在栈上分配了 440 字节的空间用于局部变量和临时数据。

③设置栈帧基址:lea rbp, [rsp+30]rbp 设置了新的基址,便于函数访问栈中的局部变量。

④初始化内存:通过 mov eax, CCCCCCCCrep stosd 初始化栈上分配的内存空间。这在某些情况下是为了确保局部变量的内存被预先设置,避免使用未初始化的数据。

再往下与x86架构程序一样是做一个堆栈保护的设置,以及函数运行的各种检查操作:

接着往下就是正式开始进行函数的参数传递

初始化参数值
mov dword ptr ss:[rbp+4], 1
mov dword ptr ss:[rbp+24], 2
mov dword ptr ss:[rbp+44], 3
mov dword ptr ss:[rbp+64], 4
mov dword ptr ss:[rbp+84], 5

这些指令将常量值 12345 存入栈中相对于 rbp 的不同偏移位置(rbp+4rbp+24 等)。这些地址似乎是用于存储函数的局部变量或传入参数的。

准备参数进行函数调用
mov eax, dword ptr ss:[rbp+84]
mov dword ptr ss:[rsp+20], eax

mov eax, [rbp+84]rbp+84 处的值(即之前存储的 5)加载到 eax 寄存器。

然后,mov [rsp+20], eaxeax 的值(5)存入栈中 rsp+20 处。这是通过栈传递的参数之一,通常用于超过寄存器限制的额外参数。

此时栈内的内容为:

接着通过寄存器传递参数

在 x64 调用约定(Windows 下的 Microsoft x64 Calling ConventionSysV ABI)中,前四个整数参数是通过寄存器 rcxrdxr8r9 传递的:

mov r9d, dword ptr ss:[rbp+64]
mov r8d, dword ptr ss:[rbp+44]
mov edx, dword ptr ss:[rbp+24]
mov ecx, dword ptr ss:[rbp+4]

r9d 被赋值为 [rbp+64] 处的值(即之前存储的 4)。

r8d 被赋值为 [rbp+44] 处的值(即之前存储的 3)。

edx 被赋值为 [rbp+24] 处的值(即之前存储的 2)。

ecx 被赋值为 [rbp+4] 处的值(即之前存储的 1)。

这些指令表明,函数 parameterpass-x64.7FF6E34C139D 将通过寄存器传递这四个参数,接着调用函数

call parameterpass-x64.7FF6E34C139D

接着我们进入函数中进行查看,因为此时我们进入了函数中,所以这个时候需要将函数的返回地址压入栈中,此时栈中的内容为:

进入函数后,直接将四个寄存器中的参数值压入栈中(可以看到x64架构的程序虽然是使用寄存器进行参数传递,但是在函数中还是需要将这些寄存器中的值压入栈中)

此时栈中的内容如下:

接着就是序言阶段,这边就不做过多赘述了。

后续就是通过ebp去取出刚刚压入栈中的几个数据进行相加运算,并把最后的和放在rax寄存器中;

具体的ebp+E8这些值可以直接右击地址,然后转到内存中进行查看即可,实际上就是刚刚压入栈中的值:

最后就是函数执行完毕后执行的还原操作即还原非易失性寄存器(rdi)中的值,以及rsp和rbp的值。

最后ret返回,至此函数运行结束。

后续做的printf操作这边就不做过多赘述,具体可以根据动态调试时窗口的变化进行判断。

参数传递-指针

接着我们来看一下关于x64架构的指针参数传递

事实上指针的传递过程与整数差不多,是将最后一个指针指向的先放入rsp+20处,接着将其他的,其他的指针参数从左往右依次放入r9-rcx中,接着调用函数。由于调用了函数,程序自动向栈中压入的函数的返回地址,原来的rsp+20处此时就是rsp+28,接着再将r9-rcx存放中的地址值分别压入rsp20-rsp+8中。

接着就是做初始化与检查:

接着就是通过如下形式,提取处栈中的地址,对地址上的值重新赋值:

mov rax,qword ptr ss:[rbp+E0] ;从栈上 rbp+E0 的位置读取一个 64 位的值(假设是一个指针),并将其存入 rax 寄存器。
mov dword ptr ds:[rax],A    ;将常量 A(十六进制,等于 10)存储到 rax 指向的内存地址中。意味着将值 10 写入该指针所指的内存位置。
mov rax,qword ptr ss:[rbp+E8]
mov dword ptr ds:[rax],14
mov rax,qword ptr ss:[rbp+F0]
mov dword ptr ds:[rax],1E
mov rax,qword ptr ss:[rbp+F8]
mov dword ptr ds:[rax],28
mov rax,qword ptr ss:[rbp+100]
mov dword ptr ds:[rax],32

这段代码从栈中读取几个指针,并向这些指针所指的内存位置分别写入值 10, 20, 30, 40, 和 50。函数执行的结果直接就体现在指针指向的地址中了。

接着将各种栈指针、寄存器恢复到调用之前的状态,同上。

最后ret返回到原来的执行地址上,函数结束。

在本文中,我们深入探讨了函数逆向工程的整体流程,通过对函数的结构、调用约定及其参数传递方式的详细分析,能够有效地识别和理解目标程序的行为,为后续的漏洞分析和安全研究奠定基础。


http://www.ppmy.cn/server/132226.html

相关文章

Golang | Leetcode Golang题解之第476题数字的补数

题目&#xff1a; 题解&#xff1a; func findComplement(num int) int {highBit : 0for i : 1; i < 30; i {if num < 1<<i {break}highBit i}mask : 1<<(highBit1) - 1return num ^ mask }

Python快速编程小案例——猜数字

提示&#xff1a;&#xff08;个人学习&#xff09;&#xff0c;案例来自工业和信息化“十三五”人才培养规划教材&#xff0c;《Python快速编程入门》第2版&#xff0c;黑马程序员◎编著 猜数游戏是一种经典的密码破译类益智游戏&#xff0c;通常由两个人参与。一个人在心中设…

Unity3D相关知识点总结

Unity3D使用的是笛卡尔三维坐标系&#xff0c;并且是以左手坐标系进行展示的。 1.全局坐标系&#xff08;global&#xff09; 全局坐标系描述的是游戏对象在整个世界&#xff08;场景&#xff09;中的相对于坐标原点&#xff08;0&#xff0c;0&#xff0c;0&#xff09;的位置…

机器学习与神经网络的发展前景

目录 引言 1 机器学习与神经网络在各领域的具体应用和作用 2 展望机器学习与神经网络的未来 个人对机器学习与神经网络的看法 引言 在2024年&#xff0c;诺贝尔物理学奖破天荒地颁给了机器学习与神经网络领域的研究者&#xff0c;这一决定不仅震惊了科学界&#xff0c;也标…

前端vue部署网站

这里讲解一下前端vue框架部署网站&#xff0c;使用工具是 xshell 和 xftp &#xff08;大家去官网安装免费版的就行了&#xff09; 服务器 我使用的阿里云服务器&#xff0c;买的是 99 一年的&#xff0c;淘宝有新手9.9 一个月服务器。可以去用&#xff0c;学生的话是有免费三…

extern

1. 声明外部变量 现代编译器一般采用按文件编译的方式&#xff0c;因此在编译时&#xff0c;各个文件中定义的全局变量是相互透明的&#xff0c;也就是说&#xff0c;在编译时&#xff0c;全局变量的可见区域限制在文件内部。extern可以看到别的文件中的全局变量。 2. 在c中用…

2024年区块链钱包现状与未来趋势分析

钱包作为Web3世界的入口&#xff0c;充当了用户与区块链应用交互、管理资金和传递信息的关键工具。随着区块链技术的发展&#xff0c;钱包生态系统日益多样化&#xff0c;涌现出大量不同类型的解决方案。这些解决方案不仅极大地改善了用户体验&#xff0c;还推动了区块链技术和…

鹅厂JS面试题——0.1+0.2=0.3吗?

首先公布答案:在JavaScript 中&#xff0c;0.1 0.2 ≠ 0.3 为什么&#xff1f; JavaScript 中的数字使用 IEEE 754 标准的双精度浮点数&#xff08;64 位&#xff09;进行表示。这种表示方式在处理十进制小数时&#xff0c;不能精确地表示某些数字。比如0.1 和 0.2 这样的十进…