Eazfuscator.net 2020 虛擬化保護(Virtulization)機制分析
- 2020 年 9 月 1 日
- 筆記
- eazfuscator, 億萬殭屍, 逆向.安全, 逆向工程
一、前言與目標
周末接觸了一款遊戲They are billons即億萬殭屍,想添加一些新的玩法元素比如新的兵種進去,
打開dnspy看了下,發現是Eazfuscator.net的Virtulization即虛擬化保護,並帶了欄位、方法混淆,字元串也加密了,
那就開始分析吧!
二、準備過程
1,先試試能不能插入自己的程式碼
打開dnspy,插入一段簡單的探測用IL程式碼,可以編譯,但打開程式報錯:不能讀取DAT文件(當然是英文的)。
這個提示很有意思,最開始我猜測是程式作了完整性/防篡改校驗,但其實是也不是,後文揭曉吧。
2,正向閱讀程式碼,建立對程式行為的整體性理解
這裡用了比較久的時間,一個是虛擬化保護機制的單步跟蹤十分耗時,另一個是遊戲自己業務邏輯也算複雜。
3,抽象出虛擬化保護的邏輯框架
這裡先列幾個重點概念:
3.1 虛擬機
主要解釋執行虛擬指令,姑且叫VLR;
3.2 虛擬IL程式碼
對比MSIL,MSIL算標準實現的話,這個算自定義實現,姑且叫VIL;在這套保護機制中,VIL的數據存儲結構是字典即Dictionary<int32,Delegate>,其中Key就是VIL標識,Value就是此VIL對應的C#方法,這個方法「模擬」實現了MSIL的功能
3.3 指令指針
指向下條指令的”位置”,這個”位置”,就是第(2)條提到的Key即VIL標識,怎麼來的呢?
3.4 計算堆棧,
自定義地實現了一個EvaluationStack(舉個例子,在MSIL中的ldfld,stloc.s操作的就是這個棧了),其具體結構是:
(a)局部變數區:數組,
(b)方法參數區:數組,
(c)CallStack:自定義的LIFO的堆棧結構
3.5 跳轉指令
控制程式流程,通過操作第(3)條的指令指針實現
3.6 單條指令執行的抽象形式
operate_instruction(EazDataType parameterData),
劃重點:
3.6.1 指令執行的方法:其中Instruction_Operatate是一個封裝了指令執行委託的結構,委託是關鍵:`private delegate void g(I #=zOSg$HgU=);`,
3.6.2 指令執行的參數:而EazDataType 是一個對所有基本類型,如int8,16,32,64,及其無符號類型,還有數組、object及IntPtr等類型的自定義封裝
為什麼要劃重點?
3.6.3 理解了虛擬指令的行為及參數數值的含義,是理解被保護下的程式邏輯的基礎,也是寫出脫殼工具即DeVirtualizer的基礎(github上有個15年後不再更新的,那時候的Eaz還很簡單)
3.6.4 Eaz團隊花這麼大功夫做自定義類型,肯定不是把真實參數擺在類型結構內的一個欄位就完事了,後面會知道,這跟序列化、出入EvaluationStack有關,沒錯,它就是極大地增加了我們閱讀和還原程式的難度
3.7 序列化與反序列化
3.7.1 虛擬指令哪裡來的?從程式的嵌入資源即EmbeddedResource來
3.7.2 Stream操作的Seek,Read,Write均被重寫,同樣混淆+反覆跨多個類+高深度調用,增加難度
3.7.3 ReadInt4,8,16,32,64即無符號形式,均被自定義重寫,其中還跨幾個混淆後的類進行反覆穿插調用,沒錯,也是為了增加我們閱讀和還原程式的難度
3.8 Assembly Resolve
程式對DXVison.dll,DXPlatform_Desktop.dll等類庫,採用了如下方式處理來增加難度:
3.8.1 將dll作為EmbeddedResource來構建,當然dll本身做了加密 and/or 壓縮處理
3.8.2 在ResolveAssembly中進行Assembly.Load,具體的,當然會解密 and/or 解壓縮
三、VIL執行過程分析
//----------------------------------------------------------------------------
// Eaz VIL執行邏輯 2020.8.23 6:22 A.M. Ben
//----------------------------------------------------------------------------
// Token: 0x06002490 RID: 9360 RVA: 0x0006F4F4 File Offset: 0x0006D6F4
private void #=zPq6qoiyuLMY82$aYQR3G2PDDewUYassYkHNyaic6mupX()
{
long num = this.#=z3Ey5Z$A=.a.d;
while (!this.#=zIohob_Q=)
{
//跳轉標記,不空則順序執行,否則跳轉執行
if (this.#=z46nfKvA= != null)
{
//置指令指針
this.#=z3Ey5Z$A=.a.e = (long)((ulong)this.#=z46nfKvA=.Value);
//清跳轉標記
this.#=z46nfKvA= = null;
}
//找出並執行指令
this.#=zSDEDP1kaZWXW$45uMxtJcKw=();
if (this.#=z3Ey5Z$A=.a.e >= num && this.#=z46nfKvA= == null)
{
break;
}
}
}
從上面的程式碼可以看出就是一個簡單的while型結構,其中具體的「找出並執行指令」的方法,最終會來到這裡:
// Token: 0x06002492 RID: 9362 RVA: 0x0006F60C File Offset: 0x0006D80C
private void #=zreNohAiSE8iKLx4yhAUiRL0=()
{
//指令指針
long num = this.#=z3Ey5Z$A=.a.e;
//指令標識
int key = this.#=z3Ey5Z$A=.#=zQ_ANng9RuwjiUHLMdDTa3uFQlfZa();
//執行指令的具體C#方法,這裡即模擬MSIL執行的過程
Sa.h h;
if (!this.#=zY0bMZDI= /* <VIL,Delegate>型字典 */.TryGetValue(key, out h))
{
throw new InvalidOperationException(#=qgZ0DNC_7EOR3zfCQRQFvJ6zpp28vu_oHH5ALnGtD3WQ=.#=zAuKOdtM=(-105951893));
}
this.#=zmBYAt_U= = num;
//封裝指令參數,並執行指令,這裡的VIL一共203條,不是MSIL的226條
h.#=zBHOdjps=(this.#=zp5urEB_sgKnN5sPVX9mxjurqsdh7nWJ3ig==(this.#=z3Ey5Z$A=, h.#=zOSg$HgU=.b));
}
其中有幾個點,
關於key即指令標識怎麼來的,也就是如何取指令的,看這裡:
//取指令
// Token: 0x0600241C RID: 9244 RVA: 0x0006CF2C File Offset: 0x0006B12C
internal int #=zKCiIwS5PZc7nU6m85A==()
{
if (!this.#=zzepStOk=)
{
throw new Exception();
}
//非跳轉即順序執行的情況下,下條指令在VIL Stream中的位置
int num = this.#=zTxm7_P0= += 4;
if (num > this.#=z1rdegSo=)
{
this.#=zTxm7_P0= = this.#=z1rdegSo=;
throw new Exception();
}
//this.#=zOSg$HgU=即VIL Stream的位元組形式,這一句就是反序列化得出指令標識了
return (int)this.#=zOSg$HgU=[num - 3] << 8 | (int)this.#=zOSg$HgU=[num - 1] << 16 | (int)this.#=zOSg$HgU=[num - 4] << 24 | (int)this.#=zOSg$HgU=[num - 2];
}
五、字元串解密過程
太長,有興趣的同學自己跟,我們列出這段的目的是尋找對我們理解被保護下的程式執行過程,為打開思路和診斷問題打下基礎。
//字元串解密,此方法調用深度太深,略,通過看局部變數的變化,拿到返回的字元串,可以幫助理解程式執行流程
這裡的字元串會出現:
a. "cctor"
b. "TheyAreBillions.exe"
c. "Log"
d. 類名
e. 方法名
// #=qgZ0DNC_7EOR3zfCQRQFvJ6zpp28vu_oHH5ALnGtD3WQ=
// Token: 0x0600008B RID: 139 RVA: 0x000045A0 File Offset: 0x000027A0
[MethodImpl(MethodImplOptions.NoInlining)]
internal static string #=zAuKOdtM=(int #=zJUrJqCXmqVi3rqXNtbpf4_w$1MCa)
{
#=qgZ0DNC_7EOR3zfCQRQFvJ6zpp28vu_oHH5ALnGtD3WQ=.l11ll11l111lll111 obj = #=qgZ0DNC_7EOR3zfCQRQFvJ6zpp28vu_oHH5ALnGtD3WQ=.#=zX2TmEXwAWH2uDIzJb_$ykHVBWImv;
string result;
lock (obj)
{
string text = #=qgZ0DNC_7EOR3zfCQRQFvJ6zpp28vu_oHH5ALnGtD3WQ=.#=zX2TmEXwAWH2uDIzJb_$ykHVBWImv.get_Item(#=zJUrJqCXmqVi3rqXNtbpf4_w$1MCa);
if (text != null)
{
result = text;
}
else
{
result = #=qgZ0DNC_7EOR3zfCQRQFvJ6zpp28vu_oHH5ALnGtD3WQ=.#=zAcaXirSWgb6OCMOieJ96pes=(#=zJUrJqCXmqVi3rqXNtbpf4_w$1MCa, true);
}
}
return result;
}
六、程式流程式控制制手段
主要手段如下:
1,從程式的嵌入資源中,得到VIL Stream及其位元組表示形式,順序執行 + 跳轉執行;
2,反射執行:Stream -> Seek -> Read -> 得到 Type Name -> 字元串解密 -> 通過反射初始化此類型,方法同理
七、一些建議:如何更好地分析虛擬化保護下的程式?
到這裡,我們可以愉快地調試並分析程式,但同樣有幾個細節需要注意,否則會迷失在看不懂的程式碼里:
1,關注dnspy中的方法調用堆棧,
2,關注EvaluationStack,局部變數和參數變化,儘快分析出操作局部變數、方法參數和EvaluationStack的VIL即對應的C#方法,及跳轉指令如brfalse.s,brtrue,ceq及ret等
3,關注類型反射關注MethodBase及Constructor的調用
4,關注字元串解密的調用及重點字元串
以上建議,目的只有一個:全面、準確地理解程式是如何執行的
劃重點:逆向工作和寫業務程式碼不一樣啊,在逆向工作中,搜索引擎能幫到你的有限,所以基本功夫做紮實不會錯
八、最後
回到初心,做這個分析的目的是為了改改遊戲,順便練練技術內功。
遊戲對部分資源文件(.dat)加了密,經過分析就是一個帶密碼的標準Zip協議壓縮後的文件,
- 如何得到密碼?
a. 修改其使用的zip.dll,列印出密碼
or b. 調試得出
- 如何插入自己的程式碼?
前文提到過,插點程式碼程式就起不來了,但難得住我們嗎?
a. 故布疑雲,程式計算解壓密碼
讀懂了程式後,發現其會根據exe本身的內容和大小,做一系列計算,得出zip包的解壓縮密碼
(還記得嗎,當初我以為是Eaz保護後做了完備性/防篡改校驗,其主要意義是保護資源,但是間接也防止了篡改,算一個一箭雙鵰的保護技巧吧 :|)
b. 張冠李戴,我們來代入正確密碼
既然我們改了程式會導致「密碼計算」出錯,那直接寫死個正確密碼不就得了(實際上是多個文件,多套密碼),
c. 偷梁換柱,載入我們修改後的dll
而使用這個密碼並進行解壓縮操作的dll恰好是通過上文提到的作為「EmbeddedResource」載入的,那麼如法炮製,在dnspy中添加資源,並去掉程式對資源文件的解密/哈希過程,載入我們自己的dll即可
3,自由王國開啟,但仍有霧霾籠罩
爽點:
可以Hook進我們自己的程式碼後,基本就是進入了自由王國,想幹啥幹啥,配個圖,實現新兵種添加:
更爽點:
我們脫掉了虛擬機保護了嗎?沒有,我們只是十分熟悉了它並利用規律達到我們的目的。
技術男的終極目標必須得是:寫出一個脫殼機DeVirtualizer!!!
那這個就交給大家吧 ^^
(完)