THM免杀学习SignatureEvasion
Introduction
当面对先进的防病毒引擎或 EDR(端点检测和响应)解决方案时,对手可能很难克服特定的检测。 即使采用了混淆原则中讨论的一些最常见的混淆或规避技术,恶意文件中的签名可能仍然存在。
工具箱的装饰图像
为了对抗持久签名,攻击者可以单独观察每个签名并根据需要对其进行处理。
在这个房间里,我们将了解什么是签名以及如何找到它们,然后尝试按照不可知论的思维过程来打破它们。 为了更深入地研究和对抗启发式签名,我们还将讨论更高级的代码概念和“恶意软件最佳实践”。
学习目标
了解签名的起源以及如何在恶意代码中观察/检测它们
实施记录的混淆方法来破坏签名
利用基于非混淆的技术来破坏非面向功能的签名。
这个房间是混淆原则的继承者; 如果您尚未完成,我们强烈建议您在此房间之前完成。
在开始本课程之前,请先熟悉基本的编程逻辑和语法。 建议了解 C 和 PowerShell,但不是必需的。
Signature Identification
在开始破解签名之前,我们需要了解并确定我们要寻找的内容。 如防病毒简介中所述,防病毒引擎使用签名来跟踪和识别可能的可疑和/或恶意程序。 在此任务中,我们将观察如何手动识别签名开始的确切字节。
在识别签名时,无论是手动还是自动,我们都必须采用迭代过程来确定签名从哪个字节开始。 通过递归地将编译的二进制文件分成两半并对其进行测试,我们可以粗略估计字节范围以进一步研究。
我们可以使用本机实用程序 head、dd 或 split 来拆分已编译的二进制文件。 在下面的命令提示符中,我们将使用 head 来查找 msfvenom 二进制文件中存在的第一个签名。
分割后,将二进制文件从您的开发环境移至具有您想要测试的防病毒引擎的计算机。 如果出现警报,请移至拆分二进制文件的下半部分并再次拆分它。 如果没有出现警报,请移至分割二进制文件的上半部分并再次分割它。 继续这种模式,直到你无法确定该去哪里; 这通常发生在千字节范围内。
一旦达到不再准确分割二进制文件的程度,您可以使用十六进制编辑器查看存在签名的二进制文件的末尾。
0000C2E0 43 68 6E E9 0A 00 00 00 0C 4D 1A 8E 04 3A E9 89 Chné.....M.Ž.:é‰ |
我们有签名的位置; 它的可读性将由工具本身和编译方法决定。
现在……没有人愿意花几个小时来回尝试追踪坏字节; 让我们自动化它! 在下一个任务中,我们将研究一些 FOSS(自由开源软件)解决方案,以帮助我们识别编译代码中的签名。
Automating Signature Identification
上一个任务中显示的过程可能相当艰巨。 为了加快速度,我们可以使用脚本将其自动化,以在一定时间间隔内分割字节。 Find-AVSignature 将通过给定的时间间隔分割提供的字节范围。
Find-AVSignature
PS C:\> . .\FInd-AVSignature.ps1 |
该脚本减轻了很多手动工作,但仍然有一些限制。 虽然它比前一个任务需要更少的交互,但仍然需要设置适当的间隔才能正常运行。 该脚本也只会在将二进制文件放入磁盘时观察其字符串,而不是使用防病毒引擎的完整功能进行扫描。
为了解决这个问题,我们可以使用其他 FOSS(Free 和 Open-Source Software)工具,这些工具利用引擎本身来扫描 文件,包括 DefenderCheck、ThreatCheck 和 AMSITrigger。 com/RythmStick/AMSITrigger)。 在本任务中,我们将主要关注 ThreatCheck,并在最后简要提及 AMSITrigger 的使用。
ThreatCheck
ThreatCheck 是 DefenderCheck 的一个分支,可以说是三者中使用最广泛/最可靠的。 为了识别可能的签名,ThreatCheck 利用多个防病毒引擎来对抗分割编译的二进制文件,并报告它认为存在坏字节的地方。
ThreatCheck 不向公众提供预编译版本。 为了方便使用,我们已经为您编译了该工具; 它可以在所连接计算机的“C:\Users\Administrator\Desktop\Tools”中找到。
以下是 ThreatCheck 的基本语法用法。
ThreatCheck Help Menu
C:\>ThreatCheck.exe --help |
对于我们的使用,我们只需要提供一个文件和一个可选的引擎; 但是,在处理 AMSI(Anti-Malware Scan Interface)时,我们主要希望使用AMSITrigger,我们将在稍后讨论 在这个任务中。
ThreatCheck
C:\>ThreatCheck.exe -f Downloads\Grunt.bin -e AMSI |
就这么简单! 不需要其他配置或语法,我们可以直接修改我们的工具。 为了有效地使用这个工具,我们可以识别第一次发现的任何坏字节,然后递归地破坏它们并再次运行该工具,直到没有识别出签名。
注意:可能存在误报情况,其中该工具不会报告任何坏字节。 这需要你自己的直觉来观察和解决; 不过,我们将在任务 4 中进一步讨论这一点。
AMSITrigger
正如运行时检测规避中所述,AMSI 利用运行时,使签名更难以识别和解析。 ThreatCheck 也不支持某些文件类型,例如 AMSITrigger 所支持的 PowerShell。
AMSITrigger 将利用 AMSI 引擎并针对提供的 PowerShell 脚本扫描功能,并报告它认为需要发出警报的任何特定代码部分。
AMSITrigger 确实在其 GitHub 上提供了预编译版本,也可以在所连接计算机的桌面上找到。
以下是AMSITrigger的语法用法
AMSITrigger Help Menu
C:\>amsitrigger.exe --help |
对于我们的用途,我们只需要提供一个文件和报告签名的首选格式。
AMSI Trigger Example
PS C:\> .\amsitrigger.exe -i bypass.ps1 -f 3 |
在下一个任务中,我们将讨论如何使用从这些工具收集的信息来破解签名。
Static Code-Based Signatures
一旦我们发现了一个麻烦的签名,我们就需要决定如何处理它。 根据签名的强度和类型,可以使用混淆原则中所述的简单混淆来破坏签名,或者可能需要特定的调查和补救措施。 在此任务中,我们的目标是提供几种解决方案来修复函数中存在的静态签名。
分层混淆分类法 涵盖了作为 混淆方法 和 * 一部分的最可靠的解决方案 *混淆类**层。
混淆方法
混淆方法 | 目的 |
---|---|
方法代理 | 创建代理方法或替换对象 |
方法分散/聚合 | 将多个方法合并为一个或将一个方法分散为多个 |
方法克隆 | 创建方法的副本并随机调用每个 |
混淆类
混淆方法 | 目的 |
---|---|
类层次结构扁平化 | 使用接口为类创建代理 |
类拆分/合并 | 将局部变量或指令组传输到另一个类 |
删除修饰符 | 删除类修饰符(公共、私有)并使所有成员成为公共 |
查看上表,即使它们可能使用特定的技术术语或想法,我们也可以将它们分组为适用于任何对象或数据结构的一组核心不可知方法。
技术类拆分/合并和方法分散/聚合可以分组为拆分或合并任何给定 OOP (Object-O 定向P编程)功能。
其他技术(例如删除修饰符或方法克隆)可以分为删除或模糊可识别信息的总体概念。
分割和合并对象
拆分或合并对象所需的方法与混淆原则中介绍的串联目标非常相似。
这个概念背后的前提相对简单,我们正在寻求创建一个新的对象函数,它可以在保持以前的功能的同时打破签名。
为了提供更具体的示例,我们可以使用“GetMessageFormat”字符串中存在的 Covenant 中的众所周知的案例研究。 我们将首先了解该解决方案是如何实现的,然后将其分解并将其应用于混淆分类法。
原始字符串
下面是检测到的原始字符串
string MessageFormat = @"{{""GUID"":""{0}"",""Type"":{1},""Meta"":""{2},""IV"":""{3}"",""EncryptedMessage"":""{4}"",""HMAC"":""{5}""}}"; |
Obfuscated Method
下面是用于替换和连接字符串的新类。
public static string GetMessageFormat // Format the public method |
回顾一下这个案例研究,类分割用于为要连接的局部变量创建一个新类。 我们将在本任务后面以及整个实际挑战中介绍如何识别何时使用特定方法。
删除和隐藏可识别信息
删除可识别信息背后的核心概念类似于模糊处理原则中介绍的模糊变量名称。 在此任务中,我们更进一步,将其专门应用于任何对象(包括方法和类)中的已识别签名。
Mimikatz 中可以找到这样的示例,其中为字符串“wdigest.dll”生成警报。 这可以通过用在字符串的所有实例中更改的任何随机标识符替换字符串来解决。 这可以归类为方法代理技术下的混淆分类法。
这与混淆原则中讨论的几乎没有什么不同; 然而,它适用于特定情况。
利用您在整个任务中积累的知识,使用 AmsiTrigger 来混淆以下 PowerShell 代码片段以实现可视化签名。
$MethodDefinition = " |
充分混淆后,将代码片段提交到网络服务器“http://10.10.254.33/challenge-1.html”。 文件名必须保存为“challenge-1.ps1”。 如果正确混淆,警报弹出窗口中将出现一个标志
Static Property-Based Signatures
各种检测引擎或分析人员可能会考虑不同的指标而不是字符串或静态签名来促进他们的假设。 签名可以附加到多个文件属性,包括文件哈希、熵、作者、名称或其他可单独或结合使用的可识别信息。 这些属性通常用于 YARA 或 Sigma 等规则集中。
某些属性可能很容易操纵,而另一些属性可能更困难,特别是在处理预编译的闭源应用程序时。
此任务将讨论操作开源和闭源应用程序的文件哈希和熵。
注意:其他几个属性(例如 PE 标头或模块属性)可以用作指示符。 由于这些属性通常需要代理或其他措施来检测,因此我们不会在这个房间中介绍它们,以将重点放在签名上。
文件哈希值
文件哈希,也称为校验和,用于标记/标识唯一文件。 它们通常用于验证文件的真实性或其已知目的(恶意或非恶意)。 文件哈希值通常可以任意修改,并且会因对文件的任何修改而改变。
如果我们有权访问应用程序的源代码,我们可以修改代码的任意部分并重新编译它以创建新的哈希值。 该解决方案很简单,但如果我们需要预编译或签名的应用程序怎么办?
在处理签名或闭源应用程序时,我们必须采用位翻转。
位翻转是一种常见的加密攻击,它将通过翻转和测试每个可能的位直到找到可行的位来改变给定的应用程序。 通过翻转一个可行的位,它将更改应用程序的签名和哈希,同时保留所有功能。
我们可以使用脚本通过翻转每一位并创建新的变异变体(~3000 - 200000 个变体)来创建位翻转列表。 下面是 python 位翻转实现的示例。
import sys |
创建列表后,我们必须搜索文件的完整独特属性。 例如,如果我们对“msbuild”进行位翻转,则需要使用“signtool”来搜索具有可用证书的文件。 这将保证文件的功能不会被破坏,并且应用程序将保持其签名的属性。
我们可以利用脚本循环遍历位翻转列表并验证功能变体。 以下是批处理脚本实现的示例。
FOR /L %%A IN (1,1,10000) DO ( |
这种技术可能非常有利可图,尽管它可能需要很长时间,并且在发现哈希值之前只有有限的时间。 下面是原始 MSBuild 应用程序和位翻转变体的比较。
Entropy
在 IBM 中,熵被定义为“数据的随机性” 用于确定文件是否包含隐藏数据或可疑脚本的文件。” EDR 和其他扫描程序通常利用熵来识别潜在的可疑文件或对总体恶意评分做出贡献。
对于模糊的脚本来说,熵可能会产生问题,特别是在模糊可识别信息(例如变量或函数)时。
为了降低熵,我们可以用随机选择的英文单词替换随机标识符。 例如,我们可以将变量从“q234uf”更改为“nature”。
为了证明更改标识符的有效性,我们可以使用 [CyberChef](https://gchq.github.io/CyberChef/#recipe=Entropy(‘Shannon scale’)) 观察熵如何变化。
以下是标准英语段落的香农熵表。
Shannon entropy: 4.587362034903882
**
下面是带有随机标识符的小脚本的香农熵标度。
Shannon entropy: 5.341436973971389
**
根据所采用的 EDR,“可疑”熵值 ~ 大于 6.8。**
随着文件变大和出现次数增多,随机值和英文文本之间的差异将会变得更大。
请注意,熵通常不会单独使用,而仅用于支持假设。 例如,命令“pskill”和 hivenightmare 漏洞利用的熵几乎相同。
为了了解熵的作用,让我们看看 EDR 如何使用它来贡献威胁指标。
在白皮书中,针对高级持续威胁攻击向量的端点检测和响应系统的实证评估, ** SentinelOne** 显示由于高熵而检测 DLL,特别是通过 AES 加密。
Behavioral Signatures
混淆函数和属性可以通过最少的修改实现很多效果。 即使在破坏附加到文件的静态签名之后,现代引擎仍然可以观察二进制文件的行为和功能。 这给攻击者带来了许多无法通过简单的混淆来解决的问题。
正如防病毒简介中所述,现代防病毒引擎将采用两种常见方法来检测行为:观察导入和挂钩已知的恶意调用。 虽然导入(正如本任务中将介绍的那样)可以很容易地以最低的要求进行混淆或修改,但挂钩需要复杂的技术,超出了本房间的范围。 由于 API 调用的普遍存在,观察这些函数以及其他行为测试/考虑因素可能是确定文件是否可疑的重要因素。
在深入讨论重写或导入调用之前,我们先讨论一下传统上如何利用和导入 API 调用。 我们将首先介绍基于 C 的语言,然后在此任务中稍后简要介绍基于 .NET 的语言。
API 调用和操作系统本机的其他函数需要指向函数地址的指针和使用它们的结构。
函数的结构简单; 它们位于导入库中,例如“kernel32”或“ntdll”,它们存储 Windows 的函数结构和其他核心信息。
函数导入最重要的问题是函数地址。 获取指针可能看起来很简单,但由于 ASLR (Address Space Layout Randomization),函数地址是动态的并且必须找到 。
不是在运行时更改代码,而是使用 Windows 加载程序 windows.h
。 在运行时,加载程序会将所有模块映射到进程地址空间并列出每个模块的所有函数。 它处理模块,但是函数地址是如何分配的呢?
Windows 加载程序最关键的功能之一是IAT(Import Address Table)。 IAT将存储所有可以为函数分配指针的导入函数的函数地址。
IAT 存储在 PE (Portable Executable) 标头“IMAGE_OPTIONAL_HEADER”中,并由 Windows 加载程序在运行时填充。 Windows 加载程序从指针表获取函数地址,或者更准确地说,从 API 调用或 thunk 表 访问的 thunk。 有关 PE 结构的更多信息,请查看 Windows Internals room。
乍一看,API 被分配了一个指向 thunk 的指针,作为来自 Windows 加载程序的函数地址。 为了使这一点更加具体,我们可以观察一个函数的 PE 转储示例。
导入表可以提供对二进制文件功能的深入了解,这可能对攻击者不利。 但是如果我们的函数需要分配函数地址的话,如何才能防止我们的函数出现在IAT中呢?
正如简要提到的,thunk 表并不是获取函数地址指针的唯一方法。 我们还可以利用 API 调用从导入库本身获取函数地址。 此技术称为“动态加载”,可用于避免 IAT 并最大限度地减少 Windows 加载程序的使用。
我们将编写结构并为函数创建新的任意名称以采用动态加载。
在较高的层次上,我们可以将 C 语言中的动态加载分为四个步骤,
- 定义调用的结构
- 获取调用地址所在模块的句柄
3.获取调用的进程地址
4.使用新创建的调用
要开始动态加载 API 调用,我们必须首先在主函数之前定义调用的结构。 调用结构将定义调用函数可能需要的任何输入或输出。 我们可以在微软文档中找到特定调用的结构。 例如,可以在此处找到“GetComputerNameA”的结构。 因为我们将其实现为 C 中的新调用,所以语法必须稍作更改,但结构保持不变,如下所示。
// 1. Define the structure of the call |
要访问 API 调用的地址,我们必须首先加载定义它的库。 我们将在主函数中定义它。 对于任何 Windows API 调用,这通常是“kernel32.dll”或“ntdll.dll”。 下面是将库加载到模块句柄所需的语法示例。
// 2. Obtain the handle of the module the call address is present in |
使用之前加载的模块,我们可以获得指定API调用的进程地址。 这将直接在“LoadLibrary”调用之后发生。 我们可以通过将其与先前定义的结构一起转换来存储此调用。 以下是获取 API 调用所需的语法示例。
// 3. Obtain the process address of the call |
尽管这种方法解决了许多问题,但仍有一些注意事项必须注意。 首先,GetProcAddress
和LoadLibraryA
仍然存在于IAT中; 尽管不是直接指标,但它可能导致或加剧怀疑; 这个问题可以使用PIC(P位置I独立C代码)来解决。 现代代理还将挂钩特定功能并监视内核交互; 这可以使用 API 取消挂钩 来解决。
使用您在整个任务中积累的知识,混淆以下 C 代码片段,确保 IAT 中不存在可疑的 API 调用。
|
充分混淆后,将代码片段提交到网络服务器“http://10.10.254.33/challenge-2.html”。 文件名必须保存为“challenge-2.exe”。 如果正确混淆,警报弹出窗口中将出现一个标志。
Putting It All Together
正如本会议室和混淆原则 所重申的那样,没有一种方法是 100% 有效或可靠的。
为了创建更有效、更可靠的方法,我们可以结合本房间和上一个房间中介绍的几种方法。
在确定要开始混淆的顺序时,请考虑每种方法的影响。 例如,混淆一个已经被破坏的类更容易,还是破坏一个被混淆的类更容易?
注意:一般来说,您应该在特定签名破解后运行自动混淆或不太具体的混淆方法,但是,您不需要这些技术来应对此挑战。
考虑到这些注释,修改提供的二进制文件以满足以下规范。
- 不存在可疑的库调用
- 没有泄露函数或变量名
- 文件哈希与原始哈希不同
- 二进制绕过常见的反病毒引擎
注意:在考虑库调用和泄漏函数时,请注意二进制文件的 IAT 表和字符串。
|
充分混淆后,使用 GCC 或其他 C 编译器在您选择的 AttackBox 或 VM 上编译有效负载。 文件名必须保存为“challenge.exe”。 编译后,将可执行文件提交到位于http://MACHINE_IP/
.的网络服务器。如果您的有效负载满足列出的要求,它将运行并将信标发送到 提供服务器IP和端口。
注意:还必须更改提供的有效负载中的“C2Server”和“C2Port”变量,否则此挑战将无法正常工作,并且您将不会收到 shell 返回。
注意:使用 GCC 编译时,您需要添加“winsock2”和“ws2tcpip”的编译器选项。 可以使用编译器标志“-lwsock32”和“-lws2_32”包含这些库
如果您仍然遇到困难,我们在下面提供了解决方案的演练。
对于这个挑战,我们得到了一个不是我们创建的二进制文件。 我们的第一个目标是从逆向工程师的角度熟悉二进制文件。 有签名吗? 它的PE结构是什么样的? IAT中有什么重要信息吗?
如果您针对 ThreatCheck 或类似工具运行二进制文件,您会注意到它当前没有检测到,因此我们可以继续前进。
如果您按照任务 6 中的讨论检查二进制文件 IAT 表,您会注意到大约有 7 个独特的 API 调用可以指示该二进制文件的目标。
我们将演练动态调用一个 API 调用,然后希望您重复其余调用的步骤。
让我们看一下“WSAConnect”的Windows文档; 下面是从文档中获得的结构
int WSAAPI WSAConnect( |
我们现在可以重写它以满足结构定义的要求。
typedef int(WSAAPI* WSACONNECT)(SOCKET s,const struct sockaddr *name,int namelen,LPWSABUF lpCallerData,LPWSABUF lpCalleeData,LPQOS lpSQOS,LPQOS lpGQOS); |
现在我们需要导入存储调用的库。这只需要发生一次,因为所有调用都使用相同的库。
HMODULE hws2_32 = LoadLibraryW(L"ws2_32"); |
要使用API调用,我们必须获取指向该地址的指针。
WSACONNECT myWSAConnect = (WSACONNECT) GetProcAddress(hws2_32, "WSAConnect"); |
获得指针后,我们可以使用新指针更改所有出现的 API 调用。
mySocket = myWSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, 0); |
完成后,您的结构定义应类似于以下代码片段
typedef int(WSAAPI* WSASTARTUP)(WORD wVersionRequested,LPWSADATA lpWSAData); |
下面的代码片段定义了与上述结构相对应的所有所需的指针地址。
HMODULE hws2_32 = LoadLibraryW(L"ws2_32"); |
请注意,结构定义应该位于代码开头的任何函数之外。 指针定义应该位于“RunShell”函数的顶部
我们现在应该以适当的最佳实践随机化指针和其他变量名称。 我们还应该删除二进制文件中的任何符号或其他可识别信息。
一旦彻底混淆并删除信息,我们就可以使用“mingw-gcc”编译二进制文件。
x86_64-w64-mingw32-gcc challenge.c -o challenge.exe -lwsock32 -lws2_32 |