一、加壳的宏观流程
从一个高层次的角度看,软件加壳是一个将原始程序(Original Program)封装成壳程序(Packed Program)的过程,这个壳程序可以自我解密并运行原始程序。
这个过程可以分解为以下几个主要步骤:
- 分析原始程序:加壳工具读取原始可执行文件,分析其结构,找到程序入口点(OEP)、代码段、数据段、导入表等关键信息。
- 压缩/加密:使用压缩算法或加密算法,处理原始程序的代码和数据。
- 构建壳代码:编写一段小的引导程序,也就是Loader(加载器)。这段代码是加壳后程序运行的第一个部分。
- 组合生成新文件:将 Loader 代码、压缩/加密后的原始数据、以及一些必要的元数据(如原始入口点地址、加密密钥等)组合成一个新的可执行文件。
- 修改文件头:将新文件的入口点指向 Loader 的起始地址,并调整文件头信息以确保操作系统能正确加载。
二、核心原理与伪代码示例
下面我们将通过伪代码,模拟加壳和解密(脱壳)的核心逻辑。
1. 模拟加壳过程
假设我们有一个非常简单的原始程序,其功能就是打印一句话。
// 原始程序 (Original Program) 的伪代码
// 原始入口点 OEP (Original Entry Point)
void OriginalProgram_Main() {
printf("Hello from the original program!");
// ... 其他程序逻辑 ...
}
现在,我们编写一个加壳器来处理它。
// 加壳器 (Packer) 的伪代码
// 这是一个在加壳时执行的程序,不是壳本身
void PackAndProtect(char* original_file, char* packed_file) {
// 1. 读取原始程序文件
byte* original_data = ReadFile(original_file);
// 2. 找到原始程序的入口点(OEP)
// 假设OEP地址是0x401000
DWORD original_entry_point = GetEntryPointFromHeader(original_data);
// 3. 压缩/加密原始程序的代码和数据
// 这里我们用一个简单的XOR加密作为示例
byte* encrypted_data = new byte[original_data.size];
byte key = 0xAA;
for (int i = 0; i < original_data.size; i++) {
encrypted_data[i] = original_data[i] ^ key;
}
// 4. 构建解密 Loader 代码
// 这段代码将成为加壳后程序的新入口点
byte* loader_code = GenerateLoaderCode(
encrypted_data,
original_entry_point,
key
);
// 5. 将Loader和加密数据组合成新文件
CreateNewExecutableFile(packed_file, loader_code, encrypted_data);
// 6. 修改新文件的文件头
// 将新文件的入口点设置为Loader的起始地址
SetEntryPointInHeader(packed_file, GetLoaderStartAddress());
}
2. 模拟运行加壳程序(解密/脱壳过程)
当用户运行我们加壳后的 packed_file.exe 时,操作系统会首先加载并执行其中的Loader代码。
// 加壳后程序运行时的伪代码
// 这是真正被执行的 Loader 代码
void Loader_Main() {
// 1. 获取 Loader 自己的内存地址
// 假设 Loader 在内存中的地址是 0x100000
// 2. 找到加密后的原始数据和元数据
// 假设加密数据紧跟在Loader代码之后
byte* encrypted_data = GetEncryptedDataPointer();
DWORD original_entry_point = GetOriginalEntryPointFromMetadata();
byte key = GetKeyFromMetadata();
// 3. 在内存中解密数据
// 动态申请一块新的内存空间来存放解密后的代码
byte* decrypted_data = VirtualAlloc(NULL, encrypted_data.size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
for (int i = 0; i < encrypted_data.size; i++) {
decrypted_data[i] = encrypted_data[i] ^ key;
}
// 4. 重定位(非常关键的一步,这里简化处理)
// 实际过程中,需要修复被加密程序中所有的硬编码地址
// 5. 将执行流跳转到原始程序的入口点(OEP)
// Loader 的使命完成,将控制权交给原始程序
JumpToAddress(decrypted_data + original_entry_point_offset);
}
三、关键技术点的深入解析
1. 程序入口点(OEP)的查找与保存
- 在加壳时,加壳器必须准确地找到原始程序的
OEP 。这通常通过解析 PE (Portable Executable) 文件头来实现。ImageBase + AddressOfEntryPoint 就是 OEP 的虚拟内存地址。
- 在加壳后,
OEP 的地址信息会被加密或混淆,作为元数据保存在壳的代码中。解密 Loader 必须能够找到这个地址,才能在解密完成后正确跳转。
2. 内存中的解密与重定位
这是加壳的核心。在磁盘上,程序是加密或压缩的,但它在内存中必须是可执行的。
- 解密后,代码和数据会被写入到内存中一个新的地址。
- 重定位的作用是修复所有因地址变化而产生的错误引用。例如,一个
CALL 指令的参数是另一个函数的地址,如果这个地址没有被修正,程序就会崩溃。
3. 反调试与反虚拟机
高级的壳会采用多种手段来阻止逆向分析。
- 反调试:通过检测
IsDebuggerPresent() 、NtQueryInformationProcess() 或 FindWindow() 等 API 函数,或检查调试器设置的断点(例如 INT 3 指令)来判断是否被调试。
- 反虚拟机:通过检测虚拟机的硬件特征(如 VMware 的
VMMouse.sys 驱动、CPUID 指令返回的特定信息)来判断是否运行在虚拟机中。
当壳检测到这些环境时,它通常会启动反制措施,例如跳转到无限循环、故意崩溃、或者直接退出,从而让逆向分析无法继续。
|