angr中间表示——官方文档翻译

中间表示

为了能够分析和执行来自不同 CPU 架构(如 MIPS、ARM 和 PowerPC)的机器代码,除了经典的 x86 之外,angr 还在中间表示法上执行大部分分析,中间表示法是对每条 CPU 指令执行的基本操作的结构化描述。通过了解 angr 的 IR、VEX(我们从 Valgrind 借鉴了 VEX),你将能够编写非常快速的静态分析,并更好地理解 angr 是如何工作的。

在处理不同架构时,VEX IR 会抽象掉若干架构差异,从而允许在所有架构上运行单一分析:

寄存器名称。不同架构下的寄存器数量和名称各不相同,但现代 CPU 设计有一个共同的主题:每个 CPU 都包含几个通用寄存器、一个用于保存堆栈指针的寄存器、一组用于存储条件标志的寄存器,等等。IR 为不同平台上的寄存器提供了一致的抽象接口。具体来说,VEX 将寄存器建模为一个独立的内存空间,具有整数偏移量(例如,AMD64 的 rax 从地址 16 开始存储在该内存空间中)。

内存访问。不同的架构以不同的方式访问内存。例如,ARM 可以小前沿和大前沿两种模式访问内存。IR 对这些差异进行了抽象。

内存分段。某些体系结构(如 x86)通过使用特殊的段寄存器来支持内存分段。IR 可以理解这种内存访问机制。

指令的副作用。大多数指令都有副作用。例如,ARM 上 Thumb 模式下的大多数操作都会更新条件标志,而堆栈推/弹指令则会更新堆栈指针。在分析中以临时方式跟踪这些副作用将是非常疯狂的,因此 IR 将这些影响明确化。

集成电路有很多选择。我们使用 VEX,因为将二进制代码提升到 VEX 得到了很好的支持。VEX 是一种与体系结构无关、无副作用的表示法,适用于多种目标机器语言。它将机器代码抽象为一种表示法,旨在使程序分析更容易。这种表示法主要有四类对象

表达式。IR 表达式表示计算值或常量值。这包括内存加载、寄存器读取和算术运算结果。

操作。IR 运算描述了对 IR 表达式的修改。这包括整数运算、浮点运算、位操作等。对 IR 表达式进行 IR 运算的结果是一个 IR 表达式。

临时变量。VEX 将临时变量用作内部寄存器:IR 表达式在使用间隙存储在临时变量中。使用 IR 表达式可以检索临时变量的内容。这些临时变量从 t0 开始编号。这些临时变量是强类型的(例如,"64 位整数 "或 "32 位浮点数")

。IR 块是 IR 语句的集合,代表目标架构中的扩展基本块(称为 "IR 超级块 "或 "IRSB")。一个区块可以有多个出口。对于从基本程序块中间的有条件退出,需要使用特殊的退出 IR 语句。一个 IR 表达式用于表示块结束时无条件退出的目标。

下面是 ARM 上 IR 转换的一个示例。在该示例中,减法运算被翻译成一个由 5 个 IR 语句组成的 IR 代码块,每个 IR 语句至少包含一个 IR 表达式(不过,在现实生活中,一个 IR 代码块通常不止包含一条指令)。寄存器名称被转换成数字索引,分别赋予 GET 表达式和 PUT 语句。精明的读者会发现,实际的减法是由该代码块的前 4 个 IR 语句模拟的,而将程序计数器递增以指向下一条指令(在本例中,该指令位于 0x59FC8)是由最后一个语句模拟的

以下是 ARM 指令:

subs R2, R2, #8

成为这个 VEX IR:

t0 = GET:I32(16)
t1 = 0x8:I32
t3 = Sub32(t0,t1)
PUT(16) = t3
PUT(68) = 0x59FC8:I32

既然你已经了解了 VEX,就可以在 angr 中实际操作一些 VEX:我们使用了一个名为 PyVEX 的库,将 VEX 移植到 Python 中。此外,PyVEX 还实现了自己的漂亮打印功能,可以在 PUT 和 GET 指令中显示寄存器名称,而不是寄存器偏移量。

