加密壳
1、加密壳原理
1、课程目标
- 理解加密壳的概念和作用
- 掌握加壳的基本原理和流程
- 了解常见的加壳技术
- 理解壳与免杀的关系
2、名词解释
| 名词 | 全称 | 解释 |
|---|---|---|
| Packer | 加壳器 | 对PE文件进行加密/压缩的工具 |
| Shell/Stub | 壳/存根 | 加载原始代码的代码段 |
| OEP | Original Entry Point | 原始入口点 |
| UPX | Ultimate Packer for eXecutables | 常见压缩壳 |
| VMProtect | - | 虚拟机保护壳 |
| Themida | - | 商业保护壳 |
| Unpacker | 脱壳器 | 还原加壳程序的工具 |
| IAT重建 | Import Address Table Rebuild | 脱壳后恢复导入表 |
3、使用工具
- Visual Studio 2022
- x64dbg
- PE-bear
- CFF Explorer
- DIE (Detect It Easy)
4、技术原理
4.1、加壳的作用
1. 保护代码
- 防止逆向分析
- 保护核心算法
- 防止破解
2. 压缩体积
- 减小文件大小
- 加快传输速度
3. 免杀
- 改变文件特征
- 绕过静态检测
- 加密恶意代码
4. 反调试
- 检测调试器
- 阻止动态分析
4.2、加壳流程
原始PE文件 加壳后PE文件
+-------------+ +------------------+
| DOS Header | | DOS Header |
+-------------+ +------------------+
| PE Header | | PE Header (修改) |
+-------------+ +------------------+
| .text | 加密 | .packed (加密后 |
| .data | --------> | 的原始区段) |
| .rdata | +------------------+
| .rsrc | | .shell (壳代码) |
+-------------+ +------------------+
| OEP: 0x1000 | | EP: 壳入口 |
+-------------+ +------------------+
运行时流程:
1. 执行壳代码(Shell)
2. 解密原始区段到内存
3. 恢复导入表
4. 执行重定位
5. 跳转到OEP执行原始代码
4.3、壳的分类
1. 压缩壳
- UPX, ASPack, PECompact
- 主要目的:减小体积
- 相对容易脱壳
2. 加密壳
- ASProtect, Armadillo
- 主要目的:保护代码
- 中等难度脱壳
3. 虚拟机壳
- VMProtect, Themida, Code Virtualizer
- 主要目的:最强保护
- 极难脱壳
4. 自定义壳
- 针对特定需求开发
- 免杀效果好
- 本章重点
5、代码实现
5.1、壳的基本结构
// 壳需要的基本信息
typedef struct _SHELL_CONFIG {
DWORD originalOEP; // 原始入口点RVA
DWORD originalImageBase; // 原始基址
DWORD packedDataRVA; // 加密数据RVA
DWORD packedDataSize; // 加密数据大小
DWORD key; // 解密密钥
DWORD sectionCount; // 原始区段数
// 原始区段信息
struct {
DWORD virtualAddress;
DWORD virtualSize;
DWORD characteristics;
} sections[16];
} SHELL_CONFIG;
// 壳代码入口(伪代码)
void __stdcall ShellEntry() {
// 1. 获取当前基址
LPBYTE imageBase = GetCurrentImageBase();
// 2. 获取必要的API
HMODULE kernel32 = GetKernel32Base();
FARPROC pGetProcAddress = GetFunctionAddress(kernel32, "GetProcAddress");
FARPROC pLoadLibrary = GetFunctionAddress(kernel32, "LoadLibraryA");
FARPROC pVirtualProtect = GetFunctionAddress(kernel32, "VirtualProtect");
// 3. 定位配置信息
SHELL_CONFIG* config = GetShellConfig(imageBase);
// 4. 解密原始代码
DecryptPackedData(imageBase, config);
// 5. 恢复区段属性
RestoreSectionProtection(imageBase, config);
// 6. 处理导入表
ResolveImports(imageBase);
// 7. 执行重定位(如果需要)
if ((ULONGLONG)imageBase != config->originalImageBase) {
ProcessRelocations(imageBase, config->originalImageBase);
}
// 8. 跳转到OEP
LPBYTE oep = imageBase + config->originalOEP;
((void(*)())oep)();
}
5.2、简单XOR加密/解密
// XOR加密
void XorEncrypt(LPBYTE data, DWORD size, DWORD key) {
LPBYTE keyBytes = (LPBYTE)&key;
for (DWORD i = 0; i < size; i++) {
data[i] ^= keyBytes[i % 4];
}
}
// XOR解密(与加密相同)
void XorDecrypt(LPBYTE data, DWORD size, DWORD key) {
XorEncrypt(data, size, key); // XOR是对称的
}
// 多轮加密增加复杂度
void MultiRoundEncrypt(LPBYTE data, DWORD size, DWORD key, int rounds) {
for (int r = 0; r < rounds; r++) {
// 每轮使用不同的密钥
DWORD roundKey = key ^ (r * 0x12345678);
for (DWORD i = 0; i < size; i++) {
data[i] ^= ((LPBYTE)&roundKey)[i % 4];
data[i] = (data[i] << 3) | (data[i] >> 5); // 循环左移
}
}
}
// 多轮解密
void MultiRoundDecrypt(LPBYTE data, DWORD size, DWORD key, int rounds) {
for (int r = rounds - 1; r >= 0; r--) {
DWORD roundKey = key ^ (r * 0x12345678);
for (DWORD i = 0; i < size; i++) {
data[i] = (data[i] >> 3) | (data[i] << 5); // 循环右移
data[i] ^= ((LPBYTE)&roundKey)[i % 4];
}
}
}
5.3、获取当前模块基址
// 通过TEB获取基址(x86)
__declspec(naked) LPBYTE GetImageBase32() {
__asm {
mov eax, fs:[0x30] // PEB
mov eax, [eax + 0x08] // ImageBaseAddress
ret
}
}
// 通过call-pop获取EIP再计算
__declspec(naked) LPBYTE GetImageBaseByCode32() {
__asm {
call next
next:
pop eax
and eax, 0xFFFF0000 // 假设对齐到64KB
find_mz:
cmp word ptr [eax], 0x5A4D // MZ
je found
sub eax, 0x10000
jmp find_mz
found:
ret
}
}
// x64版本
LPBYTE GetImageBase64() {
// 通过GS段获取TEB
// GS:[0x60] = PEB
// PEB+0x10 = ImageBaseAddress
LPBYTE peb;
__asm {
mov rax, gs:[0x60]
mov peb, rax
}
return *(LPBYTE*)(peb + 0x10);
}
5.4、壳的编译和提取
// 壳代码需要满足的条件:
// 1. 位置无关(PIC)
// 2. 无导入表依赖
// 3. 无全局变量
// 4. 无C运行时库
// 编译设置:
// - 入口点设置为ShellEntry
// - 禁用所有安全检查 (/GS-)
// - 禁用C运行时 (/NODEFAULTLIB)
// - 关闭优化或使用特定优化
#pragma comment(linker, "/ENTRY:ShellEntry")
#pragma comment(linker, "/NODEFAULTLIB")
// 从编译后的壳程序提取代码段
BOOL ExtractShellCode(const char* shellPath, LPBYTE* code, DWORD* size) {
// 加载壳PE
HANDLE hFile = CreateFileA(shellPath, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
DWORD fileSize = GetFileSize(hFile, NULL);
LPBYTE fileData = (LPBYTE)malloc(fileSize);
DWORD read;
ReadFile(hFile, fileData, fileSize, &read, NULL);
CloseHandle(hFile);
// 解析PE
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)fileData;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(fileData + dos->e_lfanew);
PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);
// 找到.text区段
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (memcmp(sections[i].Name, ".text", 5) == 0) {
*size = sections[i].SizeOfRawData;
*code = (LPBYTE)malloc(*size);
memcpy(*code, fileData + sections[i].PointerToRawData, *size);
free(fileData);
return TRUE;
}
}
free(fileData);
return FALSE;
}
5.5、加壳工具框架
// 加壳器主要步骤
BOOL PackPE(const char* inputPath, const char* outputPath, DWORD key) {
// 1. 加载原始PE
LPBYTE originalPE = LoadFile(inputPath);
// 2. 解析原始PE信息
PE_INFO originalInfo;
ParsePE(originalPE, &originalInfo);
// 3. 加密原始代码段
EncryptSections(originalPE, &originalInfo, key);
// 4. 加载壳代码
LPBYTE shellCode;
DWORD shellSize;
ExtractShellCode("shell.exe", &shellCode, &shellSize);
// 5. 准备壳配置
SHELL_CONFIG config = {0};
config.originalOEP = originalInfo.entryPoint;
config.originalImageBase = originalInfo.imageBase;
config.key = key;
// ... 填充其他信息
// 6. 构建新PE
LPBYTE packedPE = BuildPackedPE(originalPE, &originalInfo,
shellCode, shellSize, &config);
// 7. 保存加壳后的PE
SaveFile(outputPath, packedPE);
return TRUE;
}
6、课后作业
-
分析UPX壳
- 使用x64dbg分析UPX壳的执行流程
- 找出OEP
- 理解解压算法
-
设计简单壳结构
- 设计壳的配置结构
- 设计加密方案
- 规划解密流程
-
研究壳检测
- 使用DIE检测各种壳
- 分析壳的特征
- 了解脱壳方法