黑盒下的逆向分析:利用 nm 与 Hopper 定位 Incode SDK 符号覆盖问题
不提供阅读源码权限,如何排查问题?
一 背景
在引入第三方Incode SDK的过程中,发现公司内埋点SDK,数据上报功能全部失效。
由于Incode不提供给我们阅读源码权限,因此无法直接分析其逻辑。
二 集团业务方调用链路
埋点SDK调用的是SSZipArchive SDK,具体方法是
1 | - (**BOOL**)writeData:(NSData *)data filename:(**nullable** NSString *)filename withPassword:(**nullable** NSString *)password |
接着调用的是zipOpenNewFileInZip5
打断点发现,没有执行这里的zipOpenNewFileInZip5方法。所以怀疑底层函数行为已经发生了变化。询问双方SSZipArchive SDK版本号,发现分别是2.2.3和2.5.4,git上可以搜索到这两个是跨了几年的大更新。
2.2.3是最后一个版本,后来代码仓库都换了。
1 | https://github.com/karmarama/SSZipArchive/tags |
三 incode SDK分析步骤
strings libIncdOnboarding.a | grep zipOpenNewFileInZip5
结果是zipOpenNewFileInZip5
可以看到有一个结果,说明该字符串存在于二进制中,可能来源于函数名、日志或其他常量,但不能证明该函数被实现或导出。
nm -o libIncdOnboarding.a | grep "zipOpenNewFileInZip5"
结果可以看到
1 | libIncdOnboarding.a:SSZipArchive.o: U zipOpenNewFileInZip5 |
结果分析:
U 代表这段代码在这个文件里只是被引用(调用)。
T 代表这段代码在 TEXT 段(代码段)中被真正定义(实现)了。
1 | T (Text Section)。 这里的 T 代表该函数真实的代码指令就存放在 mz_compat.o 的代码段(Text Segment)中。00000000000003bc 是它在这个 .o 文件里的相对内存地址。 |
所以SSZipArchive.o 仅仅是调用方。需要找到实际的定义。
1 | ar -x libIncdOnboarding.a mz_compat.o |
可以将mz_compat.o 从libIncdOnboarding.a中抽离出来。
不过其实使用Hopper Disassembler也可以直接拆分出这个。
打开Hopper Disassembler,按下 Option + Enter 生成类似 C 语言的伪代码(Pseudocode)。
Incode SDK 中的实现位于 mz_compat.o,而埋点 SDK 中未发现同名 object file,说明两者内部实现来源或封装方式不同。
四 问题原因
C语言处于扁平命名空间(Flat Namespace),在Mach-O中属于全局符号。当多个静态库包含同名全局 C 函数时,链接器在链接阶段(Link Time)进行符号解析(Symbol Resolution),可能导致符号被覆盖(Symbol Collision / Interposition),从而改变实际执行逻辑。
五 解决方案
符号前缀隔离方案(Symbol Prefixing)。通过 加前缀重命名所有相关函数(声明、实现、调用、头文件暴露)。
六 命令与软件介绍
- strings:二进制扫描;查找所有可打印字符串。
- nm:Mach-O符号表;查看函数/变量符号。
- otool:汇编级;查看机器指令。
nm -gU libIncdOnboarding.a | grep -i "zipOpenNewFileInZip5"
提取该函数的反汇编otool -tv libIncdOnboarding.a | grep -A 100 "zipOpenNewFileInZip5:"
objdump -t libIncdOnboarding.a | grep zipOpenNewFileInZip5
-t,–syms,查看符号表
otool (macOS/iOS) / objdump: 查看 Mach-O 或 ELF 文件的汇编代码、依赖库(otool -L)和头文件信息。
Hopper Disassembler / IDA Pro
如果不仅仅是看符号,还要看 zipOpenNewFileInZip5 在 2.5.4 版本到底改了什么内部逻辑,就可以用这些伪代码反编译工具。Hopper Disassembler:50M,价格US$99.00。
IDA Pro:770M,官方价格不菲,订阅制。个人版每年$365。
Hopper Disassembler 操作步骤
1 将二进制拖进软件,需要选择File。有Entry+ZIP64.o,URL + ZIP.o,FileManager+ZIP.o,Archive+ZIP64.o。
因为要查SSZip,所以选择SSZipArchive.o
可以看到Hopper Disassembler主界面的显示
1
2
3
4 zipOpenNewFileInZip5:
0000000000006518 **extern function code** ; CODE XREF=-[IncdZipArchive writeSymlinkFileAtPath:withFileName:compressionLevel:password:AES:]+404, +[IncdZipArchive _zipOpenEntry:name:zipfi:level:password:aes:]+192
CODE XREF=… (Cross Reference,交叉引用)。
双击方法名,比如writeSymlinkFileAtPath:,Hopper 会跳转到调用的那行汇编代码。
主界面可以看到调用顺序,右侧有简化的Control Flow Graph。
对incode的分析
Hopper Disassembler可以查看其实现的伪函数。
1 | int zipOpenNewFileInZip5(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7) { |
七 汇编操作含义
1. 提取 C 字符串 (UTF8String)
1 | mov x0, x21 ; 将某个 Objective-C 对象(可能是 NSString 实例)放入 x0 作为消息接收者 |
- 解释: 这一步相当于源码里的
const char *cString = [myString UTF8String];。将 OC 的字符串转换成了 C 语言的字符指针,返回值存放在x0寄存器中。这个字符串大概率是解压密码或者文件名。
2. 准备 zipOpenNewFileInZip5 的海量参数
minizip 库的 zipOpenNewFileInZip5 是一个参数极其庞大的函数(足足有 19 个参数)。
在 ARM64 架构的调用约定(Calling Convention)中:前 8 个参数放在 x0 到 x7 寄存器中,剩下的参数必须压入栈(Stack, 即 sp)中传递。
1 | ; ----- 这里是准备压入栈(sp)的参数 (第9个到第19个参数) ----- |
- 解释: 这段代码看似凌乱,其实就是在老老实实地凑齐
zipOpenNewFileInZip5所需要的 19 个参数。大部分额外字段都被写死了NULL(0x0)。
3. 执行核心调用
1 | bl zipOpenNewFileInZip5 ; 调用方法! |
4. 写入数据 (zipWriteInFileInZip)
打开了 Zip 里的空文件后,接下来要往里面写东西。
1 | ldr x24, [x22, #0x10] ; 从 x22 对象的偏移 0x10 处取出 zipFile 句柄,暂存到 x24 |
- 解释: 相当于 C 源码:
zipWriteInFileInZip(file, buffer, strlen(buffer));。它在把一段字符串(可能是一个符号链接的内容、或者简单的文本配置)写入到刚才创建的 Zip 文件条目中。
5. 关闭文件并判断结果
1 | ldr x0, [x22, #0x10] ; 再次取出 zipFile 句柄放入 x0 |
- 解释: 对应源码:
BOOL success = (openResult == ZIP_OK);。记录刚才打开文件的操作是否成功,供后续逻辑使用。
技术点总结
在无三方源码的限制下,使用 strings 命令进行全盘文本嗅探。
使用 nm 工具分析 Mach-O 符号表,精准判别符号的定义,锁定冲突源文件。
使用 ar 命令对庞大的静态库进行 .o 目标文件的抽离与精准降噪。
借助逆向工具Hopper Disassembler,提取其伪代码。
通过符号前缀隔离方案(Symbol Prefixing)解决命名冲突而导致符号覆盖的问题。