伪装入口与内存中的隐藏 PE:Hideit 样本逆向分析记录
1. 缘起:为什么“入口”值得单独分析
恶意样本分析和 CTF Reverse 题目里经常遇到一类情况:main 函数看起来很正常,但程序真正的行为根本不在 main 里。攻击者或出题人会把真正要执行的代码藏起来,可能是加壳、可能是进程镂空、也可能是把一段 shellcode 复制到内存后再执行。
这次分析的 Hideit 是一道 CTF Reverse 题(湖湘杯 2021 Reverse),题目给出的提示只有一句话:“真正的入口往往不是表面上看到的那样。” 运行后程序只是打印:
1 | Let's start the game! |
表面上看是一个等待输入的小程序,但用 IDA 打开后,main 只有短短几行,真正干活的函数是它调用的另一个函数。顺着这个函数往下看,才发现程序把一整段 shellcode 复制到内存并直接执行——这才是它真正的入口。
这篇文章不打算只写这道题的求解过程,而是想整理一条可复用的“伪装入口”逆向分析方法链:从识别入口伪装,到 dump 内存中的隐藏模块,再到还原隐藏 PE 里的校验逻辑。
2. 先看表面:main 是个幌子
main 的反编译非常短:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
真正的动作在 sub_140001000:
1 | puts("Let's start the game!"); |
这段代码做了三件事:
- 分配一块可写内存(
VirtualAllocEx); - 把
unk_140003040处的 0xBC85 字节复制进去(WriteProcessMemory); - 把执行流直接跳到这块内存(
call lpAddress)。
也就是说,程序真正的入口是数据段里的一段 shellcode,而不是编译出来的 main。这种手法在真实恶意样本和 CTF Reverse 题目里都很常见:静态分析只看到“分配内存 + 复制数据”,真正的行为要等运行时才出现。
方法要点:遇到
VirtualAlloc(Ex) + WriteProcessMemory/Memcpy + 调用指针的组合,基本可以断定存在“内存中执行”的隐藏代码。不要停在main,要跟着这块内存走。
3. 从静态文件到运行时内存:dump 隐藏 PE
shellcode 本身是位置无关代码,直接线性反汇编会在 call 之后迷失——call 后面的字节并不是顺序执行的代码,而是加密/压缩的数据,线性反汇编会把它们当作指令继续解析,产生垃圾代码。Hideit 的 blob 开头是:
1 | e8 80 49 00 00 ; call $+0x4985 |
call 跳到 blob 内部的解包/加载代码,后面跟的才是被保护的数据。顺着跳转目标分析,会发现这段 shellcode 是一个反射式 loader:它解析并映射一个内嵌的 PE,然后跳到 PE 的入口点继续执行。
到这里,静态分析已经比较吃力了。更直接的办法是动态 dump:让程序跑起来,停在输入提示处,然后扫描进程内存,把映射出来的隐藏 PE 完整读出来。
具体做法:
- 用
CreateProcess启动样本,保持 stdin 不关闭,让它停在First secret here:; - 用
VirtualQueryEx遍历进程内存区域; - 在每个可读区域里搜索
MZ+ 合法PE\0\0头; - 命中后把整个内存镜像写到文件。
实践中会扫到很多系统 DLL,需要过滤:隐藏 PE 通常出现在低地址的私有内存区域,且镜像基址处直接就是 MZ(而不是在区域偏移处)。把 dump 下来的镜像交给 PE 解析工具,就能得到隐藏 PE 的节区、入口点和字符串。
方法要点:静态分析遇到“自解密/反射加载”时,动态内存 dump 往往比继续逆 loader 更快。扫描内存时优先找“MZ 位于区域起始处”的镜像,那通常是真正的 PE 基址。
4. 隐藏 PE 里的两段校验
Dump 出来的隐藏 PE 很小,静态 PE 工具看不到有效导入:导入项由外层反射式 loader 在运行时解析并填充,静态分析只能看到空/无效的导入表结构,看不到实际 API 绑定。程序逻辑是一个“找秘密”的小程序:
1 | First secret here: <-- 读取输入 |
隐藏 PE 对输入做了两段校验。
4.1 第一段:魔改 TEA
第一段校验是一个自定义的 TEA 变体:
- 密钥:
{ 0x72, 0x202, 0x13, 0x13 }(4 个 32 位字,0x202是字值而非单字节) - 轮数:32 轮
- 常数:
0x9E3779B9(TEA 的 delta)
但它不是标准 TEA,混合函数被改过。从汇编里还原出来的轮函数大致是:
1 | def mix(x, s, k): |
程序把输入的前 8 字节当作两个 dword,加密后与固定密文比较。逆推得到的 8 字节明文是 dotitsit——它并不是最终 flag,而是第二段校验要用到的 nonce。
方法要点:看到
0x9E3779B9不要直接套标准 TEA 解密,先确认轮函数是否被魔改。用“自洽加解密”验证:先按汇编实现加密,再用逆轮解密,最后用“加密(解密(cipher)) == cipher”做自检。
4.2 第二段:ChaCha20
第二段校验是 ChaCha20。隐藏 PE 在 .rdata 里存了 32 字节密文,同时运行时通过常量相减还原出 32 字节 key:
1 | key = 0N3@aYI_M3l0dy_KurOm1_W_Suk1dqy0 |
用标准 ChaCha20 对密文解密,得到的就是最终秘密。这里有个很值得记录的坑:程序里存在一段被手工改写的 ChaCha20 轮函数,和标准 ChaCha20 并不完全一致,如果直接拿这段魔改函数当“解密 oracle”会对不上。但实际校验/密文生成等价于标准 ChaCha20,因此从程序里提取 key、nonce、密文后,用标准库反而能直接解出——魔改实现更像是干扰项或未参与该校验路径。
1 | from Crypto.Cipher import ChaCha20 |
注意:
PyCryptodome的ChaCha20默认支持 8 字节 nonce(原始 ChaCha20,64 位 nonce + 64 位计数器;IETF 变体为 12 字节 nonce)。这里 nonce 是 8 字节,不要误改成 12 字节。
方法要点:不要假设程序里的密码学实现一定和标准库一致。提取出 key/nonce/密文后,用标准算法独立验证;如果对不上,再回去检查程序是否改了轮函数、字节序或状态布局。
5. 可复用的“伪装入口”分析流水线
这次分析可以总结成一条通用流水线:
1 | 识别伪装入口 |
每一步都有对应的“为什么”:
- 识别伪装入口是为了不被
main带偏; - 动态 dump是为了绕过自解密/反射加载;
- 隐藏 PE 分析是为了拿到真正的逻辑;
- 独立实现算法是为了避免被程序自身的 bug 或魔改实现误导。
工具在这里只是加速器,真正兜底的是对 Windows 内存执行、PE 结构、反射加载和常见密码学算法的理解。
6. 与真实恶意样本分析 / ARK 检测的关联
6.1 对真实恶意样本分析的意义
Hideit 演示的“隐藏入口”本质上就是真实恶意软件常用的内存执行手法:
- 把 shellcode 或完整 PE 放在数据段/资源里;
- 运行时分配 RWX 内存并复制过去;
- 通过反射式 loader 映射执行;
- 进程内存里出现一个文件系统里不存在的 PE 镜像。
如果只分析磁盘文件,很容易漏掉这类内存执行手法;必须结合内存视角才能看到真正执行的代码。
6.2 对 SnowSword / ARK 检测的启发
这和 SnowSword 做隐藏驱动/进程检测的思路是相通的:
- 攻击者隐藏的是“文件里看不到,但内存里真实存在”的对象;
- 检测方不能只依赖单一数据源(模块列表、对象目录、磁盘文件);
- 内存扫描(找 MZ/PE 头、扫描可执行内存)是发现隐藏模块的重要补充。
Hideit 的隐藏 PE 在内存里有完整的 MZ/PE 头,如果检测工具具备“扫描内存中的 PE 镜像”能力,就能在运行时发现它。反过来,这也说明攻击者只要抹掉 PE 头或把内存改成非可执行属性,就能进一步提高检测门槛——检测与对抗始终是动态博弈。
7. 遇到的问题与局限
call跳板后面的数据不能线性反汇编:blob 开头是call $+0x4985,目标之后的字节不是顺序代码,直接线性反汇编会把数据解析成垃圾指令;需要先识别跳板,再跟着控制流走。- 内存里出现多个 MZ:进程里有大量系统 DLL,扫描时会命中很多假阳性。需要根据“镜像基址是否就是 MZ”“是否位于私有内存区域”等条件过滤。
- 静态看不到隐藏 PE 的有效导入:隐藏 PE 的导入项由反射式 loader 在运行时填充,静态 PE 工具只能看到空/无效的导入表结构,需要结合动态 dump 和代码分析。
- 程序内存在魔改 ChaCha20 干扰项:直接拿魔改函数当解密 oracle 会对不上;实际校验等价于标准 ChaCha20,提取 key/nonce/密文后用标准实现即可验证。
- 依赖运行时行为:如果样本检测到调试器/沙箱就不执行隐藏代码,动态 dump 可能失败;需要配合反反调试或直接静态还原 loader。
8. 总结与展望
Hideit 这道题的价值不在于“解出最终字符串”,而在于完整演示了 CTF Reverse 与真实恶意样本中常见的入口伪装手法:
1 | main → VirtualAlloc → WriteProcessMemory → shellcode → 反射加载隐藏 PE |
分析这类程序时,最重要的习惯是不要信任表面入口。看到内存分配和指针调用,就要意识到真正的代码可能在内存里。把“静态识别 + 动态 dump + 隐藏 PE 还原 + 算法独立验证”这条流水线跑通,比记住单个样本的答案更有价值。
后续可以继续完善的方向:
- 把内存 dump 流程脚本化:自动过滤系统模块、自动定位隐藏 PE、自动修复内存镜像;
- 增加对更多反射加载/进程镂空变体的识别(如清除 PE 头、手动映射、无 MZ 内存);
- 把“内存 PE 扫描”作为 SnowSword 的一个检测视角,和模块列表、对象目录交叉验证;
- 对自定义密码学实现做自动化比对:识别 TEA/ChaCha20 变体、提取轮函数参数。
参考
- ChaCha20 标准文档(RFC 8439)
- TEA / XTEA 算法参考实现
- Microsoft Learn:
VirtualAllocEx、WriteProcessMemory、PE 格式 - 湖湘杯 2021 Reverse 相关 writeup(用于交叉验证 key/nonce 提取结果)
- 本系列前篇:
第十二篇日报_PyInstaller样本逆向学习记录、日报_PEPEPE_已知明文攻击
相关项目与联系
项目SnowSword —— Windows 10/11 x64 内核分析与 Anti-Rootkit 工具,源码与发布包在 Gitee。
技术合作Windows 内核 / 驱动开发 / 逆向分析 / 兼容性开发,范围与边界见 技术合作。
联系2160752730@qq.com · QQ 2160752730 · 微信 lst_3258110
