【Angr源码分析】5. VEX语句和表达式

我更关心的问题其实是,执行一条 stmt 后,如何反映到内存和寄存器上,如何记录约束信息,以及 VEX IR 的变量如何和机器的寄存器对应起来。

VEX IR

参考链接:

知乎专栏的这篇文章说得已经很详细了。这里主要想引用的部分是,GET 和 PUT 指令都是用来操作寄存器的。而 offset 表示的是寄存器对应的序号,VEX 并没有为 CPU 寄存器创建一个新的存储类型,而是将 CPU 指令对寄存器的访问描述成对内存的访问,实现对寄存器操作的中间表示。VEX 在内存中为寄存器分配了存储空间,为每个寄存器分配了一个索引地址。

Untitled

现在我想知道应该去哪查这个索引对应哪个寄存器,比如 offset=28 到底对应啥。看了这篇博客我回想起来了 irsb.pp() 方法可以打印当前的 IRSB:

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
IRSB {
t0:Ity_I32 t1:Ity_I32 t2:Ity_I32 t3:Ity_I32 t4:Ity_I32 t5:Ity_I32 t6:Ity_I32 t7:Ity_I32 t8:Ity_I32 t9:Ity_I32 t10:Ity_I32 t11:Ity_I32 t12:Ity_I32 t13:Ity_I32 t14:Ity_I32 t15:Ity_I32 t16:Ity_I32 t17:Ity_I32 t18:Ity_I32 t19:Ity_I32 t20:Ity_I32 t21:Ity_I32 t22:Ity_I32 t23:Ity_I32 t24:Ity_I32 t25:Ity_I32

00 | ------ IMark(0x8048450, 2, 0) ------
01 | PUT(ebp) = 0x00000000
02 | PUT(eip) = 0x08048452
03 | ------ IMark(0x8048452, 1, 0) ------
04 | t4 = GET:I32(esp)
05 | t3 = LDle:I32(t4)
06 | t15 = Add32(t4,0x00000004)
07 | PUT(esi) = t3
08 | ------ IMark(0x8048453, 2, 0) ------
09 | PUT(ecx) = t15
10 | ------ IMark(0x8048455, 3, 0) ------
11 | t5 = And32(t15,0xfffffff0)
12 | PUT(cc_op) = 0x0000000f
13 | PUT(cc_dep1) = t5
14 | PUT(cc_dep2) = 0x00000000
15 | PUT(cc_ndep) = 0x00000000
16 | PUT(eip) = 0x08048458
17 | ------ IMark(0x8048458, 1, 0) ------
18 | t8 = GET:I32(eax)
19 | t17 = Sub32(t5,0x00000004)
20 | PUT(esp) = t17
21 | STle(t17) = t8
22 | PUT(eip) = 0x08048459
23 | ------ IMark(0x8048459, 1, 0) ------
24 | t19 = Sub32(t17,0x00000004)
25 | PUT(esp) = t19
26 | STle(t19) = t17
27 | PUT(eip) = 0x0804845a
28 | ------ IMark(0x804845a, 1, 0) ------
29 | t12 = GET:I32(edx)
30 | t21 = Sub32(t19,0x00000004)
31 | PUT(esp) = t21
32 | STle(t21) = t12
33 | PUT(eip) = 0x0804845b
34 | ------ IMark(0x804845b, 5, 0) ------
35 | t23 = Sub32(t21,0x00000004)
36 | PUT(esp) = t23
37 | STle(t23) = 0x08048460
NEXT: PUT(eip) = 0x08048483; Ijk_Call
}

这么看来 28 对应 ebp,68 对应 eip。对应关系保存在 irsb.arch.register_size_names 中,可以调用 irsb.arch.translate_register_name() 来解析 offset。和程序的架构有关。具体这个表怎么构建的就不看了(其实是因为太麻烦了)。


IR 的执行

简单例子 1 —— “PUT(ebp) = 0x00000000”

上一篇文章提到了,执行一条 stmt 是通过调用 _handle_vex_stmt() 函数:

1
2
3
4
5
6
# venv/lib/python3.8/site-packages/angr/engines/vex/light/light.py
def _handle_vex_stmt(self, stmt: pyvex.stmt.IRStmt):
# 根据 stmt 的类型找到合适的处理函数
handler = self._vex_stmt_handlers[stmt.tag_int]
# 调用处理函数
handler(stmt)

