二、BSBacktraceLogger 分析
当谈到在 iOS 上获取任意线程的堆栈信息,大部分文章都是在介绍如何进行栈回溯来还原调用堆栈,而 BSBacktraceLogger 是其中比较出名的一个工具。
BSBacktraceLogger 具体的实现原理分析本文不再赘述,感兴趣的读者可以自行搜索,网上有很多分析透彻的文章;在这里只说明一下 BSBacktraceLogger 在得物动态合规检测场景中不适用的原因:
-
使用 BSBacktraceLogger 拿到的堆栈是一堆内存地址,这样的堆栈传到合规监测平台没有任何意义。虽然 BSBacktraceLogger 提供了一种符号还原的方法,但 BSBacktraceLogger 的符号还原算法原理是通过解析 machO 文件,读取符号表,根据偏移量来判断应该是哪个函数。但这套算法的准确度一般,当符号表很大时,恢复速度慢,且占用大量的内存和 CPU 资源。
-
服务端恢复测试包的符号较为困难且耗时长。测试包出包频率高,得物每天测试包构建数量接近三位数,假设当前有 100 台测试机均安装了不同的测试包,每台设备上传 1000 条堆栈信息,即使通过一些方式进行去重,这个数量级的堆栈还原还是比恢复崩溃的堆栈大得多,毕竟崩溃是个小概率事件,而隐私 API 的调用是个必然事件。
-
测试包本身没有去除符号,无需再做一次离线的符号恢复,给服务端增加难度和工作量。
经过一系列测试,最终发现在得物中使用 BSBacktraceLogger 的方式,效率甚至不如直接使用 +[NSThread callStackSymbols]。
既然 BSBacktraceLogger 不符合要求,那么深入研究一下系统 +[NSThread callStackSymbols] 方法的实现方式,或许会有收获。
三、callstackSymbols 分析
首先通过 lldb 看一下 +[NSThread callStackSymbols] 出自哪一个系统库,通过 image lookup 命令可以看到, +[NSThread callStackSymbols] 在系统的 Foundation.framework 中:
callstackSymbols
进入 /Users/admin/Library/Developer/Xcode/iOS DeviceSupport/iPhone14,4 15.2 (19C57)/Symbols/System/Library/Frameworks/Foundation.framework/ 找到 Foundation 的二进制,通过逆向分析工具 IDA,可以直接定位到函数实现的伪代码:
callstackSymbols 伪代码
伪代码的实现比较简单,能够看到 +[NSThread callStackSymbols] 内部主要调用 thread_stack_async_pcs 以及 _NSCallStackArray 的某个函数。静态分析由于缺乏符号,因此具体调用 _NSCallStackArray 的哪个函数无法知晓;但是可以通过动态调试,更加清晰的了解函数的运作方式。
启动 Xcode,运行 Demo 工程并给 +[NSThread callStackSymbols] 下断点,调试方式选择始终进入汇编模式:
lldb 中的动态汇编代码
通过查看对应的汇编代码,可以定位到实际调用的函数在 51 行的 bl,通过动态调试可以看出,这里 0x180f19dd8 是 objc_msgSend 方法:
IDA 中的静态分析结果
0x180f19dd8
在 iOS 中 objc_msgSend 方法存在两个固定的参数,分别是 id 和 SEL ,一个代表函数的调用者,可能是个类,也可能是实例对象,另一个代表 Selector ,是需要调用的函数指针,这两个参数按照 ARM 汇编的传参顺序,一个放在 X0 寄存器,一个放在 X1 寄存器;这里通过静态分析可以得知,此处的 objc_msgSend ,第一个参数是类 _NSCallStackArray ,可以通过 po 命令打印验证,SEL 也可以一并输出结果:
id 和 SEL 的动态调试结果
通过动态调试,可以看到此处调用的是 +[_NSCallStackArray arrayWithFrames:count:symbols:] ,使用 image lookup 命令查看函数的具体位置,发现该函数的实现也在 Foundation.framework 里:
+[_NSCallStackArray arrayWithFrames:count:symbols:]
+[_NSCallStackArray arrayWithFrames:count:symbols:]
在 Foundation.framework 的静态分析结果中搜索,找到 +[_NSCallStackArray arrayWithFrames:count:symbols:] 函数的汇编,发现是一些内部的相关属性赋值操作:
+[_NSCallStackArray arrayWithFrames:count:symbols:] 汇编
通过动态调试,查看在内存中运行时的具体赋值情况,可以看到 _NSCallStackArray 类中有很多属性,也可以知晓 +[_NSCallStackArray arrayWithFrames:count:symbols:] 方法三个参数类型分别是 void /unsigned long /BOOL:
分析到这里,笔者猜测该函数的作用应该是将内存地址符号化,可以动态调试验证这一点,首先输出正常符号化后的堆栈信息:
正常输出
根据 ARM 汇编的传参规则,通过修改 X4 寄存器,可以将 +[_NSCallStackArray arrayWithFrames:count:symbols:] 中第三个参数的 BOOL 从 1 改为 0:
修改为 0
再次查看,输出的是未符号化之前的函数堆栈:
分析到这里,针对 +[_NSCallStackArray arrayWithFrames:count:symbols:] 做一个总结:
+[_NSCallStackArray arrayWithFrames:count:symbols:] 方法三个参数类型分别是 void **/unsigned long /BOOL ,函数的作用是将函数地址符号化,通过修改第三个参数 symbols,可以控制是否进行符号化操作。
thread_stack_async_pcs
+[_NSCallStackArray arrayWithFrames:count:symbols:] 分析完成,继续查看 thread_stack_async_pcs 的实现。thread_stack_async_pcs 通过 image lookup 命令可以看到出自 libsystem_c.dylib:
thread_stack_async_pcs
使用 IDA 继续静态分析 libsystem_c.dylib,thread_stack_async_pcs 内部直接调用 **__thread_stack_pcs** 函数,没有其他任何操作:
hread_stack_async_pcs 伪代码
查看 __thread_stack_pcs 函数的汇编代码,发现开头有三个函数没有符号,需要通过动态调试来查看:
__thread_stack_pcs 汇编
给 __thread_stack_pcs 下断点,可以看到三个没有符号的函数是 pthread_self 、pthread_get_stackaddr_np 、pthread_get_stacksize_np:
通过动态调试和静态分析相结合,现在可以拿到 __thread_stack_pcs 中函数符号化之后的伪代码:
__int64 __fastcall __thread_stack_pcs(__int64 a1, int a2, _DWORD *a3, int a4, unsigned __int64 a5, unsigned __int8 a6)
{
v12 = pthread_self();
v13 = pthread_get_stackaddr_np();
v14 = pthread_get_stacksize_np(v12);
*a3 = 0;
v15 = pthread_stack_frame_decode_np(vars0, 0LL);
v16 = 0LL;
if ( ((unsigned int)vars0 & 1) == 0 )
{
v17 = v13 - v14;
if ( (unsigned __int64)vars0 >= v17 )
{
v18 = v15;
v28 = a6;
v19 = (__int64 *)((char *)vars0 + v13 - v15);
if ( vars0 <= v19 )
{
v20 = vars0;
if ( !a5 )
goto LABEL_7;
LABEL_5:
if ( v18 > a5 && a5 != 0 )
{
LABEL_15:
if ( a2 )
{
v16 = 0LL;
v21 = 1 - a2;
StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 3));
while ( 1 )
{
if ( (unsigned __int64)*v20 >> 60 == 1 )
{
v23 = *(_QWORD *)(StatusReg + 824);
if ( v23 )
{
v24 = *(unsigned int *)(v23 + 36);
v16 = (_DWORD)v24 ? 1LL : (unsigned int)v16;
if ( !(((_DWORD)v24 == 0) | (v28 ^ 1) & 1) )
break;
}
}
v25 = pthread_stack_frame_decode_np(v20, &v29);
v26 = (unsigned int)*a3;
*(_QWORD *)(a1 + 8 * v26) = v29;
*a3 = v26 + 1;
if ( (unsigned __int64)v20 < v25 && (v25 & 1) == 0 && v17 <= v25 && (unsigned __int64)v19 >= v25 )
{
++v21;
v20 = (__int64 *)v25;
if ( v21 != 1 )
continue;
}
return v16;
}
__thread_stack_async_pcs(a1, (unsigned int)-v21, a3, v20);
return v24;
}
else
{
return 0LL;
}
}
else
{
while ( 1 )
{
v16 = 0LL;
if ( (unsigned __int64)v20 >= v18 || (v18 & 1) != 0 || v17 > v18 || (unsigned __int64)v19 < v18 )
break;
v20 = (__int64 *)v18;
v18 = pthread_stack_frame_decode_np(v18, 0LL);
if ( a5 )
goto LABEL_5;
LABEL_7:
if ( !a4 )
goto LABEL_15;
--a4;
}