Mar 19

利用Hopper进行基于汇编的iOSCrash栈分析

Lrdcq , 2017/03/19 20:36 , 程序 , 閱讀(6908) , Via 本站原創
对于已经获得的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:一次完整的分析

这次我们分析的完整的崩溃堆栈是这样的:
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分析方法和经验,可以更可靠有效的解决问题。总之,聊胜于无吧~
关键词:ios , 调试 , 反编译 , 汇编
logo