【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