當靜態分析沒法獲取足夠的信息時,就須要進行動態分析,在 app 運行時,追蹤方法調用、查看內存信息。最後找到想要分析的關鍵函數。javascript
這篇文章包括:html
參照靜態分析中的安裝 openSSH
小結。java
openSSH 默認是用 wifi 鏈接到 iOS 設備的,可是這樣速度慢,不穩定。所以能夠安裝usbmuxd
,用 USB 鏈接:python
brew install usbmuxd
複製代碼
安裝後就能夠用iproxy
工具,將設備上的端口號映射到電腦上的某一個端口:ios
iproxy 2222 22
複製代碼
用 USB 鏈接設備到 mac 上,以前 openSSH 鏈接 iOS 的命令是ssh root@10.5.53.182
,如今改爲ssh root@localhost -p 2222
。git
使用 lldb 調試須要準備 debugserver。使用 OSX 中的 lldb 遠程鏈接 iOS 上的 debugserver,由 debugserver 做爲 lldb 和 iOS 的中轉,執行命令和返回結果。在默認狀況下,iOS 上並無安裝 debugserver,只有在設備鏈接過一次 Xcode,安裝了開發者插件後,debugserver 纔會被 Xcode 安裝到iOS的/Developer/usr/bin/
目錄下。github
在 iOS 11 越獄以前,須要對 debugserver 進行重簽名,在 iOS 11 上能夠直接使用/Developer/usr/bin/debugserver
,或者直接用 Xcode 對 iOS 上的 app 進行調試。iOS 11 以前用 Xcode 調試須要對 app 進行重簽名,而 iOS 11 以後不須要重簽名 app 也能調試了。正則表達式
iOS 11 以前重簽名 debugserver 步驟:spring
1.拷貝 debugserver 到本地計算機中:scp root@iOSDeviceIP:/Developer/usr/bin/debugserver ~/debugserver
。sql
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
複製代碼
第三方 app 每每都去除了符號,建議進行一下恢復符號表的操做。恢復符號表後,在調試時就能直接在堆棧中看到方法名,免去了計算偏移量而後在 hopper 裏查找的麻煩。參考:iOS符號表恢復&逆向支付寶, restore-symbol。
除了用命令行,也能夠直接用 Xcode 進行 lldb 調試,有了圖形界面,也能使用Debug UI Hierarchy
和Debug Memory Graph
工具。參考iOS逆向:用Xcode直接調試第三方app。
若是是 iOS 11 以前的越獄設備,須要重簽名後才能用 Xcode 調試。iOS 11 以後沒有限制,能夠直接用 Xcode 調試 App Store 上下載的 app。
要想充分發揮 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 可使用 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
:打印對象或者類的全部實例變量和值
你能夠 用 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 的接口,例如設置斷點、執行命令。不過編寫命令有些坑:
打開~/.lldbinit
添加:
# 導入自定義腳本的路徑
command script import /path/to/some_script.py
# 能夠經過 chisel 提供的函數導入目錄下的全部腳本
script fblldb.loadCommandsInDirectory('/Users/xxx/Documents/code/lldbScript/')
複製代碼
下面演練一下 lldb 調試的過程。
有時候邏輯是經過block回調來執行的,追蹤調用路徑時,須要找出block的執行地址。直接打印block對象並不會顯示執行地址,須要分析內存才能找出。下面的分析流程和 lldb 命令pblock
是同樣的。
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),
};
複製代碼
演示代碼以下:
- (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的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 是 Saruik 大佬開發的動態調試工具,內置了一套 JavaScript 解釋器,能夠用 js 腳本和 OC 交互,用 js 執行 OC 代碼,內置了一些頗有用的功能。官網:www.cycript.org,源碼地址: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}
複製代碼
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
複製代碼
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>>",...
複製代碼
經過 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 是一個跨平臺的動態調試工具,能夠用 js 腳本和 OC 進行交互,從而執行代碼、打log、hook 函數。和 cycript 相似,不過兼容性比 cycript 要好。一樣的,frida 能作的,用 lldb 基本上也能作到。Frida 的優點是跨平臺,以及提供的 js 庫、命令行,可以實現腳本化。Frida 官網。
Mac 端安裝 frida:
pip install --user frida-tools
複製代碼
iOS 設備在 Cydia 中添加源: 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 類列表: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 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 函數指針:
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 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 也有一個動態調試工具,不過沒有 lldb 這麼多針對 iOS 平臺的命令,只要是用來輔助逆向分析,查看控制流,打log。IDA 的 trace 功能能夠從指令級別上記錄運行時程序的流程,查看寄存器和內存的值,不過 IDA 調試的同時不能使用 lldb,若是想要查看其餘詳細信息,能夠配合 cycript 或 frida。
若是你想分析代碼的控制流,可使用 IDA 的動態調試。IDA 也有一些第三方插件用於輔助調試。
IDA 提供了 iOS 的 debugger。首先將砸殼後的 app 用 IDA 分析完畢,再重簽名後安裝到 iOS 設備上,目的是讓 IDA 分析的二進制文件和設備上的保持一致。
以後設置 IDA 的 debugger 配置:
Debugger
->Switch Debugger
中選擇Remote iOS Debugger
Debugger
->Debugger options
,在彈出的面板中打開 Set specific options
Symbol path
爲當前設備的符號文件路徑,例如~/Library/Developer/Xcode/iOS DeviceSupport/11.2.2 (15C202)/Symbols/
Launch debugserver automatically
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 有些開源插件用於加強動態調試功能。例如funcap能夠記錄運行時的寄存器信息做爲註釋,輔助分析。不過這個工具如今只支持 32 位。
其餘的插件你能夠自行搜索。不過能用到 iOS 上的動態調試插件並很少。
動態調試的整個流程以及用到的工具大部分都總結在此了。還有一個強力的工具這裏沒有講解,就是 tweak 插件。因爲內容有點多,留到以後的文章中再展開。