iOS逆向指南:动态分析

当静态分析无法获取足够的信息时,就需要进行动态分析,在 app 运行时,追踪方法调用、查看内存信息。最后找到想要分析的关键函数。

这篇文章包括:

  • 环境搭建
  • 反调试
  • 动态调试的思路
  • lldb 调试命令与脚本
  • cycript 配置与使用
  • frida 配置与使用
  • IDA 动态调试

环境搭建

安装 openSSH

参照静态分析中的安装 openSSH小结。

用 USB 进行 SSH 连接

openSSH 默认是用 wifi 连接到 iOS 设备的,但是这样速度慢,不稳定。因此可以安装usbmuxd,用 USB 连接:

1
brew install usbmuxd

安装后就可以用iproxy工具,将设备上的端口号映射到电脑上的某一个端口:

1
iproxy 2222 22

用 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 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
<key>run-unsigned-code</key>
<true/>
</dict>
</plist>

然后执行以下命令添加权限:ldid -Sent.xml debugserver

4.给 debugserver 重新签名,保存以下内容为 entitlements.plist 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
</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 了。

  1. SSH 到 iOS,使用 debugserver 来 attach 一个进程,要查看当前正在运行的进程,使用ps -e命令。比如我们要 attach 的进程号为 693,我们可以输入如下命令:debugserver *:1234 -a 693

  2. iOS 11 上debugserver *:1234中的*:1234要替换成localhost:1234。如果用的是 Electra 越狱,命令变成/Developer/usr/bin/debugserver localhost:1234 -a 693,如果用的是unc0ver越狱,则是debugserver localhost:1234 -a 693。同理,下文中的对应命令也要相应的替换

  3. 如果要用 debugserver 启动 app,而不是附加到已经启动的 app,则使用debugserver *:1234 <app二进制文件路径>,例如debugserver *:1234 /var/containers/Bundle/Application/107F3307-2900-4720-B9BA-0C7792D89DF2/APP_TO_DEBUG.app/APP_TO_DEBUG

  4. 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 ptracec 之后看 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
hopper6

而这个二进制文件的基址为0x100000000
hopper7

所以此函数在文件中的偏移量是0x0000000100723bcc - 0x100000000 = 0x723bcc。因此当前内存中的运行时地址是0x1000bc000 + 0x723bcc = 0x1007dfbcc

反汇编指定地址

