反混淆:恢復被OLLVM保護的程序


譯者序:

OLLVM作為代碼混淆的優秀開源項目,在國內主流app加固應用中也經常能看到它的身影,但是公開的分析研究資料寥寥。本文是Quarkslab團隊技術博客中一篇關於反混淆的文章,對OLLVM項目中的控制流平坦化、虛假控制流、指令替換等混淆策略進行了針對性分析,並展示了利用符號執行對OLLVM混淆進行清理的一些思路和成果。

文章側重於反混淆思路的講解,並沒有提供完整的代碼方案,部分關鍵點也有所保留。文章末尾會提供編譯好的測試程序(基於LLVM3.6.1),對OLLVM反混淆感興趣的讀者可以結合實例繼續研究,后續有機會也會分享關於OLLVM還原的實例分析案例。外文技術文章翻譯確實不易,初次嘗試難免有誤,歡迎留言指正。

【原文】: 《Deobfuscation: recovering an OLLVM-protected program》

我們最近研究了Obfuscator-LLVM項目,目的是測試其不同的保護策略。下面是我們關於如何處理代碼混淆的結論與解決方案。

介紹

由於有時需要處理一些經過重度混淆的代碼,我們打算研究Obfuscator-LLVM項目,檢查其所生成混淆代碼的優缺點。我們使用其可用的最新版本(基於LLVM3.5)。下面將展示如何使用Miasm逆向工程框架來突破所有的保護。

敬告:本文只是為了展示解決OLLVM混淆處理的一種方法。雖然包含許多代碼示例,但本文不是Miasm教程,也沒有提供相應的Python腳本打包下載。當然我們可以搞長篇大論,分析一個被OLLVM混淆保護的復雜程序,或者是一個Miasm不支持的CPU架構等等…然而並沒有這些。我們將保持簡單化,並展示我們如何清理混淆代碼。

首先,我們會介紹分析過程中使用的所有工具,然后針對一個簡單的示例程序,展示如何逐層(最后累加)打破OLLVM的保護方案。

免責聲明:Quarkslab團隊也在開發基於LLVM的混淆保護。所以我們對於設計混淆器和反混淆都有過一些研究。但這些成果尚未公開,並且也不准備公開。所以我們研究了OLLVM,因為我們了解其中會遇到的的困難。OLLVM是一個有意義的項目,在混淆的世界里所有一切都是關於(錯位)的秘密。

混淆是什么?

代碼混淆意味着代碼保護。混淆修改后的代碼更難以被理解。例如它通常被用於DRM(數字版權保護)軟件中,通過隱藏算法和加密密鑰等機密信息來保護多媒體內容。

當任何人都可以獲取訪問你的代碼或二進制程序,但你不想讓別人了解程序的工作原理的時候,你就需要代碼混淆。但混淆的安全性基於代碼模糊程度,破解它也只是時間問題。因此混淆工具的安全性取決於攻擊者打破它所必須花費的時間。

使用的工具

測試例子

我們的測試目標是一個對輸入值進行簡單計算的單一函數。代碼包含4個條件分支,這些分支取決於輸入參數。測試程序在x86 32位架構下編譯:

 

unsigned int target_function(unsigned int n)  {   unsigned int mod = n % 4;   unsigned int result = 0;   if (mod == 0) result = (n | 0xBAAAD0BF) * (2 ^ n);   else if (mod == 1) result = (n & 0xBAAAD0BF) * (3 + n);   else if (mod == 2) result = (n ^ 0xBAAAD0BF) * (4 | n);   else result = (n + 0xBAAAD0BF) * (5 & n);   return result; }

 

下面是IDA的控制流程圖展示:

1.png 

我們可以看到使用布爾比較和算術指令計算的3個條件和4個分支路徑。我們這樣做的目的是為了充分測試OLLVM所有的混淆策略。

這個函數雖然很簡單,但它是研究反混淆最好的方式。因為我們的目標是為了研究OLLVM行為,而不是獲得一個百分百通用的反混淆工具。

Miasm框架

Miasm是一個Python開源逆向工程框架。最新版本獲取:https://github.com/cea-sec/miasm。正如我們前面所說的,雖然我們會展示一些代碼示例,但是本文並不是Miasm教程。雖然其他工具可以同樣用來做反混淆,但本文對於Miasm框架是一個很好的展示機會,Miasm框架更新比較活躍,可以用來做為強大的反混淆工具。

敬告!Miasm API可能在以后的提交更新中發生變化,所以請務必注意本文中提供的Miasm代碼示例僅適用於本文發布時的最新版本(commit a5397cee9bacc81224f786f9a62adf3de5c99c87)。

圖形展示

在我們開始分析混淆代碼之前,關鍵的一點是決定我們想要的反混淆輸出結果的展示形式。這並不簡單,因為反混淆工作可能需要耗費一些時間,並且我們想有一個簡單易懂的輸出。

我們可以將基本塊內容轉換為LLVM中間表示(IR),這樣可以重新編譯混淆后的代碼,並通過“優化傳遞”來清除無用代碼然后生成新的程序,但是這個方案比較耗時,不過可以作為未來的改進目標。所以我們選擇IDAPython構建流程圖(使用GraphViewer類)作為我們的反混淆輸出。這樣,我們可以輕松地構建出節點和路徑,然后使用Miasm的中間表示(IR)去填充代碼基本塊。

示例,下面的流程圖是我們通過腳本對前面的測試程序生成的:

2.png 

使結果輸出展示更容易理解還需要有一些努力改進,但對於本文來說足夠了。在上圖中,我們可以看到3個條件和4個路徑及其各自的運算。當然展示的圖形是無法去執行的,但它為分析者正確地理解函數含義保留了足夠的信息。而這就是反混淆的意義所在。

我們的腳本生成圖形比較簡陋,在基本塊中沒有顏色和美化。並且這不是純粹的Miasm IR代碼,閱讀起來比較困難。所以我們選擇將IR轉換為一些偽代碼(近似Python)。

所以當我們完成一些反混淆測試需要展示結果時,我們可以使用同樣的方法生成圖形輸出,將其與上面的原始截圖進行比較。

OLLVM反混淆

快速演示

