Mach-O 二进制检查器
像 otool 和 lipo 那样,直接从字节读取 Mach-O 二进制,全部在浏览器内完成。macOS 或 iOS 的文件要么是只含一个架构的瘦文件,要么是通用文件:一张大端的架构表,每一项指向一份完整映像。两种情况都支持;如果是通用文件,先给出带有各切片偏移、大小和对齐的架构表,选定一个切片后再展示其余细节。头部给出 CPU 类型与子类型(arm64、arm64e、x86_64、i386、ppc)、文件类型(可执行文件、动态库、插件包、目标文件、dSYM),以及值得关注的标志:MH_PIE、MH_TWOLEVEL、MH_DYLDLINK、MH_ALLOW_STACK_EXECUTION 和 MH_NO_HEAP_EXECUTION。加载命令给出其余信息:每个段的虚拟地址、内存大小、文件偏移与文件大小,段内的节(__TEXT,__text 或 __DATA,__const),每条 LC_LOAD_DYLIB 与 LC_LOAD_WEAK_DYLIB 的当前版本和兼容版本,@rpath 将被替换成的 LC_RPATH 路径,库在 LC_ID_DYLIB 中声明的安装名称,与 dSYM 配对的 UUID,以及来自 LC_BUILD_VERSION(或更早的 LC_VERSION_MIN_MACOSX)的平台、最低系统与 SDK。段大小就是“这个二进制为什么有 40 MB”的答案,但要注意内存大小可能远大于文件大小:__bss 之类的零填充节和 4 GB 的 __PAGEZERO 完全不占磁盘。有两件事它不做:不反汇编,也不验证代码签名。LC_CODE_SIGNATURE 只表示文件带有签名数据;签名是否有效、由谁的证书签发、是否公证,只有 Mac 上的 codesign 和 spctl 能回答。字节序和字长都从魔数读出而不是假定,所以二十年前的大端 32 位 PowerPC 二进制和 arm64e 一样能打开;如果加载命令表声称的字节数超过文件本身,会以具名错误拒绝,而不是读一半。
使用方法
- 把二进制拖进方框:没有扩展名的命令行工具、.dylib、.bundle、.o、dSYM 里的 DWARF 文件,或 .app 内部的可执行文件都可以。
- 如果是通用文件,先看架构表——这就是“有没有 arm64”的答案——然后点击某个架构,查看它的头部、段和依赖库。
- 查看加固标记:现代二进制的 PIE 应为“是”,而栈可执行在分发前值得给出解释。
- 在“链接的库”里看运行时需要什么,在“运行时搜索路径”里看 @rpath 会展开到哪些目录,在“安装名称”里看库为自己声明的路径。
- 追查体积时展开节列表:__TEXT 是代码,__DATA 是可写数据,__LINKEDIT 存放符号表与签名,而 dSYM 的 __DWARF 节通常是磁盘上最大的部分。
常见问题
- 怎么确认某个 App 是否包含 arm64(Apple 芯片)切片?
- 打开 .app 内的 Contents/MacOS/<名称>,查看架构表。通用文件以大端 fat 头开始,列出每个架构、它在文件中的偏移,以及该切片所对齐的边界;没有列出的架构就不在文件里,所以只有 x86_64 的应用会通过 Rosetta 运行而非原生运行。每个切片都是拥有自己头部和加载命令的完整 Mach-O 映像,因此通用二进制的体积大致等于各部分之和,而 lipo 可以只取出一个切片来瘦身。
- 有代码签名就说明这个二进制可信吗?
- 不是,这个工具刻意区分这一点。LC_CODE_SIGNATURE 只说明 __LINKEDIT 里的某个偏移处存在签名数据;它不说明哈希是否仍与所覆盖的页面吻合、证书是否能链到 Apple、证书是否被吊销,也不说明是否已公证。要验证这些需要完整的证书链,公证还需要 Apple 的服务器,也就是要在 Mac 上运行 codesign --verify --deep --strict 和 spctl --assess。强化运行时(hardened runtime)也不在头部,而在签名的授权与标志里,所以仅凭头部无法判断它是否开启。
- 为什么二进制比里面的代码大得多?
- 把段表的两列大小放在一起看。__TEXT 是代码和只读数据,通常按读取加执行映射;__DATA 与 __DATA_CONST 是可写数据;__LINKEDIT 存放符号表、function starts、dyld 修正信息和代码签名,在发布版二进制里常常是最大的一块。内存大小与文件大小不同是有意为之:__bss 这类零填充节只占地址空间,不占磁盘字节;__PAGEZERO 保留的 4 GB 永远不会从文件映射。如果是通用文件,还要记得你同时携带了所有架构。
- 什么是安装名称,为什么到处都是 @rpath?
- 动态库在 LC_ID_DYLIB 里记录自己期望被找到的路径,链接它的程序会把这串字符原样复制到自己的 LC_LOAD_DYLIB。如果是绝对路径,那么每台机器上都必须恰好存在那个位置。现代框架改用 @rpath/Something.framework/Something,把解析交给程序:这里列出的 LC_RPATH 就是 dyld 依次用来替换 @rpath 的目录,通常用 @executable_path 或 @loader_path 写成相对于二进制的形式,这样应用包可以随意移动。启动时报“image not found”,最常见的原因就是缺少 rpath。
- iOS 二进制上的“已加密”标记是什么意思?
- App Store 分发的 iOS 二进制带有 cryptid 为 1 的 LC_ENCRYPTION_INFO(或其 64 位形式),标记一段字节范围——通常是 __TEXT 的大部分——已用商店施加的密钥加密。内核在加载时解密该范围,因此磁盘上的这段区域不可读:strings 找不到有用内容,反汇编器只能看到噪声。自己编译的二进制,或在商店重新签名前从 IPA 取出的二进制,cryptid 为 0,是明文。很多文件里这条命令存在但取值为零,所以这里报告的是 cryptid 而不仅仅是命令是否存在。
- 二进制会被上传到什么地方吗?
- 不会。文件通过浏览器的 File API 读取,由页面内的 JavaScript 解析。没有任何内容被发送到服务器,也不存在可发送的服务器端组件。这一点在这里比在多数工具中更重要:尚未发布的构建或客户的应用,你不会想交给一个网站。页面加载完成后断网,所有功能依然可用。
相关工具
ELF 二进制检查器
在浏览器中打开 Linux 可执行文件、.so 或 .o:架构、依赖的共享库、构建 ID,以及是否启用 PIE、NX 和 RELRO。
PE 检查器:Windows EXE 与 DLL 查看
在浏览器里打开 Windows 的 .exe、.dll、.sys 或 .efi:架构、子系统、导入表、导出表、节,以及是否开启 ASLR、DEP 和 CFG。
Java 类文件分析器
在浏览器中解析编译好的 .class 文件:类文件版本与对应的 Java 版本、访问标志、常量池、字段、方法和引用的类。
字体文件检查器
打开 .ttf、.otf 或 .woff,读取真实字族名、字形与字符数量、版本、许可文本以及嵌入权限。
gzip(.gz)结构检查器
逐字段读取 .gz 头部,遍历每个成员,并在浏览器中重新计算 CRC32 和真实大小来校验尾部记录。
xz(.xz)流结构分析器
读取 .xz 容器:流头部、每个块的头部与过滤器链、索引和尾部,并给出无需解压就能确定的真实原始大小。