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 Sanitizer
的memory history
需要重新編譯app,但是malloc stack
只需要在app啟動前設置環境變量MallocStackLogging
和MallocStackLoggingNoCompact
即可。開啟后會在系統的/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_APP
和YOUR_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。