# intro
利用符号执行去除控制流平坦化 - 博客 - 腾讯安全应急响应中心
在真实块中 会出现 cmov
- cmovg 是条件移动指令,它的全称是 "Conditional Move if Greater"
1 | loc_400BB0: |
任何跳转到预处理器去
预处理器跳到主分发器上去
1 | loc_4012E3: |
主分发器根据之前 cmov 出来的值 处理过后 又送到栈中去
1 | loc_400876: |
# supergraph
am_graph.py 脚本来自于 angr-management/utils/graph.py,用于将 CFG 转换为 supergraph,因为 angr 框架中 CFG 与 IDA 中的不太一样。
A super transition graph is a graph that looks like IDA Pro’s CFG, where calls to returning functions do not terminate basic blocks.
超级块的概念是:一个函数的多个基本块合并起来组成的超级快 不包括这个函数跳转到其他函数
# 构建超级图的过程
先删除外部转移边 也就是 outside=True 的 edge 这样这个图就没有向外的边了(比如跳转到其他函数的边)。
再合并那种 jmp 的块 也就是 A 块的出口只能到 B (比如 jmp) 这两个本来就应该合在一起的 分成其他块意义不大 因为没有其他路径可以走
1 |
|
再就是合并 fake_return 的边 所谓 fake_return 就是在函数内部发生 syscall 或者 call 一个系统调用函数而产生的边 由于在建模超级图的时候 将内核 api 当做黑盒 所以要合并 fake_return
总结:
- 删除 outside edge
- 合并 B->A 的边(当且仅当 B 出度为 1 且 A 入度为 1)
- 合并 fake_return 的边
# 在 supergraph 去平坦化
先看下效果
可以看到这种单纯为了选择判断的块已经被全部 nop 掉了 只留下了最后一个语句用于 patch 到 angr 执行出来的块
看看 0x4007EC 这个地址 patch 前
1 | .text:00000000004007EC loc_4007EC: ; CODE XREF: check_password+127↑j |
patch 后
1 | 4007ec: b8 3a c4 26 2d mov eax,0x2d26c43a |
cmovx 之下都被替换了
对于一个 supergraph
- 先找出预分发器 (在 supergraph 中 我们确定了序言块和主分发器就能确定预分发器 因为主分发器只有两个入度 而唯一没有入度的只有序言块)
- 所有与预分发器相邻的边的另外一个结点 大部分都是真实块 除去以上提及的 都是无关的子分发器 需要 nop 掉
- 双重遍历 遍历每个真实块和每个真实块中的每一条指令
- 真实块中存在三种情况(实际上可以合并为两种
- 纯纯的控制块 就是往栈里写数据 然后 jmp 到预处理器 没有任何实际代码内容 方便主分发器进行分支分配
- 执行了部分要执行的代码内容 然后直接 jmp 到预处理器
- 执行了部分要执行的代码内容 然后用 cmov 去选择性存储部分寄存器 然后 jmp 到预处理器
- 这样三种情况实际上分为两类 有无 cmov 因为 cmov 在未混淆的代码中本质上是一个条件跳转的指令 没有 cmov 就是直接跳转
- 分为有 cmov (需要 patch 成条件跳转指令)
- 分为没 cmov (需要 patch 成直接跳转指令)
- 这里格外提一嘴 作者在这个步骤中加入所有 call 到一个有待 hook 的列表 (就是在 angr 中遇到 call 直接 hook 里面修改 ip 到下一条指令) 这是为了方便后面的模拟执行 我们只需要知道 如果 cmovx 这里 条件为真 我们下一个走到的真实块是啥 如果为假 我们走到的又是啥 根本不关心到底在运行时是真是假 因为只要把两种条件的 jmp 全 patch 好 就会自然而然的走到我们要去的地方
- 真实块中存在三种情况(实际上可以合并为两种
- 再就是模拟执行 如果有分支 就要跑两次 分别把 ITE (if-then-else) 修改为 0 和 1 然后看最终跑到了哪个真实块 这就是要 patch 的地址 如果没有分支 只需要跑一次
- patch: 如果是 cmove 那就把刚刚模拟执行得到的地址 patch 成 je 然后下面跟一个 jmp 即可 如果是 jmp 直接 patch 过去即可
最终 ida 中偏底下的就这几种类型