zoukankan      html  css  js  c++  java
  • 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
    程序对DXVision.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,关注字符串解密的调用及重点字符串

    以上建议,目的只有一个:全面、准确地理解程序是如何执行的
    划重点:逆向工作和写业务代码不一样啊,在逆向工作中,搜索引擎能帮到你的有限,所以基本功夫做扎实不会错

    七、将VIL还原为MSIL
    本文分析的Eazfuscator.net版本共203条VIL,而MSIL则是226条(没记错的话)。
    要还原,就是搞清楚每条VIL指向的委托方法的含义,示意如下:

    重点是怎么搞清楚呢???

    1,跳转指令:找出置跳转标记的方法,分析其是jump,brfalse,brtrue还是ceq,cgt.U,等等;
    2,非跳转指令:
    以下面3条较为简单的IL为例,就是观察分析局部变量、方法参数和计算堆栈的关系和数据流向

    // Stloc.1	从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。 **
    private void #=zeax70SK78qvBksv_HSIylvILyENMKeW1tJlKedMyAxHj(I #=zOSg$HgU=)
    {
        //这个方法会执行"出栈"操作,即EvaluationStack -> Locals,那参考前文提到的局部变量结构是个数组,我们可以猜出含义为Stloc.1
        this.#=zVbHppRUNVJW9QfM8DwpAVfDEksiK(1);
    }
    //Ldc.I4.M1	将整数值 -1 作为 int32 推送到计算堆栈上。
    private void #=zocym16spuuKSpZD6IdNfJyY=(I #=zOSg$HgU=)
    {
        //R就是EazDataType即对.net标准数据类型的封装,这里是Int32类型,值为-1,而下面的方法是"入栈"操作,故得出含义Ldc.I4.M1
        this.#=zexMJOnk$ibBRsUo6c2liuPA=(new R
        {
            a = -1
        });
    }
    //Ldloc.1	将索引 1 处的局部变量加载到计算堆栈上。
    private void #=ztDmUwukFujyUgcajdkLL0FGsab_BTNnUlQjeQ6o=(I #=zOSg$HgU=)
    {
        //this.#=zlSwQmt8=就是局部变量区,数组结构,#=zsntypKXXZWym_5GnnkCPoEbigOhG_tJ_w9w9r1rJTKkwhnz7dR47jMXtH5hsockjVL046YeT2OhXbWy5CfHcmcg=()方法是转换为EazDataType即封装类型
        //this.#=zexMJOnk$ibBRsUo6c2liuPA=()方法执行"入栈"操作,故得出含义Ldloc.1
        this.#=zexMJOnk$ibBRsUo6c2liuPA=(this.#=zlSwQmt8=[1].#=zsntypKXXZWym_5GnnkCPoEbigOhG_tJ_w9w9r1rJTKkwhnz7dR47jMXtH5hsockjVL046YeT2OhXbWy5CfHcmcg=());
    }
    

    这是个体力活 :]

    八、最后

    回到初心,这个分析的目的是为了改改游戏。

    游戏对部分资源文件(.dat)加了密,经过分析就是一个带密码的标准Zip协议压缩后的文件,

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

    前文提到过,插点代码程序就起不来了,但难得住我们吗?

    a. 故布疑云,程序计算解压密码
    读懂了程序后,发现其会根据exe本身的内容和大小,做一系列计算,得出zip包的解压缩密码,所以更改了程序的任何地方包括代码,都会导致密码计算出错。
    (还记得吗,当初我以为是Eaz保护后做了完备性/防篡改校验,但分析后发现这个手段的主要目的是保护资源,同时又间接防止了篡改,算一个一箭双雕的保护技巧吧 :|)
    
    b. 张冠李戴,我们来代入正确密码
    既然我们改了程序会导致“密码计算”出错,那直接写死个正确密码不就得了(实际上是多个文件,多套密码),
    
    c. 偷梁换柱,载入我们修改后的dll
    而使用这个密码并进行解压缩操作的dll恰好是通过上文提到的作为“EmbeddedResource”载入的,那么如法炮制,在dnspy中添加资源,并通过修改IL代码去掉程序对资源文件的解密/哈希过程,载入我们自己的dll即可
    

    3,自由王国开启,但仍有雾霾笼罩

    爽点:
    可以Hook进我们自己的代码后,基本就是进入了自由王国,想干啥干啥,配个图,实现新兵种添加:

    更爽点:
    我们脱掉了虚拟机保护了吗?没有,我们只是十分熟悉了它并利用规律达到了我们的目的。

    技术男的终极目标必须得是:写出一个脱壳机DeVirtualizer!!!

    如何写出?再次友情提示:
    IL指令还原:203条VIL,对应到MSIL,
    参数还原:VIL具体执行的Delegate的参数,将EazDataType转换为.net常规类型,
    反序列化:从虚拟指令流(VIL Stream,在EmbeddedResource中)找到指令的Position,将指令读出来,
    局部变量及方法参数还原:同上,
    异常处理还原:同上,
    会用到需要的辅助工具:dnspy,dnlib

    那这个就交给大家吧 ^^

    (完)

  • 相关阅读:
    第四周作业
    RHEL6+GFS2+MYSQL高可用
    第三周作业
    第二周作业
    centos7 安装redis 开机启动
    无线网卡连接网络后共享给本地有线网卡使用(Win10)
    第一周作业
    2019.8.13加入博客园
    智力题
    Python入门基础学习(模块,包)
  • 原文地址:https://www.cnblogs.com/foreach-break/p/eazfuscator_virtualization_they_are_billions_crack.html
Copyright © 2011-2022 走看看