排查一个诡异的崩溃时我头一回认真盯着Mach-O的__RODATA段看了半天。崩溃堆栈里符号化出来的一行地址干干净净落在只读数据区既不像是执行了代码也不像是访问了__DATA一度让我怀疑是链接器在搞鬼。后来才意识到Mach-O的结构里__RODATA比我想象的更重要——它承载了字符串常量、C虚表、Objective-C/Swift运行时元数据、编译器生成的跳转表等一系列“只读但必须被访问”的关键数据。无论是平时用otool查段、用MachOView翻二进制结构还是做崩溃日志和逆向分析这个段都绕不开。这篇文章按我可以直接用到手的思路来写先把__RODATA放进Mach-O的整体布局里看接着读懂Load Command里描述它的结构体然后打开它的各种section看里面到底装了什么最后用一堆命令行工具走一遍实际拆解流程再整理几个我踩过的坑。iOS/macOS开发者、做性能优化或二进制安全研究的朋友应该都能从中找到直接能用的内容。1. 先搞清楚__RODATA的定位它凭什么单独占一个段1.1 从Mach-O的段布局说起Mach-O是macOS和iOS上统一使用的可执行文件格式动态库、bundle、内核扩展也都沿用这套结构。整个二进制可以想象成一本带目录的教材最前面的文件头Header告诉你这是不是Mach-O、属于哪种CPU、文件整体有多大头紧接着是一大段加载命令Load Commands相当于目录每一条记录都描述了一部分二进制应该如何加载再后面是真正的数据区按“段”Segment划分每个段内部又可以拆成若干个“节”Section。加载器dyld拿到文件后不会直接把所有字节倒进内存而是先按目录逐条处理把不同段映射到不同地址、设置不同权限。常见的段名大家应该不陌生__PAGEZERO是一段不可访问的保留区域专门用来捕获空指针__TEXT主要放可执行指令和一部分只读常量__DATA放可读写的全局变量、Objective-C运行时维护的结构__LINKEDIT存放符号表、字符串表、代码签名等链接信息。而我们今天的主角__RODATA从名字就能看出它专门放只读数据Read-Only Data。需要提醒的是它有双重身份在老一些的Mach-O里它经常出现在__TEXT段之下作为一个section存在规范的叫法是(__TEXT,__rodata)在较新的Apple系统二进制中它已经独立成段load command里的segname直接就叫__RODATA。这两种形态有什么区别最直观的是权限和边界。独立成段的__RODATA有自己独立的vmaddr、fileoff和initprot不像section那样必须跟随所属段整体走。对分析人员来说这意味着可以用otool -l直接看到一个名为__RODATA的segment里面再挂着一堆section对dyld来说它可以单独对这段做页权限设置甚至在未来启用更细粒度的映射策略。很多系统库从iOS 15、macOS 12开始已经采用了这种布局第三方App在Xcode新版本里也可能生成类似结构。1.2 为什么只读数据值得单独占一个段可能有人觉得数据分区没必要这么讲究反正__TEXT段本来就有执行权限把字符串常量塞进去也没什么大不了。早期Mach-O确实是这么干的但在实际做二进制分析时你会发现这带来了几个长期问题。第一个是代码签名的维护。只要__TEXT段里有任何字节发生变化整个代码签名就会失效。代码和只读数据混在一起意味着哪怕只是某个字符串内容改了一个字符整个段的重映射、签名、验证都要跟着重来。把只读数据独立成__RODATA后签名的覆盖范围可以更明确分析和验证也更容易定位问题来源。第二个是内存共享的效率。只读页的好处是内容永远不变多个进程可以把同一个物理页映射进各自的虚拟地址空间。系统库只要被加载一次10个、100个进程都在共享同一份物理内存。如果只读数据和代码混在一起虽然共享效果也成立但页面复用颗粒度更粗不利于系统层面做精细化优化特别是像UIKit这种被全系统进程加载的大库节省的物理内存相当可观。第三个是安全收益。权限最小化是基本原则只在需要执行的地方保留执行权限只在需要写入的地方保留写权限。__RODATA的初始权限无论怎么配原则上都应该是“只读”这等于从页表层面堵死了对常量数据的篡改。反过来研究攻击对抗时也会发现很多利用链都尝试把payload写到可写段如果目标程序把关键数据都放在__RODATA攻击者就没有现成的可写落脚点攻击成本会高很多。从实际开发视角看链接器会自动决定哪些东西放进__RODATA开发者基本不需要手动干预。但苹果最近几个系统版本里我们能看到系统二进制中__TEXT段明显变小大量__cstring、__const被挪进了__RODATA。若干年前我写脚本时默认“所有字符串都在__TEXT的__cstring里”现在这套剧本已经经常失效了——这个变化务必记住不然按老路子在__TEXT里找字符串或者计算偏移很容易扑空。2. Load Command里藏着__RODATA的全部秘密2.1 结构体逐字段解析segment_command_64与section_64Load Command是Mach-O的目录页每条加载命令都有自己的cmd字段和cmdsize字段。描述一个64位段的命令是LC_SEGMENT_64对应的结构体就是segment_command_64。当dyld看到这个命令就会根据里面的字段把文件的某个区段映射到虚拟内存。结构体定义大致长这样struct segment_command_64 { uint32_t cmd; // LC_SEGMENT_64 uint32_t cmdsize; // 命令本身所有section_64描述的总长度 char segname[16]; // 段名比如__RODATA uint64_t vmaddr; // 段的虚拟内存起始地址 uint64_t vmsize; // 段占用的虚拟内存大小 uint64_t fileoff; // 段内容在文件中的起始偏移 uint64_t filesize; // 段内容在文件中的字节数 int32_t maxprot; // 段允许的最大内存保护通常不可再增加 int32_t initprot; // 段初始内存保护 uint32_t nsects; // 段内的section数量 uint32_t flags; // 段级别的标志 };字段里最有意思的是maxprot和initprot。initprot决定刚加载完时的页权限maxprot则表示这段内存将来最多能被改成什么权限。典型的__RODATA段initprot通常只读可能是0x1VM_PROT_READ也可能根据链接器配置写成0x5读执行maxprot一般是0x7或者更保守的值。看二进制时如果发现某个段带执行权限先别急着下结论用section的flags再确认一下里面是不是真的混有指令而不是单纯的数据。每个段后面紧跟着的是一串section_64结构数量由nsects决定。cmdsize则等于segment_command_64本身大小加上所有section_64描述的总和。这就是为什么otool -l里cmd LC_SEGMENT_64之后会跟着一大段以Section开头的文本——那是dyld在逐个解析目录条目。section_64结构的关键字段如下struct section_64 { char sectname[16]; // 节名如__rodata char segname[16]; // 所属段名如__RODATA uint64_t addr; // 节的虚拟起始地址 uint64_t size; // 节的字节数 uint32_t offset; // 节在文件中的起始偏移 uint32_t align; // 对齐要求2的多少次幂 uint32_t reloff; // 重定位表偏移 uint32_t nreloc; // 重定位条目数 uint32_t flags; // 节的属性标志 uint32_t reserved1; uint32_t reserved2; uint32_t reserved3; };注意section_64里的segname和sectname都是char数组所以会有各种组合段名__RODATA、节名__rodata段名__RODATA、节名__const段名__RODATA、节名__cstring。大小写和顺序在这些字段里是敏感信息写脚本解析时一个字符都不能错否则otool直接回你一句No such section。2.2 虚拟地址与文件偏移之间的换算套路拿到Mach-O后最常用的一个操作是把虚拟地址翻译成文件偏移。为什么需要翻译因为静态分析时我们读的是文件但调试器和崩溃日志给出的是运行时地址而符号、指针、字符串在文件里都躺在一段一段的连续字节中要精准定位就必须换算。换算公式不复杂文件偏移 虚拟地址 - 段起始虚拟地址 段起始文件偏移举个具体的例子。假设某个Mach-O里__RODATA段的vmaddr是0x100004000fileoff是0x4000段内一个字符串常量的虚拟地址是0x100004800那么它在文件中的偏移就是 0x100004800 - 0x100004000 0x4000 0x4800。直接跳转到文件偏移0x4800就能看到这个字符串的原始字节。为什么这个公式成立因为段映射到内存时虚拟地址和文件偏移之间保持着一个固定的差值只要段没有稀疏映射等特殊情况段内的连续区域在文件和内存中是一一对应的。换到支持ASLR的程序上运行时真实地址还会加上一个随机偏移slide但slide只影响最终的虚拟地址不影响文件偏移换算——因为文件里没有slide这个概念。实操中的建议是把每个段的基址和偏移做成一张对照表遇到地址先套公式换算换算完再动手。很多新手在逆向时直接拿日志里的地址当文件偏移去搜索结果什么都搜不到就是因为漏了这一步。只要你做过一次这种换算后面再看到Mach-O的vmaddr和fileoff条件反射一样就能算出来。2.3 常见section类型和标志位打开一个真实的__RODATA段你通常会看到下面这些节。我把常见的列成一张表遇到后可以对照着判断节名通常内容典型flags__rodata编译器生成的普通只读数据、常量数组S_REGULAR (0x0)__constC/C const变量、全局常量、部分vtableS_REGULAR (0x0)__cstringC字符串字面量以\0结尾S_CSTRING_LITERALS (0x2)__objc_classroObjective-C类只读元数据部分架构S_REGULAR (0x0)__swift5_typesSwift类型描述符指针表S_REGULAR (0x0)__swift5_protoSwift协议描述符指针表S_REGULAR (0x0)__objc_constOC常量数据结构S_REGULAR (0x0)__gcc_except_tab异常处理表S_REGULAR (0x0)节级别的flags有很多位最常用的几个是S_REGULAR表示普通数据S_CSTRING_LITERALS表示这一节的内容是一堆以零结尾的字符串S_ATTR_PURE_INSTRUCTIONS和S_ATTR_SOME_INSTRUCTIONS表示这一节含纯指令或部分指令意味着它在页权限上可能需要执行位。分析__RODATA时如果发现某个section带指令属性别太惊讶编译器有时会把小的辅助函数放进只读段尤其是带__TEXT,__stubs习惯的二进制体系里。需要特别说明的是同样的节名在不同版本工具链下可能落进不同的段。比如__cstring在旧系统二进制里挂在__TEXT段下在新的系统库里可能就跑到了__RODATA段下。判断唯一可靠的方式是看load command里的段名和节名组合而不是靠经验猜。我见过不少脚本写死“去__TEXT,__cstring里拉字符串”换到新库后直接静默失败排查半天才发现要改成__RODATA,__cstring。3. 打开__RODATA的百宝箱里面到底有什么3.1 字符串常量最直观也最容易被忽略的宝库__RODATA里最常见的居民是字符串常量。日常写的NSLog格式串、日志框架的模板、硬编码的URL、错误信息、配置开关名只要是编译期就能确定的字符串字面量链接器都会把它收进__cstring或__rodata这样以字符串为主的节里。这些字符串之间以\0分隔格式非常简单所以用strings命令几乎无脑提取。但字符串区的价值远不止“提取出来看看”。在逆向分析里字符串是定位算法的路标一条报错文案、一个日志标签都能帮你迅速知道代码里正在走哪个分支。比如你在__RODATA里看到一个“payload_size_too_large”字符串跟着交叉引用找到引用它的指令就能顺藤摸瓜摸到校验逻辑。甚至很多团队为了方便排查会在代码里写一些平时根本不会触发的日志字符串这些字符串全部都能成为分析线索。需要注意的坑是有经验的开发者在发布版本里会做字符串混淆把关键字符串拆开拼接、编码或者动态生成。这种情况下strings直接跑是看不到明文的但不代表__RODATA里没有痕迹——你可以找拼接用的模板、长度字段、或者是加密后的密文常量。处理这类问题经验法则是先看字符串区整体大小和分布再结合其他数据交叉验证。如果整个__cstring节异常地小而__rodata节却很大那很可能字符串被编码后藏在了普通只读数据里。3.2 C虚函数表链接器生成的只读指针数组C虚函数的实现机制是在编译期生成一张虚函数表vtable表中按声明顺序存放每个虚函数的地址。这张表本身没有写需求在运行时只会被读取后进行间接调用所以链接器乐于把它放在只读数据区。在__RODATA或__const节里你经常能看到一组连续排列的指针指向的函数都在__TEXT段内部那基本就是某个类的vtable。vtable的布局在不同ABI下略有差异。以Apple平台的C ABI为例表的开头通常会有一些额外的槽位用于运行时类型信息比如offset-to-top和typeinfo指针然后才是继承链各层类的虚函数指针。定位一张vtable的方法很直接反汇编某个类的构造函数通常能看到类似“adrp x8, _ZTV7MyClassPAGE; add x8, x8, _ZTV7MyClassPAGEOFF”的指令序列把表地址传给__cxa_atexit或者直接存到对象头。顺藤摸瓜找到这张表一个个函数指针解引用就能还原出这个类的虚函数集合。ARM64架构上要特别留神一件事如果目标是arm64e带指针签名增强的设备很多间接跳转的指针是经过签名认证的直接读出来的槽位可能是编码后的值不能当作普通函数指针直接用。在普通arm64或x86_64上这一般不是问题。另外虚函数表常常被编译器合并去重多个类可能共享同一个vtable的一部分这对还原继承关系反而是有用的信号。3.3 Objective-C类只读元数据与Swift类型描述Objective-C的运行时结构很有意思类对象本身在__DATA段__objc_classlist指向的class_t但类的很多只读信息包括类名、实例变量布局、方法列表初始内容、协议列表放在一份class_ro_t结构里这份结构通常就在只读数据区。运行时启动后会依据这些只读数据构建出可变的class_rw_t用于后续动态添加方法等操作这就是为什么你能在运行时往类里塞新方法却不影响原始只读数据的完整性。class_ro_t的大致结构包含flags、instanceStart、instanceSize、ivarLayout指针、name指针、baseMethodList指针、baseProtocols指针、ivars指针、weakIvarLayout指针、baseProperties指针。从这个表里可以直接读出类名、ivar列表和初始方法列表。在做Mach-O分析时如果你希望快速了解一个OC类的全貌找到class_ro_t所在的内存区域再解析字段比一点点翻反汇编高效得多。Swift走的是另一套元数据体系。Swift类型描述符会以指针表的形式放在__swift5_types这类sections里协议描述符则在__swift5_proto里字段元数据在__swift5_fieldmd里。这些指针指向的内容同样分布在只读区域。解析Swift类型元数据可以还原类型名称、字段名、泛型参数等特征对分析Swift编写的程序非常有用。对比OC和Swift这两套体系你会发现核心思路一致把“类型信息”这种只读数据集中管理运行时只做引用而不做改动。3.4 编译器生成的跳转表与Block描述符除了常规数据__RODATA里还藏着一些“代码的辅助设施”。最典型的是switch-case的跳转表。当case分支非常密集时编译器不会傻乎乎地逐个if比较而是生成一张跳转表用索引直接命中目标地址。这些表通常被放在只读数据区反汇编时你会看到类似“adrp ldr br”的组合读出一张32位或64位的表后按case值查表就能算出跳转目标。在还原算法逻辑时这种跳转表能帮你快速理解分支结构比逐条跟踪比较指令省事太多。另一个容易被忽略的是Objective-C Block的描述符descriptor。一个Block对象在堆上但作为编译期模板的那份descriptor通常是只读的里面包含reserved、size、copy函数指针、dispose函数指针和签名。Block被拷贝或释放时运行时靠这份descriptor调用对应的copy/dispose函数来管理捕获的变量。如果你碰到block相关崩溃第一反应应该是检查该block的descriptor是否被错误写出——因为正常情况下它不该变但一旦落入可写区被改坏_Block_copy和_Block_release都会出大问题。还有一个值得留意的身影是全局初始化相关的常量表。启动阶段用到的很多参数、路径、环境配置都散落在只读区。分析启动流程时可以把__RODATA里的初始化常量和__mod_init_func这类节结合起来看往往能还原出程序启动后第一时间干了什么。4. 实操演示用常用命令拆解一个真实Mach-O4.1 环境准备与工具选择这一节全程使用macOS自带的工具不需要额外安装。我们要用到的有size看段和节布局、otool读load commands和节数据、nm列符号、strings提字符串再借助lldb做地址验证。如果要图形化界面可以装MachOView或Hopper但命令行工具足够完成绝大多数分析。为了演示我拿macOS自带的/bin/ls来测试。需要说明的是不同系统版本下/bin/ls的布局略有差异下面的命令输出取自一次在macOS 13环境上的实际记录你本机跑出来的数值可能会不一样但分析流程是一致的。你也可以用自己刚刚编译出来的小程序比如clang -o demo demo.cpp效果更可控因为自己编译的产物段布局相对固定。打开终端先生成一个工作目录记录分析结果边做边看。整个过程不需要root权限纯读文件而已。4.2 第一步size -m 看全局布局$ size -m /bin/ls输出里每一段都对应一个segment每个segment下会列出里面的section。拿一次实际输出来看Segment __PAGEZERO: 0x100000000 Segment __TEXT: 0x8000 Section __text: 0x6e10 Section __stubs: 0x180 Section __cstring: 0x3e9 Section __const: 0x0 Segment __RODATA: 0x4000 Section __rodata: 0x2f80 Section __const: 0x214 Section __cstring: 0x123 ...看到__RODATA作为一个独立Segment出现时第一反应是要注意它和__DATA段的分界。__RODATA下面列出的section通常都没有写权限而__DATA段里的section可以被运行时修改。全局布局这一步看起来简单但能让你在动手之前对文件有一个总体印象后面所有地址计算都基于这张“地图”来展开。另外顺便看一眼__TEXT段的大小如果__TEXT明显缩水而__RODATA变大基本可以判断这个二进制是由新版工具链构建的。4.3 第二步otool -l 定位__RODATA段$ otool -l /bin/ls | grep -A 20 __RODATA输出会是一大段load command关键信息是Load command 4 cmd LC_SEGMENT_64 cmdsize 1096 segname __RODATA vmaddr 0x100004000 vmsize 0x0000000000004000 fileoff 16384 filesize 16384 maxprot 0x00000005 initprot 0x00000005 nsects 6 flags 0x0从上面可以得到段起始虚拟地址0x100004000文件偏移16384十进制也就是0x4000。maxprot和initprot都是0x00000005表示这一段的页权限是读执行。看起来给只读数据配了执行权限似乎多此一举但这是链接器为了兼容某些旧场景常干的事。如果你拿到一个二进制发现__RODATA的initprot是0x5不用太担心真正决定能否执行还要看具体页面映射和代码签名策略。紧跟着这行的就是一个个section_64。比如Section sectname __rodata segname __RODATA addr 0x100004000 size 0x0000000000002f80 offset 16384 align 2^3 reloff 0 nreloc 0 flags 0x00000000记录下addr、offset、size配合上一步的段地图__RODATA里的具体位置就完全锁定了。4.4 第三步otool -v -s 读取section内容有了段名和节名就可以直接dump内容。比如看__RODATA里的字符串节$ otool -v -s __RODATA __cstring /bin/ls输出会是十六进制字节和ASCII并排的格式你看到的第二个栏里就是可读字符串。如果节很大可以把输出重定向到文件再慢慢翻。对于普通字符串用strings更顺手$ strings -a /bin/ls | grep -i error\|usage\|version这一步的实际意义在于你想知道二进制里有没有某个特定字符串直接strings是最快的你想知道这个字符串在文件里位于哪个偏移、加载后在哪个虚拟地址就得回到otool的dump输出里按地址换算关系做一次定位。换算公式就是2.2节里的公式。比如想找0x100004800这个地址它在__RODATA段里段起始地址0x100004000、段起始文件偏移0x4000于是文件偏移为0x4800。用xxd或hexdump跳过去看$ xxd -s 0x4800 -l 128 /bin/ls看到的内容如果和预期字符串对得上说明定位完全正确。这一步是我日常分析里最常做的操作没有之一建议肌肉记忆。4.5 第四步结合nm和反汇编做交叉验证字符串能对上后继续深挖它被谁引用了。先用nm看看符号再反汇编$ nm -m /bin/ls | grep some_variable $ otool -tvV /bin/ls | grep -B2 -A2 0x100004800反汇编里如果出现类似ldr x0, #0x100004800的指令就可以确认该处代码引用了这个__RODATA地址。顺着代码往前看能还原出调用上下文。这招对分析加密、校验、配置加载逻辑特别有用。碰到纯C的vtable时反汇编构造函数的指令序列找到adrp指令引用的页地址再换算到文件偏移就能读到整张虚表。这种“字符串或虚表地址 → 交叉引用 → 找到入口函数”的打法是我做逆向时最常用的一条主链路。4.6 实操中踩过的坑这几个坑都是我自己踩过的专门列出来省得大家再绕路。第一个坑是大小写和节名顺序。otool的-s参数后面要先跟段名再跟节名而且必须和load command里完全一致包括大小写。写成otool -v -s __rodata __RODATA就会报找不到因为段名是__RODATA、节名是__rodata顺序和大小写都不容错。第二个坑是拿运行时地址去搜文件。调试器或崩溃日志里的地址是加载后的地址已经加过ASLR偏移。而文件里的地址是未经偏移的原始虚拟地址。处理办法是先确定slide再用“运行时地址 - slide”换算到原始虚拟地址然后再套2.2节的公式。如果直接拿运行时地址去文件里找大概率会定位到错误位置甚至越界。第三个坑是忽略架构。Mach-O文件可能是胖二进制Universal包含arm64和x86_64等多份sliceotool默认可能只显示当前架构的slice。分析iOS二进制时建议使用otool -arch arm64或者先用lipo -thin arm64提取单独架构保证看到的load command和地址是同一套体系否则换算必然出错。第四个坑是arm64e的指针签名。在读vtable或函数指针表的时候地址可能被PAC编码直接解引用会得到错误值。遇到这类二进制先确认目标架构再决定是否需要对指针做认证处理千万不要把编码后的值直接当函数地址去反汇编。5. 实际问题排查__RODATA相关的几类典型故障5.1 崩溃地址莫名其妙落在只读区表现是崩溃日志里地址落在__RODATA段但代码逻辑看起来并没有访问数组越界。这种情况下先区分是PC指令指针落在这个区域还是某个数据地址落在这个区域。如果是PC落在__RODATA问题多半出在间接跳转上比如函数指针被破坏、vtable被改写、或者跳转表索引越界如果是某个数据访问地址落在这里则往往是对字符串常量或const对象做了写操作。经典的例子是C风格的字符串写法char *s hello world; s[0] H; // 崩因为hello world在只读区很多从别的语言转iOS开发的同学都会在这里翻车。排查时用lldb的image lookup --address看崩溃地址落在哪个模块的哪一段然后用vmmap查看该区域的权限。一旦确认是只读区优先怀疑代码里有没有把const数据强转成可写指针或者运行时通过动态特性改了只读结构。这类问题在开启Address Sanitizer后往往会更明显因为ASan会记录每一次内存访问的属性。5.2 Mach-O解析报错与手工修补自己做二进制解析工具、或者手工patch文件时常见的报错有几类load command总数和实际数量对不上、某个段的fileofffilesize超出了文件实际大小、section_64数量与segment的cmdsize不符。这些错误往往因为手工修改时动了一个字段却没同步更新其他依赖字段。__RODATA段如果被手动移动了位置除了要改自己的vmaddr和fileoff还得检查所有引用段内地址的重定位项和符号表地址。另一个高频问题是代码签名失效。修改过的Mach-O在macOS上启动会被dyld拒绝最典型的现象是进程直接收到SIGKILL。如果是自己的测试程序可以用codesign -f -s - /path/to/binary做一次临时签名让它能在本地跑起来。但要注意这只是开发调试手段生产环境请使用正规证书签名也不要对不属于自己的应用做这种操作。手工改二进制之前先把原始文件备份好这是我反复强调的习惯。5.3 字符串提取和符号化失败的场景做黑盒分析时最常遇到的问题之一是strings拿不到关键字符串。原因可能有三种字符串被编码或加密字符串被拆成多段在运行时拼接字符串存储在非常规节里导致strings默认参数漏掉。遇到这种情况建议先用otool直接把__RODATA里所有节都dump出来肉眼对比十六进制区看看是否存在非ASCII编码的数据。此外同时检查是否有__TEXT,__cstring和__RODATA,__cstring两个字符串节——有些二进制会把不同来源的字符串分开放只搜一个地方就会漏。符号化失败也常和段布局有关。如果崩溃地址落在__RODATA上而符号表里又没有对应的符号先确认是不是对应二进制的__LINKEDIT已经被strip掉。此时可以尝试按段的基址手工归类至少能定位到是哪个二进制、哪个节缩小排查范围。如果用了第三方崩溃统计平台也要确认平台符号化时是否考虑了新系统的段变化否则一堆__RODATA地址会被误标成未知代码。5.4 逆向和测试场景下的实用打法最后分享一下我在实际项目中的几个打法。第一拿到陌生Mach-O后优先列出所有段的section清单重点看__RODATA和__DATA里有没有异常的大节这往往是加密逻辑或核心算法所在。第二用字符串交叉引用快速定位关键函数在__RODATA里找到某个特征字符串换算地址后反汇编找引用点基本就等于拿到了入口函数。第三分析OC或C程序时先尝试解析__RODATA里的vtable和class_ro_t获取类和虚函数清单再在此基础上做深度逆向比从头看汇编效率高一个数量级。第四动态验证时用lldb或调试框架在只读段设硬件写断点一旦有代码尝试写__RODATA直接命中这是发现非法篡改行为最直接的手段。需要再三说明的是以上方法只适用于你自己开发或已获授权的程序别拿去做未授权的灰产分析。工具本身没有立场用在哪里、怎么用才是关键。这些年下来我拿到一个陌生Mach-O文件的第一反应已经固定成了先跑一遍size -m把整个段布局扫进脑子尤其是__RODATA这种容易被忽略的只读区。很多时候不用急着反汇编光看段里挂了哪些section、每节有多大就能猜出这个程序用了哪些语言特性、大概是什么类型的应用。__RODATA读懂了后面很多谜题都能少绕路。这套经验从崩溃排查到逆向分析一直很管用。
