iOS逆向:在任意app上開啟malloc stack追蹤記憶體來源

黑超熊貓zuik發表於2018-03-25

lldb有一個記憶體除錯工具malloc stack,開啟以後就可以檢視某個記憶體地址的malloc和free記錄,追蹤物件是在哪裡建立的。

這個工具可以列印出物件建立的堆疊,而在逆向時,也經常需要追蹤某些方法的呼叫棧,如果可以隨時列印出某個物件的建立記錄,也就能直接找到其所在的類和方法,不用再花費大量的時間去打log和動態除錯追蹤了。

malloc stack

在自己的專案中,要開啟malloc stack,需要在Product->Scheme->Edit Scheme->Diagnistic裡勾選Malloc Stack選項。

效果如下。

測試程式碼:

- (IBAction)create:(id)sender {
    NSString *testString = [NSString stringWithFormat:@"string created by %@",self];
    
}
複製程式碼

斷點後在lldb中使用lldb.macosx.heap裡的malloc_info命令,雖然官網上說是Mac app才能用的命令,但是經測試現在在iOS上也能用了:

(lldb) p/x testString
(__NSCFString *) $3 = 0x16eac000 @"string created by <ViewController: 0x16e9d7c0>"
(lldb) command script import lldb.macosx.heap //載入lldb.macosx.heap
"malloc_info", "ptr_refs", "cstr_refs", "find_variable", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.
(lldb) malloc_info -s 0x16eac000
0x0000000016eac000: malloc(    64) -> 0x16eac000 __NSCFString.NSMutableString.NSString.NSObject.isa
stack[0]: addr = 0x16eac000, type=malloc, frames:
     [0] 0x00000000242948ab libsystem_malloc.dylib`malloc_zone_malloc + 123
     [1] 0x00000000244e3bc1 CoreFoundation`_CFRuntimeCreateInstance + 237
     [2] 0x00000000245a6ffd CoreFoundation`__CFStringCreateImmutableFunnel3 + 1657
     [3] 0x00000000244ee0f7 CoreFoundation`CFStringCreateCopy + 359
     [4] 0x00000000245a725d CoreFoundation`_CFStringCreateWithFormatAndArgumentsAux2 + 89
     [5] 0x0000000024d17dd3 Foundation`-[NSPlaceholderString initWithFormat:locale:arguments:] + 139
     [6] 0x0000000024d17cd1 Foundation`+[NSString stringWithFormat:] + 61
     [7] 0x00000000000d7343 testMallocStack`-[ViewController create:] + 97 at ViewController.m:23:28
     [8] 0x00000000287a5771 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 81
     [9] 0x00000000287a5701 UIKit`-[UIControl sendAction:to:forEvent:] + 65
     [10] 0x000000002878d61f UIKit`-[UIControl _sendActionsForEvents:withEvent:] + 447
     [11] 0x00000000287a5051 UIKit`-[UIControl touchesEnded:withEvent:] + 617
     [12] 0x00000000287a4cbf UIKit`-[UIWindow _sendTouchesForEvent:] + 647
     [13] 0x000000002879d5d7 UIKit`-[UIWindow sendEvent:] + 643
     [14] 0x000000002876e119 UIKit`-[UIApplication sendEvent:] + 205
     [15] 0x000000002876c757 UIKit`_UIApplicationHandleEventQueue + 5135
     [16] 0x0000000024599257 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 15
     [17] 0x0000000024598e47 CoreFoundation`__CFRunLoopDoSources0 + 455
     [18] 0x00000000245971af CoreFoundation`__CFRunLoopRun + 807
     [19] 0x00000000244e9bb9 CoreFoundation`CFRunLoopRunSpecific + 517
     [20] 0x00000000244e99ad CoreFoundation`CFRunLoopRunInMode + 109
     [21] 0x0000000025763af9 GraphicsServices`GSEventRunModal + 161
     [22] 0x00000000287d5fb5 UIKit`UIApplicationMain + 145
     [23] 0x00000000000d7587 testMallocStack`main + 107 at main.m:14:9
     [24] 0x000000002419c873 libdyld.dylib`start + 3
     [25] 0x000000003a9c0001 libsystem_pthread.dylib`_thread + 1
複製程式碼

這個工具是繼承自gdb的malloc_history,不過malloc_history只能用在模擬器上,而malloc_info在模擬器和真機上都可以使用。另外,新版Xcode又增加了一個新的lldb工具memory history,在Product->Scheme->Edit Scheme->Diagnistic裡勾選Address Sanitizer即可,效果類似。

使用非官方版的heap.py

注意,在Xcode8.3以後使用malloc_info會導致lldb偵錯程式crash,似乎是出bug了,一直沒修復。在Xcode8.2上可以正常使用。

所以我們需要替換一下lldb自帶的lldb.macosx.heap模組。使用這個非官方的版本:heap.py

lldb可以載入自定義的pthon指令碼。只需要在lldb中輸入:

command script import python指令碼的地址
複製程式碼

因此把上面的heap.py下載到本地後,輸入:

command script import /你的路徑/lldb/examples/darwin/heap_find/heap.py
複製程式碼

即可。

在任意app上開啟malloc stack

Address Sanitizermemory history需要重新編譯app,但是malloc stack只需要在app啟動前設定環境變數MallocStackLoggingMallocStackLoggingNoCompact即可。開啟後會在系統的/tmp目錄下生成一個.index檔案,這個檔案裡的內容是依賴於app的執行時環境的,程式退出以後這個檔案也就沒用處了。

那麼,現在的問題就變成了如何給app設定啟動環境變數。

方法一:execve

這是我一開始使用的方法。使用execve函式來執行app的二進位制檔案。

由於沙盒的限制,需要讓app擁有root許可權才能使用execve。步驟如下。

1.重簽名ipa

重簽名需要逆向的app。因為需要對app內容作出修改。重簽名後安裝到越獄裝置上。

2.移動app到系統app目錄下,修改許可權

只有系統目錄下的app才有root許可權。

假設需要逆向的app是YOUR_APP.app。把app移動到系統app目錄下:mv -f /var/containers/Bundle/Application/xxxxxxxxxxxxx/YOUR_APP.app /Applications/YOUR_APP.app

然後修改檔案許可權:

cd /Applications

chown -R root:wheel YOUR_APP.app

chmod 4755 YOUR_APP.app/YOUR_APP

移動後,用uicache重新整理app圖示,用killall SpringBoard重啟SpringBoard

3.使用載入程式啟動app

最終的目的就是使用載入程式用execve啟動app,在啟動前設定環境變數。

首先重新命名原來的二進位制檔案:mv YOUR_APP.app/YOUR_APP YOUR_APP.app/YOUR_APP_Orig

然後製作載入程式,隨便建立一個iOS工程,替換main.m裡的內容為:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSString* string = [[NSBundle mainBundle] pathForResource:@"YOUR_APP_Orig" ofType:nil];//YOUR_APP_Orig是所要啟動的二進位制檔名
        argv[0] = (char*)[string UTF8String];
        char *envp[] =
        {
            "HOME=/var/root",
            "LOGNAME=root",
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11:/usr/games",
            "USER=root",
            "MallocStackLogging=1",
            "MallocStackLoggingNoCompact=1"
            0
        };
        execve([string UTF8String], argv, envp);
        return 0;
    }
}
複製程式碼

編譯後,取出二進位制檔案,重新命名為YOUR_APP,複製到越獄裝置的/Application/YOUR_APP.app/目錄下。

給載入程式設定執行許可權:chmod +x /Application/YOUR_APP.app/YOUR_APP

最後重啟SpringBoard:killall SpringBoard

這樣,每次啟動app就都會使用載入程式間接啟動app。

缺點

  • 步驟繁瑣。
  • 有些app重簽名很麻煩。
  • 越獄後的系統分割槽容量很小,很容易就被佔滿了,想要測試大一點的app就麻煩了。
  • 無法使用debugserver喚醒app,除錯啟動過程。因為YOUR_APPYOUR_APP_Orig是兩個程式,第一個在execve執行完就退出了。
  • 把app放到系統目錄下有時候會引起crash。

方法2:debugserver引數

方法1實在是太麻煩了,有時候遇上重簽名失敗的app就更麻煩了。但其實還有另一個更直接的方法。就是使用debugserver的命令。

debugserver是動態除錯工具,參考:IOS平臺lldb動態除錯介紹

安裝好後,在越獄裝置上輸入debugserver *:1234 /var/containers/Bundle/Application/589822B6-BFDA-4A3D-A71C-AD0D30BA6077/WeChat.app/WeChat就能喚醒app進行除錯。

但是網上的教程都沒有提到,其實debugserver還有一個隱藏的引數--env(-env,-e都可以),就是用來設定程式的環境變數的:

debugserver *:1234 /var/containers/Bundle/Application/589822B6-BFDA-4A3D-A71C-AD0D30BA6077/WeChat.app/WeChat -env MallocStackLogging=1 -env MallocStackLoggingNoCompact=1

當時我想debugserver會不會有設定環境變數的功能,沒想到隨便試了個-env就成功了。後來在debugserver的原始碼裡也發現了它的存在:debugserver.cpp(搜尋g_long_options可以找到env)。

這樣,即使app沒有重簽名,也可以直接除錯了。

缺點

debugserver無法啟動除錯extension app,因為extension app是依賴於宿主app而存在的,不能單獨執行。這種情況就只能使用方法1了。

測試

這裡使用一個重簽名,並且恢復了符號表的微信進行測試。

比如找到微信檢視錶情的介面,列印出記憶體地址為0x108795c20

