案例来自
RITSEC 2022 DataFun
这篇随笔记录一次调教 angr 拆解某个比较弱的 VM 的经历
(字节码的控制流不因输入改变、检验函数不由 VM 执行)
函数 sub_1588 中有一个未被修复的跳转表
修复之可以看出这是一个虚拟机

综合位于 main 函数中初始化的函数
以及 sub_1588 中对 vm 进行操作的小函数
可以复原出 vm 的数据结构
显然这是一个栈
将数据结构送到 F5 可以轻松理解 main 函数的代码
通关字串为R3V3RS1NG_1S_E4SY
load 函数比较特殊
它使用了 PTRACE_TRACEME 反调试
在有调试器的情况下会将opcode XOR 0x36 而不是 0x37
这题的 opcode 并不复杂并且检验函数并不由 VM 执行
opcode 长度为 139
共有 38 次读取单字节的操作即输入应长 38
尝试 angr 一把梭
不过很快就失败了
对于这道题来说一把梭是行不通的
而作为一个懒人,自然是懒得逆向那一大坨字节码
(后来才知道官方的 wp 真就是纯手工逆的,洋洋洒洒 20 多个方程,不过出题人没说怎么解的估计是用了 z3)
于是一些调整是必须的
这题对抗符号执行的地方主要有两处


这个多余的分支每取一次指令就会产生一次,因此每一个 state 经过一遍循环就会导出两个后继,时间复杂度从无分支时的 O(n) 变成了 O(2^n)
这显然是跑不完的
因此本懒人的思路如下
钩子(强行让 ptrace 返回 0):



分支筛选的代码(相当于手动执行基本块)
可以看到在 VM 的执行过程中 angr 不断地踏入危险分支

最后得到一组合法输入
本地验证

25年3月13日于清水湾
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
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
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
;
}
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;
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2025-3-22 23:52
被狗敦子编辑
,原因: 添加图片