当静态分析无法获取足够的信息时,就需要进行动态分析,在 app 运行时,追踪方法调用、查看内存信息。最后找到想要分析的关键函数。
这篇文章包括:
- 环境搭建
- 反调试
- 动态调试的思路
- lldb 调试命令与脚本
- cycript 配置与使用
- frida 配置与使用
- IDA 动态调试
环境搭建
安装 openSSH
参照静态分析中的安装 openSSH
小结。
用 USB 进行 SSH 连接
openSSH 默认是用 wifi 连接到 iOS 设备的,但是这样速度慢,不稳定。因此可以安装usbmuxd
,用 USB 连接:
brew install usbmuxd
安装后就可以用iproxy
工具,将设备上的端口号映射到电脑上的某一个端口:
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 文件:
<!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 文件:
<?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 了。
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
命令来反汇编找到的地址,如果反汇编结果和静态分析中的汇编代码一致,说明找到的是正确的:
(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
打断点:
(lldb) br set -a 0x1007dfbcc
Breakpoint 1: where = DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550, address = 0x00000001007dfbcc
查看寄存器的值
当触发了断点后,可以用register read
查看当前寄存器的值:
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
查看调用堆栈,缩写为bt
。thread backtrace -e true
可以显示线程嵌套的堆栈。
(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 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
监控地址
- 内存监控:
// 获取需要监控的内存地址
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
:
# ~/.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
添加快捷命令,例如:
# reloadscript 命令:修改脚本文件后,重新加载
command alias reloadscript command source ~/.lldbinit
之后输入reloadscript
就相当于输入command source ~/.lldbinit
。
编写自定义脚本
编写 Python 脚本,格式如下:
# 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
里封装好的模块和各种函数:
# 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
添加:
# 导入自定义脚本的路径
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的结构
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指针的地址
演示代码如下:
- (void)modifyUIAtBackbround {
void(^crash)() = ^ {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.view addSubview:[[UIView alloc] init]];
});
};
crash();
}
当获取到一个block变量时:
Printing description of $x1:
<__NSStackBlock__: 0x16fd465b8>
查看0x16fd465b8
的内存,由于字节对齐的原因,结构体内的数据是按照最大的8字节对齐的:
(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指针的值是0x00000001000bf77c
,descriptor
的地址是0x000000010012c9f0
。
对地址反汇编:
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,确定内存布局:
(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_helper
和dispose_helper
,signature
在第8 + 8 + 8 + 8 = 32个字节。
查看descriptor
的内存,第32个字节的内容:
(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"
查看签名:
(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 方法:
cy# UIApplication.sharedApplication().windows[0].contentView().subviews()[0]
#"<SBFStaticWallpaperView: 0x1590ca730; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x1590cabd0>>"
cy# var c = [[UIApp windows][0] contentView]
#"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>"
通过地址获取对象:
cy# c = #0x10e883d40
#"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>"
获取实例变量的值:
cy# c->_subviewCache
@[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"]
打印对象的实例变量:
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 函数
cy# extern "C" int getuid();
(extern "C" int getuid())
cy# getuid()
501
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
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
类似:
cy# choose(SBIconModel)
[#"<SBIconModel: 0x1590c8430>"]
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 原型操作对象:
cy# var oldm = NSObject.prototype.description
(extern "C" id ":description"(id, SEL))
修改 prototype 进行 hook:
cy# NSObject.prototype.description = function() { return oldm.call(this) + ' (of doom)'; }
cy# [new NSObject init]
#"<NSObject: 0x100d11520> (of doom)"
Cycript 中也可以使用越狱机上的 hook 框架Cydia Substrate
。使用MS.hookMessage
hook OC 方法:
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.hookFunction
hook C 函数:
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:
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 的调用。
# 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
~ $ 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 交互
获取 OC 类列表:ObjC.classes
获取指定类:var NSString = ObjC.classes.NSString;
,并调用类方法:NSString.stringWithString_("Hello World");
调用实例方法:NSString.alloc().initWithString_("Hello World");
GCD 线程:
ObjC.schedule(ObjC.mainQueue, function () {
NSString.stringWithString_("Hello World");
});
获取内存中指定类的所有实例:
ObjC.choose(ObjC.classes.UIViewController, {
onMatch: function (instance) {
console.log("Found instance: " + instance);
},
onComplete: function () { }
// 搜索完毕
});
var viewControllers = ObjC.chooseSync(ObjC.classes.UIViewController)
获取 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 函数指针:
var sqlite3_sql = Module.getExportByName('libsqlite3.dylib', 'sqlite3_sql');
var openPtr = Module.findExportByName(null,"open");
调用 C 函数:
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 方法:
// 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 方法同理):
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 函数:
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 配置:
- 在 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 插件。由于内容有点多,留到之后的文章中再展开。