吾愛破解 - LCG - LSG |安卓破解|病毒分析|破解軟件|www.mlqcje.live

 找回密碼
 注冊[Register]

QQ登錄

只需一步,快速開始

搜索
查看: 7809|回復: 55
上一主題 下一主題

[.NET逆向] Eazfuscator.NET虛擬機殼還原方法

  [復制鏈接]
跳轉到指定樓層
樓主
JemmyloveJenny 發表于 2019-5-18 01:27 回帖獎勵
本帖最后由 JemmyloveJenny 于 2019-5-18 19:25 編輯

前言

廢話

我開始分析Eazfuscator的原因真的是好奇葩……
Eazfuscator是一個.Net程序的保護殼,而且據 @Kido  和 @wwh1004  大神說,這個殼還算是比較強的一個……
說來真的很慚愧,我很久之前就在電腦里裝了Eazfuscator.NET 2018.1破解版,但是我并不知道Eazfuscator是一個強殼,一直都只用了它默認的模式(好像是有字符串加密和名稱混淆),沒發現它的特別之處。
五一的時候,手賤把VisualStudio升級到了2019版本,Eazfuscator也必須要升級到2019.1。沒有Eazfuscator的激活碼,于是便想到了春節的時候Kido大大發布的Eazfuscator的Keygen(點擊此處),看了帖子才知道Eazfuscator還有IL級虛擬化保護!
然鵝那時候我用戶組還不夠,無權下載,便連夜發了一篇精華掙積分。積分夠了之后,總算把Keygen下載下來了……但是,Keygen生成的注冊碼對2019.1版本居然還無效!!!真的是心累……我猜想可能是注冊碼生成時候的參數需要改變,就想著把Kido大大的Keygen修改一下吧!然而Keygen還被Eazfuscator的IL虛擬化保護著……
因此我就從激活Eazfuscator開始,走上了還原Eazfuscator保護的不歸路……

廢話好長…不過和正文比起來了,這就不算長了[笑哭]

附件下載

Attachment.zip (115.78 KB, 下載次數: 64)


附件里面有我本篇文章分析的UnpackMe1,分為兩個版本:
UnpackMe1.clear.exe是未經混淆的,可以用來看原始IL代碼
UnpackMe1.obfs.exe是受Eazfuscator.NET虛擬化保護的,也是本文主要的分析對象

另外一個BreakPoints.xml,是動態調試用到的斷點,可以直接導入dnSpy-x86,(工具欄-調試-窗口-斷點 Ctrl+Alt+B)
斷點調試法的斷點已經默認啟用,IL反推法的斷點需要手動設為啟用

最后還有各個方法的IL代碼,以及Eazfuscator的虛擬IL代碼文本文件,對照著看幫助理解!

分析工具

對象:上面下載的UnpackMe
軟件:只要dnSpy就夠了!也可以再多一個ILSpy開與dnSpy對照著看。(或者用Notepad++打開附件里的il)
其他:還有就是要對.Net平臺的IL中間語言有所了解,不然還原了虛擬機保護也沒有用的

更新說明

目前為止文章里寫出的研究結果已經足夠分析Eazfuscator.NET的虛擬化IL保護了。
<font color=red>如果以后有新的研究結果會在文章中更新修改。</font>

Eazfuscator.NET保護介紹

我之前沒發現IL級虛擬化保護,就是因為沒看過Eazfuscator的文檔。我現在已經把文檔很不仔細地讀過兩遍了,接下來介紹一下Eazfuscator主要用到的幾種保護措施。如果說錯了別噴我ORZ……
以下的例子都可以在UnpackMe里找到,大家可以dnSpy開混淆過的UnpackMe,ILSpy開沒混淆的UnpackMe對照起來看。

名稱混淆

這是.Net保護最基本的操作了……代碼中所有的類名、方法名、變量名混淆成沒有意義的字符串,讓人不能直接看出代碼的作用。這個就不舉例子了

數據(資源)加密

對于字符串、數字、數組等等數據進行加密,在運行過程中還原。例如

// UnpackMe1.MainForm
private void BtnStringEncryption_Click(object sender, EventArgs e)
{
        MessageBox.Show("The strings in this method is encrypted!", "String Encryption", MessageBoxButtons.OK, MessageBoxIcon.None);
}

加密后變為

// UnpackMe1.MainForm
private void BtnStringEncryption_Click(object sender, EventArgs e)
{
        MessageBox.Show(\u0003\u2004\u2001.\u0002(2084363179), \u0003\u2004\u2001.\u0002(2084362362), MessageBoxButtons.OK, MessageBoxIcon.None);
        if (6 == 0)
        {
        }
}

IL虛擬化保護

此保護就是本文的分析重點了。這種保護又有點類似于IL代碼的解釋執行,本質上還是IL。放段代碼讓大家感受一下:
原本的代碼是這樣,很簡單的一個調用

// UnpackMe1.MainForm
private void BtnCallVirtualization_Click(object sender, EventArgs e)
{
        MessageBox.Show("Leave CallVirtualization");
        BtnVirtualization_Click(sender, e);
        MessageBox.Show("Back to CallVirtualization");
}

虛擬化之后

// UnpackMe1.MainForm
private void BtnCallVirtualization_Click(object sender, EventArgs e)
{
        object[] array = new object[3];
        object[] array2;
        if (-1 != 0)
        {
                array2 = array;
        }
        array2[0] = this;
        array2[1] = sender;
        array2[2] = e;
        \u0002\u2007 u0002_u = \u0006\u2004\u2001.\u0002\u2005\u2001();
        Stream u = \u0006\u2004\u2001.\u0003\u2005\u2001();
        string u2 = "@]iC5AnPXc";
        object[] u3 = array2;
        if (!false)
        {
                u0002_u.\u0002(u, u2, u3);
        }
}

呵呵,這保護完的代碼親媽都不認識……

代碼亂序

Kido大大在他的帖子里說Eazfuscator的激活算法被亂序了,而沒有被虛擬化。Eazfuscator的文檔里也說有"Code Control Flow Obfuscation",但是我覺得,亂不亂序好像沒有區別欸……我并沒有感覺到代碼亂序的存在和作用。
一定要說有亂序的話,那只能認為,動態調試的分析方法直接無視了亂序吧……

Eazfuscator.NET還原思路

