Jump to content

現代軟件經常將混淆技術作為其反篡改策略的一部分,以防止黑客逆向分析軟件的關鍵組件。他們經常使用多種混淆技術來抵禦黑客的攻擊,這有點像滾雪球:隨著雪層的增多,軟件規模也隨之變大,使其逆向分析難度隨之提高。

在這篇文章中,我們將仔細研究兩種常見的混淆技術,以了解它們是如何工作的,並弄清楚如何去混淆。

概述這裡,我們將研究以下混淆技術:

基於IAT導入表的混淆技術

基於控制流的混淆技術

基於IAT導入表的混淆技術在深入介紹基於IAT導入表的混淆方法之前,先讓我解釋一下導入表到底是什麼。

什麼是導入函數?當進行逆向分析時,需要弄清楚的第一件事,就是它如何調用操作系統的函數。在我們的例子中,我們將重點關注Windows 10系統,因為大多數視頻遊戲只能在Windows系統上運行。無論如何,對於那些還不知道的人來說,Windows提供了一系列重要的動態鏈接庫(DLL)文件,幾乎每個Windows可執行文件都會用到這些庫文件。這些DLL文件中保存了許多函數,可以供Windows可執行文件“導入”,使其可以加載和執行給定DLL中的函數。

1.png

它們為何如此重要?例如,Ntdll.dll庫負責幾乎所有與內存有關的功能,如打開一個進程的句柄(NtOpenProcess),分配一個內存頁(NtVirtualAlloc,NtVirtualAllocEx),查詢內存頁(NtVirtualQuery,NtVirtualQueryEx),等等。

另一個重要的DLL庫是ws2_32.dll,它通過以下函數處理各種網絡活動:

Socket

Connect/WSAConnect

Send/WSASend

SendTo/WSASendTo

Recv/WSARecv

RecvFrom/WSARecvFrom

現在讀者可能會問,知道這些有什麼意義呢?好吧,如果您把一個二進製文件扔到像IDA這樣的反彙編器中(我通常會做的第一件事),就是檢查所有導入的函數,以便對二進製文件的功能有一個大致的了解。例如,當ws2_32.dll存在於導入表中時,表明該二進製文件可能會連接到Internet。

現在,我們可能想要進行更深入的研究,並考察使用了哪些ws2_32.dll函數。如果我們使用Socket函數並找出它的調用位置,我們就可以檢查它的參數,這樣,我們就可以通過搜索引擎查找相應的函數名,從而輕鬆地找出它所使用的協議和類型。

1.png

注意:IDA已自動向反彙編代碼中添加了註釋。

經過混淆處理的導入表無論如何,這些Windows函數能提供相當多的信息,因為它們是有據可查的函數。因此,攻擊者希望能夠把這些函數藏起來,以掩蓋正在發生的事情。

我們在反彙編器中看到的所有這些導入函數都是從導入地址表(IAT)加載的,該表在可執行文件的PE頭文件中的某個地方被引用。一些惡意軟件/遊戲通常試圖通過不直接指向DLL函數來隱藏這些導入地址。相反,他們可能會使用一個蹦床或迂迴函數。

考察我們的示例在這個例子中,我們使用的是一種蹦床式混淆技術,具體如下所示:

1.png

下面的地址0x7FF7D7F9B000引用了我們的函數0x19AA1040FE1,儘管看起來完全不是這麼回事。您可能認為這是垃圾代碼,但仔細看看,您會發現並非如此。

請仔細查看前兩個指令:前面的指令是mov rax, FFFF8000056C10A1,後面的指令是jmp 19AA1040738,後面的都是垃圾指令。不管怎樣,讓我們跟隨跳轉指令,看看它會跳到哪裡:

1.png

看,又是4個有效的指令,這次是一個異或指令和兩個加法指令,然後是另一個跳轉指令。讓我們把這個過程再重複幾遍.

1.png

1.png

最後,我們來到jmp rax指令!需要注意的是,所有的XOR、SUB和ADD指令都是在Rax寄存器上執行的,這意味著它可能包含導入函數的實際指針。下面,讓我們算算看。

1.png

實際上,在經過數學運算之後,我們得到了指向advapi32.regopenkeyexa的指針!

1.png

現在,我們所要做的就是重複幾百次運算,從而徹底消除針對IAT導入表的混淆處理。

基於IAT的自動去混淆處理我想,沒有人喜歡用計算器手工重複上述過程,做一次已經很煩了。從現在開始,我們將使用C#實現自動計算。正如您可能已經看到的,我們只需要處理在同一個寄存器上執行的ADD、SUB和XOR操作。原因是Rax被用作返回地址,而諸如Rcx、Rdx、R8、R9和其他寄存器對於被調用方來說是不安全的,並且可能與調用約定衝突。這意味著,我們甚至不需要使用反彙編器,因為我們可以很輕鬆地區分這些指令,這要歸功於涉及的寄存器和操作碼寥寥無幾。

到此為止,我們已經詳細解釋了混淆處理技術。接下來,大家不妨以Unsnowman項目中的importfix.cs為例,來了解與去混淆處理相關的代碼。

基於控制流的混淆技術在逆向分析二進製文件時,另一個有價值的信息來源是彙編指令本身。對於人類來說,它們可能難以理解,但對於像IDA這樣的反編譯器來說,我們只需按下F5鍵,IDA就會生成我們人類可以理解的偽代碼。

混淆實際指令的一個簡單方法,是組合使用垃圾代碼和不透明分支(即該分支條件總是為不成立,也就是說,該分支用於也不會被執行)。這意味著:把垃圾代碼放在一個分支指令之後。訣竅在於,我們可以使用條件轉移,但是,要確保條件永遠為真,這樣分支就會一直被執行。反彙編器不知道的是,條件跳轉在運行時總是為真,這使得它相信條件跳轉的兩個分支都可以在運行時到達。

好吧,如果還不太明白的話,可以藉助下面的截圖來加深理解。第一張截圖顯示的是落到另一條指令中的jbe。

1.png

注意:用紅色標記的字節是垃圾代碼。

現在仔細看看下面的第二張圖片,我在這裡所做的只是NOP最後一條指令的兩個字節,以便讓IDA顯示隱藏在and [rdx+24448B48h], bh指令後面的指令。

1.png

我們也可以用無條件跳轉來修補條件跳轉,以確保IDA不會再次上當。

在我們繼續之前,我想展示最後一個例子,因為前面的例子太簡單了。當我們將這些實現混淆處理的跳轉鏈接起來時,事情就變得複雜起來,具體如下圖所示。

1.png

然而,這張圖只顯示了它在控制流方面造成的混亂,但想像一下,當IDA竭盡全力根據垃圾指令創建這張圖時,我的CPU是多麼的痛苦。

現在,您可能想知道去混淆後的函數到底是什麼樣子的,別急,請看下圖!

1.png

看到我在左邊畫的那個藍色小箭頭了嗎?右邊顯示的就是這部分內容的放大版本。現在看一下右邊,在函數的一小部分中就有七個去混淆的跳轉。想像一下,以手動或半自動方式去混淆得需要多少時間。實際上,就算用IDA腳本手工完成這個過程,也花了我40分鐘……這還只是處理了一個函數。設想一下,為了找到真正要找的東西,還得需要處理多少其他的函數呢?

基於控制流的自動去混淆技術好了,現在我們已經考察了基於控制流的去混淆原理,接下來,我們將對這個過程實現自動化。正如我之前提到的,我們曾經用IDA腳本來修補無條件跳轉指令,並將垃圾指令替換為NOP指令。

然而,這個去混淆過程還是花了我40分鐘,因為識別不透明的分支非常費勁。那麼,我們該如何解決這個問題呢?大家可能認為應該檢查每一個條件跳轉指令,並檢查它是否是不透明的,如果是的話,就用NOP替換它,然後重複上述過程,對吧?錯了!

讓我告訴你一個秘密,我們並不關心什麼是不透明的,或諸如此類的事情。我真正關心的是,當我按下F5鍵時,IDA能否返回反編譯好的代碼——只要這些經過混淆的跳轉指令導致垃圾指令與實際的彙編指令發生衝突,這種情況就不會發生。

但這是否意味著我們需要弄清楚一個條件跳轉是否是不透明的呢?不,我們只需檢查跳轉操作是否與現有的指令相衝突,如果是的話,就對這個指令進行相應的修改,就像我們第一個例子中看到的那樣。

DeFlow去混淆算法現在,我們知道瞭如何解決這個問題,下面,我們開始深入研究本人想出的算法,以便對用這種混淆技術處理的內容進行去混淆。

List

//Bufferisacopyofthe.textsection

functionDeflow(byte[]buffer,ulong[]functions)

for(inti=0;ifunctions.Length;i++)

