深入解析Binary Ninja的低级中间语言(LLIL)

本文详细介绍了Binary Ninja的低级中间语言(LLIL)的工作原理、结构特点以及如何使用Python API进行程序分析,包括LLIL指令的树形结构、可视化方法和实际应用案例。

解析Binary Ninja的低级中间语言

嗨,我是Josh。我最近加入了Trail of Bits团队,并且一直是Binary Ninja逆向平台的布道者和插件开发者。我开发了多个简化逆向工程的插件,并扩展了Binary Ninja的架构支持以协助参加microcorruption CTF。Binary Ninja中最让我喜爱的功能之一就是低级中间语言(LLIL),它使得开发强大的程序分析工具成为可能。在Trail of Bits,我们使用LLIL自动化处理大量CTF二进制文件,并自动化识别内存破坏漏洞。

我经常被问到LLIL是如何工作的。在这篇博客文章中,我将回答关于LLIL基础的常见问题,并演示如何使用Python API编写操作LLIL的简单函数。在后续文章中,我将展示如何使用API编写同时利用LLIL和Binary Ninja自身数据流分析的插件。

什么是低级中间语言?

编译器使用中间表示(IR)来分析和优化正在编译的代码。这种IR通过将源语言转换为工具链组件理解的单一标准语言来生成。然后,工具链组件可以在各种架构上执行通用任务,而无需单独实现这些任务。

类似地,Binary Ninja不仅反汇编二进制代码,还利用其自己的IR(称为低级中间语言)的力量来执行数据流分析。数据流分析使用户能够在任意指令处查询寄存器值和堆栈内容。这种分析是与架构无关的,因为它是在LLIL上执行的,而不是在汇编代码上。事实上,当我为MSP430架构编写提升器(lifter)时,我自动免费获得了这种数据流分析。

让我们直接深入了解低级中间语言的工作原理。

查看低级中间语言

在UI中,低级中间语言只能在图形视图(Graph View)中查看。可以通过右下角的"Options"菜单或通过i热键访问。IL视图和图形视图之间的区别很明显;IL视图使用中缀表示法,看起来更接近高级语言。这一点,加上IL是所有架构都转换到的一套标准化指令集,使得处理不熟悉的语言变得容易。

图形视图与IL视图;左侧是ARM(顶部)和x86-64(底部)相同函数的汇编图形视图。右侧是它们各自图形视图的IL视图。

如果您不熟悉这个特定架构,那么可能不容易理解汇编代码的语义。然而,LLIL的含义是清晰的。您可能还会注意到,通常LLIL指令比汇编指令多。汇编到LLIL的转换实际上是一对多而不是一对一的转换,因为LLIL是指令集的简化表示。例如,x86的repne cmpsb指令甚至会在LLIL中生成分支和循环:

x86指令repne cmpsb的低级中间语言表示

如何在LLIL上执行分析?要弄清楚这一点,我们首先深入了解LLIL的结构。

低级中间语言结构

根据API文档,LLIL指令具有基于树的结构。LLIL指令树的根是一个表达式,由一个操作和零到四个操作数作为子节点组成。子节点可以是整数、字符串、整数数组或另一个表达式。由于每个子表达式可以有自己的子表达式,因此可以构建任意顺序和复杂度的指令树。以下是一些示例表达式及其操作数:

操作 操作数1 操作数2 操作数3 操作数4
LLIL_NOP
LLIL_SET_REG dest: 字符串或整数 src: 表达式
LLIL_LOAD src: 表达式
LLIL_CONST constant: 整数
LLIL_IF condition: 表达式 true: 整数 false: 整数
LLIL_JUMP_TO dest: 表达式 targets: 整数数组

让我们看几个提升的x86示例,以更好地理解在提升指令时这些树是如何生成的:首先是一个简单的mov指令,然后是一个更复杂的lea指令。

示例:mov eax, 2

mov eax, 2的LLIL树