Eazfuscator的保護沒有傳說中那么強,但是保護力度比一般的殼是要強一些的。
我覺得不太可能完全還原為最初的IL,也不容易修改代碼流程(比如直接return true這種)
不管怎么說,Eazfuscator也是一個虛擬機殼啊,不要幻想著把整個程序完全能脫出來,我們只要把關鍵部分的的代碼流程分析明白就夠了。

還原的思路我說不太清楚,很多方法都是靈光一現突然想到。
大體思路我只能提出來一兩點,實在不能理解的就回帖問我吧!

動態調試優于靜態分析

IL的和PE的虛擬機殼的效果都是相近的,對于靜態分析干擾非常大,對于動態調試影響較小。

我之前嘗試過靜態分析(直接看dnSpy中的代碼),查看每一次函數調用并且分析作用。那叫一個惡心……代碼里面用了不少接口、抽象類、委托這種東西,就是說,看到調用的函數只有一個聲明,靜態分析很難看到真正的函數體,根本就無從下手。
光是字符串等等的數據解密都能把人繞蒙掉。

用了動態調試之后,感受就好多了(雖然還是很惡心)……有些時候,看一下方法的輸入和輸出,就能大概了解到它的作用,不必再一條條看代碼了。而且加密的字符串在運行時會自動解密,我們直接下斷點截下來即可!

而且在我們最開始沒有思路的時候,直接dnSpy單步執行,看一看局部變量的變化也能對我們分析有所啟發和幫助。

逆向思維找到關鍵下斷點

我們一起設想一下,假如我就是Eazfuscator虛擬機殼的作者,那么我會怎樣運行虛擬化的代碼呢?
我隨便寫一段代碼

public bool IsActivated(string serial) {
        byte[] EncryptedData = Convert.FromBase64String(serial);
        byte[] DecryptedData = dESCryptoServiceProvider.CreateDecryptor().TransformFinalBlock(EncryptedData, 0, EncryptedData.Length);
        return Encoding.ASCII.GetString(DecryptedData) == "AAAAA";
}

以上這段代碼里存在不少的方法調用,比如FromBase64String, TransformFinalBlock, GetString
這些存在于.Net Framework之中的方法應該是不會被虛擬化的,所以虛擬化的代碼一定會調用這些方法。
那么用dnSpy的分析功能,分析這些方法被的調用的地方
我知道我的UnpackMe里用到了DESCryptoServiceProvider,那我們當做白盒測試,分析一下看看吧!
把UnpackMe1.exe拖入dnSpy-x86(非x86的那個不能調試),在mscorlib.dll找到System.Security.Cryptography命名空間,對其中的DESCryptoServiceProvider分析,

emmm?居然沒有被使用?!是dnSpy出問題了嗎?難道有辦法調用函數而不被分析器察覺到嗎?
當然是有的,別忘了.Net有一個很神奇的功能——Reflection
不太了了解Reflection的童鞋點這里
如果是用了Reflection的話,Reflection一定能被分析到,因為就算Reflection被虛擬化,還是Reflection。
那我們重新試一次,選擇Reflection的MethodInfo吧

分析器里面那些\u000x\u000x名稱的方法就是和虛擬化調用有關的方法了。

虛擬IL還原 - 基本方法 (一般夠用了)

接下來我介紹一下我最初分析IL虛擬機時的方法。自認為這個方法比較簡單,而且有效。如果各位高手有更好的想法,希望能提出來教教我欸……

附件中有兩個UnpackMe, UnpackMe1.obfs.exe是我們要分析還原的虛擬化代碼,這個UnpackMe很純凈,我把名稱混淆關了,用的是比較純粹的虛擬保護。另外一個UnpackMe1.clear.exe是直接編譯后沒有混淆的原文件。
我建議是用dnSpy-x86打開UnpackMe1.obfs.exe顯示C#代碼來動態調試;旁邊在用ILSpy打開UnpackMe1.clear.exe顯示IL代碼(或者用Notepad++打開附件中我導出的IL代碼),兩邊對照起來看有助于我們理解虛擬機的流程。

UnpackMe有四個按鈕:
StringEncryption:解密字符串并彈窗而已,就是測試下斷點用的。
VirtualizedForm:彈出一個被虛擬化的窗口,讓大家分析窗口字符串的產生流程。
Virtualization:在上方文本框輸入注冊碼,點擊按鈕驗證注冊碼是否正確,驗證代碼被虛擬化保護。
CallVirtualization:用虛擬化代碼調用Virtualization的handler,分析調用堆棧用。

動態調試初試 (高手直接跳過吧)

初試就是嘗試一下下斷點、查看局部變量,就拿最簡單的字符串解密開刀了……
dnSpy里打開 UnpackMe1.MainForm.BtnStringEncryption_Click(object,EventArgs) 方法

private void BtnStringEncryption_Click(object sender, EventArgs e)
{
        MessageBox.Show(\u0003\u2004\u2001.\u0002(2084363179), \u0003\u2004\u2001.\u0002(2084362362), MessageBoxButtons.OK, MessageBoxIcon.None);
        if (6 == 0)
        {
        }
}

可以看到,Eazfuscator是調用了\u0003\u2004\u2001.\u0002(int)來獲得解密的字符串
所以直接對\u0003\u2004\u2001.\u0002(int)的return下斷點就可以截獲字符串了。(128行)
下斷點的方法和VisualStudio里一樣的,點擊代碼行號左邊就行了……
然后點上方的啟動,程序就會被斷下來好幾次,產生一些和MainForm初始化有關的字符串。
點繼續十幾次之后窗口跳出來,點擊StringEncryption按鈕,再次斷下的字符串就分別是MessageBox的Text和Caption了。

調用方法處下斷點 (斷點記錄法)

接下來我不會再寫那么詳細了……不然文章寫不完了。
代碼十分分散,不方便貼圖貼代碼了。想要看懂的話,最好是把dnSpy打開,親手操作一遍
我們接下來對Virtualization按鈕開刀

分析虛擬化代碼需要抓重點,舉個例子:
單步調試可以知道以上的代碼中真正有效的是

u0002_u.\u0002(u, u2, u3);
//方法簽名為\u0002(Stream, string, object[]):void

