DTrace(第二十七章:DTrace?VS objc_msgSend (一))

你已經(jīng)看到了強有力的DTrace是如何破解你擁有的Objective-CSwift代碼的, 或者那些Framework中的代碼比如UIKit. 你已經(jīng)用DTrace追蹤了這些代碼并且在沒有對編譯過的代碼做任何改變的情況下做了一些有趣的改變.
不幸的是, 在DTrace在破解脫殼了的可執(zhí)行文件時, 它不能夠創(chuàng)建任何探針來動態(tài)檢查這些函數(shù).
然而在瀏覽Apple的代碼的時候, 你仍然有一個非常強大的助手在你旁邊:objc_msgSend. 在本章中你將會使用DTrace去攔截objc_msgSend的入口并且提取出所有調(diào)用Objective-C selector的類名.在這一章的末尾, 你將會用LLDB生成一個腳本 一個僅僅生成追蹤道德在主執(zhí)行文件中調(diào)用objc_msgSend的代碼的信息.

構建你的概念證明

starter文件夾中是一個叫做VCTransitions的APP, 是一個非常基礎的Objective-C/Swift應用程序, 它展示了一個普通的UINavigationControllerpush動畫, 以及一個自定義的push動畫.
打開這個Xcode項目, 用iPhone 7 Plus 模擬器構建并運行并快速的預覽一遍.

注意: 通常情況下我不關心你正在運行的軟件的具體版本, 截至目前是iOS 10. 然而這一次我要求你運行在iOS 10.3.x(或者之前的版本), 因為你即將看到的匯編在將來的版本中可能會被改變. 在本章中你可能會看到一點匯編, 但是我不能保證在最新版的iOS中它不會被改變畢竟在我寫這本書的時候我還沒看見后面發(fā)布的版本的情況.

圖片.png

這里有一些按鈕來執(zhí)行兩種push動畫, 并且這里有還有一個叫做Execute Methods的按鈕. 他將會遍歷一個給定類的所有已知的Objective-C的implemented/overriden方法. 如果這個方法沒有參數(shù), 它就會執(zhí)行這個方法.
例如, 第一個視圖控制器是以ObjCViewController顯示的.如果你點擊了Execute Methods, 它將會調(diào)用anEmptyMethod以及所有被重寫的屬性的getter方法, 因為這些方法不需要參數(shù). 現(xiàn)在, 開始愉快的學習吧!
跳轉(zhuǎn)到OjbCViewController.m文件里然后看一下這個類實現(xiàn)的IBAction方法.
在終端中創(chuàng)建一個DTrace并確保你你看到了這些方法被觸發(fā)了.
在終端中:

sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrepVCTransitions`

確保模擬器運行的是VCTransitions項目. 按下回車鍵來啟動這個壞孩子. 當DTrace需要你輸入密碼的時候請輸入你的密碼然后回到模擬器中開始點擊按鈕.你將會看到DTrace終端窗口充滿了ObjCViewController實現(xiàn)的IBAction方法.

圖片.png

現(xiàn)在, 在SwiftViewController視圖控制器點擊一個push按鈕.
盡管這是一個UIViewController的子類, 點擊IBActions, objcPID探針不會產(chǎn)生任何結果. 盡管這里有動態(tài)方法的實現(xiàn)或者重寫的SwiftViewController的方法, 并且是通過objc_msgSend執(zhí)行的, 但是實際上是Swift的代碼(即便這些事@objc橋接的方法).
你可以通過在你的DTrace腳本中增加提取任何Objective-C方法的方式確認這些內(nèi)容而且你可以檢索關鍵字cool, 它是SwiftViewController中一個變量的名字.想下面這樣:

sudo dtrace -n 'objc$target:::entry' -p `pgrep VCTransitions` | grep -icool

你可能認為這將會產(chǎn)生一些輸出因為SwiftViewController包含下面的代碼:

dynamic var coolViewDTraceTest: UIView? = nil
dynamic var coolBooleanDTraceTest: Bool = false

然而, 這個探針不會做任何事情. 你需要使用pid$target替代objc$target和打亂的的Swift的名字, 就像你在前面章節(jié)中做的那樣.因為調(diào)用objc_msgSend
可能先與Swift代碼, 這是用objc_msgSend替代objc$target探針的另外一個原因.

在stripped scheme中重復以便你剛才的操作步驟

在這個項目中包含著一個叫做Stripped VCTransitions的scheme.

圖片.png

這會運行同樣的target(可執(zhí)行文件)作為VCTransitionsAPP, 除了Xoce將會生成一個沒有包含任何調(diào)試信息的stripped build.
選擇Stripped VCTransitionsscheme, 確保它是在 iPhone 7 Plus模擬器上(系統(tǒng)版本是iOS10.3.x之前的版本)構建并運行的.
運行起來之后, 暫停應用程序并進入LLDB控制臺.搜索屬于SwiftViewController的任何代碼使用你最近創(chuàng)建的image lookup命令, 你在第22“SB Examples, Improved Lookup”中創(chuàng)建的lookup命令.(如果你跳過了那一章, 默認情況下是使用image lookup -rn).

(lldb) lookup SwiftViewController

呃....你不會觸發(fā)任何斷點.難道是Swift的bug?嘗試提取出與ObjCViewController有關的代碼:

(lldb) lookup ObjCViewController

仍然什么都沒有.發(fā)生了什么事?
這個可執(zhí)行文件已經(jīng)去掉了他的信息. 你不能使用調(diào)試中的symbols最典型的就是一個內(nèi)存中的引用.
然而, 事實上LLDB足夠智能到意識到這些函數(shù)在內(nèi)存中的位置. LLDB會為方法生成一個唯一的沒有信息的函數(shù)名.自動生成的函數(shù)名將會遵循下面的形式:

___lldb_unnamed_symbol[FUNCTION_ID]$$[MODULE_NAME]

這就意味著你可以使用下面的命令列出LLDB在VCTransitions可執(zhí)行文件中生成的所有的函數(shù):

(lldb) lookup VCTransitions

我的到了296結果, 下面就是其中的一部分:

...
___lldb_unnamed_symbol293$$VCTransitions
___lldb_unnamed_symbol294$$VCTransitions
___lldb_unnamed_symbol295$$VCTransitions
___lldb_unnamed_symbol296$$VCTransitions

該死, LLDB獲取不到這些函數(shù)的名字. 你認為DTrace可以督導精簡后的二進制文件的內(nèi)容嗎?
在終端中輸入下面的內(nèi)容:

sudo dtrace -ln 'objc$target:ObjCViewController::' -p `pgrepVCTransitions`

這條指令查詢VCTransitions進程中包含在ObjCViewController模塊中的探針的數(shù)量, 這就是DTrace引用一個Objective-C類的方式.
我獲取到下面的輸出:

 ID         PROVIDER            MODULE          FUNCTION NAME
dtrace: failed to match objc57009:ObjCViewController:: No probe matchesdescription

我可以知道我的PID是57009并且我捕獲到了0個!
如果我想確認ObjCViewController產(chǎn)生了有效的探針(正如你在前面看到的那樣), 只需要簡單的使用沒有精簡過的Xcode scheme重新構建這個項目, 然后再次運行上面的終端命令. 如果你對于證明這是有效的很感興趣, 我就把他留給你作為一個練習.

如何繞過精簡過的沒有探針的二進制文件

所以如何設計一個可以繞過這些不能夠檢查的精簡過的二進制文件的DTrace命令或者探針呢?
既然你已經(jīng)知道了Objective-C (和 動態(tài)的 Swift) 方法許啊喲通過objc_msgSend (或者類似的父類方法), 那么你就可以使用這些你已經(jīng)學過的關于objc_msgSend知識來弄清楚, 如何創(chuàng)建一個可以打印出這個類中Objective-C的selector的名字的DTrace指令.
這里有一個objc_msgSend是如何工作的快速的提示. 這個函數(shù)的生靈看起來像下面這個樣子:

objc_msgSend(instance_or_class, SEL,  ...);

所以, objc_msgSend需要一個類或者實例作為第一個參數(shù), Objective-C selector作為第二個參數(shù), 后面跟著的是一些參數(shù)的變量.
知道了那些之后, 如果你有下面的代碼:

UIViewController *vc = [UIViewController new];
[vc setTitle:@"yay, DTrace"];

編譯器將會把它翻譯成下面的偽代碼:

vc = objc_msgSend(UIViewControllerClassRef, "new");objc_msgSend(vc, "setTitle", @"yay, DTrace");

DTrace的角度來看, 獲取Objective-C selector 是相當輕松的.只需要copyinstr(arg1). 正如你前面學到的那樣, 這將會復制arg1中的指針, Objective-C selector(是一個 char*), 因此DTrace在內(nèi)核中可以讀到它.
現(xiàn)在看一下難點:你想要獲取到作為一個char*傳給objc_msgSend的類名.
DTrace不會讓你執(zhí)行任意的方法, 因此你可以使用Objective-C的運行時, 或者任何它實現(xiàn)的方法, 從未挖掘出你想要的信息. 取而代之的是, 你通過查看arg0實例的內(nèi)存并且自己發(fā)現(xiàn)代表著類名的char*, 然后通過DTrace腳本實現(xiàn)自動化.
嗨, 這是你DTrace技術綜合運用的高潮! 你可能同樣也想將他們都用出來.

使用DTrace重新搜索調(diào)用的方法!

讓我們看一下是否有一些成文的方法來做這些事. 在objc/runtime.h頭文件中, 你會看到這下面這些聲明:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class
OBJC2_UNAVAILABLE;
    const char *name
OBJC2_UNAVAILABLE;
    long version
OBJC2_UNAVAILABLE;
    long info
OBJC2_UNAVAILABLE;
    long instance_size
OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars
OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists
OBJC2_UNAVAILABLE;
    struct objc_cache *cache
OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols
OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

回到在64位的機器上使用Objective-C 2.0的日子里, 如果你有一個指向一個有效類X的指針,, 你可以獲取到那個在#if !__OBJC2__中描述的const char *name.

po *(char *)(X + 0x10)

不幸的是,這已經(jīng)相當陳舊了. 這個類數(shù)據(jù)結構回到之前的Objective-C 2.0的樣子. 結構和指針的位置已經(jīng)改變很久了. Apple已經(jīng)選擇為objc_class結構體使用更少的公開信息的結構布局, 這在你觀察的時候可以更愉悅.
這就意味著你需要捕獲一個帶著 Objective-C 類(或者一個類的實例)并且為這個類返回一個char*的函數(shù), 以便我們弄明白它做了哪些事情.
幸運的是, 回到objc/runtime.h頭文件中, 這里同樣有一個 class_getName函數(shù).
通過查看頭文件, 你會發(fā)現(xiàn)class_getName函數(shù)有著下面的聲明:

/**
 * Returns the name of a class.
 *
 * @param cls A class object.
 *
 * @return The name of the class, or the empty string if \e cls is \c
Nil. */
OBJC_EXPORT const char *class_getName(Class cls)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

這個函數(shù)帶著一個Class參數(shù)并且返回一個char*. 你將會用DTrace去跟蹤這個方法并且查看這個類調(diào)用了哪些方法.
能讓我們看到希望的是, 你的VCTransitionsAPP仍然在運行. 如果沒有, 則重新運行這個程序. 在運行起來之后, 在LLDB中暫停這個程序.
獲取UIView Class的引用:

(lldb) p/x [UIView class]

你將會得到一些下面的輸出:

(Class) $0 = 0x0000000109d4ce60 UIView

這個引用是從UIView這個類里獲取的, 將它應用到class_getName函數(shù)中:

(lldb) po class_getName(0x0000000109d4ce60)

你將會得到一個數(shù)字?為什么是一串數(shù)字呢?

0x000000010999319f

哦, 是的. 這個函數(shù)返回了一個C char*. 你需要指明這些:

(lldb) po (char *)class_getName(0x0000000109d4ce60)

現(xiàn)在你將可以使用DTrace去追蹤class_getName調(diào)用后所有的非Objective-C方法.
跳到一個新的終端窗口中并且執(zhí)行下面的DTrace指令:

sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`

這一次, LLDB應該仍然會被暫停當設置你的DTrace腳本時.回到LLDB中然后用UIView類的引用重新執(zhí)行class_getName函數(shù). 你的UIView類額指針可能會有點不同:

(lldb) po (char *)class_getName(0x0000000109d4ce60)

在你執(zhí)行完上面的命令之后, DTrace腳本會輸出下面這些class_getName調(diào)用后的函數(shù)列表:

:~ sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
Password:
dtrace: description 'pid$target:::entry' matched 901911 probes
CPU     ID                    FUNCTION:NAME
  6 1405417              class_getName:entry
  6 1405416 objc_class::demangledName(bool):entry
  6 566986        _NSPrintForDebugger:entry
  6 1405847               objc_msgSend:entry

看起來objc_class::demangledName(bool):函數(shù)是一個值得我們?yōu)g覽的有趣的地方.
殺掉DTrace腳本. 你肯定不想它干擾到你的LLDB斷點, 因為在DTrace探針上設置一個斷點會出現(xiàn)重大的意外.在DTrace腳本中斷之后, 用LLDB在objc_class::demangledName(bool) 出設置一個斷點, 像這樣:

(lldb) b objc_class::demangledName(bool)

重新運行這個表達式, 但是告訴LLDB要重視這個斷點.

(lldb) exp -i0 -O -- class_getName([UIView class])

在你按下回車鍵之后, LLDB將會停在objc_class::demangledName(bool)函數(shù)處.

圖片.png

好好看看這些匯編.

嚇人的匯編, 第一部分

像往常一樣, 這些內(nèi)容第一眼看上去的時候非??植? 但是當你有條不紊的看一遍之后, 它并沒有想象中的那么可怕. 實際上你可以將這些匯編拆成一部分一部分的瀏覽.第一部分將會是0~55.
查看一下寄存器以便知道你在處理的內(nèi)容是什么:

(lldb) po $rdi

你將會得到一個nil. 這是bool參數(shù). 而nil是0, 因此這里就是false.
是時候拆解一下這些內(nèi)容了. 在這里涉及到的偏移量就是中括號里的這些值. 因此偏移13就是<+13>.

圖片.png

? Offset 13: 在這一行之后, 函數(shù)序言就執(zhí)行完畢了. 是在這個函數(shù)中實際應用一下了.
? Offset 17: 這將esi賦值給r12d. 這就是傳進來的Boolean值.我們之前查看的rsi并且看到了它是0, 因此r12d將會同樣是0.
? Offset 20: rdi包含著UIView類的引用并且賦值給了r15.
? Offset 33:這與r15的偏移量是0x20的值并且解引用. 也就是說rax = (*([UIView class] + 0x20)).
? Offset 37:這個值存儲在rax 是用0x7ffffffffff8AND'd(可能是操作)然后存儲到了rax.
? Offset 48:這個值是rax偏移0x38后的值然后解引用并存儲在rbx. 也就是說, rbx = *(rax + 0x38).
? Offset 52-55: 檢查rab是否是0. 如果它返回一個非零的數(shù)字, 然后跳到<+310>結束這個函數(shù), 在函數(shù)結束之前是正確的.
如果這個檢查偏移量55的值失敗了(也就是說, 如果rbx是0 ), 執(zhí)行將會矩形下一句湖邊指令, <+61>.
偏移量在0~55的邏輯是負責將一個Objective-C的類作為一個char*返回給你 如果(并且僅僅只在如果)那個類已經(jīng)被正確的加載之后. 這通常發(fā)生在那個類里面至少有一個方法(也就是說, 那個方法在那個類中必須被實現(xiàn)或者重寫)被執(zhí)行了.
例如, 如果一個新類被調(diào)用了, 然而在你的進程存活期間還沒有執(zhí)行任何初始化, 偏移量在0~55之間的邏輯將會返回nil. 稍后你將會構建一個command regex來確認這點.
看這些匯編, 你可以推斷出下面的內(nèi)容.
如果你有一個已經(jīng)初始化的X類的實例, 并且如果你將X偏移了0x20然后引用它, 輸出的內(nèi)容看起來應該是下面這個樣子:

*(uint64_t *)(X + 0x20)