这条指令有一个单一操作mov,它被转换为LLIL表达式LLIL_SET_REG。LLIL_SET_REG指令有两个子节点:dest和src。dest是一个reg节点,它只是一个表示将被设置的寄存器的字符串。src是另一个表达式,表示如何设置dest寄存器。

在我们的x86指令中,目标寄存器是eax,所以dest子节点就是eax;很简单。源表达式是什么?嗯,2是一个常量值,所以它将被转换为LLIL_CONST表达式。LLIL_CONST表达式有一个单一子节点constant,它是一个整数。树中没有其他节点有子节点,因此指令完成。将所有内容放在一起,我们得到上面的树。

示例:lea eax, [edx+ecx*4]

lea eax, [edx+ecx*4]的LLIL树

这条指令的最终结果也是设置寄存器的值。这棵树的根也将是LLIL_SET_REG,其dest将是eax。src表达式是一个包含加法和乘法的数学表达式……是吗?

如果我们添加括号来明确定义操作顺序,我们得到(edx + (ecx * 4));因此,src子树的根将是一个LLIL_ADD表达式,它有两个子节点:left和right,两者都是表达式。加法的左侧是一个寄存器,因此我们树中的left表达式将是一个LLIL_REG表达式。这个表达式只有一个子节点。加法的右侧是我们的乘法,但lea指令中的乘数必须是2的幂,可以转换为左移操作,而这正是提升器所做的:ecx * 4变为ecx « 2。因此,树中的right表达式实际上是一个LLIL_LSL表达式(逻辑左移)。

LLIL_LSL表达式也有left和right子表达式节点。对于我们的左移操作,左侧是ecx寄存器,右侧是常量2。我们已经知道LLIL_REG和LLIL_CONST分别以字符串和整数终止。随着树的完成,我们得到了上面呈现的树。

现在我们了解了LLIL的结构,我们准备深入了解如何使用Python API。在回顾API特性之后,我将演示一个简单的Python函数来遍历LLIL指令并检查其树结构。

使用Python API

Python API中有几个与LL相关的重要类:LowLevelILFunction、LowLevelILBasicBlock和LowLevelILInstruction。还有其他一些,如LowLevelILExpr和LowLevelILLabel,但这些更多用于编写提升器而不是消费IL。

访问指令

要开始使用IL,第一步是获取对函数LLIL的引用。这是通过Function对象的low_level_il属性完成的。如果您在GUI中,可以使用current_function.low_level_il或current_llil获取当前显示函数的LowLevelILFunction对象。

LowLevelILFunction类有很多方法,但它们基本上都用于实现提升器,而不是执行分析。事实上,这个类实际上只用于检索或枚举基本块和指令。__iter__方法已实现并迭代LLIL函数的基本块,getitem__方法已实现并根据索引检索LLIL指令。LowLevelILBasicBlock类也实现了__iter,它迭代属于该基本块的各个LowLevelILInstruction对象。因此,可以根据需要以两种不同的方式迭代LowLevelILFunction的指令:

1
2
3
4
5
6
7
8
9
# 使用基本块迭代指令
for bb in current_llil.basic_blocks:
  for instruction in bb:
    print instruction

# 直接迭代指令
for index in range(len(current_llil)):
  instruction = current_llil[index]
  print instruction

直接访问指令目前很麻烦。在Python中,这是通过function.get_low_level_il_at(function.arch, address)完成的。应该注意的是,Function.get_low_level_il_at()方法返回给定地址处第一个LLIL指令的LowLevelILInstruction对象;对于像repne cmpsb这样的指令,您必须增加指令索引才能访问其他LLIL指令。

解析指令

LLIL的真正核心暴露在LowLevelILInstruction对象中。所有指令共享的常见成员允许您确定:

  • LLIL指令的包含函数
  • 提升到LLIL的汇编指令的地址
  • LLIL指令的操作
  • 操作的大小(即此指令是操作字节/短整型/长整型/长整型长)

