使用Binary Ninja逆向C++虚拟函数调用
在我的第一篇博客文章中,我介绍了Binary Ninja低级中间语言(LLIL)的基本结构,以及如何使用Python API遍历和操作它。现在,我们将做一些更有趣的事情。
逆向工程从面向对象语言编译的二进制文件可能具有挑战性,特别是在处理虚拟函数时。在C++中,调用虚拟函数涉及在虚拟表(vtable)中查找函数指针,然后进行间接调用。在反汇编中,你只能看到类似mov rax, [rcx+0x18]; call rax
的指令。如果你想知道对于给定的类对象它将调用什么函数,你必须找到虚拟表,然后确定该偏移处的函数指针。
或者你可以使用这个插件!
示例插件:导航到虚拟函数
vtable-navigator.py
是一个示例插件,可以从调用指令导航到给定类的虚拟函数。首先,插件使用LLIL在构造函数中引用时识别指定类的vtable。接下来,它将预处理调用指令的基本块以跟踪寄存器分配及其相应的LLIL表达式。最后,插件将处理调用的LowLevelILInstruction对象,并通过递归访问寄存器分配表达式来计算要调用的函数的偏移量。
发现vtable指针
图1:两个类继承自基虚拟类。每个类的虚拟表指向其各自的虚拟函数实现。
在最简单的形式中,类构造函数将其vtable的指针存储在内存中的对象结构中。存储vtable指针的两种最常见方式是:直接引用vtable指针的硬编码值,或将vtable指针存储在寄存器中,然后将该寄存器的值复制到内存中。因此,如果我们查找从寄存器到内存地址的写入操作且没有偏移量,那么它很可能是vtable。
一个示例构造函数。高亮显示的指令将vtable存储在对象结构中。
我们可以通过查找构造函数LowLevelILFunction对象中的LLIL指令(如第1部分所述)来检测第一种vtable指针分配,该指令将常量值存储到寄存器中包含的内存地址。
根据API,LLIL_STORE指令有两个操作数:dest和src。两者都是LLIL表达式。对于这种情况,我们寻找由寄存器提供的目标值,因此dest应该是LLIL_REG表达式。要存储的值是一个常量,因此src应该是LLIL_CONST表达式。如果我们匹配这个模式,那么我们假设该常量是vtable指针,读取该常量指向的值(即il.src.value),并再次检查那里是否有函数指针,以确保它确实是vtable。
|
|
相当直接,但让我们看看第二种情况,即值首先存储在寄存器中。
对于这种情况,我们搜索LLIL_STORE的dest和src操作数都是LLIL_REG表达式的指令。现在我们需要仅基于寄存器来确定虚拟表的位置。
这就是事情变得酷的地方。这种情况不仅展示了LLIL的使用,还展示了在LLIL上执行的数据流分析有多么强大。如果没有数据流分析,我们将不得不解析这个LLIL_STORE指令,找出正在引用的寄存器,然后向后步进以找到分配给该寄存器的最后一个值。通过数据流分析,寄存器的当前值可以通过单次调用get_reg_value_at_low_level_il_instruction
轻松获得。
|
|
寄存器分配的传播
现在我们知道了vtable的位置,让我们找出调用的偏移量。要确定这个值,我们需要从调用指令回溯程序状态到从内存检索vtable指针的时刻,计算虚拟表中的偏移量,并发现正在调用的函数。我们通过实现一个基本的数据流分析来完成这个回溯,该分析预处理包含调用指令的基本块。这个预处理步骤将让我们查询基本块中任何点的寄存器状态。
|
|
在基本块的每条指令处,我们维护一个寄存器状态表。当我们遍历每个LowLevelILInstruction时,遇到LLIL_SET_REG操作时会更新此表。对于每个跟踪的寄存器,我们存储负责更改其值的LowLevelILInstruction。稍后,我们可以查询此寄存器的状态并检索LowLevelILInstruction,并递归查询src操作数的值,这是寄存器当前表示的表达式。
此外,如果遇到LLIL_CALL操作,那么我们从那时起清除寄存器状态。被调用的函数可能会修改寄存器,因此最安全的假设是调用后的所有寄存器都具有未知值。
现在我们拥有了建模vtable指针解引用和计算虚拟函数偏移量所需的所有数据。
计算虚拟函数偏移量
在深入计算偏移量的任务之前,让我们考虑如何建模行为。回顾图1,分发虚拟函数可以概括为四个步骤:
- 从内存中的对象结构读取指向vtable的指针(LLIL_LOAD)
- 如果要分发的函数不是第一个函数,则向指针值添加偏移量(LLIL_ADD)
- 在计算的偏移量处读取函数指针(LLIL_LOAD)
- 调用函数(LLIL_CALL)
因此,可以通过评估LLIL_CALL指令的src操作数表达式来建模虚拟函数的分发,递归访问每个表达式。当遇到步骤1的LLIL_LOAD指令时,达到递归的基本情况。该LLIL_LOAD的值是指定的vtable指针。vtable指针值被返回并通过先前的递归迭代传播回来,用于这些迭代的评估。
让我们逐步评估一个示例,看看模型如何工作以及如何在Python中实现。以下是在x86中的虚拟函数分发:
|
|
这个汇编将被翻译成以下LLIL:
|
|
为这两个LLIL指令构建树结构会产生以下结构:
图2:示例vtable分发汇编的LLIL树结构。
LLIL_CALL的src操作数是LLIL_LOAD表达式。我们基于其操作为LLIL_CALL的src操作数评估一个处理程序。
|
|
因此,我们对这个虚拟函数分发的递归评估的第一次迭代是调用handle_load
。
|
|
handle_load
首先增加遇到的LLIL_LOAD表达式计数。回想一下,我们解引用vtable的模型期望两个LLIL_LOAD指令:vtable指针,然后是我们想要的函数指针。向后追踪程序状态意味着我们将首先遇到函数指针的加载,然后是vtable指针的加载。此时计数为1,因此递归还不应该终止。相反,LLIL_LOAD的src操作数由src表达式的处理程序函数递归评估。当对处理程序的调用完成时,addr
应该包含指向要分发的函数指针的地址。在这种情况下,src是LLIL_ADD,因此调用handle_add
。
|
|
handle_add
递归评估LLIL_ADD表达式的左侧和右侧,并将这些值的总和返回给其调用者。在我们的示例中,左操作数是LLIL_REG表达式,因此调用handle_reg
。
|
|
这就是我们的数据流分析发挥作用的地方。使用current_defs
描述的当前寄存器状态,我们识别表示此LLIL_REG表达式当前值的LLIL表达式。基于上面的示例,current_defs['eax']
将是表达式[ecx].d
。这是另一个LLIL_LOAD表达式,因此再次调用handle_load
。这次,load_count
增加到2,满足基本情况。如果我们假设在我们的示例中用户选择的类的构造函数位于0x1000,那么handle_load
将返回值0x1000。
左操作数评估完成后,现在是handle_add
评估右操作数的时候了。这个表达式是LLIL_CONST,非常容易评估;我们只需返回表达式的值操作数。左右操作数都评估完成后,handle_add
返回表达式的和,即0x1004。handle_load
接收从handle_add
返回的值,然后从BinaryView读取位于该地址的函数指针。然后,我们可以通过调用BinaryView对象中的bv.file.navigate(bv.file.view, function_pointer)
来更改当前显示的函数。
回到之前的LLIL树结构,我们可以注释这些结构以可视化递归和具体数据传播是如何发生的。
图3:示例vtable分发汇编的LLIL树结构,注释了处理程序调用和具体值沿调用链传播。
示例:矩形和三角形
对于一个真实世界的示例,我使用了这个C++教程的稍微修改版本,你可以在这里找到。以下是插件实际运行的演示:
如果你为x86-64和ARM编译virtual-test.cpp,并在安装了插件的Binary Ninja中打开二进制文件,你会发现它可以在两种架构上工作,而无需任何特定于架构的代码。这就是中间表示的美丽之处!
前进并分析
Binary Ninja的LLIL是一个强大的功能,可以轻松开发跨平台程序分析。正如我们所看到的,它的结构简单,但允许表示甚至最复杂的指令。Python API是一个高质量的接口,我们可以有效地使用它来遍历指令并轻松处理操作和操作数。更重要的是,我们已经看到了简单的示例,说明由LLIL启用的数据流分析如何允许我们开发跨平台插件来执行程序分析,而无需实现复杂的启发式方法来计算程序值。你还在等什么?拿起一份Binary Ninja,开始用LLIL编写你自己的二进制分析,别忘了参加Sophia在INFILTRATE 2017上使用Binary Ninja LLIL进行的"Next-level Static Analysis for Vulnerability Research"演讲。
如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News
页面内容
- 示例插件:导航到虚拟函数
- 发现vtable指针
- 寄存器分配的传播
- 计算虚拟函数偏移量
- 示例:矩形和三角形
- 前进并分析
最近文章
- The Unconventional Innovator Scholarship
- Hijacking multi-agent systems in your PajaMAS
- We built the security layer MCP always needed
- Exploiting zero days in abandoned hardware
- Inside EthCC[8]: Becoming a smart contract auditor
© 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。