flat and deflat theory learning

# intro

利用符号执行去除控制流平坦化 - 博客 - 腾讯安全应急响应中心


在真实块中 会出现 cmov

  • cmovg 是条件移动指令,它的全称是 "Conditional Move if Greater"
1
2
3
4
5
6
7
8
loc_400BB0:
mov eax, 0D3E10A81h
mov ecx, 1E2AAEDEh
mov edx, [rbp+var_8]
cmp edx, 0
cmovg eax, ecx
mov [rbp+var_94], eax
jmp loc_4012E3

任何跳转到预处理器去
预处理器跳到主分发器上去

1
2
3
4
loc_4012E3:
jmp loc_400876
; } // starts at 4007E0
main endp

主分发器根据之前 cmov 出来的值 处理过后 又送到栈中去

1
2
3
4
5
6
7
loc_400876:
mov eax, [rbp+var_94] <<- cmov 移动进去的值
mov ecx, eax
sub ecx, 8364E2E6h
mov [rbp+var_A4], eax
mov [rbp+var_A8], ecx
jz loc_400DD3

# 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
2
3
4
5
6
7
8
9
10

.text:0000000000400A02                 jmp     $+5
.text:0000000000400A07 ; ---------------------------------------------------------------------------
.text:0000000000400A07
.text:0000000000400A07 loc_400A07:                             ; CODE XREF: main+222↑j
.text:0000000000400A07                 mov     eax, [rbp+var_A4]
.text:0000000000400A0D                 sub     eax, 16FA3CEh
.text:0000000000400A12                 mov     [rbp+var_E0], eax
.text:0000000000400A18                 jz      loc_40108C

再就是合并 fake_return 的边 所谓 fake_return 就是在函数内部发生 syscall 或者 call 一个系统调用函数而产生的边 由于在建模超级图的时候 将内核 api 当做黑盒 所以要合并 fake_return

总结:

  1. 删除 outside edge
  2. 合并 B->A 的边(当且仅当 B 出度为 1 且 A 入度为 1)
  3. 合并 fake_return 的边

# 在 supergraph 去平坦化

先看下效果

可以看到这种单纯为了选择判断的块已经被全部 nop 掉了 只留下了最后一个语句用于 patch 到 angr 执行出来的块

看看 0x4007EC 这个地址 patch 前

1
2
3
4
5
6
7
8
9
.text:00000000004007EC loc_4007EC:                             ; CODE XREF: check_password+127↑j
.text:00000000004007EC mov eax, 2D26C43Ah
.text:00000000004007F1 mov ecx, 2709F73h
.text:00000000004007F6 movsxd rdx, [rbp+var_14]
.text:00000000004007FA mov rsi, [rbp+var_10]
.text:00000000004007FE cmp byte ptr [rsi+rdx], 0
.text:0000000000400802 cmovnz eax, ecx
.text:0000000000400805 mov [rbp+var_1C], eax
.text:0000000000400808 jmp loc_40099B

patch 后

1
2
3
4
5
6
7
4007ec:       b8 3a c4 26 2d          mov    eax,0x2d26c43a
4007f1: b9 73 9f 70 02 mov ecx,0x2709f73
4007f6: 48 63 55 ec movsxd rdx,DWORD PTR [rbp-0x14]
4007fa: 48 8b 75 f0 mov rsi,QWORD PTR [rbp-0x10]
4007fe: 80 3c 16 00 cmp BYTE PTR [rsi+rdx*1],0x0
400802: 0f 85 11 00 00 00 jne 400819 <check_password+0x2e9>
400808: e9 00 00 00 00 jmp 40080d <check_password+0x2dd>

cmovx 之下都被替换了

对于一个 supergraph

  1. 先找出预分发器 (在 supergraph 中 我们确定了序言块和主分发器就能确定预分发器 因为主分发器只有两个入度 而唯一没有入度的只有序言块)
  2. 所有与预分发器相邻的边的另外一个结点 大部分都是真实块 除去以上提及的 都是无关的子分发器 需要 nop 掉
  3. 双重遍历 遍历每个真实块和每个真实块中的每一条指令
    1. 真实块中存在三种情况(实际上可以合并为两种
      1. 纯纯的控制块 就是往栈里写数据 然后 jmp 到预处理器 没有任何实际代码内容 方便主分发器进行分支分配
      2. 执行了部分要执行的代码内容 然后直接 jmp 到预处理器
      3. 执行了部分要执行的代码内容 然后用 cmov 去选择性存储部分寄存器 然后 jmp 到预处理器
    2. 这样三种情况实际上分为两类 有无 cmov 因为 cmov 在未混淆的代码中本质上是一个条件跳转的指令 没有 cmov 就是直接跳转
      1. 分为有 cmov (需要 patch 成条件跳转指令)
      2. 分为没 cmov (需要 patch 成直接跳转指令)
    3. 这里格外提一嘴 作者在这个步骤中加入所有 call 到一个有待 hook 的列表 (就是在 angr 中遇到 call 直接 hook 里面修改 ip 到下一条指令) 这是为了方便后面的模拟执行 我们只需要知道 如果 cmovx 这里 条件为真 我们下一个走到的真实块是啥 如果为假 我们走到的又是啥 根本不关心到底在运行时是真是假 因为只要把两种条件的 jmp 全 patch 好 就会自然而然的走到我们要去的地方
  4. 再就是模拟执行 如果有分支 就要跑两次 分别把 ITE (if-then-else) 修改为 0 和 1 然后看最终跑到了哪个真实块 这就是要 patch 的地址 如果没有分支 只需要跑一次
  5. patch: 如果是 cmove 那就把刚刚模拟执行得到的地址 patch 成 je 然后下面跟一个 jmp 即可 如果是 jmp 直接 patch 过去即可

最终 ida 中偏底下的就这几种类型