我們可以猜測Stream是虛擬指令流,string是指令流解密key之類的東西,object[]是方法參數(包括this,sender,eventArgs)
我們的重點是虛擬代碼的執行部分,因此我們就可以忽略指令流Stream u的產生過程,忽略執行器\u0002\u2007 u0002_u的產生過程,把心思放在真正執行指令的u0002_u.\u0002(u, u2, u3);方法里

這很重要!!!我最初沒有想到Reflection,就是抓重點一路摸到關鍵斷點位置的。

在調用方法處下斷點真的十分簡單,因為現在知道調用會用到Reflection,我又根據經驗得出,好幾個有關調用的關鍵函數都用到了MethodBase.IsConstructor。因此我們用分析器,分析(mscorlib.dll)System.Reflection.MethodBase.IsConstructor.get()被使用的情況,就可以找到關鍵函數。

斷點剛開始可以多下一些,對分析器找到的4個方法全部添加方法斷點(在函數入口和出口各下一個,總共8個斷點),然后面再把功能重復或者不需要的斷點再刪掉就行了,我最后在8個斷點中保留了2個

\u0002\u2007.\u0002(MethodBase, object, object[]) : object + 0x0000 (入口處)

\u0002\u2007.\u0002(MethodBase, bool) : \u0002\u2007 + 0x2B7 (出口處)

另外還有一個地方比較關鍵,也是我單步調試跟隨找到的,就在

(mscorlib.dll)System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(object, object[], object[]) : object + 0x0039 (出口處)

我的附件中有一個Breakpoints.xml,斷點下不對的童鞋可以dnSpy導入我的斷點,體驗一下。

下完斷點或導入斷點之后,斷點窗口如下:

只要看那三個打鉤激活的斷點就行了,別的斷點是高級方法里用的。

斷點下完之后,再次開始調試,輸入框里隨便輸入點東西(我輸了AAAAA),點擊Virtualization,程序就會被dnSpy斷下來。

圖中\u0002就是現在調用的方法MethodBase,然后按繼續

可以看到u4就是調用的執行結果

然后不停地按繼續就可以了,總結之后可以知道,每個斷點處的局部變量含義:

//\u0002\u2007.\u0002(MethodBase, object, object[]) : object + 0x0000 (入口處)

\u0002 目標方法(要調用的函數)MethodBase
\u0003 非靜態方法的this
\u0005 向目標方法傳遞的參數
//\u0002\u2007.\u0002(MethodBase, bool) : \u0002\u2007 + 0x2B7 (出口處)

\u0002 目標方法(要調用的函數)MethodBase
obj 非靜態方法的this
array5 向目標方法傳遞的參數
u4 方法調用后的返回值
//System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(object, object[], object[]) : object + 0x0039 (出口處)

this 目標方法(要調用的函數)MethodBase
obj 非靜態方法的this
arguments, parameters 都是參數,暫未發現區別

每次斷點都把收集到的信息記錄下來
然后就是連蒙帶猜地反推代碼咯,比如Virtualization一路繼續,記錄如下(MethodBase,參數,this,返回值結合起來分析,若this不為null,我記錄時就加上Instance):

textBox.Instance.get_Text();
data=Base32Decode(text); //這里有點難,名字被混淆了,根據MethodBase的FullName找到目標函數,看一眼代碼能分析出是Base32
new DESCryptoServiceProvider();
InitializeArray(new byte[8] { 0x1F, 0x28, 0x87, 0x08, 0xCC, 0x32, 0x22, 0x35 }, RuntimeFieldHandle);
DESCryptoServiceProvider.Instance.SetKey(new byte[8] { 0x1F, 0x28, 0x87, 0x08, 0xCC, 0x32, 0x22, 0x35 });
InitializeArray(new byte[8] { 0xE2, 0xA2, 0x83, 0xBE, 0x99, 0x76, 0x84, 0x38 }, RuntimeFieldHandle);
DESCryptoServiceProvider.Instance.SetIV(new byte[8] { 0xE2, 0xA2, 0x83, 0xBE, 0x99, 0x76, 0x84, 0x38 });
DESCryptoServiceProvider.Instance.CreateDecryptor();
CryptoAPITransform.Instance.TransformFinalBlock(data);//這里的this是上一步驟CreateDecryptor()的返回值;此步驟報錯,解密出錯

以上的只是偽代碼,然后自己根據語法重寫一下:

string serial = tbInput.Text;
byte[] bEncryptedData = Base32.FromBase32String(serial);
DESCryptoServiceProvider cryptoServiceProvider = new DESCryptoServiceProvider();
cryptoServiceProvider.Key = new byte[8] { 0x1F, 0x28, 0x87, 0x08, 0xCC, 0x32, 0x22, 0x35 };
cryptoServiceProvider.IV = new byte[8] { 0xE2, 0xA2, 0x83, 0xBE, 0x99, 0x76, 0x84, 0x38 };
byte[] bDecryptedData = cryptoServiceProvider.CreateDecryptor().TransformFinalBlock(bEncryptedData, 0, bEncryptedData.Length);

那就很清晰明了了啊,Virtualization的實際操作是獲取輸入,Base32解碼以后用設定的Key,IV用DES解密。因為隨便輸入的數據無效,所以解密報錯了。
至于將注冊碼驗證代碼轉換為注冊碼生成代碼,那就是不是本文的主題了……
提供幾組注冊碼:前5個有效,后3個無效,但是報錯位置不同

1 Of 5             WYYCZJM7UUNXNLWPGWTYG89S73
2 Of 5             8S9BZUVPPMY9J5FRYDZXXHXJL5
3 Of 5             UVGD4CC2NMD63RA5HG6EPAAWU9
4 Of 5             W6W5LE28GNBF5ANR3UPJTKRXP2
5 Of 5             76WWW7Z2VXUQMVCB38CXFG9W55
N Of 5             TUYD94XEX4UR8R26MECH78FUE8
WrongLength        TUYD94XEX4URQXEGBP46943AS3
Invalid            AAAAAAAAAAAAAAAAAAAAAAAAAA

用有效的注冊碼就可以不報錯繼續分析下去。
之后的流程繼續分析的話,會遇到字符串解密,調用一個System.String(Int32)的MethodBase,返回的string是重點,傳入參數Int32可以不管他。(解密字符串的函數是\u0003\u2004\u2001.\u0002,在局部變量窗口可能顯示不完全,因為\u0000這些都是Unicode的非可見字符)

