【CTF对抗-调教angr拆解一道VM逆向】此文章归类为:CTF对抗。
案例来自
RITSEC 2022 DataFun
这篇随笔记录一次调教 angr 拆解某个比较弱的 VM 的经历
(字节码的控制流不因输入改变、检验函数不由 VM 执行)
心路历程
函数 sub_1588 中有一个未被修复的跳转表
修复之可以看出这是一个虚拟机

综合位于 main 函数中初始化的函数
以及 sub_1588 中对 vm 进行操作的小函数
可以复原出 vm 的数据结构
1 2 3 4 5 6 7 | 00000000 vm_t struc ; (sizeof=0x18, mappedto_8)
00000000 sz dd ?
00000004 field_4 dd ?
00000008 buf dq ?
00000010 top dd ?
00000014 field_14 dd ?
00000018 vm_t ends
|
显然这是一个栈
将数据结构送到 F5 可以轻松理解 main 函数的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | int main() {
setbuf(stdout, 0LL);
puts("Welcome to my program. Give me some data, and let's see if you get the flag!");
read(0, buf, 512uLL);
vm = load(code, 139);
v5 = 0;
for ( i = 0; i <= 138; ++i )
{
run(code[i], buf[v5], vm);
if ( get_byte_from_input(code[i]) )
++v5;
}
s = clone_stack(vm);
v3 = strlen(s);
if ( v3 == strlen(s) && !strcmp(s, "R3V3RS1NG_1S_E4SY") )
printf("You got it. The flag is RS{%s}\n", "PLACEHOLDER_FLAGS_ROCK_!!!!!!");
else
puts("Not quite");
return 0LL;
}
|
通关字串为R3V3RS1NG_1S_E4SY
load 函数比较特殊
它使用了 PTRACE_TRACEME 反调试
在有调试器的情况下会将opcode XOR 0x36 而不是 0x37
1 2 3 4 5 6 7 8 9 10 | vm_t *__fastcall load(char *a1, int a2)
{
int i; // [rsp+20h] [rbp-10h]
int v4; // [rsp+24h] [rbp-Ch]
v4 = 0x37 - (ptrace(PTRACE_TRACEME, 0LL) == -1);
for ( i = 0; i < a2; ++i )
a1[i] ^= v4;
return vm_init(0x10000);
}
|
这题的 opcode 并不复杂并且检验函数并不由 VM 执行
opcode 长度为 139
共有 38 次读取单字节的操作即输入应长 38
尝试 angr 一把梭
不过很快就失败了
正文
对于这道题来说一把梭是行不通的
而作为一个懒人,自然是懒得逆向那一大坨字节码
(后来才知道官方的 wp 真就是纯手工逆的,洋洋洒洒 20 多个方程,不过出题人没说怎么解的估计是用了 z3)
于是一些调整是必须的
符号执行的一大弱点就在于分支爆炸
简单来说,需要对两点进行改动
- ptrace 的干扰:angr 会添加一个其返回值不确定的符号从而导致 opcode 不固定爆出多余分支
- run 函数中如果遇到运算错误会跳转到一个纠错分支即在栈上插入 0xAA
但在 angr 的世界中没有“如果”:所有这样的分支都会被纳入后继状态中导致指数爆炸
因此本懒人的思路如下
- 在 ptrace 上下钩子
- 将 stdin 设置为 38 字节的位符号
- 在执行 run 函数的过程中由于 opcode 固定 不会产生除了纠错分支外的其他分支
一旦遇到大于一个分支的状态且地址为纠错分支的地址(大概有 3 处)就消减掉它
- 除此之外一旦遇到大于一个分支就停下 代表我们到达了最终的 strcmp
钩子:
1 2 3 4 5 | class MutePtrace(angr.SimProcedure):
def run(self,*args,**kwargs):
return claripy.BVV(0,32)
project.hook_symbol('ptrace',MutePtrace(),replace=True)
|
分支筛选(手动执行基本块)
1 2 3 4 5 6 7 8 9 10 11 12 | blocked=[0x165c,0x1748,0x182b]
true_addr=0x1a45
false_addr=0x1a5f
while len(simgr.active)==1:
while len(simgr.active)==1:
simgr.step()
print(simgr.active)
simgr.move(from_stash='active', to_stash='unsat', filter_func=lambda s:s.addr-base_addr in blocked)
true_state=next(x for x in simgr.active if x.addr==base_addr+true_addr)
true_state
|
可以看到在 VM 的执行过程中 angr 不断地踏入危险分支

最后得到一组合法输入
1 | expect=b'R3\x00\xff\xaa3\x00R\x01\xae5\x00\x8d\x1d\x07_1\x00\x00:\x19\x00\x0cx\x86\x01\x02\x03\x04\x05\x06<\x00\xa3S\xff\xaf\x00\n'
|
本地验证

25年3月13日于清水湾
更多【CTF对抗-调教angr拆解一道VM逆向】相关视频教程:www.yxfzedu.com