作为序言
在当今社会,分析过程中很少遇到纯粹干净的恶意软件。恶意软件代码通常会被修改,以阻碍研究人员对其进行分析和反编译。
篡改代码以阻碍分析的软件被称为混淆器。有些混淆器旨在改变机器代码,针对主要使用 C/Asm/Rust 开发的恶意软件,而另一些则修改由 .NET 编译器生成的 IL(中间语言)代码。
本系列文章将深入探讨.NET Reactor和SmartAssembly等混淆器所采用的现代技术,这些技术深受恶意软件开发者的青睐。我们将熟悉反混淆方法,并尝试开发自己的反混淆器或改造现有的反混淆器。此外,我们还将探索一些旨在对抗这些反混淆器的工具(如果有)。
我们的目标是尽可能简化内容,确保即使是对 .NET 有基本了解的初学者也能掌握这些概念。然而,我们要求读者具备恶意软件分析工具和概念的基础知识。具备混淆代码分析经验者优先。
你准备好踏上这段旅程了吗?让我们开始吧。
介绍
要真正理解混淆器,我们应该像开发者一样思考。这有点像网络安全中的红蓝队:要想防御得当,就必须了解攻击方式。那么,让我们尝试构建一个简单的混淆器。
简单的混淆器
它应该是什么样子的?
首先,让我们看一下我们将要实验的程序https://github.com/anyrun/blog-scripts/blob/main/NetObfuscatorExample/Example1/Program.cs:
“Example1”的源代码
是的,只有几行代码,一个变量,以及一个唯一的函数“ProtectMe”,它会打印“No_On3_Can_Find_My_S3cr37_Pass”。是不是很简单?
我们来看看.NET调试器“ DnSpy ”中的反编译代码:
DnSpy 中“Example1”的反编译代码
显然,任何人都可以通过使用适当的工具打开编译后的程序轻松找到密码,无需花费太多精力。那么,该如何保护我们的密码呢?
以下是我们将采用的一些策略,以加强对秘密的保护:
-
代理函数:将每个静态字符串放入具有疯狂名称的自己的函数中;
-
字符分解:将字符串分成单个字符;
-
数字转换:用数字值替换字符;
-
重数学:使用大量涉及大数字的数学运算;
-
CFG 混淆:使控制流变得复杂且难以理解。
让我们看看这些方法是否真的可以保证我们的秘密安全,并使任何试图破解它的人都难以破解。
代理函数
按照我们的策略,我们将所有字符串赋值操作移至单独的函数(代理)。此举使我们能够更好地控制这些单独的函数,并迫使研究人员在其他地方寻找每个字符串的定义。
我们的方法所期望的结果在Example2反编译清单中展示:
单独函数中的字符串赋值
为了实现这一点,我们需要修改 IL 代码。我们可以在下图中看到如何修改(在 DnSpy 中将视图更改为“使用 C# 的 IL”):
第一次迭代时修改 IL 代码
我们使用“ Dnlib ”库对已编译的“Example1”进行修改。此过程需要分几个步骤完成:
-
找到“ProtectMe”功能。
-
浏览所有指令并找到“ldstr”(加载字符串)的每个实例。
-
创建一个具有随机名称的新类和新函数。
-
在创建的函数主体中添加“ldstr”和“ret”指令。
-
用对新函数的调用替换原始的“ldstr”。
上述所有步骤均已在Example3中实现。我们不会在这里详细分析源代码,因为这有点枯燥,您可以自行完成。不过,我们会指出两个有趣的方面。
首先,看一下我们如何简单而优雅地使用“dnlib”创建新方法的主体:
第一次迭代时修改 IL 代码
其次,考虑一下随机函数名应该如何显示。它们必须完全由可打印字符组成吗?当然不是。为了让研究人员的工作更具挑战性,我们改用 UTF-32编码!
好吧,让我们看看我们得到了什么:
第一步混淆 的结果
看起来挺吓人的,对吧?我们可以看到,原始字符串现在隐藏在一个非常烦人的方法调用后面。现在,是时候进入下一部分了。
角色分析
即使我们隐藏了原始字符串,它仍然很容易被找到并读取。为了解决这个问题,我们需要修改密钥本身。因此,我们将密钥拆分成单个字符,这样我们稍后就可以打乱它们的顺序,从而以一种更难读取的形式呈现代码。
首先,检查Example4-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example4的反编译代码,你可以看到我们的目标:
上面的屏幕截图显示字符串是逐字节推送到堆栈的,这与前面的示例不同,前面的示例是一次性推送整个字符串。
其次,看一下示例5-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example5,我们对混淆器做了一些小改动,添加了“SplitStringByCharToInstr”函数。该函数会拆分字符串并生成相应的IL代码。改进后的效果如下图所示:
按字节构造字符串
看来 DnSpy 足够强大,能够解析 IL 代码并以人类可读的形式呈现分割后的字符串。我们将在接下来的章节中深入探讨这一点。目前,我们先从另一个角度来探讨这项改进。
让我们比较一下混淆前后“string”命令的输出:
文件中缺少整个字符串
终于到了!字符串从文件中消失了。这或许是混淆器如何绕过签名检测的一个很好的例子。
现在,让我们继续解决万能的 DnSpy。
数值转换
到目前为止,我们隐藏密码的尝试还没有真正奏效。但如果我们将符号替换成相应的数字表示呢?让我们看看示例 6-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example6,看看这种方法的实际效果:
数字转换的实际应用
上面的源代码和反编译代码显示没有可见的字符,这证明了该方法的有效性。在这种方法中,每个字符都用一个数字表示,然后“Conv.U2”指令将其转换为无符号整数。随后,我们将该数字转换回字符串并将其附加到最终结果中。
为了利用这项技术,我们需要对混淆器进行一些调整。修改后的结果如示例 7所示,其中我们集成了“MaskCharsWithNumVal”函数来执行此转换。下图展示了结果:
数字转换击败了 DnSpy,但失败了 IlSpy
我们看到的图像表明,尝试读取 DnSpy 反编译的代码可能会有点麻烦。它会把所有内容都转换成数字,你必须使用 ASCII 表才能理解——如果你手动操作的话,肯定会有点麻烦。
另一方面,另一个出色的IL代码解析工具IlSpy做得相当漂亮。它似乎识破了我们的诡计,把那些数字重新转换成字符,使其更易于阅读。此外,如果你查看文件的二进制视图,你会发现我们的秘密仍然在那里,只是稍微分散了一些:
在二进制视图中查找密码
现在,让我们进入下一章,彻底抹去所有角色的痕迹。
繁重的数学
首先,看一下以下数学表达式:
将字符转换为数学表达式
以上两个表达式都展示了字符“A”的数字表示。此外,它还表明任何数字都可以写成数学表达式。更妙的是,用数学表达式来表达任何数字的方法数不胜数。那么,为什么不发挥创意,用随机生成的数学表达式来表示我们的字符呢?
就像我们之前所做的那样,现在让我们看一下示例 8-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example8以了解预期结果:
数学表达式的 IL 代码示例
反编译后的代码看起来很丑,不是吗?这正是我们需要的!
因此,我们应该开发一个需要两个参数的函数:
-
我们想要达到的 目标数字;
-
强度——我们可以使用的最大ADD / SUB / XOR运算次数,以达到目标数字。
该函数将遍历该方法的所有指令,并修改那些涉及“int32”数字的指令,用一组新的混淆指令替换它们。此外,所有数字以及数学运算都应随机生成。
为了满足上述要求,我们修改了混淆器,并将其显示在Example9-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example9中。我们来看一下它的执行结果:
经过大量数学混淆后的结果
终于搞定了!你能解读一下上面截图里的内容吗?我们来分享一下我们对混淆器进行调整的一些想法。
首先,敏锐的读者可能会注意到,我们没有将异或运算与加/减运算混合使用。这是因为它们的表达式优先级更高,因此需要更复杂的逻辑。实际上,我们随机选择对每个数字使用哪种运算。
接下来,我们巧妙地利用临时变量来绕过 IlSpy。在计算数学表达式之前,我们首先将初始随机值存储在这个临时变量中。这一步至关重要,因为 IlSpy 拥有一个精巧的数学合成器,可以立即计算出常量之间数学运算的结果。因此,如果没有这个技巧,反编译后的代码就会直接暴露我们试图隐藏的字符。
最后,我们做了一点小改动,将 int 随机转换为 uint。这个小小的改动足以让好奇的研究人员更加愤怒。
尽管密码现在更难破解了,但我们反编译后的代码仍然是线性的,稍加努力还是可以读懂的。所以,让我们更进一步,再添加一层混淆。
CFG混淆
简单来说,所有控制流图(CFG)混淆都可以归结为:
-
将代码分成基本块;
-
随机打乱它们;
-
连接这些块,以便执行代码的结果保持不变。
为了理解将代码分解为基本块的思路,我们来回顾一下Example4-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example4。我们将代码分解为基本块,对它们进行重新排序,然后看看下图中会发生什么:
拆分和混合基本块的示例
上图展示了代码重排如何使解密变得更加困难。然而,这其中有一个明显的陷阱:如果我们尝试运行这段新的重排代码,最终会得到错误的密码,因为指令的顺序现在错了。那么,我们该如何以正确的方式运行它们呢?
为了以正确的顺序执行打乱顺序的代码,我们需要一种方法来引导其执行。这需要通过添加控制结构或标记来重建原始的控制流。请看下图,我们分析了Example10-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example10:
控制结构和引导控制流的标记示例
上面的示例演示了一种控制 shuffled 代码执行的方法。它的特点如下:
-
一个无限的‘while’循环,不断地将我们带到‘switch’;
-
直接选择后续代码块的“switch”语句;
-
“num”变量充当标记,保存着起始和下一个块的选择;
-
switch 语句中的默认情况,用于退出无限循环。
看起来我们已经成功地将代码拆分成基本块并进行了打乱。我们还学习了如何使用 switch 语句和标记来控制执行。但我们需要记住,我们处理的是IL,而不是源代码。现在,问题来了:如何将IL代码拆分成基本块?
据我们所知,IL 虚拟机使用求值栈进行操作。这意味着在执行操作之前,我们首先需要将所需的值压入栈中。例如,要执行异或运算,我们将两个值压入栈中,进行异或运算,然后将结果压回栈顶。
考虑到以上情况,我们可以大致认为堆栈的初始状态为空,即堆栈指针为空。在操作过程中,堆栈指针会从此初始状态变为非空。操作完成并保存结果后,堆栈将恢复到其初始空状态。因此,我们似乎可以根据堆栈的初始值(特别是在堆栈指针为空的位置)将IL代码拆分为基本块。
让我们检查一下最新混淆示例中的IL代码。这里,我们根据堆栈值划分了指令:
根据堆栈值将 IL 指令拆分为基本块的示例
需要注意的是,代码块并不一定与代码行完全对应。一行反编译代码完全有可能包含多个基本代码块,如下例所示:
反编译代码的两行对应三个基本块(见上图)
到目前为止,我们已经讨论了所有内容,现在可以开始开发 CF 混淆器了。示例 11-https://github.com/anyrun/blog-scripts/tree/main/NetObfuscatorExample/Example11中已经完成了这一工作。其执行结果如下图所示:
控制流混淆的结果
Example11 的详细代码分析留给你作为家庭作业。不过,我们先强调一下一个需要注意的关键点。
我们介绍的 CF 混淆方法非常基础。它没有考虑异常块、前缀或条件表达式。事实上,它忽略了很多方面。我们的目的仅仅是以直观的方式演示它的工作原理。因此,这种方法很可能无法有效地用于复杂的方法,并且需要更复杂的开发。
攻击简单的混淆器
断点
我们付出了巨大的努力,试图掩盖我们的秘密,避免被分析,并用复杂的代码恐吓研究人员。我们甚至在某种程度上成功创建了一种充满复杂数学和令人困惑的控制流的方法。
然而,我们所有建立“强”保护措施的努力在实时执行面前都显得苍白无力。为了绕过我们的安全措施,只需在返回处或目标函数之后设置断点,并读取其结果即可,如下图所示:
混淆器因一个断点而惨遭淘汰
内存转储
内存转储是发现隐藏字符串最有效的方法之一,因为.NET 编译器通常会留下大量解密字符串的痕迹。使用 ProcessHacker 进行内存扫描的结果证明了这一点,共发现了 24 条结果:
内存转储揭示了我们的秘密
De4dot
我们的老朋友“De4dot”仍然可以派上用场。只需“一键”,它就能彻底删除CFG和数学混淆:
旧版 De4dot 成功反混淆了代码
除此之外,它还提供了另一个强大的功能,即直接执行混淆的方法并用字符串文字替换代理调用:
> de4dot.exe Example1_obf.exe --strtyp emulate --strtok 0x06000004
不幸的是,对于我们的混淆器来说,结果却是惊人的:
De4dot 用字符串文字替换了代理调用
最后的想法
在本系列文章的这一部分中,我们开发了自己的简单混淆器,并使用各种攻击技术彻底拆解了它的概念。这是否意味着简单的混淆器天生就很脆弱?在某种程度上,是的。但这是否意味着我们使用的技术已经过时,应该被丢弃?绝对不是。这些方法仍然在现代混淆器中使用,尽管形式更加复杂。这是否意味着我们现在对最常见的混淆技术有了更好的理解,并准备好深入剖析现代混淆器的核心?绝对是这样的。现在,我们已经做好准备,可以深入探索混淆器的世界了。
在即将推出的第二部分中,我们将探索更多保护代码的方法。我们将研究混淆器如何对抗断点、De4dot 和内存转储。我们还将研究如何突破它们的防御机制,从而理解代码以及其他许多有趣的方面。
原文始发于微信公众号(Ots安全):深入探究 .NET 恶意软件混淆器:第一部分
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论