斷點記錄法能夠獲取的信息只有 MethodBase,參數,this,返回值 這4項,其它的操作都需要靠你的分析和聯想了。只要正確猜測、分析出哪次調用的返回值,又成為了哪次調用的參數之一,就可以還原出程序執行的虛擬代碼。

不同的注冊碼解密字符串的結果各不相同,彈窗字符也各不相同,這是為什么呢?
因為我在之后的流程中加入了if, switch這兩種判斷語句。不同的判斷結果會跳轉到不同的分支。斷點記錄法僅僅能夠讓我們知道程序現在運行的分支的流程,而分支跳轉是斷點記錄法無法得知的,這也是斷點記錄法的缺點。要想了解分支跳轉的信息,就必須用高級方法。

斷點記錄法總結一下:

要點:

分析器查找使用(mscorlib.dll)System.Reflection.MethodBase.IsConstructor.get()的函數
在函數入口出口處下斷點截取4條信息(MethodBase,參數,this,返回值)
System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(object, object[], object[]):object出口處下斷點
正確地連蒙帶猜還原代碼。

優點:

只要下斷點和記錄,操作簡單
在大部分沒有分支跳轉的情況下就夠用了(我就是用斷點記錄法還原了Kido大大的Keygen)

缺點:

分析能力不夠的話,可能會分析錯,而且靠猜測還原代碼,不太嚴謹
對于非調用語句,如判斷語句(if,switch等)的分析比較困難(這一點對于分析注冊算法影響還是挺大的……)、

虛擬IL還原 - 高級方法 (IL還原法)

Eazfuscator的IL虛擬機執行方式和.Net CLR的執行方式是很相近的。
有足夠的時間和精力的話,能夠從Eazfuscator的虛擬IL全部轉換為.Net的正常IL。
高級方法復雜一些,要求對.NetCLR運行比較了解

.Net CLR 執行方式

IL指令(OpCodes)

MSDN上面有關于OpCodes的詳細介紹(點擊此處)(建議Opcodes, OpCodeType, OperandType這三章都看一下)
不需要對所有OpCodes爛熟于心,但是對于常用的IL都需要知道是什么意思。尤其是有關分支跳轉的那些IL指令。

有一點需要注意,就是IL的參數,指令可能有不同類型的參數
有些IL沒有參數,如 add, ldc.i4.0, ldloc.0, stloc.0, br.s
有些IL有int8參數,如 ldc.i4.s, ldloc.s, stloc.s
還有一些IL分別帶有int16, int32, int64, float32, float64
甚至還有一些IL帶有奇怪類型的參數,如 call, box

.Net CLR 堆棧 (數據流向)

首先要知道.Net CLR執行IL時的幾個堆棧

注意這里的堆棧和PE程序對的堆棧不同,這不是對于CPU真實存在的堆棧,只是一個CLR抽象出來、CLR管理的邏輯堆棧。

實際上 FunctionParameters 和 LocalParameters 都不符合先進后出,并不是一般意義上的堆棧,我不知道該怎么稱呼。把他倆當做存儲數據的地方就行了。

Evaluation Stack (計算堆棧)

上圖中最中間那個就是EvaluationStack,這個堆棧也是.Net執行時最重要的堆棧。所有指令的執行都和EvaluationStack有關。

方法A調用方法B時,.Net程序會依次把 arg0(非靜態方法中為this), arg1, ..., argx 依次壓入堆棧
方法B在return時,EvaluationStack中唯一的數據(只能有0個或1個)就被壓入方法A的EvaluationStack

所有計算操作都是發生在EvaluationStack里的

Function Parameters (傳入參數)

FunctionParameters好像是屬于CallStack的一部分的,但是我分不清楚……就按照自己的理解瞎說了
上圖中最右側那個就是Function Parameters,這里儲存了調用該方法時傳遞的參數。
方法A調用方法B時壓入A的EvaluationStack的參數會被依次彈出,進入方法B的Function Parameters
方法B通過 ldarg.1, ldarg.s 等等指令將FunctionParameters里對應的參數壓入方法B自己的EvaluationStack

Locals Variables (局部變量)

LocalsVariables好像也是CallStack的一部分,我也不太能分清楚……
上圖中最左側那個就是LocalsVariables,這個很好理解,就是臨時存放變量的地方,在IL代碼的.locals中就會定義出LocalsVariables的大小,比如UnpackMe1.clear.exe的UnpackMe1.MainForm.BtnVirtualization_Click就有如下的.locals定義

.maxstack 4
.locals init (
        [0] uint8[],
        [1] class [mscorlib]System.Security.Cryptography.DESCryptoServiceProvider,
        [2] uint8[],
        [3] uint8
)

這段IL就已經限定了此方法的LocalsVariables數量和各自的類型。

方法通過 stloc.0, stloc.s 等將EvaluationStack彈出的數據存入LocalVariables;通過 ldloc.0, ldloc.s 等將LocalVariables的數據壓入EvaluationStack。
注意,ldloc不會刪除LocalVariables的數據,但是stloc會彈出EvaluationStack的數據

.Net CLR 運行

真正的運行其實還是有點復雜的,需要JIT編譯器編譯成Native代碼執行。但是我們可以不管那些,理解為CLR解釋執行就行了。這樣的話,運行過程其實挺簡單的……
CLR運行的內容說到底就是一串IL指令流唄
可以想象一下,CLR運行進入一個方法時,就會獲取到一串該方法的指令流Stream。CLR首先按照.locals分配LocalsVariables,準備好FunctionParameters。
執行過程就是不停地Read這個Stream獲取指令,然后按照指令的要求操作三個堆棧或者跳轉(跳轉相當于Seek指令Stream,重新設定讀取的位置)
然后就一直重復以上的執行過程,直到遇到ret指令返回計算結果。

將.Net CLR中的概念類比到Eazfuscator中

Eazfuscator的IL虛擬化保護,真正運行的指令還是IL指令啊。想要運行的話,就必須模仿.NetCLR的執行方式,所以從.Net CLR理解之后類比到Eazfuscator就可以了。

找到Eazfuscator.NET的虛擬CLR

這個CLR肯定就是一個類嘛,不可能是一個單獨的方法。其實很好找,各種線索都在指向一個類 \u0002\u2007