找到地址后,可以使用di --start-address <address> -count 10命令来反汇编找到的地址,如果反汇编结果和静态分析中的汇编代码一致,说明找到的是正确的:

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) di --start-address 0x1007dfbcc -c 10
DuoYiIMOrig`-[CLoginController keyboardWillShow:]:
0x1007dfbcc <+0>: stp d11, d10, [sp, #-0x80]!
0x1007dfbd0 <+4>: stp d9, d8, [sp, #0x10]
0x1007dfbd4 <+8>: stp x28, x27, [sp, #0x20]
0x1007dfbd8 <+12>: stp x26, x25, [sp, #0x30]
0x1007dfbdc <+16>: stp x24, x23, [sp, #0x40]
0x1007dfbe0 <+20>: stp x22, x21, [sp, #0x50]
0x1007dfbe4 <+24>: stp x20, x19, [sp, #0x60]
0x1007dfbe8 <+28>: stp x29, x30, [sp, #0x70]
0x1007dfbec <+32>: add x29, sp, #0x70 ; =0x70
0x1007dfbf0 <+36>: sub sp, sp, #0x40 ; =0x40

和上面 hooper 中的汇编代码比较,可以看到是一致的。

在32位设备上,可能会出现反汇编出来的是 arm 指令集,出现很多unknown opcode的指令,和 hopper 中显示的不一致。可以加上-A thumbv7显示 thumb 指令集的反汇编结果:di --start-address 0x1007dfbcc -c 10 -A thumbv7

再用br set -a 0x1007dfbcc打断点:

1
2
(lldb) br set -a 0x1007dfbcc
Breakpoint 1: where = DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550, address = 0x00000001007dfbcc

查看寄存器的值

当触发了断点后,可以用register read查看当前寄存器的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Process 1252 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001007dfbcc DuoYiIMOrig`-[CLoginController keyboardWillShow:](self=0x0000000123e492f0, _cmd="keyboardWillShow:", aNotification=@"UIKeyboardWillShowNotification") at CLoginController.m:550 [opt]
(lldb) register read
General Purpose Registers:
x0 = 0x0000000123e492f0
x1 = 0x0000000191ad138c
x2 = 0x0000000125a81580
x3 = 0x00000001a2d26a50 CoreFoundation`__block_literal_global
x4 = 0x0000000000000002
x5 = 0x0000000000000001
x6 = 0x0000000000000000
x7 = 0x0000000000000000
x8 = 0x0000000125a81580
x9 = 0x0000000191ad138c
x10 = 0x000000012407f400
x11 = 0x0000008a000000ff
x12 = 0x000000012407fcc0
x13 = 0x000005a1011f1c65
x14 = 0x000000000022a802
x15 = 0x000000000000358f
x16 = 0x00000001011f1c60 (void *)0x000001a1011f1c89
x17 = 0x00000001007dfbcc DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550
x18 = 0x0000000000000000
x19 = 0x0000000125a81580
x20 = 0x0000000123da3cb0
x21 = 0x0000000000000000
x22 = 0x0000000000000000
x23 = 0x00000001a8cae000 CoreFoundation`_CFXNotificationPost.samples + 352
x24 = 0x00000001a8cae000 CoreFoundation`_CFXNotificationPost.samples + 352
x25 = 0x0000000000000000
x26 = 0x000000010b651440
x27 = 0x00000001a8ca9ef8 __kCFNull
x28 = 0x0000000000000001
fp = 0x000000016fd3ffe0
lr = 0x0000000183be2b10 CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20
sp = 0x000000016fd3ffe0
pc = 0x00000001007dfbcc DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550
cpsr = 0x60000000
(lldb) po 0x0000000123e492f0
<CLoginController: 0x123e492f0>

如果Mac上安装了chisel,还可以用pinternals遍历出对象的实例变量。或者调用私有方法_ivarDescription打印实例变量:po [0x0000000123e492f0 _ivarDescription]

查看调用堆栈

thread backtrace查看调用堆栈,缩写为btthread backtrace -e true可以显示线程嵌套的堆栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001007dfbcc DuoYiIMOrig`-[CLoginController keyboardWillShow:](self=0x0000000123e492f0, _cmd="keyboardWillShow:", aNotification=@"UIKeyboardWillShowNotification") at CLoginController.m:550 [opt]
frame #1: 0x0000000183be2214 CoreFoundation`_CFXRegistrationPost + 400
frame #2: 0x0000000183be1f90 CoreFoundation`___CFXNotificationPost_block_invoke + 60
frame #3: 0x0000000183c51b8c CoreFoundation`-[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1504
frame #4: 0x0000000183b23e64 CoreFoundation`_CFXNotificationPost + 376
frame #5: 0x0000000184658e0c Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 68
frame #6: 0x000000018a4cbb40 UIKit`-[UIInputWindowController postStartNotifications:withInfo:] + 400
frame #7: 0x000000018a4cdcf0 UIKit`__77-[UIInputWindowController moveFromPlacement:toPlacement:starting:completion:]_block_invoke.907 + 388
frame #8: 0x0000000189b2d0f0 UIKit`+[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 636
frame #9: 0x0000000189bfe52c UIKit`+[UIView(UIViewAnimationWithBlocks) _animateWithDuration:delay:options:animations:start:completion:] + 128
frame #10: 0x000000018a4cd76c UIKit`-[UIInputWindowController moveFromPlacement:toPlacement:starting:completion:] + 1368
frame #11: 0x000000018a4d4268 UIKit`-[UIInputWindowController setInputViewSet:] + 1444
frame #12: 0x000000018a4cce38 UIKit`-[UIInputWindowController performOperations:withAnimationStyle:] + 56
frame #13: 0x0000000189bbe278 UIKit`-[UIPeripheralHost(UIKitInternal) setInputViews:animationStyle:] + 1276
frame #14: 0x0000000189b1da78 UIKit`-[UIResponder(UIResponderInputViewAdditions) reloadInputViews] + 80
frame #15: 0x0000000189b7bb4c UIKit`-[UIResponder becomeFirstResponder] + 600
frame #16: 0x0000000189b7bebc UIKit`-[UIView(Hierarchy) becomeFirstResponder] + 148
frame #17: 0x0000000189bfe0b4 UIKit`-[UITextField becomeFirstResponder] + 60
frame #18: 0x0000000189ca5128 UIKit`-[UITextInteractionAssistant(UITextInteractionAssistant_Internal) setFirstResponderIfNecessary] + 192
frame #19: 0x0000000189ca4630 UIKit`-[UITextInteractionAssistant(UITextInteractionAssistant_Internal) oneFingerTap:] + 3024
frame #20: 0x000000018a0bff80 UIKit`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 64
frame #21: 0x000000018a0c3688 UIKit`_UIGestureRecognizerSendTargetActions + 124
frame #22: 0x0000000189c8a73c UIKit`_UIGestureRecognizerSendActions + 260
frame #23: 0x0000000189b290f0 UIKit`-[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 764
frame #24: 0x000000018a0b3680 UIKit`_UIGestureEnvironmentUpdate + 1100
frame #25: 0x000000018a0b31e0 UIKit`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 408
frame #26: 0x000000018a0b249c UIKit`-[UIGestureEnvironment _updateGesturesForEvent:window:] + 268
frame #27: 0x0000000189b2730c UIKit`-[UIWindow sendEvent:] + 2960
frame #28: 0x0000000189af7da0 UIKit`-[UIApplication sendEvent:] + 340
frame #29: 0x000000018a2e175c UIKit`__dispatchPreprocessedEventFromEventQueue + 2736
frame #30: 0x000000018a2db130 UIKit`__handleEventQueue + 784
frame #31: 0x0000000183bf6b5c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #32: 0x0000000183bf64a4 CoreFoundation`__CFRunLoopDoSources0 + 524
frame #33: 0x0000000183bf40a4 CoreFoundation`__CFRunLoopRun + 804
frame #34: 0x0000000183b222b8 CoreFoundation`CFRunLoopRunSpecific + 444
frame #35: 0x00000001855d6198 GraphicsServices`GSEventRunModal + 180
frame #36: 0x0000000189b627fc UIKit`-[UIApplication _run] + 684
frame #37: 0x0000000189b5d534 UIKit`UIApplicationMain + 208
frame #38: 0x00000001000da9a4 DuoYiIMOrig`main(argc=<unavailable>, argv=<unavailable>) at main.m:15 [opt]
frame #39: 0x0000000182b055b8 libdyld.dylib`start + 4

