逆向分析及修復稀土掘金iOS版客戶端閃退bug

susnm發表於2019-02-05

感覺是要搞個《逆向分析及修復app》系列的節奏啊

故事概要

大家好,我又來了。上次給大家分享了《逆向及修復最新iOS版少數派客戶端的閃退bug》,@了一些iOS界的大神,沒想到受到了大家的轉發和關注,還真有點小激動。同時也在微博的評論下收到了稀土掘金的歡迎,還給我開了專欄許可權,希望可以到稀土掘金上分享寫作。備感榮幸的同時,突然發現,一直在稀土掘金上面看文章的我,一直沒有註冊賬戶(好像註冊了但是忘了密碼,在1Password中也沒有記錄,看來是很久之前登陸過了)。這也是這次故事的開始,收到稀土掘金的評論後,馬上開啟了早已不知道什麼時候下載的 掘金 app。果然,沒有登陸記錄,作為開發者馬上用 GitHub 授權進行登陸,尷尬的事情發生了,在授權成功後,app閃退了。好尷尬啊,好吧稀土掘金上的處女座就獻給你了。

跟之前寫《逆向及修復最新iOS版少數派客戶端的閃退bug》文章時不同,在文章寫到這裡的時候,我其實還沒有完全分析完崩潰的原因。我是分析途中決定開始寫這篇文章的,決定一邊分析一遍寫,這樣才會儘可能的保留分析的全過程,當然最後能不能分析到關鍵點,且看下去吧,因為我也不知道。

問題描述

  • 最新版 4.1.3 稀土掘金 app 在使用 github 登陸成功後會 crash
  • 是在登陸成功後才 crash,因為再次進入後會發現已經登陸成功
  • 可能不是所有人都會遇到這個bug,根據後來的分析,可能是Github的某個個人資訊中缺少某些設定導致的授權後crash

著手分析

###找到崩潰原因
分析一個bug的開始,當然是從崩潰原因找起,前一篇文章,我們使用了lldb除錯,在觸發 app 崩潰的時候,從 Terminal 找到了關鍵詞:EXE_BAD_ACCESS。這次,我們使用最簡單的,檢視系統日誌來定位崩潰原因,使用最新的 Mac,開啟控制檯,連線手機並清空多餘的系統日誌列印,然後觸發崩潰,觀察控制檯中輸出的內容(如果輸出日誌過多,可以使用程式名Xitu作為關鍵詞,進行過濾)。如果可以還是不要使用關鍵詞過濾,因為不是所有的崩潰都是由於 app 本身的問題導致的,正如簽名破壞的 app 在沒有越獄的手機上會安裝失敗,這個安裝失敗的原因查詢就需要在日誌中的 SpringBoard 程式列印中進行查詢
SpringBoardApple用來管理我們iPhone應用的一個應用,可以理解為我們 PC 端的桌面,它還負責應用的桌面排列、管理通知中心等)。

正如我們希望的一樣,崩潰的原因最常見不過了:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

這個崩潰就很有意思了,開發者對NSNull呼叫了length方法,因為找不到該方法而崩潰!當然開發者肯定是不會主動對NSNull型別呼叫length方法,猜想:

  • 開發不知道物件會變成NSNull型別,說明這個物件可能是在執行時確定的
  • 呼叫length,說明開發者認為這個類應該是屬於NSString型別的

大膽猜測:結合這個崩潰是發生在登陸的時候,需要從網路獲取登陸資訊,那麼會不會是程式在對獲取到的登陸資訊進行處理的時候導致的崩潰(比如登陸使用者的一些資訊沒有設定,所以程式從登陸資訊裡取到的是空值)。

分析入口

1.找到登陸介面的控制類

逆向分析及修復稀土掘金iOS版客戶端閃退bug

分析從該登陸介面出發,使用cycript注入app後,列印找到該介面的控制器:

susnms-iPhone:~ root# ps -e | grep Xitu
24240 ??         0:00.00 (Xitu.Widget)
24241 ??         0:00.00 (XituShare)
24242 ??         0:00.00 (Xitu)
24283 ??         0:02.03 /var/containers/Bundle/Application/994C4217-88DD-4F55-A016-55BDD5998C49/Xitu.app/Xitu
24288 ttys000    0:00.01 grep Xitu
susnms-iPhone:~ root# cycript -p 24283 common.cy ; cycript -p 24283
cy# currentVCWithKeyWindow ()
#"<XTGithubLoginViewController: 0x15e0ef340>"
cy#複製程式碼

currentVCWithKeyWindow() 來自我自己寫的一個指令碼,你可以前往common.cy下載。下載之後,將其放到/var/root目錄下即可。
使用時,只需要在cycript注入的時候加上即可。
其中還有一些好用的函式,比如快速列印UIControl所有的targetaction

2.檢視登陸介面的呼叫流程

使用class-dump等工具,匯出app的所有類標頭檔案,使用logify.pl工具生成XTGithubLoginViewController類的所有hook函式,使用theos編寫安裝外掛後,觸發崩潰,然後在控制檯查詢該類的函式呼叫邏輯。如下:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

逆向分析及修復稀土掘金iOS版客戶端閃退bug

可以發現,程式是在-[XTGithubLoginViewController viewWillDisappear:]呼叫之後才收到的崩潰的通知。所以很有可能崩潰的原因不是XTGithubLoginViewController導致的,而是在XTGithubLoginViewController類返回登陸資訊後。

仔細研究XTGithubLoginViewController的呼叫過程,發現了幾個有意思的方法,呼叫順序如下:

  • -(void)setCallBack:
  • -(id)onAuthCompleted:
  • -(id)callBack

callback!太熟悉不過了,這不就是我們經常在開發中使用的設定回撥block的時候使用的引數名嗎!!並且,該callback在類初始化的時候被設定,類方法-(id)onAuthCompleted:呼叫之後才被回撥。結合這個方法名onAuthCompleted,這個邏輯是不是很像:我們在登陸成功之後,使用block傳回了我們的登陸資訊進行處理。

分析到這裡,其實我們已經可以使用lldb除錯,列印該block的記憶體地址,然後去掉記憶體地址偏移後,在Hopper中檢視block的實現,看是否該block導致的崩潰。但是作為逆向的學習,我們可以先繼續深入,檢視一下這個block的傳遞呼叫過程。

繼續深入callback的回撥過程

既然該回撥是在XTGithubLoginViewController被初始化的時候被設定的,那麼我們先找到是誰初始化的該Github登陸控制器:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

使用cycript注入列印該控制器類名,並嘗試github的授權登入入口:

susnms-iPhone:~ root# cycript -p Xitu common.cy ; cycript -p Xitu
cy# currentVCWithKeyWindow ()
#"<XTLoginViewController: 0x12e43f740>"
cy# [#0x12e43f740 github
githubBt      githubLogin:
cy# [#0x12e43f740 githubLogin: nil]複製程式碼

發現成功觸發登入,所以可以確定githubLogin:方法確實是登入的入口

1.githubLogin:實現

Hopper中檢視該方法的實現:

-[XTLoginViewController githubLogin:]:
0000000100194c24         sub        sp, sp, #0x50                               ; Objective C Implementation defined at 0x1009d2708 (instance method), DATA XREF=0x1009d2708
0000000100194c28         stp        x20, x19, [sp, #0x30]
0000000100194c2c         stp        x29, x30, [sp, #0x40]
0000000100194c30         add        x29, sp, #0x40
0000000100194c34         mov        x19, x0
0000000100194c38         adrp       x8, #0x100ab4000                            ; @selector(detectSocketStatus)
0000000100194c3c         ldr        x0, [x8, #0x878]                            ; objc_cls_ref_LoginCloud,__objc_class_LoginCloud_class
0000000100194c40         adrp       x8, #0x100aa0000                            ; @selector(computeTime:)
0000000100194c44         ldr        x1, [x8, #0xd0]                             ; "singleClass",@selector(singleClass)
0000000100194c48         bl         imp___stubs__objc_msgSend                   ; login = [LoginCloud singleClass]
0000000100194c4c         mov        x29, x29
0000000100194c50         bl         imp___stubs__objc_retainAutoreleasedReturnValue
0000000100194c54         mov        x20, x0
0000000100194c58         adrp       x8, #0x100914000
0000000100194c5c         ldr        x8, [x8, #0x448]                            ; __NSConcreteStackBlock_100914448,__NSConcreteStackBlock
0000000100194c60         str        x8, [sp, #0x8]
0000000100194c64         movz       w8, #0xc200
0000000100194c68         stp        w8, wzr, [sp, #0x10]
0000000100194c6c         adr        x8, #0x100194cc4
0000000100194c70         nop
0000000100194c74         str        x8, [sp, #0x18]
0000000100194c78         adrp       x8, #0x100923000
0000000100194c7c         add        x8, x8, #0x9d0                              ; 0x1009239d0
0000000100194c80         str        x8, [sp, #0x20]
0000000100194c84         mov        x0, x19
0000000100194c88         bl         imp___stubs__objc_retain
0000000100194c8c         str        x0, [sp, #0x28]
0000000100194c90         adrp       x8, #0x100aa7000                            ; @selector(isTableViewScrolledToBottom)
0000000100194c94         ldr        x1, [x8, #0x848]                            ; "githubLoginCallback:",@selector(githubLoginCallback:)
0000000100194c98         add        x2, sp, #0x8
0000000100194c9c         mov        x0, x20
0000000100194ca0         bl         imp___stubs__objc_msgSend                   ; [login githubLoginCallback: block]
0000000100194ca4         mov        x0, x20
0000000100194ca8         bl         imp___stubs__objc_release
0000000100194cac         ldr        x0, [sp, #0x28]
0000000100194cb0         bl         imp___stubs__objc_release
0000000100194cb4         ldp        x29, x30, [sp, #0x40]
0000000100194cb8         ldp        x20, x19, [sp, #0x30]
0000000100194cbc         add        sp, sp, #0x50
0000000100194cc0         ret複製程式碼

內容很簡單,呼叫了單例類,並傳入回撥callback引數:

LoginCloud *login = [LoginCloud singleClass];
[login githubLoginCallBack: callback];複製程式碼

2.[LoginCloud singleClass]實現

Hopper檢視如下:

-[LoginCloud githubLoginCallback:]:
sub        sp, sp, #0x80 ; Objective C Implementation defined at 0x1009c7cc8 (instance method), DATA XREF=0x1009c7cc8
stp        x26, x25, [sp, #0x30]
stp        x24, x23, [sp, #0x40]
stp        x22, x21, [sp, #0x50]
stp        x20, x19, [sp, #0x60]
stp        x29, x30, [sp, #0x70]
add        x29, sp, #0x70
mov        x22, x0
mov        x0, x2
bl         imp___stubs__objc_retain
mov        x21, x0
adrp       x8, #0x100ab4000 ; @selector(detectSocketStatus)
ldr        x0, [x8, #0x6a0] ; objc_cls_ref_UIStoryboard,_OBJC_CLASS_$_UIStoryboard
adrp       x8, #0x100aa0000 ; @selector(computeTime:)
ldr        x1, [x8, #0x788] ; "storyboardWithName:bundle:",@selector(storyboardWithName:bundle:)
adrp       x2, #0x100949000 ; @"%@ %@ %@"
add        x2, x2, #0x500 ; @"Login"
movz       x3, #0x0
bl         imp___stubs__objc_msgSend ; loginSb = [UIStoryboard storyboardWithName: @"Login" bundle: nil]
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x19, x0
adrp       x8, #0x100aa0000 ; @selector(computeTime:)
ldr        x1, [x8, #0x790] ; "instantiateViewControllerWithIdentifier:",@selector(instantiateViewControllerWithIdentifier:)
adrp       x2, #0x10094f000 ; @"nickname"
add        x2, x2, #0xd00 ; @"githubVC"
bl         imp___stubs__objc_msgSend ; githubVC = [loginSb instantiateViewControllerWithIdentifier: @"githubVC"]
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x20, x0
adrp       x8, #0x100914000
ldr        x8, [x8, #0x448] ; __NSConcreteStackBlock_100914448,__NSConcreteStackBlock
str        x8, sp
movz       w8, #0xc200
stp        w8, wzr, [sp, #0x8]
adr        x8, #0x10015ccbc
nop
str        x8, [sp, #0x10]
adrp       x8, #0x100921000
add        x8, x8, #0x6f0 ; 0x1009216f0
stp        x8, x21, [sp, #0x18]
mov        x0, x21
bl         imp___stubs__objc_retain
mov        x21, x0
mov        x0, x22
bl         imp___stubs__objc_retain
str        x0, [sp, #0x28]
adrp       x8, #0x100aa6000 ; @selector(hasText)
ldr        x1, [x8, #0x440] ; "setCallBack:",@selector(setCallBack:)
mov        x2, sp
mov        x0, x20      ; [githubVC setCallBack: black]
bl         imp___stubs__objc_msgSend
adrp       x8, #0x100ab4000 ; @selector(detectSocketStatus)
ldr        x0, [x8, #0x938] ; objc_cls_ref_UINavigationController,_OBJC_CLASS_$_UINavigationController
adrp       x8, #0x100a9f000
ldr        x1, [x8, #0xd78] ; "alloc",@selector(alloc)
bl         imp___stubs__objc_msgSend
adrp       x8, #0x100aa1000 ; @selector(setUpdatedAt:)
ldr        x1, [x8, #0x948] ; "initWithRootViewController:",@selector(initWithRootViewController:)
mov        x2, x20
bl         imp___stubs__objc_msgSend
mov        x22, x0
adrp       x8, #0x100ab4000 ; @selector(detectSocketStatus)
ldr        x0, [x8, #0x820] ; objc_cls_ref_UIApplication,_OBJC_CLASS_$_UIApplication
adrp       x8, #0x100aa1000 ; @selector(setUpdatedAt:)
ldr        x1, [x8, #0x2d8] ; "sharedApplication",@selector(sharedApplication)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x23, x0
adrp       x8, #0x100a9f000
ldr        x1, [x8, #0xf88] ; "delegate",@selector(delegate)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x24, x0
adrp       x8, #0x100aa1000 ; @selector(setUpdatedAt:)
ldr        x1, [x8, #0x930] ; "window",@selector(window)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x25, x0
adrp       x8, #0x100aa1000 ; @selector(setUpdatedAt:)
ldr        x1, [x8, #0x938] ; "rootViewController",@selector(rootViewController)
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x26, x0
mov        x0, x25
bl         imp___stubs__objc_release
mov        x0, x24
bl         imp___stubs__objc_release
mov        x0, x23
bl         imp___stubs__objc_release
adrp       x8, #0x100aa0000 ; @selector(computeTime:)
ldr        x1, [x8, #0x220] ; "presentViewController:animated:completion:",@selector(presentViewController:animated:completion:)
orr        w3, wzr, #0x1
mov        x0, x26
mov        x2, x22
movz       x4, #0x0
bl         imp___stubs__objc_msgSend
mov        x0, x26
bl         imp___stubs__objc_release
mov        x0, x22
bl         imp___stubs__objc_release
ldr        x0, [sp, #0x28]
bl         imp___stubs__objc_release
ldr        x0, [sp, #0x20]
bl         imp___stubs__objc_release
mov        x0, x21
bl         imp___stubs__objc_release
mov        x0, x20
bl         imp___stubs__objc_release
mov        x0, x19
bl         imp___stubs__objc_release
ldp        x29, x30, [sp, #0x70]
ldp        x20, x19, [sp, #0x60]
ldp        x22, x21, [sp, #0x50]
ldp        x24, x23, [sp, #0x40]
ldp        x26, x25, [sp, #0x30]
add        sp, sp, #0x80
ret複製程式碼

解釋如下:

UIStoryboard *loginSb = [UIStoryboard storyboardWithName: @"Login" bundle: nil]
UIViewController *githubVC = [loginSb instantiateViewControllerWithIdentifier: @"githubVC"]
[githubVC setCallBack: black]
UINavigatinController *nav = [[UINavigatinController alloc] initWithRootViewControler: githubVC];
UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController;
[vc presentViewController: nav animaed: YES completion: nil];複製程式碼
  • 初始化UIStoryBoard,並從中初始化控制器githubVC
  • githubVC控制器設定回撥block(另一個callback引數回撥)
  • 顯示控制器

其實這裡的githubVC應該就是XTGithubLoginViewController型別,我們可以使用cycript確認一下:

cy# var sb = [UIStoryboard storyboardWithName: @"Login" bundle: nil]
#"<UIStoryboard: 0x12e0ebf00>"
cy# [#0x12e0ebf00 instantiateViewControllerWithIdentifier: @"githubVC"]
#"<XTGithubLoginViewController: 0x12e382e00>"複製程式碼

現在callback的傳遞應該很清楚了:點選 github 登入按鈕時,呼叫單例LoginCloud,並傳入callbackLoginCloud根據傳入的callback初始化登入控制器XTGithubLoginViewController,並設定另外的一個callback回撥,XTGithubLoginViewController在登陸成功後,通過該callback進行回撥。

分析callback實現

block有點特殊,它也是一個物件型別。在這裡我們涉及到了兩個block的傳遞,為了方便之後的分析,我們可以先將兩個block的實現地址和傳遞的引數型別都列印出來。接下來的操作中可能會因為程式多次重啟導致文中上下顯示的記憶體地址不一致的情況,我每次都會進行說明。

1.獲取block的函式地址

因為我們知道這個callback作為引數傳給了-[LoginCloud githubLoginCallback:]方法,所以lldb連線服務(如何lldb可以看我的第一篇文章)後,我們在這個方法上打個斷點,同理設定-[XTGithubLoginViewController setCallBack:]斷點。
根據Block的記憶體結構可以查詢block的函式實現地址和引數返回值型別,具體可以看這篇文章。開源就是好,節省步驟,我們可以使用facebook開源的chisel。觸發斷點後,獲取到block的記憶體地址後,使用pblock列印block:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

逆向分析及修復稀土掘金iOS版客戶端閃退bug

獲取到第一個 block 的函式實現地址:0x0000000100194cc4
引數及返回值:void ^(bool, NSString *)

逆向分析及修復稀土掘金iOS版客戶端閃退bug

獲取到第二個 block 的函式實現地址:0x000000010015ccbc
引數及返回值:void ^(NSDictionary *, ThirdLoginModel *, NSDictionary *)

2.獲取block的具體傳回引數值

知道了 block 的引數的型別,那麼我們可以寫個 tweakshook 這個 block 然後列印一下,傳遞的這些引數內容,根據回傳順序,我們先列印第二個block

%hook XTGithubLoginViewController

-(callBackType)callBack {
  callBackType block = ^(NSDictionary *dic1, ThirdLoginModel *model, NSDictionary *dic2) {
    HBLogInfo(@"dic1: %@, model: %@, dic2: %@", dic1, [model debugDescription], dic2);
  };
  return block;
}
%end // end hook複製程式碼

列印結果如下:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

果然發現第二個字典中,出現了多個值為nullvalue,很有可能就是我們要找的點。那麼這個字典是從哪裡來的呢?想起當時列印XTGithubLoginViewController類的呼叫順序的時候,發現的一個函式onAuthCompleted:,它返回的就是一個這樣的字典,那麼我們hook一下方法,過濾掉這些值為nullvalue看看:

-(NSDictionary *)onAuthCompleted:(id)arg1 {
  HBLogInfo(@"%s", __func__);
  NSDictionary *result = %orig;
  NSMutableDictionary *dict = [result mutableCopy];
  for (NSString *key in result.allKeys) {
    if ([result[key] isKindOfClass:[NSNull class]]) {
      HBLogInfo(@"%s key: %@", __func__, key);
      [dict removeObjectForKey:key];
    }
  }
  HBLogInfo(@"result: %@", dict);
  return dict;
}複製程式碼

打包,安裝後發現,並沒有修復這個bug,還是報找不到length方法的日誌。看來我們還沒有找到 bug 點。

3.獲得 block 的具體實現過程

在第二個回撥上下斷點,觸發後,進入實現內部,根據lldb列印出實現內部的每個方法的方法呼叫者和函式名、引數值。過程如下(主要是找到了這個block的實現地址後,在Hopper中找到這個程式碼塊,可以發現對於這個 block 的內部方法,Hopper是沒有註釋呼叫者、selector、引數的,我們可以對Hopper中的這些程式碼中的所有顯示為objc_msgSend的地方下一個斷點,一一觸發,分別列印每個方法的呼叫者和呼叫方法,傳遞引數等資訊),主要過程如下:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

解釋後主要是以下過程:

// swift class
ZEHud *hud = [Xitu.ZEHud sharedInstance];
[hud showHud];
[AVUser loginWithAuthData:  dict platform: @"github" block: block] // block Imp: 0x000000010015cdf8    Signature: void ^(AVUser *, NSError *);複製程式碼

可以看到,其中也有一個block,列印一下block的內容,根據Xituimage的偏移得到block實現的地址:0x100208df8-0x00000000000ac000=0x10015CDF8,在Hopper中找到:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

在這個block上下斷點,c執行後來到該斷點出:si進入該實現內部,斷點定位到如圖中的唯一一個objc_msgSend方法上,獲取其呼叫資訊:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

逆向分析及修復稀土掘金iOS版客戶端閃退bug

可以看到該方法-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:]中還有一個block引數,繼續列印:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

可以發現得到的block:
0x0000000100194cc4,void ^(bool, NSString *)
和我們得到的第一個block是同一個。

既然第二個block引數還沒有分析完,第一個block已經迫不及待的回來了,那麼我們先在Hopper中檢視一下這個block的實現:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

分析到這裡ni後app不小心退出了,那麼我們在這裡重新啟動,既然已經知道這個block的地址,可以直接通過檢視Hopper內的每個objc_msgSend的地址下斷點:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

逆向分析及修復稀土掘金iOS版客戶端閃退bug

可以知道該block的呼叫如下:

login = [LoginCloud singleClass];
[login refreshGithubLogin];
[XTLoginViewController callback];複製程式碼

既然分析到了這裡,程式還沒有崩潰,那麼說程式這之前都沒有問題,那麼問題可能出在了XTLoginViewController這個callback中,那麼我們列印一下這個block

逆向分析及修復稀土掘金iOS版客戶端閃退bug

得到:0x00000001001935d0,void ^(bool, NSString *)

那麼我們繼續深入,列印一下這個block的實現,步驟如上,如果不想每個objc_msgSend都手動打斷點,可以在這個block上打斷點,然後si,進入後,一步一步向下執行,等到執行到objc_msgSend的時候,列印這個方法的內容。但是在這裡下到的斷點都沒有執行程式就已經崩潰了!!!說明什麼?說明這個傳入的callback還沒有執行,程式已經crash了。

  • 從正向開發的角度,傳入這個block,可能是在程式滿足一定條件的時候才會回撥這個block,來傳遞資訊或處理一些事情。
  • 那麼程式應該是崩潰在這個block被執行前,我們需要檢視一下這個函式的內部實現。
  • 可能後知後覺了,現在仔細想想,我前面說的分析到這個函式時,使用ni下一步後,程式不小心退出了!!,所以應該不是不小心退出了,而是這個函式的內部實現導致了崩潰。

確定bug點

分析到了這個方法-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:],那麼我們去Hopper中看看它的內部實現:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

果然,如圖看到的,我們看到了之前在控制檯輸出的崩潰的關鍵詞length,既然找到了這裡,我們利用這個呼叫函式的地址:0x10015d228來下一個斷點,看看呼叫者是誰,是不是真的是null,從而導致的bug。

重新啟動,lldb下斷點:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

如圖,呼叫者為null,並且ni後,完美的崩潰了。文章寫了這麼多,終於找到這個bug點了!!!激動啊。

找到了bug點,那麼我們應該研究一下如何修復這個bug,首先我們需要知道為什麼開發者會用這個null,呼叫了length,程式發生了什麼導致了這個呼叫者為null

我們來仔細看一下,這個函式呼叫所在的程式碼塊的彙編程式碼:

loc_10015d1a8:
add        x8, sp, #0xb0 ; CODE XREF=-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:]+660
stp        xzr, x8, [sp, #0xb0]
orr        w8, wzr, #0x30
stp        w28, w8, [sp, #0xc0]
adr        x8, #0x10015d84c
nop
fmov       d0, x8
adr        x8, #0x10015d85c
nop
ins        v0, x8
add        x8, sp, #0xb0
stur       q0, [x8, #0x18]
ldr        x0, [x25, #0x618]
mov        x24, x27
mov        x1, x24
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
mov        x23, x0
adrp       x2, #0x10094b000 ; @"Z"
add        x2, x2, #0xc60 ; @"self_description"
mov        x1, x26
bl         imp___stubs__objc_msgSend
mov        x29, x29
bl         imp___stubs__objc_retainAutoreleasedReturnValue
str        x0, [sp, #0xd8]
mov        x0, x23
bl         imp___stubs__objc_release
ldr        x8, [sp, #0xb8]
ldr        x0, [x8, #0x28]
adrp       x8, #0x100aa0000 ; @selector(computeTime:)
ldr        x1, [x8, #0x340] ; "length",@selector(length)
bl         imp___stubs__objc_msgSend
ldr        x20, [sp, #0x18]
cmp        x0, #0x24
b.lo       loc_10015d24c複製程式碼

可以看到,ldr x0, [x8, #0x28]。這個null是從[x8, #0x28]載入的。但是我們在這個程式碼塊中(包括整個-[LoginCloud thirdLogRefreshCurrentUserDatatype:ThirdData:Error:CallBack:]方法內)都沒有找到。這個情況就比較特殊了,自從學習逆向以來,還是第一次碰到這種情況。該呼叫者是從一個從來沒有被寫入過內容的暫存器中讀取的。應該也是因為這個原因才導致讀取的內容為null吧。

換個思路,我們結合上下文找線索。我們翻譯一下上下文方法呼叫的彙編程式碼:

AVUser *user = [AVUser currentUser];
NSString *name = [user valueForKey: @"username"];
NSString *headImg = [user ValueForKey: @"avatar_large"];
NSString *descrip = [user ValueForKey: @"self.description"];
//...複製程式碼

我們可以使用cycript來列印一下這個user是什麼:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

然後順便把彙編中的內容,列印一下:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

果然,其中有一個列印是null。而且正好出現在length呼叫的上方:

逆向分析及修復稀土掘金iOS版客戶端閃退bug

所以很有可能,這個bug是因為通過keyself.descriptionuser中獲取了一個沒有的值,並對他呼叫了length導致的。

那麼我們通過theos建立一個外掛,然後hook一下這個方法,返回一個我們設定了值的AVUser物件,

%hook AVUser
+(id)currentUser {
  AVUser *user = %orig;
  id descri = [user valueForKey: @"self_description"];
  if ([descri isKindOfClass: [NSNull class]]) {
    HBLogWarn(@"the value for key: self_description is null");
    [user updateValue: @"" forKey: @"self_description"];
  }
  return user;
}

%end複製程式碼

安裝後,再次登陸發現登陸成功,並沒有crash,終於修復了這個bug,發現已經寫了不少字了。

總結

最後的修復bug的tweaks可以在這裡下載XituHook

其實在我自己分析的時候,分析的內容還要多,也比較雜。逆向分析正是這樣,我們需要順著程式執行的順序分析,但是事情往往不如我們想的這樣簡單,分析的岔路很多,特別是這裡,涉及到了多個block的回撥,需要對block的實現原理及其記憶體結果比較熟悉才行。

在分析的過程中,分析到的還有一些文中沒有寫到的block和方法,並且在其中發現了length關鍵詞,激動不已啊,但是當我深入分析的時候發現,開發者都做了防護

if ([obj isKindOfClass:[NSNull class]]) {
    obj = @"";
}複製程式碼

預防出現NSNull的情況,所以都不是 crash 的罪魁禍首。我猜測self_description是獲取的 github 上的某個個人資訊(根據名字推測是自我介紹?)。如果想分析的話也可以,需要從AVUser這個類如果獲得這些資訊開始分析。但是我想本文分享到這裡已經差不多了。

寫在最後

求工作,求工作,求工作,重要的事情說三遍。現在的iOS就業形勢,不提也罷,興趣所致,跪著走完。

相關文章