C/C++逆向:虚函数逆向分析

虚函数(Virtual Function)是C++中实现多态的一种机制,它允许在运行时通过基类的指针或引用调用派生类中的函数,而不是基类中的版本。虚函数通常与继承多态结合使用。通过在基类中使用 virtual 关键字声明函数,允许派生类重写该函数。当通过基类指针或引用调用时,根据对象的实际类型决定调用哪个版本的函数;它依赖虚函数表(vtable)实现,用于动态绑定,常用于多态场景下的接口设计和扩展。

虚函数表的布局

每个包含虚函数的类都有一张虚函数表:

如果类没有派生类,则虚函数表存储类自身的虚函数地址。
如果类有派生类,并且派生类中重写了基类的虚函数,则派生类的虚函数表中会存储派生类的函数地址。

现有如下程序:

#include <iostream>
using namespace std;
​
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }virtual ~Base() { cout << "Base::destructor func2" << endl; } // 虚析构函数
};
​
class Derived : public Base {
public:void func1() override { cout << "Derived::func1" << endl; }void func2() override { cout << "Derived::func2" << endl; }virtual ~Derived() { cout << "Derived::destructor func2" << endl; } // 虚析构函数
};
​
int main() {Base* obj = new Derived();obj->func1(); // 调用 Derived::func1obj->func2(); // 调用 Derived::func2delete obj;return 0;
}

以下是该程序中的基类、派生类的虚函数表在内存中的大致结构:

基类的虚函数表
索引虚函数地址
0Base::func1()地址
1Base::func2()地址
派生类的虚函数表
索引虚函数地址
0Derived::func1()地址
1Derived::func2()地址

每个包含虚函数的对象会有一个隐藏的虚指针(vptr),指向该对象所属类的虚表。而vptr 通常存储在对象内存布局的开头部分(具体位置依赖于编译器实现)。

现使用VS将上述代码进行编译,生成exe文件后,放入x96dbg中进行调试分析。

定位到main函数后,对反汇编函数进行查看,跳过初始化指令:

后续代码则为程序主体代码;接着就针对第一部分指令进行分析。

push 4
call virtual-func.7F012D
add esp,4

x32dbg中双击call指令调用的virtual-func.7F012D函数,可以看到该函数已经被识别为new运算符的实现;在 C++ 中,new 运算符用于动态分配内存,其底层实现通常会调用 operator new,负责分配指定大小的内存块。

此时该代码的反汇编解释为:push 4:表示分配内存的大小(Derived 类大小为 4 字节);

Derived 继承了 Base,且包含虚函数,此时在对象中则保存了指向虚表(vtable)的指针大小为4字节,且因为 Base 类没有数据成员;Derived 类也没有额外的数据成员。因此,Derived 对象的大小只包括一个虚指针(vptr) 的大小,所以分配内存大小为4.

call virtual-func.7F012D:调用分配内存的函数(即 operator newmalloc);add esp, 4:调用结束后清理参数栈。接着持续跟进代码,可以发现:new运算符实际上就是调用了底层分配函数 malloc

在 x86 和 x64 架构下,new 运算符调用内存分配函数(如 operator newmalloc)后,分配的内存地址(即对象指针)通常存放在返回值寄存器中。具体存放的位置取决于目标架构:

x86 架构:返回值寄存器:EAX

在 x86 的调用约定中,函数的返回值通常存放在 EAX寄存器中。因此,当调用 new 或类似的内存分配函数时,分配的内存地址(即指针)会存放在 EAX 中。

x64 架构:返回值寄存器:RAX

在 x64 的调用约定中,函数的返回值通常存放在 RAX 寄存器中;当调用 new 运算符时,分配的内存地址会存放在 RAX 中。

后续代码解释:

mov dword ptr ss:[ebp-D4],eax
cmp dword ptr ss:[ebp-D4],0
je virtual-func.803E9E
xor eax,eax
mov ecx,dword ptr ss:[ebp-D4]
mov dword ptr ds:[ecx],eax
mov ecx,dword ptr ss:[ebp-D4]
call virtual-func.7ED79D
mov dword ptr ss:[ebp-F4],eax
jmp virtual-func.803EA8
mov dword ptr ss:[ebp-F4],0
mov edx,dword ptr ss:[ebp-F4]
mov dword ptr ss:[ebp-8],edx
mov eax,dword ptr ss:[ebp-8]
mov edx,dword ptr ds:[eax]
mov esi,esp
mov ecx,dword ptr ss:[ebp-8]
mov eax,dword ptr ds:[edx]
call eax

这段汇编代码展示了一个典型的 C++ 对象创建、初始化和调用操作的流程。它可能涉及到动态内存分配 (new)、构造函数调用,以及对象的虚函数表 (vtable) 操作。以下是逐行的分析和解释:

