说明:
该文章为本人原创写作于去年,简书和掘金上的文章皆为本人发表,现将其发表于看雪。
感觉是要搞个《逆向分析及修复app》系列的节奏啊
大家好,我又来了。上次给大家分享了《逆向及修复最新iOS版少数派客户端的闪退bug》,@了一些iOS界的大神,没想到受到了大家的转发和关注,还真有点小激动。同时也在微博的评论下收到了稀土掘金的欢迎,还给我开了专栏权限,希望可以到稀土掘金上分享写作。备感荣幸的同时,突然发现,一直在稀土掘金上面看文章的我,一直没有注册账户(好像注册了但是忘了密码,在1Password中也没有记录,看来是很久之前登陆过了)。这也是这次故事的开始,收到稀土掘金的评论后,马上打开了早已不知道什么时候下载的 掘金 app。果然,没有登陆记录,作为开发者马上用 GitHub 授权进行登陆,尴尬的事情发生了,在授权成功后,app闪退了。好尴尬啊,好吧稀土掘金上的处女座就献给你了。
跟之前写《逆向及修复最新iOS版少数派客户端的闪退bug》文章时不同,在文章写到这里的时候,我其实还没有完全分析完崩溃的原因。我是分析途中决定开始写这篇文章的,决定一边分析一遍写,这样才会尽可能的保留分析的全过程,当然最后能不能分析到关键点,且看下去吧,因为我也不知道。
###找到崩溃原因
分析一个bug的开始,当然是从崩溃原因找起,前一篇文章,我们使用了lldb调试,在触发 app 崩溃的时候,从 Terminal 找到了关键词:EXE_BAD_ACCESS
。这次,我们使用最简单的,查看系统日志来定位崩溃原因,使用最新的 Mac,打开控制台,连接手机并清空多余的系统日志打印,然后触发崩溃,观察控制台中输出的内容(如果输出日志过多,可以使用进程名Xitu
作为关键词,进行过滤)。如果可以还是不要使用关键词过滤,因为不是所有的崩溃都是由于 app 本身的问题导致的,正如签名破坏的 app 在没有越狱的手机上会安装失败,这个安装失败的原因查找就需要在日志中的 SpringBoard
进程打印中进行查找
(SpringBoard
是Apple
用来管理我们iPhone应用的一个应用,可以理解为我们 PC 端的桌面,它还负责应用的桌面排列、管理通知中心等)。
正如我们希望的一样,崩溃的原因最常见不过了:

这个崩溃就很有意思了,开发者对NSNull
调用了length
方法,因为找不到该方法而崩溃!当然开发者肯定是不会主动对NSNull
类型调用length
方法,猜想:
大胆猜测:结合这个崩溃是发生在登陆的时候,需要从网络获取登陆信息,那么会不会是程序在对获取到的登陆信息进行处理的时候导致的崩溃(比如登陆用户的一些信息没有设置,所以程序从登陆信息里取到的是空值)。

分析从该登陆界面出发,使用cycript注入app后,打印找到该界面的控制器:
currentVCWithKeyWindow() 来自我自己写的一个脚本,你可以前往common.cy下载。下载之后,将其放到/var/root
目录下即可。
使用时,只需要在cycript注入的时候加上即可。
其中还有一些好用的函数,比如快速打印UIControl
所有的target
和action
使用class-dump
等工具,导出app
的所有类头文件,使用logify.pl
工具生成XTGithubLoginViewController
类的所有hook
函数,使用theos
编写安装插件后,触发崩溃,然后在控制台查找该类的函数调用逻辑。如下:


可以发现,程序是在-[XTGithubLoginViewController viewWillDisappear:]
调用之后才收到的崩溃的通知。所以很有可能崩溃的原因不是XTGithubLoginViewController
导致的,而是在XTGithubLoginViewController
类返回登陆信息后。
仔细研究XTGithubLoginViewController
的调用过程,发现了几个有意思的方法,调用顺序如下:
callback
!太熟悉不过了,这不就是我们经常在开发中使用的设置回调block
的时候使用的参数名吗!!并且,该callback
在类初始化的时候被设置,类方法-(id)onAuthCompleted:
调用之后才被回调。结合这个方法名onAuthCompleted
,这个逻辑是不是很像:我们在登陆成功之后,使用block传回了我们的登陆信息进行处理。
分析到这里,其实我们已经可以使用lldb调试,打印该block
的内存地址,然后去掉内存地址偏移后,在Hopper
中查看block的实现,看是否该block导致的崩溃。但是作为逆向的学习,我们可以先继续深入,查看一下这个block的传递调用过程。
既然该回调是在XTGithubLoginViewController
被初始化的时候被设置的,那么我们先找到是谁初始化的该Github
登陆控制器:

使用cycript
注入打印该控制器类名,并尝试github
的授权登录入口:
发现成功触发登录,所以可以确定githubLogin:
方法确实是登录的入口
在Hopper
中查看该方法的实现:
内容很简单,调用了单例类,并传入回调callback参数:
在Hopper
查看如下:
解释如下:
其实这里的githubVC
应该就是XTGithubLoginViewController
类型,我们可以使用cycript
确认一下:
现在callback
的传递应该很清楚了:点击 github 登录按钮时,调用单例LoginCloud
,并传入callback
,LoginCloud
根据传入的callback
初始化登录控制器XTGithubLoginViewController
,并设置另外的一个callback
回调,XTGithubLoginViewController
在登陆成功后,通过该callback
进行回调。
block有点特殊,它也是一个对象类型。在这里我们涉及到了两个block的传递,为了方便之后的分析,我们可以先将两个block的实现地址和传递的参数类型都打印出来。接下来的操作中可能会因为程序多次重启导致文中上下显示的内存地址不一致的情况,我每次都会进行说明。
因为我们知道这个callback
作为参数传给了-[LoginCloud githubLoginCallback:]
方法,所以lldb连接服务(如何lldb可以看我的第一篇文章)后,我们在这个方法上打个断点,同理设置-[XTGithubLoginViewController setCallBack:]
断点。
根据Block
的内存结构可以查找block
的函数实现地址和参数返回值类型,具体可以看这篇文章。开源就是好,节省步骤,我们可以使用facebook
开源的chisel。触发断点后,获取到block
的内存地址后,使用pblock
打印block:


获取到第一个 block 的函数实现地址:0x0000000100194cc4
参数及返回值:void ^(bool, NSString *)

获取到第二个 block 的函数实现地址:0x000000010015ccbc
参数及返回值:void ^(NSDictionary *, ThirdLoginModel *, NSDictionary *)
。
知道了 block 的参数的类型,那么我们可以写个 tweaks
来 hook
这个 block 然后打印一下,传递的这些参数内容,根据回传顺序,我们先打印第二个block
:
打印结果如下:

果然发现第二个字典中,出现了多个值为null
的value
,很有可能就是我们要找的点。那么这个字典是从哪里来的呢?想起当时打印XTGithubLoginViewController
类的调用顺序的时候,发现的一个函数onAuthCompleted:
,它返回的就是一个这样的字典,那么我们hook
一下方法,过滤掉这些值为null
的value
看看:
打包,安装后发现,并没有修复这个bug,还是报找不到length
方法的日志。看来我们还没有找到 bug 点。
在第二个回调上下断点,触发后,进入实现内部,根据lldb打印出实现内部的每个方法的方法调用者和函数名、参数值。过程如下(主要是找到了这个block
的实现地址后,在Hopper中找到这个代码块,可以发现对于这个 block 的内部方法,Hopper是没有注释调用者、selector、参数的,我们可以对Hopper中的这些代码中的所有显示为objc_msgSend
的地方下一个断点,一一触发,分别打印每个方法的调用者和调用方法,传递参数等信息),主要过程如下:

解释后主要是以下过程:
可以看到,其中也有一个block
,打印一下block的内容,根据Xitu
image的偏移得到block实现的地址:0x100208df8-0x00000000000ac000=0x10015CDF8
,在Hopper中找到:

在这个block上下断点,c
运行后来到该断点出:si
进入该实现内部,断点定位到如图中的唯一一个objc_msgSend
方法上,获取其调用信息:


可以看到该方法-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:]
中还有一个block
参数,继续打印:

可以发现得到的block
:
0x0000000100194cc4
,void ^(bool, NSString *)
。
和我们得到的第一个block
是同一个。
既然第二个block
参数还没有分析完,第一个block
已经迫不及待的回来了,那么我们先在Hopper中查看一下这个block的实现:

分析到这里ni
后app不小心退出了,那么我们在这里重新启动,既然已经知道这个block
的地址,可以直接通过查看Hopper
内的每个objc_msgSend
的地址下断点:


可以知道该block的调用如下:
既然分析到了这里,程序还没有崩溃,那么说程序这之前都没有问题,那么问题可能出在了XTLoginViewController
这个callback
中,那么我们打印一下这个block
:

得到:0x00000001001935d0
,void ^(bool, NSString *)
。
那么我们继续深入,打印一下这个block
的实现,步骤如上,如果不想每个objc_msgSend
都手动打断点,可以在这个block
上打断点,然后si
,进入后,一步一步向下执行,等到运行到objc_msgSend
的时候,打印这个方法的内容。但是在这里下到的断点都没有执行程序就已经崩溃了!!!说明什么?说明这个传入的callback
还没有执行,程序已经crash了。
分析到了这个方法-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:]
,那么我们去Hopper
中看看它的内部实现:

果然,如图看到的,我们看到了之前在控制台输出的崩溃的关键词length
,既然找到了这里,我们利用这个调用函数的地址:0x10015d228
来下一个断点,看看调用者是谁,是不是真的是null,从而导致的bug。
重新启动,lldb下断点:

如图,调用者为null,并且ni
后,完美的崩溃了。文章写了这么多,终于找到这个bug点了!!!激动啊。
找到了bug点,那么我们应该研究一下如何修复这个bug,首先我们需要知道为什么开发者会用这个null,调用了length,程序发生了什么导致了这个调用者为null
。
我们来仔细看一下,这个函数调用所在的代码块的汇编代码:
可以看到,ldr x0, [x8, #0x28]
。这个null是从[x8, #0x28]
加载的。但是我们在这个代码块中(包括整个-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:]
方法内)都没有找到。这个情况就比较特殊了,自从学习逆向以来,还是第一次碰到这种情况。该调用者是从一个从来没有被写入过内容的寄存器中读取的。应该也是因为这个原因才导致读取的内容为null吧。
换个思路,我们结合上下文找线索。我们翻译一下上下文方法调用的汇编代码:
我们可以使用cycript
来打印一下这个user
是什么:

然后顺便把汇编中的内容,打印一下:

果然,其中有一个打印是null
。而且正好出现在length
调用的上方:

所以很有可能,这个bug是因为通过key
:self.description
从user
中获取了一个没有的值,并对他调用了length
导致的。
那么我们通过theos
创建一个插件,然后hook
一下这个方法,返回一个我们设置了值的AVUser
对象,
安装后,再次登陆发现登陆成功,并没有crash
,终于修复了这个bug,发现已经写了不少字了。
最后的修复bug的tweaks
可以在这里下载XituHook。
其实在我自己分析的时候,分析的内容还要多,也比较杂。逆向分析正是这样,我们需要顺着程序执行的顺序分析,但是事情往往不如我们想的这样简单,分析的岔路很多,特别是这里,涉及到了多个block
的回调,需要对block
的实现原理及其内存结果比较熟悉才行。
在分析的过程中,分析到的还有一些文中没有写到的block
和方法,并且在其中发现了length
关键词,激动不已啊,但是当我深入分析的时候发现,开发者都做了防护
预防出现NSNull的情况,所以都不是 crash
的罪魁祸首。我猜测self_description
是获取的 github 上的某个个人信息(根据名字推测是自我介绍?)。如果想分析的话也可以,需要从AVUser
这个类如果获得这些信息开始分析。但是我想本文分享到这里已经差不多了。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2019-2-2 13:55
被kanxue编辑
,原因: