k4yt3x/bytegraft

GitHub: k4yt3x/bytegraft

bytegraft 是一款声明式、锚定式二进制补丁工具,通过 TOML 描述精确的字节级补丁并强制校验不变量,确保对固件和可执行文件的修改安全、可追溯且可重现。

Stars: 1 | Forks: 0

# bytegraft 针对固件和可执行文件的声明式、锚定二进制补丁工具。 基础二进制文件保持不可变。补丁以 TOML 格式声明,每个补丁都精确锚定在它期望替换的确切字节上,随后构建过程会将选定的一部分补丁组合成最终的二进制文件,并附带一份溯源清单。如果输入的任何内容与编写补丁时所依据的内容不符,构建将直接失败,而不是产生一个存在微妙错误的结果。 ## 核心概念 - **Binary**(`bytegraft.toml` 中的 `[binaries.]`):一个基础文件,通过 SHA-256 进行锚定,并包含其虚拟地址如何映射到文件偏移量的规则。输出根据注册表键名进行命名,因此两个条目可以共享同一个源文件或文件名而不会发生冲突。 - **Patch**:在指定地址用 N 个新字节替换 N 个旧字节,并声明它期望的原始字节(即其*锚点*)。 - **Blob**:与 Patch 相同,但基于文件支持,适用于那些以 hex 内联表示不切实际的区域。通过它所替换区域的 SHA-256 进行锚定,并可选择将其写入文件的 SHA-256 进行锚定。 - **Signature**:一种带通配符的字节模式,通过定位更改来代替字面量地址,从而使得补丁能够在目标重建后依然有效。它必须且只能匹配一次。 - **Value**:更改所写入的内容,可以是原始的 hex,也可以是数字、字符串或填充值,bytegraft 会按照二进制文件的字节序对其进行编码。 - **Checksum**:二进制文件对其自身进行的一种校验,只需声明一次,并在每次构建后重新计算,使得打过补丁的镜像依然能够通过目标自身的验证。 - **Mod**(`mods//mod.toml`):包含元数据以及补丁和 blob。其名称即为它在 `mods/` 下的路径。 - **Profile**(`profiles/.toml`):一组同时应用的命名 mod 集合。 ## 不变量 在每次构建时、写入任何内容之前都会进行以下检查: 1. 基础二进制文件的 SHA-256 与 `bytegraft.toml` 中的锚定值匹配。 2. Signature 必须且只能匹配一次。零匹配和多重匹配均为错误。 3. 每个 Patch 的 `expect` 与其指定地址处的字节匹配。 4. Patch 的长度与它所替换的内容长度相同,因此不会发生任何数据移动。 5. 每个地址都通过二进制文件声明的映射关系进行解析,且位于单个 segment 内。 6. 来自不同 mod 的 Patch 不得重叠。 7. 没有两个已注册的二进制文件会写入同一个输出文件。 8. 声明的 checksum 能够重现未被改动的二进制文件中已存储的值。 清单或 `mod.toml` 中的未知键将被拒绝,因此拼写错误的 `[[patchs]]` 会明确报错,而不是静默地不应用任何内容。构建是可重现的:相同的基础文件和 mod 会产生逐字节相同的输出。 ## 寻址 二进制文件通过以下两种方式之一声明其地址到偏移量的映射。 **Flat**,适用于固件、ROM 转储和内存镜像,即整个文件进行线性映射: ``` [binaries.firmware] path = "firmware.bin" base = 0x08000000 # address of file offset 0; omit to address by file offset sha256 = "..." ``` **Segmented**,适用于任何非平坦映射的情况,为每个映射声明一个区域。这些信息直接从你的反汇编器已经展示的 section 或程序头中抄录即可;bytegraft 本身不解析 ELF、PE 或 Mach-O 文件,这也正是为什么 mod 中的地址能与反汇编中的地址保持完全一致的原因: ``` [binaries.game] path = "game.exe" sha256 = "..." [[binaries.game.segment]] name = ".text" address = 0x00401000 offset = 0x00000400 size = 0x0002A000 [[binaries.game.segment]] name = ".rdata" address = 0x00430000 offset = 0x0002A400 size = 0x00008000 ``` Segment 既不能在地址上重叠,也不能覆盖相同的文件字节;这两种情况在项目打开时都会被拒绝。请将 section 的**文件**大小填入 `size`,而不是其虚拟大小:否则,一个报告的虚拟大小大于其在磁盘上占用空间的 section(如 `.bss` 及类似 section)会将其地址错误地映射到下一个 section 的字节上。Patch 不能跨越两个 segment,这会在构建时被拒绝。 地址如果带有 `0x` 前缀则为十六进制,没有前缀则为十进制,加上引号永远不会改变这一规则:`0x401000` 和 `"0x401000"` 是同一个值,`100` 和 `"100"` 是另一个值。地址是 64 位的,当其符合 32 位时回显为 8 位十六进制数,不符合时则回显为 16 位。TOML 的整数是有符号的,因此大于等于 `0x8000000000000000` 的地址必须是字符串,这涵盖了内核空间:`base = "0xFFFFFFFF81000000"`。 ## 项目布局 任何包含 `bytegraft.toml` 的目录都可以作为一个项目。这种布局是约定俗成的,不可配置: ``` bytegraft.toml [project] identity + [binaries.*] registry binaries/ base inputs, resolved against [binaries.*] path mods//mod.toml one directory per mod, assets alongside profiles/.toml named mod selections build// output binaries + manifest.toml ``` 这五个名称构成了 bytegraft 所需的全部命名空间,因此一个项目可以位于仓库的根目录下,而该仓库还可以同时存放反汇编数据库或辅助工具。`bytegraft init` 会为其生成基础骨架。 项目根目录是通过从当前工作目录向上查找来确定的;可以使用 `--root PATH` 进行覆盖。 发现规则: - Mod 是任何包含 `mod.toml` 的目录,由其在 `mods/` 下的路径命名。发现过程是递归的,因此 `mods/display/battery-icon/` 就是 `display/battery-icon` mod;并且发现过程会在遇到 `mod.toml` 时停止,因此 mod 自身的资源目录永远不会被当作 mod。 - 以 `_` 开头的路径片段会被置为挂起状态:在 `list` 和 profile 中会被跳过,但仍可通过显式指定名称进行加载。`mods/_parked/` 会将其整个子树挂起,而且挂起操作是一个 `git mv` 而不是编辑,因此它会显示在差异对比中。 - `blob` 的 `file` 路径是相对于 mod 自身的目录进行解析的,这使得 mod 保持自包含。 - `binaries/` 通常会被 gitignore,因为基础文件往往是专有的或从硬件中转储出来的。克隆代码库的人只能获得锚定的 `sha256`:提供基础文件的人要么匹配该锚定值,要么就会遇到明确的报错。 ## 用法 ``` bytegraft init [PATH] # scaffold a new project (default: the current directory) bytegraft binaries # registered binaries, their mapping, and pin status bytegraft caves --min 64 --align 4 # room to put code, aligned bytegraft list # available mods and the binaries they touch bytegraft show # a mod's metadata and patches bytegraft capture 0x401000 8 # current bytes at an address bytegraft verify # check every mod on its own: what still applies? bytegraft verify --profile default # check one selection together, anchors and hashes bytegraft build --profile default --binaries fw-v2 # only the revision you have bytegraft build --profile default # write build/default/ + manifest.toml bytegraft build --mods some-mod,other-mod bytegraft diff base.bin build/default/game.exe --binary game ``` 不带任何选择项的 `bytegraft verify` 会独立检查每个 mod,并打印一张通过/失败状态表,这正是目标重建后人们最关心的问题:不是“我的 profile 是否还能构建”,而是“这七十个更改中有哪些存活了下来”。`bytegraft binaries` 会打印磁盘上每个文件的 SHA-256,这正是新项目填充其锚定值所需要的。`bytegraft caves` 用于查找连续重复的字节段,因为等长补丁集会通过跳转进入死空间来添加代码;`--align` 会从每个连续段下一个对齐的地址开始报告,因为代码通常必须从对齐的地址开始。`diff --binary NAME` 会报告每个存在差异的连续段的虚拟地址而非文件偏移量,因此输出可以直接粘贴到补丁中。`RUST_LOG=debug` 会增加针对每个补丁的构建监测信息。 ## 编写 mod `mods/skip-region-check/mod.toml`: ``` [mod] name = "skip-region-check" binary = "game" # default target; individual changes may override it version = "1.0.0" summary = "Return success from the region check instead of reading the fuse." references = ["docs/region-lock.md"] [[patch]] address = 0x00401120 expect = "0000a0e3" patch = "0100a0e3" note = "MOV R0,#0 -> MOV R0,#1" [[blob]] address = 0x00430200 file = "strings.bin" expect_sha256 = "..." # the region it replaces file_sha256 = "..." # and the asset it writes, so the mod fully describes the build [[patch]] pattern = "83 F8 ?? 74 ?? B8 01 00 00 00" # located by signature, not address offset = 3 expect = "74" patch = "EB" note = "take the branch unconditionally" ``` `profiles/default.toml`: ``` mods = ["skip-region-check", "display/custom-branding"] ``` `bytegraft capture` 会打印指定地址处的字节,这正是 `expect` 所需要的内容。 ## Signatures 一个更改通过字面量 `address` 或 `pattern` 来指定其位置:`pattern` 是一种字节签名,其中 `??` 匹配任意字节。Signature 能够在目标重建后依然有效,这种情况下代码虽然保持了结构但发生了移动,并且它能够容忍不同构建之间存在差异的操作数。 ``` [[patch]] pattern = "74 ?? 8B 45 F8 83 F8 01" offset = 0 # signed distance from the start of the match expect = "74" patch = "75" note = "JE -> JNE" ``` **Signature 必须且只能匹配一次。** 零匹配是错误,多于一个匹配也是错误。这里故意没有提供“首次匹配”规则:采用第一个匹配会静默地对排在最前面的站点打补丁,而这正是这款工具旨在防止的失败情况。模糊的签名意味着签名尚未完成,因此错误信息会报告它匹配到的每一个站点: ``` signature '48 8B 05 ?? 00 00 00' matched 2 times in flat 0x00400000..0x00400400, at 0x00400100, 0x00400200; a signature has to name exactly one site, so make it more specific ``` `offset` 是有符号的,因此你可以锚定在一段特征明显的字节上,并向后回溯到你真正想要修改的指令。`segment = ".text"` 会针对声明了 segment 的二进制文件限制扫描范围;否则只扫描已映射的字节,因此匹配结果绝对不会落在没有地址的文件偏移量上。 Pattern 可以带空格也可以连在一起,`?`、`*` 和 `**` 同样也是通配符,因此 `"48 8B ?? ?? 89"` 和 `"488B????89"` 是同一个签名。一个字节必须严格是两位十六进制数,所以 `48 8` 会被视为拼写错误,而不会悄悄地当作 `48 08`。 使用 signature 时,`expect` 是可选的:pattern 本身就是锚点,并且当被替换的字节处于通配符之下时,它们在不同构建之间合理地存在差异是允许的。而使用字面量 `address` 时,`expect` 是必填的。无论使用哪种方式来定位更改,构建清单都会记录最终解析到的地址。 ## Values 当字节本身是关键所在时,更改的 `expect` 和 `patch` 会使用原始的 hex,如果不是,则使用 value。锚点会检查更改所替换的内容;但没有任何机制会检查它所写入的内容,因此手动编码正是容易产生静默错误的地方。如果你的字节顺序写对了,`"30750000"` 就是 30000 的 小端序,如果写错了,就会变成别的东西。 ``` patch = "0100a0e3" # bytes, when the bytes are the point patch = { int = 30000, width = 4 } # a number, in the binary's byte order patch = { int = -1, width = 2 } # signed or unsigned, whichever reads better patch = { ascii = "MODDED", len = 9 } # NUL-padded, or pad = 0x20 for spaces patch = { fill = 0x00, len = 12 } # blank a region ``` 字节序只需在二进制文件上声明一次,因为它是目标文件的属性,而不是每次更改的属性: ``` [binaries.firmware] path = "firmware.bin" base = 0x08000000 endian = "big" # "little" by default sha256 = "..." ``` 不符合宽度的 value 会被拒绝,文本因长度 `len` 限制会被截断的情况同样也会被拒绝。当某个字段与二进制文件的其他部分确实存在差异时,单个 value 可以覆盖 `endian` 设置。 ## Checksums 会对自身镜像进行校验的目标会拒绝被篡改过的镜像。二进制文件需要声明其携带的校验信息,每次构建时都会重新计算它们: ``` [[binaries.firmware.checksum]] algorithm = "crc32" over = { address = 0x08000000, len = 0x1FFFC } address = 0x0801FFFC width = 4 note = "image CRC the bootloader checks" ``` 该校验属于二进制文件而不是某个 mod,因为它是文件格式的属性:无论写入了什么内容,每次构建都需要它,而且不应指望有人记住去添加一个“修复 checksum”的 mod。 **在进行任何补丁操作之前,声明会先与未改动的二进制文件进行验证。** 如果算法、范围和地址都正确,那么在原始镜像上重新计算就能重现其中已存储的值。因此,把多项式弄错会导致构建错误,而不会生成一个被设备静默拒绝的镜像: ``` checksum at 0x080003FC computes to 0x6F74C96A over the untouched image but 0x2BE010E1 is stored there; the algorithm, range, or address is wrong, or this base file's checksum was already stale ``` 预设项:`crc32`, `crc32c`, `crc32-bzip2`, `crc16-ccitt-false`, `crc16-xmodem`, `crc16-modbus`, `crc16-ibm`, `crc8`, `crc8-maxim`, `crc64-xz`, `sum`, `sum-complement`, `xor`。供应商特定的变体属于配置问题,而不是功能请求,因为任何 CRC 都可以完整地写出来: ``` algorithm = { crc = { width = 16, poly = 0x1021, init = 0xFFFF, reflect_in = true, reflect_out = true, xor_out = 0x0000 } } ``` 这五个参数构成了通用形式,每一个命名的 CRC 都是基于该形式的一个实例。当某个校验覆盖了它自身的存储位置时,设置 `zero_field = true`,这样在进行计算时,该字段会被读取为零。构建清单会记录每次校验的操作及其最终结果。 ## 尚不支持的功能 - **加壳或压缩的 sections。** 在经过 UPX 加壳或 LZ4 压缩的区域中,地址根本不对应实际的文件字节,而该工具完全无法察觉这一点;所有的检查都会通过,但补丁会被错误地写入压缩后的数据中。 - **多架构容器。** 一个注册表条目将一个地址空间映射到一个输出文件,因此一个胖 Mach-O 文件无法在一次构建中同时修补两个架构的切片。 - **资源转换。** 替换纹理或图标意味着你需要自行将其转换为目标格式,并将一个 `[[blob]]` 指向该结果。bytegraft 只负责写入字节;它不知道这些字节的含义。 - **调整任何内容的大小。** Patch 只能替换与其消耗的字节数量相同的内容。添加代码意味着需要寻找死空间并跳转进去。 ## 开发 各项标准(见 `AGENTS.md`): ``` cargo +nightly fmt --check cargo clippy --all-targets -- -D warnings cargo test RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --document-private-items ``` MSRV 为 1.85。代码格式化需要 nightly 版本;其他所有内容均可在 stable 版本上构建。测试会在磁盘上构建临时的项目,因此不需要真实的目标二进制文件。 ## AI 使用声明 本项目在设计 and 实现过程中使用了 AI 工具进行辅助。所有的设计决策均由人类做出,并且每一项更改都经过人类维护者的审查和批准。 ## 许可证 基于 [MIT License](./LICENSE) 授权。\ 版权所有 2026 K4YT3X。
标签:Python安全, 二进制补丁, 云资产清单, 可视化界面, 固件修改, 底层开发, 文档结构分析, 逆向工程, 通知系统