吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 39970|回复: 85
收起左侧

[.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, 下载次数: 301)


附件里面有我本篇文章分析的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还原法还原精确度高,但是耗费体力和耐心,对手工还原不太友好……能写出辅助工具可能会好一些。
我已经尽力讲清楚了…反复多看几遍,如果还有无法理解的地方可以回帖来问我。
其实也没什么好总结的了,只想说写了这么长的文章,好累……

免费评分

参与人数 51吾爱币 +79 热心值 +49 收起 理由
Haleclipse + 1 + 1 谢谢@Thanks!
poqiang520 + 1 我很赞同!
无极实验室 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
iNIC + 1 + 1 谢谢@Thanks!
1287417511 + 1 + 1 用心讨论,共获提升!
jary163 + 1 + 1 我很赞同!
Lugia + 1 + 1 谢谢@Thanks!
yutu925 + 1 + 1 我很赞同!
backtrace + 2 + 1 谢谢@Thanks!
安尼大大 + 1 + 1 我很赞同!
yixi + 1 + 1 谢谢@Thanks!
sajuuk1129 + 1 谢谢@Thanks!
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 谢谢@Thanks!
yAYa + 3 + 1 又一位深藏不露的大牛~谢谢师傅~
爱飞的猫 + 3 + 1 用心讨论,共获提升!
qaz789a4 + 2 + 1 热心回复!
笙若 + 1 + 1 谢谢@Thanks!
独行风云 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
a417765659 + 1 + 1 大佬,很强大。努力学习中。。。感谢感谢!
yicong135 + 1 + 1 好顶赞
tchivs + 3 + 1 谢谢@Thanks!
wwh1004 + 3 + 1 非常好!!!
cyw1912911875 + 1 + 1 我很赞同!
非凡公子 + 1 + 1 谢谢@Thanks!
lies2014 + 2 + 1 用心讨论,共获提升!
netle8 + 1 + 1 热心回复!
yyhf + 1 总结:一个程序员把保护自己程序的程序破解了
错的是世界 + 1 + 1 不明觉厉
hjw52 + 1 + 1 谢谢@Thanks!
jgs + 1 + 1 谢谢@Thanks!
黑的思想 + 2 + 1 用心讨论,共获提升!
Monitor + 2 + 1 鼓励转贴优秀软件安全工具和文档!
3yu3 + 1 + 1 我很赞同!
sunnylds7 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
wmsuper + 3 + 1 谢谢@Thanks!
chen4321 + 1 我很赞同!
涛之雨 + 1 + 1 用心讨论,共获提升!
hxz303 + 1 + 1 又一篇精华帖诞生!很详细!!很有责任感!!!很NB 拜见了!
anntonny + 1 + 1 用心讨论,共获提升!
Ravey + 1 + 1 谢谢@Thanks!
FleTime + 1 膜拜大佬 不明觉厉
CrazyNut + 3 + 1 膜拜大佬 不明觉厉

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

夜泉 发表于 2019-5-18 03:00
大佬,你能到这一步的水平到底学了那些东西呢?我也想走分析这些东西
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 01:51
当我在读之前这个帖子还没加精……读完就加精了
这个速度有多快啊
 楼主| JemmyloveJenny 发表于 2019-5-18 02:02
艾莉希雅 发表于 2019-5-18 01:51
当我在读之前这个帖子还没加精……读完就加精了
这个速度有多快啊

我也才发现……H大深夜不睡在加精!
feob 发表于 2019-5-18 06:06
收藏了,感谢分享
kunsun 发表于 2019-5-18 06:32
佩服佩服佩服
小航哥 发表于 2019-5-18 07:11
先前排留个名字!
Ravey 发表于 2019-5-18 07:54
如果 Runtime 是多态变形,要自动还原应该会很难
chen4321 发表于 2019-5-18 07:56
厉害了,看天书一样看完,收藏了
hjw52 发表于 2019-5-18 09:49
厉害了,谢谢分享!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-23 16:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表