1、强大的lldb
上文我们说到了调试。在iOS逆向中,很多人推荐debugserver + lldb 其实调试只需要lldb就够了。
debugserver配置的文章有很多,从14年到18年不等,但大部分都过时了。所以我也没用。那 只用lldb该怎么断点调试三方app呢?我们先来简单看下lldb的断点命令。
1.1 LLDB断点命令:
1.1.1 设置断点 下断点命令我们只需要先会两个就够了:
a: 给指定内存地址下断点: br set -a 0x00000000
全拼我记不住
b: 给某方法下断点:b -n "[ClassName methodName]"
全拼同样记不住
关于a,怎么定位方法内存,一会再讲。
关于b,很多app打App Store包的时候,是去掉了符号表的编译配置项的。所以,我们暂时打不了三方app的方法断点。不过不用急,后面我们会教大家如何还原三方app的符号表。
1.1.2 查看断点
br list
这个全拼我能记住:breakpoint list
1.1.3 删除断点
br del n
这里的n是上一步br list
列出的断点的序号。根据对应的序号删除想删的断点。当然你也可以直接br del
,然后lldb会问你是否要删除全部断点。[Y/n] ?
输入Y即可。
1.2 如何定位函数地址
好 我们先来看看如何找到想断点的函数的地址。为了降低被黑客攻击的风险,操作系统大都采用ASLR(地址空间布局随机化) 技术: 详细解释请看这 这个人的内存管理讲的不错,一共7篇,建议大家有时间看看。
ASLR是一种避免类似攻击的有效保护。进程每一次启动时,地址空间都会被简单地随机化:进行整体的地址偏移,而不是搅乱。通过内核将整个进程的内存空间“平移”某个随机数,进程的基本内存布局如程序文本、数据、库等相对位置仍然是一样的,但其具体的地址都不同了。
简单来说,ASLR会在进程启动时候随机一个基地址。
在lldb中的查看命令是image list
或image list -o -f
。
如过命令后面不加参数,它打印的就是每个image(镜像)的虚拟内存地址。
如果加了-o参数,打印的就是image的偏移量(相对于谁,暂时还没研究)。
具体区别如下:
image list
打印的基地址: 0x102a94000
image list -o -f
的地址: 0x002a94000
看出区别了么?第9位差了一个1。
这个1正好跟IDA工具中的0x 1 0000 0000对上了。
原因终于找到了,在:杨潇玉大哥的这篇文章。main()调用之前会调用exec()
,exec()
是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用 ASLR)。并将起始位置到 0x000000
这段范围的进程权限都标记为不可读写不可执行。如果是 32 位进程,这个范围至少是 4KB;对于 64 位进程则至少是4GB。NULL 指针引用和指针截断误差都是会被它捕获。4GB = 232,用二进制表示就是:第32+1位为1,剩下的32位为0。而4位二进制位可以表示一个十六进制位,所以十六进制表示就是:0x 1 0000 0000。这样就满足了64位进程至少4GB的最小偏移范围了。
这里我们提到的地址是虚拟地址,为什么不是物理地址?
原因是:操作系统将我们(程序猿)跟物理内存划分了一个安全界限,我们程序猿只需要用虚拟内存跟操作系统打交道即可,操作系统调用底层硬件api,跟物理内存打交道。如果程序猿直接跟物理内存交流,恐怕不是那么安全的。程序一旦出了bug,可能会导致系统瘫痪。
基地址有了,在前面ASLR技术中我们提到内核将整个进程的内存“平移”,所以数据段、代码段等等内存的偏移量其实是固定的,他们相对于mach-o header部分的偏移量是个常亮,那既然如此,我们的IDA工具又派上用场了:
这里列出的偏移地址,就是函数相对于mach-o的起始地址的偏移量,所以:
虚拟地址 = 基地址 + 偏移地址。
那么,我们就可以在lldb用
br set -a 0x102a94000
下断点了。在这里在交给大家两个好用的命令:
读内存命令:
x 0x102a94000
全拼是:memory read 0x102a94000
反汇编命令:
dis -s 0x102a94000
dis是dissamble的简写。
我们调试别人的app,是看不到源码的,只能看汇编。所以这里建议大家学一下汇编。我知道很多人听到汇编就头大,不过不要紧,找对了教材,汇编真的可以通俗易懂。比如这本:《汇编语言第3版》王爽著。学习时长两三天就够。不信你试试。
我是从网上下的影印版PDF,阅读效果不太好,大家可以买正版书或者正版PDF来学习。
学完了汇编,CPU工作原理你就基本了解了,再跟内存交流起来就方便多了。不过你可能还是看不懂xcode中出现的上古语言。因为两者的汇编指令集不一样,书里的CPU是古老的8086,是地址总线20位,数据总线16位的16位机器。而iPhone 5s之后都是arm64 CPU,命令不太一样,总线位数也不一样,不过如果你理解了CPU的工作原理,arm64其实只是换了一种语法而已。而且CPU寻址操作也变得更简单。毕竟arm64数据总线跟地址总线位数相同(皆为64位),CPU不再需要地址加法器计算地址了。
这些都掌握了之后,我们可以看一下runtime源码,重点看一下objc/message部分。
目前苹果官方最新的是objc4-762版本,为了方便调试,我从github上下载了objc4-750版本,就差一个版本,不影响我们理解原理。
runtime 非官方Git地址
可以结合这篇文章进行理解。作者梳理的很好,从objc_msgSend的汇编代码入口开始,梳理到最终的runtime消息转发机制。
有了这些基础,下面我们再进行断点调试的时候,就会方便很多。
比如说,我们给没有隐藏符号表的app下断点:
未隐藏符号表的程序,断点的时候,函数名也会显示出来。
接下来我们将用到一个进阶命令:register read
查看寄存器信息:
前面如果你研究了runtime的objc_msgSend函数,你就会知道,该函数有两个默认参数
(receiver, cmd)
,第一个参数是消息接收者,第二个是函数地址。在iOS arm64 CPU中,通用寄存器中的x0~x7
寄存器 用于参数传递。所已x0
寄存器的值就是我们该函数的第一个参数:消息接收者,也就是DJHomeViewController类型的一个对象。结合po命令,我们就可以查看很多东西了。第二个参数是函数地址,对应
x1
寄存器。objc_msgSend中第三个参数(
x2
寄存器),就是viewDidAppear:后面的传参了,我们看到这个值是0. 回头看看class-dump
,可以看到这个参数要求传一个布尔值,那么 我们就可以猜出该函数入参是false。有了对象和参数地址,你就拥有了一切。
比如,我们通过class-dump 看到该类有这么一个属性,那我们就可以直接访问!
我们可以用
p
命令 输出一下对象,该功能有点类似于expression
命令。此时会生成一个
$
符号开头的变量,这个变量可以在后续lldb中使用。那么接下来我们就可以利用kvc进行访问任何内容了。也可以直接像写OC代码一样,在lldb中写方法调用。例如:
po [$10 class]
;lldb常用命令可以看这里、或这里。网上有很多这类文章,大家可以自行查找。
到这里,我们可以看到,假如调试的时候能看到函数名,那我们逆向就没有任何阻碍。所以,符号表是我们的必争之地!
如何还原符号表,请看下集:iOS逆向资料(四):还原符号表,再无障碍。