reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
131
vendor-new/rev-skills/.claude/skills/re-format-macho/SKILL.md
Normal file
131
vendor-new/rev-skills/.claude/skills/re-format-macho/SKILL.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: re-format-macho
|
||||
description: >
|
||||
Mach-O 格式解析:mach_header、LC_*、segment、dyld 信息。
|
||||
触发词:Mach-O、解析mac程序、dyld
|
||||
capabilities: [macho-parser]
|
||||
---
|
||||
|
||||
# Mach-O 格式解析
|
||||
|
||||
## 何时使用 / 何时不用
|
||||
|
||||
- 用:目标是 macOS/iOS 的 Mach-O 二进制(可执行文件/.dylib/.bundle/Framework),需要理解结构、load commands、入口、代码签名、动态库依赖链
|
||||
- 用:检查 dylib 注入(LC_LOAD_DYLIB 异常条目)、签名/公证状态、通用二进制多架构片
|
||||
- 不用:PE(走 [[re-format-pe]])、ELF(走 [[re-format-elf]])
|
||||
- 不用:只需函数逻辑(直接反编译技能);iOS 越狱环境问题属 [[re-mobile]] 域
|
||||
|
||||
## 工具准备
|
||||
|
||||
参考 [[re-analyze/platform-tips]] macOS 分支——attach/调试受 SIP 与 TCC 限制;静态解析不受影响。
|
||||
|
||||
### otool(Apple 自带)
|
||||
|
||||
- macOS: Xcode Command Line Tools: `xcode-select --install`
|
||||
- 验证: `otool --version`
|
||||
- Linux 替代: `llvm-otool`(`apt install llvm` / `brew install llvm` / `pacman -S llvm`),功能覆盖 otool 常用子命令
|
||||
|
||||
### llvm-objdump(跨平台)
|
||||
|
||||
- macOS: `brew install llvm`;Linux: `apt install llvm` / `dnf install llvm` / `pacman -S llvm`
|
||||
- 验证: `llvm-objdump --version`
|
||||
- 用法: `llvm-objdump --macho --private-headers sample`(Linux 上可直接解析 Mach-O,与 macOS 输出一致)
|
||||
|
||||
### jtool2(深入 Mach-O 结构,macOS)
|
||||
|
||||
- macOS: 从 GitHub(intezer/jtool2)clone 后 `make`,或下载 release 二进制
|
||||
- 验证: `jtool2 -h sample` 输出 load commands
|
||||
|
||||
### codesign / lipo(macOS 自带)
|
||||
|
||||
- 验证: `codesign --version`;`lipo -info sample`
|
||||
- Linux 替代: `llvm-lipo -info sample`(llvm 自带)
|
||||
|
||||
### dyld_shared_cache_util(macOS)
|
||||
|
||||
- 系统自带(`/usr/bin/dyld_shared_cache_util`);用途: 解析 dyld 共享缓存中的系统 dylib 时把单个库抽出来
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. **初勘(先确认类型与架构)**:
|
||||
```sh
|
||||
file sample # 可执行文件 / dylib / 通用二进制 / 字节序
|
||||
lipo -info sample # 通用二进制列出各架构片;单架构提示 "non-fat file"
|
||||
xxd -l 16 sample # 魔数: cffaedfe=64位小端, feedfacf=64位大端, cebafaed=32位小端, cafebabe=fat
|
||||
```
|
||||
魔数(文件里看到的字节):`cffa edfe`(MH_MAGIC_64)、`cefa edfe`(MH_MAGIC 32 位)、`cafe babe`(FAT,后跟 4 字节架构数)。通用二进制先 `lipo -thin <arch> -output piece sample` 提取单片再解析。
|
||||
|
||||
2. **mach_header 与 load commands(核心)**:
|
||||
```sh
|
||||
otool -h sample # magic/cputype/filetype/ncmds/flag 一行全出
|
||||
otool -l sample | head -80 # 全部 load commands
|
||||
```
|
||||
64 位 mach_header 固定 32 字节(32 位为 28 字节):magic(4) + cputype(4) + cpusubtype(4) + filetype(4) + ncmds(4) + sizeofcmds(4) + flags(4) + reserved(4,仅 64 位)。load commands 是 Mach-O 的"节表+导入表+入口+签名"综合体——从头到尾按 `cmdsize` 顺序遍历,`ncmds` 计数、`sizeofcmds` 为总字节数,两者互相校验。filetype: 2=MH_EXECUTE 6=MH_DYLIB 8=MH_BUNDLE。常见 LC 值与字段表见 [[layout]]。
|
||||
|
||||
3. **段与节(__TEXT/__DATA/__LINKEDIT)**:
|
||||
```sh
|
||||
otool -l sample | grep -A8 'segname __TEXT'
|
||||
otool -l sample | grep -A8 'segname __DATA'
|
||||
llvm-objdump --macho --section-headers sample
|
||||
```
|
||||
记录各段 vmaddr/vmsize/fileoff;`__TEXT,__text` 是代码,`__DATA,__data` 是可写数据,GOT 在 `__DATA_CONST,__got`(只读常量段;旧二进制在 `__DATA,__got`);`__LINKEDIT` 是**有 vmaddr/vmsize 的正常段**,通常映射为只读区域(vmmap 里能看到),存放符号表/字符串表/重定位/签名等链接元数据——具体存在方式受 dyld 与 shared cache 影响。
|
||||
|
||||
4. **入口(LC_MAIN / LC_UNIXTHREAD)**:
|
||||
```sh
|
||||
otool -l sample | grep -A6 LC_MAIN
|
||||
```
|
||||
`entryoff` 是入口相对文件头的偏移,入口 VA = __TEXT 段 vmaddr + entryoff(64 位下 __TEXT 基址通常 0x100000000)。老二进制用 LC_UNIXTHREAD(寄存器 state 含入口)。可执行文件无 LC_MAIN 且无 LC_UNIXTHREAD 属畸形,重点排查。
|
||||
|
||||
5. **dyld 信息(LC_DYLD_INFO_ONLY / 导出 trie)**:
|
||||
```sh
|
||||
otool -l sample | grep -A9 LC_DYLD_INFO_ONLY
|
||||
llvm-objdump --macho --exports-trie sample # 导出符号(trie 解码)
|
||||
```
|
||||
offset+size 共五组、合计 48 字节,都在 LC_DYLD_INFO_ONLY 里:rebase(基址修正点)、bind(外部符号绑定,非惰性)、weak_bind(弱符号绑定,弱符号合并语义独立,不得并入 bind)、lazy_bind(惰性绑定)、export(导出 trie);新版二进制用 LC_DYLD_CHAINED_FIXUPS(链式修正,iOS 13+ 默认)。dylib 的导出符号在 trie 里,`nm -gU` 同效果。
|
||||
|
||||
6. **动态库依赖链(注入检测)**:
|
||||
```sh
|
||||
otool -L sample # LC_LOAD_DYLIB 列表(依赖库 + install name)
|
||||
otool -l sample | grep -A4 LC_LOAD_DYLIB
|
||||
```
|
||||
每个 LC_LOAD_DYLIB:name(绝对路径或 @rpath/@executable_path 相对)+ timestamp/current_version。**恶意 dylib 持久化 = 新增 LC_LOAD_DYLIB 条目**——与签名时列表比对不一致是重要告警(详见坑)。two-level namespace 下符号查找按 (库, 符号) 二元组,导入解析严格。
|
||||
|
||||
7. **符号表(nlist_64)**:
|
||||
```sh
|
||||
llvm-nm -m sample # 含段归属与 N_EXT 标记;undefined = 导入
|
||||
llvm-nm -gU sample # 仅外部定义符号(导出)
|
||||
otool -l sample | grep -A3 LC_SYMTAB # symoff/nsyms/stroff/strsize 定位表
|
||||
```
|
||||
nlist_64 每条 16 字节:n_strx(字符串表偏移)+ n_type(N_EXT/N_SECT 等)+ n_sect + n_desc + n_value。stripped 后 `.symtab` 消失,只剩 LC_DYLD_INFO 的导出 trie 与动态绑定信息——恢复思路同 ELF([[re-imports]] 签名匹配 + 字符串交叉引用)。
|
||||
|
||||
8. **代码签名检查**:
|
||||
```sh
|
||||
codesign -dv sample 2>&1 # 签名类型(adhoc / Apple Development / not signed)
|
||||
codesign --verify --deep sample # 完整性验证
|
||||
jtool2 --sig sample | head -20 # LC_CODE_SIGNATURE 细节(CodeDirectory/哈希)
|
||||
```
|
||||
- `adhoc`:自签,可被修改后重签(`codesign -f -s - sample`)
|
||||
- Apple 签名:修改任何字节都会失效;Library Validation 开启时注入 dylib 会失败
|
||||
- 无 LC_CODE_SIGNATURE = 未签名(iOS 上无法直接运行)
|
||||
|
||||
9. **手工解析与验证**:load command 输出异常/头字段被伪造时,用 `xxd` + Python `struct` 按偏移直接解析 mach_header 与各 LC(最小可运行示例与字节样例见 [[examples]]),别把解析失败当"损坏文件"丢弃。
|
||||
|
||||
## 跨域联合
|
||||
|
||||
- [[re-binary-core]]:工作流第 3 步,Mach-O 目标格式解析
|
||||
- [[re-mobile]]:iOS App/越狱环境分析的前置(arm64 Mach-O 主二进制与 dylib)
|
||||
- [[re-ios]] / [[re-macos]]:生态衔接(App 层、签名公证、TCC/SIP 限制)
|
||||
- [[re-cracking]]:macOS 破解类任务(补丁后重签流程见本技能步骤 8)
|
||||
- [[re-imports]]:dylib 依赖与导出符号的劫持面分析
|
||||
- 动态调试配合 [[re-lldb]](SIP/TCC 限制见 [[re-analyze/platform-tips]])
|
||||
|
||||
## 常见坑与陷阱
|
||||
|
||||
- **签名验证会拦调试/注入**:修改过的二进制 codesign 校验失败无法运行——先 `codesign -f -s -` 重签(仅限自签/adhoc 场景);Library Validation 开启时 `DYLD_INSERT_LIBRARIES` 失效
|
||||
- **iOS 上需越狱环境**:真机调试要开发者证书或越狱,模拟器进程受沙盒限制——静态解析可先行,动态部分交给 [[re-mobile]]
|
||||
- **LC_LOAD_DYLIB 注入检测**:恶意 dylib 通过新增 LC_LOAD_DYLIB 实现持久化——`otool -L` 列表与签名时列表不一致是重要告警
|
||||
- **通用二进制含多架构片**:先 `lipo -info` 确认架构再 `lipo -thin` 提取,别对全片解析;fat 头里各片 offset 是绝对文件偏移,按 (offset, size) 切出单片
|
||||
- **GOT 位置随版本迁移**:新产物 GOT 在 `__DATA_CONST,__got`(只读,装载后重定位一次);旧产物在 `__DATA,__got`(可写)——找 GOT 先 `otool -l` 看段名,别假设
|
||||
- **dyld 共享缓存里没有独立 dylib 文件**:系统库(libSystem.dylib 等)实际在 `/System/Library/dyld/` 缓存内,文件系统里只有 stub——分析系统库用 `dyld_shared_cache_util -extract` 抽出
|
||||
- **load command 遍历错位**:现象——`otool -l` 输出中途乱码/报错;原因——ncmds 与 sizeofcmds 被伪造或某条 LC 的 cmdsize 异常;对策——所有 LC 至少 8 字节(`load_command` 头本身 `cmd`+`cmdsize`),64 位下 cmdsize 须为 8 的倍数、32 位为 4 的倍数,再按具体 cmd 校验其结构最小尺寸与尾随 section/字符串,累计不超 sizeofcmds
|
||||
- **__LINKEDIT 是映射段,但内容可能被 dyld 加工**:它有自己的 vmaddr/vmsize,通常以只读映射存在(`vmmap` 可见);不过 shared cache 共享库的链接元数据驻留在共享缓存里,直接从进程内存按文件偏移找符号表常常对不上——定位符号表优先用 `nm`/`dyldinfo`/MachOView 等按 LC_SYMTAB 解析,而不是照抄文件偏移
|
||||
@@ -0,0 +1,192 @@
|
||||
# Mach-O 最小解析示例
|
||||
|
||||
以下示例以本机(Linux + clang 22 + ld64.lld)构建的真实 x86_64 Mach-O 可执行文件为标本,otool/llvm-objdump 输出与 Python/字节样例可逐字段对照。构建命令(无 macOS 环境时可复现,仅需 clang + lld 的 darwin 支持):
|
||||
|
||||
```sh
|
||||
# hello.c: 定义 _start 入口(Mach-O 中 C 符号加下划线,链接器 -e 用 __start)
|
||||
clang -target x86_64-apple-darwin -c hello.c -o hello.o
|
||||
ld64.lld -arch x86_64 -platform_version macos 10.13 10.13 \
|
||||
-e __start -undefined dynamic_lookup hello.o -o hello_macho
|
||||
```
|
||||
|
||||
## 1. 工具输出对照
|
||||
|
||||
### `llvm-otool -h hello_macho`(mach_header,全字段)
|
||||
|
||||
```
|
||||
Mach header
|
||||
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
|
||||
0xfeedfacf 16777223 3 0x80 2 13 1288 0x00200085
|
||||
```
|
||||
|
||||
对照要点:magic=0xfeedfacf(64 位 LE);cputype=16777223=0x01000007=x86_64;cpusubtype=3、caps=0x80(x86_64h 能力位);filetype=2=MH_EXECUTE;ncmds=13、sizeofcmds=1288(二者乘积关系可校验遍历);flags=0x00200085 = NOUNDEFS(1)+DYLDLINK(2)+TWOLEVEL(4)+PIE(0x200000)。
|
||||
|
||||
### `llvm-otool -l hello_macho`(load commands,节选)
|
||||
|
||||
```
|
||||
Load command 0
|
||||
cmd LC_SEGMENT_64
|
||||
cmdsize 72
|
||||
segname __PAGEZERO
|
||||
vmaddr 0x0000000000000000
|
||||
vmsize 0x0000000100000000 ← 4GB 空洞(空指针保护)
|
||||
maxprot 0x00000000 initprot 0x00000000 nsects 0
|
||||
Load command 1
|
||||
cmd LC_SEGMENT_64
|
||||
cmdsize 552
|
||||
segname __TEXT
|
||||
vmaddr 0x0000000100000000
|
||||
fileoff 0 filesize 8192
|
||||
maxprot 0x00000005 initprot 0x00000005 nsects 6 ← RX
|
||||
Section
|
||||
sectname __text addr 0x100000550 size 0x1e offset 1360 align 2^4
|
||||
sectname __stubs addr 0x100000570 size 0x6 offset 1392 reserved1 1 (index into indirect symbol table)
|
||||
sectname __cstring addr 0x100000592 size 0xf offset 1426 flags 0x00000002 (CSTRING_LITERALS)
|
||||
Load command 2
|
||||
cmd LC_SEGMENT_64
|
||||
segname __DATA vmaddr 0x100002000 fileoff 8192 initprot 0x3 nsects 3
|
||||
Section
|
||||
sectname __got addr 0x100002000 size 8 offset 8192 flags 0x6 (POINTERS) reserved1 0
|
||||
sectname __la_symbol_ptr addr 0x100002008 size 8 offset 8200 flags 0x7 (LAZY_POINTERS) reserved1 2
|
||||
Load command 3
|
||||
cmd LC_SEGMENT_64 segname __LINKEDIT vmaddr 0x100003000 fileoff 12288 nsects 0
|
||||
Load command 4
|
||||
cmd LC_DYLD_INFO_ONLY
|
||||
cmdsize 48
|
||||
rebase_off 12288 rebase_size 8
|
||||
bind_off 12296 bind_size 24
|
||||
lazy_bind_off 12320 lazy_bind_size 16
|
||||
export_off 12336 export_size 48
|
||||
Load command 5
|
||||
cmd LC_SYMTAB
|
||||
symoff 12392 nsyms 5 stroff 12488 strsize 72
|
||||
Load command 7
|
||||
cmd LC_LOAD_DYLINKER cmdsize 32 name /usr/lib/dyld (offset 12)
|
||||
Load command 8
|
||||
cmd LC_UUID cmdsize 24 uuid ...
|
||||
Load command 9
|
||||
cmd LC_VERSION_MIN_MACOSX cmdsize 16 version 10.13 sdk 10.13
|
||||
Load command 10
|
||||
cmd LC_MAIN
|
||||
cmdsize 24
|
||||
entryoff 1360 ← 入口文件偏移 = __text 的 offset(0x550)
|
||||
stacksize 0
|
||||
Load command 11
|
||||
cmd LC_LOAD_DYLIB
|
||||
cmdsize 40
|
||||
name libstub.dylib (offset 24) ← 依赖库(本示例用 -L. -lstub 链接出的)
|
||||
```
|
||||
|
||||
对照要点:LC_SEGMENT_64 的 cmdsize=72+80×nsects(552=72+80×6,自查一致);__text 的 addr 与 LC_MAIN.entryoff 一致(0x100000550 与 1360);__got/__la_symbol_ptr 的 reserved1 指向 LC_DYSYMTAB 的间接符号表索引;LC_LOAD_DYLIB 的 name 从偏移 24 开始(16+8=24)。入口 VA = 0x100000000 + 0x550 = 0x100000550。
|
||||
|
||||
### `llvm-nm -m hello_macho`(符号表,含段归属)
|
||||
|
||||
```
|
||||
0000000100002010 (__DATA,__data) non-external __dyld_private
|
||||
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
|
||||
0000000100000550 (__TEXT,__text) external __start
|
||||
(undefined) external _puts (dynamically looked up)
|
||||
(undefined) external dyld_stub_binder (dynamically looked up)
|
||||
```
|
||||
|
||||
对照要点:外部定义符号带 (段,节) 归属;undefined 条目 = 导入(依赖 dyld 绑定);`__mh_execute_header` 是 Mach-O 特有的"文件头即符号"(macho_header 的地址)。nlist_64 的 n_sect 值指向节序号。
|
||||
|
||||
## 2. Python struct 解析 mach_header(最小可运行)
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
def parse_macho_header(path):
|
||||
d = open(path, 'rb').read(32)
|
||||
magic = struct.unpack_from('<I', d, 0)[0]
|
||||
if magic == 0xFEEDFACF: # MH_MAGIC_64(LE 文件字节 cffaedfe)
|
||||
f = struct.unpack_from('<IIIIIIII', d, 0) # 8 个 u32 = 32 字节
|
||||
return dict(magic=hex(f[0]), cputype=f[1], cpusubtype=f[2],
|
||||
filetype=f[3], ncmds=f[4], sizeofcmds=f[5],
|
||||
flags=hex(f[6]), reserved=f[7])
|
||||
if magic == 0xFEEDFACE: # 32 位,28 字节
|
||||
f = struct.unpack_from('<IIIIIII', d, 0)
|
||||
return dict(magic=hex(f[0]), cputype=f[1], cpusubtype=f[2],
|
||||
filetype=f[3], ncmds=f[4], sizeofcmds=f[5], flags=hex(f[6]))
|
||||
raise ValueError('not a Mach-O (magic %x)' % magic)
|
||||
|
||||
print(parse_macho_header('hello_macho'))
|
||||
# {'magic': '0xfeedfacf', 'cputype': 16777223, 'cpusubtype': 3, 'filetype': 2,
|
||||
# 'ncmds': 13, 'sizeofcmds': 1288, 'flags': '0x200085', 'reserved': 0}
|
||||
# —— 与 llvm-otool -h 逐字段一致
|
||||
```
|
||||
|
||||
按 mach_header 的 ncmds/cmdsize 可继续遍历 LC:前 8 字节 `I I`(cmd, cmdsize),cmdsize=0 或超过 sizeofcmds 总量即遍历异常。LC_SEGMENT_64 用 `'<I I 16s QQQQQ II II'` 展开(72 字节),再按 nsects 解析 80 字节的 section_64。
|
||||
|
||||
## 3. 字节样例(hello_macho 前 32 字节,逐字段标注)
|
||||
|
||||
```
|
||||
00000000: cffa edfe 0700 0001 0300 0080 0200 0000 ................
|
||||
magic──┘ └cputype(0x01000007=x86_64)┘ └cpusubtype(0x80000003)┘
|
||||
filetype=02(MH_EXECUTE)───────────┘
|
||||
00000010: 0d00 0000 0805 0000 8500 2000 0000 0000 .......... .....
|
||||
ncmds=13(0x0d)──┘ └sizeofcmds=1288(0x508) └flags=0x00200085
|
||||
reserved=0────────────────────────┘
|
||||
```
|
||||
|
||||
对照流程:`xxd -l 32 hello_macho` 与 `llvm-otool -h` 输出对照;magic 不符先查是不是 fat(cafebabe)再决定是否 `lipo -thin` 拆片(见 SKILL.md 坑)。
|
||||
|
||||
## 4. 导出 trie 与绑定表(LC_DYLD_INFO_ONLY 解析)
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
d = open('hello_macho', 'rb').read()
|
||||
# 读 LC_DYLD_INFO_ONLY(第 5 条 LC,前 8 字节 cmd/cmdsize)
|
||||
off = 32
|
||||
for _ in range(5):
|
||||
cmd, size = struct.unpack_from('<II', d, off)
|
||||
assert size >= 8
|
||||
off += size
|
||||
if cmd == 0x80000022: # LC_DYLD_INFO_ONLY
|
||||
(r_off, r_sz, b_off, b_sz, w_off, w_sz, l_off, l_sz, e_off, e_sz) = \
|
||||
struct.unpack_from('<10Q', d, off)
|
||||
print('export trie: off=%d size=%d' % (e_off, e_sz))
|
||||
print('export trie 原始字节:', d[e_off:e_off+e_sz].hex())
|
||||
break
|
||||
```
|
||||
|
||||
本例 trie 原始字节(export_off=12336 起,48 字节):
|
||||
|
||||
```
|
||||
00003030: 0001 5f5f 0006 0002 7374 6172 7400 226d ..__....start."m
|
||||
00003040: 685f 6578 6563 7574 655f 6865 6164 6572 h_execute_header
|
||||
00003050: 0027 0300 d00a 0002 0000 0000 0000 0000 .'..............
|
||||
```
|
||||
|
||||
解码(dyld 导出 trie 格式:边是 `\0` 结尾字符串,子节点偏移是相对 trie 起点的 uleb128——Apple dyld(MachOAnalyzer.cpp)、lld(ExportTrie.cpp)与 LLVM 解析器三方一致,无第二种边格式):
|
||||
|
||||
```
|
||||
0x00: 00 root 节点 terminalSize=0(非终端)
|
||||
01 子节点数=1
|
||||
5f 5f 00 边字符串 "__"(Mach-O 符号名含下划线前缀)
|
||||
06 子节点绝对偏移 6 → 0x3036
|
||||
0x06: 00 02 terminalSize=0;子节点数=2
|
||||
73..74 00 边 "start"
|
||||
22 子节点偏移 34 → 0x3052
|
||||
6d..72 00 边 "mh_execute_header"
|
||||
27 子节点偏移 39 → 0x3057
|
||||
0x22: 03 00 d0 0a 00 terminalSize=3;flags=0;address=uleb128(d0 0a)=0x550
|
||||
→ __start @ 0x100000000+0x550=0x100000550(与 otool 对照 ✓)
|
||||
0x27: 02 00 00 00 00 terminalSize=2;flags=0;address=0
|
||||
→ __mh_execute_header @ 0x100000000(文件头即符号)
|
||||
```
|
||||
|
||||
注意 `d0 0a` 是两字节 uleb128(0xd0 最高位=1 续读):0x50 | (0x0a<<7) = 0x550。手工解码过一遍即掌握 dyld 导出表格式:所有生成端(Apple ld64/lld)都是同一种 `\0` 结尾边 + 相对 trie 起点 uleb128 偏移,解码脚本只需处理这一种。
|
||||
|
||||
## 实现教训(内化)
|
||||
|
||||
- 一切解析先看 magic 分 64/32 位(字段数不同),fat 先拆片
|
||||
- load commands 遍历只信 cmdsize,不信 ncmds 之外的信息;每条 LC 至少 8 字节(`load_command` 头),ncmds×8 > sizeofcmds 即异常,且各条 cmdsize 之和须等于 sizeofcmds
|
||||
- 段节是一棵树(段→节),不是平铺数组;节地址 = 段 vmaddr 内的相对偏移
|
||||
- otool 输出与文件字节必须能互相印证:对不上说明头被修改或工具解析歧义,先手工核对再下结论
|
||||
|
||||
## 使用注意
|
||||
|
||||
- Linux 上解析 Mach-O 用 llvm-otool/llvm-objdump/llvm-lipo(llvm 包自带),输出与 macOS 一致;反汇编才需交叉环境
|
||||
- 样例取自本机构建的极简二进制;分析真实样本时以样本自身 load commands 为准,别套模板
|
||||
@@ -0,0 +1,148 @@
|
||||
# Mach-O 布局:mach_header / load commands / 段节与 dyld 信息
|
||||
|
||||
Mach-O 文件 = mach_header + load commands 区 + 段数据(__TEXT/__DATA/__LINKEDIT)。load commands 同时承载节表、导入导出、入口与签名信息,是 Mach-O 与 ELF 最大的结构差异(ELF 的节头表是独立数组,Mach-O 的"节"嵌在 LC_SEGMENT_64 里)。本文件以 64 位为主,32 位差异单独标注。
|
||||
|
||||
## mach_header(文件头)
|
||||
|
||||
64 位共 32 字节(32 位 28 字节,无 reserved):
|
||||
|
||||
| 偏移 | 字段 | 大小 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0x00 | magic | u32 | 0xFEEDFACF=MH_MAGIC_64(LE 文件中字节为 cffaedfe);0xFEEDFACE=32 位;0xCAFEBABE=fat |
|
||||
| 0x04 | cputype | u32 | 0x01000007=x86_64(7\|CPU_ARCH_ABI64)**0x0100000C=arm64**(12\|CPU_ARCH_ABI64)7=i386 12=arm;**arm64e 的 cputype 与 arm64 相同(都是 0x0100000C),靠 cpusubtype 区分**——不要用 cputype 判 arm64e |
|
||||
| 0x08 | cpusubtype | u32 | **x86_64:3=`X86_64_ALL`、8=`X86_64_H`(x86_64h,Haswell 特性子集)**;**arm64:0=`ARM64_ALL`、1=`ARM64_V8`、2=`ARM64E`**(arm64e 由这里区分,不是 cputype);高位是 **capability bit**:`CPU_SUBTYPE_LIB64=0x80000000`,它**改变不了基础 subtype 的含义**(`0x80000003` 仍是 X86_64_ALL 带 64 位库标记,不是 x86_64h);arm64e 另有 `0x80000000` 作 versioned-ptrauth-ABI 掩码 |
|
||||
| 0x0C | filetype | u32 | 1=MH_OBJECT 2=MH_EXECUTE 4=MH_CORE 6=MH_DYLIB 8=MH_BUNDLE 9=MH_DYLIB_STUB |
|
||||
| 0x10 | ncmds | u32 | load commands 条数 |
|
||||
| 0x14 | sizeofcmds | u32 | load commands 区总字节数(每条 cmdsize 之和) |
|
||||
| 0x18 | flags | u32 | 按 Apple `loader.h` 的**位值**读,不要手工排成连续编号:`0x1`=NOUNDEFS、`0x2`=INCRLINK、`0x4`=DYLDLINK、`0x8`=BINDATLOAD、`0x10`=PREBOUND、`0x20`=SPLIT_SEGS、**`0x40`=LAZY_INIT(已废弃)**、**`0x80`=TWOLEVEL**、`0x100`=FORCE_FLAT、`0x200`=NOMULTIDEFS、**`0x200000`=PIE**、**`0x800000`=HAS_TLV_DESCRIPTORS(TLS 描述符,现代产物常见)** |
|
||||
| 0x1C | reserved | u32 | 仅 64 位,恒 0 |
|
||||
|
||||
## load commands(LC)通用头
|
||||
|
||||
每条 LC 前 8 字节固定:`cmd: u32 + cmdsize: u32`;cmdsize 是整条命令长度(含这 8 字节),按此顺序遍历。下界只有这 8 字节(`load_command` 头本身),无 16 字节之说;64 位下 cmdsize 须为 8 的倍数、32 位为 4 的倍数,且按具体 cmd 校验其结构最小尺寸与尾随 section/字符串。
|
||||
|
||||
### 常见 LC 类型值
|
||||
|
||||
| 值 | 名称 | 含义 |
|
||||
|---|---|---|
|
||||
| 0x01 | LC_SEGMENT | 段(32 位) |
|
||||
| 0x02 | LC_SYMTAB | 符号表(symoff/nsyms/stroff/strsize) |
|
||||
| 0x05 | LC_UNIXTHREAD | 老入口(寄存器 state 含 rip) |
|
||||
| 0x0B | LC_DYSYMTAB | 符号表分类(本地/外部/未定义范围索引) |
|
||||
| 0x0C | LC_LOAD_DYLIB | 依赖动态库 |
|
||||
| 0x0D | LC_ID_DYLIB | dylib 自身 install name |
|
||||
| 0x0E | LC_LOAD_DYLINKER | 加载器路径(/usr/lib/dyld) |
|
||||
| 0x19 | LC_SEGMENT_64 | 段(64 位) |
|
||||
| 0x1B | LC_UUID | 24 字节 UUID |
|
||||
| 0x1D | LC_CODE_SIGNATURE | 签名区(dataoff/datasize) |
|
||||
| 0x22 / 0x80000022 | LC_DYLD_INFO / _ONLY | dyld 信息表(新) |
|
||||
| 0x24 | LC_VERSION_MIN_MACOSX | 最低 macOS 版本(旧式) |
|
||||
| 0x26 | LC_FUNCTION_STARTS | 函数起始地址压缩表 |
|
||||
| 0x80000028 | LC_MAIN | 新入口(entryoff/stacksize) |
|
||||
| 0x29 | LC_DATA_IN_CODE | 代码内数据区表 |
|
||||
| 0x32 | LC_BUILD_VERSION | 构建版本(platform+版本,新式,替代 LC_VERSION_MIN_*) |
|
||||
| 0x80000033 | LC_DYLD_EXPORTS_TRIE | 独立导出 trie(新) |
|
||||
| 0x80000034 | LC_DYLD_CHAINED_FIXUPS | 链式重定位(iOS 13+/macOS 10.15+) |
|
||||
|
||||
## LC_SEGMENT_64(段,72 字节;32 位 LC_SEGMENT 56 字节)
|
||||
|
||||
| 偏移 | 字段 | 大小 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0x00 | cmd / cmdsize | u32×2 | 0x19 / 72+80×nsects |
|
||||
| 0x08 | segname | 16 | 段名(__TEXT/__DATA/__LINKEDIT...) |
|
||||
| 0x18 | vmaddr | u64 | 段装载虚拟地址(__TEXT 通常 0x100000000) |
|
||||
| 0x20 | vmsize | u64 | 内存大小 |
|
||||
| 0x28 | fileoff | u64 | 文件偏移 |
|
||||
| 0x30 | filesize | u64 | 文件内大小 |
|
||||
| 0x38 | maxprot | u32 | 最大内存权限(7=RWX) |
|
||||
| 0x3C | initprot | u32 | 初始权限(5=RX 3=RW 1=R) |
|
||||
| 0x40 | nsects | u32 | 节数 |
|
||||
| 0x44 | flags | u32 | 0 |
|
||||
|
||||
## section_64(节,80 字节,嵌在段内)
|
||||
|
||||
| 偏移 | 字段 | 大小 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0x00 | sectname | 16 | 节名(__text/__data/__got...) |
|
||||
| 0x10 | segname | 16 | 所属段名 |
|
||||
| 0x20 | addr | u64 | 节起始 VA |
|
||||
| 0x28 | size | u64 | 节大小 |
|
||||
| 0x30 | offset | u32 | 文件偏移 |
|
||||
| 0x34 | align | u32 | 2 的幂(4=16 字节对齐) |
|
||||
| 0x38 | reloff / nreloc | u32×2 | 重定位区(.o 常用) |
|
||||
| 0x40 | flags | u32 | 低位=节类型(0x1=REGULAR 0x2=CSTRING_LITERALS 0x6=POINTERS 0x7=LAZY_POINTERS);高位属性 |
|
||||
| 0x44 | reserved1 | u32 | 间接符号表索引(__got/__la_symbol_ptr 用) |
|
||||
| 0x48 | reserved2 | u32 | stub 大小等 |
|
||||
| 0x4C | reserved3 | u32 | 0 |
|
||||
|
||||
## 典型段节布局(64 位可执行文件)
|
||||
|
||||
```
|
||||
__PAGEZERO vmaddr 0x0000000000000000 vmsize 4GB (不可访问,空指针崩溃防护)
|
||||
__TEXT vmaddr 0x0000000100000000 RX
|
||||
__text 代码 / __stubs / __stub_helper / __cstring 字符串
|
||||
__unwind_info / __eh_frame
|
||||
__swift5_* (Swift 元数据,见 [[re-swift]])
|
||||
__DATA vmaddr 0x100002000 RW
|
||||
__got GOT(新产物在 __DATA_CONST) / __la_symbol_ptr / __data / __bss
|
||||
__DATA_CONST 只读常量(__got、__const;Swift 产物常见)
|
||||
__LINKEDIT vmaddr 末尾段 R (映射只读;链接元数据,shared cache 场景驻留方式不同)
|
||||
LC_SYMTAB 符号表 / LC_DYSYMTAB 分类 / 重定位 / 代码签名 / 导出 trie
|
||||
```
|
||||
|
||||
- 加载范围 = __TEXT..__LINKEDIT 覆盖的 vmaddr 区间;__PAGEZERO 不映射
|
||||
- 入口 VA = __TEXT.vmaddr + LC_MAIN.entryoff(实测示例见 [[examples]])
|
||||
- GOT 槽初始化靠 dyld 重定位(bind 表),装载后 __DATA_CONST 段被置只读
|
||||
|
||||
## dyld 信息
|
||||
|
||||
### LC_DYLD_INFO_ONLY(48 字节;LC_DYLD_INFO 同结构)
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| rebase_off / rebase_size | 基址修正表(64 位地址低位写入) |
|
||||
| bind_off / bind_size | 外部符号绑定表(非惰性,如 __got 槽) |
|
||||
| weak_bind_off / weak_bind_size | 弱符号绑定 |
|
||||
| lazy_bind_off / lazy_bind_size | 惰性绑定(__la_symbol_ptr,首次调用才绑) |
|
||||
| export_off / export_size | 导出符号 trie(前缀树:字节为 key 分叉,节点存符号偏移+标志) |
|
||||
|
||||
### 动态解析链
|
||||
|
||||
```
|
||||
LC_LOAD_DYLIB → 依赖库列表(otool -L)
|
||||
LC_SYMTAB → 符号表(nlist_64×nsyms + 字符串表)
|
||||
LC_DYLD_INFO_ONLY → rebase/bind/weak_bind/lazy_bind/export 五组表(weak_bind 语义独立于 bind,不得并入)
|
||||
导出符号定位:export trie 逐字节匹配前缀 → 叶子存 symbol 偏移(相对 trie 所在区域)
|
||||
绑定信息定位:bind 表的 opcode 流(ADDR/SYMBOL/TYPE 等)→ GOT 槽 ← 符号
|
||||
```
|
||||
|
||||
### nlist_64(符号条目,16 字节;32 位 12 字节)
|
||||
|
||||
| 偏移 | 字段 | 大小 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0x00 | n_strx | u32 | 符号名在字符串表内的偏移 |
|
||||
| 0x04 | n_type | u8 | **拆两层读**:基础类型 = `n_type & N_TYPE(0x0e)`,取值 `0x0=N_UNDF`、`0x2=N_ABS`、**`0xE=N_SECT`**、`0xA=N_INDR`;属性位单独判——`N_EXT(0x01)` 外部、`N_PEXT(0x10)` 私有外部。**不要把组合值当基础类型**:`0x0f` = `N_SECT\|N_EXT`(外部的节定义符号),不是 undefined |
|
||||
| 0x05 | n_sect | u8 | 所在节序号(1 起) |
|
||||
| 0x06 | n_desc | u16 | 引用计数/库序号(two-level namespace) |
|
||||
| 0x08 | n_value | u64 | 地址或值 |
|
||||
|
||||
## fat(通用二进制)头
|
||||
|
||||
| 字段 | 大小 | 含义 |
|
||||
|---|---|---|
|
||||
| magic | u32 | **fat 结构在磁盘上恒为大端**(与宿主字节序无关):正常 FAT 文件开头**就是字节 `CA FE BA BE`**(大端读得 `FAT_MAGIC=0xcafebabe`);小端读这 4 字节得 `0xbebafeca`,即 `FAT_CIGAM`——**那是同一份字节被小端解释的结果,不是"小端文件"**。64 位 fat:`FAT_MAGIC_64=0xcafebabf`。**别用宿主的端序去猜文件端序** |
|
||||
| nfat_arch | u32 | 架构片数 |
|
||||
| 每条 fat_arch:cputype/cpusubtype/offset/size/align | 各 u32(**同样大端**) | offset 是指向该 slice 的绝对文件偏移;**`align` 是"对齐的 2 的幂"**——slice 必须满足 `2^align` 对齐,而不是固定 4 字节对齐(尤其 64 位 fat 用 `fat_arch_64`,另有 reserved 字段) |
|
||||
|
||||
## 版本差异要点
|
||||
|
||||
- 入口:老二进制 LC_UNIXTHREAD(0x05),新二进制 LC_MAIN(0x80000028)
|
||||
- 版本命令:LC_VERSION_MIN_MACOSX(0x24)→ LC_BUILD_VERSION(0x32,多 platform 字段)
|
||||
- dyld 重定位:LC_DYLD_INFO_ONLY 五组表(macOS 10.8+)→ LC_DYLD_CHAINED_FIXUPS 链式(iOS 13+/macOS 10.15+,减小启动开销)
|
||||
- GOT 位置:__DATA,__got → __DATA_CONST,__got(iOS 13+/macOS 10.14+)
|
||||
- 导出 trie 可独立成 LC_DYLD_EXPORTS_TRIE(LC_DYLD_INFO 的 export 字段为 0)
|
||||
|
||||
## 使用注意
|
||||
|
||||
- 静态解析无需沙箱;跨架构 Mach-O(arm64 片)在 x86 Linux 上可用 llvm-objdump/llvm-otool 直接解析,反汇编需交叉工具
|
||||
- 与 [[re-imports]](dylib 依赖/劫持面)、[[re-triage]](初勘)、[[re-lldb]](动态)配合使用
|
||||
Reference in New Issue
Block a user