加密壳

1、加密壳原理

1、课程目标

  1. 理解加密壳的概念和作用
  2. 掌握加壳的基本原理和流程
  3. 了解常见的加壳技术
  4. 理解壳与免杀的关系

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、课后作业

  1. 分析UPX壳

    • 使用x64dbg分析UPX壳的执行流程
    • 找出OEP
    • 理解解压算法
  2. 设计简单壳结构

    • 设计壳的配置结构
    • 设计加密方案
    • 规划解密流程
  3. 研究壳检测

    • 使用DIE检测各种壳
    • 分析壳的特征
    • 了解脱壳方法