
文件头与文件尾这套东西看着像一张死记硬背的对照表实际上是CTF杂项里性价比最高的一块基本功。你拿到一个“打不开”“已损坏”“格式不支持”的附件十有八九第一步就是把它拖进十六进制编辑器看前几个字节是不是对得上、末尾有没有被追加东西。CTF杂项里大量的签到题、修复题、分离题考的不是多高深的算法而是你对文件头文件尾的熟悉程度以及能不能在十几秒内判断出“这文件到底被怎么动过”。这篇文章我想把这件事讲透不只给你一张表照着背而是讲清楚每个签名为什么长这样、改哪里会触发什么后果、修的时候参数怎么算。适合刚入门还在硬记89 50 4E 47的新手也适合做了一阵子、遇到伪加密和宽高修改还是会卡住的中段选手。看完你至少能做到三件事拿到未知文件能快速定性、能手工把被改坏的头尾补回来、能判断哪些题该上工具、哪些题必须动手改字节。下面全是我在实际刷题和带人练习时反复验证过的做法能抄就抄。1. 先搞清楚文件头到底在解决什么问题1.1 从“双击打不开”倒推文件身份很多人一开始的误区是以为文件类型由扩展名决定改个后缀就换了个身份。实际恰恰相反扩展名只是给人看的标签真正决定解析器怎么处理它的是文件开头那几个固定字节。操作系统和解析库通常的做法是先读前若干字节跟内置的签名表比对命中哪个就按哪个格式去解析。所以把一张 PNG 的后缀改成.jpg看图软件照样能打开因为它是按内容识别的。这也解释了一个很常见的现象把一个文件的后缀删掉很多工具仍然能认出来。反过来如果你把文件开头几个字节改坏了哪怕后缀完全正确解析器也会直接报“格式不支持”或者“文件已损坏”。CTF 里最经典的送分题就是这个套路——给一张打不开的图让你自己把签名补回去。在十六进制层面文件头就是文件起始位置的一段固定字节序列业内叫魔数magic number。它有长有短短的只有 2 个字节BMP 的42 4D就是字母 BM长的能到 16 个字节比如某些数据库文件。判断一个格式时字节数越长的签名越可靠短签名容易误判这也是后面要讲的交叉验证的由来。1.2 文件头、文件尾、以及“它们中间那段”文件尾的意义经常被低估。它主要干三件事第一标记内容结束让解析器知道后面不用再读了第二校验完整性很多格式把长度或者校验和放在尾部第三判断有没有“多出来的东西”。第三点在杂项题里极其关键——出题人非常喜欢在一张正常图片的结束标记后面追加一个压缩包或者另一张图你只看头是看不出来的必须知道正确的尾长什么样。举个具体例子一张标准 PNG 的结尾固定是49 45 4E 44 AE 42 60 82也就是IEND加它自己的 CRC。如果你用十六进制编辑器打开一张图滚到最后发现IEND后面还跟着一大段以50 4B 03 04开头的数据那基本可以断定是拼接进去的 ZIP。这个判断过程不需要任何工具肉眼就能完成前提是你记得IEND长什么样。提示判断拼接不要只看尾部也要看头部有没有异常。有些题是“前面接一段垃圾数据 后面是正常文件”这时文件头就不在偏移 0 的位置了需要先搜索签名再截取。1.3 杂项题里文件头的四类典型考法我把遇到过的题型归成四类心里有这个分类看到题就不会乱。第一类是伪装与还原把扩展名改掉或者把头部几个字节改掉让你通过签名判断真实格式再改回来。第二类是残缺修复故意删掉文件头或者尾部要求你根据结构特征补齐PNG 的高度被改小是最常见的一种。第三类是拼接与分离一个文件里塞了多个文件需要按签名切出来。第四类是结构级篡改比如 ZIP 伪加密、CRC 校验被改、分卷标记被动手脚这类必须懂格式内部结构光看头尾没用。这四类的难度是递增的但用的底层能力是同一种把文件当成一段字节流去看而不是当成一个“图片”或者“压缩包”。这个视角转换过来之后很多题就不再神秘了。2. 动手之前十六进制视角的准备2.1 偏移量、字节序这两个概念必须先过看文件头永远绕不开偏移量。偏移量就是从文件第一个字节开始数的位置第一个字节偏移是 0。十六进制编辑器通常会在左侧显示偏移右侧显示 ASCII 预览中间是字节。你要习惯“偏移 0x10 处是什么”这种表达方式因为几乎所有格式文档都是这么描述的。字节序是第二个坎。同一个 4 字节的数值大端序是高位在前小端序是低位在前。PNG 用大端BMP 和 GIF 用小端。这个区别直接决定你改宽度时该往哪个方向写。举个例子宽度 512 在大端下是00 00 02 00在小端下是00 02 00 00。写反了不会报错只会得到一张宽度离谱的图然后你盯着屏幕怀疑人生。我的习惯是先确认格式再确认字节序最后才动手改数值。顺序错了排错成本会翻好几倍。2.2 常用的查看与提取手段命令行下最顺手的是xxd和hexdump看图足够用xxd -l 64 target.bin hexdump -C -n 64 target.bin file target.binfile命令值得单独说一句它内部就是靠签名库判断类型的遇到不确定的文件先跑一次能省掉很多猜测。但它也有盲区签名被改坏或者被前置垃圾数据干扰时会误判成data这时候就得自己上十六进制看了。图形界面的十六进制编辑器各有偏好选一个顺手的长期用就行重点是要支持按偏移跳转、按十六进制搜索、能直接编辑保存。另外准备一个脚本环境Python 加struct、zlib、binascii三个模块基本能覆盖杂项里九成的字节级操作。2.3 三种交叉验证避免被短签名骗了单看头部两个字节很容易翻车。我一般按三层验证第一层看头签名第二层看内部结构特征第三层看尾部标记。三层对得上结论才可靠。比如一个文件开头是50 4B 03 04这是 ZIP 本地文件头。但 DOCX、XLSX、APK、JAR 全都是这个头这时候就要看内部有没有[Content_Types].xml这类条目来区分。再比如52 49 46 46开头的一堆格式WAV、AVI、WEBP 都是 RIFF 容器区别在偏移 8 处的四个字节57 41 56 45是 WAVE41 56 49 20是 AVI57 45 42 50是 WEBP。只看前四个字节是分不出来的。注意遇到以52 49 46 46开头的文件一定要往后看 4 个字节的类型标识否则很容易把音频当成视频把视频当成图片。3. 图片类格式杂项题的主战场3.1 PNG签名、IHDR 与 IEND 三件套PNG 是杂项里出现频率最高的格式没有之一。它的结构非常规整8 字节固定签名89 50 4E 47 0D 0A 1A 0A之后是一串数据块chunk每个块的结构都是「4 字节长度 4 字节类型 数据 4 字节 CRC」最后以IEND块收尾。签名里那串0D 0A 1A 0A是有意设计的目的是让早期某些按文本模式处理文件的工具在遇到换行符时不会把文件搞坏。这个细节知道就行实战里主要是记住前四个字节89 50 4E 47。关键偏移我列一下改宽高的时候直接照着来字段偏移长度说明签名0x00889 50 4E 47 0D 0A 1A 0AIHDR 长度0x084固定 00 00 00 0DIHDR 类型0x0C449 48 44 52宽度0x104大端高度0x144大端位深0x181常见 08颜色类型0x19102 为真彩IHDR CRC0x1D4覆盖 0x0C 到 0x1C改完宽高必须重算 CRC否则大部分解析器会直接拒绝加载。重算范围是从块类型开始到数据结束也就是偏移 0x0C 到 0x1C 这 17 个字节用标准 CRC32 算完写回 0x1D。3.2 JPEGSOI 与 EOI 之间是一堆分段JPEG 的头是FF D8后面通常紧跟FF E0JFIF或者FF E1Exif。尾是FF D9叫 EOI。整个文件由若干段组成每段以FF加段标识开头段内包含长度。图片的真实宽高在 SOF 段里段标识是FF C0到FF CF之间的某几个C0、C1、C2最常见。找到FF C0后跳过 2 字节长度跳过 1 字节精度接下来的 2 字节是高度再 2 字节是宽度都是大端。这个位置比 PNG 难找一些因为 SOF 段不是固定在某个偏移得先搜索。JPEG 的容错性比 PNG 强改完宽高通常不会立刻报错只是显示异常或者渲染失败。这一点在题目里被利用得很充分把高度改小图片下半部分被裁掉答案就藏在那块被裁掉的内容里。这种题的思路是把高度改回一个较大的值让解析器把后面的像素读出来。3.3 GIF、BMP、TIFF、WEBP 的差异化记忆这几个格式我不建议死背抓住它们各自的“记忆锚点”就行。GIF 的头是GIF89a或GIF87a对应十六进制47 49 46 38 39 61和47 49 46 38 37 61很好记就是 ASCII 字符串。宽高在偏移 6 到 9各 2 字节小端。尾是单字节3B。GIF 还有个特点是可以多帧杂项里经常遇到帧分离那是另一套操作但判断它是不是 GIF 靠头就够了。BMP 头是42 4D就是字母 BM总共只有 2 字节所以最容易误判。它的宽度在偏移 0x12高度在 0x16都是 4 字节小端而且高度可以是负数表示从上往下存储。没有固定尾标记。TIFF 的头分两种字节序小端是49 49 2A 00大端是4D 4D 00 2A。这个设计很有意思前两个字节本身就告诉了你该用什么字节序去读后面的内容。识别的时候看到II或者MM就能确定。WEBP 属于 RIFF 容器头是52 49 46 46偏移 8 处是57 45 42 50。注意它和 WAV、AVI 共享前四个字节必须看到偏移 8 才能区分。3.4 图片类最常见的三个动手点第一个是宽高篡改。这类题的逻辑前面讲过把高度改小藏内容。修复的方法是先确定原始高度有时候题目会留下线索比如 CRC 校验值可以反推有时候直接改成一个明显更大的值就能看到内容。如果是 PNG改完记得重算 CRC脚本大概是这样import struct, zlib data bytearray(open(broken.png, rb).read()) new_w, new_h 800, 1200 data[0x10:0x14] struct.pack(I, new_w) data[0x14:0x18] struct.pack(I, new_h) crc zlib.crc32(bytes(data[0x0C:0x1D])) 0xFFFFFFFF data[0x1D:0x21] struct.pack(I, crc) open(fixed.png, wb).write(data)第二个是CRC 反推。PNG 的每个块都有 CRC如果出题人只改了数据没改 CRC你就能通过校验失败定位到具体是哪个块被动了。反过来如果 CRC 是对的但内容看着不对那说明改得很彻底。这个思路在做隐写类题目时特别有用能帮你快速缩小范围。第三个是多图拼接。一个文件里塞了好几张图每张都有自己的签名和结束标记。手工办法是搜FF D9或者49 45 4E 44找到就切一刀。工具办法是用分离工具跑一遍。我个人的习惯是先用工具跑跑不出来再手工搜因为工具对某些变体支持不好会漏。实操心得分离工具不是万能的。很多题的图没有标准结束标记或者被二次压缩过工具识别不出来。这时候xxd加搜索才是最后的手段。4. 压缩包格式结构比头尾更重要4.1 ZIP 的四个签名各管一段ZIP 是杂项里结构最值得研究的格式因为它有四个不同的签名分别对应不同结构签名含义出现位置50 4B 03 04本地文件头每个条目开头50 4B 01 02中央目录头文件后段50 4B 05 06中央目录结束EOCD文件接近末尾50 4B 07 08分卷标记分卷场景本地文件头偏移 6 的两个字节是通用位标志第一位为 1 表示这个条目加密。中央目录头偏移 8 的两个字节也是标志位。伪加密的原理就是出题人把这两个标志位改成 1但实际并没有设置密码于是解压软件提示要密码你随便输什么都是错的。破解办法就是把标志位改回 00 00。这里有个必须提的坑。网上流传的脚本很多是全局搜索50 4B 03 04然后无脑替换这在真实文件里会出事——压缩数据内部完全可能恰好出现这 4 个字节你一改就把正常内容破坏了。稳妥的做法是先读 EOCD 拿到中央目录的偏移和条目数然后顺着中央目录的条目逐个跳到本地头只改真正的头部位置。看起来麻烦但能避免把文件改废。4.2 RAR、7z 与各种压缩的尾部特征RAR 有版本差异。RAR4 的头是52 61 72 21 1A 07 00共 7 字节RAR5 是52 61 72 21 1A 07 01 008 字节。判断版本的时候看第 7 个字节是 00 还是 01 就行。RAR 的尾部没有像 ZIP 那样明确的结束标记但有一个结束块RAR4 是C4 3D 7B 00 40 07 00RAR5 是1D 77 56 51 03 05 04 00判断文件是否完整可以看这个。7z 的头是37 7A BC AF 27 1C这串字节很有辨识度末尾是27 1C不容易和其他格式混淆。它的结构里带 CRC头部本身就有校验改坏了会直接打不开。GZIP 的头是1F 8B 08第三个字节是压缩方法08 表示 deflate。它的尾部是固定 8 字节4 字节 CRC32 加 4 字节原始长度模 2 的 32 次方。这个尾部设计对判断截断很有用如果文件末尾不足 8 字节基本可以确定被截断了。BZIP2 的头是42 5A 68加一个数字也就是BZh后面跟压缩等级通常是31到39之间的 ASCII 码。完整看就是BZh9这类。尾部有固定的结束魔数17 72 45 38 50 90。XZ 的头是FD 37 7A 58 5A 00这串里包含7zXZ的 ASCII辨识度很高。流结束标记是59 5A也就是字母 YZ。TAR 比较特殊它没有开头的魔数判断标志在偏移 257 处是75 73 74 61 72即ustar。所以判断一个文件是不是 TAR你得跳到 257 去看。这个设计经常被新手忽略导致把一个 tar 包当成未知格式。4.3 ZIP 相关的高频实战场景第一个场景是伪加密前面讲过原理和改法。补充一点有些题只改了中央目录的标志位没改本地的有些反过来稳妥做法是两边都检查。还有一类是标志位被改成别的值不一定是 1这时候就看第 0 位是不是 1。第二个场景是已知明文攻击。ZIP 传统加密在已知一部分明文的情况下可以被还原出内部密钥从而解开整个包。CTF 里常见的出法是包里有一个文件你手里也有一份一模一样的比如一张公开的图片用这份已知内容去攻击。这类操作必须在授权环境和本地靶场里练习工具和原理了解即可不要用在任何未授权的数据上。理解它的意义在于知道传统 ZIP 加密不等于安全从而在真实场景里优先选择更现代的加密方案。第三个场景是CRC 碰撞与校验利用。ZIP 里每个条目都记录了未压缩数据的 CRC32。如果包里的文件很小比如只有几个字节理论上可以通过枚举所有可能的内容去匹配 CRC从而还原出文件内容。这个思路在做一些小体积文本文件被加密的题目时很好用因为 3 到 4 字节的穷举量是可以接受的。5. 文档、音视频与其他格式速查5.1 办公文档类一个容易搞混的分水岭这里有个分水岭必须记清楚老版本的 Office 文档doc、xls、ppt用的是复合文档结构头是D0 CF 11 E0 A1 B1 1A E1这 8 个字节非常独特一眼能认出来。新版本的docx、xlsx、pptx本质上是 ZIP 包头就是50 4B 03 04。这个区别在题目里经常被利用。给你一个头是D0 CF 11 E0的文件后缀写着 docx那就是老格式换了新马甲。反过来给你一个 PK 头却叫 doc那就是新格式改了老后缀。判断的时候看头别信后缀。PDF 的头是25 50 44 46 2D也就是%PDF-后面跟版本号。它的尾是25 25 45 4F 46即%%EOF。PDF 有个特点它的结构是可以增量更新的所以文件里可能出现多个%%EOF最后一个才是真正的结尾。这个特性在题目里被用来藏东西把内容追加在第一个%%EOF之后大部分阅读器仍然能正常打开但文件里其实多了东西。RTF 的头是7B 5C 72 74 66就是{\rtf本质是纯文本格式用文本编辑器就能看。格式头十六进制尾备注PDF25 50 44 46 2D25 25 45 4F 46可能有多个 EOFDOC/XLS/PPTD0 CF 11 E0 A1 B1 1A E1无固定复合文档DOCX/XLSX/PPTX50 4B 03 0450 4B 05 06实为 ZIPRTF7B 5C 72 74 667D纯文本XML3C 3F 78 6D 6C无固定可能带 BOM5.2 音视频RIFF 家族与容器格式音视频这块RIFF 家族占了半壁江山。WAV 是52 49 46 46加偏移 8 的57 41 56 45。AVI 是52 49 46 46加偏移 8 的41 56 49 20注意最后有个空格。这两个的区别就在那 4 个字节。MP3 有两种情况。带 ID3 标签的头是49 44 33就是ID3。不带标签的从帧同步开始头两个字节常见FF FB、FF F3、FF F2。所以看到FF FB开头不要以为是 JPEGJPEG 是FF D8。MP4 和 MOV 的头不在偏移 0而是在偏移 4 处是66 74 79 70即ftyp前 4 个字节是这个 box 的长度。这是新手最容易卡住的地方——你在偏移 0 找不到魔数就以为文件坏了。实际上要看偏移 4。FLV 的头是46 4C 56 01就是FLV加版本号。MKV 和 WEBM 的头是1A 45 DF A3。RMVB 的头是2E 52 4D 46即.RMF。FLAC 是66 4C 61 43即fLaC。OGG 是4F 67 67 53即OggS。格式头关键位置WAV52 49 46 46 ... 57 41 56 45类型在偏移 8AVI52 49 46 46 ... 41 56 49 20类型在偏移 8MP349 44 33 或 FF FBID3 标签可选MP4/MOV偏移 4 处 66 74 79 70不是偏移 0FLV46 4C 56 01版本在末字节MKV1A 45 DF A3无固定尾FLAC66 4C 61 43无固定尾5.3 可执行文件、抓包、数据库与字体ELF 的头是7F 45 4C 46第一个字节是 0x7F后面是ELF的 ASCII很好记。PE 格式Windows 可执行的头是4D 5A即MZ。Java class 的头是CA FE BA BE这串字节读起来像“咖啡坏掉了”是最容易记的签名之一。抓包文件分两代。经典 pcap 的头有字节序差异D4 C3 B2 A1和A1 B2 C3 D4分别对应不同的字节序和精度组合还有4D 3C B2 A1表示纳秒精度。新版 pcapng 的头是0A 0D 0D 0A四个字节都很规整。SQLite 数据库的头是53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00其实就是 ASCII 的SQLite format 3加一个 0 字节长度 16 字节非常明确。字体文件里TTF 的头是00 01 00 00也可能是74 72 75 65true。OTF 是4F 54 54 4FOTTO。WOFF 是77 4F 46 46WOFF2 是77 4F 46 32。字体文件在杂项里不算高频但一旦出现往往和字形隐写有关。格式头备注ELF7F 45 4C 46Linux 可执行PE/EXE4D 5AWindows 可执行Java classCA FE BA BE也可能是 JAR 内部条目PCAPD4 C3 B2 A1 / A1 B2 C3 D4注意字节序PCAPNG0A 0D 0D 0A新版抓包SQLite53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 0016 字节TTF00 01 00 00 或 74 72 75 65两种都可能OTF4F 54 54 4FOTTOISO 9660偏移 0x8001 处 43 44 30 30 31CD001ISO 那一行值得单独说一句它的标志不在文件开头而在偏移 0x800132769处这是光盘扇区对齐的设计。判断一个镜像文件是不是 ISO得跳到那个位置看直接看头是看不出来的。6. 从“看得懂”到“修得好”实操流程6.1 三步定位法定性、定界、定性我处理任何一个未知附件基本按这三步走很少出错。第一步定性就是判断它到底是什么格式。手段是看头、跑file、搜关键字。如果头被改坏了就退一步看内部特征比如搜IHDR、搜ftyp、搜ustar这些结构标记不会轻易被全部清掉。第二步定界判断文件的真实边界在哪里有没有前置垃圾、有没有尾部追加、有没有多文件拼接。手段是搜结束标记、比对文件大小和头部记录的长度字段。很多格式的头部就写了总长度或者各段长度一算就知道对不对。第三步定性——这里是第二个“定性”指确定被动了哪里。把当前状态和标准结构比对差在哪就是改在哪。这一步最考验经验但前两步做扎实了第三步往往一眼就能看出来。6.2 头部被改坏的还原手法如果只是几个字节被改直接按标准值写回去就行。但更多情况是被删掉了几个字节或者被插入了垃圾导致后面的偏移全部错位。这时候不能简单覆盖得先把错位修正。处理插入型的方法是从插入点开始把后面的数据整体前移或后移。手工在十六进制编辑器里做这件事很痛苦用dd更靠谱# 保留前 8 字节签名从偏移 0x10 开始截取到文件末尾 dd ifbroken.bin ofpart1.bin bs1 count8 dd ifbroken.bin ofpart2.bin bs1 skip16 cat part1.bin part2.bin fixed.bin处理删除型就反过来先把缺的字节补上再拼回去。这里的关键是搞清楚到底缺了几个字节靠的是结构字段之间的相互印证——比如 IHDR 的长度字段写着 13但实际数据只有 11那就说明缺了 2 个字节。6.3 分段分离的正确姿势一个文件里塞多个文件分离的优先级是先看有没有现成工具能覆盖跑不出来再手工。工具这块binwalk和foremost是最常用的两个。用法大概是binwalk -e target.bin foremost -i target.bin -o outdir实际用下来两者各有短板。binwalk对新版本、特殊变体的签名覆盖不一定全某些版本因为签名库调整会漏报foremost默认只认几种格式遇到冷门格式需要改配置。所以工具跑完没结果不代表没有东西只是没认出来。手工分离的做法是搜索所有可能的起始签名记录偏移再搜索对应的结束标记成对截取。写成脚本比手工切快得多data open(target.bin, rb).read() sigs { b\x89PNG\r\n\x1a\n: (bIEND\xaeB\x82, .png), b\xff\xd8\xff: (b\xff\xd9, .jpg), bPK\x03\x04: (None, .zip), } for sig, (tail, ext) in sigs.items(): pos 0 while True: pos data.find(sig, pos) if pos 0: break if tail: end data.find(tail, pos) if end 0: chunk data[pos:end len(tail)] open(fout_{pos}{ext}, wb).write(chunk) pos 1这个脚本很粗糙只是为了说明思路。实际用的时候要处理大量边界情况比如 ZIP 没有固定结束标记得靠 EOCD 反推。6.4 参数计算的一个完整示例举个我实际碰到过的场景一张 PNG 打不开打开十六进制一看签名正常IHDR 也正常但高度字段是00 00 00 0A也就是 10 像素。文件大小却有几百 KB明显不匹配。这种题基本可以断定高度被改小了。我的处理是先把高度改成一个较大的值比如 1000重算 CRC保存打开看看。如果内容完整显示说明原始高度至少是 1000再逐步调整到刚好不出现黑边或重复为止。如果改完还是显示不全说明还有别的问题。CRC 的算法用标准库就行关键是范围要对import zlib, struct data bytearray(open(broken.png, rb).read()) data[0x14:0x18] struct.pack(I, 1000) crc zlib.crc32(bytes(data[0x0C:0x1D])) 0xFFFFFFFF data[0x1D:0x21] struct.pack(I, crc) open(step1.png, wb).write(data)这里的范围 0x0C 到 0x1C 是块类型加数据共 17 字节不含长度字段也不含 CRC 自身。写错范围是新手最常见的错误算出来的 CRC 永远是错的然后以为是算法有问题。注意如果你的图片显示出来是斜的、颜色错乱那不是宽高的问题多半是颜色类型或者位深字段被改了。这个时候要回去看偏移 0x18 和 0x19对照标准值检查。7. 常见问题与排查速查表7.1 症状对原因一眼定位下面这张表是我攒下来的遇到问题先查能省不少时间。症状可能原因处理手段提示格式不支持头签名被改对照标准值改回图片能开但下半截没了高度字段被改小改大高度并重算 CRC图片能开但有奇怪色块颜色类型或位深被改检查偏移 0x18、0x19压缩包提示要密码但不知道密码伪加密标志位被置 1本地头和中央目录都改成 00 00压缩包打不开且头部正常中央目录损坏从 EOCD 反推偏移重建文件大小和头部记录不符尾部被追加数据搜索结束标记截断分离工具跑不出东西签名变体或尾部缺失手工搜索签名截取改完字节后文件彻底坏了改错位置或字节序反了恢复备份重来打开是乱码文本可能是 RTF 或 XML用文本编辑器看结构文件头在偏移 0 找不到前置了垃圾数据先搜签名再截取7.2 我踩过的几个坑第一个坑是盲目相信工具的输出。早期我用分离工具跑一遍出来一个文件就以为是答案结果打开是空的或者乱码。后来才明白工具切出来的东西需要自己验证看头对不对、看大小合不合理、看有没有结构特征。工具只是帮你缩小范围判断还是要自己做。第二个坑是改字节不备份。这个坑吃过大亏一次改错位置把原始文件覆盖掉了只能重新下载题目附件。后来养成习惯所有操作都写成脚本输入输出分开原始文件永远不动。哪怕只是改两个字节也走脚本。这个习惯看起来笨但省下的返工时间远多于写脚本的时间。第三个坑是字节序想当然。改 BMP 宽度的时候按大端写了00 00 02 00结果宽度变成 51200图片直接炸了。这种错误很难从报错信息里看出来因为解析器不会告诉你字节序错了它只是忠实地按小端去读读出个荒谬的值。后来我的做法是每次改数值之前先读一遍现有值验证字节序读出来是合理的数字说明字节序判断对了再写。第四个坑是忽略结构内部的长度字段。有些格式头部记录了后续数据的总长度如果你在中间插入了字节长度字段就对不上了解析器读到一半会直接崩。这类问题光看头尾看不出来必须把长度字段一起改。第五个坑是把伪加密和真加密混为一谈。看到要密码就认定是伪加密改完标志位还是打不开才发现是真的加密了。区分方法很简单伪加密改掉标志位就能直接解真加密改完会报密码错误或者 CRC 错误。先判断清楚再动手能避免在错误的方向上浪费时间。7.3 几个提升效率的小技巧第一个技巧是建一个自己的签名速查表。网上现成的表很多但自己整理一遍的过程才是记忆的关键。我习惯把常遇到的二十来个格式做成一个文本文件标注好头、尾、关键偏移遇到不确定就翻一下。时间长了自然就记住了。第二个技巧是用脚本代替手工。凡是要改超过 4 个字节的操作都写成脚本。理由不只是效率更重要的是可复现和可回退。脚本里把原始文件读成 bytes改完写新文件原始文件永远不动出问题重跑一遍就行。第三个技巧是先判断再动手。很多题目其实不需要改任何字节只需要看出它是什么格式、里面有什么。上来就改容易把原本正常的部分也破坏掉。我的习惯是先用只读操作把所有信息摸清楚确定要改哪里、改成什么再动手。这个习惯让我少走了很多弯路。第四个技巧是注意隐藏的编码问题。有些文本类文件带 BOM 头比如EF BB BF看起来像文件头其实是编码标记。如果你把它当成格式签名去匹配会得出错误结论。判断文本文件的时候要留意这一点。第五个技巧是保存中间状态。做复杂题的时候每改一步存一个文件命名带序号。这样某一步改错了可以从上一步继续不用从头来。题目做到一半思路断了回头看看中间文件往往能找到新线索。8. 最后聊几句实际体会我个人的经验是文件头文件尾这块内容背固然要背但只背表是走不远的。真正拉开差距的是理解每个字段为什么在那里、被改了会引发什么连锁反应。比如知道了 PNG 的 CRC 覆盖范围你就明白为什么改高度必须重算知道了 ZIP 有本地头和中央目录两套结构你就明白为什么伪加密要改两处知道了 MP4 的ftyp在偏移 4你就不会在偏移 0 死磕。再分享一个小技巧遇到完全没见过的格式不要急着搜资料先自己数一数结构。绝大多数文件格式的设计逻辑都很朴素——开头标识身份中间写数据和索引末尾做校验或者标记结束。你按这个逻辑去看即使不知道具体字段名也能猜个八九不离十。猜完再去验证比直接抄答案记得牢得多。这个内容后续还能这样扩展一是往隐写方向走图片的附加数据块比如 PNG 的tEXt、zTXt和音视频的元数据都是藏东西的好地方二是往格式内部结构走比如 ZIP 的压缩算法、PNG 的滤波类型理解了这些才能处理更深层的题。但那是下一阶段的事了现阶段把签名和结构边界摸熟杂项里的八成题目都不会再卡住你。