mov dword ptr ss:[ebp-D4],eax
cmp dword ptr ss:[ebp-D4],0
je virtual-func.803E9E

mov dword ptr ss:[ebp-D4],eax:将 eax 的值(返回的对象指针)存储到 [ebp-D4],这通常是一个局部变量用于保存对象的内存地址。

cmp dword ptr ss:[ebp-D4],0:检查对象指针是否为 nullptr

je virtual-func.803E9E:如果对象指针为 nullptr(分配失败),跳转到错误处理或清理代码。如果对象指针不为 nullptr则继续往下执行。

xor eax,eax
mov ecx,dword ptr ss:[ebp-D4]
mov dword ptr ds:[ecx],eax

xor eax,eax:将寄存器 eax 置为 0

mov ecx,dword ptr ss:[ebp-D4]:将对象的指针加载到 ecx,准备操作。

mov dword ptr ds:[ecx],eax:将 eax(值为 0)写入对象的第一个成员变量(通常是虚表指针 vptr 或类的其他重要数据)。

接下去进行构造函数的调用:

mov ecx,dword ptr ss:[ebp-D4]
call virtual-func.7ED79D

mov ecx,dword ptr ss:[ebp-D4]:将对象指针加载到 ecx,作为调用构造函数的上下文(在 C++ 中,构造函数隐式地以对象指针为 this 参数)。

call virtual-func.7ED79D:调用构造函数的实际实现(地址 7ED79D),完成对象的初始化。

存储构造完成的对象指针
mov dword ptr ss:[ebp-F4],eax
jmp virtual-func.803EA8
mov dword ptr ss:[ebp-F4],0 ;运行时被跳过

mov dword ptr ss:[ebp-F4],eax:将构造函数返回值(eax,可能是同一个对象指针)存储到 [ebp-F4]。接着可以通过eax中的地址在内存中定位(右击寄存器窗口中的eax进行跳转即可),查看对象;可以看到在构造函数完成后,对象中的第一个元素则成为虚表地址。

准备虚函数调用
mov edx,dword ptr ss:[ebp-F4]
mov dword ptr ss:[ebp-8],edx
mov eax,dword ptr ss:[ebp-8]
mov edx,dword ptr ds:[eax]

mov edx,dword ptr ss:[ebp-F4]:将对象指针从 [ebp-F4] 加载到 edx

mov dword ptr ss:[ebp-8],edx:保存对象指针到 [ebp-8],可能是另一个局部变量。

mov eax,dword ptr ss:[ebp-8]:将对象指针加载到 eax

mov edx,dword ptr ds:[eax]:加载对象的虚表指针 vptredx

调用虚函数
mov esi,esp
mov ecx,dword ptr ss:[ebp-8]
mov eax,dword ptr ds:[edx]
call eax

mov esi,esp:保存当前栈指针 espesi,通常用于记录调用现场或检查栈空间。

mov ecx,dword ptr ss:[ebp-8]:将对象指针加载到 ecx,作为虚函数调用的 this 参数。

mov eax,dword ptr ds:[edx]:从虚表中加载虚函数的地址到 eax

call eax:调用虚函数,eax 中保存了函数的实际地址。

此时,派生类的虚函数成功运行输出。在虚函数调用完毕后会进行栈状态检查:

接着进行第二个虚函数的调用,具体步骤与第一个虚函数步骤相似,相关代码如下:

mov eax,dword ptr ss:[ebp-8]
mov edx,dword ptr ds:[eax]

mov eax,dword ptr ss:[ebp-8]:从 [ebp-8] 加载一个指针到 eax[ebp-8] 通常是存储的对象指针(即 this 指针)。