在斷點記錄法中,下的3個斷點有2個都在 \u0002\u2007 之中
在被虛擬化的 MainForm.BtnVirtualization_Click() 中也是產生了 \u0002\u2007 這樣的東西。

這個奇奇怪怪的 \u0002\u2007 就是Eazfuscator的虛擬CLR了。

找到產生運行指令的地方

指令流肯定要循環Read,獲取到現在要執行的指令才能執行起來對吧,那我們接下來就要找到這個有循環的地方。
先就用斷點記錄法里設置的3個斷點就夠了,任意輸入后點擊Virtualization,斷點斷下后,我們看一下調用堆棧(工具欄-調試-窗口-調用堆棧)

然后我們就從上往下一個個看吧,直到這里

我們才發現有一個循環,雖然現在還看不懂具體的作用,但是這里確實就是循環執行指令,使虛擬IL連續運行的地方。
\u0002\u2007.\u0005(bool):void 這里就是一個虛擬化函數的標志,每個虛擬化的函數一定都會運行到這里。
調用堆棧里該函數的數量也體現出虛擬化調用層級的個數。
比如說,點擊Virtualization,我們看到調用堆棧里的\u0002\u2007.\u0005(bool):void個數始終只有一個。
但是點擊CallVirtualization,當MessageBox彈出"Leave CallVirtualization"之后,再點幾次繼續,就能看到調用堆棧迅速膨脹,出現兩層\u0002\u2007.\u0005(bool):void,如圖:

然后從這個地方跟著一步步單步調試,就可以發現其他類比過來的東西了。

Eazfuscator.NET虛擬CLR的堆棧 (數據流向)

數據包裝類型

Eazfuscator堆棧中的數據都被包裝過,數據包裝的基本類型是\u0002\u2001,然后它有幾種派生類型,如圖所示:

這些派生類型的用來包裝不同的基本類型,比如:

數據包裝類名 存儲數據類型
\u0002\u2003 byte
\u0002\u2004 long
\u0002\02005 uint
\u0003\u2002 Enum
\u0005\u2001 Array
\u0005\u2003 short
\u0005\u2005 ulong
\u0005\u2006 UintPtr
\u0005\u2009 string
\u0006\u2001 bool
\u0006\u2005 MethodBase
\u0006\u2009 object
\u0008\u2002 double
\u0008\u2004 sbyte
\u000E\u2002 float
\u000E\u2003 int
\u000E\u2004 ushort
\u000F\u2001 char
\u000F\u2005 IntPtr
\u000F\u2006 object (用于存儲任意其他類型)
Evaluation Stack

這個EvaluationStack被處理得好奇怪der……
EvaluationStack被分成了三部分,堆棧從頂部到底部依次為\u0003\u2003, \u0008\u2001, \u0005\u2001
當數據壓入EvaluationStack的時候,堆棧中所有的數據會依次從上往下移動。彈出的時候反向移動。

FunctionParameters

\u000F\u2000是一個數組,數組中\u000F\u2000[x]就是ldarg.x的x
注意,在非靜態方法中arg.0是this,而不是第一個參數!

LocalsVariables

LocalVariables全部存儲在\u0005中,\u0005[x]就是ldloc.x的x

Eazfuscator.NET的虛擬CLR變量含義

具體含義的發現思路就不細講了……實在是說不清楚。
主要思想就是把clear.exe的IL代碼和obfs的CLR局部變量變化對照起來看就可以了。
另外就是多用分析器分析一下各個函數之間的關系。
我們在任意一個\u0002\u2007里的斷點斷下,然后把this展開,看到的結果如下:

局部變量類型 局部變量名稱 局部變量作用
bool \u0002\u2000 <未知>
\u0003\200A \u0002\u2002 <確定>指令key和參數類型的對照表(后面解釋)
System.Reflection.Module \u0002\u2004 <確定>當前運行的Module(對分析無用)
\u000E\u2001 \u0003 <未知>
\u000E\u2001 \u0003\u2000 <猜測>從\u0006\u2003指令數組創建的讀取器
System.IO.Stream \u0003\u2001 <猜測>傳入的指令(數據)流Stream
\u0002\u2001 \u0003\u2003 <確定>EvaluationStack的一部分
System.Collections.Generic.Stack<\u0002\u2007.\u0008> \u0003\u2004 <未知>
\u0002\u2001[] \u0005 <確定>LocalsVariables
System.Collections.Generic.Stack<\u0002\u2001> \u0005\u2001 <確定>EvaluationStack的一部分
\u0008\u2000 \u0005\u2002 <確定>當前正在運行的方法的一些信息
uint \u0005\u2003 <確定>下一條指令(數據)位置(指令指針)
long \u0005\u2004 <未知>
System.Type[] \u0006 <未知>
object \u0006\u2000 <未知>
System.Type[] \u0006\u2002 <未知>
byte[] \u0006\u2003 <確定>指令(數據)流數組
uint \u0008 <確定>指令(數據)流數組的長度
System.Type \u0008\u2000 <未知>
\u0002\u2001 \u0008\u2001 <確定>EvaluationStack的一部分
System.Collections.Generic.Stack<\u0002\u2007.\u0006\u2000> \u0008\u2003 <未知>
\u0008\u2009[] u000E\u2000 <未知>
uint? \u000E\u2001 <確定>分支跳轉目標
object[] \u000E\u2002 <未知>
uint \u000F <確定>當前運行指令(數據)位置 (來自\u0005\u2003)
\u0002\u2001[] \u000F\u2000 <確定>FunctionParameters
bool \u000F\u2001 <確定>返回標志(return)

雖然虛擬CLR里很多變量的含義未知,但是這并不影響我們分析的流程……現在已了解的變量就足以還原IL代碼了,其余的變量等我有空的時候再試試分析含義吧。

Eazfuscator.NET虛擬IL指令解析

我們上文找到了循環執行指令的地方 \u0002\u2007.\u0005(bool):void
但是這并不是指令的來源,我們仔細看一下代碼

上面的判斷是有關return和分支跳轉的,我們先不用管,可以發現每次循環都會調用this.\u000E(),那我們就跟過去看看。

這里就是指令執行的重點了

看一下代碼的功能,先獲取\u0005\u2003指令指針,將\u0005\u2003設置到\u000F (變量含義見上表)
接下來調用了\u0003\u2000的\u0006()方法,返回值為num,設置到key。
然后\u0005\u2003指令指針增加4(設定下一條指令的位置)
最后一步是用Dictionary.TryGetValue(key)得到了一個類型為\u0002\u2007.\u0002\u2000的結構,調用結構里的\u0003方法(delegate)。