do

intnewDiscovered=0;

List

while(chunks.Count!=0)

List

foreach(varcinchunks)

newChunks.AddRange(DeflowChunk(buffer,c));

newDiscovered+=chunks.Count;

chunks=newChunks;

while(newDiscovered!=0)

functionDeflowChunk(address)

List

//63thbitindicatesifthisaddresswasextractedfromanegativejumpornot

boolisNegative=address63==1;

address=163;

//Checkifalreadydiscovered

if(_alreadyDiscovered.Contains(address))

returnnewChunks;

_alreadyDiscovered.Add(address);

ulonglastBranch=0;//Indicatesourlastconditionaljumpaddress

ulonglastBranchSize=0;//Sizeofthelastconditionaljumpaddress

ulonglastTarget=0;//Targetlocationofthelastconditionaljump

intstepsLeft=0;//Steps(bytes)lefttoreachlastTargetfromcurrentaddress

//UsageofSharpDisasm

vardisasm=newDisassembler(buffer,address-base);//NOTE:base=BaseAddress+.textoffset

foreach(varinsnindisasm.Disassemble())

ulongtarget=0;

ulonglastAddrStart

boolisJmp=true;

switch(insn.Mnemonic)

//StopanalysingwhenweencounterainvalidorreturninstructionwhilewehavenolastTarget

caseud_mnemonic_code.Invalid:

caseud_mnemonic_code.Ret:

if(lastTarget==0)

returnnewChunks;//OnlyacceptwhennolastTargetaswemaybelookingatjunkcode

break;

caseud_mnemonic_code.ConditionalJump://allconditionaljumps

if(lastTarget==0)

target=calcTargetJump(insn);//Helpertoextractjumplocationfrominstruction

if(!isInRange(target))//HelpertoseeiftargetaddressislocatedinourBuffer

isJmp=false;

break;

//Checkifinstructionisbiggerthen2,ifsoitwontbeobfuscatedbutwe

//dowanttoanalysethetargetlocation

if(insn.Length2)

isJmp=false;

newChunks.Add(target);

break;

else

isJmp=false;//Donotthisconditionaljumpacceptwhilewealready

//haveatarget(mightbelookingatjunkcode)

break;

caseud_mnemonic_code.UnconditionalJump:

caseud_mnemonic_code.Call:

if(lastTarget==0)

ulongnewAddress=calcTargetJump(insn);//Helpertoextractjumplocationfrominstruction

if(!isInRange(newAddress))

isJmp=false;

break;

//AddtargetandnextinstructionIFnotJMP(CALLdoesreturn,JMPnot)

if(insn.Mnemonic==ud_mnemonic_code.Call)

newChunks.Add(address+insn.PC);

//Addinstructiontargetforfurtheranalyses

newChunks.Add(newAddress);

returnnewChunks;

break;

//quickmafs

ulonglocation=(address+insn.Offset);

stepsLeft=(int)(lastTarget-location);//OnlyvalidifwehavealastTarget!

//SetupanewtargetifcurrentinstructionisconditionaljumpwhilethereisnolastTarget

if(lastTarget==0isJmp)

lastBranch=loction;

lastBranchSize=insn.Length;

lastTarget=target;

elseif(stepsLeft=0lastTarget!=0)

//ifstepsLeftisn'tzerothenourlastTargetislocatedslighltaboveus,

//meaningthatwearepartlylocatedinsidethepreviousinstructionandthuswearehidden(obfuscated)

if(stepsLeft!=0)

intcount=lastTarget=lastBranch;//calculatehowmuchbytesweareinthenextinstruction

if(count0)

//makingsureweareapositivejump

intbufferOffset=lastBranch-base;//subtractbasefromoutaddresssowecanwritetoourlocalbuffer

//NOPslideeverythingexceptourowninstruction

if(inti=0;icount-lastBranchSize;i++)

buffer[bufferOffset+lastBranchSize+i]=isNegative?0x90:0xCC;//WeuseNOPfornegativejumps

//andint3forpositive

if(!isNegative)

buffer[bufferOffset]=0xEB;//ForceunconditionalJump

//addnextinstructionforanalysesandexitcurrentanalysis

newChunks.Add(lastTarget);

returnnewChunks;

else

//weareanegativejump,set63thbittoindicatenegativejump

lastTarget=|=163;

//addtargetto

0 Comments

Recommended Comments

There are no comments to display.

Guest
Add a comment...