当静态分析无法获取足够的信息时,就需要进行动态分析,在 app 运行时,追踪方法调用、查看内存信息。最后找到想要分析的关键函数。
这篇文章包括:
- 环境搭建
- 反调试
- 动态调试的思路
- lldb 调试命令与脚本
- cycript 配置与使用
- frida 配置与使用
- IDA 动态调试
环境搭建
安装 openSSH
参照静态分析中的安装 openSSH
小结。
用 USB 进行 SSH 连接
openSSH 默认是用 wifi 连接到 iOS 设备的,但是这样速度慢,不稳定。因此可以安装usbmuxd
,用 USB 连接:
|
|
安装后就可以用iproxy
工具,将设备上的端口号映射到电脑上的某一个端口:
|
|
用 USB 连接设备到 mac 上,之前 openSSH 连接 iOS 的命令是ssh root@10.5.53.182
,现在改成ssh root@localhost -p 2222
。
修改 debugserver
使用 lldb 调试需要准备 debugserver。使用 OSX 中的 lldb 远程连接 iOS 上的 debugserver,由 debugserver 作为 lldb 和 iOS 的中转,执行命令和返回结果。在默认情况下,iOS 上并没有安装 debugserver,只有在设备连接过一次 Xcode,安装了开发者插件后,debugserver 才会被 Xcode 安装到iOS的/Developer/usr/bin/
目录下。
在 iOS 11 越狱之前,需要对 debugserver 进行重签名,在 iOS 11 上可以直接使用/Developer/usr/bin/debugserver
,或者直接用 Xcode 对 iOS 上的 app 进行调试。iOS 11 之前用 Xcode 调试需要对 app 进行重签名,而 iOS 11 之后不需要重签名 app 也能调试了。
iOS 11 之前重签名 debugserver 步骤:
1.拷贝 debugserver 到本地计算机中:scp root@iOSDeviceIP:/Developer/usr/bin/debugserver ~/debugserver
。
2.然后用 ldid 添加权限。由于 ldid 不支持 fat 二进制文件,所以要给 debugserver 瘦身,通过 lipo 指定要支持的指令类型,例如:lipo -thin arm64 ~/debugserver -output ~/debugserver
。
3.给 debugserver 添加 task_for_pid 权限,保存以下内容为 ent.xml 文件:
|
|
然后执行以下命令添加权限:ldid -Sent.xml debugserver
4.给 debugserver 重新签名,保存以下内容为 entitlements.plist 文件:
|
|
然后运行以下命令给的 debugserver 签名:codesign -s - --entitlements entitlements.plist -f debugserver
5.重新拷贝 debugserver 回手机中:scp ~/debugserver root@iOSDeviceIP:/usr/bin/debugserver
6.第一次使用 debugserver 时需要为其添加可执行权限:chmod +x /usr/bin/debugserver
连接到指定进程进行调试
准备好 debugserver 后,就可以调试任意第三方 app 了。
SSH 到 iOS,使用 debugserver 来 attach 一个进程,要查看当前正在运行的进程,使用
ps -e
命令。比如我们要 attach 的进程号为 693,我们可以输入如下命令:debugserver *:1234 -a 693
iOS 11 上
debugserver *:1234
中的*:1234
要替换成localhost:1234
。如果用的是 Electra 越狱,命令变成/Developer/usr/bin/debugserver localhost:1234 -a 693
,如果用的是unc0ver越狱,则是debugserver localhost:1234 -a 693
。同理,下文中的对应命令也要相应的替换如果要用 debugserver 启动 app,而不是附加到已经启动的 app,则使用
debugserver *:1234 <app二进制文件路径>
,例如debugserver *:1234 /var/containers/Bundle/Application/107F3307-2900-4720-B9BA-0C7792D89DF2/APP_TO_DEBUG.app/APP_TO_DEBUG
Mac 端打开终端,输入 lldb,回车,进入 lldb 界面,使用
process connect
命令连接客户端。
用 WiFi 连接到 iOS 设备时:process connect connect://iOSDeviceIP:1234
。如果要用 usbmux 连接,则先使用
iproxy 1234 1234
进行一次端口转发,再使用process connect connect://localhost:1234
,即可用 USB 连接到 iOS 设备。
回车后需要等待几分钟,时间有点久。
连接成功后,即可用 lldb 命令进行调试。
反调试
有些 app 使用了反调试功能,禁止了动态调试。
系统提供了禁止调试依附的接口,可以通过ptrace
syscall
svc指令
调用,禁止调试。也可以通过sysctl检查 ptrace
isatty 或者 ioctl 检查终端
task_get_exception_ports获取异常端口
等方式检查是否正在被调试,之后再让 app 崩溃。
可以参考关于反调试&反反调试那些事、反调试与绕过的奇淫技巧。
如果你发现 app 一调试就闪退,多半就是有反调试机制。
为验证是否调用了 ptrace 可以用 debugserver -x backboard *:1234 binaryPath
启动 app,然后下符号断点 b ptrace
,c
之后看 ptrace 第一行代码的位置,然后 p $lr
找到函数返回地址,再根据 image list -o -f
的ASLR偏移,计算出原始地址。最后在 IDA 中找到调用ptrace的代码,分析如何调用的ptrace。其他的反调试类似,参考上面的文章。
常用的动态调试方法
断点
使用lldb的br s -a [地址]
命令,在指定地址处下断点。但是动态调试时,无法准确地找到需要断点的地址。可以先静态分析 app 的二进制文件,找到需要研究的方法,再在方法处下断点。
根据二进制文件中方法的地址,找到需要断点的地址
app 加载到内存里时,有一个偏移:
运行时的地址 = 二进制文件中的相对地址 + 偏移量
使用image list
列出所有加载的模块,查看偏移量,找到第一行:
(lldb) image list
[0] 7A6179DA-8D91-315A-8BD2-546A54648D37 0x00000001000bc000 /Applications/APP_TO_DEBUG.app/APP_TO_DEBUG
其中的0x00000001000bc000
就是加载的基址,偏移量就是0x1000bc000
。
例如,需要分析-[CLoginController keyboardWillShow]
方法,方法在二进制文件中的地址为0x0000000100723bcc
:
而这个二进制文件的基址为0x100000000
:
所以此函数在文件中的偏移量是0x0000000100723bcc
- 0x100000000
= 0x723bcc
。因此当前内存中的运行时地址是0x1000bc000
+ 0x723bcc
= 0x1007dfbcc
。
反汇编指定地址
找到地址后,可以使用di --start-address <address> -count 10
命令来反汇编找到的地址,如果反汇编结果和静态分析中的汇编代码一致,说明找到的是正确的:
|
|
和上面 hooper 中的汇编代码比较,可以看到是一致的。
在32位设备上,可能会出现反汇编出来的是 arm 指令集,出现很多unknown opcode
的指令,和 hopper 中显示的不一致。可以加上-A thumbv7
显示 thumb 指令集的反汇编结果:di --start-address 0x1007dfbcc -c 10 -A thumbv7
。
再用br set -a 0x1007dfbcc
打断点:
|
|
查看寄存器的值
当触发了断点后,可以用register read
查看当前寄存器的值:
|
|
如果Mac上安装了chisel,还可以用pinternals
遍历出对象的实例变量。或者调用私有方法_ivarDescription
打印实例变量:po [0x0000000123e492f0 _ivarDescription]
。
查看调用堆栈
用thread backtrace
查看调用堆栈,缩写为bt
。thread backtrace -e true
可以显示线程嵌套的堆栈。
|
|
恢复 OC 符号
第三方 app 往往都去除了符号,建议进行一下恢复符号表的操作。恢复符号表后,在调试时就能直接在堆栈中看到方法名,免去了计算偏移量然后在 hopper 里查找的麻烦。参考:iOS符号表恢复&逆向支付宝, restore-symbol。
用 Xcode 直接调试
除了用命令行,也可以直接用 Xcode 进行 lldb 调试,有了图形界面,也能使用Debug UI Hierarchy
和Debug Memory Graph
工具。参考iOS逆向:用Xcode直接调试第三方app。
如果是 iOS 11 之前的越狱设备,需要重签名后才能用 Xcode 调试。iOS 11 之后没有限制,可以直接用 Xcode 调试 App Store 上下载的 app。
lldb常用命令
要想充分发挥 lldb 的动态调试功能,必须要学会使用 lldb 命令。
lldb命令可以在官网查看:GDB to LLDB Command Map。也可以参考:与调试器共舞 - LLDB 的华尔兹。
常用命令如下:
求值、打印
expression
,expr
,e
:后面可以执行一段代码print
,prin
,pri
,p
。是expression --
的缩写。可以用p/x
,p/t
,p/c
,p/s
分析打印16进制、二进制、字符、字符串格式po
是e -o --
的缩写。表示以 对象 (Object) 的格式来打印结果- 求值之后会保存为临时变量,使用变量时以
$
开头:e int $a = 2
p $a * 19
流程控制
process continue
,continue
,c
thread step-over
,next
,n
thread step in
,step
,s
thread step-out
,finish
thread return <RETURN EXPRESSION>
:返回指定值
断点
breakpoint list
,br li
:列出所有断点breakpoint enable
,breakpoint disable
,br dis
,br del
:后面跟断点的序号,打开、关闭某个断点breakpoint set -f main.m -l 16
:在源码文件的某一行断点b main.m:17
。b
是_regexp-break
的缩写- 符号断点:
b isEven
,br s -F isEven
- 用正则表达式进行符号断点:
br set -r '正则'
- 断点条件:
breakpoint modify -c 'i == 99' 1
- 断点时附加自定义操作:
breakpoint command add 1
监控地址
- 内存监控:
|
|
watchpoint set expression -- (int *)$myView + 8
:监控_layer
的地址
- 变量监控:
watchpoint set variable -w read_write
- 条件监控:
watchpoint modify -c '(global==5)'
内存,栈信息
- 打印参数:
frame variable
,fr v
- 打印方法名和行数:
frame info
- 打印寄存器的值:
register read
- 修改寄存器的值:
register write rax 123
- 打印栈回溯:
thread backtrace
,bt
,bt all
- 打印线程嵌套的栈回溯:
thread backtrace -e true
- 读取内存:
memory read --size 4 --format x --count 4 0xbffff3c0
,me r -s4 -fx -c4 0xbffff3c0
- 获取内存创建栈:
script import lldb.macosx.heap
malloc_info --stack-history 0x10010d680
。可以快速追溯对象的创建来源,参考iOS逆向:在任意app上开启malloc stack追踪内存来源
反汇编
disassemble --start-address 0x1eb8 --end-address 0x1ec3
disassemble --start-address 0x1eb8 --count 20
disassemble --frame --mixed
,di -f -m
image list
image lookup --address 0x1ec4
lldb 命令扩展
lldb 可以使用 python 脚本编写自定义功能。可以安装 facebook 的开源库chisel,提供了很多非常有用的命令。
安装步骤如下。
Mac 中用brew install chisel
下载 chisel,默认安装到/usr/local/opt/chisel
。
也可以手动从 github 上下载。
下载后打开 Mac 上的~/.lldbinit
,如果不存在则手动创建一个。在里面添加chisel
:
|
|
之后重启 Xcode,就能使用下面这些非常有用的命令了。
命令 | 描述 |
---|---|
目录 | |
pdocspath | 打印 app 的沙盒 Documents 目录 |
pbundlepath | 打印 app 的 bundle 目录 |
对象查找 | |
fv | 用正则查找所有类的 view 实例 |
fvc | 用正则查找所有类的 view controller 实例 |
findinstances | 在内存中查找某个类的所有实例 |
flicker | 闪烁某个 view,用于快速定位 |
对象分析 | |
pinternals | 打印对象内部的所有实例变量 |
pkp | 用 -valueForKeyPath: 获取对象的数据 |
pmethods | 打印类的所有方法 |
poobjc | 用 ObjC++ 语言执行和获取表达式的结果,expression -O -l ObjC++ — 的缩写 |
pproperties | 打印对象或者类的属性 |
pivar | 打印对象的某个 ivar |
wivar | 给对象的某个实例变量地址设置 watchpoint,监控变化 |
pclass | 打印某个对象的类继承链 |
pbcopy | 打印对象并且把结果复制到粘贴板 |
pblock | 打印 block 的实现函数地址和签名 |
pactions | 打印 UIControl 的 target 和 action |
断点 | |
bdisable | 用正则查找并关闭一组断点 |
benable | 用正则查找并开启一组断点 |
binside | 用相对地址设置断点,自动加上 ALSR 偏移 |
bmessage | 给某个类的 method 设置断点,同时会在其父类上查找 method |
pinvocation | 打印方法调用堆栈,仅支持x86 |
视图查找 | |
visualize | 显示 UIImage, CGImageRef, UIView 或 CALayer 的图片内容,用 Mac 的预览打开,在调试绘图时非常有用 |
taplog | 打印触摸到的 view,用于快速定位 |
border | 给 view 加上边框,用于定位某个 view 对象 |
unborder | 移除 view 或 layer 的边框 |
caflush | 修改 UI 后刷新 Core Animation 界面 |
hide | 隐藏 view 或 layer |
show | 显示一个 view 或者 layer,相当于执行view.hidden = NO |
mask | 给 view 添加半透明的 mask,可以用来查找被隐藏的 view |
unmask | 移除 view layer 的 mask |
setinput | 给作为 first responder 的 text field 或 text view 输入文本 |
slowanim | 减慢动画速度 |
unslowanim | 动画速度回复正常 |
present | Present 一个 view controller |
dismiss | 消除 present 出来的 view controller |
视图层级 | |
pvc | 循环打印 view controller 的层级 |
pviews | 循环打印 view 的层级 |
pca | 打印 layer 树 |
vs | 在 view 层级中搜索 view |
ptv | 打印最顶层的 table view |
pcells | 打印最顶层 table view 的所有可见的 cell |
presponder | 打印 UIResponder 响应者链 |
其他工具 | |
sequence | 执行多条命令,用; 分隔 |
pjson | 打印 NSDictionary 或 NSArray 的 JSON 格式 |
pcurl | 用 curl 的格式显示 NSURLRequest (HTTP) |
pdata | 用字符串的形式显示 NSData |
mwarning | 模拟内存警告 |
视图调试 | |
alamborder | 给有约束错误的 view 加上边框 |
alamunborder | 有约束错误的 view 加上边框 |
paltrace | 打印 view 的约束信息,相当于调用_autolayoutTrace |
panim | 是否正在执行动画,相当于调用[UIView _isInAnimationBlock] |
几个有用的私有方法
NSObject
有一些很有用的私有方法,可以方便查看对象的内容:
_methodDescription
:打印对象或者类的整个继承链上的方法列表,同时显示方法的地址,可以直接用于断点_shortMethodDescription
:打印对象或者类的方法列表,不显示父类_ivarDescription
:打印对象或者类的所有实例变量和值
自定义 lldb 脚本
你可以 用 Python 脚本编写自己的 lldb 命令,可以进一步提升动态调试的效率。
命令别名
可以在~/.lldbinit
中添加 lldb 的初始化命令,如果没有这个文件就创建一个。
用command alias
添加快捷命令,例如:
|
|
之后输入reloadscript
就相当于输入command source ~/.lldbinit
。
编写自定义脚本
编写 Python 脚本,格式如下:
|
|
如果有chisel
,可以直接使用chisel
里封装好的模块和各种函数:
|
|
详情请见chisel
代码。
写脚本时可以随时在 lldb 里调用reloadscript
命令重新加载,进行测试。
脚本提供了操作 lldb 的接口,例如设置断点、执行命令。不过编写命令有些坑:
- 大部分 OC 方法和函数都需要明确声明返回值类型
- 指针声明时需要初始化,不会默认设为 nil,否则在使用时会出现野指针
导入自定义脚本
打开~/.lldbinit
添加:
|
|
实战演练:追踪block回调
下面演练一下 lldb 调试的过程。
有时候逻辑是通过block回调来执行的,追踪调用路径时,需要找出block的执行地址。直接打印block对象并不会显示执行地址,需要分析内存才能找出。下面的分析流程和 lldb 命令pblock
是一样的。
block的结构
|
|
查看invoke指针的地址
演示代码如下:
|
|
当获取到一个block变量时:
|
|
查看0x16fd465b8
的内存,由于字节对齐的原因,结构体内的数据是按照最大的8字节对齐的:
|
|
void *isa 占用8字节,int占用4字节,所以invoke指针的值是0x00000001000bf77c
,descriptor
的地址是0x000000010012c9f0
。
对地址反汇编:
|
|
查看block的签名
如果要进一步查看block的签名,首先检查block的flags,确定内存布局:
|
|
flags BLOCK_HAS_COPY_DISPOSE
为YES,说明descriptor
里有dispose_helper
和dispose_helper
,signature
在第8 + 8 + 8 + 8 = 32个字节。
查看descriptor
的内存,第32个字节的内容:
|
|
查看签名:
|
|
Cycript
Cycript 是 Saruik 大佬开发的动态调试工具,内置了一套 JavaScript 解释器,可以用 js 脚本和 OC 交互,用 js 执行 OC 代码,内置了一些很有用的功能。官网:http://www.cycript.org,源码地址:https://git.saurik.com/cycript.git。
其实 cycript 的大部分功能通过 lldb 都能实现。它的优势是集成了越狱系统中的 substrate 库,可以快速地进行 hook,并且 js 语法写起来比较简单。
安装
越狱机去 Cydia 中可以直接搜索下载 cycript。现在 cycript 的兼容性有点问题,没有适配 iOS 11 越狱,因此 iOS 11 在 Cydia 里找不到 cycript。需要自己去使用这个bfinject,如果安装失败,则尝试这个分支:klmitchell2/bfinject。
除了从第三方安装,你也可以去官网下载 cycript 的 SDK 集成到 app 中使用。
使用
安装后,ssh 连接到 iOS 设备,使用ps -e
找到想要调试的进程,使用cycript -p <pid>
连接指定的进程号后,就进入了cycript
的调试控制台。
在控制台里,可以把 js 和 OC 语法混用。
方法调用和求值
调用 OC 方法:
|
|
|
|
通过地址获取对象:
|
|
获取实例变量的值:
|
|
打印对象的实例变量:
|
|
调用 C 函数
|
|
|
|
添加 category
|
|
查找指定类的对象
Cycript 的choose
命令可以列出指定类的所有实例对象,和 lldb 命令findinstances
类似:
|
|
|
|
hook
通过 js 原型操作对象:
|
|
修改 prototype 进行 hook:
|
|
Cycript 中也可以使用越狱机上的 hook 框架Cydia Substrate
。使用MS.hookMessage
hook OC 方法:
|
|
使用MS.hookFunction
hook C 函数:
|
|
Frida
Frida 是一个跨平台的动态调试工具,可以用 js 脚本和 OC 进行交互,从而执行代码、打log、hook 函数。和 cycript 类似,不过兼容性比 cycript 要好。同样的,frida 能做的,用 lldb 基本上也能做到。Frida 的优势是跨平台,以及提供的 js 库、命令行,能够实现脚本化。Frida 官网。
安装
Mac 端安装 frida:
|
|
iOS 设备在 Cydia 中添加源: https://build.frida.re,之后在 Cydia 中搜索 frida 安装。
使用
列出进程
在 mac 上执行frida-ps -U
等待 iOS 设备连接到 USB,连接到后就会列出 iOS 设备上正在运行的进程。
也可以用frida-ps -Uai
列出正在运行的 app。
调用追踪
可以使用frida-trace
追踪 app 的调用。
|
|
|
|
连接指定进程
使用frida -U <app名>
连接到指定进程,也可以同时注入 js 脚本:frida -n Twitter -l demo1.js
。
连接上后,就可以执行 frida 的 js 命令,以及运行注入的 js 脚本。
Frida 提供了强大的 js 库,可以去官网查看完整的 API 文档:JavaScript API。这里只列出一些有用的接口。
执行脚本
与 OC 交互
获取 OC 类列表:ObjC.classes
获取指定类:var NSString = ObjC.classes.NSString;
,并调用类方法:NSString.stringWithString_("Hello World");
调用实例方法:NSString.alloc().initWithString_("Hello World");
GCD 线程:
|
|
获取内存中指定类的所有实例:
|
|
|
|
获取 OC 对象:new ObjC.Object(ptr("0x1234"))
可以通过 js 对象的属性获取 OC 对象的内容:
$kind
: string specifying eitherinstance
,class
ormeta-class
$super
: an ObjC.Object instance used for chaining up to super-class method implementations$superClass
: super-class as an ObjC.Object instance$class
: class of this object as an ObjC.Object instance$className
: string containing the class name of this object$protocols
: object mapping protocol name toObjC.Protocol
instance for each of the protocols that this object conforms to$methods
: array containing native method names exposed by this object’s class and parent classes$ownMethods
: array containing native method names exposed by this object’s class, not including parent classes$ivars
: object mapping each instance variable name to its current value, allowing you to read and write each through access and assignment
调用 C 函数
获取 C 函数指针:
|
|
调用 C 函数:
|
|
hook
Hook OC 方法:
|
|
替换 C 函数(OC 方法同理):
|
|
Hook C 函数:
|
|
IDA 动态调试
IDA 也有一个动态调试工具,不过没有 lldb 这么多针对 iOS 平台的命令,只要是用来辅助逆向分析,查看控制流,打log。IDA 的 trace 功能可以从指令级别上记录运行时程序的流程,查看寄存器和内存的值,不过 IDA 调试的同时不能使用 lldb,如果想要查看其他详细信息,可以配合 cycript 或 frida。
如果你想分析代码的控制流,可以使用 IDA 的动态调试。IDA 也有一些第三方插件用于辅助调试。
配置 debugger
IDA 提供了 iOS 的 debugger。首先将砸壳后的 app 用 IDA 分析完毕,再重签名后安装到 iOS 设备上,目的是让 IDA 分析的二进制文件和设备上的保持一致。
之后设置 IDA 的 debugger 配置:
- 在 IDA 的
Debugger
->Switch Debugger
中选择Remote iOS Debugger
- 配置 debugger:打开
Debugger
->Debugger options
,在弹出的面板中打开Set specific options
- 设置
Symbol path
为当前设备的符号文件路径,例如~/Library/Developer/Xcode/iOS DeviceSupport/11.2.2 (15C202)/Symbols/
- 勾选
Launch debugserver automatically
- 设置
- 配置 process:打开
Debugger
->Process options
,设置Application
和Input file
为 app 二进制文件在 iOS 设备上的路径,例如/var/containers/Bundle/Application/F366E63D-602B-47D9-B92E-1739A347192B/AppToDebug.app/AppToDebug
启动调试
配置完后,在 iOS 设备上 kill 掉 app,就可以用 IDA 的Debugger
->Start Process
启动进程进行调试。
启动前可以先设置断点,在断点上设置 trace,可以用不同颜色表示控制流的路径。
IDA 动态调试插件
IDA 有些开源插件用于增强动态调试功能。例如funcap可以记录运行时的寄存器信息作为注释,辅助分析。不过这个工具现在只支持 32 位。
其他的插件你可以自行搜索。不过能用到 iOS 上的动态调试插件并不多。
结尾
动态调试的整个流程以及用到的工具大部分都总结在此了。还有一个强力的工具这里没有讲解,就是 tweak 插件。由于内容有点多,留到之后的文章中再展开。