PyVEX 可通过 Project.factory.block 接口访问 angr。您可以使用许多不同的表示法来访问代码块的语法属性,但它们的共同特点都是分析特定的字节序列。通过 factory.block 构造函数,你可以得到一个 Block 对象,它可以很容易地转换成多种不同的表示法。试着用 .vex 表示 PyVEX IRSB,或用 .capstone 表示 Capstone 代码块。

让我们来运行PyVEX:

import angr
​
# 加载一个二进制文件
>>> proj = angr.Project("/bin/true")
​
# 翻译起始基本模块
>>> irsb = proj.factory.block(proj.entry).vex
# and then pretty-print it
>>> irsb.pp()
​
#翻译并漂亮地打印从地址开始的基本块
>>> irsb = proj.factory.block(0x401340).vex
>>> irsb.pp()
​
#这是基本程序块结束时无条件退出的跳转目标的 IR 表达式
>>> print(irsb.next)
​
# 这是无条件退出的类型(如调用、ret、系统调用等)
>>> print(irsb.jumpkind)
​
# 您还可以美化打印
>>> irsb.next.pp()
​
# 遍历每条语句并打印所有语句
>>> for stmt in irsb.statements:
...     stmt.pp()
​
# 漂亮地打印代表数据的 IR 表达式,以及每个存储语句写入的 IR 表达式的*类型
>>> import pyvex
>>> for stmt in irsb.statements:
...     if isinstance(stmt, pyvex.IRStmt.Store):
...         print("Data:",)
...         stmt.data.pp()
...         print("")
...         print("Type:",)
...         print(stmt.data.result_type)
...         print("")
​
#漂亮地打印基本程序块中每个条件退出的条件和跳转目标
>>> for stmt in irsb.statements:
...     if isinstance(stmt, pyvex.IRStmt.Exit):
...         print("Condition:",)
...         stmt.guard.pp()
...         print("")
...         print("Target:",)
...         stmt.dst.pp()
...         print("")
​
# 这些是 IRSB 中每个临时变量的类型
>>> print(irsb.tyenv.types)
​
# 这是一种方式去得到临时变量0的类型
>>> print(irsb.tyenv.types[0])

条件标志计算(针对 x86 和 ARM)

在 x86 和 ARM CPU 上,最常见的指令副作用之一就是更新条件标志,例如零标志、进位标志或溢出标志。计算机架构师通常会将这些标志的连接(是的,标志的连接,因为每个条件标志都是 1 位宽)放入一个特殊寄存器(即 x86 上的 EFLAGS/RFLAGS,ARM 上的 APSR/CPSR)。这个特殊寄存器存储了有关程序状态的重要信息,对于正确模拟 CPU 至关重要。

VEX 使用 4 个寄存器作为 "标志重块描述符",以记录最新标志设置操作的细节。VEX 采用了一种懒惰策略来计算标志:当更新标志的操作发生时,VEX 不会计算标志,而是将代表该操作的代码存储到 cc_op 伪寄存器中,并将该操作的参数存储到 cc_dep1 和 cc_dep2 中。然后,每当 VEX 需要获取实际的标志值时,它就可以根据其标志重块描述符,找出与相关标志相对应的一位的实际值。这是对标志计算的优化,因为 VEX 现在可以直接在 IR 中执行相关操作,而不必费心计算和更新标志值。

在可以放入 cc_op 的各种操作中,有一个特殊值 0 与 OP_COPY 操作相对应。该操作应该是将 cc_dep1 中的值复制到 flags 中。angr利用这一事实让我们高效地获取标志值:每当我们需要实际的标志值时,angr都会计算它们的值,然后将它们转储回 cc_dep1,并设置 cc_op = OP_COPY 以缓存计算结果。我们还可以使用这个操作来允许用户写入标志:我们只需设置 cc_op = OP_COPY 来表示正在为标志设置一个新值,然后将 cc_dep1 设置为这个新值。

  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值