恢复 OC 符号

第三方 app 往往都去除了符号,建议进行一下恢复符号表的操作。恢复符号表后,在调试时就能直接在堆栈中看到方法名,免去了计算偏移量然后在 hopper 里查找的麻烦。参考:iOS符号表恢复&逆向支付宝, restore-symbol

用 Xcode 直接调试

除了用命令行,也可以直接用 Xcode 进行 lldb 调试,有了图形界面,也能使用Debug UI HierarchyDebug 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进制、二进制、字符、字符串格式
  • poe -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:17b_regexp-break的缩写
  • 符号断点:b isEven, br s -F isEven
  • 用正则表达式进行符号断点:br set -r '正则'
  • 断点条件:breakpoint modify -c 'i == 99' 1
  • 断点时附加自定义操作:breakpoint command add 1

监控地址

  • 内存监控:
1
2
3
4
// 获取需要监控的内存地址
p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8

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

1
2
3
4
5
6
7
# ~/.lldbinit
# 如果是通过 brew install chisel 安装
command script import /usr/local/opt/chisel/libexec/fblldb.py
# 如果是手动下载,则填写 chisel 里的 fblldb.py 路径
command script import /path/to/fblldb.py

之后重启 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添加快捷命令,例如:

1
2
# reloadscript 命令:修改脚本文件后,重新加载
command alias reloadscript command source ~/.lldbinit

