作者:bird@tsrc
1. 背景
1.1 控制流平坦化
控制流平坦化(control flow flattening)的基本思想主要是通過一個主分發器來控制程序基本 塊的執行流程,例如下圖是正常的執行流程

經過控制流平坦化后的執行流程就如下圖

這樣可以模糊基本塊之間的前后關系,增加程序分析的難度,同時這個流程也很像VM的執行 流程。更多控制流平坦化的細節可以看Obfuscating C++ programs via control flow flattening,本文以 Obfuscator-LLVM 的控制流平坦化為例。
1.2 符號執行
符號執行 是一種重要的形式化方法和軟件分析技術,通過使用符號執行技術,將程序中變量的值表示為符號值和常量組成的計算表達式,符號是指取值集合的記號,程序計算的輸出被表示為輸入符號值的函數,其在軟件測試和程序驗證中發揮著重要作用,并可以應用于程序漏洞的檢測。
符號執行的發展是從靜態符號執行到動態符號執行到選擇性符號執行,動態符號執行會以具體 數值作為輸入來模擬執行程序,是混合執行(concolic execution)的典型代表,有很高的精確度,目前較新的符號執行工具有Triton和angr,本文是以angr為例。
2. 分析
首先寫一個簡單的示例程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_password(char *passwd) {
int i, sum = 0;
for (i = 0; ; i++) {
if (!passwd[i]) {
break;
}
sum += passwd[i];
}
if (i == 4) {
if (sum == 0x1a1 && passwd[3] > 'c' && passwd[3] < 'e' &&passwd[0] == 'b') {
if ((passwd[3] ^ 0xd) == passwd[1]) {
return 1;
}
puts("Orz...");
}
}
else{
puts("len error");
}
return 0;
}
int main(int argc, char **argv) {
if (argc != 2){
puts("error");
return 1;
}
if (check_password(argv[1])){
puts("Congratulation!");
}
else{
puts("error");
}
return 0;
}
編譯
gcc check_passwd.c -o check_passwd
用IDA查看未經過控制流平坦化的控制流程圖(CFG)

添加控制流平坦化
build/bin/clang check_passwd.c -o check_passwd_flat -mllvm -fla
可以看到控制流平坦化后的CFG非常漂亮

通過分析可以發現原始的執行邏輯只在真實塊(自己想的名稱...)以及序言和retn塊中,其中會
產生分支的真實塊中主要是通過CMOV指令來控制跳轉到哪一個分支,因此只要確定這些塊的 前后關系就可以恢復出原始的CFG,這個思路主要是參考Deobfuscation: recovering an OLLVM-protected program。
3. 實現
3.1 獲取真實塊、序言、retn塊和無用塊
由于angr的CFG跟IDA的有點不同,因此本文使用BARF來獲取,后來問了Fish Wang可以 用angr-management 下的to_supergraph來獲取。主要思路:
- 函數的開始地址為序言的地址
- 序言的后繼為主分發器
- 后繼為主分發器的塊為預處理器
- 后繼為預處理器的塊為真實塊
- 無后繼的塊為retn塊
- 剩下的為無用塊
主要代碼:


3.2 確定真實塊、序言和retn塊的前后關系
這個步驟主要是使用符號執行,為了方便,這里把真實塊、序言和retn塊統稱為真實塊,符號 執行從每個真實塊的起始地址開始,直到執行到下一個真實塊。如果遇到分支,就改變判斷值 執行兩次來獲取分支的地址,這里用angr的inspect在遇到類型為ITE的IR表達式時,改變臨時 變量的值來實現,例如下面這個塊

使用statement before類型的inspect:

修改臨時變量28為false或true再執行就可以得到分支的地址
t48 = ITE(t28,0xca2df6de,0xaf59b039)
如果遇到call指令,使用hook的方式直接返回

主要代碼:

3.3 Patch二進制程序
首先把無用塊都改成nop指令

然后針對沒有產生分支的真實塊把最后一條指令改成jmp指令跳轉到下一真實塊

針對產生分支的真實塊把CMOV指令改成相應的條件跳轉指令跳向符合條件的分支,例
如 CMOVZ 改成 JZ ,再在這條之后添加 JMP 指令跳向另一分支

上述就是去除控制流平坦化的總體實現思路。
4. 演示
去除制定函數的控制流平坦化
python deflat.py check_passwd_flat 0x400530
用IDA查看恢復后的CFG


可以看到CFG跟原來的大致一樣,然后反編譯恢復出原始代碼

5. 總結
本文主要針對 x86 架構下 Obfuscator-LLV M的控制流平坦化,但最重要的是去除控制流平坦化 過程中的思路,同時當函數比較復雜時可能速度會有點慢。有時間會以此為基礎嘗試分析偽造 控制流、指令替換和 VM 等軟件保護手段,另外符號執行也可以應用于漏洞挖掘領域,例如借 助符號執行生成覆蓋率更高的Fuzzing測試集以及求解達到漏洞點的路徑等。由于小弟剛學習 符號執行,可能有理解錯誤的地方,歡迎研究符號執行或者認為有更好思路的師傅們吐槽。。 。最后,感謝 angr 主要開發者 Fish Wang 在這期間的耐心幫助。
6. 參考
- Obfuscating C++ programs via control flow flattening
- https://github.com/obfuscator-llvm/obfuscator/tree/llvm-3.6.1
- Symbolic Execution and Program Testing
- Selective Symbolic Execution
- CUTE: A Concolic Unit Testing Engine for C
- https://github.com/JonathanSalwan/Triton
- https://github.com/angr/angr
- http://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html
- https://github.com/programa-stic/barf-project
- https://github.com/angr/angr-management
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/192/
暫無評論