我們看到key的來源是num,num來自this.\u0003\u2000.\u0006();
其中\u0003\u2000是什么東西,在上表中我只是猜測,那我們分析一下,用分析器分析\u0003\u2000賦值

在\u0002\u2007.\u0002(object[],Type[],Type[],object[]):object中能找到這樣的一串代碼:

\u0006\u2003 u0006_u = new \u0006\u2003(this.\u0006\u2003);  \\用\u0006\u2003指令(數據)流初始化類型為\u0006\u2003的變量u0006_u
try
{
        using (this.\u0003\u2000 = new \u000E\u2001(u0006_u))  \\又用剛才的u0006_u初始化類型為\u000E\u2001的變量,將變量設定為\u0003\u2000
        {
                this.\u0008 = (uint)u0006_u.\u0008\u2003\u2008\u200A\u2005\u2004\u0002();
                this.\u000F\u2001 = false;
                this.\u000E\u2001 = null;
                this.\u000F = 0u;
                this.\u0005\u2003 = 0u;
                this.\u0005();
                this.\u0006();
        }
}

所以看出來了吧?\u0003\u2000是一個包含了數據(指令)流的東西,所以我猜測它是從指令數組創建的讀取器。

然后回到上面圖片里那段代碼(以下為節選)

int num = this.\u0003\u2000.\u0006();
key = num;
\u0002\u2007.\u0002\u2000 u0002_u;
global::\u0002\u2007.\u0002.TryGetValue(key, out u0002_u);
u0002_u.\u0003(this, this.\u0002(this.\u0003\u2000, u0002_u.\u0002));

現在我們知道key=num=\u0003\u2000.\u0006(),先放一下,等一會再繼續分析。來看最后三行
定義了一個類型為\u0002\u2007.\u0002\u2000的變量u0002_u,把Dictionary.TryGetValue的結果放進了這個變量里面
變量的類型是一個結構,如圖:

其中有一個byte,叫做\u0002
另外有一個\u0002\u2007.\u000F,叫做\u0003
那個\u0002\u2007.\u000F是一個委托的類型,定義如下:

private delegate void \u000F(\u0002\u2007 \u0002, global::\u0002\u2001 \u0003);

所以說最后一行會把this和this.\u0002(this.\u0003\u2000,u0002_u.\u0002)作為參數,調用結構體中的delegate。
this是什么,想必大家已經有部分理解了,雖然還看不懂傳遞給delegate的目的……
那么我們研究一下另一個參數this.\u0002(this.\u0003\u2000,u0002_u.\u0002)
傳入參數中,this.\u0003\u2000是讀取器,u0002_u.\u0002是結構中的那個byte

這個方法簡單直接地switch了結構中的byte,根據不同的case,有不同的操作。

先看case到0,7的那個代碼塊,指令指針增加4,返回值是用\u0002.\u0006()初始化的\u000E\u2003
來來來,考驗記憶力的時候到了!
\u000E\u2003是什么?還記不記得?
\u000E\u2003是int的數據包裝!
然后\u0006()熟不熟悉?有沒有覺得眼熟?
結合傳入參數看看,就能發現\u0002.\u0006()其實就是this.\u0003\u2000.\u0006()
再看一眼我之前說放下一會分析的key=num=\u0003\u2000.\u0006()
發現了沒有?
\u0003\u2000.\u0006()就相當于從哪個指令(數據)流中讀取4字節,并轉換為int (并不是直接轉換,4字節要調換順序)

其他的case也是如此,列表如下:

case 數據類型 讀取數據長度 IL指令舉例(沒想到的打問號)
0,7 int 4 call,ldc.i4
1 float 4 ldc.r4
2,9 ushort 2 ???
3 Array 4 * (1 + length) ???
4,5 byte 1 ???
6 long 8 ???
8 double 8 ldc.r8
10 null 0 add,ldarg.0,stloc.0
11 sbyte 1 ???
12 ulong 4 所有跳轉指令(指令指針)

有幾點需要注意!

首先是case12讀取的ulong,照理說ulong占8字節,而這個ulong只用4字節。
因為case12只有跳轉語句會用到,Eazfuscator知道數值不會超過4字節能表示的范圍,ulong讀取到的結果就是新分支的指令指針\u0005\u2003

另一點是不同類型的數據讀取順序不太一樣……
比如說有4個字節的數據 0xA1,0xA2,0xA3,0xA4
按照int(case0)讀取,那么結果就是 0xA4,0xA1,0xA2,0xA3
按照ulong(case12)讀取,那么結果是 0xA2,0xA4,0xA1,0xA3

現在就算猜,也能知道delegate是什么了吧…?
調用delegate的那句才是真正產生作用的語句,這個delegate指向一個函數,這個函數會操作虛擬機的三個堆棧。

圖中是\u0002\u2007中的一些委托可能指向的函數,這些函數,每一個都相當于一個IL指令。

剛才講的有點復雜,而且名稱混淆擾亂講解,我把重要部分的代碼重寫一下,再看一遍應該能懂了……

// \u0002\u2007.\u000F (VirtualCLR.OperationDelegate)
private delegate void OperationDelegate(VirtualCLR \u0002, DataWrapper \u0003);

// \u0002\u2007.\u0002\u2000 (VirtualCLR.OpCodeStructure)
private struct OpcodeStructure
{
        public readonly byte operandType;
        public readonly OperationDelegate operate;
}

// \u0002\u2007.\u000E():void
int num = this.DirectiveReader.\u0006();
int key = num;
OpCodeStructure structure;
global::VirtualCLR.OpCodeDict.TryGetValue(key, out structure);
structure.operate(this, this.GetOperand(this.DirectiveReader, structure.operandType))

現在我們知道 structure.operate() 指向的函數等效替代了一個IL指令,那么現在就需要分析每一個函數等效替代的IL語句

這一步方法有很多種,我就舉一個例子:
比如說,我通過靜態分析算法,或者查看執行前后堆棧的變化知道下圖

這個函數是ldarg.0,那么u0002\u2007.u0003(x)就相當于是ldarg.x咯
我們用分析器分析u0002\u2007.u0003(x),如圖