<MMEmoticonView: 0x108795c20; frame = (276.25 404.25; 215.5 215.5); autoresize = LM+RM+TM+BM; layer = <CALayer: 0x170828700>>
複製程式碼

第一次使用malloc_info需要在lldb裡匯入lldb.macosx.heap,這裡需要匯入非官方版本的heap.py

(lldb) command script import heap.py的路徑
"malloc_info", "ptr_refs", "cstr_refs", "find_variable", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.
複製程式碼

使用malloc_info列印建立堆疊:

(lldb) malloc_info -s 0x108795c20
0x0000000108795c20: malloc(   480) -> 0x108795c20 MMEmoticonView.UIView.UIResponder.NSObject.isa
stack[0]: addr = 0x108795c20, type=malloc, frames:
     [0] 0x000000018374e0ac libsystem_malloc.dylib`calloc + 40
     [1] 0x000000018318b624 libobjc.A.dylib`class_createInstance + 76
     [2] 0x0000000183199ae4 libobjc.A.dylib`_objc_rootAlloc + 52
     [3] 0x00000001026d8fd4 WeChat`-[MMImageBrowseView InitEmoticonView:] + 432
     [4] 0x000000010245e950 WeChat`-[MMEmotionMsgBrowseViewController initImageViewWithFrame:] + 404
     [5] 0x000000010245ea74 WeChat`-[MMEmotionMsgBrowseViewController setupImageView] + 156
     [6] 0x000000010245e024 WeChat`-[MMEmotionMsgBrowseViewController initView] + 224
     [7] 0x000000010245d76c WeChat`-[MMEmotionMsgBrowseViewController viewDidLoad] + 112
     [8] 0x000000018a5f7924 UIKit`-[UIViewController loadViewIfRequired] + 1056
     [9] 0x000000018a60f4b4 UIKit`-[UIViewController __viewWillAppear:] + 132
     [10] 0x00000001026e05f8 WeChat`-[MMUIViewController beginAppearanceTransition:animated:] + 92
     [11] 0x000000018a7975b4 UIKit`-[UINavigationController _startCustomTransition:] + 1136
     [12] 0x000000018a6afe74 UIKit`-[UINavigationController _startDeferredTransitionIfNeeded:] + 676
     [13] 0x000000018a6afadc UIKit`-[UINavigationController __viewWillLayoutSubviews] + 64
     [14] 0x000000018a6afa40 UIKit`-[UILayoutContainerView layoutSubviews] + 188
     [15] 0x000000018a5f4a80 UIKit`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1196
     [16] 0x0000000187aa29d8 QuartzCore`-[CALayer layoutSublayers] + 148
     [17] 0x0000000187a974cc QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 292
     [18] 0x0000000187a9738c QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 32
     [19] 0x0000000187a143e0 QuartzCore`CA::Context::commit_transaction(CA::Transaction*) + 252
     [20] 0x0000000187a3ba68 QuartzCore`CA::Transaction::commit() + 512
     [21] 0x0000000187a3c488 QuartzCore`CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 120
     [22] 0x00000001846f60c0 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
     [23] 0x00000001846f3cf0 CoreFoundation`__CFRunLoopDoObservers + 372
     [24] 0x00000001846f4180 CoreFoundation`__CFRunLoopRun + 1024
     [25] 0x00000001846222b8 CoreFoundation`CFRunLoopRunSpecific + 444
     [26] 0x00000001860d6198 GraphicsServices`GSEventRunModal + 180
     [27] 0x000000018a6627fc UIKit`-[UIApplication _run] + 684
     [28] 0x000000018a65d534 UIKit`UIApplicationMain + 208
     [29] 0x00000001000ebea4 WeChat`-[WATemplateMsgMngSwitchCell .cxx_destruct] + 372
     [30] 0x00000001836055b8 libdyld.dylib`start + 4
複製程式碼

這樣就直接找到表情介面所在的類,以及在哪裡初始化了。

這樣的話,只要能找到一個物件,就能快速定位到其所在模組。比原來打log,打斷點一步步回溯高效多了。

恢復符號表

建議在對app重簽名時恢復符號表。恢復符號表後,就能直接在堆疊中看到方法名,免去了計算偏移量然後在hopper裡查詢的麻煩。

參考:iOS符號表恢復&逆向支付寶, restore-symbol

其他幾個除錯命令

ptr_refs

可以在記憶體中找出哪些地址引用了某個指標,也就相當於檢視某個變數在哪裡被引用。

cstr_refs

在記憶體中尋找某個C String在哪裡被引用。

find_variable

在當前棧幀上尋找某個區域性變數在哪裡被引用。

objc_refs

在記憶體中尋找某個類的例項。

轉到Xcode中除錯

如果想要在Xcode中除錯並開啟malloc stack,則需要先用debugserver啟動app,在終端的lldb裡連線上以後,再用process detach斷開連線。接下來用Xcode的Attach to Process就可以了,參考:iOS逆向:用Xcode直接除錯第三方app

相關文章