复盘:一次"二进制文件提取失误"的经验总结
事件:通过RDP远程桌面从比赛Windows服务器提取恶意样本
systemutil.exe(809,472 字节)
失误:在命令行用记事本打开隐藏文件,然后另存为 .exe
后果:文件被"去 NUL"破坏,0x00全变空格;表面上完好,实则已废;第三层载荷与 C2 永久丢失
0. 一句话版本(可直接转发)
别用记事本打开 exe 再"另存为"。 记事本会把文件当文本处理,而文本通道装不下
0x00,于是 NUL 被一对一写成空格——文件大小看着没变,但内容已经废了。
取文件只做两件事:命令行copy/robocopy复制 + 两端哈希对齐。非要过文本通道,就先certutil -encode成 Base64 再搬。
1. 事件经过
| 时间线 | 动作 | 评价 |
|---|---|---|
| T0 | 文件在服务器上是隐藏属性 | 正常(可疑样本常隐藏) |
| T1 | 在命令行执行 notepad systemutil.exe 打开 |
❌ 用文本编辑器打开二进制 |
| T2 | 在记事本里"另存为 systemutil.exe" | ❌ 走文本通道写回 |
| T3 | 文件被拷到本地,扩展名 .exe 正常、大小 809,472 也"正常" |
⚠️ 无人校验 → 隐患潜伏 |
| T4 | 上机分析:e_lfanew 读出来是 0x20202080,PE 一堆"空格" |
才发现文件是坏的 |
关键点:T2 和 T4 之间没有任何校验环节,所以这个错误安静地穿过了整条流程。
2. 根因分析
2.1 技术机理
- 文本通道天生装不下
0x00:C 字符串以 NUL 作结束符;CF_TEXT/CF_UNICODETEXT也是;记事本的编辑控件同理。 - 这一层会把"不可打印字符"做净化,把
0x00一对一替换成0x20(空格)。 - 因为是一对一替换,文件长度完全不变,所以肉眼、
dir、资源管理器属性都看不出异常。
2.2 本次损坏的精确画像(已实测)
| 检查项 | 结果 | 结论 |
|---|---|---|
| 文件长度 | 809,472 → 809,472 | 没增没减 → 不是 UTF-8(会加 BOM)/ UTF-16(会翻倍)重编码 |
| 起始字节 | 仍是 4D 5A(MZ) |
开头没坏、没加 BOM |
| 除 NUL 外 | 逐字节原样 | 例:PNG 的 gAMA 值 0x0000B18F 里高位字节 B1 8F 原样保留,逆变换后 CRC 仍命中 |
| 被改的 | 0x00 → 0x20,共 39,095 处 |
0x0D/0x0A、0x01~0x1F(IL 操作码、元数据长度)全部正常 |
→ 说明那台机器的记事本是单字节编码(ANSI/CP1252 之类)往返,只坏了 NUL、没坏高位字节。
2.3 根本原因(不是技术,是流程)
- 把"取证/分析对象"当普通文档看:为了看内容而用编辑器打开样本;
- 对"复制成功"的盲目信任:没有哈希、没有自检;
- 没有明确的操作规范:隐藏文件该怎么取、只能过终端时该怎么办,没人定过。
3. 为什么这么难发现
四个"看起来正常"让人放松警惕:
- 扩展名还是
.exe,图标照旧; - 大小一模一样(1:1 替换的欺骗性);
- 复制/另存过程没有任何报错,还弹了"保存成功";
- 文件甚至还能被某些工具打开、显示出"像字符串"的内容。
教训:没有校验,就等于没有提取。 文件"能打开/能复制"绝不等于"是完整的"。
4. 代价盘点
| 项目 | 情况 |
|---|---|
| 可救回 | PE 头 / 段表 / CLI 头 / .NET 元数据表(受格式约束,可用结构约束反解)→ 整个加载器逻辑、IL 反汇编都还原了 |
| 不可逆 | 压缩/加密数据。本例承载第三层载荷的 PNG(DAKO,deflate 流)彻底损坏 |
| 直接损失 | 第三层加载器 + 最终 C2 拿不到,任务卡死 |
| 时间成本 | 为了绕开这个坑,额外花了两小时写逆变换 + 结构修复;而正确操作只需 copy 一下,5 秒 |
| 真实场景风险 | 若是应急响应/司法取证,这属于证据污染,可能让整条证据链失效 |
5. 六条硬规则(贴在显示器上那种)
- 二进制永远不走文本通道:犯禁名单 ——
notepad 另存为、type x > y、cat/tee、Get-Content|Set-Content、FTP ASCII 模式、串口/终端 canonical 模式、把内容复制进聊天框/表单。 - 取完必须做两件事:
0x00计数 > 0、两端哈希一致(certutil -hashfile/sha256sum)。 - 隐藏属性不影响复制:
copy/robocopy照样能取,完全不需要"用编辑器打开把内容拿出来"。 - 只读原则:任何时候都不要在原文件上"另存为"——可能直接覆盖掉唯一原件,且不可逆。
- 只能过文本通道时:先
certutil -encode(或base64 -w0/xxd -p)编码再搬,大文件分块。 - 留痕:把取文件的命令、时间、两端哈希记下来(可复现、可追溯、可甩锅定位)。
6. 标准作业流程 SOP(Windows 远程主机 + 隐藏文件)
:: ① 先确认服务器上原件还在不在("另存为"通常新建文件,原件很可能没被覆盖)
dir /a "C:\path\systemutil.exe"
certutil -hashfile "C:\path\systemutil.exe" SHA256
:: ② 正确复制(命令行复制是二进制安全的;隐藏属性不拦复制)
copy /b "C:\path\systemutil.exe" "D:\out\systemutil.exe"
robocopy "C:\path" "D:\out" "systemutil.exe" /COPY:DAT /R:2 /W:1
attrib -h -s "C:\path\systemutil.exe" :: 想去掉隐藏再去,非必须
:: ③ 只给终端/剪贴板时:先编码,只搬纯文本
certutil -encode "C:\path\systemutil.exe" "C:\tmp\x.b64"
:: —— 记事本此刻只当"文本搬运工",开 x.b64 复制是安全的
:: 本地: certutil -decode x.b64 systemutil.exe
:: ④ 两端哈希对齐 + 本地自检
certutil -hashfile "D:\out\systemutil.exe" SHA256
python mangled_repair.py systemutil.exe --check
7. 判据速查表(出问题 3 秒定位)
| 现象 | 含义 | 处置 |
|---|---|---|
0x00 计数 = 0 |
一定被"去 NUL"(正常 PE 含大量 0x00) | 判定为提取事故,重取 |
e_lfanew = 0x20202080 这类大值 |
PE 头失效 | 同上 |
| 大小和原件一致但内容不对 | 1:1 替换(NUL→空格) | 同上;注意这是最隐蔽的一种 |
| 大小变小 | NUL 被丢弃(bash x=$(cat f)、文本模式截断) |
同上 |
大小变大/带 EF BB BF |
被当 UTF-8/UTF-16 重编码 | 同上 |
| 本地哈希 ≠ 服务器哈希 | 传输/导出环节出问题 | 重取,别修 |
自检命令:python mangled_repair.py <file> --check
(会输出 0x00/0x20/0x0D 0x0A 计数、e_lfanew、常见成因与对策)
8. 给自己的提醒(可直接转发)
取样本别用记事本开!exe 是二进制,记事本"另存为"会把里面所有
0x00写成空格,大小不变但文件已经废了(我们已经因此丢掉了第三层载荷和 C2)。
以后取文件就两条:
1)命令行copy /b或robocopy复制(隐藏属性不影响复制);
2)传完两端certutil -hashfile xx.exe SHA256对一下,一样才叫取到了。
只能走聊天/终端时,先在服务器上certutil -encode成 base64 再传,本地certutil -decode还原。
9. 本次的补救路径(存档)
mangled_repair.py:逆变换0x20→0x00+ 用BSJB锚点与 CLI 头反解被 0x20 破坏的字段
(SectionAlignment/BaseOfCode/.text VA/ CLI 目录 RVA / MetaData RVA),输出与手工修复逐字节一致;- 从修复后的 PE 里还原出第二层 DLL
OptiMax.dll(藏在SR1位图像素里),加载链完整; - 但第三层(藏在
DAKOPNG 里)不可逆 → 必须拿到服务器上的干净原件才能继续。 - 结论:先
dir /a看原件还在不在——大概率还在,重取一次即可。
浙公网安备 33010602011771号