正如我们在上表中看到的,操作数因指令而异。这些可以通过operands成员顺序访问,或通过操作数名称(例如dest、left等)直接访问。当访问具有目标操作数的指令的操作数时,dest操作数将始终是列表的第一个元素。

示例:简单的递归遍历函数

消费LLIL信息的一个非常简单的例子是递归遍历LowLevelILInstruction。在下面的示例中,LLIL指令表达式的操作输出到控制台,以及其操作数。如果操作数也是表达式,则函数也会遍历该表达式,依次输出其操作和操作数。

1
2
3
4
5
6
7
8
9
def traverse_IL(il, indent):
  if isinstance(il, LowLevelILInstruction):
    print '\t'*indent + il.operation.name

    for o in il.operands:
      traverse_IL(o, indent+1)

  else:
    print '\t'*indent + str(il)

将其复制粘贴到Binary Ninja控制台后,选择任何您希望输出树的指令。然后您可以使用bv、current_function和here分别访问当前的BinaryView、当前显示函数的Function对象和当前选定的地址。在以下示例中,我选择了ARM指令ldr r3, [r11, #-0x8]:

提升IL与低级IL

在查看API时,您可能会注意到有诸如Function.get_lifted_il_at与Function.get_low_level_il_at之类的函数调用。这可能会让您不确定应该为分析处理哪一个。答案相当直接:几乎没有任何例外,您总是希望使用低级IL。

提升IL是提升器在解析可执行代码时首先生成的;优化版本是在UI中向用户公开的低级IL。为了演示这一点,尝试创建一个新的二进制文件,并用一堆nop指令填充它,后跟一个ret。反汇编函数并切换到IL视图(在图形视图中按i)后,您将看到只有一个IL指令存在:jump(pop)。这是由于nop指令被优化掉了。

可以在UI中查看提升IL:在首选项中勾选"Enable plugin development debugging mode"。一旦勾选,窗口底部的"Options"选项卡现在将呈现两个查看IL的选项。使用前面的示例,切换到提升IL视图现在将显示一长串nop指令,以及jump(pop)。

通常,除非您正在开发架构插件,否则不需要提升IL。

开始使用LLIL

在这篇博客文章中,我描述了Binary Ninja低级中间语言的基础知识,以及如何使用Python API与之交互。在办公室,Ryan使用LLIL及其数据流分析通过识别要溢出的缓冲区和必须保持完整的canary值来解决2000个CTF挑战二进制文件。Sophia将在INFILTRATE 2017上使用Binary Ninja LLIL展示"Next-level Static Analysis for Vulnerability Research",每个人都应该参加。我希望本指南使使用Binary Ninja编写自己的插件更容易!

在本博客文章的第2部分中,我将通过另一个简单示例展示低级IL及其数据流分析的力量。我们将开发一个简单的、与平台无关的插件,通过解析对象的虚拟方法表的LLIL并计算被调用函数指针的偏移量来导航到虚函数。这使得反转C++二进制文件的行为更容易,因为诸如call [eax+0x10]之类的指令可以解析为已知函数,如object->isValid()。同时,获取您自己的Binary Ninja副本并开始使用LLIL。

更新(2017年2月11日):新版本的Binary Ninja于2017年2月10日发布;本博客文章已更新以反映API的更改。

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

页面内容 什么是低级中间语言? 查看低级中间语言 低级中间语言结构 示例:mov eax, 2 示例:lea eax, [edx+ecx*4] 使用Python API 访问指令 解析指令 示例:简单的递归遍历函数 提升IL与低级IL 开始使用LLIL 最近文章 非传统创新者奖学金 劫持您的PajaMAS中的多代理系统 我们构建了MCP一直需要的安全层 利用废弃硬件中的零日漏洞 Inside EthCC[8]:成为智能合约审计员 © 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计