然后你用0x7ffffffffff8 按位這個值:

*(uint64_t *)(X + 0x20) & 0x7ffffffffff8

接下來, 使用這個值, 用0x38偏移這個值然后解引用:

*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)

這是最終的地址, 因此你只需要將它輸出到正確的類型里, 一個char *:

(char *)*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)

現(xiàn)在, 如果你有一個NSObject的引用, 你從第21章 “ScriptBridging with SBValue & Language Contexts” 中了解到這個對象起始位置的內(nèi)存地址指向它自己(就是那個isa指針) . 如果你不理解那些, 回過頭去重新閱讀第21章-- 否則這一章剩下的內(nèi)容將會變得更刺激.
將所有這些內(nèi)容放在一起, 將一個實例的類名作為一個char*獲取, 看這個怪物:

(char *)*(uint64_t *)((*(uint64_t *)((*(uint64_t *)Instance_of_X) + 0x20)& 0x7ffffffffff8) + 0x38)

是的, 你可以將這條指令復制到LLDB中來確認一下它是OK的!

注意: 我會再重復一次: 這不適用于還沒有被初始化的Objective-C的類. 這里有一個你為什么使用UIView的原因, 因為如果你可以在你的屏幕上看到UI, 然后UIView類已經(jīng)明確的被初始化了, 至少有一個UIView被初始化了.

在LLDB中, 看一下UIView 類:

(lldb) p/x [UIView class]
(Class) $1 = 0x000000010c09ce60 UIView

你將會得到一個不同的地址. 將它復制到你的剪切板中:
取到那個地址并將它偏移0x20然后查看那個位置的內(nèi)存:

(lldb) x/gx '0x000000010c09ce60 + 0x20'

你將會得到一些值:

0x10c09ce80: 0x0000608000064b80

0x7ffffffffff8(that's 10 f's)按位那個值:

(lldb) p/x 0x7ffffffffff8 & 0x0000608000064b80

你將會得到另一個數(shù)字:

0x0000608000064b80

取到那個數(shù)字, 將它偏移0x38然后解引用:

(lldb) x/gx '0x0000608000064b80 + 0x38'

你將會得到一些下面的輸出:

0x608000064bb8: 0x000000010bce319f

觀察一下0x000000010bce319f(至少在我這里是這個地址)的值是否包含char*指針.

(lldb) po (char *)0x000000010bce319f

如果一切順利的話, 你將會得到一個代表UIView的char*:

圖片.png

創(chuàng)建一個新的regex command來確認一下我告訴你的都是真的. 只需要將這些輸入到控制臺中; 不需要將這些放到你的~/.lldbinit文件里:

command regex getcls 's/(.+)/expression -lobjc -O -- (char *)*(uint64_t*)((*(uint64_t *)((*(uint64_t *)%1) + 0x20) & 0x7ffffffffff8) + 0x38)/'

這條指令會從已經(jīng)加載到你的進程里的任何實例上抓取char*類名.
在你將這條指令輸入到LLDB控制臺之后, 用它驗證一下之前OK的UIView:

(lldb) getcls [UIView new]

現(xiàn)在看一下還沒有被初始化或者還沒有執(zhí)行任何方法的那些類, 比如UIAlertController:

(lldb) getcls [UIAlertController new]

你將會得到一個nil, 因為這個類還沒有執(zhí)行可以唯一標示這個類的任何代碼.

(lldb) po [UIAlertController class]

重新執(zhí)行getcls命令:

(lldb) getcls [UIAlertController new]

現(xiàn)在你將會得到一個的代表UIAlertControllerchar*類型的引用.記住如果這個類獨有的方法被執(zhí)行了, Objective-C運行時就會加載這個類.
現(xiàn)在, 這個類方法(i.e. -[NSObject class])不是UIAlertController獨有的, 但是猜一下什么是他獨有的?
你正在po這個對象然而debugDescriptiondescription方法是這個類獨有(重寫)的!
因此, 在po一個UIAlertController類的時候, 它會被加載到運行時里!
如果你有任何疑惑的話在UIAlertController上運行你在第十四章“DynamicFrameworks”中的自定義命令, 方法來確認一下.

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容