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

news2024/11/26 13:58:28

虚函数(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::func1
    obj->func2(); // 调用 Derived::func2
    delete 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.coloradmin.cn/o/2247884.html

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

相关文章

es写入磁盘的过程以及相关优化

数据写入到内存buffer同时写入到数据到translog buffer,这是为了防止数据不会丢失每隔1s数据从buffer中refresh到FileSystemCache中,生成segment文件,这是因为写入磁盘的过程相对耗时,借助FileSystemCache,一旦生成segment文件,就能通过索引查询到了refresh完,memory bu…

linux部署Whisper 视频音频转文字

github链接&#xff1a;链接 我这里使用anaconda来部署&#xff0c;debian12系统&#xff0c;其他linux也同样 可以使用gpu或者cpu版本&#xff0c;建议使用n卡&#xff0c;rtx3060以上 一、前期准备 1.linux系统 链接&#xff1a;debian安装 链接&#xff1a;ubuntu安装 …

论文阅读:A Software Platform for Manipulating theCamera Imaging Pipeline

论文代码开源链接&#xff1a; A Software Platform for Manipulating the Camera Imaging Pipelinehttps://karaimer.github.io/camera-pipeline/摘要&#xff1a;论文提出了一个Pipline软件平台&#xff0c;可以方便地访问相机成像Pipline的每个阶段。该软件允许修改单个模块…

【科研绘图】Matplotlib 教学

以下是一个针对 Matplotlib 教学 的博客结构&#xff0c;按照分步骤教学方式撰写&#xff0c;以帮助读者从基础到高级逐步掌握 Matplotlib。 Matplotlib 教学&#xff1a;从基础到进阶绘图 Matplotlib 是 Python 中功能强大的数据可视化库&#xff0c;可以用来绘制多种类型的图…

【网络系统管理】2023年全国职业院校技能大赛:组策略--10套题组合--4

16、只有域管理员和IT部门员工可以登陆服务器 (1)计算机配置\策略\Windows设置\安全设置\本地策略\用户权限分配 17、创建ChinaSkills23为GPO管理员,加入到企业管理、域控管理员组 (1)gpmc.msc\林\域\%domain%--在这个域中创建GPO 18、为所有域用户设置漫游文件 (1)用…

钉钉授权登录

一.找开钉钉开发平台【钉钉开放平台 (dingtalk.com)】 二。点击菜单【应用开发】->左边【钉钉应用】->【创建应用】 三。创建应用-》保存成功后&#xff0c;点击自己【新建的应用】&#xff0c;进入详细页面 四。进入应用详细页面。左边【分享设置】 注意&#xff1a;进…

应用系统开发(14) 涡流检测系统硬件设计

涡流检测整体系统架构 涡流检测系统整体结构如上图 所示,DAC 转换与功率放大电路将数字正弦信号转 换为模拟正弦信号,为涡流探头提供正弦激励。互感式探头由两个线圈组成,一个作为 激励,另一个接收检测信号,AD 转换电路将传感器探头感应到的电压滤波放大,将电 压值调整到…

介绍一下strupr(arr);(c基础)

hi , I am 36 适合对象c语言初学者 strupr(arr)&#xff1b;函数是把arr数组变为大写字母 格式 #include<string.h> strupr(arr); 返回值为arr 链接分享一下arr的意义(c基础)(必看)(牢记)-CSDN博客 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #incl…

人工智能之数学基础:向量的基本知识

本文重点 向量的基本性质是线性代数和向量空间理论的核心,它们为向量运算提供了坚实的基础,并在物理、工程、计算机图形学等领域有着广泛的应用。本文对向量的一些基本知识进行介绍,帮助大家快速理解向量。 向量的定义与表示 向量是一个既有大小又有方向的量,通常用带箭…

Redis中的zset底层实现

文章目录 Redis中的zset底层实现一、引言二、zset的底层实现1、ziplist编码1.1、代码示例 2、skiplist编码2.1、代码示例 三、总结 Redis中的zset底层实现 一、引言 Redis的有序集合&#xff08;zset&#xff09;是一种非常强大的数据结构&#xff0c;它不仅能够存储元素&…

TSmaster CAN/CANFD 诊断(Diagnostic_CAN)

文章目录 1、Diagnostic TP 参数配置1.1 传输层参数&#xff1a;1.2 服务层参数1.3 Seed&Key 2、基础诊断配置2.1 添加/删除 服务2.2 配置 BasicDiagnostic 服务参数 3、诊断控制台4、自动诊断流程4.1 流程用例管理4.2 配置诊断流程&#xff08;UDS Flow&#xff09;4.2.1 …

大语言模型---LoRA中损失值的计算

文章目录 概要损失计算流程小结 概要 Llama-7B模型的LoRA微调训练中&#xff0c;通过使用Cross-Entropy Loss来度量模型输出的预测分布和真实标签分布之间的距离&#xff0c;来衡量模型的准确性。 本文主要介绍LoRA中损失值的计算流程。 Cross-Entropy Loss作用&#xff1a;是…

Linux笔记--基于OCRmyPDF将扫描件PDF转换为可搜索的PDF

1--官方仓库 https://github.com/ocrmypdf/OCRmyPDF 2--基本步骤 # 安装ocrmypdf库 sudo apt install ocrmypdf# 安装简体中文库 sudo apt-get install tesseract-ocr-chi-sim# 转换 # -l 表示使用的语言 # --force-ocr 防止出现以下错误&#xff1a;ERROR - PriorOcrFoundE…

使用 Nginx 在 Ubuntu 22.04 上安装 LibreNMS 开源网络监控系统

#LibreNMS 是一个功能强大的开源网络监控系统&#xff0c;它能够为你的网络性能和设备提供全面的监控。本文将引导你通过一系列步骤&#xff0c;在 Ubuntu 22.04 服务器上安装和配置 LibreNMS&#xff0c;使用 Nginx 作为 Web 服务器。 简介 LibreNMS 提供了对网络设备和性能…

elementUI非常规数据格式渲染复杂表格(副表头、合并单元格)

效果 数据源 前端代码 (展示以及表格处理/数据处理) 标签 <el-table :data"dataList" style"width: 100%" :span-method"objectSpanMethod"><template v-for"(item, index) in headers"><el-table-column prop"…

使用脚本实现hadoop-yarn-flink自动化部署

本文使用脚本实现hadoop-yarn-flink的快速部署&#xff08;单机部署&#xff09;。 环境&#xff1a;①操作系统&#xff1a;CentOS 7.6&#xff1b;②CPU&#xff1a;x86&#xff1b;③用户&#xff1a;root。 1.前置条件 把下面的的脚本保存到“pre-install.sh”文件&#x…

Linux系统编程之进程基础知识

概述 在Linux系统中&#xff0c;进程是指一个正在运行的程序实例。每个进程都有一个唯一的进程标识符&#xff0c;即PID&#xff0c;操作系统通过这个PID来唯一识别和管理各个进程。进程不仅仅是程序代码的运行实例&#xff0c;它还包含了程序运行时所需的各种资源&#xff0c;…

H.264/H.265播放器EasyPlayer.js网页全终端安防视频流媒体播放器关于iOS不能系统全屏

在数字化时代&#xff0c;流媒体播放器已成为信息传播和娱乐消遣的主流载体。随着技术的进步&#xff0c;流媒体播放器的核心技术和发展趋势不断演变&#xff0c;影响着整个行业的发展方向。 EasyPlayer播放器属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持…

【数据结构】二叉树的建立与遍历

1.二叉树 1.1 二叉树的定义 首先先来回顾一下什么是二叉树&#xff1a; 二叉树&#xff08;binary tree&#xff09;是指树中节点的度不大于2的有序树&#xff0c;它是一种最简单且最重要的树。二叉树的递归定义为&#xff1a;二叉树是一棵空树&#xff0c;或者是一棵由一个根…

51单片机基础 06 串口通信与串口中断

目录 一、串口通信 二、串口协议 三、原理图 四、串口通信配置参数 1、常用的串行口工作方式1 2、数据发送 3、数据接收 4、波特率计算 5、轮询接收 6、中断接收 一、串口通信 串口通信是一种常见的数据传输方式&#xff0c;广泛用于计算机与外部设备或嵌入式系统之间…