一、前言與目標
週末接觸了一款遊戲They are billions即億萬殭屍,想添加一些新的玩法元素比如新的兵種進去,
打開dnspy看了下,發現是Eazfuscator.net的Virtualization即IL指令級別的虛擬化保護,並帶了字段、方法混淆,字符串加密等常規保護手段,
那就開始分析吧!
友情提示:閱讀本文需要對CLR和指令虛擬化等有基本瞭解
啥是虛擬化?簡單說就是保護程序自己寫了個虛擬機,將MSIL及其處理過程從CLR搬進了自己的虛擬機執行,參考:https://www.gapotchenko.com/eazfuscator.net/features/virtualization
啥是CLR?百度
二、準備過程
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!!!
如何寫出?再次友情提示:
IL指令還原:203條VIL,對應到MSIL,
參數還原:VIL具體執行的Delegate的參數,將EazDataType轉換爲.net常規類型,
反序列化:從虛擬指令流(VIL Stream,在EmbeddedResource中)找到指令的Position,將指令讀出來,
局部變量及方法參數還原:同上,
異常處理還原:同上,
會用到需要的輔助工具:dnspy,dnlib
那這個就交給大家吧 ^^
(完)