然后就可以順藤摸瓜找到

//ldarg.0
private static void \u0003\u2008(\u0002\u2007 \u0002, \u0002\u2001 \u0003)
{
        if (7 == 0)
        {
        }
        \u0002.\u0003(1);
}
//ldarg.s
private static void \u0008\u2008\u2000(\u0002\u2007 \u0002, \u0002\u2001 \u0003)
{
        if (6 == 0)
        {
        }
        if (4 == 0)
        {
        }
        \u0002.\u0003((int)((\u0002\u2003)\u0003).\u0002()); //這里要根據數據包裝\u0002\u2003反推,得知類型為byte,即int8,所以是ldarg.s
}
//ldarg.3
private static void \u0008\u200A\u2000(\u0002\u2007 \u0002, \u0002\u2001 \u0003)
{
        if (false)
        {
        }
        \u0002.\u0003(3);
}
//ldarg
private static void \u000E\u2000(\u0002\u2007 \u0002, \u0002\u2001 \u0003)
{
        if (8 == 0)
        {
        }
        if (false)
        {
        }
        \u0002.\u0003((int)((\u000E\u2004)\u0003).\u0002());//同樣,根據數據包裝\u000E\u2004反推得知類型是ushort,即int16,所以是ldarg
}
//ldarg.2
private static void \u000F\u200A\u2000(\u0002\u2007 \u0002, \u0002\u2001 \u0003)
{
        if (5 == 0)
        {
        }
        \u0002.\u0003(2);
}

我數了一下,大概有207個符合 OpCodeDelegate 的函數,比MSDN上的Opcodes個數226要少……
我猜測可能是因為沒有nop, break等等這些對于虛擬IL沒用的指令
還有一些加載數據的指令比如ldstr被ldc.i4+call等效替代了;
跳轉指令全部都是讀取ulong,不再區分br和br.s等等;
當然,也有可能是我數錯了……

我很好奇dnSpy能根據函數的參數、返回類型搜索函數嗎?或者找到實現delegate的所有函數?
我沒發現有這功能,知道的大神教教我吧……手動數數實在太low了

其實沒必要分析清除每一個 OpCodeDelegate 的功能,只要分析出的結果夠用就行了……
用動態調試法觀察每個delegate執行前后的堆棧和指令指針變化(其實就是觀察\u0002\u2007里this展開的變量)比較容易吧
靜態分析就用用分析器順藤摸瓜得了,想要靜態分析明白實在太累……
把一般常用的IL指令,比如ldarg.s, ldc.i4.s, ldloc.s, stloc.s, ldarg.s, call 和分支跳轉指令的 delegate找到就差不多了 (下一章會講一下分析跳轉指令的方法)

接下來的步驟,就是把指令(數據)流按照 Key,Operand 轉化為對應的IL (見附件里的BtnVirtualization_Click.vil)

/ /之間的是指令指針和虛擬化IL對應的十六進制數據
可以看到,每個IL指令對應的十六進制總是相同的,讀取十六進制轉化為key和參數的時候別忘了根據數據類型調換順序!
至于call和callvirt的MethodBase還是需要通過斷點記錄法的三個斷點得到。

分支跳轉的實現原理

我們高級的IL還原法,優勢就在于可以分析分支,那就講一下分析分支的方法:
回到 \u0002\u2007.\u0005(bool):void 就是那個while循環的地方

while判斷的條件this.\u000F\u2001是return的標志,就是說執行虛擬return時會設置this.\u000F\u2001=true,因此退出while循環,執行break,退出那個并沒有任何作用的for循環,然后按照調用堆棧逐級返回。

那個if語句就是判斷跳轉的地方了,當遇到 br, btrue, bfalse, ble, blt, bge, bgt 等等分支跳轉語句的時候,會將 \u000E\u2001 設置為跳轉的目標。
\u000E\u2001是一個比較神奇的類型,uint?,實際上是Nullable<uint>,就是可以為null的uint。
當不需要跳轉的時候,保持為null,需要跳轉時,將指令指針\u0005\u2003設置為此unit?的值。
所以分析跳轉,只要分析哪個函數將\u000E\u2001設置為非null即可,用分析器,只找到一個函數,就是u0002(uint):void

可以看出它只負責設置跳轉指令,而沒有判斷。所以可以知道其他的跳轉語句都會調用它,那么繼續用分析器分析。

這些就是等效替代IL跳轉指令的函數了……然后需要有耐心地一點點分析每一個函數的作用了。
實在不想分析的話,那就每次修改EvaluationStack上的數據(分別大于等于小于IL指令的參數),調用相同的delegate,看一下是否跳轉(看指令指針\u0005\u2003的變化),總是有辦法分析出跳轉條件的。

IL指令反推法再總結一下

要點:

找到while循環的位置和通過Dictionary.TryGetValue(key)獲取OpCodesStructure的地方
然后找到虛擬機執行器的this里面三個堆棧在哪里

通過OpCodesStructure不同的delegate確定IL指令的種類

或者

通過觀察每步驟IL執行前后堆棧數據和指令指針位置的變化,來反向推測這個IL的種類和功能。

截取GetParameters的返回值,得知IL指令的參數
將種類和參數結合,得知此刻執行的IL

然后不斷重復以上步驟,直到把整條方法的指令流全部還原。

優點:

可以得知指令流的全部信息,彌補斷點記錄法無法判斷分支的缺點。
分析每步驟IL的執行,反推的代碼更加準確。

缺點:

耗費時間
分析delegate等效替代的IL讓人想吐,這是體力活……

不過我覺得如果寫一個程序,用IL反推法分析Eazfuscator虛擬化IL,還是可以做得到的……
因為每條IL指令執行后堆棧和指針的變化總是可以看到的,
而且程序分析每個delegate等效替代的IL可能很簡單!
將虛擬IL流反推成標準IL流,之后再轉化為C#應該是可實現的。
但是我不會寫.Net脫殼工具,可能像@wwh1004 這樣的牛人才能做到吧……

總結

文章主要就是介紹了兩種分析Eazfuscator.NET虛擬化IL保護殼的方法。
基本方法斷點記錄法操作起來了比較簡單,但是不能判定分支跳轉。
高級方法IL還原法還原精確度高,但是耗費體力和耐心,對手工還原不太友好……能寫出輔助工具可能會好一些。
我已經盡力講清楚了…反復多看幾遍,如果還有無法理解的地方可以回帖來問我。
其實也沒什么好總結的了,只想說寫了這么長的文章,好累……

