本文是Advanced Apple Debugging的學(xué)習(xí)筆記.
首先將Xcode升級到8.3版本.可以通過下載地址下載.
我們主要是通過LLDB,Python和DTrace來查看apple code.
在這一章中,你將會熟悉如何使用LLDB查看內(nèi)部進程,并且調(diào)試一個程序.
你將在使用LLDB 的調(diào)試的過程中會發(fā)現(xiàn)你可以對一個你沒有源代碼的程序做出令人驚喜的改變.第一章會作為學(xué)習(xí)的一個過度章節(jié), 因此許多重要的概念和深入到LLDB函數(shù)的功能將會在后面的章節(jié)介紹.
通過Rootless開始
在你開始使用LLDB之前, 你需要學(xué)習(xí)一些關(guān)于apple挫敗惡意軟件的特點介紹.讓人不爽的是,這些特點會打擊你使用LLDB和其他類似的工具比如DTrace調(diào)試內(nèi)部代碼的企圖.但是不要慫,因為apple為那些知道自己在做什么的人提供了一個方法來關(guān)閉這個功能.并且你就將成為那些知道自己在做什么的人中的一員.
阻止你企圖進入內(nèi)部調(diào)試的功能是System Integrity Protection
, 也號稱Rootless
.
這個系統(tǒng)限制了哪些程序可以(即便是他們本身有root權(quán)限)阻止惡意程序安裝在你系統(tǒng)的中.
盡管rootless 在安全方面是實質(zhì)性的進步, 但同時他也帶來了一些讓程序變得難以調(diào)試的麻煩.說白了就是它阻止了其他進程調(diào)試apple簽名了的進程.
由于本書要調(diào)試的不僅僅是你自己的程序, 還有你非常感興趣的程序, 因此在你學(xué)習(xí)的時候移除這些功能就變得非常重要,以便你可以查看你選擇的程序.
如果你現(xiàn)在已經(jīng)啟用了roorless, 你將不能夠調(diào)試apple的主要程序.但是也有例外, 比如iOS模擬器里的自帶的幾個程序可以用.
例如, 嘗試將LLDB附加到Finder應(yīng)用程序.
打開Terminal, 通過下面的命令查看finder的進程.
lldb -n Finder
你將會看到下面的錯誤:
error: attach failed: cannot attach to process due to System Integrity
注意: 有許多方法可以附加一個進程, 此外當(dāng)LLDB附加成功的時候可以指定配置. 想要學(xué)習(xí)更多關(guān)于附加進程的知識可以查看第三章的內(nèi)容,"用LLDB附加"
禁用Rootless
要禁用Rootless, 請執(zhí)行下面的步驟:
- 重啟你的macOS 設(shè)備
- 當(dāng)屏幕變成空白的時候, 按住Command+R直到apple的啟動logo出現(xiàn),這將會使你的電腦進入恢復(fù)模式.
3.現(xiàn)在, 在頂部找到Utilities菜單然后選擇Terminal,.
4.當(dāng)Terminal窗口打開的時候, 輸入:
csrutil disable; reboot
5.你的電腦將會用禁用Rootles的模式重新啟動.
注意:練習(xí)本書中所有操作的一個安全的方法是在VMWare or VirtualBox創(chuàng)建一個虛擬機來操作, 并僅僅在虛擬機中禁用Rootless.
你可以通過在終端中輸入同樣的命令來驗證一下你是否成功的禁用了Rootless.
lldb -n Finder
LLDB應(yīng)該會講自己附加到當(dāng)前Finder的進程.附加成功后的輸出因該是下面這個樣子.
在確認成功附加以后.通過關(guān)閉Terminal窗口或者輸入
quit
退出LLDB并且在LLDB的控制臺中確認是否已經(jīng)退出.
附加LLDB到Xcode
現(xiàn)在你已經(jīng)禁用了Rootless, 并且你可以將LLDB附加到進程上, 是時候開始你調(diào)試的旋風(fēng)之旅了.你查看的第一個應(yīng)用程序?qū)⑹悄阍谌諒?fù)一日的開發(fā)中經(jīng)常使用的Xcode!
打開一個新的Terminal窗口. 接下來, 通過按下? + Shift + I來編輯, 窗口的標(biāo)題. 一個新的彈窗將會出現(xiàn).在Tab Title 一欄輸入LLDB.
然后確保Xcode沒有運行, 否則你最終會因為運行了多個Xcode的實例而造成混亂.
在Terminal中輸入下面的命令:
lldb
這將會啟動LLDB.
現(xiàn)在通過按住? + T
來創(chuàng)建一個新的Terminal窗口.再次通過按住? + Shift + I
編輯這個窗口的標(biāo)題, 并將這個窗口命名為Xcode stderr.這個窗口將會輸出你在調(diào)試器中打印的所有的內(nèi)容.
確保你再Xcode stderr 這個終端的窗口中, 并且輸入下面的命令:
tty
你將會看到一些類似于下面的輸出:
/dev/ttys027
如果你輸出的內(nèi)容跟上面的不一樣也不要擔(dān)心.如果輸出的結(jié)果與我的不同的話我并不會很驚訝.因為這是您的終端會話的地址.
舉例說明一下你將會用Xcode stderr這個窗口來做哪些事情, 創(chuàng)建另外一個窗口并鍵入一下內(nèi)容:
echo "hello debugger" 1>/dev/ttys027
確保你將上面的/dev/ttys027
替換成了你通過tty
命令得到的終端的路徑.
現(xiàn)在切換到Xcode stderr窗口中, hello debugger
這些單詞應(yīng)該已經(jīng)出現(xiàn)了.你將使用同樣的方法將Xcode’s stderr這個窗口作為輸出的管道.
最后, 關(guān)閉第三個沒有命名的終端的窗口, 并返回到LLDB選項卡中.
在LLDB選項卡中, 將以下命令鍵入到LLDB中:
(lldb) file /Applications/Xcode.app/Contents/MacOS/Xcode
這條命令會將Xcode設(shè)置為可執(zhí)行的目標(biāo)文件.
注意:如果你是用的是之前發(fā)布的Xcode的版本, Xcode的名字和路徑可能會有所不同.
你可以通過在Terminal鍵入以下命令來查看你當(dāng)前運行的Xcode的路徑.
$ ps -ef `pgrep -x Xcode`
如果你的到了Xcode的路徑, 用新的路徑來替換
現(xiàn)在從LLDB中啟動Xcode進程, 在Xcode stderr 選項卡中鍵入以下命令, 并將/dev/ttys027
用tty
命令得到的路徑替換掉.
(lldb) process launch -e /dev/ttys027 --
啟動參數(shù)e
指定了stderr的位置.常見的日志功能, 比如 Objective-C的NSLog或者Swift的print函數(shù), 輸出到stderr-是的沒錯, 不是stdout! 稍后你將會打印自己的日志到stderr.
過一會兒xcode就會啟動. 切換到Xcode并且選擇File\New\Project....然后選擇, iOS\Application\Single View Application并且點擊Next.將工程命名為Hello Debugger.確保選擇了Swift作為程序語言,并且沒有選擇Unit 和 UI tests中的任意一項.點擊Next并且將工程保存到你希望的位置.
現(xiàn)在你有了一個新的Xcode的工程. 整理一下程序的窗口以便你可以同時看到Xcode和Terminal.
在Xcode中打開
ViewController.swift
.
注意: 你可能會留意到Xcode stderr終端窗口中有一些輸出.這是由于Xcode的作者通過NSLog或者其他stderr 控制臺打印函數(shù)輸出的日志.
點擊一下找到一個類
現(xiàn)在Xcode已經(jīng)設(shè)置好了,而且終端調(diào)試窗口也已經(jīng)正確的創(chuàng)建并擺放在正確的位置上, 是時候開始使用調(diào)試的help來瀏覽Xcode了.
在調(diào)試的過程中, Cocoa SDK的知識會有極大的幫助.例如, [NSView hitTest:]
是一個在run loop中返回響應(yīng)當(dāng)前點擊或者手勢操作的類的非常有用的方法.這個方法首先得到觸發(fā)時包含的NSView 并且最大程度的遞歸可以處理此次觸摸事件的的子視圖.你可以使用這些Cocoa SDK來幫助找出你點擊的視圖的類.
在LLDB選項卡中, 鍵入Ctrl + C來暫定調(diào)試器, 鍵入:
(lldb) breakpoint set -n "-[NSView hitTest:]"
Breakpoint 1: where = AppKit`-[NSView hitTest:], address =
0x000000010338277b
這是即將學(xué)習(xí)的許多斷點中的第一個.你將在第四章'Stopping in Code'中學(xué)習(xí)到如何創(chuàng)建,修改和刪除斷點, 但是現(xiàn)在只需要簡單的知道你在-[NSView hitTest:]
中創(chuàng)建了一個斷點.
Xcode現(xiàn)在因為調(diào)試器而暫停了. 鍵入以下命令繼續(xù)運行程序:
(lldb) continue
在Xcode窗口中點擊任意位置Xcode將會立即暫停這表明LLDB觸發(fā)了一個斷點.
那個
hitTest:
斷點被觸發(fā)了.你可以通過檢查CPU注冊表的RDI
來檢查哪一個視圖被點擊了.將他打印在LLDB中:
(lldb) po $rdi
這個命令要求LLDB
打印出匯編寄存器中的內(nèi)存地址引用的對象的內(nèi)容.
好奇為什么這個命令是po
?po
的含義是print object
.這里也可以使用p
簡單的打印出RDI的內(nèi)容. po
通常情況下更有用, 它給出的是NSObject的description或者debugDescription(如果可用的話).
如果你想將你的調(diào)試能力提升一個等級的話,匯編是一個你要學(xué)的非常重要的技能. 它將讓你洞察apple的代碼, 即便是你從來沒有閱讀過源碼.它將讓你非常了解Swift編譯團隊是如何在Objective-C中用Swift跳來跳去.它將會讓你非常了解你的Apple設(shè)備是如何做每一件事的.你將會在第十章“Assembly Register Calling Convention”中學(xué)到更多關(guān)于寄存器和匯編的知識.
但是現(xiàn)在, 簡單的知道$rdi
寄存器包含著上面調(diào)用hitTest:
方法的NSView或者NSView子類的一個實例.
注意輸出可能產(chǎn)生不同的結(jié)構(gòu)這取決于你使用的Xcode的版本和你點擊的位置.他可能會給出一個xcode特有的私有類, 也有可能給出一個Cocoa中的公有類.
在LLDB選項卡中鍵入一下命令繼續(xù)運行程序:
(lldb) continue
替代繼續(xù)運行的是, Xcode將會觸發(fā)hitTest:
的另外一個斷點并且暫停執(zhí)行.這是由于hitTest:
這個方法會被所有包含在被點擊視圖的父視圖里的所有子視圖遞歸調(diào)用的事實決定的. 你可以檢查這個斷點的內(nèi)容但是這會很乏味因為Xcode里包含很多的視圖.
為重要的內(nèi)容過濾斷點
鑒于Xcode創(chuàng)建了很多的NSView實例, 你需要過濾掉那些沒用的NSView, 只關(guān)注于與你尋找的那些目標(biāo)相關(guān)的NSView上.在你需找一個唯一的對象的時候這是一個在調(diào)試過程中經(jīng)常調(diào)用的方法, 有助于你找到你想要的對象.
在Xcode8中, 你用來寫編輯代碼的地方是一個私有類NSTextView的一個子類.它就類似于UIKit中的UITextView.這個類作為可視化的界面來處理你所有的代碼, 并幫助其他私有類來編譯和創(chuàng)建你的應(yīng)用程序.
如果說你只想在點擊NSTextView實例的時候觸發(fā)斷點.你可以通過修改已有斷點的breakpoint conditions
來設(shè)置只有在NSTextView被點擊的時候才停止.
假如你前面設(shè)置的-[NSView hitTest:]
斷點還在, 并且它是LLDB會話中唯一的斷點, 你可以用下面的LLDB命令修改這個斷點:
(lldb) breakpoint modify 1 -c "(BOOL)[$rdi isKindOfClass:[NSTextView
class]]"
這條命令修改了斷點1并且設(shè)置了一個觸發(fā)條件,只有條件語句返回的Boolean值為true的時候才觸發(fā)斷點.截至目前你只有一個斷點,這就是為什么斷點號為1.
這條Boolean表達式通過isKindOfClass:
來檢查當(dāng)前的類是否是NSTextView的子類.
你的斷點經(jīng)過上面的修改以后, 在Xcode的區(qū)域里點擊. LLDB應(yīng)該會停在hitTest:
.通過下面的命令打印出當(dāng)前實例所屬的類:
(lldb) po $rdi
輸出的內(nèi)容應(yīng)該像下面的樣子:
<NSTextViewSubclass: 0x14b7a65c0>
Frame = {{0.00, 0.00}, {1089.00, 1729.00}}, Bounds = {{0.00, 0.00},
{1089.00, 1729.00}}
Horizontally resizable: NO, Vertically resizable: YES
MinSize = {1089.00, 259.00}, MaxSize = {10000000.00, 10000000.00}
上面的NSTextViewSubclass
是一個私有類的名字.在本章節(jié)余下的內(nèi)容里面你都會用到它.在輸出里里面你會得到一個唯一的指針.在這個例子中, 指針的內(nèi)存地址是0x14b7a65c0
, 但你得到的內(nèi)存地址可能是不同的.
鑒于它沒有立即顯示出它是一個NSTextView
的子類, 但是你可以通過重復(fù)的手動輸入下面的命令來查看它的父類來查看當(dāng)前實例是否是NSTextViewsubclass
的子類.
(lldb) po [$rdi superclass]
... 一直重復(fù)到你找到為止.
(lldb) po [[$rdi superclass] superclass]
等一下-那是Objective-C. 但你要想到Swift的情況. 在swift中要那樣做, 首先鍵入一下命令:
(lldb) ex -l swift -- import Foundation
(lldb) ex -l swift -- import AppKit
ex
命令是expression
的縮寫, 可以讓你執(zhí)行代碼.-l swift
告訴LLDB這是swift的代碼.這些命令告訴LLDB需要知道Foundation 和 AppKit的相關(guān)情況.
你還將用到下面的兩條命令.
輸入下面的命令, 并將0x14bdd9b50
替換成你在前面獲取到的NSTextView
子類的內(nèi)存地址.
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, to: NSObject.self)
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, to: NSObject.self) is
NSTextView
這些命令打印出了, NSTextView的子類, 并且檢查它是否是 NSTextView
的子類-但這一次使用的是swift!你將會看到類似下面的這些輸出:
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, NSObject.self)
<NSTextViewSubclass: 0x14b7a65c0>
Frame = {{0.00, 0.00}, {1089.00, 1729.00}}, Bounds = {{0.00, 0.00},
{1089.00, 1729.00}}
Horizontally resizable: NO, Vertically resizable: YES
MinSize = {1089.00, 259.00}, MaxSize = {10000000.00, 10000000.00}
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, NSObject.self) is
NSTextView
true
使用swift需要輸入更多的東西,.此外, 當(dāng)調(diào)試器在在藍色的地方停下來的時候, 或者在Objective-C 的代碼中, LLDB將默認使用Objective-C.這些是可以改變的, 但是這本書更推薦使用 Objective-C,鑒于Swift REPL可能在調(diào)試器中無緣無故的檢查出錯誤.
從現(xiàn)在開始, 你將會使用 Objective-C 的調(diào)試環(huán)境幫助控制NSTextView
.
鑒于這是一個NSTextView
的子類, 所以可以調(diào)用NSTextView
的所有方法.
輸入下面的命令:
(lldb) po [$rdi string]
這將會打印出你再Xcode中打開的文件的內(nèi)容.
甚至還可以設(shè)置text view的內(nèi)容:
(lldb) po [$rdi setString:@"http:// Yay! Debugging!"]
(lldb) po [CATransaction flush]
可以看到Xcode窗口中的內(nèi)容已經(jīng)改變了.
獲取modules中的私有類和私有方法
現(xiàn)在回到NSTextView
的子類, 選擇從NSTextView
子類開始是因為這里可能有一些額外的或者重寫了父類的方法.但是你如何去找到他們呢?Xcode中的私有類Apple是不會公開在文檔中的.
輸入下面的命令, 將NSTextViewSubclass
替換成你找到的text view的類:
(lldb) image lookup -rn 'NSTextViewSubclass\ '
這條命令可以讓你查看正在運行的二進制文件和已經(jīng)加載的所有動態(tài)庫的內(nèi)部.r
選項的含義是用正則表達式搜索. n
選項的含義是通過名字搜索函數(shù)或者符號表.
你將會看到NSTextView
子類實現(xiàn)了的方法列表.很酷是不是?如果你想要學(xué)習(xí)使用image lookup
命令查找代碼, 你可以參考第七章"Image".
用代碼塊注入切換方法
Objective-C的運行時在逆向工程中真的很有用很有幫助.你現(xiàn)在即將感受到Objective-C運行時的強大, 并且了解你可以用它在LLDB中做什么.
首先, 用頭文件導(dǎo)入的方法導(dǎo)入Foundation庫中Objective-C的運行時信息:
(lldb) po @import Foundation
盡管代碼已經(jīng)編譯過了,在Xcode內(nèi)部知道包含哪些方法, 然而LLDB進程卻不知道.通過導(dǎo)入Foundation可以讓你通過LLDB訪問Objective-C 運行時的所有內(nèi)容.
現(xiàn)在在LLDB控制臺中輸入po
指令, 不必輸入其他內(nèi)容, 就像這樣:
(lldb) po
LLDB將會進入多行模式. 你會看到下面這些輸出:
(lldb) po
Enter expressions, then terminate with an empty line to evaluate:
1:
在這里, 你可以一次輸入多行表達式來執(zhí)行. 添加下面的代碼:
@import Cocoa;
id $class = [NSObject class];
SEL $sel = @selector(init);
void *$method = (void *)class_getInstanceMethod($class, $sel);
IMP $oldImp = (IMP)method_getImplementation($method);
再次按下Return
鍵來輸入一個空行.LLDB將會按照順序執(zhí)行上面的表達式.
注意:在鍵入這些代碼的時候必須非常小心,只有在你按下`Return`以后你才會知道. 如果你出現(xiàn)了一個錯誤,所有這些代碼你都必須重新輸入, 盡管你可以通過朝上的箭頭按鈕使用歷史輸入, 確認一下你沒有忘記句子末尾的分號.
你已經(jīng)通過LLDB在內(nèi)存中創(chuàng)建了幾個變量: $class
, $sel
, 和 $oldImp
.LLDB中的變量需要$
作為前綴.其他部分就跟你在Xcode編輯代碼的時候一樣.
試著打印出一些變量來確保你正確的創(chuàng)建了它們:
(lldb) po $class
NSObject
(lldb) po $oldImp
(libobjc.A.dylib`-[NSObject init])
現(xiàn)在通過鍵入po
指令來回到LLDB的多行模式.
用imp_implementationWithBlock
函數(shù)來創(chuàng)建一個新的IMP:
id (^$block)(id) = ^id(id object) {
if ((BOOL)[object isKindOfClass:[NSView class]]) {
fprintf(stderr, "%s\n", (char *)[[[object class] description]
UTF8String]);
}
return object;
};
IMP $newImp = (IMP)imp_implementationWithBlock($block);
method_setImplementation($method, $newImp);
再次在結(jié)尾的地方輸入一個空行來告訴LLDB來處理這些表達式.
這些代碼的目標(biāo)是替換你剛剛發(fā)現(xiàn)的-[NSObject init]
方法.在默認情況下, -[NSObject init]
除了返回對象本身一外不會做任何事情.這個代碼塊檢查這個對象本事是不是NSView的一個實例.如果是, 這個對象的類就會被打印出來.
它的工作原理是這樣的:
1.你創(chuàng)建了一個持有對象指針的代碼塊.
2.這個代碼快檢查傳入的是不是NSView類型的對象.
3.如果是, 代碼塊會把view的description打印到stderr, 也就會出現(xiàn)在Xcode stderr
選項卡中.
4.代碼快返回的是執(zhí)行了被替換過的-[NSObject init]
方法的對象.在理想狀況下,會徹底的替換這些方法的實現(xiàn), 并且你可以用正確的參數(shù)簡單的執(zhí)行$oldImp
.然而, LLDB在這里卻有一個bug, 當(dāng)你用IMPs來代替block執(zhí)行的時候LLDB會閃退.
5.最后, 一個新的IMP在代碼塊里被創(chuàng)建, 并且放的實現(xiàn)也被設(shè)置到了新的IMP里.這樣你就用新的實現(xiàn)替換了-[NSObject init]
方法.
接下來, 通過鍵入continue
來繼續(xù)調(diào)試.
觀察Xcode stderr
控制臺中的輸出.檢查你通過點擊Xcode中不同的項目時創(chuàng)建的所有的類.
你也可以用這種方法將LLDB附加到任何程序上.不管是apple的程序還是你感興趣的第三方應(yīng)用程序.你可以用同樣的技巧去瀏覽他們的類名.唯一不同的是你只要改變可執(zhí)行文件的路徑即可.
我們學(xué)這些干嘛?
這是我們第一次在你沒有任何源代碼的情況下使用LLDB并且附加到其他程序上.本章節(jié)忽略了很多細節(jié), 但我們的目標(biāo)是讓你正確的調(diào)試進程.接下來還有許多章節(jié)讓你深入的了解細節(jié).