0x0 前言
外网新增一个 Crash,数量较多,短时间内就上升到 Top2,需要尽快排查修复。
Crash 堆栈如下:
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
| Thread 21 Crashed: 0 QQKSong 0x0000000105dad868 +[UIView(GDTExtensions) isDisplayedInScreen:] + 179108 1 QQKSong 0x0000000105d9dc88 +[UIView(GDTExtensions) isDisplayedInScreen:] + 114628 2 QQKSong 0x0000000105da1514 +[UIView(GDTExtensions) isDisplayedInScreen:] + 129104 3 QQKSong 0x0000000105da226c +[UIView(GDTExtensions) isDisplayedInScreen:] + 132520 4 QQKSong 0x0000000105da2b4c +[UIView(GDTExtensions) isDisplayedInScreen:] + 134792 5 QQKSong 0x0000000105d8d624 +[UIView(GDTExtensions) isDisplayedInScreen:] + 47456 6 QQKSong 0x0000000105d8e414 +[UIView(GDTExtensions) isDisplayedInScreen:] + 51024 7 QQKSong 0x0000000105d8e0f4 +[UIView(GDTExtensions) isDisplayedInScreen:] + 50224 8 QQKSong 0x0000000105d8e2d4 +[UIView(GDTExtensions) isDisplayedInScreen:] + 50704 9 QQKSong 0x0000000105d90ee8 +[UIView(GDTExtensions) isDisplayedInScreen:] + 61988 10 QQKSong 0x0000000105d914dc +[UIView(GDTExtensions) isDisplayedInScreen:] + 63512 11 QQKSong 0x0000000105d919a0 +[UIView(GDTExtensions) isDisplayedInScreen:] + 64732 12 QQKSong 0x0000000105da5abc +[UIView(GDTExtensions) isDisplayedInScreen:] + 146936 13 QQKSong 0x0000000105d886dc +[UIView(GDTExtensions) isDisplayedInScreen:] + 27160 14 QQKSong 0x0000000105da5990 +[UIView(GDTExtensions) isDisplayedInScreen:] + 146636 15 QQKSong 0x0000000105d93270 +[UIView(GDTExtensions) isDisplayedInScreen:] + 71084 16 CFNetwork 0x00000001aa43b8e4 _CFNetServiceBrowserSearchForServices + 75936 17 CFNetwork 0x00000001aa44c3b4 __CFHTTPMessageSetResponseProxyURL + 9332 8 libdispatch.dylib 0x00000001a9a7d298 __dispatch_call_block_and_release + 24 10 libdispatch.dylib 0x00000001a9a7e280 __dispatch_client_callout + 16 10 libdispatch.dylib 0x00000001a9a5a4f0 __dispatch_lane_serial_drain$VARIANT$armv81 + 568 21 libdispatch.dylib 0x00000001a9a5b010 __dispatch_lane_invoke$VARIANT$armv81 + 456 12 libdispatch.dylib 0x00000001a9a64800 __dispatch_workloop_worker_thread + 692 13 libsystem_pthread.dylib 0x00000001f24eb5a4 __pthread_wqthread + 272 14 libsystem_pthread.dylib 0x00000001f24ee874 _start_wqthread + 8 SEGV_ACCERR
|
以上堆栈表面上看起来像是子线程调用 +[UIView(GDTExtensions) isDisplayedInScreen:]
,且该方法循环调用了,不过看后面的偏移量较大,如有些偏移量为 + 179108
,不太正常。
该分类名为 GDTExtensions
,为 AMS SDK 内的文件,问了 SDK 侧要到源码,该分类的代码量实际也没那么多,且基本不存在循环调用的可能。
此时无法知道 Crash 具体是在哪里,下面将简单介绍如何排查这种堆栈偏移量过大的问题。
PS: 这里分享一个小技巧,如何通过一个方法名或是类名等,找到是在哪个二进制文件内:
1 2 3
| $ cd ~/your_proj_path $ grep -r GDTExtensions . Binary file ./Pods/AMSSDK/AMSSDK/lib/libGDTMobTangramSDK.a matches
|
0x1 Crash 关键信息
这里先贴一下其它 Crash 关键信息:
Crash 版本、架构、访问的地址及还原前堆栈:
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
| Version: 7.24.20.631_JAIL(TestFlight) Code Type: ARM-64 (Native) Exception Codes: SEGV_ACCERR at 000000000000000000
Thread 21 Crashed: 0 QQKSong 0x0000000105dad868 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2930176 1 QQKSong 0x0000000105d9dc88 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2865696 2 QQKSong 0x0000000105da1514 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2880176 3 QQKSong 0x0000000105da226c _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2883584 4 QQKSong 0x0000000105da2b4c _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2885856 5 QQKSong 0x0000000105d8d624 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2798528 6 QQKSong 0x0000000105d8e414 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2802096 7 QQKSong 0x0000000105d8e0f4 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2801296 8 QQKSong 0x0000000105d8e2d4 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2801776 9 QQKSong 0x0000000105d90ee8 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2813056 10 QQKSong 0x0000000105d914dc _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2814576 11 QQKSong 0x0000000105d919a0 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2815808 12 QQKSong 0x0000000105da5abc _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2898000 13 QQKSong 0x0000000105d886dc _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2778224 14 QQKSong 0x0000000105da5990 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2897712 15 QQKSong 0x0000000105d93270 _ZN9audiobase9AudioHttp13AudioHttpImpl10threadFuncEPv + 2822160 16 CFNetwork 0x00000001aa43b8e4 CFNetServiceBrowserSearchForServices + 75932 17 CFNetwork 0x00000001aa44c3b4 _CFHTTPMessageSetResponseProxyURL + 9328 8 libdispatch.dylib 0x00000001a9a7d298 0x00000001a9a1d000 + 393872 10 libdispatch.dylib 0x00000001a9a7e280 0x00000001a9a1d000 + 397952 10 libdispatch.dylib 0x00000001a9a5a4f0 0x00000001a9a1d000 + 251120 21 libdispatch.dylib 0x00000001a9a5b010 0x00000001a9a1d000 + 253968 12 libdispatch.dylib 0x00000001a9a64800 0x00000001a9a1d000 + 292864 13 libsystem_pthread.dylib 0x00000001f24eb5a4 _pthread_wqthread + 268 14 libsystem_pthread.dylib 0x00000001f24ee874 start_wqthread + 4
|
寄存器信息:
1 2 3 4 5 6 7 8 9 10
| Thread 21 crashed with ARM 64 Thread State: x0: 000000000000000000 x1: 0x000000016e79d97c x2: 000000000000000000 x3: 0x000000016e79db44 x4: 0x000000000000c413 x5: 000000000000000000 x6: 000000000000000000 x7: 000000000000000000 x8: 0x0000000000000009 x9: 0x00000001f5a48954 x10: 000000000000000000 x11: 0x000000007a200000 x12: 0x00000000000000c0 x13: 0x0000000000000001 x14: 0x000000000000001b x15: 0x000000000000007e x16: 0xffffffffffffffe1 x17: 0x00000001a9d355d8 x18: 000000000000000000 x19: 0x0000000280f66400 x20: 0x00000002800292a0 x21: 0x00000001f590b2d4 x22: 0x00000000feedfacf x23: 0x875f6484f469a2a6 x24: 0x000000010c950000 x25: 000000000000000000 x26: 000000000000000000 x27: 0x000000010a4eb4a8 x28: 0x000000010b5ea000 fp: 0x000000016e79dba0 lr: 0x0000000105dad838 sp: 0x000000016e79db10 pc: 0x0000000105dad868 +[UIView(GDTExtensions) isDisplayedInScreen:] + 179108
|
QQKSong 地址段:
1
| 0x1021d8000 - 0x10a35ffff +QQKSong arm64 <8e69fce9d64730278f0e08c45fb22df5> /private/var/containers/Bundle/Application/A09F4678-9FE1-403A-B457-07372E6C83DE/QQKSong.app/QQKSong
|
0x2 手动还原堆栈
symbolicatecrash 还原全部堆栈
1、找到 symbolicatecrash
位置
1
| find /Applications/Xcode.app -name symbolicatecrash -type f
|
结果类似:
1 2 3 4 5
| /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/iOSSupport/Library/PrivateFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash /Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash /Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
|
SharedFrameworks
路径下的为需要的:
1
| /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
|
2、执行命令
将还原前的 crash 堆栈、dsym 文件放到同一个目录,然后执行:
1
| /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash origin.crash QQKSong.app.dSYM > result.crash
|
如果报以下错误:
1
| Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
|
则设置一下环境变量再跑一次上面那命令还原堆栈:
1
| export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
atos 还原某行堆栈
使用 atos
命令,或使用 dSYMTools
1
| atos -o executable -arch architecture -l loadAddress address
|
小结
以上还原堆栈的结果,跟 bugly 还原的结果一致,都为该分类方法,且偏移量同样过大,所以手动还原堆栈无法解决问题。
根据上面的其它关键 Crash 信息,我们可以看到 pc 寄存器的地址为 0x0000000105dad868
,是落在 QQKSong 地址段内的:
1
| 0x1021d8000 - 0x10a35ffff +QQKSong arm64 <8e69fce9d64730278f0e08c45fb22df5> /private/var/containers/Bundle/Application/A09F4678-9FE1-403A-B457-07372E6C83DE/QQKSong.app/QQKSong
|
pc 寄存器: 保存 CPU 将要执行的下一条指令的地址,也就是即将要执行的指令代码。
所以接下来尝试进行反编译来获取 Crash 时的具体位置。
0x3 反编译
使用 Hopper Disassembler
进行反编译,可以去其官网下载,免费版可以每次使用半小时,基本够我们进行分析了(良心软件)。
计算真实地址
因为 Apple 使用了 ASLR(下面会讲到什么是 ASLR),每次启动时的二进制地址都会随机偏移,所以我们需要先解出 pc 寄存器在二进制文件内的真实地址,结果如下(下面 0x0021d8000
为 QQKSong 地址段首地址 0x1021d8000 - 0x100000000
,为什么要减去 0x100000000
后面 ASLR 部分会有说明):
1
| (0x0000000105dad868-0x0021d8000).toString(16) = 0x103bd5868
|
反汇编结果
我们找到 ipa 里的 QQKSong 二进制文件,丢进 Hopper,等加载完成后,按 G
键,跳转到上面计算出的地址 0x103bd5868
,结果如下:
挂在这条汇编指令,很明显是 +[TSEnvironmentXXQ injectingImageNames]
附近代码导致的 Crash:
1
| 0000000103bd5868 ldr x20, [x26] ; CODE XREF=+[TSEnvironmentXXQ injectingImageNames]+368
|
汇编指令分析
上述汇编指令表示将 x26
寄存器的值赋值给 x20
,上面我们知道 Crash 时访问的地址为 0x000000000000000000
:
1
| Exception Codes: SEGV_ACCERR at 000000000000000000
|
根据以往经验,这里 x26
有可能为一个 C++ 空指针,访问空指针时发生了 Crash。我们继续往上看其它汇编指令,看 x26
的值是从哪里来的:
1
| 0000000103bd584c ldr x26, [x24, #0x60]
|
该指令表示取 x24
偏移 0x60
个地址的数据赋值给 x26
,这种可以猜测是访问 x24
内某个成员变量。所以这里大致可以推测出这样的伪代码:
1 2 3
| struct x24 = xxx; // 某个结构体实例 struct x26 = x24->0x60_member; // 访问了 +0x60 的成员变量,此时 x26 的值为 NULL x20 = NULL->first_addr; // 访问了 x26(此时为 NULL)的首地址,此时就 Crash 了
|
从 Hopper 看到的伪代码也基本符合我们预期,不过这里有更进一步的信息,Crash 是挂在一个循环内的第一句代码:
源码分析
然后就找到 SDK 同学要到该方法源码如下:
结合上面的推测,这里 Crash 基本就确定是因为 p_uuid_info
为 NULL 了。
这里的寄存器相关对应关系如下:
1 2 3
| x26: p_uuid_info x24: infos x24+0x60: infos->uuidArray
|
由于是系统方法,所以只能进行规避:进行空指针的判断,为空则不执行下一步逻辑。
0x4 ASLR 介绍
什么是 ASLR?
- Address Space Layout Randomization,地址空间布局随机化。
- 是一种通过增加攻击者预测目的地址难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。
- MachO 可执行文件每次启动的起始地址不一样。
- 苹果从 iOS 4.3 之后引入了 ASLR 技术,也就是说把 Mach-O 载入内存后,所有的地址都会经过 ASLR 偏移。
Mach-O 文件 Header、Load Commmands、Data 由三部分组成,如下图所示:
Mach-O 内容也可以通过此工具查看: MachO-Explorer
Header
: 位于 Mach-O 文件的开始位置,包含了该文件的基本信息,如目标架构,文件类型,加载指令数量和 dyld 加载时需要的标志位。可以使用 otool 命令查看内容:
1 2 3 4 5
| $ otool -v -h QQKSong.app/QQKSong
Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 85 9248 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE MH_HAS_TLV_DESCRIPTORS
|
Load Commands
: 包含 Mach-O 里命令类型信息,名称和二进制文件的位置。可以使用 otool 命令查看内容:
1
| $ otool -v -l QQKSong.app/QQKSong
|
Data
: 由 Segment 的数据组成,是 Mach-O 占比最多的部分,有代码有数据,比如符号表。Data 共三个 Segment,TEXT、DATA、LINKEDIT。其中 TEXT 和 DATA 对应一个或多个 Section,LINKEDIT 没有 Section,需要配合 LC_SYMTAB 来解析 symbol table 和 string table,包含 dyld 所需各种数据,比如符号表、间接符号表、rebase 操作码、绑定操作码、导出符号、函数启动信息、数据表、代码签名等。这些里面是 Mach-O 的主要数据。下面介绍几条有用的命令:
1 2 3 4 5 6 7 8
| # 查看内容分布 $ xcrun size -x -l -m QQKSong.app/QQKSong
# 查看某个 Section 内容 $ xcrun otool -s __TEXT __text QQKSong.app/QQKSong
# 查看 Mach-O 汇编内容(注:QQKSong 二进制输出结果大小有 1.23GB) xcrun otool -v -t QQKSong.app/QQKSong
|
Mach-O 结构及 ASLR 地址偏移图解如下:
根据上图可以知道,前面计算真实地址为什么要减去 0x100000000
,主要就是 __PAGESIZE
的大小,该大小为 4GB,也即 0x100000000
,从 Hopper 也可以看到其是从 0x100000000 开始解析的:
0x5 参考资料
https://ming1016.github.io/2020/03/29/apple-system-executable-file-macho/
https://alvinzhu.me/2017/07/22/start-from-launch.html