type
status
date
slug
tags
summary
category
icon
password
以前一直没有完整写一个去混淆脚本出来,这两天一并学完它们。ollvm这块总体思路参考deflat,进行了一定的优化,脚本基于angr和idapython,最后均适配到idapro9.0上面去。
demo1
识别
首先是ollvm的识别,对于标准的ollvm,其序言,主分发器,retn块,预处理器都比较容易判别,块与块的连接关系很明显,然后就能通过里面的内容判断真实块(更复杂的ollvm中,往往会将真实块的判定作为一个难点,例如一定程度上混淆真实块和子分发器,甚至模糊ollvm的各个组成部分)

首先,我们要知道我们最终的目的:将所有真实块按原有逻辑串联起来,所以首先是要将所有真实块识别出来。
观察前面的图,我们可以得出一个结论(并不普适)
一个程序只有一个序言,序言块没有入度 序言的后继是主分发器 主分发器的入度除了序言块就是预处理器 预处理器的入度是真实块 没有后继的是retn块 其它的块是子分发器
可以获得所有的块

控制流恢复
找到一个伪代码,可以看到这里的v9完全接管了控制流,这里也可以作为ollvm判断的一个特征

我们知道,通常控制流有三种变化方式,姑且归纳为call,jmp,jz/jnz,更多的控制流变化我们暂时归在这里面
这三种情况我们都需要处理。
- call
angr是比较怕掉系统函数的,这个demo中我们把call全部hook掉,具体操作而言,扫描所有hook的地址
给它们挂上hook,,proj.hook默认跳过length长度的指令,函数只用声明state参数然后return即可。
- jmp

比较明显,执行到这个块后,这个块就会给v9赋值,就相当于跳转到下一个块
- jz/jnz

针对一个非v9变量做一个判断,依此对v9做赋值,我们这里提取关键:cmovxx这个指令作为识别标准,检测到cmov后知道这个块存在不同的分叉
angr本身的模拟执行特别容易路径爆炸,测试过从头到尾都模拟,很容易陷进奇怪的地方。因此策略是,从一个基本块开始,执行到下一个基本块就结束,获得一条路径;如果有路径分叉,就本策略走两次获得两条路径:
执行过程:
对cmov的情况,是这样处理的:
(最后给完整代码)
目前就能跑出来控制流了

写回控制流
有前面的铺垫,到这就比较容易了
- jmp直接patch基本块最后一条指令
- jz/jnz从cmov开始patch
- 序言块特判一下
完整代码:
没有patch很干净,但伪代码清晰了,够用

demo2
这个和上一个差的没那么大,还是属于相对标准的处理
差异
其它部分差不多,主要是控制流这部分需要注意,找一个来看看
初始化:

中间的控制流:


首先,call不能无脑跳过了,5720这个call必须保留出来
其次,v13的值会被修改和记忆,导致每个控制流之间没有完全独立了,用前面的方法会去不掉(实际上,这个demo虽然简单,但d810也去不掉它的)
这时候应该从头执行到尾了吗?
答案是不能,我的测试是还会爆(也许有更好的优化方法?目前我剪枝剪很久没弄出来,有的话以后再学学)
那么还是得用前面的策略,一个基本块执行到下一个就cut掉,这时候可以在每次创建的时候不使用blank_state,而是记录上一条入度(注意只用一条)的state并继承来用。这时候整个循环也做修改,从顺序跑改为dfs跑一遍所有的块
其它部分不变:
这样跑完的话,控制流出来了,但还需要nop掉这些真实块中的无用函数才能完全恢复,不过总体来说不影响阅读

- 作者:moyaoxue
- 链接:https://moyaoxue.de/article/29225ac9-6ea8-8043-bf46-e16b6314a3d6
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

