iOS 逆向指南:動態分析

當靜態分析沒法獲取足夠的信息時,就須要進行動態分析,在 app 運行時,追蹤方法調用、查看內存信息。最後找到想要分析的關鍵函數。javascript

這篇文章包括:html

  • 環境搭建
  • 反調試
  • 動態調試的思路
  • lldb 調試命令與腳本
  • cycript 配置與使用
  • frida 配置與使用
  • IDA 動態調試

環境搭建

安裝 openSSH

參照靜態分析中的安裝 openSSH小結。java

用 USB 進行 SSH 鏈接

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 2222git

修改 debugserver

使用 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 ~/debugserversql

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 了。

  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

而這個二進制文件的基址爲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查看調用堆棧,縮寫爲btthread 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 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

監控地址

  • 內存監控:
// 獲取須要監控的內存地址
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指針的值是0x00000001000bf77cdescriptor的地址是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_helperdispose_helpersignature在第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 代碼,內置了一些頗有用的功能。官網: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}
複製代碼

調用 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.hookMessagehook 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.hookFunctionhook 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 中添加源: 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 交互

參考JavaScript API: ObjC

獲取 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 函數

獲取 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 配置:

  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 插件。因爲內容有點多,留到以後的文章中再展開。

參考

相關文章
相關標籤/搜索