Wasserpuncher/x86-disasm

GitHub: Wasserpuncher/x86-disasm

一个纯 Python 编写的 x86-64 反汇编器,在真实二进制文件上与 GNU objdump 实现字节级一致的解码输出。

Stars: 0 | Forks: 0

# x86-disasm **一个纯 Python 编写的 x86-64 反汇编器。对五个系统二进制文件中的 333,005 条指令进行了反汇编,解码结果与 GNU objdump 在字节级完全一致。零分歧。** x86 指令没有固定长度。你无法知道下一条指令的起始位置, 除非你已经完全解码了当前这条——而解码意味着 在一个状态机中游走,其中的每一部分都会改变下一部分的读取方式: ``` [prefixes] [REX] opcode [ModRM] [SIB] [displacement] [immediate] ``` 只要弄错一个长度,你不只是会打印错一个操作数。你会 *在一条指令的中间*恢复解码,而之后的每一个字节都将是 乱码。这正是让它成为一个令人满意的构建项目的原因:它要么 完全正确,要么明显崩溃。 ## 唯一重要的数字 将它指向真实的编译器输出,并让一个已知正确的反汇编器来评判: ``` $ python -m x86disasm verify /usr/bin/git /usr/bin/git instructions: 61,412 decoded: 59,349 (96.6%) wrong length: 0 wrong text: 0 exact: 59,349 (100.0000%) ``` 跨越五个二进制文件——bash、objdump、gcc、git 和 glibc: | 二进制文件 | 指令数 | 长度错误 | 与 objdump 的分歧 | | --- | ---: | ---: | ---: | | bash | 67,528 | 0 | 0 | | objdump | 68,519 | 0 | 0 | | gcc | 72,342 | 0 | 0 | | git | 59,349 | 0 | 0 | | libc.so.6 | 65,267 | 0 | 0 | | **总计** | **333,005** | **0** | **0** | 这些二进制文件中 **96.6%** 的指令被成功解码;其余的是 SSE 和 AVX,本项目未实现这部分。在它确实解码的指令中,**100.0000% 的输出与 objdump 逐字节相同**——相同的长度、相同的助记符、 相同的操作数。 ## 它能准确读取 tinyjit 的写入内容 这是 [tinyjit](https://github.com/Wasserpuncher/tinyjit) 的另一半, 该项目将一种小语言编译为真实的 x86-64 机器码。那个项目发射 字节;而这个项目将它们还原回指令。闭环就此形成: ``` $ python -m tinyjit dump examples/fib.tj | ... 00000000 55 push rbp 00000001 48 89 e5 mov rbp,rsp 00000004 48 81 ec 20 00 00 00 sub rsp,0x20 0000000b 48 89 bd f8 ff ff ff mov QWORD PTR [rbp-0x8],rdi 00000020 48 b8 02 00 00 00 00 00 00 00 movabs rax,0x2 00000034 48 39 c8 cmp rax,rcx 00000037 0f 9c c0 setl al 0000003a 48 0f b6 c0 movzx rax,al ``` tinyjit 的 `fib()` 全部 45 条指令——与 objdump 完全相同。 ## 使用方法 ``` $ python -m x86disasm dump /usr/bin/ls --section --limit 64 $ python -m x86disasm verify /usr/bin/bash ``` ``` from x86disasm import disassemble for ins in disassemble(bytes.fromhex("554889e5")): print(f"{ins.addr:04x} {ins.raw.hex(' '):<12} {ins}") # 0000 55 push rbp # 0001 48 89 e5 mov rbp,rsp ``` ## 容易踩坑的地方 以下每一项都是通过与 objdump 对比时捕获到的真实 bug,并且每一项 都有对应的测试: - **`0x40` 是一个没有设置任何位的 REX 前缀——但它依然很重要。** 它改变了 存在的 8 位寄存器:`84 ff` 是 `test bh,bh`,但 `40 84 ff` 是 `test dil,dil`。仅仅测试 REX 的*位*会漏掉这点;你必须测试它的存在。 - **`0x66` 前缀会缩小立即数。** `66 3d 00 30` 是 `cmp ax,0x3000`—— 四个字节,而不是六个。如果你仍然读取 imm32,你就会吃掉下一条指令。 - **`mod=00 rm=101` 并不意味着 `[rbp]`。** 它是唯一意味着 RIP 相对寻址的编码,并且它包含一个 disp32,否则你将不会读取到它。 - **`lock` 不是装饰。** `lock sub` 是原子操作,而 `sub` 不是。一个 丢弃该前缀的反汇编器在撒谎,掩盖了线程安全问题。 - **`fs:` 也不是装饰。** `mov rax, QWORD PTR fs:0x28` 是栈 canary。丢弃了 segment 前缀,你就丢失了指令访问对象的信息。 - **立即数是位模式,而不是数字。** `and rsp,-0x10` 和 `and rsp,0xfffffffffffffff0` 是相同的 64 位;objdump 打印后者, 因为那是 CPU 看到的内容。 - **`ff /2` 和 `ff /4` 在没有 REX 的情况下是 64 位的。** 在长模式下,`call`、`jmp` 和 `push` 默认为 64 位操作数——它是 `jmp rax`,绝不可能是 `jmp eax`。 ## 自行运行验证 ``` $ python -m pytest -q 24 passed ``` 该测试套件会反汇编它在你的 机器上找到的任何真实二进制文件的 `.text` 段,并将每一条指令与 objdump 进行比较。长度错误属于 硬故障——这是反汇编器无法存活的致命错误。 ## 限制 - **不支持 SSE/AVX/x87。** 它跳过的 3.4% 是 `movaps`、`pxor`、`punpcklqdq` 和 它们的同类。它将它们报告为不可解码,而不是去猜测——一个编造答案的解码器 比一个承认自己不知道的解码器更糟糕。 - **仅支持 Intel 语法**,以匹配 `objdump -M intel`。 - **无符号化。** 只有地址,没有函数名。 ## 安装 ``` $ git clone https://github.com/Wasserpuncher/x86-disasm $ cd x86-disasm $ python -m x86disasm verify /usr/bin/bash ``` Python 3.10+,无其他依赖。仅需在运行 `verify` 和测试套件时需要 `objdump` (binutils)——反汇编器本身什么都不需要。 ## 许可证 MIT
标签:Python, x86-64, 反汇编器, 底层开发, 无后门, 机器码, 编译器工具链, 解析器, 逆向工具