mov edx,dword ptr ds:[eax]:从对象指针 eax 的第一个成员变量中加载虚表指针(vptr

mov esi,esp
mov ecx,dword ptr ss:[ebp-8]
mov eax,dword ptr ds:[edx+4]

mov esi,esp:将当前的栈指针 esp 保存到 esi;这一步是为了后续检查栈的状态。

mov ecx,dword ptr ss:[ebp-8]:将对象指针从 [ebp-8] 加载到 ecx,在 C++ 的调用约定中,ecx 通常用来传递 this 指针给成员函数。

mov eax,dword ptr ds:[edx+4]:从虚表指针 edx 中偏移 4 字节加载函数地址到 eax,在虚表中,edx 指向的是一张函数地址表,偏移 4 字节可能是虚表中第二个虚函数的地址。

call eax:调用 eax 中存储的地址,这实际上是虚表中的函数地址。此时第二个虚函数调用成功:

同样的,调用成功以后也是一个栈检查:

后续代码:

mov eax,dword ptr ss:[ebp-8]
mov dword ptr ss:[ebp-EC],eax
mov ecx,dword ptr ss:[ebp-EC]
mov dword ptr ss:[ebp-E0],ecx
cmp dword ptr ss:[ebp-E0],0
je virtual-func.803F20

这段代码的作用是从栈中加载、保存对象指针,并进行空指针检查。通过这些操作,程序可以安全地访问对象,避免在没有有效对象时进行操作。

接着就是以同样的手法,调用了虚析构函数。

跟进函数中分析(通过edx中的函数指针地址,跳转至对应的反汇编代码),最后除了派生类的虚析构函数之外还执行了包含基类的析构函数。

在本文中,我们深入探讨了虚函数在编译后产生的内存布局、调用过程以及在逆向分析中的解读方法。通过对关键实现细节的拆解和案例分析,相信读者能够更清晰地理解虚函数的工作机制以及如何在逆向工程中定位相关特征。希望本文能为逆向分析虚函数的学习者提供启发和指导。如果您有任何疑问或见解,欢迎交流讨论,共同探索更深层次的技术细节!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/61946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【prism】遇到一个坑,分享!

背景 我通用prism的方式写了一个弹窗,弹窗绑定一个 Loaded 事件,但是Loaded事件一直不触发!!! 具体过程 我的loaded事件也是通过命令的方式绑定的: <i:Interaction.Triggers><i:EventTrigger EventName="Loaded

永磁同步电机末端振动抑制(输入整形)

文章目录 1、前言2、双惯量系统3、输入整形3.1 ZV整形器3.2 ZVD整形器3.3 EI整形器 4、伺服系统位置环控制模型5、仿真5.1 快速性分析5.2 鲁棒性分析 参考 1、前言 什么是振动抑制&#xff1f;对于一个需要精确定位的系统&#xff0c;比如机械臂、塔吊、码头集装箱等&#xff…

Pytorch使用手册-Optimizing Model Parameters(专题七)

优化模型参数 现在我们已经有了一个模型和数据,是时候通过优化模型参数来训练、验证和测试模型了。训练模型是一个迭代的过程:在每次迭代中,模型会对输出进行预测,计算预测的误差(损失),收集误差相对于参数的导数(在上一节中我们已看到),然后使用梯度下降优化这些参…

pywinauto常见用法详解

1 安装 pip install pywinauto0.6.3 -i https://pypi.tuna.tsinghua.edu.cn/simple 2 启动app appApplication(backenduia).start(程序路径) backend 参数有2种 win32和uia 3 窗口选择 3.1通过类选择 dlgapp["类名"] dlgapp.类名 3.2通过标题选择 dlgapp[&…

20241125复盘日记

昨日最票&#xff1a; 南京化纤 滨海能源 广博股份 日播时尚 众源新材 返利科技 六国化工 丰华股份 威领股份 凯撒旅业 华扬联众 泰坦股份 高乐股份高均线选股&#xff1a; 理邦仪器高乐股份日播时尚领湃科技威领股份资金最多的票&#xff1a; 资金攻击最多的票&#xff1a; …

【实用向】Django 框架入门

声明 这是一篇实用向的Django框架教程博客&#xff0c;适用于想要快速入门的开发者&#xff0c;有前后端开发以及语言基础&#xff0c;想要学习语法或者特性。&#xff0c;包括一些基础的使用&#xff0c;想要学习请结合文章初识 Django并按照我的顺序一步步进行&#xff0c;做…

Git的使用_仓库管理_CI/CD介绍

文章目录 一、Git的基础知识一-1、什么是GitLinux命令行的git的简易安装Git项目的组成Git的基本工作流程Git文件的三种状态 一-2、存储库远程存储库与本地存储库创建存储库git init命令的使用方法1. 初始化一个新的 Git 仓库2. 在指定目录初始化一个新的 Git 仓库3. 初始化一个…

畅游Diffusion数字人(6): JoyHallo: Digital human model for Mandarin

Diffusion Models专栏文章汇总:入门与实战 前言:目前音频驱动大部分的论文和数据都是围绕英文输入驱动打造的,应用在东亚人和国语上效果有所降低。与英语相比,普通话中复杂的嘴唇动作使模型训练更加复杂。这篇博客介绍京东提出的数字人技术《JoyHallo: Digital human model…

SpringCloud入门实战-Spring Cloud Stream消息驱动概述

❤️ 《SpringCloud入门实战系列》解锁SpringCloud主流组件入门应用及关键特性。带你了解SpringCloud主流组件,是如何一战解决微服务诸多难题的。项目demo&#xff1a;源码地址 ❤️ 作者&#xff1a;一只IT攻城狮。关注我&#xff0c;不迷路。 ❤️ 再小的收获x365天都会成就…

【初级测试常用的sql命令及实例解析】

连接数据库 命令行语句&#xff08;以MySQL为例&#xff09;&#xff1a;mysql -u username -p。其中-u表示指定用户名&#xff0c;-p表示需要输入密码。解析&#xff1a;这是登录MySQL数据库服务器的基本命令。执行后&#xff0c;系统会提示输入密码&#xff0c;正确输入密码后…

Flink 安装与入门:开启流式计算新时代

在当今大数据蓬勃发展的时代&#xff0c;数据处理的时效性愈发关键。传统基于先存储再批量处理的数据方式&#xff0c;在面对诸如网站实时监控、异常日志即时分析等场景时&#xff0c;显得力不从心。随着 5G、物联网等技术的兴起&#xff0c;海量数据如潮水般涌来&#xff0c;且…

【科研绘图】Matplotlib 中文字符乱码(debug)

在使用 Matplotlib 绘图时&#xff0c;如果图中包含中文字符&#xff0c;可能会遇到中文无法正常显示的问题。这通常是因为默认的字体不支持中文。为了解决这个问题&#xff0c;你可以通过以下几种方法来设置 Matplotlib 使用支持中文的字体。 1. 设置 Matplotlib 使用支持中文…

如何在CodeIgniter中添加或加载模型

在CodeIgniter框架中&#xff0c;模型&#xff08;Model&#xff09;是用于与数据库进行交互的重要组件。模型通常包含数据库查询、业务逻辑以及与数据库表相关的函数。以下是如何在CodeIgniter中添加或加载模型的步骤&#xff1a; 1. 创建模型文件 首先&#xff0c;你需要在…

神经网络归一化方法总结

在深度学习中&#xff0c;归一化 是提高训练效率和稳定性的关键技术。以下是几种常见的神经网络归一化方法的总结&#xff0c;包括其核心思想、适用场景及优缺点。 四种归一化 特性Batch NormalizationGroup NormalizationLayer NormalizationInstance Normalization计算维度…

设计理念与数据反馈:面向火星熔岩管探索的跳跃机器人

随着人类对火星探索的深入&#xff0c;熔岩管作为潜在资源和居住地的科学价值受到广泛关注。然而&#xff0c;这些复杂且规模宏大的地下空间&#xff0c;对传统探测技术提出了严峻挑战。因此&#xff0c;本文介绍了一款专为火星熔岩管探索设计的跳跃机器人&#xff0c;其核心设…

MTK 展锐 高通 sensorhub架构

一、MTK平台 MTK框架可以分为两部分&#xff0c;AP和SCP。 AP是主芯片&#xff0c;SCP是协处理器&#xff0c;他们一起工作来处理sensor数据。 SCP 是用来处理sensor和audio相关功能和其他客制化需求的一个协处理理器&#xff0c;MTK SCP选择freeRTOS作为操作系统&#xff0c…

解决JWT解析CDN不稳定问题

最近在项目开发中&#xff0c;我遇到了一个令人头疼的问题&#xff1a;JWT解析所依赖的CDN源不稳定。这导致应用在某些情况下无法正常运行&#xff0c;严重影响了用户体验。经过一番探索和尝试&#xff0c;我最终通过手写解析函数的方式解决了这个问题。本文将分享我的解决过程…

SD NAND 的 SDIO在STM32上的应用详解

四.SDIO功能框图(重点) SDIO包含2个部分&#xff1a; ● SDIO适配器模块&#xff1a;实现所有MMC/SD/SD I/O卡的相关功能&#xff0c;如时钟的产生、命令和数据的传送。 ● AHB总线接口&#xff1a;操作SDIO适配器模块中的寄存器(由STM32控制SDIO外设)&#xff0c;并产生中断和…

深入解析:用Scala验证身份证号码的合法性

引言 身份证号码&#xff0c;这个由18位数字组成的唯一标识&#xff0c;不仅包含了个人的出生年月日&#xff0c;还隐藏着性别信息&#xff0c;并且通过特定的算法来确保其唯一性和正确性。今天&#xff0c;我们将通过Scala编程语言&#xff0c;一步步揭开身份证号码的神秘面纱…

C语言数据结构学习:循环队列

C语言 数据结构学习 汇总入口&#xff1a; C语言数据结构学习&#xff1a;[汇总] 1. 循环队列 队列的博客&#xff1a;C语言数据结构学习&#xff1a;队列 循环队列会预先定义最大队列空间&#xff0c;然后定义一个数组&#xff0c;通过队列头和队列尾指针分别指向开头和结尾&…