通過逆向深入理解 Block 的內(nèi)存模型

這是我博客上的文章鞍瞻!尽纽!大家去看我博客啊咐蚯,我簡書上不更新了啊E摺4悍妗!快去吧2畎肌F诒肌!
原文地址:http://www.swiftyper.com/2016/12/16/debuging-objective-c-blocks-in-lldb/

自從對 iOS 的逆向初窺門徑后危尿,我也經(jīng)常通過它來分析一些比較大的應用呐萌,參考一下這些應用中某些功能的實現(xiàn)。這個探索的過程樂趣多多谊娇,不僅能滿足自己對未知的好奇心肺孤,還經(jīng)常能發(fā)現(xiàn)一些意外的驚喜。

正常情況下济欢,通過分析界面以及 class-dump 出來頭文件就能對某個功能的實現(xiàn)猜個八九不離十赠堵。但是 Block 這種特殊的類型在頭文件中是看不出它的聲明的,一些有 Block 回調(diào)的方法名 dump 出來是類似這樣的:

- (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(CDUnknownBlockType)arg3;

因為這種回調(diào)看不到它的方法簽名法褥,我們無法知道這個 Block 到底有幾個參數(shù)茫叭,也不知道它函數(shù)體的具體地址,因此在使用 lldb 進行動態(tài)調(diào)試的時候也是困難重重半等。我也一度被這個困難所阻擋揍愁,以為調(diào)用到有 Block 的方法就是進了死胡同呐萨,沒辦法繼續(xù)跟蹤下去了。我還因此放棄過好幾次對某個功能的分析吗垮,特別受挫垛吗。

好在凹髓,我們還有 Google 這個強大的武器烁登。沒有什么問題是一次 Google 不能解決的。如果有蔚舀,那就兩次饵沧。

這篇文章就來講講如何通過 Block 的內(nèi)存模型來分析出它的函數(shù)體地址,以及函數(shù)簽名赌躺。

Block 的內(nèi)存結(jié)構(gòu)

在 LLVM 文檔中狼牺,可以看到 Block 的實現(xiàn)規(guī)范,其中最關鍵的地方是對于 Block 內(nèi)存結(jié)構(gòu)的定義:

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)
    } *descriptor;
    // imported variables
};

可以看到第一個成員是 isa礼患,說明了 Block 在 Objective-C 當中也是一個對象是钥。我們重點要關注的就是 void (*invode)(void *, ...); 和 descriptor 中的 const char *signature,前者指向了 Block 具體實現(xiàn)的地址缅叠,后者是表示 Block 函數(shù)簽名的字符串悄泥。

實戰(zhàn)

注:本篇文章都是在 64 位系統(tǒng)下進行分析,如果是 32 位系統(tǒng)肤粱,整型與指針類型的大小都是與 64 位不一致的弹囚,請自行進行修改。

知道了 Block 的內(nèi)存模型后领曼,就可以直接打開 hopper 和 lldb 進行調(diào)試了鸥鹉。

我這里使用了邏輯思維的得到 APP 作為分析的例子。順便說一句庶骄,得到上面的內(nèi)容都相當不錯毁渗,很多付費專欄的內(nèi)容都是很贊的,值得一看单刁。

準備

設備:iPhone 5s iOS 8.2 越獄

usbmuxd

$ tcprelay -t 22:2222 1234:1234
Forwarding local port 2222 to remote port 22
Forwarding local port 1234 to remote port 1234
......

ssh 到 iOS 設備并啟動 debugserver:

$ ssh root@localhost -p 2222
iPhone $ debugserver *:1234 -a "LuoJiFM-IOS"
ebugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for arm64.
Attaching to process LuoJiFM-IOS...
Listening to port 1234 for a connection from *...

本地打開 lldb 并遠程附加進程祝蝠,進行動態(tài)調(diào)試:

$ lldb
(lldb) process connect connect://localhost:1234

找到偏移地址:

