Eazfuscator.net 2020 IL級指令虛擬化保護(Virtualization)機制分析

一、前言與目標
週末接觸了一款遊戲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協議壓縮後的文件,

  1. 如何得到密碼?
a. 修改其使用的zip.dll,打印出密碼
or b. 調試得出
  1. 如何插入自己的代碼?

前文提到過,插點代碼程序就起不來了,但難得住我們嗎?

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

那這個就交給大家吧 ^^

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章