2020年4月24日 星期五

淺談DNGuard解殼演算法

DNGuard是一款針對.Net Framework程式的商用加殼軟體,許多惡意程式會使用它來保護程式,以免被別人反編譯,如果想要知道程式是如何運作,我們需要了解DNGuard是如何在運行時解殼,這樣我們也可以學習還原本來的程式碼,不過這篇還是聚焦在它的解密演算法,因為要還原本來的程式碼還需要更進一步了解.Net Framework編譯的原理和.Net執行檔的架構,所以我們就不探討這部分。

DNGuard它會將原本的執行檔,重新打包成另一個新的執行檔,主要會修改程式進入點(EntryPoint)並且加密以及混淆原本的主程式,然後運行時會解密主程式再利用.Net Framework的編譯函式來編譯(Compile)程式碼,所以我們可以透過編譯函式來回推解密程式碼在哪裡,不過DNGuard有導入Themida商用殼來混淆解密相關的程式碼,因此會耗費大量的時間來找尋解密程式碼,不過我們可以透過記憶體分析,來解決此問題。

一、.Net編譯函式

這邊的.Net Framework編譯函式是C#,可能與CLR C++、VB .Net有所不同,不過分析方法都一樣。

編譯函式call 6C73568B總共有6個參數,主要第二個參數(push eax)的第8項位移([arg2+0x8])是需要被編譯的程式碼,第0xC([arg2+0xC])項位移是它的程式碼長度;第三個參數(push edx)是回傳編譯完程式碼的位址;第四個參數(push [ebp+0x1C])是回傳編譯完程式碼長度的位址。



二、尋找DNGuard利用.Net編譯函式的位置

DNGuard不管是解密還是使用.Net編譯函式,都是在HVMRuntm.dll裡面運作,所以尋找解密函式都是分析此DLL。

之前說過DNGuard有使用Themida混淆,不過我們可以透過記憶體分析來接近DNGuard利用.Net編譯函式的位置,雖然程式碼的運作會看起來很零散,但是還是能了解它是如何運作。

push 0是.Net編譯函式的第6個參數;push ecx是第5個參數;push edx是第4個參數。



push ecx是第3個參數;push esi是第2個參數。



push edx是第1個參數。



call eax,其中eax就是.Net編譯函式的位址。



三、尋找解密的程式碼

我們已經知道它會在哪裡來使用.Net編譯函式,因此可以輕易的推敲解密程式碼的位置。

這裡是解密的演算法,其中cmp ecx, eax是判斷是否完成解密,[edx+edi]是解密的Key(總共16 Bytes,因為會做and edx, F),[ecx+esi]是要被解密的密文,解密完後,會將明文寫入[ecx+esi],它的演算法非常簡單,密文和Key做xor(互斥或運算),就能解出明文,不過真正的程式碼還會在這裡解密第二次,因此它還會去解密編譯相關資訊和程式碼的長度,然後再進來此程序,再運作一遍才是真正的主程式碼資訊。



在載入HVMRuntm.dll時,會透過DLLMain來初始化16 Bytes的Key。



四、分析DNGuard解密的Header資訊

我們只知道解密的演算法在哪裡還是不夠,還要知道加密的內容在執行檔的偏移位置,因此我們要知道它的解密Header在哪裡,Header裡面有存放密文的起始偏移和密文長度。

這邊會將解密Header移動到新的記憶體空間,其中[eax+ebx]的ebx是Header的起始位址,然後將Header所有數值存入[eax+esi](eax是Counter,esi是新的記憶體空間位址),另外詳細的偏移如下:

1. [ebx+0x4]是被加密過的起始加密偏移位置的Key1。

2. [ebx+0x20]是編譯相關的資訊和解密後的程式碼長度的密文長度。

3. [ebx+0x1C]是DNGuard驗證相關偏移的長度。

4. [ebx+0x48]是被加密過的起始加密偏移位置。

5. [ebx+0x4C]是程式碼密文的長度。



五、獲取起始加密的偏移位址

因為[ebx+0x48]有被加密,所以需要經過解密,才能獲取起始加密的偏移位置,這邊需要補充一下,它主要是透過2種Key來完成解密,其中Key1是步驟四的[ebx+0x4],Key2在試用版的DNGuard是使用GetVolumeInformation來獲取硬碟的lpVolumeSerialNumber,但是惡意程式不可能會使用試用版,所以這邊還需要留意一下。