执行 PUT 操作对应的 handler 就是 _handle_vex_stmt_Put() 函数:

1
2
3
4
5
# venv/lib/python3.8/site-packages/angr/engines/vex/light/light.py
def _handle_vex_stmt_Put(self, stmt):
self._perform_vex_stmt_Put(
self._handle_vex_const(pyvex.const.U32(stmt.offset)),
self._analyze_vex_stmt_Put_data(stmt.data))

对于 PUT(offset=28) = 0x00000000,stmt.offset 是寄存器的编号即 28,stmt.data 是一个 pyvex.expr.Const 对象,其值为 0x0。重点在 self._perform_vex_stmt_Put 函数。这个函数的具体实现在 SimStateStorageMixin 中:

1
2
3
4
5
6
7
8
9
# venv/lib/python3.8/site-packages/angr/engines/vex/heavy/heavy.py
class SimStateStorageMixin(VEXMixin):
... ...
def _perform_vex_stmt_Put(self, offset, data, action=None, inspect=True):
self.state.registers.store(offset, data, action=action, inspect=inspect)

def _perform_vex_stmt_Store(self, addr, data, endness, action=None, inspect=True, condition=None):
self.state.memory.store(addr, data, endness=endness, action=action, inspect=inspect, condition=condition)
... ...

可以看到 SimStateStorageMixin 中定义的全都是和内存,寄存器存储有关的操作。PUT 操作的实现也非常简单,就是单纯的调用了 self.state.registers.store() 方法将表达式(BVV 0x0)保存到寄存器(EBP)里。

简单例子 2 —— “t4 = GET:I32(esp)”

由于 VEX IR 并没有新定义寄存器,所以 PUT/GET 操作直接对通用寄存器操作即可。不过 VEX IR 使用了临时变量如 t3,t4,我想看看这些中间变量是如何保存的。

对临时变量的读写操作定义在 VEXMixin 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# venv/lib/python3.8/site-packages/angr/engines/vex/light/light.py
class VEXMixin(SimEngineBase):
... ...
def _analyze_vex_stmt_WrTmp_data(self, *a, **kw):
return self._handle_vex_expr(*a, **kw)

def _handle_vex_stmt_WrTmp(self, stmt):
self._perform_vex_stmt_WrTmp(
stmt.tmp,
self._analyze_vex_stmt_WrTmp_data(stmt.data)
)
... ...
def _handle_vex_expr(self, expr: pyvex.expr.IRExpr):
handler = self._vex_expr_handlers[expr.tag_int]
result = handler(expr)
return self._instrument_vex_expr(result)

这里明显只是入口函数,stmt.tmp 表示临时变量的序号。在这里 t4 对应的值就是 4。这里提一下,有一个成员变量 self.tmps 是一个数组,长度为 26,负责保存 26 个临时变量 (t0~t25)。另一个函数 self._analyze_vex_stmt_WrTmp_data() 负责解析出最终向临时变量保存什么值。在这个例子中,就是将 esp 的值取出来。真正的解析工作由 _handle_vex_expr() 实现。

之前在 VEXMixin 中看到 _handle_vex_expr 和 _handle_vex_stmt 这两个函数的时候还有点分不清楚,现在来看 stmt 表示的就是一个完整的语句,而 expr 则表示其内部取值取地址使用的表达式。

对于 GET:I32 表达式,expr.tag = Iex_Get ,找到的 handler 是 _handle_vex_expr_Get 函数:

1
2
3
4
5
6
7
# venv/lib/python3.8/site-packages/angr/engines/vex/light/light.py
class VEXMixin(SimEngineBase):
... ...
def _handle_vex_expr_Get(self, expr: pyvex.expr.Get):
return self._perform_vex_expr_Get(
self._handle_vex_const(pyvex.const.U32(expr.offset)),
expr.ty)

_handle_vex_const 之前已经见过了,就是把 expr.offset 转成 BVV 然后再转回整数。_perform_vex_expr_Get 函数还是在 SimStateStorageMixin 实现的。

1
2
3
4
5
# venv/lib/python3.8/site-packages/angr/engines/vex/heavy/heavy.py
class SimStateStorageMixin(VEXMixin):
def _perform_vex_expr_Get(self, offset, ty, action=None, inspect=True):
return self.state.registers.load(offset, self._ty_to_bytes(ty), action=action, inspect=inspect)
... ...