之后输入reloadscript就相当于输入command source ~/.lldbinit

编写自定义脚本

编写 Python 脚本,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# some_script.py
import lldb
# 执行命令
def run(debugger, command, result, internal_dict):
"""
Print root view controller of key window
"""
print("hello world!")
debugger.HandleCommand('po (id)[(id)[(id)[UIApplication sharedApplication] keyWindow] rootViewController]')
# lldb 启动入口
def __lldb_init_module(debugger, internal_dict):
# 添加 ptopvc 命令
debugger.HandleCommand('command script add -f some_script.run ptopvc')

如果有chisel,可以直接使用chisel里封装好的模块和各种函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# some_script.py
import lldb
import fblldbbase as fb
# 可以同时声明多个命令
def lldbcommands():
return [ SomeCommand() ]
# 定义命令
class SomeCommand(fb.FBCommand):
# 命令名
def name(self):
return 'ptopvc'
# 描述
def description(self):
return 'Print root view controller of key window'
# 选项
def options(self):
return [
fb.FBCommandArgument(short='-v', long='--verbose', help='Show ivar of the result object', default=False, boolean=True)
]
# 参数
def args(self):
return [ fb.FBCommandArgument(arg='instance or class', type='instance or Class', help='an Objective-C Class.') ]
# 执行命令
def run(self, arguments, options):
print("hello world!")
fb.evaluateExpression('(id)[(id)[(id)[UIApplication sharedApplication] keyWindow] rootViewController]')

详情请见chisel代码。

写脚本时可以随时在 lldb 里调用reloadscript命令重新加载,进行测试。

脚本提供了操作 lldb 的接口,例如设置断点、执行命令。不过编写命令有些坑:

  • 大部分 OC 方法和函数都需要明确声明返回值类型
  • 指针声明时需要初始化,不会默认设为 nil,否则在使用时会出现野指针

导入自定义脚本

打开~/.lldbinit添加:

1
2
3
4
5
# 导入自定义脚本的路径
command script import /path/to/some_script.py
# 可以通过 chisel 提供的函数导入目录下的所有脚本
script fblldb.loadCommandsInDirectory('/Users/xxx/Documents/code/lldbScript/')

实战演练:追踪block回调

下面演练一下 lldb 调试的过程。

有时候逻辑是通过block回调来执行的,追踪调用路径时,需要找出block的执行地址。直接打印block对象并不会显示执行地址,需要分析内存才能找出。下面的分析流程和 lldb 命令pblock是一样的。

block的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
// void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
// void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
// const char *signature; // IFF (1<<30)
void* rest[1];
} *descriptor;
// imported variables
};
enum {
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30),
};

查看invoke指针的地址

演示代码如下:

1
2
3
4
5
6
7
8
9
- (void)modifyUIAtBackbround {
void(^crash)() = ^ {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.view addSubview:[[UIView alloc] init]];
});
};
crash();
}

当获取到一个block变量时:

1
2
Printing description of $x1:
<__NSStackBlock__: 0x16fd465b8>

查看0x16fd465b8的内存,由于字节对齐的原因,结构体内的数据是按照最大的8字节对齐的:

1
2
3
4
5
(lldb) memory read --size 8 --format x 0x16fd465b8
0x16fd465b8: 0x00000001a94520d8 0x00000000c2000000
0x16fd465c8: 0x00000001000bf77c 0x000000010012c9f0
0x16fd465d8: 0x0000000131e0a610 0x00000001700517f0
0x16fd465e8: 0x00000001700517f0 0x000000016fd46650

void *isa 占用8字节,int占用4字节,所以invoke指针的值是0x00000001000bf77cdescriptor的地址是0x000000010012c9f0

对地址反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
disassemble -a 0x00000001000bf77c
MyApp`__38-[ViewController modifyUIAtBackbround]_block_invoke_2:
0x1000bf77c <+0>: sub sp, sp, #0x30 ; =0x30
0x1000bf780 <+4>: stp x29, x30, [sp, #0x20]
0x1000bf784 <+8>: add x29, sp, #0x20 ; =0x20
0x1000bf788 <+12>: adrp x8, 125
0x1000bf78c <+16>: add x8, x8, #0xe0 ; =0xe0
0x1000bf790 <+20>: stur x0, [x29, #-0x8]
0x1000bf794 <+24>: mov x9, x0
0x1000bf798 <+28>: str x9, [sp, #0x10]
0x1000bf79c <+32>: ldr x9, [x0, #0x20]
0x1000bf7a0 <+36>: ldr x1, [x8]
0x1000bf7a4 <+40>: mov x0, x9
0x1000bf7a8 <+44>: bl 0x100113e98 ; symbol stub for: objc_msgSend
0x1000bf7ac <+48>: mov x29, x29
0x1000bf7b0 <+52>: bl 0x100113eec ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1000bf7b4 <+56>: adrp x8, 124
0x1000bf7b8 <+60>: add x8, x8, #0xed0 ; =0xed0
0x1000bf7bc <+64>: adrp x9, 126
0x1000bf7c0 <+68>: add x9, x9, #0x308 ; =0x308
0x1000bf7c4 <+72>: ldr x9, [x9]
0x1000bf7c8 <+76>: ldr x1, [x8]
0x1000bf7cc <+80>: str x0, [sp, #0x8]
0x1000bf7d0 <+84>: mov x0, x9
0x1000bf7d4 <+88>: bl 0x100113e98 ; symbol stub for: objc_msgSend
0x1000bf7d8 <+92>: adrp x8, 124
0x1000bf7dc <+96>: add x8, x8, #0xed8 ; =0xed8
0x1000bf7e0 <+100>: ldr x1, [x8]
0x1000bf7e4 <+104>: bl 0x100113e98 ; symbol stub for: objc_msgSend
0x1000bf7e8 <+108>: adrp x8, 125
0x1000bf7ec <+112>: add x8, x8, #0xe8 ; =0xe8
0x1000bf7f0 <+116>: ldr x1, [x8]
0x1000bf7f4 <+120>: ldr x8, [sp, #0x8]
0x1000bf7f8 <+124>: str x0, [sp]
0x1000bf7fc <+128>: mov x0, x8
0x1000bf800 <+132>: ldr x2, [sp]
0x1000bf804 <+136>: bl 0x100113e98 ; symbol stub for: objc_msgSend
0x1000bf808 <+140>: ldr x0, [sp]
0x1000bf80c <+144>: bl 0x100113ebc ; symbol stub for: objc_release
0x1000bf810 <+148>: ldr x0, [sp, #0x8]
0x1000bf814 <+152>: bl 0x100113ebc ; symbol stub for: objc_release
0x1000bf818 <+156>: ldp x29, x30, [sp, #0x20]
0x1000bf81c <+160>: add sp, sp, #0x30 ; =0x30
0x1000bf820 <+164>: ret

查看block的签名

如果要进一步查看block的签名,首先检查block的flags,确定内存布局:

1
2
3
4
(lldb) p (BOOL)(0x00000000c2000000 & (1<<30))
(BOOL) $37 = YES
(lldb) p (BOOL)(0x00000000c2000000 & (1<<25))
(BOOL) $38 = YES

flags BLOCK_HAS_COPY_DISPOSE为YES,说明descriptor里有dispose_helperdispose_helpersignature在第8 + 8 + 8 + 8 = 32个字节。

查看descriptor的内存,第32个字节的内容:

1
2
3
4
5
6
7
(lldb) memory read --size 8 --format x 0x000000010012c9f0
0x10012c9f0: 0x0000000000000000 0x0000000000000028
0x10012ca00: 0x00000001000bf824 0x00000001000bf870
0x10012ca10: 0x0000000100115c77 0x0000000000000100
0x10012ca20: 0x0000000000000000 0x0000000000000028
(lldb) po (const char *)0x0000000100115c77
"v8@?0"

查看签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(lldb) po [NSMethodSignature signatureWithObjCTypes:"v8@?0"]
<NSMethodSignature: 0x17027bd80>
number of arguments = 1
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}

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 方法:

1
2
cy# UIApplication.sharedApplication().windows[0].contentView().subviews()[0]
#"<SBFStaticWallpaperView: 0x1590ca730; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x1590cabd0>>"
1
2
cy# var c = [[UIApp windows][0] contentView]
#"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>"

通过地址获取对象:

1
2
cy# c = #0x10e883d40
#"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>"

获取实例变量的值:

1
2
cy# c->_subviewCache
@[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"]

打印对象的实例变量:

1
2
cy# *c
{isa:"UIView",_layer:#"<CALayer: 0x10e883e00>",_gestureInfo:null,_gestureRecognizers:null,_subviewCache:@[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"],_charge:0,_tag:0,_viewDelegate:null,_backgroundColorSystemColorName:null,_countOfMotionEffectsInSubtree:1,_viewFlags:@error,_retainCount:8,_tintAdjustmentDimmingCount:0,_shouldArchiveUIAppearanceTags:false,_interactionTintColor:null,_layoutEngine:null,_boundsWidthVariable:null,_boundsHeightVariable:null,_minXVariable:null,_minYVariable:null,_internalConstraints:null,_constraintsExceptingSubviewAutoresizingConstraints:null}

调用 C 函数

1
2
3
4
cy# extern "C" int getuid();
(extern "C" int getuid())
cy# getuid()
501
1
2
3
4
5
6
7
8
cy# getuid = dlsym(RTLD_DEFAULT, "getuid")
(typedef void*)(0x7fff885f95b0)
cy# getuid()
throw new Error("cannot call a pointer to non-function")
cy# getuid = (typedef int())(getuid)
(extern "C" int getuid())
cy# getuid()
501

添加 category

1
2
3
4
5
6
7
8
cy# @implementation NSObject (MyCategory)
- description { return "hello"; }
- (double) f:(int)v { return v * 0.5; }
@end
cy# o = [new NSObject init]
#"hello"
cy# [o f:3]
1.5

查找指定类的对象

Cycript 的choose命令可以列出指定类的所有实例对象,和 lldb 命令findinstances类似:

1
2
cy# choose(SBIconModel)
[#"<SBIconModel: 0x1590c8430>"]
1
2
cy# var views = choose(SBIconView)
[#"<SBIconView: 0x159460fa0; frame = (27 92; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x159518ae0>; layer = <CALayer: 0x159461220>>",#"<SBIconView: 0x159468e50; frame = (114 356; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x15946d2f0>; layer = <CALayer: 0x1592c9a70>>",...

hook

通过 js 原型操作对象:

1
2
cy# var oldm = NSObject.prototype.description
(extern "C" id ":description"(id, SEL))

修改 prototype 进行 hook:

1
2
3
cy# NSObject.prototype.description = function() { return oldm.call(this) + ' (of doom)'; }
cy# [new NSObject init]
#"<NSObject: 0x100d11520> (of doom)"

Cycript 中也可以使用越狱机上的 hook 框架Cydia Substrate。使用MS.hookMessagehook OC 方法:

1
2
3
4
5
6
7
cy# @import com.saurik.substrate.MS
cy# var oldm = {};
cy# MS.hookMessage(NSObject, @selector(description), function() {
return oldm->call(this) + " (of doom)";
}, oldm)
cy# [new NSObject init]
#"<NSObject: 0x100203d10> (of doom)"

使用MS.hookFunctionhook C 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
cy# @import com.saurik.substrate.MS
cy# extern "C" void *fopen(char *, char *);
cy# var oldf = {}
cy# var log = []
cy# MS.hookFunction(fopen, function(path, mode) {
var file = (*oldf)(path, mode);
log.push([path.toString(), mode.toString(), file]);
return file;
}, oldf)
cy# fopen("/etc/passwd", "r");
(typedef void*)(0x7fff774ff2a0)
cy# log
[["/etc/passwd","r",(typedef void*)(0x7fff774ff2a0)]]

Frida

Frida 是一个跨平台的动态调试工具,可以用 js 脚本和 OC 进行交互,从而执行代码、打log、hook 函数。和 cycript 类似,不过兼容性比 cycript 要好。同样的,frida 能做的,用 lldb 基本上也能做到。Frida 的优势是跨平台,以及提供的 js 库、命令行,能够实现脚本化。Frida 官网

安装

Mac 端安装 frida:

1
pip install --user frida-tools

iOS 设备在 Cydia 中添加源: https://build.frida.re,之后在 Cydia 中搜索 frida 安装。

使用

列出进程

在 mac 上执行frida-ps -U等待 iOS 设备连接到 USB,连接到后就会列出 iOS 设备上正在运行的进程。

也可以用frida-ps -Uai列出正在运行的 app。

调用追踪

可以使用frida-trace追踪 app 的调用。

1
2
3
4
5
# Trace recv* and send* APIs in Safari
$ frida-trace -i "recv*" -i "send*" Safari
# Trace ObjC method calls in Safari
$ frida-trace -m "-[NSView drawRect:]" Safari
1
2
3
4
5
6
7
8
9
~ $ frida-trace -i "recv*" -i "read*" *twitter*
recv: Auto-generated handler: …/recv.js
# (snip)
recvfrom: Auto-generated handler: …/recvfrom.js
Started tracing 21 functions. Press Ctrl+C to stop.
39 ms recv()
112 ms recvfrom()
128 ms recvfrom()
129 ms recvfrom()

连接指定进程

使用frida -U <app名>连接到指定进程,也可以同时注入 js 脚本:frida -n Twitter -l demo1.js

连接上后,就可以执行 frida 的 js 命令,以及运行注入的 js 脚本。

Frida 提供了强大的 js 库,可以去官网查看完整的 API 文档:JavaScript API。这里只列出一些有用的接口。

执行脚本

与 OC 交互

参考JavaScript API: ObjC

获取 OC 类列表:ObjC.classes

获取指定类:var NSString = ObjC.classes.NSString;,并调用类方法:NSString.stringWithString_("Hello World");

调用实例方法:NSString.alloc().initWithString_("Hello World");

GCD 线程:

1
2
3
ObjC.schedule(ObjC.mainQueue, function () {
NSString.stringWithString_("Hello World");
});

获取内存中指定类的所有实例:

1
2
3
4
5
6
7
ObjC.choose(ObjC.classes.UIViewController, {
onMatch: function (instance) {
console.log("Found instance: " + instance);
},
onComplete: function () { }
truetruetruetruetruetruetrue// 搜索完毕
});
1
var viewControllers = ObjC.chooseSync(ObjC.classes.UIViewController)

获取 OC 对象:new ObjC.Object(ptr("0x1234"))

可以通过 js 对象的属性获取 OC 对象的内容:

  • $kind: string specifying either instance, class or meta-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 to ObjC.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 函数指针:

1
2
3
var sqlite3_sql = Module.getExportByName('libsqlite3.dylib', 'sqlite3_sql');
var openPtr = Module.findExportByName(null,"open");

调用 C 函数:

1
2
3
4
5
var sqlite3_sql = new NativeFunction(sqlite3_sqlPtr, 'char', ['pointer']);
sqlite3_sql(statement);
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var fd = open(Memory.allocUtf8String('/tmp/test.txt'), 0);

hook

Hook OC 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Get a reference to the openURL selector
var openURL = ObjC.classes.UIApplication["- openURL:"];
// Intercept the method
Interceptor.attach(openURL.implementation, {
onEnter: function(args) {
// 方法执行前调用
// As this is an ObjectiveC method, the arguments are as follows:
// 0. 'self'
// 1. The selector (openURL:)
// 2. The first argument to the openURL selector
var myNSURL = new ObjC.Object(args[2]);
// Convert it to a JS string
var myJSURL = myNSURL.absoluteString().toString();
// Log it
console.log("Launching URL: " + myJSURL);
},
onLeave: function (retval) {
// 执行后调用
// 修改返回值
retval.replace(1)
}
});

替换 C 函数(OC 方法同理):

1
2
3
4
5
6
7
8
9
var openPtr = Module.getExportByName(null, 'open');
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
Interceptor.replace(openPtr, new NativeCallback(function (pathPtr, flags) {
var path = pathPtr.readUtf8String();
log('Opening "' + path + '"');
var fd = open(pathPtr, flags);
console.log('Got fd: ' + fd);
return fd;
}, 'int', ['pointer', 'int']));

Hook C 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Interceptor.attach(Module.getExportByName(null, 'open'), {
onEnter: function (args) {
// 执行前调用
console.log('Context information:');
console.log('Context : ' + JSON.stringify(this.context));
console.log('Return : ' + this.returnAddress);
console.log('ThreadId : ' + this.threadId);
console.log('Depth : ' + this.depth);
console.log('Errornr : ' + this.err);
// Save arguments for processing in onLeave.
this.fd = args[0].toInt32();
this.buf = args[1];
this.count = args[2].toInt32();
},
onLeave: function (result) {
// 执行后调用
console.log('----------')
// Show argument 1 (buf), saved during onEnter.
var numBytes = result.toInt32();
if (numBytes > 0) {
console.log(hexdump(this.buf, { length: numBytes, ansi: true }));
}
console.log('Result : ' + numBytes);
}
})

IDA 动态调试

IDA 也有一个动态调试工具,不过没有 lldb 这么多针对 iOS 平台的命令,只要是用来辅助逆向分析,查看控制流,打log。IDA 的 trace 功能可以从指令级别上记录运行时程序的流程,查看寄存器和内存的值,不过 IDA 调试的同时不能使用 lldb,如果想要查看其他详细信息,可以配合 cycript 或 frida。

如果你想分析代码的控制流,可以使用 IDA 的动态调试。IDA 也有一些第三方插件用于辅助调试。

配置 debugger

IDA 提供了 iOS 的 debugger。首先将砸壳后的 app 用 IDA 分析完毕,再重签名后安装到 iOS 设备上,目的是让 IDA 分析的二进制文件和设备上的保持一致。

之后设置 IDA 的 debugger 配置:

  1. 在 IDA 的Debugger->Switch Debugger中选择Remote iOS Debugger
  2. 配置 debugger:打开Debugger->Debugger options,在弹出的面板中打开 Set specific options
    1. 设置Symbol path为当前设备的符号文件路径,例如~/Library/Developer/Xcode/iOS DeviceSupport/11.2.2 (15C202)/Symbols/
    2. 勾选 Launch debugserver automatically
  3. 配置 process:打开Debugger->Process options,设置 ApplicationInput 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 插件。由于内容有点多,留到之后的文章中再展开。

参考