按照惯例,在使用工具开始逆向工程之前,要先分析一下抽象的目标,并将其具体化,然后制定这次逆向工程的思路,再贯彻思想实地执行。
使用MobileSMS的朋友都会注意到,在发送一条信息的整个过程中,苹果都会通过文字及颜色的变化,来提示用户当前发送的是一条短信(以下简称SMS)还是一条iMessage,具体表现在以下几方面。
A.当开始编写一条信息,刚输入完对方的地址,还没有输入信息内容时,如果iOS检测到对方支持iMessage,信息输入框处的占位符(placeholder)就会由“Text Message”变成“iMessage”,如图10-1所示。
图10-1 placeholder的变化
B.当输入信息内容时,如果对方仅支持SMS,则输入框旁的“Send”字样是绿色的;如果对方支持iMessage,则“Send”是蓝色的;
C.当点击“Send”发送此条信息时,如果这是一条SMS,信息气泡的颜色是绿色的;如果是一条iMessage,气泡是蓝色的。
这3种现象会依次出现,不过,因为检测iMessage的操作在第一个现象里已经出现了,所以仅把这一个现象作为切入点来分析就已经能够达到本节的目标了。下面会把火力集中在现象A上。
确定了切入点之后,跟笔者一起思考,怎么把这个现象给具象化成逆向工程的思路:
我们能够观察到的是发生在UI上的现象,即“Text Message”变成“iMessage”。我们知道,UI上显示的内容不是凭空生成的,它显示的是其数据源的值——那么就可以根据这个现象,用Cycript找到UI的数据源,即placeholder。
placeholder也不是凭空生成的,它的值也来自它的数据源——它之所以发生改变,是因为它的数据源(的数据源的数据源……以下简称N重数据源)发生了改变,类似于下面的伪代码:
id dataSource = ?; id a = function(dataSource); id b = function(a); id c = function(b); … id z = function(y); NSString *placeholder = function(z);
从上面的伪代码可以知晓,原始数据源是dataSource,dataSource发生了变化,间接导致placeholder变化。可以理解吧?那原始数据源是什么呢?在现象A里,我们唯一输入的就是收件人地址,因此原始数据源当然就是收件人地址啊!对于“检测iMessage”来说,MobileSMS中应该存在一个dataSource转换为placeholder的过程,这个过程就是“检测iMessage”的确切含义,也就是本节的目标,如图10-2所示。
图10-2 dataSource转化为placeholder的过程(1)
你可能会想,图10-2这么直观,既然dataSource已知,那就直接从它入手,找到placeholder,不就达到目的了?但是现实往往没有这么美好——我们没有源代码,进程流程一般也没有这么简单,在大多数时候,dataSource转换为placeholder的过程如图10-3所示。
dataSource要经过多重转换才能生成placeholder,它们之间的关系非常复杂。如果从dataSource入手,如何知道它要走4条路中的哪条路才能到达placeholder?在这种情况下,因为placeholder是唯一的,所以从placeholder入手,用终点倒推起点,来还原过程,是更高效更可行的做法。
图10-3 dataSource转化为placeholder的过程(2)
总结一下,逆向工程的思路是这样的:先用Cycript定位placeholder,然后通过IDA和LLDB寻找placeholder的N重数据源,直到找到dataSource,最后还原dataSource生成placeholder的过程。看起来很简单是吧?实际操作起来你就知道有多复杂了。这就开始吧。
打开MobileSMS,新建一条信息,在地址输入框中填写“bbs.iosre.com”,然后点击“return”结束输入,如图10-4所示。
图10-4 新建一条信息
既然要用Cycript找placeholder,那就用Cycript来找找显示placeholder当前值“Text Message”的是哪个view,placeholder一定跟那个view密切相关,对吧?走起!执行下面的代码:
FunMaker-5:~ root# cycript -p MobileSMS cy# ?expand expand == true cy# [[UIApp keyWindow] recursiveDescription]
上面代码执行之后,Cycript会打印出keyWindow的视图结构,内容很多,就不贴在这里了。在输出里搜索“Text Message”,发现一个匹配也搜不到。这是怎么回事?相信你也想到了——“Text Message”不在keyWindow里。为了验证我们的猜想,来看看当前这个界面有几个window,如下:
cy# [UIApp windows] @[#"<UIWindow: 0x1575ca10; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x15629c60>; layer = <UIWindowLayer: 0x156e36f0>>",#"<UITextEffectsWindow: 0x1579ab70; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; gestureRecognizers = <NSArray: 0x1579b300>; layer = <UIWindowLayer: 0x1579adf0>>",#"<CKJoystickWindow: 0x1552bf90; baseClass = UIAutoRotatingWindow; frame = (0 0; 320 568); hidden = YES; gestureRecognizers = <NSArray: 0x1552b730>; layer = <UIWindowLayer: 0x1552bdc0>>",#"<UITextEffectsWindow: 0x1683a2e0; frame = (0 0; 320 568); hidden = YES; gestureRecognizers = <NSArray: 0x1688b9e0>; layer <UIWindowLayer: 0x168b9ad0>>"]
可以看到,每一个以“#”开头的都是一个window,这里一共有4个window,其中第一个是keyWindow。那么哪一个是“Text Message”所在的window呢?从关键字上也能猜测出,带“Text”字样的第2和第4个window可能是我们的目标,而第4个window的hidden属性是YES,它压根儿没有显示在界面上,因此肯定不是它。可见,第2个window很可能就是我们的目标,用Cycript测测看,如下:
cy# [#0x1579ab70 setHidden:YES]
回车之后发现,不只是信息输入框,整个键盘都被隐藏起来了,如图10-5所示。
图10-5 信息输入框及键盘被隐藏
从而可以得出结论,“Text Message”就位于这个window中,继续通过Cycript来定位它,如下所示:
cy# [#0x1579ab70 setHidden:NO] cy# [#0x1579ab70 subviews] @[#"<UIInputSetContainerView: 0x1551fb10; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x1551f950>>"] cy# [#0x1551fb10 subviews] @[#"<UIInputSetHostView: 0x1551f5e0; frame = (0 250; 320 318); layer = <CALayer: 0x1551f480>>"] cy# [#0x1551f5e0 subviews] @[#"<UIKBInputBackdropView: 0x16827620; frame = (0 65; 320 253); userInteractionEnabled = NO; layer = <CALayer: 0x1681c3f0>>",#"<_UIKBCompatInputView: 0x157b88d0; frame = (0 65; 320 253); layer = <CALayer: 0x157b8a10>>",#"<CKMessageEntryView: 0x1682ca50; frame = (0 0; 320 65); opaque = NO; autoresize = W; layer = <CALayer: 0x168ec520>>"]
上面代码中又有3个subview,哪个是“Text Message”的所在?用下面的排除法来过一遍:
cy# [#0x16827620 setHidden:YES]
上面语句执行之后变成了图10-6所示的界面,说明这个view只是键盘背景而已。
执行下面的代码:
cy# [#0x16827620 setHidden:NO] cy# [#0x157b88d0 setHidden:YES]
这两个语句执行之后界面变成了图10-7所示的状态。
图10-6 键盘背景被隐藏
图10-7 键盘按键被隐藏
说明这个view就是键盘本身。也就是说,UIKBInputBackdropView和UIKBCompatInputView共同构成了一个键盘的view,这种官方设计模式可以供第三方键盘开发者或键盘主题制作者参考。
现在只剩最后一个subview了,CKMessageEntryView这个名字的意义其实也很明显,还是像下面这样验证一下吧:
cy# [#0x157b88d0 setHidden:NO] cy# [#0x1682ca50 setHidden:YES]
执行后界面如图10-8所示。
图10-8 信息输入框被隐藏
通过验证,说明“Text Message”在CKMessageEntryView中。继续缩小范围,如下:
cy# [#0x1682ca50 setHidden:NO] cy# [#0x1682ca50 subviews] @[#"<_UIBackdropView: 0x168ce210; frame = (0 0; 320 65); opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <_UIBackdropViewLayer: 0x168f5300>>",#"<UIView: 0x168d2b70; frame = (0 0; 320 0.5); layer = <CALayer: 0x168d2be0>>",#"<UIButton: 0x1684b240; frame = (266 27; 53 33); opaque = NO; layer = <CALayer: 0x168d64b0>>",#"<UIButton: 0x168b88b0; frame = (266 30; 53 26); hidden = YES; opaque = NO; gestureRecognizers = <NSArray: 0x16840030>; layer = <CALayer: 0x16858420>>",#"<UIButton: 0x16833ac0; frame = (15 33.5; 25 18.5); opaque = NO; gestureRecognizers = <NSArray: 0x1682d9b0>; layer = <CALayer: 0x16838780>>",#"<_UITextFieldRoundedRectBackgroundViewNeue: 0x168fba00; frame = (55 8; 209.5 49.5); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1682da50>>",#"<UIView: 0x168dcf10; frame = (55 8; 209.5 49.5); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x168e4170>>",#"<CKMessageEntryWaveformView: 0x1571b710; frame = (15 25.5; 251 35); alpha = 0; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1578fc90>>"]
还是使用排除法寻找“Text Message”所在的view,这里的过程就不再重复了,留给读者自己练习。在定位到了“UIView:0x168dcf10”(注意,是第二个UIView对象)之后,继续查看它的subview,如下:
cy# [#0x168dcf10 subviews] @[#"<CKMessageEntryContentView: 0x16389000; baseClass = UIScrollView; frame = (3 -4; 203.5 57.5); clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x168f0730>; layer = <CALayer: 0x168e41a0>; contentOffset: {0, 0}; contentSize: {203.5, 57}>"]
上面代码中只有一个subview,继续查看这个subview的subview,如下:
cy# [#0x16389000 subviews] @[#"<CKMessageEntryRichTextView: 0x16295200; baseClass = UITextView; frame = (0 20.5; 203.5 36.5); text = ''; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x168f5a60>; layer = <CALayer: 0x168f59c0>; contentOffset: {0, 0}; contentSize: {203.5, 36.5}>",#"<CKMessageEntryTextView: 0x15ad2a00; baseClass = UITextView; frame = (0 0; 203.5 36.5); text = ''; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x1578e600>; layer = <CALayer: 0x157dcff0>; contentOffset: {0, 0}; contentSize: {203.5, 36.5}>",#"<UIView: 0x157e9160; frame = (5 28; 193.5 0.5); layer = <CALayer: 0x15733bd0>>",#"<UIImageView: 0x157308d0; frame = (-0.5 55; 204 2.5); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer: 0x15730950>>",#"<UIImageView: 0x157ef530; frame = (201 0; 2.5 57.5); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x157ef5b0>>"]
还是用排除法寻找“Text Message”所在的view,我们发现,当执行“[#0x16295200 setHidden:YES]”时,只有“Text Message”被隐藏了,界面上的其他控件并未受影响,如图10-9所示。
这说明CKMessageEntryRichTextView就是我们想要定位的view。打开CKMessageEntry-Rich-TextView.h,看看能不能找到placeholder的踪影,如图10-10所示。
可是,在CKMessageEntryRichTextView中搜不到placeholder。难道推理出了差错?不要着急,转战到它的父类CKMessageEntryTextView里去看看,如图10-11所示。
可以看到,满屏的placeholder字眼。其中,placeholderLabel和placeholderText这2个property很可疑,难道它们就是我们在找的placeholder?用Cycript验证一下,执行如下命令:
cy# [#0x16295200 setPlaceholderText:@"iOSRE"]
图10-9 placeholder被隐藏
图10-10 CKMessageEntryRichTextView.h
此时,界面变成了下面这个样子(如图10-12所示)。
图10-11 CKMessageEntryTextView.h
图10-12 更改placeholder为“iOSRE”
不错,placeholderText就是我们要找的placeholder,自此它们两者等同,为了避免混淆,下文统一使用placeholderText。旗开得胜,我们跨出了万里长征的第一步!
placeholderText是一个property,而要改变一个property,笔者的第一反应就是它的setter。在通过调用setPlaceholderText:函数,把placeholderText从“Text Message”改为“iOSRE”的同时,不妨大胆假设一下,MobileSMS会不会也是通过调用这个setter来改变placeholderText的呢?实际验证一下是最好不过的,接下来轮到IDA和LLDB上场了。
因为最后定位到的CKMessageEntryTextView类来自ChatKit,所以接下来的目标是分析MobileSMS这个可执行文件中的ChatKit库,可以理解吧?好的,把ChatKit的二进制文件丢到IDA中,初始分析结束后,定位到[CKMessageEntryTextView setPlaceholderText:],如图10-13所示。
图10-13 [CKMessageEntryTextView setPlaceholderText:](1)
然后用LLDB附加MobileSMS,待初始化完成后执行“c”命令,让MobileSMS运行起来,如下:
(lldb) process connect connect://iOSIP:1234 Process 200596 stopped * thread #1: tid = 0x30f94, 0x316554f0 libsystem_kernel.dylib`mach_msg_trap + 20, queue = 'com.apple.main-thread, stop reason = signal SIGSTOP frame #0: 0x316554f0 libsystem_kernel.dylib`mach_msg_trap + 20 libsystem_kernel.dylib`mach_msg_trap + 20: -> 0x316554f0: pop {r4, r5, r6, r8} 0x316554f4: bx lr libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x316554f8: mov r12, sp 0x316554fc: push {r4, r5, r6, r8} (lldb) c Process 200596 resuming
之后,再看看ChatKit的ASLR偏移,如下:
(lldb) image list -o -f [ 0] 0x00079000 /private/var/db/stash/_.29LMeZ/Applications/MobileSMS.app/MobileSMS(0x000000000007d000) [ 1] 0x0019c000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x000000000019c000) [ 2] 0x01eac000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/Foundation.framework/Foundation …… [ 9] 0x01eac000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/PrivateFrameworks/ChatKit.framework/ChatKit
这个偏移值是0x1eac000。有了这个值,就可以在[CKMessageEntryTextView setPlaceholderText:]中下个断点,看看它有没有被调用,如果被调用,调用者又是谁。为了把断点下在这个函数的开头位置,要先在IDA中看看这个函数的基地址,如图10-14所示,可以看到,是0x2693BCE0。
图10-14 [CKMessageEntryTextView setPlaceholderText:](2)
所以断点地址是0x1eac000+0x2693BCE0=0x287E7CE0。
(lldb) br s -a 0x287E7CE0 Breakpoint 1: where = ChatKit`-[CKMessageEntryTextViewsetPlaceholderText:], address = 0x287e7ce0
接着,把地址输入框中的“bbs.iosre.com”换成一个支持iMessage的地址“snakeninny@gmail.com”,看看进程会不会停在断点上。这时,你会发现,在实际操作中,随着地址的变动,进程会多次停在断点上,这说明[CKMessageEntryTextView setPlaceholderText:]被调用了很多次。那么如何知道是在哪一次调用中,placeholderText从“Text Message”变成“iMessage”呢?此刻,之前介绍的comm命令就派上用场了,命令如下:
(lldb) br com add 1 Enter your debugger command(s). Type 'DONE' to end. > po $r2 > p/x $lr > c > DONE
这个命令的意思很简单,就是在断点触发时打印R2的“Objective-C description”,即setPlaceholdeText:的参数;然后以十六进制格式打印LR的值,即[CKMessageEntry-TextView setPlaceholderText:]的返回地址。如果R2的值是“iMessage”,说明系统自身也是通过setPlaceholderText:来改变placeholderText的,这个函数的参数就是placeholderText的一重数据源;同时因为对应LR的值位于调用者的地址范围内,所以可以找到setPlaceholderText:的调用者,继续跟踪参数的数据源,即placeholderText的二重数据源。清空地址输入框,再重新输入“snakeninny@gmail.com”,看看LLDB什么时候会打印“iMessage”,如下:
<object returned empty description> (unsigned int) $11 = 0x28768b33 Process 200596 resuming Command #3 'c' continued the target. <object returned empty description> (unsigned int) $13 = 0x28768b33 Process 200596 resuming Command #3 'c' continued the target. <object returned empty description> (unsigned int) $15 = 0x28768b33 Process 200596 resuming Command #3 'c' continued the target. Text Message (unsigned int) $17 = 0x28768b33 Process 200596 resuming Command #3 'c' continued the target. iMessage (unsigned int) $19 = 0x28768b33 Process 200596 resuming Command #3 'c' continued the target.
可以看到,当placeholder变成“iMessage”时,LR的值是0x28768b33。根据第4章“偏移后指令基地址=偏移前指令基地址+指令所在模块的ASLR偏移”公式,0x28768b33-0x1eac000=0x268BCB33。可见,这个地址位于ChatKit内,如图10-15所示。
图10-15 跳转到ChatKit中的0x268BCB33
说明MobileSMS确实就是通过setPlaceholder:函数来改变placeholder的,函数的参数就是placeholder的一重数据源,同时二重数据源的线索也有了着落。万里长征第二步走完,有惊无险!
前面在操作中多次编辑地址时,不知大家有没有注意到一个现象,那就是当我们正在编辑时,placeholderText是空白的;只有在点击了键盘上的“return”结束编辑后,placeholderText才会显示“Text Message”或“iMessage”。也就是说,当地址编辑结束时,iOS才会检测当前地址是否支持iMessage,出于节省性能的考虑,这样的设计合情合理。也正是基于这样的设计,在接下来的调试中,可以先把目标地址编辑好,然后设置断点,最后点击“return”,这样断点如果被触发,则说明进程停在了iMessage检测的过程中。下面把IDA往上拉,看看[CKMessageEntryTextView setPlaceholderText:]的调用者是谁,如图10-16所示。
图10-16 [CKMessageEntryTextView setPlaceholderText:]的调用者
虽然在“更新输入视图”的时候“设置占位符”,说得过去,但是,[CKMessageEntryView updateEntryView]没有参数,它怎么知道placeholderText应该设置成“Text Message”还是“iMessage”呢?那只有一种可能,就是它的内部进行了判断,得出了该地址支持iMessage的结论,改变了placeholderText的二重数据源。回到IDA,看看二重数据源来自哪里,如图10-17所示。
图10-17 寻找二重数据源
R2即函数参数,也就是一重数据源;而R2来自于R5,因此R5就是二重数据源。R5又是来自于哪里呢?这里出现了分支,我们看看分支的跳转条件,如图10-18所示。
图10-18 分支的跳转条件
可以看到,跳转条件是“[$r0 recipientCount]==0”。“recipient”的字面意思很明显,就是指信息的收件人,当收件人数为0,即没有收件人时,进程走右边,否则走左边。因为这里已经有一个收件人了,收件人数不为0,所以进程很有可能走左边。要验证也很简单,先清空地址输入框,然后在地址输入框中输入“snakeninny@gmail.com”,接着在右边的分支尾部下一个断点,最后点击键盘上的“return”。此时,会发现断点并没有得到触发,因此可以肯定地说,R5来自于左边的[$r8__ck_displayName],即[$r8__ck_displayName]是三重数据源。但R8又来自哪里呢?往上拉IDA,在[CKMessageEntryView updateEntryView]的开始部分,可以看到R8来自[[self conversation]sendingService],如图10-19所示。
因此,[[self conversation]sendingService]是四重数据源。再用LLDB来验证一下判断:清空地址输入框,输入“snakeninny@gmail.com”,然后在图10-19的“MOV R8,R0”处下一个断点,接着点击“return”。待断点触发时执行“po[$r0__ck_displayName]”,看看是否会输出“iMessage”,如下:
图10-19 寻找四重数据源
(lldb) br s -a 0x28768962 Breakpoint 14: where = ChatKit`-[CKMessageEntryView updateEntryView] + 54, address = 0x28768962 (lldb) br com add 14 Enter your debugger command(s). Type 'DONE' to end. > po [$r0 __ck_displayName] > c > DONE Text Message Process 200596 resuming Command #2 'c' continued the target. iMessage Process 200596 resuming Command #2 'c' continued the target.
从上面代码来看,断点被触发两次,第二次被触发时,iOS检测到“snakeninny@gmail.com”支持iMessage。既然“iMessage”来自于[[[self conversation]sendingService]__ck_displayName],那么[self conversation]是个什么类型的对象?[[self conversation]sendingService]呢?别急,下面一个一个来看。
重新输入地址,然后在[CKMessageEntryView updateEntryView]的前两个“objc_msgSend”上各下一个断点,最后点击“return”,现在来看看这里的对象类型,如下:
Process 14235 stopped * thread #1: tid = 0x379b, 0x2b528948 ChatKit`-[CKMessageEntryView updateEntryView] + 28, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1 frame #0: 0x2b528948 ChatKit`-[CKMessageEntryView updateEntryView] + 28 ChatKit`-[CKMessageEntryView updateEntryView] + 28: -> 0x2b528948: blx 0x2b5f5f44 ; symbol stub for: MarcoShouldLogMadridLevel$shim 0x2b52894c: mov r6, r0 0x2b52894e: movw r0, #51162 0x2b528952: movt r0, #2547 (lldb) p (char *)$r1 (char *) $6 = 0x2b60cc16 "conversation" (lldb) ni Process 14235 stopped * thread #1: tid = 0x379b, 0x2b52894c ChatKit`-[CKMessageEntryView updateEntryView] + 32, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2b52894c ChatKit`-[CKMessageEntryView updateEntryView] + 32 ChatKit`-[CKMessageEntryView updateEntryView] + 32: -> 0x2b52894c: mov r6, r0 0x2b52894e: movw r0, #51162 0x2b528952: movt r0, #2547 0x2b528956: add r0, pc (lldb) po $r0 CKPendingConversation<0x1587e870>{identifier:'(null)' guid:'(null)'}(null)
可见,[self conversation]返回的是一个CKPendingConversation类型的对象。接着看下一个,如下:
(lldb) c Process 14235 resuming Process 14235 stopped * thread #1: tid = 0x379b, 0x2b52895e ChatKit`-[CKMessageEntryView updateEntryView] + 50, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1 frame #0: 0x2b52895e ChatKit`-[CKMessageEntryView updateEntryView] + 50 ChatKit`-[CKMessageEntryView updateEntryView] + 50: -> 0x2b52895e: blx 0x2b5f5f44 ; symbol stub for: MarcoShouldLogMadridLevel$shim 0x2b528962: mov r8, r0 0x2b528964: movw r0, #52792 0x2b528968: movt r0, #2547 (lldb) p (char *)$r1 (char *) $8 = 0x2b6105e1 "sendingService" (lldb) ni Process 14235 stopped * thread #1: tid = 0x379b, 0x2b528962 ChatKit`-[CKMessageEntryView updateEntryView] + 54, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2b528962 ChatKit`-[CKMessageEntryView updateEntryView] + 54 ChatKit`-[CKMessageEntryView updateEntryView] + 54: -> 0x2b528962: mov r8, r0 0x2b528964: movw r0, #52792 0x2b528968: movt r0, #2547 0x2b52896c: add r0, pc (lldb) po $r0 IMService[SMS] (lldb) po [$r0 class] IMServiceImpl
显然,[CKPendingConversation sendingService]返回的是一个IMSerciceImpl对象,且其值为“IMService[SMS]”(第2次被触发时就会变成“IMService[iMessage]”)。因此,四重数据源就是“[CKPendingConversation sendingService]”,没问题吧?
分析到这里,再回到IDA,定位[CKPendingConversation sendingService],看看它的内部是如何工作的,如图10-20所示。
其中的逻辑并不算太复杂,但在若干分支里,进程实际走的是哪一条路呢?用LLDB调试一下(下断点前先重新输入地址),在所有条件跳转指令上留意一下跳转条件的值和下一条指令的地址,如下:
图10-20 [CKPendingConversation sendingService]
Process 14235 stopped * thread #1: tid = 0x379b, 0x2b5f0264 ChatKit`-[CKPendingConversation sendingService], queue = 'com.apple.main-thread, stop reason = breakpoint 3.1 frame #0: 0x2b5f0264 ChatKit`-[CKPendingConversation sendingService] ChatKit`-[CKPendingConversation sendingService]: -> 0x2b5f0264: push {r4, r5, r7, lr} 0x2b5f0266: add r7, sp, #8 0x2b5f0268: sub sp, #8 0x2b5f026a: mov r4, r0 (lldb) ni Process 14235 stopped …… * thread #1: tid = 0x379b, 0x2b5f027e ChatKit`-[CKPendingConversation sendingService] + 26, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2b5f027e ChatKit`-[CKPendingConversation sendingService] + 26 ChatKit`-[CKPendingConversation sendingService] + 26: -> 0x2b5f027e: cbz r0, 0x2b5f02a4 ; -[CKPendingConversation sendingService] + 64 0x2b5f0280: movw r0, #38082 0x2b5f0284: movt r0, #2535 0x2b5f0288: str r4, [sp] (lldb) p $r0 (unsigned int) $11 = 0 (lldb) ni Process 14235 stopped …… * thread #1: tid = 0x379b, 0x2b5f02b8 ChatKit`-[CKPendingConversation sendingService] + 84, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2b5f02b8 ChatKit`-[CKPendingConversation sendingService] + 84 ChatKit`-[CKPendingConversation sendingService] + 84: -> 0x2b5f02b8: cbz r0, 0x2b5f02c4 ; -[CKPendingConversation sendingService] + 96 0x2b5f02ba: mov r0, r4 0x2b5f02bc: mov r1, r5 0x2b5f02be: blx 0x2b5f5f44 ; symbol stub for: MarcoShouldLogMadridLevel$shim (lldb) p $r0 (unsigned int) $12 = 341691792 (lldb) ni Process 14235 stopped …… * thread #1: tid = 0x379b, 0x2b5f02c2 ChatKit`-[CKPendingConversation sendingService] + 94, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2b5f02c2 ChatKit`-[CKPendingConversation sendingService] + 94 ChatKit`-[CKPendingConversation sendingService] + 94: -> 0x2b5f02c2: cbnz r0, 0x2b5f032c ; -[CKPendingConversation sendingService] + 200 0x2b5f02c4: movw r0, #35464 0x2b5f02c8: movt r0, #2535 0x2b5f02cc: add r0, pc (lldb) p $r0 (unsigned int) $13 = 341691792 (lldb) ni Process 14235 stopped …… * thread #1: tid = 0x379b, 0x2b5f032e ChatKit`-[CKPendingConversation sendingService] + 202, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2b5f032e ChatKit`-[CKPendingConversation sendingService] + 202 ChatKit`-[CKPendingConversation sendingService] + 202: -> 0x2b5f032e: pop {r4, r5, r7, pc} ChatKit`-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]: 0x2b5f0330: push {r4, r5, r6, r7, lr} 0x2b5f0332: add r7, sp, #12 0x2b5f0334: push.w {r8, r10, r11}
进程的执行流程就显而易见了。这里一共有3次条件跳转,分别是CBZ、CBZ和CBNZ,而此时的R0分别是0、341691792和341691792,由此可知进程的执行流程是绿线、红线、蓝线、绿线,如图10-21所示。
图10-21 进程的执行流程
那么[CKPendingConversation sendingService]的值实际来自[CKPendingConversation composeSendingService],它是五重数据源,没问题吧?现在,在IDA中定位到了新的函数(如图10-22所示),继续分析。
图10-22 [CKPendingConversation composeSendingService]
很明显,[CKPendingConversation composeSendingService]只是读取了实例变量_composeSendingService的值,也就是说,_composeSendingService是六重数据源。既然如此,我们只要找到写入此实例变量的位置,就找到了七重数据源了。
单击_OBJC_IVAR_$_CKPendingConversation._composeSendingService,让光标停留在其上,然后按下“x”,打开此变量的xrefs窗口,如图10-23所示。
在这里,显式调用_composeSendingService的正好是一个getter和一个setter,因此composeSendingService很可能是一个property,打开CKPendingConversation.h来验证一下,如图10-24所示。
图10-23 查看交叉引用
图10-24 CKPendingConversation.h
在Objective-C中,对property的写操作一般是通过setter完成的,那么要寻找七重数据源,就要在[CKPendingConversation setComposeSendingService:]上下一个断点,然后查看其调用者的情况。操作流程跟前面完全一样,先重新输入地址,然后在[CKPendingConversation setComposeSendingService:]的开始处下一个断点,最后点击键盘上的“return”,触发断点,如下:
Process 30928 stopped * thread #1: tid = 0x78d0, 0x30b3665c ChatKit`-[CKPendingConversation setComposeSendingService:], queue = 'com.apple.main-thread, stop reason = breakpoint 1.1 frame #0: 0x30b3665c ChatKit`-[CKPendingConversation setComposeSendingService:] ChatKit`-[CKPendingConversation setComposeSendingService:]: -> 0x30b3665c: movw r1, #41004 0x30b36660: movt r1, #2535 0x30b36664: add r1, pc 0x30b36666: ldr r1, [r1] (lldb) p/x $lr (unsigned int) $0 = 0x30b3656d
用这里的LR减去ChatKit的ASLR偏移得到0x2698456D,得出其偏移前的值。再在IDA中跳到这个值所在的地址,如图10-25所示。
图10-25 跳转到ChatKit中的0x2698456D
[CKPendingConversation setComposeSendingService:]的参数R2就是七重数据源。R2来自R6,因此R6是八重数据源。再往上看看R6是什么,如图10-26所示。
图10-26 寻找九重数据源
R6来自R1,可见R1是九重数据源。那么R1又是哪里来的?因为现在位于sub_26984530的内部,而R1没有被赋值就直接取值了,说明R1来自sub_26984530的调用者,对吧?那就去看看sub_26984530的交叉引用信息,寻找它的调用者信息,如图10-27所示。
图10-27 查看交叉引用
刷新发送服务?这个名字有点“暧昧”啊!到图10-28中所示的[CKPendingConversation refreshComposeSendingServiceForAddresses:withCompletionBlock:]中看一看,sub_26984530明显是refreshStatusForAddresses:withCompletionBlock:的第二个参数,即completionBlock,如图10-28所示。
这里虽然显式用到了sub_26984530,但只是作为参数传递给objc_msgSend的,并没有直接调用。那么它的调用者到底是谁呢?这个套路前面已经反复演练过了:重新输入地址,在sub_26984530的开始处下断点,点击键盘上的“return”,触发断点,如下:
图10-28 [CKPendingConversation refreshComposeSendingServiceForAddresses:withCompletionBlock:]
Process 30928 stopped * thread #1: tid = 0x78d0, 0x30b36530 ChatKit`__86-[CKPendingConversation refreshComposeSendingServiceForAddresses:withCompletionBlock:]_block_invoke, queue = 'com.apple.main-thread, stop reason = breakpoint 6.1 frame #0: 0x30b36530 ChatKit`__86-[CKPendingConversation refreshComposeSendingServiceForAddresses:withCompletionBlock:]_block_invoke ChatKit`__86-[CKPendingConversation refreshComposeSendingServiceForAddresses:withCompletionBlock:]_block_invoke: -> 0x30b36530: push {r4, r5, r6, r7, lr} 0x30b36532: add r7, sp, #12 0x30b36534: push.w {r8, r10} 0x30b36538: sub sp, #4 (lldb) p/x $lr (unsigned int) $38 = 0x30b364bb
LR偏移前的值是0x30b364bb-0xa1b2000=0x269844BB。定位到IDA中,如图10-29所示。
图10-29 sub_26984530的调用者
可见,sub_26984530并没有被显式调用,而是在sub_26984444内部把函数的地址存放到了R6中,然后跳转过去隐式调用。那么自然,九重数据源就来自于sub_26984444,再往上看看它的来源,如图10-30所示。
图10-30 寻找十重数据源(1)
这个subroutine内部进行了多次判断,才决定是把[IMServiceImpl smsService]还是把[IMServiceImpl iMessageService]赋给R1。我们看看判断条件是什么(如图10-31所示)。
图10-31 寻找十重数据源(2)
当R0为2时,[IMServiceImpl iMessageService]是十重数据源,否则还要判断R1的值,若其值是0,则十重数据源是[IMServiceImpl smsService],否则是[IMServiceImpl iMessageService]。用伪代码表示如下:
- (BOOL)supportIMessage { if (R0 == 2 || R1 != 0) return YES; return NO; }
也就是说,这里的R0与R1共同决定十重数据源的值,它们俩共同担当起了十一重数据源的重任,我们分别称它们为十一重数据源A和十一重数据源B,上面的伪代码也可以写作:
- (BOOL)supportIMessage { if (十一重数据源A == 2 || 十一重数据源B != 0) return YES; return NO; }
回到图10-31,再看看十一重数据源是哪里来的——R0来自于“UXTB.W R0,R8”。
图10-32 UXTB的作用
依图10-32所示的ARM官方文档可知,UXTB的作用是给R8中存放的8位数值高位填充0,将其扩展到32位,然后放到R0里去(R0是32位寄存器),也就是说,R0来自于R8,R8是十二重数据源A。而arg_0=0x8,R8=*(R7+arg_0)=*(R7+0x8),R7=SP+0xc,因此R8=*(SP+0x14),即*(SP+0x14)是十三重数据源A。*(SP+0x14)又是哪里来的呢?它不是凭空产生的,因此在“LDR.W R8,[R7,#8]”之前,一定有一条指令往*(SP+0x14)写了数据,对吧?那里就是十四重数据源A的所在之处,我们要倒推回去找到这个往*(SP+0x14)写数据的地方。
但是事情远没有说起来这么简单——因为PUSH和POP等操作会改变SP的值,所以现在的*(SP+0x14)在其他的指令中可能会由于SP发生变化而变成*(SP’+offset),而offset的值现在还没法确定!这下子麻烦了,我们必须找出“LDR.W R8,[R7,#8]”之前每一个往*(SP’+offset)写数据的操作,然后检查(SP+0x14)和(SP’+offset)是否相等。而SP的变化又比较频繁,这就成了难点。请大家一定跟紧了,现在,要从“LDR.W R8,[R7,#8]”开始,倒推并检查每一个往*(SP’+offset)写数据的操作。
在sub_26984444,“LDR.W R8,[R7,#8]”前的4条指令全都跟SP相关,把当前指令还未执行时SP的值用SP1~SP4标注到指令上,如图10-33所示。
图10-33 标记不同的SP
当“PUSH{R4-R7,LR}”还未执行时,SP的值是SP1,执行之后变成SP2,可以理解吧?下面一条条指令推导一下这里的SP是怎么变化的:
“PUSH{R4-R7,LR}”将R4、R5、R6、R7和LR共5个寄存器入栈,每个寄存器都是32位即4字节,而ARM的栈是满递减的,因此,SP2=SP1-5*0x4=SP1-0x14;“ADD R7,SP,#0xC”,即R7=SP2+0xc,没有影响SP的值;“STR.W R8,[SP,#0xC+var_10]!”中的var_10=-0x10,因此这条指令等价于“STR.W R8,[SP,#-4]!”,即*(SP2–0x4)=R8,且此条指令仍未影响SP的值;“SUB SP,SP,#4”,即SP3=SP2-0x4。按照这种表达方式,十三重数据源A是*(SP2+0x14),在通过“LDR.W R8,[R7,#8]”取值时尚未在sub_26984444内赋值,因此*(SP2+0x14)的值一定来自sub_26984444的调用者。类似地,在sub_26984444内部R1未被赋值即已取值,它也一定来自sub_26984444的调用者,可以理解吗?如果还不理解,就把这一段重看一遍,一定要先想清楚,再继续看下面的内容。
好了,十三重数据源A和十一重数据源B都来自sub_26984444的调用者,下一步的任务已经很明确了:去sub_26984444的调用者寻找十四重数据源A和十二重数据源B。
重输地址,在sub_26984444的开始处下断点,点击键盘上的“return”,触发断点,如下:
Process 30928 stopped * thread #1: tid = 0x78d0, 0x30b36444 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke, queue = 'com.apple.main-thread, stop reason = breakpoint 7.1 frame #0: 0x30b36444 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke: -> 0x30b36444: push {r4, r5, r6, r7, lr} 0x30b36446: add r7, sp, #12 0x30b36448: str r8, [sp, #-4]! 0x30b3644c: sub sp, #4 (lldb) p/x $lr (unsigned int) $39 = 0x331f0d75
LR偏移前的值是0x331f0d75–0xa1b2000=0x2903ED75,并不在ChatKit中。这时候该怎么定位0x2903ED75位于哪一个模块呢?方法之前也说过了,那就是在sub_26984444的末尾下断点,然后“ni”到调用者的内部,看看它在哪个模块就好了,如下:
Process 30928 stopped * thread #1: tid = 0x78d0, 0x30b364c0 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 124, queue = 'com.apple.main-thread, stop reason = breakpoint 8.1 frame #0: 0x30b364c0 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 124 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 124: -> 0x30b364c0: pop {r4, r5, r6, r7, pc} 0x30b364c2: nop ChatKit`__copy_helper_block_: 0x30b364c4: ldr r1, [r1, #20] 0x30b364c6: adds r0, #20 (lldb) ni Process 30928 stopped * thread #1: tid = 0x78d0, 0x331f0d74 IMCore`___lldb_unnamed_function425$$IMCore + 1360, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x331f0d74 IMCore`___lldb_unnamed_function425$$IMCore + 1360 IMCore`___lldb_unnamed_function425$$IMCore + 1360: -> 0x331f0d74: movw r0, #26972 0x331f0d78: movt r0, #2081 0x331f0d7c: add r0, pc 0x331f0d7e: ldr r1, [r0]
我们来到了IMCore的内部。刚才已经计算出了sub_26984444执行完成后的返回地址是0x2903ED75,因此把IMCore拖进IDA,待分析完毕后跳转到0x2903ED75,如图10-34所示。
图10-34 sub_26984444的调用者
又是一个来自sub_2903E824的隐式调用,且它上面的4条指令又有2条跟SP相关。为方便阅读,下面把“BLX R6”前后两个模块的指令给拼到一张图里,拼接前后的效果如图10-35和图10-36所示。
图10-35 拼接前
这里先寻找十四重数据源A,它被写入了*(SP2+0x14),还记得吗?模仿上面的步骤,把loc_2903ED6A里的SP标号,如图10-37所示。
然后从loc_2903ED6A的第一条指令开始,看看这里的SP是怎么变化的:
图10-36 拼接后
图10-37 标记不同的SP
“LDR R3,[SP,#0xA8+var_98]”,即R3=*(SP1+0xA8+var_98),而var_98=-0x98(如图10-38所示)。
图10-38 sub_2903e824
因此R3=*(SP1+0x10),且本条指令不影响SP的值;“MOV R2,R8”跟SP无关;“STR R1,[SP,#0xA8+var_A8]”中的var_A8=-0xA8,即*SP1=R1,且本条指令也不影响SP的值;“MOV R1,R5”跟SP无关。这么多SP可能会把你绕晕,再梳理一下其中的逻辑:
目的:找到写入*(SP2+0x14)的地方
因为SP2=SP1-0x14
且*SP1=R1
所以,“STR R1,[SP,#0xA8+var_A8]”就是写入*(SP2+0x14)的地方,十四重数据源A就是这条指令中的R1!而十二重数据源B,显然就是“MOV R1,R5”中的R5。从十三重数据源A到十四重数据源A,从十一重数据源B到十二重数据源B的追踪跨模块,逻辑比较复杂,用图10-39来展示会较为直观,建议大家对照着这张图,把这个跨模块的地方弄清楚。
在继续分析之前,先用LLDB来验证一下到此为止的判断:重输地址,然后在“STR R1,[SP,#0xA8+var_A8]”上设置断点,打印R1,即十四重数据源A;执行“ni”命令到“MOV R1,R5”,打印R5,即十二重数据源B;接着要经历一次模块切换,执行“si”命令到“CMP R0,#2”,打印R0,即十三重数据源A;执行“ni”命令到“TST.W R1,#0xFF”,打印R1,即十一重数据源B。点击“return”,触发断点后,看看它们的值是不是像图10-39里示意的那样两两相等,如下:
图10-39 数据源间的关系
(lldb) br s -a 0x30230D6E Process 37477 stopped * thread #1: tid = 0x9265, 0x30230d6e IMCore`___lldb_unnamed_function425$$IMCore + 1354, queue = 'com.apple.main-thread, stop reason = breakpoint 11.1 frame #0: 0x30230d6e IMCore`___lldb_unnamed_function425$$IMCore + 1354 IMCore`___lldb_unnamed_function425$$IMCore + 1354: -> 0x30230d6e: str r1, [sp] 0x30230d70: mov r1, r5 0x30230d72: blx r6 0x30230d74: movw r0, #26972 (lldb) p $r1 (unsigned int) $27 = 0 (lldb) ni Process 37477 stopped * thread #1: tid = 0x9265, 0x30230d70 IMCore`___lldb_unnamed_function425$$IMCore + 1356, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x30230d70 IMCore`___lldb_unnamed_function425$$IMCore + 1356 IMCore`___lldb_unnamed_function425$$IMCore + 1356: -> 0x30230d70: mov r1, r5 0x30230d72: blx r6 0x30230d74: movw r0, #26972 0x30230d78: movt r0, #2081 (lldb) p $r5 (unsigned int) $28 = 1 (lldb) ni Process 37477 stopped * thread #1: tid = 0x9265, 0x30230d72 IMCore`___lldb_unnamed_function425$$IMCore + 1358, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x30230d72 IMCore`___lldb_unnamed_function425$$IMCore + 1358 IMCore`___lldb_unnamed_function425$$IMCore + 1358: -> 0x30230d72: blx r6 0x30230d74: movw r0, #26972 0x30230d78: movt r0, #2081 0x30230d7c: add r0, pc (lldb) si Process 37477 stopped * thread #1: tid = 0x9265, 0x2db76444 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke, queue = 'com.apple.main-thread, stop reason = instruction step into frame #0: 0x2db76444 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke: -> 0x2db76444: push {r4, r5, r6, r7, lr} 0x2db76446: add r7, sp, #12 0x2db76448: str r8, [sp, #-4]! 0x2db7644c: sub sp, #4 (lldb) ni …… Process 37477 stopped * thread #1: tid = 0x9265, 0x2db7645c ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 24, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2db7645c ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 24 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 24: -> 0x2db7645c: cmp r0, #2 0x2db7645e: bne 0x2db7647a ; __71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 54 0x2db76460: movw r0, #19376 0x2db76464: movt r0, #2535 (lldb) p $r0 (unsigned int) $29 = 0 (lldb) ni …… Process 37477 stopped * thread #1: tid = 0x9265, 0x2db7647e ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 58, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x2db7647e ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 58 ChatKit`__71-[CKPendingConversation refreshStatusForAddresses:withCompletionBlock:]_block_invoke + 58: -> 0x2db7647e: tst.w r1, #255 0x2db76482: movt r0, #2535 0x2db76486: add r0, pc 0x2db76488: ldr r0, [r0] (lldb) p $r1 (unsigned int) $30 = 1
打印结果验证了分析结果,且十四重数据源A的值是0,十二重数据源B的值是1。接下来把火力集中在IMCore上,继续寻找十五重数据源A和十三重数据源B,先从前者下手。
十五重数据源A已经直观表现在了图10-40中。
图10-40 十五重数据源A
它要么来自“MOVS R1,#1”,要么来自“MOVS R1,#0”,也就是说十五重数据源的值要么是0,要么是1。事情变得有意思起来了……
不知道大家有没有注意到,从十一重数据源A开始,数据源A的值就是一脉相承的,即十一重=十二重=十三重=十四重=十五重数据源A=0或1。但是,我们之前分析的伪代码是这样的:
- (BOOL)supportIMessage { if (十一重数据源A == 2 || 十一重数据源B != 0) return YES; return NO; }
而十一重数据源A的值非0即1,是不可能等于2的。这样的话,数据源A已经没有意义了,不是吗?伪代码可以改成:
- (BOOL)supportIMessage { if (十一重数据源B != 0) return YES; return NO; }
因此,可以把重点放在寻找十三重数据源B上来,以下简称十三重数据源。因为十二重数据源B是R5,所以十三重数据源一定被某条指令写入R5了,对吧?单击R5,IDA会贴心地帮我们将其标记为黄色,方便从大量的汇编代码中定位R5。继续逆向,看看R5是什么时候被写入的。
当向上寻找十三重数据源,跟踪到loc_2903EAE0时,会发现它的上游有4条分支(如图10-41所示)。
图10-41 loc_2903EAE0
在图10-41中,从左到右的3条分支中均含有“MOVS R5,#0”的操作,但这跟R5=1的结果是矛盾的,因此loc_2903EAE0一定是经由最右边的那一条线路到达的,十三重数据源就位于这条线路上,顺着这条线路继续向上寻找R5的踪影。
跟踪到loc_2903EA3E时,出现的情况似曾相识。它的上游虽然有3条分支,但左1和左2分支均含有“MOVS R5,#0”的操作(如图10-42所示),因此可以直接排除。
图10-42 loc_2903EA3E
它的实际上游是左3分支,即loc_2903E9C4;而loc_2903E9C4的上游有2条分支,其中均含有“MOVS R5,#1”的操作,那么进程的实际执行流程是从这2条分支中的哪一条到达loc_2903E9C4的呢?重输地址,在2个分支的跳转处各下一个断点,点击键盘上的“return”,看看会触发哪个断点,这样就一清二楚了。这里的操作流程笔者就省略掉了,请读者独立完成,相信在简单的操作之后,你也会发现,左1分支才是进程实际执行的流程,即图10-43。
图10-43 左1分支
现在,找到了十三重数据源,它是常量1。你可能会问,十三重数据源是常量的话,十四重数据源还存在吗?数据源的线索看似中断了,下一步该怎么办?问得好!
在刚才的代码中,我们看到了几处“MOVS R5,#0”的操作,而十三重数据源来自“MOVS R5,#1”,看似数据源是常量,但按照程序设计的思想,到底往R5里写0还是写1,应该由一个条件判断来决定,就像下面的伪代码:
if (iMessageIsAvailable) R5 = 1; else R5 = 0;
用熟悉的IDA流程图表示,就是图10-44所示的样子。
从宏观角度来讲,其实这个条件判断就是十四重数据源,不是吗?相信你也反应过来了,上面的伪代码其实就是:
R5 = iMessageIsAvailable;
图10-44 伪IDA流程图
弄清了这个概念,接下来的任务就是继续回溯,分析每个出现分支的地方,如果它的不同分支会往R5里写不同的值,就要搞清楚分支条件是什么,而这个分支条件就是我们要找的数据源。到图10-45所示的代码段里去看看。
如果分支走了左边,R5是有可能被置0的。由于分支条件是objc_msgSend的返回值,因此下个断点看看执行的到底是什么函数,如下:
图10-45 分支
Process 132234 stopped * thread #1: tid = 0x2048a, 0x331f092e IMCore`___lldb_unnamed_function425$$IMCore + 266, queue = 'com.apple.main-thread, stop reason = breakpoint 5.1 frame #0: 0x331f092e IMCore`___lldb_unnamed_function425$$IMCore + 266 IMCore`___lldb_unnamed_function425$$IMCore + 266: -> 0x331f092e: blx 0x332603b0 ; symbol stub for: objc_msgSend 0x331f0932: mov r8, r0 0x331f0934: cmp.w r8, #0 0x331f0938: bne 0x331f08e2 ; ___lldb_unnamed_function425$$IMCore + 190 (lldb) p (char *)$r1 (char *) $6 = 0x2f7d81d9 "countByEnumeratingWithState:objects:count:" (lldb) po $r0 <__NSArrayI 0x16706930>( mailto:snakeninny@gmail.com )
可以看到,是一个对收件人队列的遍历函数,如果收件人队列不为空,分支就会走右边。实际上收件人队列肯定不会为空,因此这个分支条件不会成立,类似于已弃用的数据源A,是不会产生分支的。继续往上,寻找上一个分支发生的地方,如图10-46所示。
在图10-46中,R11和R8各是什么?在IDA里可以很直观地看到,R11来自图10-47。
图10-46 分支
图10-47 loc_2903e8e2
R11的初始值是0,每次在执行“CMP R11,R8”之前,R11都递增1。这么看起来,R11充当了计数器的作用。“CMP”执行了减法操作,如果产生借位,则将Carry置0,否则Carry置1。这里的分支指令是“BCC”,“CC”代表“Carry Clear”,也就是Carry位为0。也就是说,如果R11–R8产生借位,即R8大于R11,则分支走右边,否则走左边。我们看看R8是什么,如图10-48所示。
图10-48 R8来源
R8来自[NSArray countByEnumeratingWithState:objects:count:]。重输地址,下断点,点击“return,”看看NSArray是什么,如下:
(lldb) br s -a 0x3023089C Breakpoint 2: where = IMCore`___lldb_unnamed_function425$$IMCore + 120, address = 0x3023089c Process 102482 stopped * thread #1: tid = 0x19052, 0x3023089c IMCore`___lldb_unnamed_function425$$IMCore + 120, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1 frame #0: 0x3023089c IMCore`___lldb_unnamed_function425$$IMCore + 120 IMCore`___lldb_unnamed_function425$$IMCore + 120: -> 0x3023089c: blx 0x302a03b0 ; symbol stub for: objc_msgSend 0x302308a0: mov r8, r0 0x302308a2: cmp.w r8, #0 0x302308a6: beq.w 0x302309c2 ; ___lldb_unnamed_function425$$IMCore + 414 (lldb) p (char *)$r1 (char *) $5 = 0x2c8181d9 "countByEnumeratingWithState:objects:count:" (lldb) po $r0 <__NSArrayI 0x178d6b20>( mailto:snakeninny@gmail.com )
NSArray是收件人队列,因此R8是收件人个数,如果收件人个数大于1,在第一次执行“CMP R11,R8”时R11是1,则R8大于R11,分支走右边,到达图10-49。
图10-49 分支
loc_2903E8E6的分支条件是R0,如果R0==0则走左边,不支持iMessage;否则走右边,到达图10-50。
图10-50的分支条件仍是R0,如果R0==2则走左边,不支持iMessage;否则走右边,回到图10-46。这3段代码循环没有更改R8的值,只要loc_2903E8E6最下面的R0!=0&&R0!=2,图10-46的分支就没有意义——R11一直递增,而R8不变,这个分支早晚会走左边,得出支持iMessage的结论;也就是说,在这个循环里,本质的分支条件是R0的值。还记得我们刚刚得出的结论吗——“分析每个出现分支的地方,如果它的不同分支会往R5里写不同的值,就要搞清楚分支条件是什么,而这个分支条件就是我们要找的数据源。”——R0就是十四重数据源。
图10-50 分支
下面用LLDB看看这里的几个objc_msgSend都是什么,R0是怎么来的,如下:
Process 154446 stopped * thread #1: tid = 0x25b4e, 0x331f0900 IMCore`___lldb_unnamed_function425$$IMCore + 220, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1 frame #0: 0x331f0900 IMCore`___lldb_unnamed_function425$$IMCore + 220 IMCore`___lldb_unnamed_function425$$IMCore + 220: -> 0x331f0900: blx 0x332603b0 ; symbol stub for: objc_msgSend 0x331f0904: ldr r0, [sp, #40] 0x331f0906: mov r2, r4 0x331f0908: ldr r1, [sp, #20] (lldb) p (char *)$r1 (char *) $7 = 0x2f7d897a "removeObject:" (lldb) po $r0 <__NSArrayM 0x170ec120>( mailto:snakeninny@gmail.com ) (lldb) po $r2 mailto:snakeninny@gmail.com (lldb) ni …… Process 154446 stopped * thread #1: tid = 0x25b4e, 0x331f090a IMCore`___lldb_unnamed_function425$$IMCore + 230, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x331f090a IMCore`___lldb_unnamed_function425$$IMCore + 230 IMCore`___lldb_unnamed_function425$$IMCore + 230: -> 0x331f090a: blx 0x332603b0 ; symbol stub for: objc_msgSend 0x331f090e: ldr r1, [sp, #24] 0x331f0910: blx 0x332603b0 ; symbol stub for: objc_msgSend 0x331f0914: cbz r0, 0x331f0946 ; ___lldb_unnamed_function425$$IMCore + 290 (lldb) p (char *)$r1 (char *) $10 = 0x2f7d8113 "valueForKey:" (lldb) po $r2 mailto:snakeninny@gmail.com (lldb) po $r0 { "mailto:snakeninny@gmail.com" = 1; } (lldb) po [$r0 class] __NSCFDictionary (lldb) ni …… Process 154446 stopped * thread #1: tid = 0x25b4e, 0x331f0910 IMCore`___lldb_unnamed_function425$$IMCore + 236, queue = 'com.apple.main-thread, stop reason = instruction step over frame #0: 0x331f0910 IMCore`___lldb_unnamed_function425$$IMCore + 236 IMCore`___lldb_unnamed_function425$$IMCore + 236: -> 0x331f0910: blx 0x332603b0 ; symbol stub for: objc_msgSend 0x331f0914: cbz r0, 0x331f0946 ; ___lldb_unnamed_function425$$IMCore + 290 0x331f0916: cmp r0, #2 0x331f0918: beq 0x331f09ca ; ___lldb_unnamed_function425$$IMCore + 422 (lldb) p (char *)$r1 (char *) $14 = 0x2f7de6f3 "integerValue" (lldb) po $r0 1 (lldb) po [$r0 class] __NSCFNumber (lldb) c
将这3个objc_msgSend还原成ObjC函数,分别是[NSArray removeObject:@"mailto:snakeninny@gmail.com"]、[NSDictionary valueForKey:@"mailto:snakeninny@gmail.com"]和[NSNumber integerValue],其中第二个objc_msgSend的R0值得关注,正是它(NSDictionary)中包含的键值对,决定了十四重数据源;因此,这个NSDictionary就是十五重数据源。由图10-49可知,它来自于[SP,#0xA8+var_80],因此[SP,#0xA8+var_80]是十六重数据源。接下来的套路已经做过好几遍了:把光标放在var_80上,然后按下“x”,看看它的交叉引用,如图10-51所示。
图10-51 查看交叉引用
可以看到,只有一处指令写入了这个地址,双击这条指令,直接跳转到了sub_2903E824的头部,如图10-52所示。
图10-52 sub_2903E824
十六重数据源来自于R5,故R5是十七重数据源;十七重数据源又来自于R1,因此R1是十八重数据源,而它没有被赋值就直接取值了,说明R1来自sub_2903E824的调用者,对吧?看看它的交叉引用,如图10-53所示。
图10-53 查看交叉引用
“为发送新的信息计算服务”这个名字的含义已经很明显了,双击第一条交叉引用,去它的显式调用者那瞅瞅,如图10-54所示。
这里要先确认一下sub_2903E824的调用者是不是来自“IMChatCalculateServiceForSending-NewCompose”——清空地址输入框,输入地址,在sub_2903E824的第一条指令上下一个断点,点击键盘上的“return”,触发断点,如下:
Process 154446 stopped * thread #1: tid = 0x25b4e, 0x331f0824 IMCore`___lldb_unnamed_function425$$ IMCore, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1 frame #0: 0x331f0824 IMCore`___lldb_unnamed_function425$$IMCore IMCore`___lldb_unnamed_function425$$IMCore: -> 0x331f0824: push {r4, r5, r6, r7, lr} 0x331f0826: add r7, sp, #12 0x331f0828: push.w {r8, r10, r11} 0x331f082c: sub sp, #144 (lldb) p/x $lr (unsigned int) $17 = 0x331f067b (lldb)
图10-54 sub_2903E824的调用者
这里的ASLR偏移是0xa1b2000,所以调用者的实际地址是0x2903E67B,正是来自“IMChatCalculateServiceForSendingNewCompose”——十八重数据源来自R5,因此R5是十九重数据源;而十九重数据源来自objc_msgSend的返回值,故而该返回值是二十重数据源。万事俱备,只欠东风,我们看看这个神秘的objc_msgSend到底做了些什么,如下:
Process 154446 stopped * thread #1: tid = 0x25b4e, 0x331f0668 IMCore`IMChatCalculateServiceForSendingNewCompose + 688, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1 frame #0: 0x331f0668 IMCore`IMChatCalculateServiceForSendingNewCompose + 688 IMCore`IMChatCalculateServiceForSendingNewCompose + 688: -> 0x331f0668: blx 0x332603b0 ; symbol stub for: objc_msgSend 0x331f066c: mov r5, r0 0x331f066e: add r0, sp, #44 0x331f0670: mov r1, r5 (lldb) p (char *)$r1 (char *) $18 = 0x33274340 "_currentIDStatusForDestinations:service:listenerID:" (lldb) po $r0 <IDSIDQueryController: 0x15dcb010> (lldb) po $r2 <__NSArrayM 0x170e7900>( mailto:snakeninny@gmail.com ) (lldb) po $r3 com.apple.madrid (lldb) po [$r3 class] __NSCFConstantString (lldb) x/10 $sp 0x001e4548: 0x3b3f52b8 0x001e459c 0x3b4227b4 0x3c01b05c 0x001e4558: 0x00000001 0x00000000 0x170828d0 0x001e4594 0x001e4568: 0x2baac821 0x00000000 (lldb) po 0x3b3f52b8 __kIMChatServiceForSendingIDSQueryControllerListenerID (lldb) po [0x3b3f52b8 class] __NSCFConstantString (lldb) c
锲而不舍,终有斩获。这个objc_msgSend还原之后,是[[IDSIDQueryController sharedInstance]_currentIDStatusForDestinations:@[@"mailto:snakeninny@gmail.com"]service:@"com.apple.madrid"listenerID:@"__kIMChatServiceForSendingIDSQueryControllerListenerID"],因为后两个参数是常量,所以可变参数只有第一个数组,也就是收件人数组,我们终于跟踪到了原始数据源!
笔者知道本节的内容很难,你可能已经被绕晕了,但行百里者半九十,还差最后一步了,打起精神来!
函数都被我们找到了,貌似可以通过更改第一个NSArray参数,来达到检测任意目标地址是否支持iMessage的效果,只要它的返回值(NSDictionary)中key所对应的value非0且非2,则key支持iMessage,否则key仅支持SMS。真的是这样吗?我们已经知道,对于邮件地址来说,参数格式为“mailto:”,那电话号码的参数格式呢?在_currentIDStatus-ForDestinations:service:listenerID:上下个断点看一看,如下:
Process 102482 stopped * thread #1: tid = 0x19052, 0x30230668 IMCore`IMChatCalculateServiceForSendingNewCompose + 688, queue = 'com.apple.main-thread, stop reason = breakpoint 6.1 frame #0: 0x30230668 IMCore`IMChatCalculateServiceForSendingNewCompose + 688 IMCore`IMChatCalculateServiceForSendingNewCompose + 688: -> 0x30230668: blx 0x302a03b0 ; symbol stub for: objc_msgSend 0x3023066c: mov r5, r0 0x3023066e: add r0, sp, #44 0x30230670: mov r1, r5 (lldb) po $r2 <__NSArrayM 0x17820560>( tel:+86PhoneNumber )
LLDB和debugserver可以暂时休息一下了。回到Cycript中,实际验证一下猜测,如下:
FunMaker-5:~ root# cycript -p MobileSMS cy# [[IDSIDQueryController sharedInstance] _currentIDStatusForDestinations:@[@"mailto:snakeninny@gmail.com", @"mailto:snakeninny@icloud.com", @"tel:bbs.iosre.com", @"mailto:bbs.iosre.com", @"tel:911", @"tel:+86PhoneNumber"] service:@"com.apple.madrid" listenerID:@"__kIMChatServiceForSendingIDSQueryControllerListenerID"]@{"tel:bbs.iosre.com":2,"mailto:snakeninny@gmail.com":1,"tel:911":2,"mailto:bbs.iosre.com":2,"mailto:snakeninny@icloud.com":1,"tel:+86PhoneNumber ":1}
哈哈,输出的结果再清楚不过了,2个支持iMessage的邮箱和1个手机号均返回了1,而另3个不支持iMessage的地址返回了2,实验结果验证了分析,而且还知道了iMessage的内部称呼为“Madrid”。任务完成,万岁!