這邊的[esi]的數值為(not Key2) or (not (not Key1 or [ebx+0x48]) or (Key1 or not [ebx+0x48])),[esi+4]的數值為Key2 or not (not (not Key1 or [ebx+0x48]) or (Key1 or not [ebx+0x48])),[esi]和[esi+4]會個別存到ecx和eax,然後再做not,最後ecx和eax再做or,結果值為ecx,ecx就是起始加密的偏移數值(執行檔起始位址+ecx就是起始加密的位置)。



六、獲取DNGuard驗證相關偏移的長度

我們從第五步驟獲取起始加密的偏移數值ecx,可以透過執行檔起始位址+第五步驟的ecx定位到要開始解密的位置,不過要被解密的起始位置是DNGuard驗證相關偏移數值的密文,所以我們需要獲取它的長度,再來取得主程式的密文位置。

接下來我們要獲取它的長度,要先透過簡易的運算,因此這邊[edi]的數值為[ebx+0x1C] + [ebx+0x1C](步驟四),[edi+4]的數值會等於[edi],然後[edi]和[edi+4]會個別存到edx和eax,最後再相加到edx,edx就是DNGuard驗證相關偏移的長度。



在獲取它的長度之後,它會透過第三步驟解密,並且獲取它的偏移值,再透過執行檔起始位址+偏移數值edx,就是DNGuard驗證相關的資訊,如果這些資訊被修改,程式會跳出錯誤的訊息,然後結束程式。

七、獲取編譯訊息和主程式的長度

我們已經知道程式的密文位於執行檔的哪裡,將執行檔起始位址+第五步驟的ecx+第六步驟的edx,全部加起來就是主程式的密文起始位置,那麼密文長度為第四步驟的[ebx+0x4C],接下來可以透過第三步驟來解密。

之前在第三步驟有說明,它會在第三步驟解密2次後,才是主程式的明文,不過在解密第二次之前,會獲取主程式的長度,然後才運行第二次解密。

這邊會獲取編譯訊息和主程式長度的密文,其中[esi+ebx]的ebx是執行檔起始位址+第五步驟ecx+第六步驟的edx+第四步驟的[ebx+0x4C],總長度為第四步驟的[ebx+0x20],這裡補充一下它的前1 Bytes是此密文的長度,edi是此密文的第4 Bytes,[esi+ebx]的esi是數值5,所以它是從第5 Bytes開始運算,[eax]和[eax+4]的eax是新的記憶體空間位址,每次循環esi會加5,eax會加0x1C,edi會減1,直到edi等於0。



上一步循環結束後,call 10010F60會建置類似二元樹的資訊來準備獲取密文資訊,[esp+0x10]就是上一步的eax的記憶體位址。



建置完類似二元樹之後,它會獲取裡面的資料,其中[edx+14]( [ecx+14])和[ecx+18]有點類似左子樹和右子樹的取值,取完值會存到[edi+eax](edi是Counter,eax是新的記憶體空間位址),直到edi大於密文的長度[ebp],這樣就順利完成獲取密文資訊,然後再利用第三步驟來解密。



從第三步驟解密後,[eax]是解密後的資訊,將它存取到edi,edi再右移8 Bytes來獲取主程式的長度。



在解密完後的[eax],除了可以獲取主程式的長度,此內容還有編譯相關的訊息,因此接下來會先運行第三步驟的第二次解密主程式碼,然後再調用.Net Framework的編譯函式,最後才會執行原本的主程式。

八、結論

DNGuard的運行依序如下:

1. 獲取解密的Header資訊

2. 解密起始密文的偏移位置

3. 獲取驗證相關偏移的長度

4. 獲取主程式碼的密文

5. 第一次解密主程式碼

6. 獲取編譯相關資訊和主程式長度的密文

7. 解密編譯相關資訊和主程式的長度

8. 第二次解密主程式碼

9. 調用.Net Framework編譯主程式

從這些步驟看起來相當複雜,不過比較麻煩的是第七步驟的演算法,這裡可以直接調用它的函式來建置(類似二元樹的函式),不需要將此函式完全分析。

惡意程式一定會使用專業版或企業版的DNGuard,因此程式的字串可能會被加密,不過透過這些分析方式,我相信一樣可以迎刃而解的找出解密演算法。