加密pe/elf需在内存中解密代码段并修复重定位与iat/eat,而非直接磁盘加密;loader stub须纯汇编、禁crt、直连系统调用,且字符串需异或加密;调试时应分步验证,避免反调试干扰。

加密代码段前必须先理解 PE/ELF 的内存映射机制
直接对磁盘上 .text 段加密后,Windows 加载器或 Linux ld-linux 会因校验失败或页保护冲突而拒绝加载。真正可行的路径是:保留原始入口点(OEP)被重定向到壳代码,由壳在内存中申请可执行页、解密原始代码段、修复重定位、跳转执行。这意味着你不是“加密整个 EXE”,而是把原始代码段提取出来加密,再用一个独立的 loader stub 替换原文件头和入口。
常见错误现象:STATUS_ACCESS_VIOLATION(Windows)、Segmentation fault (core dumped)(Linux),往往是因为解密后没调用 VirtualProtect 或 mprotect 设置 PAGE_EXECUTE_READWRITE / PROT_EXEC | PROT_READ | PROT_WRITE;或者没还原重定位表(ASLR 启用时必崩)。
- Windows 下需解析
IMAGE_NT_HEADERS、遍历IMAGE_SECTION_HEADER找到.text的VirtualAddress和SizeOfRawData - Linux ELF 需读取
Elf64_Phdr中类型为PT_LOAD且含PF_X标志的 segment,其p_vaddr对应代码虚拟地址 - 加密时只处理原始节/段内容,不碰 DOS stub、NT headers、program headers 本身——这些要留给壳 loader 解析用
loader stub 必须用纯汇编或极简 C++ 编写,且禁用 CRT 和全局对象
壳的 loader 部分一旦被链接进目标程序,就会引入依赖(如 __main、atexit、CRT 初始化代码),导致解密逻辑还没跑就崩溃。所以 loader stub 要求:无函数调用(避免 PLT/GOT)、无栈帧(避免 push rbp)、手动管理寄存器、所有系统调用直连 syscall 号(Linux)或通过 kernel32.dll 导出地址(Windows)。
例如 Windows x64 loader 中获取 VirtualAlloc 地址,不能用 GetProcAddress(LoadLibrary("kernel32.dll"), "VirtualAlloc") —— 这需要字符串和 API 调用链。正确做法是硬编码已知的 kernel32 基址偏移(不推荐)或从 PEB 手动遍历模块导出表(PEB->Ldr->InMemoryOrderModuleList)查找 kernel32.dll,再解析其 EAT 查找 VirtualAlloc RVA。
- Linux 下推荐用
syscall(SYS_mmap, ...)直接分配内存,避免libc介入 - 所有字符串(如 DLL 名、函数名)必须异或加密,运行时解密后再用 —— 否则壳会被静态扫描轻易识别
- loader 入口不能是
main或WinMain,而应设为自定义入口(/ENTRY:loader_start链接选项),并关闭默认库(/NODEFAULTLIB)
动态解密后必须修复重定位和 IAT/EAT
现代可执行文件几乎都启用 ASLR 和延迟导入。如果只解密代码段但没修正 IMAGE_BASE_RELOCATION 表(Windows)或 .rela.dyn(Linux),程序访问全局变量或调用外部函数时地址全错。更隐蔽的问题是:即使你静态链接了所有依赖,C++ 异常表(.pdata / .eh_frame)若未同步解密和重定位,throw 会直接 crash。
典型遗漏点:IMAGE_THUNK_DATA 数组指向的 IAT 条目仍为加密前的原始地址,loader 解密代码段后,若未重新填充 IAT(或绑定延迟导入),第一次调用 printf 或 CreateFileA 就跳到乱码地址。
- Windows 下必须遍历
IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_BASERELOC],对每个 relocation block 执行地址修正(注意 64 位是IMAGE_REL_BASED_DIR64) - Linux 下需解析
.dynamic段中的DT_RELACOUNT和DT_RELA,逐条应用R_X86_64_RELATIVE类型重定位 - C++ 成员函数虚表指针(vtable)也属于重定位范畴,若类对象在解密前已构造,vptr 指向的仍是加密区 —— 必须确保对象构造发生在解密之后
调试与反调试会让壳行为完全不可控
加壳后程序在调试器下大概率无法单步——因为断点(INT3)插入的是解密前的加密数据,而 CPU 执行的是解密后的内存镜像。更麻烦的是,主流反调试技术(IsDebuggerPresent、CheckRemoteDebuggerPresent、ptrace(PTRACE_TRACEME))一旦触发,loader 可能直接退出,导致你连解密逻辑都看不到。
实际调试建议:先用 VirtualAlloc 分配一页内存,把原始未加密代码段 memcpy 过去,手工 patch 入口跳过去,确认 loader 流程无误;再逐步加入解密逻辑;最后才启用反调试。不要一上来就堆满 OutputDebugStringA + GetTickCount64 校验。
- Windows 下
SetThreadContext修改Rip绕过 INT3 是可行的,但仅限本地调试;远程进程注入调试器基本无效 - Linux 下
ptrace可拦截mmap返回值,但 loader 若用brk或MAP_ANONYMOUS | MAP_STACK分配,就可能绕过监控 - 最易忽略的一点:C++ 异常处理结构(
__CxxFrameHandler3)依赖 .pdata/.eh_frame 的完整性,加密这些节会导致catch块永远不触发 —— 它们必须和代码段一起解密,且重定位顺序不能错
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











