强网杯: unicorn_like_a_pro-CTF对抗-看雪-安全社区|安全招聘|kanxue.com
发布日期:2025-01-04 10:02 点击次数:134
强网杯: unicorn_like_a_prounicorn framework 是一个基于 qemu 的模拟执行框架
GitHub链接: https://github.com/unicorn-engine/unicorn
这道题目内部就调用了 unicorn 框架模拟执行一段 x64代码,最开始以为出题人魔改了 unicorn 框架,用 bindiff 分析了一段时间,发现并没有魔改 unicorn 代码
附件 原题 & 脚本 & idb:可以去我博客文章页面下载附件:传送门
1. 符号还原本题没有符号,逆向时比较困难,用 bindiff 可以直接还原。
在 ubuntu 上编译一份 unicorn 代码,载入 bindiff 插件即可完成大部分符号还原。
IDA 中的 bindiff 插件,载入另外一份 idb 后,按下 Ctrl + 6, 点击 如下按钮,设置好阀值后即可导入符号
2. main 函数分析2.1 创建虚拟机
2.2 复制代码到虚拟机的 0x1000 地址
2.3 设置回调
Unicorn 支持很多种回调类型,当 Unicorn 运行的代码满足特定的条件时,将触发对应的回调。
在回调中可以对虚拟机中环境上下文操作,类似调试器的调试回调。
本题借助 Unicorn 指令回调,实现指令即时解密,执行后重新加密,打乱控制流。
注意控制流有两个控制回调,第二个控制回调只在 0x10A3 地址处有效,就是特殊处理的地址。
3. 代码解密分析代码解密的主要逻辑在 decrypt 函数,该函数解密当前即将执行的基本块,加密上一个执行完的基本块
基本块密钥用 miniDec 函数计算,参数为上一个基本块的入口密钥 lastKey 与当前基本块入口 rip, minidec 函数如下
size_table 是一个数组,该数组保存了基本块密钥与基本块大小的关系,元素结构如下
奇数下标数据为key,偶数为基本块字节的长度。
用 python 实现基本块解密函数
这里的部分代码还涉及控制流重建,后面会提到。
4. 控制流分析ControlFlow1 回调,执行到无效指令会被调用,用于切换程序中的控制流。
该题用 3f 0f作为基本块的结尾(无效指令)触发 ControlFlow1 回调,切换控制流。
ControlFlow1 回调函数根据结尾 rip 与 当前基本块的 key 计算另外一个 key,用于索引当前基本块的后继基本块的信息。
flowInfo 是一个数组,每一个元素有如下5个字段
根据 zf 标志位跳转
注意 v9 保存的是上一个基本块的 key,此处做的 += 运算,即上一个基本块的 key 与 下一个基本块的 key 有关联。
由此可见,基本块的 key 与控制流的路径有关!写解密脚本的时候要考虑路径问题。
另外,当虚拟机中程序运行到 0x10A3 时将调整控制流并更改 key
解密后的基本块,很多都是以读取 fs 寄存器并判断结尾
r15 的值来源于 fs:xxx ,最后再与 fs:xxx 内存的值比较,很明显最后的 zf = 1。
其实并不是,这道题对 fs 寄存器指向的那段内存做了内存读回调,回调如下
每次读取 fs 指向的内存,该内存的值都会被改写。所以 mov 与 cmp 对 fs 内存访问出的结果是不同的,自然 zf = 0,走 zf = 0 的分支。
在控制流重建的时候,需要考虑以读取 fs 内存结尾的基本块,将其看作是无条件跳转,而不是 jz/jnz。
5. 控制流重建重建思路:
以 bfs 遍历顺序,从入口基本块开始解密,解密后再查询分支信息表获取后继基本块的相对偏移与key,最后将新基本块的信息加入到队列,等待分析。遍历时注意维护路径上的 key 累计值。
所有基本块解密完成后,可以得到每个基本块的后继基本块的相对偏移。
要在基本块的结尾插入跳转指令,这将改变代码布局,使得原始相对偏移不可用,所以我采取重编译来解决这个问题,重编译之前将原始基本块的入口地址作为基本的符号名,基本块结尾用 jmp/jz 等指令连接。
code.bin 文件时 dump 出来的原始 code 数据,输出 1.bin 可以直接在 ida 中反编译。
6. 提取出来的代码分析重建控制流,输出 bin 后,代码比较清晰了。
程序入口调用 syscall,由 unicorn 回调处理获取 time(0) 的值,并根据该值计算一个数 0x1C986C3B22EA63E5
为了屏蔽 ida 的优化,方便分析,我做了一些小 patch
syscall 的 unicorn 回调会修改 rax 寄存器,但是 ida 认为 rax 的值是一个可计算的常数,于是后面与 rax 有关的表达式都被优化。
for 循环中读取了两次 fs:0 , 有unicorn回调,每次读取的值肯定不一样,ida 认为两次读取的值一样,于是优化成一次访问。
手动 patch ,即可解决这些问题。
有两种爆破思路:
直接爆破 time(0)
根据 flag 信息反向爆破
可以用 flag 开头来爆破 time(0) 时间常数
from_t0 = time_0 / 0xE10;
由于 4字节一组,qwb{与QWB{ 与 flag 与 FLAG 都是已知的 4 个字节并且 v28 可以非常轻松的获得
通过爆破开头也可以反推 time(0)
爆破出 time(0) 后的做法就是非常简单的计算任务了
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法