Mar
19
对于已经获得的crash堆栈,无论是否可以通过符号表获得代码实际情况,只要我们没发看到确切的代码,都是无法直接通过crash栈直接进行分析。特别是遇到整个crash堆栈里面完全没有自己项目的代码,或者虽然是我们的项目名下的堆栈,却是通过pod引入的第三方库。更现实的是,为了加速代码编译或者开发者干脆就是闭源的,往往pod引入的库都是二进制的静态库,所以我们得到的堆栈肯定没有具体代码行数,看到堆栈肯定是无计可施。
遇到这样的情况,我们看到的堆栈往往是:0x100072ea4 0x100050000 + 143012这样只会有堆栈指令的pc位置或者方法名 + offset显示出来的pc位置,而不是。这样我们需要分析代码,只有通过分析具体的汇编指令才能继续下去。
而Hopper这个iOS查看和半反编译工具正适合这件事。
准备工作
首先我们当然要下载一个Hopper,官网(https://www.hopperapp.com)。这个软件demo版可以直接使用完整功能,和Charles一样每次启动可以使用30分钟——对于我们勉强够用了,动心了可以买买买~
另外,我们还需要找到用于进行反编译的程序。理论上,它在ipa包的/Payload/xxx.app/xxx即对应的编译结果,其中在本地xcode编译出来的app在~/Library/Developer/Xcode/DerivedData下。
最后,我们当然要准备好需要的crash堆栈,另外在旁边准备一个科学计算器比较好。
另外再用浏览器开一个ARM汇编指令大全吧。(比如http://blog.csdn.net/forever_2015/article/details/50285865)
iOS的ARM汇编基础
虽然基本上只需要一丁点儿汇编基础知识就可以开展工作,还是有一些需要知道的。
寄存器相关:一共有31个64位通用寄存器, x0~x30。其中x29是frame pointer;x30是procedure link register;还有sp和pc。
常用的汇编指令我们需要了解的主要是:
- mov r1,r2 把r2的数据赋予r1
- ldr r1,r2 把r2指向的数据赋予r1
- str r1,r2 把r1的数据赋予r2指向的地方
- add sub之类的运算符肯定是需要的
- 那一堆草鸡麻烦的跳转判断指令
- bl 调用子程序
- [r1, 0xXXXX]这样offset的方法
另外oc方法调用的情况下:
id value = [obj methodKey1:key1 andKey2:key2];
编译到c层实际调用是:
id value = objc_msgSend(obj, @selector(methodKey1:andKey2:), key, key2);
当然,c的函数对应的其实是汇编调用子函数。因此我们需要的入口参数obj,selector,key,key2...其实是通过r0,r1,r2.....传输的,特殊情况下可能会通过堆栈传输,不过一般不会~。另外返回值会直接返回到r0里边。
嗯,知道这些就可以了。
Demo:一次完整的分析
这次我们分析的完整的崩溃堆栈是这样的:
从堆栈的角度,可以看到,倒数第三层调用到了doesNotRecognizeSelector方法然后抛出了异常,结合上下文,可以猜想到应该是某一个object存在,但是调用了不存在的方法——也许是类型错误,导致了这个崩溃的发生。
而查询后,kmall的三层均不是我们项目代码,而是闭源的第三方库中抛出来的错误,无法得到其他信息。因此现在,只有从kmall最高的那一层,即第5层堆栈开始入手分析汇编代码了。
第一层
我们看到的地址是0x1004b103c 0x100050000 + 4591676,其实就是程序的0x46103C偏移位置。直接用hopper打开程序进行反汇编找到对应的子函数:
这个子函数有点长,我先截取一部分看看。首先根据hopper部分反编译(其实是数据映射的结果),这个子函数对应的方法是 +[SAKGuardCommon encrypt:withKey:byAlgorithm:]:。嗯,糟糕,这是一个第三方库里面的代码,并且我们找不到源码,到此为止我们落实要通过分析汇编代码的方式来查crash了。
然后我们找到目标pc地址的上一句,是一句bl即调用子函数,hopper又很贴心的把ios中常见系统子函数给反编译告诉我们了,这是一句msgSend,和我们看到堆栈预期的一样,调用了不存在的方法。那么我们首先要做的就是找到msgSend的obj和selector,他们应该在调用子函数前被放置在了对应的x0和x1处。
往上看,x1很快就找到了。hopper也很贴心的把常量指向的字符串在右侧标了出来。x1是从x8加载出来的,x8指向的字符串“UTF8String”。然后x0呢,在0x461020看到x0是从x8挪过来的,而那里x8是从[x29, #-0x28]加载出来的。那么我们接下来就是需要关心[x29, #-0x28]是哪儿来的了。
继续往上看,在子函数开始部分0x460f58,把原本x9的数据放入了[x29, #-0x28]指向的位置中,但是注意到0x460f50开始的sub最后得到的x8也是指向的这个位置,所以我们综合看一下。那一段结束之后调用了objc_storeStrong方法,我们知道objc_storeStrong是处理入参的持有问题,把入参数转换到另一个新的id上。因此考虑到分别传入了一个空的指针和一个x0,因此这其实是在对x8做storeStrong初始化。
那么看到传入的x1即原始数据,是从哪儿来的?在0x460f5c从[sp, #0x40]读出来的,而[sp, #0x40]哪儿来的,就在上面几行从x3中储存进去的,x3到此为止——嗯,x3不就是子函数的入参么,应该是oc方法的第二个参数吧。即+[SAKGuardCommon encrypt:withKey:byAlgorithm:]的key咯。
到此为止,我们第一层堆栈分析完毕,可以继续往上了。
第二层
然而,分析第二层我们可见的堆栈子程序:
依然是一段分析过后的关键段落截取。首先看到的方法名-[SAKWindFingerprintGenerator tranformToFingerprint:]:,嗯,不是可见的方法,但是和刚才不同的是这是一个实例方法了,所以当前对象很重要。另外虽然方法没见过,SAKWindFingerprintGenerator却是有暴露给用户使用,所以可以找到一些有用的信息。
然后从堆栈出口看,嗯,果然是msgSend而且selector对得上,没问题。然后刚才我们注意到的是x3,那在哪儿放进去的呢?原来是0x381ee0行,从[x29, #-0x48]读取出来的。然后继续往上0x381eb4处,讲0x储存到了[x29, #-0x48]中,而x0又是从[sp, #0x40]读取出来的。
然后上面这一段是一个双goto,本质上是一个if判断,看一下判断指令:cbz x0是否存在?如果存在,往下,0x381e90把[sp, #0x48]读出来赋予了[sp, #0x40],而[sp, #0x48]正好又是x0。所以结论是如果x0存在,传给后面了x0的值。
另一个分支,如果x0不存在,0x381ea0开始从一个叫_kAESKey的静态变量读取了数据并赋予了[sp, #0x40]。
所以这一段其实是:
那关键其实就是x0了。考虑到后面的崩溃应该是对象存在但是没有方法,因此这里要么是x0不存在_kAESKey不对,要不是x0不对,我们需要继续追踪。
这里往上,x0就是0x381e7c中sendMsg的返回值,其中selector是aesKey,而对象x0是x9从[x29, #-0x20]来的。继续往上找,[x29, #-0x20]在0x381dd8从x0赋予,而这里是x0最早出现的位置,即当前子函数的obj。因此完整解释出来,就是:
诶,打住,到此为止。写过相关代码的同学立刻会发现,self,即SAKWindFingerprintGenerator的实例的aesKey好像是暴露出来给用户设置的诶。赶快去看看~~~
至此,这次crash分析就结束了,事实上看到的是api希望aesKey是一个NSString,而我们代码中设置成了NSNumber,由此导致的错误。
总结
总的来说,结合hopper帮助给子程序映射oc方法进行拆分,和对常用oc子程序进行部分反编译之后,阅读iOS的汇编结果进行crash堆栈分析并不是什么困难的事情。往往遇到不是从自己代码中曝出的crash,特别是iOS本身和闭源第三方库爆粗的,往往是非常头痛无计可施的。而通过分析汇编,我们往往可以得到很多有用的信息,结合传统的crash分析方法和经验,可以更可靠有效的解决问题。总之,聊胜于无吧~
遇到这样的情况,我们看到的堆栈往往是:0x100072ea4 0x100050000 + 143012这样只会有堆栈指令的pc位置或者方法名 + offset显示出来的pc位置,而不是。这样我们需要分析代码,只有通过分析具体的汇编指令才能继续下去。
而Hopper这个iOS查看和半反编译工具正适合这件事。
准备工作
首先我们当然要下载一个Hopper,官网(https://www.hopperapp.com)。这个软件demo版可以直接使用完整功能,和Charles一样每次启动可以使用30分钟——对于我们勉强够用了,动心了可以买买买~
另外,我们还需要找到用于进行反编译的程序。理论上,它在ipa包的/Payload/xxx.app/xxx即对应的编译结果,其中在本地xcode编译出来的app在~/Library/Developer/Xcode/DerivedData下。
最后,我们当然要准备好需要的crash堆栈,另外在旁边准备一个科学计算器比较好。
另外再用浏览器开一个ARM汇编指令大全吧。(比如http://blog.csdn.net/forever_2015/article/details/50285865)
iOS的ARM汇编基础
虽然基本上只需要一丁点儿汇编基础知识就可以开展工作,还是有一些需要知道的。
寄存器相关:一共有31个64位通用寄存器, x0~x30。其中x29是frame pointer;x30是procedure link register;还有sp和pc。
常用的汇编指令我们需要了解的主要是:
- mov r1,r2 把r2的数据赋予r1
- ldr r1,r2 把r2指向的数据赋予r1
- str r1,r2 把r1的数据赋予r2指向的地方
- add sub之类的运算符肯定是需要的
- 那一堆草鸡麻烦的跳转判断指令
- bl 调用子程序
- [r1, 0xXXXX]这样offset的方法
另外oc方法调用的情况下:
id value = [obj methodKey1:key1 andKey2:key2];
编译到c层实际调用是:
id value = objc_msgSend(obj, @selector(methodKey1:andKey2:), key, key2);
当然,c的函数对应的其实是汇编调用子函数。因此我们需要的入口参数obj,selector,key,key2...其实是通过r0,r1,r2.....传输的,特殊情况下可能会通过堆栈传输,不过一般不会~。另外返回值会直接返回到r0里边。
嗯,知道这些就可以了。
Demo:一次完整的分析
这次我们分析的完整的崩溃堆栈是这样的:
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 8
Application Specific Information:
abort() called
Filtered syslog:
None found
Last Exception Backtrace:
0 CoreFoundation 0x18a1151b8 __exceptionPreprocess + 124
1 libobjc.A.dylib 0x188b4c55c objc_exception_throw + 56
2 CoreFoundation 0x18a11c268 -[NSObject(NSObject) doesNotRecognizeSelector:] + 140
3 CoreFoundation 0x18a119270 ___forwarding___ + 916
4 CoreFoundation 0x18a01280c _CF_forwarding_prep_0 + 92
5 kmall 0x1004b103c 0x100050000 + 4591676
6 kmall 0x1003d1ef8 0x100050000 + 3677944
7 kmall 0x1003d23a0 0x100050000 + 3679136
8 libdispatch.dylib 0x188f9e1fc _dispatch_call_block_and_release + 24
9 libdispatch.dylib 0x188f9e1bc _dispatch_client_callout + 16
10 libdispatch.dylib 0x188fac3dc _dispatch_queue_serial_drain + 928
11 libdispatch.dylib 0x188fa19a4 _dispatch_queue_invoke + 652
12 libdispatch.dylib 0x188fac8d8 _dispatch_queue_override_invoke + 360
13 libdispatch.dylib 0x188fae34c _dispatch_root_queue_drain + 572
14 libdispatch.dylib 0x188fae0ac _dispatch_worker_thread3 + 124
15 libsystem_pthread.dylib 0x1891a72a0 _pthread_wqthread + 1288
16 libsystem_pthread.dylib 0x1891a6d8c start_wqthread + 4
从堆栈的角度,可以看到,倒数第三层调用到了doesNotRecognizeSelector方法然后抛出了异常,结合上下文,可以猜想到应该是某一个object存在,但是调用了不存在的方法——也许是类型错误,导致了这个崩溃的发生。
而查询后,kmall的三层均不是我们项目代码,而是闭源的第三方库中抛出来的错误,无法得到其他信息。因此现在,只有从kmall最高的那一层,即第5层堆栈开始入手分析汇编代码了。
第一层
我们看到的地址是0x1004b103c 0x100050000 + 4591676,其实就是程序的0x46103C偏移位置。直接用hopper打开程序进行反汇编找到对应的子函数:
; ================ B E G I N N I N G O F P R O C E D U R E ================
+[SAKGuardCommon encrypt:withKey:byAlgorithm:]:
0000000100460f1c stp x29, x30, [sp, #-0x10]! ; Objective C Implementation defined at 0x1009fa370 (class method), DATA XREF=0x1009fa370
0000000100460f20 mov x29, sp
0000000100460f24 sub sp, sp, #0x80
0000000100460f28 sub x8, x29, #0x20
0000000100460f2c movz x9, #0x0
0000000100460f30 stur x0, [x29, #-0x10]
0000000100460f34 stur x1, [x29, #-0x18]
0000000100460f38 stur x9, [x29, #-0x20]
0000000100460f3c mov x0, x8
0000000100460f40 mov x1, x2
0000000100460f44 str x3, [sp, #0x40]
0000000100460f48 str x4, [sp, #0x38]
0000000100460f4c bl imp___stubs__objc_storeStrong
0000000100460f50 sub x8, x29, #0x28
0000000100460f54 movz x9, #0x0
0000000100460f58 stur x9, [x29, #-0x28]
0000000100460f5c ldr x9, [sp, #0x40]
0000000100460f60 mov x0, x8
0000000100460f64 mov x1, x9
0000000100460f68 bl imp___stubs__objc_storeStrong
...
0000000100460fd0 adrp x8, #0x100a64000 ; CODE XREF=+[SAKGuardCommon encrypt:withKey:byAlgorithm:]+156
0000000100460fd4 add x8, x8, #0x630 ; objc_cls_ref_SAKGuardEncryptProcessor
0000000100460fd8 ldr x8, x8
0000000100460fdc ldur x9, [x29, #-0x20]
0000000100460fe0 mov x0, x9
0000000100460fe4 str x8, [sp, #0x30]
0000000100460fe8 bl imp___stubs__objc_retainAutorelease
0000000100460fec adrp x8, #0x100a53000 ; @selector(setTitleLabelBackgroundColor:)
0000000100460ff0 add x8, x8, #0x488 ; @selector(bytes)
0000000100460ff4 ldr x1, x8
0000000100460ff8 bl imp___stubs__objc_msgSend
0000000100460ffc adrp x8, #0x100a52000
0000000100461000 add x8, x8, #0x3a0 ; @selector(length)
0000000100461004 ldur x9, [x29, #-0x20]
0000000100461008 ldr x1, x8
000000010046100c str x0, [sp, #0x28]
0000000100461010 mov x0, x9
0000000100461014 bl imp___stubs__objc_msgSend
0000000100461018 mov x2, x0
000000010046101c ldur x8, [x29, #-0x28]
0000000100461020 mov x0, x8
0000000100461024 str w2, [sp, #0x24]
0000000100461028 bl imp___stubs__objc_retainAutorelease
000000010046102c adrp x8, #0x100a55000 ; @selector(clickGoPay:)
0000000100461030 add x8, x8, #0xfd0 ; @selector(UTF8String)
0000000100461034 ldr x1, x8
0000000100461038 bl imp___stubs__objc_msgSend
000000010046103c ldur x8, [x29, #-0x30]
这个子函数有点长,我先截取一部分看看。首先根据hopper部分反编译(其实是数据映射的结果),这个子函数对应的方法是 +[SAKGuardCommon encrypt:withKey:byAlgorithm:]:。嗯,糟糕,这是一个第三方库里面的代码,并且我们找不到源码,到此为止我们落实要通过分析汇编代码的方式来查crash了。
然后我们找到目标pc地址的上一句,是一句bl即调用子函数,hopper又很贴心的把ios中常见系统子函数给反编译告诉我们了,这是一句msgSend,和我们看到堆栈预期的一样,调用了不存在的方法。那么我们首先要做的就是找到msgSend的obj和selector,他们应该在调用子函数前被放置在了对应的x0和x1处。
往上看,x1很快就找到了。hopper也很贴心的把常量指向的字符串在右侧标了出来。x1是从x8加载出来的,x8指向的字符串“UTF8String”。然后x0呢,在0x461020看到x0是从x8挪过来的,而那里x8是从[x29, #-0x28]加载出来的。那么我们接下来就是需要关心[x29, #-0x28]是哪儿来的了。
继续往上看,在子函数开始部分0x460f58,把原本x9的数据放入了[x29, #-0x28]指向的位置中,但是注意到0x460f50开始的sub最后得到的x8也是指向的这个位置,所以我们综合看一下。那一段结束之后调用了objc_storeStrong方法,我们知道objc_storeStrong是处理入参的持有问题,把入参数转换到另一个新的id上。因此考虑到分别传入了一个空的指针和一个x0,因此这其实是在对x8做storeStrong初始化。
那么看到传入的x1即原始数据,是从哪儿来的?在0x460f5c从[sp, #0x40]读出来的,而[sp, #0x40]哪儿来的,就在上面几行从x3中储存进去的,x3到此为止——嗯,x3不就是子函数的入参么,应该是oc方法的第二个参数吧。即+[SAKGuardCommon encrypt:withKey:byAlgorithm:]的key咯。
到此为止,我们第一层堆栈分析完毕,可以继续往上了。
第二层
然而,分析第二层我们可见的堆栈子程序:
; ================ B E G I N N I N G O F P R O C E D U R E ================
-[SAKWindFingerprintGenerator tranformToFingerprint:]:
0000000100381db0 stp x29, x30, [sp, #-0x10]! ; Objective C Implementation defined at 0x1009e41f8 (instance method), DATA XREF=0x1009e41f8
0000000100381db4 mov x29, sp
0000000100381db8 sub sp, sp, #0xb0
0000000100381dbc sub x8, x29, #0x30
0000000100381dc0 movz x9, #0x0
0000000100381dc4 adrp x10, #0x100918000
0000000100381dc8 ldr x10, [x10, #0x400] ; ___stack_chk_guard_100918400,___stack_chk_guard
0000000100381dcc ldr x10, x10
0000000100381dd0 mov x3, x10
0000000100381dd4 stur x10, [x29, #-0x8]
0000000100381dd8 stur x0, [x29, #-0x20]
...
0000000100381e64 adrp x8, #0x100a5b000 ; @selector(readStream)
0000000100381e68 add x8, x8, #0x270 ; @selector(aesKey)
0000000100381e6c stur x0, [x29, #-0x40]
0000000100381e70 ldur x9, [x29, #-0x20]
0000000100381e74 ldr x1, x8
0000000100381e78 mov x0, x9
0000000100381e7c bl imp___stubs__objc_msgSend
0000000100381e80 mov x29, x29
0000000100381e84 bl imp___stubs__objc_retainAutoreleasedReturnValue
0000000100381e88 str x0, [sp, #0x48]
0000000100381e8c cbz x0, loc_100381e9c
0000000100381e90 ldr x8, [sp, #0x48]
0000000100381e94 str x8, [sp, #0x40]
0000000100381e98 b loc_100381eac
loc_100381e9c:
0000000100381e9c adrp x8, #0x10092e000 ; CODE XREF=-[SAKWindFingerprintGenerator tranformToFingerprint:]+220
0000000100381ea0 add x8, x8, #0x390 ; _kAESKey
0000000100381ea4 ldr x8, x8
0000000100381ea8 str x8, [sp, #0x40]
loc_100381eac:
0000000100381eac ldr x0, [sp, #0x40] ; CODE XREF=-[SAKWindFingerprintGenerator tranformToFingerprint:]+232
0000000100381eb0 bl imp___stubs__objc_retain
0000000100381eb4 stur x0, [x29, #-0x48]
0000000100381eb8 ldr x0, [sp, #0x48]
0000000100381ebc bl imp___stubs__objc_release
0000000100381ec0 adrp x0, #0x10096d000 ; @"- (int64_t)%@;"
0000000100381ec4 add x0, x0, #0xc60 ; @"AES"
0000000100381ec8 adrp x30, #0x100a5b000 ; @selector(readStream)
0000000100381ecc add x30, x30, #0x278 ; @selector(encrypt:withKey:byAlgorithm:)
0000000100381ed0 adrp x8, #0x100a64000
0000000100381ed4 add x8, x8, #0x338 ; objc_cls_ref_SAKGuardCommon
0000000100381ed8 ldr x8, x8
0000000100381edc ldur x2, [x29, #-0x40]
0000000100381ee0 ldur x3, [x29, #-0x48]
0000000100381ee4 ldr x1, x30
0000000100381ee8 str x0, [sp, #0x38]
0000000100381eec mov x0, x8
0000000100381ef0 ldr x4, [sp, #0x38]
0000000100381ef4 bl imp___stubs__objc_msgSend
0000000100381ef8 mov x29, x29
依然是一段分析过后的关键段落截取。首先看到的方法名-[SAKWindFingerprintGenerator tranformToFingerprint:]:,嗯,不是可见的方法,但是和刚才不同的是这是一个实例方法了,所以当前对象很重要。另外虽然方法没见过,SAKWindFingerprintGenerator却是有暴露给用户使用,所以可以找到一些有用的信息。
然后从堆栈出口看,嗯,果然是msgSend而且selector对得上,没问题。然后刚才我们注意到的是x3,那在哪儿放进去的呢?原来是0x381ee0行,从[x29, #-0x48]读取出来的。然后继续往上0x381eb4处,讲0x储存到了[x29, #-0x48]中,而x0又是从[sp, #0x40]读取出来的。
然后上面这一段是一个双goto,本质上是一个if判断,看一下判断指令:cbz x0是否存在?如果存在,往下,0x381e90把[sp, #0x48]读出来赋予了[sp, #0x40],而[sp, #0x48]正好又是x0。所以结论是如果x0存在,传给后面了x0的值。
另一个分支,如果x0不存在,0x381ea0开始从一个叫_kAESKey的静态变量读取了数据并赋予了[sp, #0x40]。
所以这一段其实是:
[sp, #0x40] = x0 ? x0 : _kAESKey;
那关键其实就是x0了。考虑到后面的崩溃应该是对象存在但是没有方法,因此这里要么是x0不存在_kAESKey不对,要不是x0不对,我们需要继续追踪。
这里往上,x0就是0x381e7c中sendMsg的返回值,其中selector是aesKey,而对象x0是x9从[x29, #-0x20]来的。继续往上找,[x29, #-0x20]在0x381dd8从x0赋予,而这里是x0最早出现的位置,即当前子函数的obj。因此完整解释出来,就是:
[sp, #0x40] = self.aesKey ? self.aesKey : _kAESKey;
诶,打住,到此为止。写过相关代码的同学立刻会发现,self,即SAKWindFingerprintGenerator的实例的aesKey好像是暴露出来给用户设置的诶。赶快去看看~~~
至此,这次crash分析就结束了,事实上看到的是api希望aesKey是一个NSString,而我们代码中设置成了NSNumber,由此导致的错误。
总结
总的来说,结合hopper帮助给子程序映射oc方法进行拆分,和对常用oc子程序进行部分反编译之后,阅读iOS的汇编结果进行crash堆栈分析并不是什么困难的事情。往往遇到不是从自己代码中曝出的crash,特别是iOS本身和闭源第三方库爆粗的,往往是非常头痛无计可施的。而通过分析汇编,我们往往可以得到很多有用的信息,结合传统的crash分析方法和经验,可以更可靠有效的解决问题。总之,聊胜于无吧~