KangQingYu
Articles57
Tags21
Categories9
黑盒下的逆向分析:利用 nm 与 Hopper 定位 Incode SDK 符号覆盖问题

黑盒下的逆向分析:利用 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
2
3
https://github.com/karmarama/SSZipArchive/tags

https://cocoapods.org/pods/SSZipArchive

三 incode SDK分析步骤

strings libIncdOnboarding.a | grep zipOpenNewFileInZip5
结果是
zipOpenNewFileInZip5

可以看到有一个结果,说明该字符串存在于二进制中,可能来源于函数名、日志或其他常量,但不能证明该函数被实现或导出。

nm -o libIncdOnboarding.a | grep "zipOpenNewFileInZip5"
结果可以看到

1
2
libIncdOnboarding.a:SSZipArchive.o:                  U  zipOpenNewFileInZip5
libIncdOnboarding.a:mz_compat.o: 00000000000003bc T 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
int  zipOpenNewFileInZip5(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7) {
r7 = arg7;
r6 = arg6;
r5 = arg5;
r2 = arg2;
r1 = arg1;
r0 = arg0;
if (r0 != 0x0) {
r31 = r31 - 0xf0;
var_60 = r28;
stack[-88] = r27;
var_50 = r26;
stack[-72] = r25;
var_40 = r24;
stack[-56] = r23;
var_30 = r22;
stack[-40] = r21;
var_20 = r20;
stack[-24] = r19;
var_10 = r29;
stack[-8] = r30;
r29 = &var_10;
r20 = r7;
r21 = r6;
r22 = r5;
r23 = r2;
r24 = r1;
r19 = r0;
r26 = *(int128_t *)(r29 + 0x40);
r25 = *(int128_t *)(r29 + 0x48);
r27 = *(int32_t *)(r29 + 0x10);
if (r2 != 0x0) {
r0 = *(int32_t *)r23;
if (r0 == 0x0) {
r0 = mz_zip_tm_to_dosdate(r23 + 0x8);
}
mz_zip_dosdate_to_time_t(r0);
}
r28 = *(int32_t *)(r29 + fill_win32_filefunc64A);
r23 = *(r29 + fill_win32_filefunc);
r8 = "-";
if (r24 == 0x0) {
if (!CPU_FLAGS & E) {
r8 = r24;
}
else {
r8 = "-";
}
}
if (r20 != 0x0) {
_strlen(r20);
}
r9 = *(int128_t *)(r29 + fill_fopen64_filefunc);
r8 = *(int128_t *)(r29 + 0x18);
if (r28 == 0x0) {
asm { cinc w10, w10, eq };
}
r0 = *(r19 + 0x8);
r0 = mz_zip_entry_write_open(r0, &var_F0, sign_extend_64(r9), r8 & 0xff, r23);
}
else {
r0 = 0xffffff9a;
}
return r0;
}

七 汇编操作含义

1. 提取 C 字符串 (UTF8String)

1
2
3
mov        x0, x21      ; 将某个 Objective-C 对象(可能是 NSString 实例)放入 x0 作为消息接收者
bl _objc_retainAutorelease ; ARC 机制:保留并自动释放,确保字符串在执行期间不被销毁
bl _objc_msgSend$UTF8String ; 调用 [NSString UTF8String]
  • 解释: 这一步相当于源码里的 const char *cString = [myString UTF8String];。将 OC 的字符串转换成了 C 语言的字符指针,返回值存放在 x0 寄存器中。这个字符串大概率是解压密码或者文件名

2. 准备 zipOpenNewFileInZip5 的海量参数

minizip 库的 zipOpenNewFileInZip5 是一个参数极其庞大的函数(足足有 19 个参数)。