在這里我們不會詳細解釋OLLVM的工作原理,因為這些在其項目網站(http://o-llvm.org)中已經被很好地闡釋說明。簡單來說,它由3種不同的保護方式組成:控制流平坦化,虛假控制流和指令替換,這些保護方式可以累加,對於靜態分析來說混淆后代碼非常復雜。在這一部分,我們將展示如何逐一去除每種保護,最后去除全部方式累加后的混淆保護。

控制流平坦化

關於“控制流平坦化”解釋如下: https://github.com/obfuscator-llvm/obfuscator/wiki/Control-Flow-Flattening

使用以下命令行編譯測試代碼:

../build/bin/clang -m32 target.c -o target_flat -mllvm -fla -mllvm -perFLA=100

 

這個編譯命令會對我們的測試程序所有函數開啟“控制流平坦化”保護,以便確保我們的測試具有針對性。

被混淆保護的函數

通過IDA查看被混淆目標函數的控制流圖如下:

3.png 

混淆代碼的行為很簡單。在“序言”中,狀態變量受數字常數的影響,而該數字常量通過“主分發器”(包括“子分發器”)指引到達目標基本“關聯塊”所需的路徑。“相關塊”就是沒有經過混淆的函數的原始塊。在每個基本“相關塊”的結尾,狀態變量受另一個數字常量的影響,再指示下一個“相關塊”,以此循環類推。

原始條件被轉換為CMOV條件傳送指令,然后根據比較結果,在狀態變量中設置下一個“相關塊”。

由於這種轉換傳遞在指令級別並沒有添加任何保護,所以混淆后代碼仍然保持了可讀性。只有控制流程圖被破壞打亂。我們現在的目標是恢復函數原始的控制流程圖。我們需要恢復所有可能的執行路徑,這意味着為了重建控制流程圖,我們需要知道所有基本“相關塊”之間的鏈接關系(父子節點)。

這里我們需要一個符號執行工具來遍歷代碼,並嘗試計算每個基本塊的目標終點。當出現判斷條件分支時,它將幫助我們嘗試運行並獲取所有分支可能到達的目標地址列表。Miasm框架包含有一個符號執行引擎(支持x86 32位等架構),其基於自身的“中間表示”(IR)實現,並且可以通過反匯編器轉換二進制代碼到“中間表示”(IR)。

下面是Miasm 腳本代碼,通過這個代碼我們可以在函數基本塊上進行符號執行來計算其目標地址:

 

 

 

# Imports from Miasm framework

from miasm2.core.bin_stream                 import bin_stream_str

from miasm2.arch.x86.disasm                 import dis_x86_32

from miasm2.arch.x86.ira                    import ir_a_x86_32

from miasm2.arch.x86.regs                   import all_regs_ids, all_regs_ids_init

from miasm2.ir.symbexec                     import symbexec

from miasm2.expression.simplifications      import expr_simp

# Binary path and offset of the target function 【程序路徑與目標函數代碼偏移】

offset = 0x3e0

fname = "../src/target"

# Get Miasm's binary stream [獲取文件二進制流]

bin_file = open(fname, "rb").read() # fix: (此處原文代碼BUG,未指定“rb”模式可能導致文件讀取錯誤)

bin_stream = bin_stream_str(bin_file)

# Disassemble blocks of the function at 'offset' 【反匯編目標函數基本塊】

mdis = dis_x86_32(bin_stream)

disasm = mdis.dis_multibloc(offset)

# Create target IR object and add all basic blocks to it 【創建IR對象並添加所有的基本塊】

ir = ir_a_x86_32(mdis.symbol_pool)

for bbl in disasm: ir.add_bloc(bbl)

# Init our symbols with all architecture known registers 【符號初始化】

symbols_init =  {}

for i, r in enumerate(all_regs_ids):

    symbols_init[r] = all_regs_ids_init[i]

# Create symbolic execution engine 【創建符號執行引擎】

symb = symbexec(ir, symbols_init)

# Get the block we want and emulate it 【獲取目標代碼塊並進行符號執行】

# We obtain the address of the next block to execute

block = ir.get_bloc(offset)

nxt_addr = symb.emulbloc(block)

# Run the Miasm's simplification engine on the next

# address to be sure to have the simplest expression 【精簡表達式】

simp_addr = expr_simp(nxt_addr)

# The simp_addr variable is an integer expression 

# (next basic block offset) 【如果simp_addr變量是整形表達式(下一個基本塊的偏移)】

if isinstance(simp_addr, ExprInt):

  print("Jump on next basic block: %s" % simp_addr)

# The simp_addr variable is a condition expression 【如果simp_addr變量為條件表達式】

elif isinstance(simp_addr, ExprCond):

  branch1 = simp_addr.src1

  branch2 = simp_addr.src2

  print("Condition: %s or %s" % (branch1,branch2))

 

上述代碼只是針對單個基本塊進行符號執行的示例。為了覆蓋目標函數代碼的所有基本塊,我們可以使用這段代碼從目標函數的序言開始跟蹤函數執行流程。如果遇到判斷條件則逐一探索每個流程分支,直到完全覆蓋目標函數。

當執行到函數返回位置后還需要繼續跟蹤下一個有效分支,我們就必須有一個分支棧來處理這些情況。我們需要保存每一個分支狀態,當需要后續處理的時候就可以用來恢復所有符號執行過程的上下文信息(例如寄存器)。

中間函數

通過應用之前的解決思路,我們可以重建中間控制流程圖。下面通過流程圖處理腳本進行展示:

4.png 

在這個中間函數流圖中,現在可以清楚看到所有有用的基本塊和分支條件。“主分發器”及“子分發器“對代碼執行有用,但對於我們恢復原始控制流程圖的目標來說是無用的。我們需要清除這些分發器代碼,只保留“相關塊。

為了實現這個目標,可以對OLLVM控制流平坦化后的函數固定“形狀”分析。實際上大多數“相關塊”(除了序言和返回基本塊)位於可以被檢測的明確位置。為了定位這些“相關塊”,我們需要從被保護的原函數開始,構建編寫一個具備通用性的算法:

  • 序言塊位於函數開始(相關塊)
  • (譯者補充:序言的后續子節點為主分發器),處於主分發器時獲取其父節點(非序言):預分發器
  • 標記預分發器的所有父節點為相關塊
  • 標記沒有子節點的單獨塊:返回塊

這個算法很容易實現,如前文所見,Miasm反匯編器可以為我們提供目標函數反匯編后的基本塊列表。當獲得相關的塊列表后,我們就能夠在符號執行時通過以下規則算法重建原控制流:

  • 定義一個父節點塊(開始處序言)的集合變量(只有“相關塊”才能加入)
  • 對於每個新塊,如果它在相關塊列表中,我們可以將它和父節點塊鏈接起來。並將此新塊設置為父節點。
  • 在每個條件分支下,每個路徑都有自己的父節點相關塊變量。
  • 其他等等。

為了說明這個算法,以下是示例代碼:

 

# Here we disassemble target function and collect relevants blocks

# Collapsed for clarity but nothing complicated here, and the algorithm is given above

relevants = get_relevants_blocks()

# Control flow dictionnary {parent: set(childs)} 【控制流字典變量 {父節點: 集合(子節點)}】

flow = {}

# Init flow dictionnary with empty sets of childs 【使用空集合初始化控制流變量】

for r in relevants: flow[r] = set()

# Start loop of symbolic execution 【開始符號執行循環】

while True:

    block_state = # Get next block state to emulate

    # Get current branch parameters 【獲取當前分支參數】

    # "parent_addr" is the parent block variable se seen earlier 

    # 【"parent_addr"即前面所說的父節點塊】

    # "symb" is the context (symbols) of the current branch 【"symb"即當前分支上下文】

    parent_addr, block_addr, symb = block_state

    # If it is a relevant block 【如果是相關塊】

    if block_addr in flow:

        # We avoid the prologue's parent, as it doesn't exist 【忽略序言父節點】

        if parent_addr != ExprInt32(prologue_parent):

            # Do the link between the block and its relevant parent 【關聯目標塊與其父節點塊】

            flow[parent_addr].add(block_addr)

        # Then we set the block as the new relevant parent 【將當前塊設置為新的父節點塊】

        parent_addr = block_addr

    # Finally, we can emulate the next block and so on.

 

 

恢復函數

通過使用上面的算法,可以生成如下控制流程圖:

5.png

 

如上,原始代碼被完整恢復。我們可以看到用於計算輸出結果的3個條件分支和4個計算表達式。

虛假控制流

關於“虛假控制流”的解釋:https://github.com/obfuscator-llvm/obfuscator/wiki/Bogus-Control-Flow

使用以下命令行編譯測試代碼:

../build/bin/clang-m32 target.c -o target_flat -mllvm -bcf -mllvm -boguscf-prob=100 -mllvm-boguscf-loop=1 

 

該編譯命令可以在我們的測試程序的函數上啟用“虛假控制流”保護。我們將“-boguscf-loop”參數(循環次數)設置為1,對反混淆處理沒有影響,只是代碼生成和恢復過程會比較慢,並且內存消耗更多。

被保護函數

當我們使用IDA Pro加載目標程序時,控制流圖如下:

6.png

屏幕分辨率在這里已變得不再重要,因為從上圖中我們足以看出混淆后的函數代碼非常復雜。“虛假控制流”保護會對每個基本塊進行混淆,創建一個包含“不透明謂詞”的新代碼塊,“不透明謂詞”會生成條件跳轉:可以跳轉到真正的基本塊或另一個包含垃圾指令的代碼塊。

我們可以同樣使用前文中的符號執行方法,找到所有有用基本塊並重建控制流。但還存在一個問題:“不透明謂詞”,如果包含垃圾代碼的基本塊返回它的父節點塊,這種情況下如果我們在符號執行過程中還按這個路徑去跟蹤,將導致陷入死循環。所以需要先解決“不透明謂詞”問題,以避免垃圾代碼塊,直接找到正確的執行路徑。

下面是“不透明謂詞”問題在OLLVM源碼中的圖形注釋:

 

// Before :

//                       entry

//                         |

//                   ______v______

//                  |   Original  |

//                  |_____________|

//                         |

//                         v

//                       return

//

// After :

//                       entry

//                         |

//                     ____v_____

//                    |condition*| (false)

//                    |__________|----+

//                   (true)|          |

//                         |          |

//                   ______v______    |

//              +-->|   Original* |   |

//              |   |_____________| (true)

//              |   (false)|    !-----------> return

//              |    ______v______    |

//              |   |   Altered   |<--!

//              |   |_____________|

//              |__________|

//

//  * The results of these terminator's branch's conditions are always true, but these predicates are

//    opacificated. For this, we declare two global values: x and y, and replace the FCMP_TRUE

//    predicate with (y < 10 || x * (x + 1) % 2 == 0) (this could be improved, as the global

//    values give a hint on where are the opaque predicates)

在進行符號執行時,我們需要簡化這個“不透明謂詞”:(y <10 || x *(x + 1)%2 == 0)。Miasm框架仍然可以幫助我們完成這個任務,因為框架包含了一個基於自身IR(中間表示)的表達式簡化引擎。我們還需加深對不透明謂詞的了解。因為這兩個表達式之間通過“或”連接,並且結果必須為真,所以簡化一個表達式足矣。 當下目標是使用Miasm框架進行模式匹配,並將表達式x *(x + 1)%2替換為0。這樣“不透明謂詞”的正確結果為真,這一點很容易解決。 看起來OLLVM項目的程序員在上面的代碼注釋中犯了一點錯誤:代碼注釋中說的“不透明謂詞”是錯的。最初使用Miasm框架的簡化引擎沒有匹配到目標表達式。通過查看Miasm框架代碼中給出的表達式,我們發現實際的“不透明謂詞”方程是:(x *(x + 1)%2)(減1而並非加1)。 這個問題可以通過查看OLLVM源碼來驗證:

 

*BogusControlFlow.cpp:620*

//if y < 10 || x*(x+1) % 2 == 0

opX = new LoadInst ((Value *)x, "", (*i));

opY = new LoadInst ((Value *)y, "", (*i));

op = BinaryOperator::Create(Instruction::Sub, (Value *)opX,

    ConstantInt::get(Type::getInt32Ty(M.getContext()), 1,

      false), "", (*i));

 

這段代碼展示了上述問題。代碼注釋中寫的(x + 1),但實際上代碼使用了sub指令,說明表達式應該是:(x-1)。因為最后做模2操作,所以對計算結果值影響不大,因為兩種表達式的結果是相同的,但對於使用模式匹配來說這個信息很關鍵。

因為我們確切地知道匹配目標,以下是使用Miasm模式匹配的代碼示例:

 

 

 

# Imports from Miasm framework

from miasm2.expression.expression           import *

from miasm2.expression.simplifications      import expr_simp

# We define our jokers to match expressions

jok1 = ExprId("jok1")

jok2 = ExprId("jok2")

# Our custom expression simplification callback  【表達式匹配回調函數】

# We are searching: (x * (x - 1) % 2)

def simp_opaque_bcf(e_s, e):

    # Trying to match (a * b) % 2 【嘗試匹配(a * b) % 2】

    to_match = ((jok1 * jok2)[0:32] & ExprInt32(1))

    result = MatchExpr(e,to_match,[jok1,jok2,jok3])

    if (result is False) or (result == {}):

        return e # Doesn't match. Return unmodified expression

    # Interesting candidate, try to be more precise 【進一步精准匹配 b == (a - 1)】

    # Verifies that b == (a - 1)

    mult_term1 = expr_simp(result[jok1][0:32])

    mult_term2 = expr_simp(result[jok2][0:32])

    if mult_term2 != (mult_term1 + ExprInt(uint32(-1))):

        return e # Doesn't match. Return unmodified expression

    # Matched the opaque predicate, return 0 【匹配到表達式后返回0】

    return ExprInt32(0)

# We add our custom callback to Miasm default simplification engine 

# 【添加自定義回調函數到Miasm默認簡化引擎中】

# The expr_simp object is an instance of ExpressionSimplifier class 

# 【expr_simp對象是ExpressionSimplifier類的實例】

simplifications = {ExprOp : [simp_opaque_bcf]}

expr_simp.enable_passes(simplifications)

 

這樣當每次我們調用方法:exprsimp(e) (“e”是一個Miasm IR 的lambda表達式),如果其中包含“不透明謂詞”就會被簡化。由於Miasm IR類有時調用exprsimp()方法,此回調函數在IR修改期間可能會被執行。

中間函數

現在我們需要使用與之前相同的符號執行算法,但無需處理相關塊。因為無用的塊不會被執行,它們將被自動清除。可以獲得以下控制流圖:

7.png

添加“不透明謂詞”識別到我們的原來的算法中后,Miasm框架會簡化這些表達式,最后得到上圖展示的結果。符號執行會找到所有的可達路徑,並忽略不可達路徑。可以看出函數流程圖的“形狀”是正確的,但是這個結果圖還比較難看,因為其中還包含了OLLVM添加的剩余指令和一些空的基本塊。

恢復函數

現在可以使用與以前相同的算法去清除殘留垃圾代碼(控制流平坦化)。雖然這個方法不是很優雅,但不得不這么做,因為我們無法使用編譯器優化這些。 最終我們獲得以下流程圖,它和原始流程圖已非常接近:

8.png 

混淆后的代碼被完好恢復。同樣可以看到3個條件分支和4個用於計算輸出值的計算表達式。

指令替換

關於“指令替換”的解釋:https://github.com/obfuscator-llvm/obfuscator/wiki/Instructions-Substitution 使用以下命令行編譯測試代碼:

 

../build/bin/clang -m32 target.c -o target_flat -mllvm -subv

 

上述編譯命令會在我們的測試程序的所有函數開啟“指令替換”保護。

保護函數

“指令替換”保護不會修改函數原始的控制流程,使用Miasm IR圖形展示中如下:

9.png 

正如期望的那樣,函數流程圖“形狀”沒有改變,仍可以看到相同的條件分支,但是在“相關塊”中,可以看到對輸入值的計算過程變得更繁雜。因為這個例子中代碼比較簡單,所以混淆復雜度看起來還能夠接受,但如果是比較龐大的函數源碼,這樣的混淆也足夠惡心。 OLLVM項目把普通算術和布爾運算替換為更復雜的操作。但由於“指令替換”只是一個等價轉換表達式列表,我們仍然可以通過使用Miasm模式匹配去解除這種保護。 從OLLVM項目官網上我們可以看到,根據運算符不同,有以下幾種指令被替換:+, – ,^,| ,& 以下是簡化OLLVM項目 XOR指令替換的代碼:

 

 

 

 

# Imports from Miasm framework

from miasm2.expression.expression           import *

from miasm2.expression.simplifications      import expr_simp

# We define our jokers to match expressions

jok1 = ExprId("jok1")

jok2 = ExprId("jok2")

jok3 = ExprId("jok3")

jok4 = ExprId("jok4")

# Our custom expression simplification callback 【表達式匹配簡化回調函數】

# We are searching: (~a & b) | (a & ~b) 【匹配表達式(~a & b) | (a & ~b)】

def simp_ollvm_XOR(e_s, e):

    # First we try to match (a & b) | (c & d)

    to_match = (jok1 & jok2) | (jok3 & jok4)

    result = MatchExpr(e,to_match,[jok1,jok2,jok3,jok4])

    if (result is False) or (result == {}):

        return e # Doesn't match. Return unmodified expression

    # Check that ~a == c

    if expr_simp(~result[jok1]) != result[jok3]:

        return e # Doesn't match. Return unmodified expression

    # Check that b == ~d

    if result[jok2] != expr_simp(~result[jok4]):

        return e # Doesn't match. Return unmodified expression

    # Expression matched. Return a ^ d  【匹配成功返回a ^ d】

    return expr_simp(result[jok1]^result[jok4])

# We add our custom callback to Miasm default simplification engine

# The expr_simp object is an instance of ExpressionSimplifier

simplifications = {ExprOp : [simp_ollvm_XOR]}

expr_simp.enable_passes(simplifications)

 

 

這個方法對於所有指令的替換恢復都是相同的。只需檢查Miasm框架是否正確匹配即可。但Miasm有時會出現一些匹配上問題,這可能會有點棘手或耗費時間,這時對公式匹配就需要特殊處理。但是當它完成…一切就搞定了。 另外在這個問題上我們有一個重要優勢,就是正在被分析的混淆器是開源的,通過查看源代碼就可以知道指令替換公式。如果是在閉源的混淆器中,我們就必須手動找到它們,這個過程可能非常耗時。

恢復函數

將所有OLLVM的替換公式添加到Miasm簡化引擎后,我們可以重新生成如下控制流圖:

10.png

可以看到在上面截圖中的基本塊比較小,指令混淆已經被清理替換。這樣我們得到了原始函數,這對分析者來說比較清晰易懂。

完整保護

逐個打破所有的保護是個可行的思路,通常來說,混淆器的保護力度是可以被疊加的,這使得未處理的混淆代碼很難被理解。另外在現實情況中,一般都會啟用最大保護級別去混淆模糊目標軟件代碼。所以能夠處理完整保護顯得非常重要。

使用以下編譯命令對測試目標函數啟用OLLVM完整保護:

 

../build/bin/clang -m32 target.c -o target_flat -mllvm -bcf -mllvm -boguscf-prob=100 -mllvm -boguscf-loop=1 -mllvm -sub -mllvm -fla -mllvm -perFLA=100

保護函數

通過IDA查看被保護的函數代碼,可以看到以下控制流圖:

11.png

可以看出很重要的一點,通過保護方式疊加似乎明顯地提高了代碼的混淆程度,因為“不透明謂詞”可以通過“指令替換”來轉換實現,而“控制流平坦化”又會被應用於“偽造控制流”保護后的代碼。從上面的截圖中可以看到更加龐雜的相關塊。

恢復函數

我們沒有對前文中描述的方法進行任何修改,直接運行我們的腳本后就可以完全恢復被混淆后的函數代碼,雖然上面混淆后的函數代碼看起來非常糟糕,但是可以說在保護級別上,疊加保護和之前的保護並沒有任何區別。

12.png

可以看到一些無用指令行並沒有被我們的啟發腳本完全清除,但這些可以被可忽略,因為我們已經恢復出原始函數的“形狀”、條件分支和運算公式。

附加

如上文所述,我們可以通過“重建控制流並使用易懂偽代碼填充”的方式清除控制流保護。在我們的下面測試例子中,我們將使用Miasm符號執行引擎展示另一種不同思路。

目標函數接受參數輸入並輸出值,結果取決於輸入。我們可以這么做,在符號執行期間,每次到達一個分支結尾(返回)時,輸出EAX(x86 32位架構中返回值寄存器)對應的表達式。對於此示例,如上文所見,我們已經激活了完整的OLLVM保護選項。

 

 

 

# .. Here we have to do basic blocks symbolic execution, as we seen earlier ..

# Jump to next basic block

if isinstance(simp_addr, ExprInt):

  # Useless code removed here...

# Condition

elif isinstance(simp_addr, ExprCond):

  # Useless code removed here...

# Next basic block address is in memory

# Ugly way: Our function only do that on return, by reading return value on the stack

elif isinstance(simp_addr, ExprMem):

    print("-"*30)

    print("Reached return ! Result equation is:")

    # Get the equation of EAX in symbols 【讀取EAX對應表達式】

    # "my_sanitize" is our function used to display Miasm IR "properly" 【格式化表達式】

    eq = str(my_sanitize(symb.symbols[ExprId("EAX",32)]))

    # Replace input argument memory deref by "x" to be more understandable 【替換參數為x更易讀】

    eq = eq.replace("@32[(ESP_init+0x4)]","x")

    print("Equation = %s" % eq)

 

通過符號執行運行上面的代碼,我們得到以下輸出:

 

starting symbolic execution...

------------------------------

Reached return ! Result equation is:

Equation = ((x^0x2) * (x|0xBAAAD0BF) & 0xffffffff)

------------------------------

Reached return ! Result equation is:

Equation = ((x&0xBAAAD0BF) * (x+0x3) & 0xffffffff)

------------------------------

Reached return ! Result equation is:

Equation = ((x^0xBAAAD0BF) * (x|0x4) & 0xffffffff)

------------------------------

Reached return ! Result equation is:

Equation = ((x&0x5) * (x+0xBAAAD0BF) & 0xffffffff)

------------------------------

symbolic execution is done.

 

非常好,這個表達式結果正是我們測試例子的源代碼中所寫的!

結論

雖然這些腳本對於反混淆是一個不錯的開端,但它不可能適用於所有被OLLVM保護的代碼,一方面在一些目標程序上總是存在特殊的情況,這些腳本還需要相應地進行改進以適應不同情況。而且它還依賴於Miasm框架,其本身還存在一些功能限制,需要實現或提交改進。

在例如“循環”這樣更復雜的函數上我們測試了這些腳本,它仍然效果良好。但也存在其無法處理的特殊情況,比如當輸入參數決定循環停止條件時。這種情況處理起來比較困難,因為我們必須處理已經遇到的分支,否則將很快導致符號執行陷入死循環。為了檢測這些情況,就需要通過分支狀態差異來推斷循環停止條件,以繼續正常地符號執行。

OLLVM是一個非常有趣並且有意義的項目,它實例展示了如何通過操作LLVM構建自己的混淆器,並支持多CPU架構。顯而易見,與閉源的商業保護方案相比,能夠查看源碼對於對抗保護非常有用。這也顯示了一個強大的混淆器所依賴的秘密:如何使用變換,變換依賴於什么,如何組合保護策略等。所以,真的非常感謝OLLVM團隊公開這些。

代碼混淆做起來是非常困難的,這與大多人的想法都不同。混淆不是禁止訪問代碼和數據,而是預見突破保護層所需的時間成本。

致謝

  • 感謝OLLVM作者的優秀項目
  • 感謝Fabrice Desclaux創建優秀的Miasm框架
  • 感謝Camille Mougey對Miasm的貢獻和幫助
  • 感謝Ninon Eyrolles的幫助和指正

附件-測試程序下載鏈接:https://share.weiyun.com/c38128f2b56d60fbfac6d6144f52fa8a


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM