
简介这是一份面向开发人员、逆向工程与数据分析学习者的 010 Editor 十六进制编辑器工具包适合处理文本、源码及二进制数据时进行查看、检索与精细修改。压缩包共 35 个文件整体约 19.64MB包含可执行程序、16 个 dll 运行库、插件与帮助文档qhc/qch、配置及说明文本等类型较完整便于在 Windows 环境下解压使用。已有 744 人学习下载说明其在二进制编辑工具中具备一定关注度。该工具支持无限撤销、正则搜索替换、列模式编辑、超大文件处理以及 SCL/Python/Perl 脚本扩展并可通过二进制模板解析磁盘映像、内存转储等复杂结构适合需要提升二进制分析、逆向调试与批量文本处理效率的读者参考使用。1. 十六进制编辑器不只是「看乱码」什么时候你必须打开 010 Editor前阵子拆一个自定义资源包文本编辑器打开全是方块用 Python 写了三遍struct.unpack_from偏移量还是对不上——每改一次长度字段后面所有字段跟着错位调试半小时只前进了 16 个字节。换成带结构模板的十六进制编辑器之后同一件事十分钟就通了。010 Editor就是这类工具里的老牌选手十六进制视图、二进制模板、脚本引擎三层能力叠在一起重点不在「能看十六进制」而在于把一段没有文档的二进制变成有名字、有类型、有嵌套的字段树。它适合做逆向分析、文件格式解析、固件与资源包改动、协议抓包落盘后的字节核对。下面按「怎么建模板 → 怎么用脚本批量改 → 哪些参数最容易算错」的顺序拆中间那些坑我都是被错误折腾到不得不改习惯的。2. 二进制模板把一坨偏移量变成有名字的字段2.1 模板到底在做什么先把机制说透不然后面所有报错都无从判断。二进制模板.bt文件本质上是一段 C 风格的描述代码编辑器打开模板时把文件指针放在偏移 0然后按声明顺序逐条消费字节声明一个uint就读 4 字节并命名声明一个结构体就连续读sizeof那么长声明一个数组就连续读 N 个元素。它更像一个「带类型的游标」而不是解析器。这个特性带来两个直接后果。第一声明顺序必须等于文件里的物理顺序中间漏一个字段后面全部错位而且不会报错只会显示成看似正常的数字。第二它不做任何隐式对齐推断——编译器给你struct补的 padding在文件格式里可能根本不存在反过来文件里手写的 2 字节填充模板也不会自动跳过得自己补一个uchar pad[2];。常用类型和字节数先摆出来写模板时对着查比翻文档快模板类型字节数常见对应写法char/uchar1int8 / uint8 / BYTEshort/ushort2int16 / uint16 / WORDint/uint4int32 / uint32 / DWORDint64/uint648long long / QWORDfloat/double4 / 8单精度 / 双精度hfloat2半精度浮点wchar_t2UTF-16 单字符char[N]N定长字符串 / 字节数组time_t4 或 8Unix 时间戳字节序用LittleEndian()/BigEndian()全局切换这两个函数要写在第一个字段声明之前写在中间只影响它之后的声明——这一点很容易被忽略导致「前半段对、后半段乱」的玄学现象。2.2 手写第一个模板文件头 索引表假设拿到一个资源包magic 是PACK头部 16 字节后面跟一张定长 16 字节的索引表。先只写头部跑通再加索引// pack.bt —— 资源包头部 LittleEndian(); // 全局小端必须在第一个字段之前 typedef struct { char magic[4]; // P,A,C,K uint16 version; // 格式版本 uint16 headerSize; // 头部总长含索引表 uint32 fileCount; // 条目数 uint32 indexOffset; // 索引表起始偏移 } PACK_HEADER; PACK_HEADER hdr;运行模板Templates 菜单里跑或者新建模板直接编译后左侧模板变量面板会列出hdr.magic、hdr.version这些名字十六进制视图里对应字节会被高亮。到这一步先别急着往下写做两件事核对hdr.magic是否真的是PACKhdr.fileCount乘上每条索引长度加上headerSize是否大致等于文件总大小。这两条对不上说明字节序或者头部长度的理解就是错的继续写只会越错越远。头部确认之后把索引表接上typedef struct { uint32 nameOffset; // 文件名在字符串池中的偏移 uint32 dataOffset; // 数据块起始偏移 uint32 dataSize; // 数据块长度 uint32 flags; // 位标记 } PACK_ENTRY; PACK_ENTRY entries[hdr.fileCount]; // 数组长度可以直接用前面的变量entries[hdr.fileCount]这一行是模板相对struct模块最大的价值数组长度可以是运行期变量。用 Python 你得先 unpack 出 count再 unpack 一遍数组中间任何一次偏移算错都要重来模板里一次声明就把整张表展开成可点开的结构树还能直接看到每条记录的dataOffset指向哪里。2.3 条件字段、字符串与前置断言真实格式很少是定长的。版本升级加字段、某些条目才有的扩展头都得靠条件声明处理// 版本 2 起头部多了 8 字节扩展字段 if (hdr.version 2) { uint32 extFlags; uint32 reserved; } // 变长字符串先读长度再按长度声明数组 local uint16 nameLen 0; // ……此处按实际布局读取 nameLen char name[nameLen 1]; // 1 吃掉结尾的 \0local声明的变量只存在于模板运行期不会写进文件视图适合放中间计算结果。字符串处理上有两个分支要分清如果格式里是 UTF-16用wchar_t数组读长度按字符数算如果是 UTF-8 定长字段用char数组长度按字节算。混用的后果是中文全变问号而英文看起来完全正常这种半对半错最容易让人怀疑人生。另外强烈建议在模板开头加一条前置校验把「错位」提前变成「明确的报错」if (hdr.magic[0] ! P || hdr.magic[1] ! A || hdr.magic[2] ! C || hdr.magic[3] ! K) { Warning(magic 不匹配当前文件可能不是 PACK 格式或字节序设错); return; }不要写assert(hdr.magic PACK)这种依赖字符串比较的写法不同版本对 char 数组和字符串字面量的比较行为有差异逐个下标比是最稳的。模板里还能用Printf往结果窗口打调试信息遇到「前几个字段对、后面全乱」时就在可疑位置插一句Printf(offset%d\n, FTell());把当前游标打出来比肉眼数十六进制快得多。3. 脚本自动化用 1Script 批量改字节、算校验和3.1 脚本的执行模型与常用函数模板负责「看懂」脚本负责「动手」。010 Editor 的脚本文件是.ics语法接近 C入口固定是main()跑在编辑器进程内能直接读写当前活动文件的任意偏移。要建立的一个关键认知是脚本读写的是文件在内存里的编辑副本WriteUInt执行完十六进制视图会立刻变化但磁盘上的文件没动必须显式保存。很多人跑完脚本发现「没反应」八成是只看了磁盘文件。几个高频函数先列出来函数作用FileSize()返回当前文件总字节数ReadUInt(addr)/WriteUInt(addr, v)读写 4 字节无符号整数ReadUShort/WriteUShort读写 2 字节ReadUByte/WriteUByte读写 1 字节ReadBytes(addr, buf, n)批量读到字节数组Checksum(algo, start, size, crc)对指定区间算校验和OutputPane(str)输出到结果窗口Warning(str)弹警告框适合脚本内部中止同样有LittleEndian()/BigEndian()它影响的是后续ReadUInt这类按类型读的函数不影响ReadBytes这种原样搬字节的调用。这点和模板一致但脚本里因为没有「声明顺序」的约束更容易忘。3.2 批量定位 改写 重算校验和接着上面的资源包写一个实际场景把所有条目的flags第 2 位置 1假设代表「已压缩」然后重算文件尾部的 CRC32。这类操作手工点十六进制视图一次能改三条脚本三秒改三千条。// patch_flags.ics —— 批量置位并重算尾部 CRC32 void main() { LittleEndian(); int64 total FileSize(); if (total 20) { OutputPane(文件太小退出\n); return; } // 1. 校验 magic防止脚本跑错文件 uchar magic[4]; ReadBytes(0, magic, 4); if (magic[0] ! P || magic[1] ! A || magic[2] ! C || magic[3] ! K) { OutputPane(magic 不匹配停止\n); return; } // 2. 按模板里的布局取字段偏移 uint32 fileCount ReadUInt(8); // version(2) headerSize(2) 之后 uint32 indexOffset ReadUInt(12); int changed 0; uint32 i; for (i 0; i fileCount; i) { int64 base indexOffset i * 16; // 每条索引 16 字节 uint32 flags ReadUInt(base 12); // flags 位于第 13~16 字节 if ((flags 0x02) 0) { WriteUInt(base 12, flags | 0x02); changed; } } // 3. 尾部 4 字节是整包 CRC32改完必须重算 int64 crcPos total - 4; int64 crc Checksum(CHECKSUM_CRC32, 0, total - 4, 0); WriteUInt(crcPos, crc); OutputPane(改写条目数: changed \n); }几个地方值得展开。base indexOffset i * 16这个乘法里的 16 必须和模板里PACK_ENTRY的实际大小一致所以最稳的做法是模板先写、脚本照抄偏移而不是两边各自数一遍字段。Checksum的参数顺序是「算法、起始偏移、长度、初始值」覆盖范围这里是0到total - 4也就是不含尾部校验字段本身——这个范围是翻车重灾区后面单独讲。返回值类型在不同版本里可能是整数也可能是字符串如果赋值给int64报类型不匹配加一句强转即可。3.3 脚本与模板的联动两者能互相调用。一种常见用法是在脚本里批量跑模板先确认格式合法再动手改// 对当前文件跑一遍 pack.bt模板内部 Warning 会直接暴露问题 if (RunTemplate(pack.bt, 0) ! 0) { OutputPane(模板解析失败放弃修改\n); return; }反过来模板运行之后它声明的顶层变量在脚本里通常能按名字取到比如hdr.fileCount这样脚本就不用再手算偏移了。取不到也别死磕——退回ReadUInt(8)这种硬编码偏移虽然难看但行为确定比依赖版本差异要可靠。还有一种更省事的入口命令行直接带上脚本参数执行配合一个批处理循环遍历整个样本目录每个文件跑一次脚本、输出一行结果。这种方式适合做回归校验不需要一个个手动打开文件。3.4 脚本的调试手段脚本没有单步调试器那么顺手靠的是三样东西OutputPane打日志、Warning中断、把关键的中间偏移和读到的值成对打出来。我习惯在for循环里加一句只在i 3时打印的日志看清前三条的实际取值剩下的靠统计计数验证。循环里几百次OutputPane会把结果窗口刷爆反而看不见有用信息。4. 三个最容易算错的参数字节序、对齐、校验和范围4.1 字节序用已知约束反推别靠猜字节序错了表现不是报错而是「数字看起来也挺像那么回事」0x00010002被读成0x02000100版本号从 1.2 变成三千三百万。判断方法有两条都不需要额外工具。第一条是找结构性约束。索引表里第一条的dataOffset加上dataSize不应该超过文件总大小fileCount乘 16 加headerSize也不应该超过总大小。同一段字节用两种序各读一遍哪个满足约束就是哪个。第二条是找版本字段这种小整数正常文件里版本号通常是个位数或两位数。用 Python 快速对比比在编辑器里来回点要快import struct raw open(sample.pack, rb).read() print(文件总大小:, len(raw)) for tag, endian in ((小端, ), (大端, )): # 偏移 4 开始uint16 version, uint16 headerSize, uint32 fileCount ver, hsize, cnt struct.unpack_from(endian HHI, raw, 4) plausible (hsize cnt * 16) len(raw) print(f{tag}: version{ver} headerSize{hsize} count{cnt} 约束成立{plausible})这个脚本十几行跑出来的约束成立一列基本就能定案。有一个反直觉的点某些格式不同区段用不同字节序头部小端、数据段大端的情况真实存在所以别在模板开头设一次LittleEndian()就以为全局搞定遇到解析到一半开始错位先怀疑这里。4.2 对齐与填充差 2 个字节后面全错文件格式的对齐规则和编译器的结构体对齐是两套东西。模板按声明顺序紧密读取不做任何自动填充而文件写的时候可能按 4 字节对齐塞了 2 个填充字节。表现就是前三个字段完全正确第四个字段开始偏移 2 字节读出来的字符串全是乱码。定位方法很笨但有效——用文件内部的自身约束交叉验证。把索引表最后一条的dataOffset dataSize算出来和文件总大小比差值就是尾部的填充或者附加数据长度再把第一条的dataOffset和headerSize比如果dataOffset比headerSize大中间那段差值大概率是对齐填充。差值如果是 2、4、6 这种偶数基本可以确定。补齐就一行typedef struct { uint16 id; uint32 offset; // 这里编译器会给 uint32 前补 2 字节 } BAD_ENTRY; typedef struct { uint16 id; uchar pad[2]; // 文件里真实存在的填充必须显式声明 uint32 offset; } GOOD_ENTRY;判断到底是哪种看sizeof如果格式文档写每条记录 8 字节而按字段累加只有 6 字节那 2 字节就是填充必须显式写出来。4.3 校验和范围最容易被「差不多」骗过去的一步校验和翻车率高是因为它错了不会立刻显形——文件还能打开只是某个功能悄悄失效。常见变体有四类覆盖全文件还是只覆盖数据段、包含不包含校验字段自身、校验字段是写成 0 还是写成旧值、CRC32 的初始值和异或输出用的是哪种。别猜列个候选表逐个算候选范围起始偏移长度与文件内值是否一致全文件去掉尾部 4 字节0size - 4一致从 header 之后开始headerSizesize - headerSize - 4不一致含尾部自身先置 00size不一致只覆盖索引表indexOffsetfileCount * 16不一致用脚本把这张表跑一遍一行行打印结果两分钟就能排除。比随手写个zlib.crc32(raw[:-4])然后发现对不上、再反复怀疑算法实现要快得多。同样的方法也适用于 Adler32、MD5 截断、简单累加和这几种变体。5. 避坑五类翻车现场与排查顺序5.1 解析类问题现象一模板一运行就报「数组长度为负」或者数组直接爆掉。原因基本只有一个——长度字段的字节序读反了一个应该是 20 的值被读成 5120 或者负数。解决方式是先在模板开头把LittleEndian()换成BigEndian()试一次两次都爆说明长度字段的偏移本身就错了去前一个字段那里找是不是漏了填充。现象二前几个字段完美从某个字段开始全部不对。原因按概率排漏了填充字节 某个字段类型宽度写错把uint64写成了uint 存在条件字段但条件判断错了。排查顺序建议从后往前推先确认最后一个字段的偏移能不能和文件尾部对齐能对上说明后半段是对的问题在中间对不上说明问题在后面逐段注释掉声明二分定位。现象三字符串解析出来是乱码英文正常中文全错。原因是编码判断反了。char数组按字节读遇到 UTF-16 的汉字会读成0x4F60两个字节拆开看wchar_t按 2 字节读遇到 UTF-8 又会把两个 ASCII 拼成一个怪字符。判断依据是看字符串区域的字节模式ASCII 字符后面跟0x00的是 UTF-16连续紧凑排列的是 UTF-8。5.2 写入类问题现象四脚本改完之后文件打不开或者打开后某个功能失效。原因是只改了目标字段没同步维护性字段。二进制文件里通常有一组互相约束的字段总长度、条目数、校验和、版本号。改任何一个都要连带更新另外几个。解决方式是把「改完必做」的三件事写进脚本末尾重算校验和、校验总长度字段、打印一条汇总日志。这三步不写出问题的时候连是哪次改坏的都不知道。现象五撤销之后文件依然是坏的。原因是对「撤销」的边界判断错了。撤销栈覆盖的是编辑操作但脚本里的批量写入往往被合并成少数几个撤销点C 一下撤销几十条改动。更麻烦的是模板运行过程中如果调用了写入撤销行为更难预测。血泪经验是动手前先复制一份原文件把它当作后悔药不要指望撤销。排查顺序固定成这个套路先看文件总大小有没有变 → 再看校验和区间算出来对不对 → 再看头部长度字段和实际长度是否一致 → 最后才去怀疑具体字段的值。绝大多数「文件打不开」都出在前三步。6. 进阶把模板 脚本做成可回归的格式校验器单次解析跑通只是及格线。真正省时间的是把它变成一套可以反复执行的校验每拿到一批新样本先无脑跑一遍报错就说明格式变了而不是靠人肉一个个点开看。做法是把模板当 schema、脚本当断言层。模板里在每个关键结构后面加约束检查条件写成文件自身的内部一致性// pack.bt 尾部追加的约束检查 if (hdr.headerSize 16 || hdr.headerSize FileSize()) { Warning(headerSize 越界布局理解有误); return; } local int64 expectEnd hdr.indexOffset hdr.fileCount * 16; if (expectEnd FileSize()) { Warning(索引表越界fileCount 或条目长度错误); return; }然后在脚本里遍历目录对每个文件跑模板、捕获结果、汇总成一行报告。跑一百个样本哪几个不符合约束一眼就能看出来比人工抽查靠谱得多。再往前走一步把校验和也纳入断言。脚本末尾不要只是重算后写回而是先算、再和文件里存的值比int64 crcPos FileSize() - 4; int64 stored ReadUInt(crcPos); int64 computed Checksum(CHECKSUM_CRC32, 0, crcPos, 0); if (stored ! computed) { OutputPane(CRC 不一致文件内 stored 重算 computed \n); }这一步在批量处理时价值最大——它能直接告诉你文件在传输或前一步处理中是否已经被改坏而且不需要你事先知道正确的值是多少。有个小技巧值得单独说给模板和脚本统一一套命名前缀比如所有模板变量用hdr_、ent_开头脚本里引用时不容易和局部变量撞名。撞名的表现是脚本读到的值和模板显示的值对不上排查起来极其费劲。从那以后我每次动二进制文件都强制先把原文件复制一份、跑一遍模板确认布局没变、再跑脚本改、最后重算校验和并对比一次四步一步不省。这套流程看着啰嗦但比事后对着一堆打不开的文件找原因要省太多时间。希望帮到你。本文还有配套的精品资源点击获取