在 ARM64 架构的调用约定(Calling Convention)中:前 8 个参数放在 x0x7 寄存器中,剩下的参数必须压入栈(Stack, 即 sp)中传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; ----- 这里是准备压入栈(sp)的参数 (第9个到第19个参数) -----
str wzr, [sp, #0x4f0 + var_4B0] ; 往栈里存 0 (wzr 是 zero register)
mov w8, #0x300 ; 准备一个常量 0x300
stp x8, xzr, [sp, #0x30] ; 把 0x300 和 0 压入栈
strb w24, [sp, #0x4f0 + var_4C8] ; 存入一个布尔值或字节
stp x0, xzr, [sp, #0x18] ; 注意这里!把刚才 UTF8String 的结果 (x0) 压入栈了,说明密码或注释作为后面的参数传递了
mov w8, #0x8
mov x9, #0xfff100000000
movk x9, #0xffff, lsl #48
stp x9, x8, [sp, #0x8] ; 压入一系列标志位 (flags / method 等参数)
stp w8, w23, [sp]

; ----- 这里是准备寄存器的参数 (前8个参数) -----
add x2, sp, #0x50 ; 参数2 (x2):将栈上的一个地址传给 x2。这通常是指向 zip_fileinfo 结构体的指针
mov x0, x25 ; 参数0 (x0):zipFile file (Zip 文件的句柄)
mov x1, x26 ; 参数1 (x1):const char* filename (要在 Zip 里创建的文件名)
mov x3, #0x0 ; 参数3 (x3):extrafield_local = NULL
mov w4, #0x0 ; 参数4 (x4):size_extrafield_local = 0
mov x5, #0x0 ; 参数5 (x5):extrafield_global = NULL
mov w6, #0x0 ; 参数6 (x6):size_extrafield_global = 0
mov x7, #0x0 ; 参数7 (x7):comment = NULL
  • 解释: 这段代码看似凌乱,其实就是在老老实实地凑齐 zipOpenNewFileInZip5 所需要的 19 个参数。大部分额外字段都被写死了 NULL (0x0)。

3. 执行核心调用

1
2
bl          zipOpenNewFileInZip5 ; 调用方法!
mov x23, x0 ; 将返回值 (通常是 int) 备份到 x23 寄存器里。0 一般代表 ZIP_OK。

4. 写入数据 (zipWriteInFileInZip)

打开了 Zip 里的空文件后,接下来要往里面写东西。

1
2
3
4
5
6
7
8
9
ldr        x24, [x22, #0x10]          ; 从 x22 对象的偏移 0x10 处取出 zipFile 句柄,暂存到 x24

add x0, sp, #0x98 ; x0 指向栈上的一个字符串缓冲区
bl _strlen ; 调用 strlen(x0) 获取字符串长度
mov x2, x0 ; 将长度 (返回值) 放入 x2,作为 write 函数的 len 参数

add x1, sp, #0x98 ; 将刚才的字符串缓冲区地址放入 x1,作为 write 的 buf 参数
mov x0, x24 ; 将 zipFile 句柄放入 x0
bl zipWriteInFileInZip ; 调用 zipWriteInFileInZip(file, buf, len)
  • 解释: 相当于 C 源码:zipWriteInFileInZip(file, buffer, strlen(buffer));。它在把一段字符串(可能是一个符号链接的内容、或者简单的文本配置)写入到刚才创建的 Zip 文件条目中。

5. 关闭文件并判断结果

1
2
3
4
5
ldr        x0, [x22, #0x10]           ; 再次取出 zipFile 句柄放入 x0
bl zipCloseFileInZip ; 调用 zipCloseFileInZip(file) 关闭当前文件条目

cmp w23, #0x0 ; w23 保存的是第3步 zipOpenNewFileInZip5 的返回值。这里将它和 0 (ZIP_OK) 进行比较
cset w22, eq ; Condition Set: 如果相等 (eq),就把 w22 寄存器设置为 1 (YES/true),否则设置为 0 (NO/false)
  • 解释: 对应源码:BOOL success = (openResult == ZIP_OK);。记录刚才打开文件的操作是否成功,供后续逻辑使用。

技术点总结

在无三方源码的限制下,使用 strings 命令进行全盘文本嗅探。
使用 nm 工具分析 Mach-O 符号表,精准判别符号的定义,锁定冲突源文件。
使用 ar 命令对庞大的静态库进行 .o 目标文件的抽离与精准降噪。
借助逆向工具Hopper Disassembler,提取其伪代码。
通过符号前缀隔离方案(Symbol Prefixing)解决命名冲突而导致符号覆盖的问题。

Author:KangQingYu
Link:http://example.com/2023/10/15/20231015%E3%80%8A%E9%BB%91%E7%9B%92%E4%B8%8B%E7%9A%84%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%EF%BC%9A%E5%88%A9%E7%94%A8%20nm%20%E4%B8%8E%20Hopper%20%E5%AE%9A%E4%BD%8D%20Incode%20SDK%20%E7%AC%A6%E5%8F%B7%E8%A6%86%E7%9B%96%E9%97%AE%E9%A2%98%E3%80%8B/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×