免費評分

參與人數 47吾愛幣 +76 熱心值 +45 收起 理由
1287417511 + 1 + 1 用心討論,共獲提升!
jary163 + 1 + 1 我很贊同!
Lugia + 1 + 1 [email protected]
yutu925 + 1 + 1 我很贊同!
backtrace + 2 + 1 [email protected]
安尼大大 + 1 + 1 我很贊同!
yixi + 1 + 1 [email protected]
sajuuk1129 + 1 [email protected]
Kido + 15 歡迎分析討論交流,吾愛破解論壇有你更精彩!
crazpro + 1 + 1 我很贊同!
JPK + 1 + 1 熱心回復!
FuSu_ChunQiu + 1 + 1 熱心回復!
vipcrack + 1 + 1 期待修改版的KEYGEN
霧落塵 + 1 + 1 .net到底有沒有個強殼?dng強度怎么樣?和eazfuscator, agile.net, koivm比.
whc2001 + 2 + 1 牛逼
yuemo520 + 1 + 1 用心討論,共獲提升!
wenyuanzh + 1 + 1 用心討論,共獲提升!
idsean + 1 + 1 [email protected]
yAYa + 3 + 1 又一位深藏不露的大牛~謝謝師傅~
jixun66 + 3 + 1 用心討論,共獲提升!
qaz789a4 + 2 + 1 熱心回復!
笙若 + 1 + 1 [email protected]
獨行風云 + 1 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
a417765659 + 1 + 1 大佬,很強大。努力學習中。。。感謝感謝!
yicong135 + 1 + 1 好頂贊
tchivs + 3 + 1 [email protected]
wwh1004 + 3 + 1 非常好!!!
cyw1912911875 + 1 + 1 我很贊同!
非凡公子 + 1 + 1 [email protected]
lies2014 + 2 + 1 用心討論,共獲提升!
netle8 + 1 + 1 熱心回復!
yyhf + 1 總結:一個程序員把保護自己程序的程序破解了
錯的是世界 + 1 + 1 不明覺厲
hjw52 + 1 + 1 [email protected]
jgs + 1 + 1 [email protected]
黑的思想 + 2 + 1 用心討論,共獲提升!
Monitor + 2 + 1 鼓勵轉貼優秀軟件安全工具和文檔!
3yu3 + 1 + 1 我很贊同!
sunnylds7 + 1 + 1 感謝發布原創作品,吾愛破解論壇因你更精彩!
wmsuper + 3 + 1 [email protected]
chen4321 + 1 我很贊同!
濤之雨 + 1 + 1 用心討論,共獲提升!
hxz303 + 1 + 1 又一篇精華帖誕生!很詳細!!很有責任感!!!很NB 拜見了!
anntonny + 1 + 1 用心討論,共獲提升!
Ravey + 1 + 1 [email protected]
201 + 1 膜拜大佬 不明覺厲
CrazyNut + 3 + 1 膜拜大佬 不明覺厲

查看全部評分

本帖被以下淘專輯推薦:

發帖前要善用論壇搜索功能,那里可能會有你要找的答案或者已經有人發布過相同內容了,請勿重復發帖。

推薦
wwh1004 發表于 2019-5-18 17:59
本帖最后由 wwh1004 于 2019-5-18 18:05 編輯
代碼亂序
Kido大大在他的帖子里說Eazfuscator的激活算法被亂序了,而沒有被虛擬化。Eazfuscator的文檔里也說有"Code Control Flow Obfuscation",但是我覺得,亂不亂序好像沒有區別欸……我并沒有感覺到代碼亂序的存在和作用。
一定要說有亂序的話,那只能認為,動態調試的分析方法直接無視了亂序吧……

代碼亂序有點用,我記得eaz的亂七八糟的br指令會造成dnspy不能正常下斷點,不過還是沒confuserex的switch控制流混淆厲害

然后那個eazfuscator激活問題,其實可以先找老版本的Eazfuscator.NET 2018.4 Setup.msi安裝然后激活,再升級到Eazfuscator.NET 2019.1 Setup.msi
推薦
 樓主| JemmyloveJenny 發表于 2019-5-19 21:40 |樓主
霧落塵 發表于 2019-5-19 21:35
.net到底有沒有個強殼?dng強度怎么樣?和eazfuscator, agile.net, koivm比起來誰最強?

別的殼沒遇到過、沒分析過,我不知道……
真的想要強的話,建議CoreRT
推薦
夜泉 發表于 2019-5-18 03:00
大佬,你能到這一步的水平到底學了那些東西呢?我也想走分析這些東西
推薦
艾莉希雅 發表于 2019-5-18 01:51
當我在讀之前這個帖子還沒加精……讀完就加精了
這個速度有多快啊
6#
 樓主| JemmyloveJenny 發表于 2019-5-18 02:02 |樓主
艾莉希雅 發表于 2019-5-18 01:51
當我在讀之前這個帖子還沒加精……讀完就加精了
這個速度有多快啊

我也才發現……H大深夜不睡在加精!
7#
feob 發表于 2019-5-18 06:06
收藏了,感謝分享
8#
kunsun 發表于 2019-5-18 06:32
佩服佩服佩服
9#
UserXCH 發表于 2019-5-18 06:34
有一個項目在 GitHub 上叫 "eazdevirt", 你可以去看看。
10#
小航哥 發表于 2019-5-18 07:11
先前排留個名字!
11#
Ravey 發表于 2019-5-18 07:54
如果 Runtime 是多態變形,要自動還原應該會很難
12#
chen4321 發表于 2019-5-18 07:56
厲害了,看天書一樣看完,收藏了
您需要登錄后才可以回帖 登錄 | 注冊[Register]

本版積分規則 警告:禁止回復與主題無關內容,違者重罰!

快速回復 收藏帖子 返回列表 搜索

RSS訂閱|小黑屋|聯系我們|吾愛破解 - LCG - LSG ( 京ICP備16042023號 | 京公網安備 11010502030087號 )

GMT+8, 2019-7-6 22:02

Powered by Discuz!

© 2001-2017 Comsenz Inc.

快速回復 返回頂部 返回列表
内蒙古11选5开奖查询百度