(lldb) image list -o -f 
[  0] 0x0000000000074000 /private/var/mobile/Containers/Bundle/Application/D106C0E3-D874-4534-AED6-A7104131B31D/LuoJiFM-IOS.app/LuoJiFM-IOS(0x0000000100074000)
[  1] 0x000000000002c000 /Users/wordbeyond/Library/Developer/Xcode/iOS DeviceSupport/8.2 (12D508)/Symbols/usr/lib/dyld

在 Hopper 下找到需要斷點的地址:

Breakpoint Address

下斷點:

(lldb) br s -a 0x0000000000074000+0x0000000100069700
Breakpoint 2: where = LuoJiFM-IOS`_mh_execute_header + 407504, address = 0x00000001000dd700

然后在應用中點擊訂閱 Tab ,此時會命中斷點(如果沒有命中幻碱,手動下拉刷新下)绎狭。

眾所周知,Objective-C 方法的調(diào)用都會轉(zhuǎn)化成 objc_msgSend 調(diào)用褥傍,因此單步的時候看到 objc_msgSend 就可以停下來了:

->  0x1000dd71c <+431900>: bl     0x100daa2bc               ; symbol stub for: objc_msgSend
    0x1000dd720 <+431904>: mov    x0, x20
    0x1000dd724 <+431908>: bl     0x100daa2ec               ; symbol stub for: objc_release
    0x1000dd728 <+431912>: mov    x0, x21
(lldb) po $x0
<DataServiceV2: 0x17400cea0>

(lldb) po (char *)$x1
"FM_GetSubscribeList:pageSize:callBack:"

(lldb) po $x4
<__NSStackBlock__: 0x16fd88f88>

可以看到儡嘶,第四個參數(shù)是個 StackBlock 對象,但是 lldb 只為我們打印出了它的地址恍风。接下來蹦狂,就靠我們自己來找出它的函數(shù)體地址和函數(shù)簽名了誓篱。

找出 Block 的函數(shù)體地址

要找出 Block 的函數(shù)體地址很簡單,根據(jù)上面的內(nèi)存模型凯楔,我們只到找到 invoke 這個函數(shù)指針的地址窜骄,它指向的就是這個 Block 的實現(xiàn)。

在 64 位系統(tǒng)上摆屯,指針類型的大小是 8 個字節(jié)邻遏,而 int 是 4 個字節(jié),如下:

Block struct members size

因此准验,invoke 函數(shù)指針的地址就是在第 16 個字節(jié)之后。我們可以通過 lldb 的 memory 命令來打印出指定地址的內(nèi)存廷没,我們上面已經(jīng)得到了 block 的地址,現(xiàn)在就打印出它的內(nèi)存內(nèi)容:

(lldb) memory read --size 8 --format x 0x16fd88f88
0x16fd88f88: 0x000000019b4d8088 0x00000000c2000000
0x16fd88f98: 0x00000001000dd770 0x0000000100fc6610
0x16fd88fa8: 0x000000017444c510 0x0000000000000001
0x16fd88fb8: 0x000000017444c510 0x0000000000000008

如前所述另锋,函數(shù)指針的地址是在第 16 個字節(jié)之后夭坪,并占用 8 個字節(jié),所以可以得到函數(shù)的地址是 0x00000001000dd770竞惋。

有了函數(shù)地址之后灰嫉,就可以對這個地址進行反匯編:

(lldb) disassemble --start-address 0x00000001000dd770
LuoJiFM-IOS`_mh_execute_header:
->  0x1000dd770 <+431984>: stp    x28, x27, [sp, #-96]!
    0x1000dd774 <+431988>: stp    x26, x25, [sp, #16]
    0x1000dd778 <+431992>: stp    x24, x23, [sp, #32]
    0x1000dd77c <+431996>: stp    x22, x21, [sp, #48]
    0x1000dd780 <+432000>: stp    x20, x19, [sp, #64]
    0x1000dd784 <+432004>: stp    x29, x30, [sp, #80]
    0x1000dd788 <+432008>: add    x29, sp, #80              ; =80
    0x1000dd78c <+432012>: mov    x22, x3

也可以直接在 lldb 當中下斷點:

(lldb) br s -a 0x00000001000dd770
Breakpoint 3: where = LuoJiFM-IOS`_mh_execute_header + 407616, address = 0x00000001000dd770

再次運行函數(shù)拆宛,就可以進到回調(diào)的 Block 函數(shù)體內(nèi)了。

但是讼撒,大多數(shù)情況下浑厚,我們并不需要進到 Block 函數(shù)體內(nèi)。在寫 tweak 的時候根盒,我們更需要的是知道這個 Block 回調(diào)給了我們哪些參數(shù)钳幅。

接下來,我們繼續(xù)進行探索炎滞。

找出 Block 的函數(shù)簽名

要找出 Block 的函數(shù)簽名敢艰,需要通過 descriptor 結(jié)構(gòu)體中的 signature 成員,然后通過它得到一個 NSMethodSignature 對象册赛。

首先钠导,需要找到 descriptor 結(jié)構(gòu)體震嫉。這個結(jié)構(gòu)體在 Block 中是通過指針持有的,它的位置正好在 invoke 成員后面牡属,占用 8 個字節(jié)票堵。可以從上面的內(nèi)存打印中看到 descriptor 指針的地址是 0x0000000100fc6610逮栅。

接下來悴势,就可以通過 descriptor 的地址找到 signature 了。但是证芭,文檔指出并不是每個 Block 都是有方法簽名的瞳浦,我們需要通過 flags 與 block 中定義的枚舉掩碼進行與判斷担映。還是在剛剛的 llvm 文檔中废士,我們可以看到掩碼的定義如下:

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),
};

再次使用 memory 命令打印出 flags 的值:

(lldb) memory read --size 4 --format x 0x16fd8a958
0x16fd8a958: 0x9b4d8088 0x00000001 0xc2000000 0x00000000
0x16fd8a968: 0x000dd770 0x00000001 0x00fc6610 0x00000001

由于 ((0xc2000000 & (1 << 30)) != 0),因此我們可以確定這個 Block 是有簽名的蝇完。

雖然在文檔中指出并不是每個 Block 都有函數(shù)簽名的官硝。但是我們可以在 Clang 源碼 中的 CGBlocks.cpp 查看 CodeGenFunction::EmitBlockLiteralbuildGlobalBlock 方法,可以看到每個 Block 的 flags 成員都是被默認設置了 BLOCK_HAS_SIGNATURE短蜕。因此氢架,我們可以推斷,所有使用 Clang 編譯的代碼中的 Block 都是有簽名的朋魔。

為了找出 signature 的地址岖研,我們還需要確認這個 Block 是否擁有 copy_helperdisponse_helper 這兩個可選的函數(shù)指針。由于 ((0xc2000000 & (1 << 25)) != 0)警检,因此我們可以確認這個 Block 擁有剛剛提到的兩個函數(shù)指針孙援。

現(xiàn)在可以總結(jié)下:signature 的地址是在 descriptor 下偏移兩個 unsiged long 和兩個指針后的地址,即 32 個字節(jié)后∩鹊瘢現(xiàn)在讓我們找出它的地址拓售,并打印出它的字符串內(nèi)容:

(lldb) memory read --size 8 --format x 0x0000000100fc6610
0x100fc6610: 0x0000000000000000 0x0000000000000029
0x100fc6620: 0x00000001000ddb64 0x00000001000ddb70
0x100fc6630: 0x0000000100dfec18 0x0000000000000001
0x100fc6640: 0x0000000000000000 0x0000000000000048

(lldb) p (char *)0x0000000100dfec18
(char *) $4 = 0x0000000100dfec18 "v28@?0q8@"NSDictionary"16B24"

看到這一串亂碼是不是覺得有點崩潰,折騰了半天镶奉,怎么打印出這么一串鬼東西础淤,雖然里面有一個熟悉的 NSDictionary,但是其它的東西完全看不懂啊哨苛。

不要慌鸽凶,這確實就是一個函數(shù)簽名,只是我們需要通過 NSMethodSignature 找出它的參數(shù)類型:

(lldb) po [NSMethodSignature signatureWithObjCTypes:"v28@?0q8@\"NSDictionary\"16B24"]
<NSMethodSignature: 0x174672940>
    number of arguments = 4
    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}
    argument 1: -------- -------- -------- --------
        type encoding (q) 'q'
        flags {isSigned}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding (@) '@"NSDictionary"'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
            class 'NSDictionary'
    argument 3: -------- -------- -------- --------
        type encoding (B) 'B'
        flags {}
        modifiers {}
        frame {offset = 24, offset adjust = 0, size = 8, size adjust = -7}
        memory {offset = 0, size = 1}

注意建峭,字符串中的雙引號需要對其進行轉(zhuǎn)義玻侥。

對我們最有用的 type encoding 字段,這些符號對應的解釋可以參考 Type Encoding 官方文檔迹缀。

所以使碾,總結(jié)來講就是:這個方法沒有返回值蜜徽,它接受四個參數(shù),第一個是 block (即我們自己的 block 的引用)票摇,第二個是 (long long) 類型的拘鞋,第三個是一個 NSDictionary 對象,第四個是一個 BOOL 值矢门。

最終盆色,我們得到了這個 Block 的函數(shù)參數(shù)。最初提到的那個方法簽名的完整版就是:

- (void)FM_GetSubscribeList:(long long)arg1 pageSize:(long long)arg2 callBack:(void (^)(long long, NSDictionary *, BOOL)arg3;

小結(jié)

因為想使用真實的例子進行演示祟剔,所以本文直接使用逆向的動態(tài)分析進行說明隔躲。其實上面提到的所有過程,都可以直接在 Xcode 通過自己寫的代碼進行操作物延。通過自己動手分析一遍宣旱,比看十篇文章來得更有效果。下次如果面試再有人問到 Block 的實現(xiàn)和內(nèi)存模型叛薯,你就可以跟它侃侃而談了浑吟。

參考文章

Tutorial: An In-Depth Guide To Objective-C Block Debugging

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市耗溜,隨后出現(xiàn)的幾起案子组力,更是在濱河造成了極大的恐慌,老刑警劉巖抖拴,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燎字,死亡現(xiàn)場離奇詭異,居然都是意外死亡阿宅,警方通過查閱死者的電腦和手機候衍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來家夺,“玉大人脱柱,你說我怎么就攤上這事±觯” “怎么了榨为?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長煌茴。 經(jīng)常有香客問我随闺,道長,這世上最難降的妖魔是什么蔓腐? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任矩乐,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘散罕。我一直安慰自己分歇,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布欧漱。 她就那樣靜靜地躺著职抡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪误甚。 梳的紋絲不亂的頭發(fā)上缚甩,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音窑邦,去河邊找鬼擅威。 笑死,一個胖子當著我的面吹牛冈钦,可吹牛的內(nèi)容都是我干的郊丛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼派继,長吁一口氣:“原來是場噩夢啊……” “哼宾袜!你這毒婦竟也來了捻艳?” 一聲冷哼從身側(cè)響起驾窟,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎认轨,沒想到半個月后绅络,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡嘁字,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年恩急,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纪蜒。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡衷恭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纯续,到底是詐尸還是另有隱情随珠,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布猬错,位于F島的核電站窗看,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏倦炒。R本人自食惡果不足惜显沈,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拉讯,春花似錦涤浇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盖彭,卻和暖如春纹烹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背召边。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工铺呵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隧熙。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓片挂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贞盯。 傳聞我的和親對象是個殘疾皇子音念,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容