IDA F5堆棧不平衡的處理

IDA F5堆棧不平衡的處理

在這裏插入圖片描述

1.引出問題

F5時出現如圖錯誤,一般是程序代碼有一些干擾代碼,讓IDA的反彙編分析出現錯誤。比如用push + n條指令 + retn來實際跳轉,而IDA會以爲retn是函數要結束,結果它分析後發現調用棧不平衡,因此就提示sp analysis failed.

我們選擇抖音老版本的libcms.so作爲分析案例,假設Java代碼如下

static {
    System.loadLibrary("cms");
}

public native int getUserInfo();

我們想要分析getUserInfo這個函數,函數被聲明爲native,這代表了它是一個native方法,真正的代碼邏輯由C/C++等native語言實現,它來自名爲cms的庫,由靜態代碼塊中的System.loadLibrary加載進內存。在加載和尋找動態庫時,Java會判斷系統並擴展庫名,在Windows平臺下cms會被擴展成libcms.dll,Android作爲基於linux的系統,則會依照linux被擴展成libcms.so,也就是我們常說的xxx SO庫,解壓APK取出libcms.so庫,我們使用ida打開,默認加載,注意觀察IDA左下角,地址值不變化時即加載完畢。
在這裏插入圖片描述
如何找到getUserInfo函數的實現代碼?按照慣例,查看Exports欄——函數導出表,意爲此SO文件提供給外部調用的函數列表。在最一般的情況下,我們會在導出表中找到它。
在這裏插入圖片描述
我們可以看出,名稱似乎是有規律的,即Java + 包名 + 類名 + 方法名,這就涉及到”綁定方式”的話題了。將Java 方法和真正實現其邏輯的Native函數實現對應也被稱爲綁定或註冊,這種通過規則命名進行綁定的方式稱爲靜態註冊/綁定,我們可以在導出函數表中搜索“Java“,找到可能採取靜態註冊方式的函數,但我們的cms庫似乎並不是採用靜態註冊。
在這裏插入圖片描述
Java_com_ss_sys_ces_a_DebugPrint這個函數似乎是靜態註冊,但很可惜的是,找不到我們要的getUserInfo,它應該叫“Java_xxx_xxx_getUserInfo”,如果按照靜態註冊的規則命名法。那麼可以推斷,它使用了動態註冊。
動態註冊不採用“Java + 包名 + 類名 + 方法名“的規則命名來對應,而是由程序員提供一個函數映射表,主動告知函數的對應關係。這種方式效率更高,而且C++層的函數名不用長且醜。

我們不妨打開Android Studio,動手寫一個動態註冊。

2.動手實現動態註冊

我們需要3.2以上版本的Android Studio,打開Android Studio新建Project,拖到最下,選擇Native C++,後續一路默認,這樣可以生成一個支持native的Android demo。
在這裏插入圖片描述
在這裏插入圖片描述
Ctrl+鼠標左鍵 停留在stringFromJNI函數聲明上,進入函數真正定義的地方。
在這裏插入圖片描述函數名爲Java_com_example_demo1(包名)_MainActivity(類名)_stringFromJNI(方法名),這是靜態註冊。我們將它修改爲動態註冊,只需要兩步。

  1. 自己編寫JNI_OnLoad覆蓋系統默認的註冊函數
    在加載so庫的時候,系統首先會尋找JNI_OnLoad(JavaVM *vm, void *reserved)方法,用於註冊JNI函數,因此我們便可以重寫該方法來覆蓋android默認的JNI_OnLoad來實現動態註冊。
  2. 調用JNIEnv->RegisterNatives()來註冊JNI函數。

第一步複製粘貼即可

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;
    jint result = -1;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
        return JNI_ERR;
    //註冊方法
   ********
    result = JNI_VERSION_1_6;
    return result;
}

在這裏插入圖片描述
第二步是重點
JNIEnv是一個很重要的概念,//TODO 附帶資料
調用這個函數即可完成動態註冊,在JNI.h中我們可以看到這個函數的定義

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)

//可以看到由三個參數
//Jclass clazz:native方法所屬的類,通過findclass(Java類名) 得到.
//const JNINativeMethod* methods: 方法數組,需要了解一下JNINativeMethod結構體
//jint nMethods:方法數組的長度,即我們動態註冊了jint個函數。

如下是JNINativeMethod結構體的定義

typedef struct {
    const char* name; // native 的方法名,比如StringFromJNI
    const char* signature; // 方法簽名,例如 ()Ljava/lang/String;
    void*       fnPtr; // 函數指針
} JNINativeMethod;

Name+signature描述了Java層的信息,別忘了RegisterNatives 函數的參數一是方法所屬的Java類,再加上此處結構體所提供的方法名和方法簽名,加起來最終得到了完整的Java層信息,結構體中的fnPtr則是native中的函數名,Java世界和Native世界完成對應關係。

我們先把“Java_com_example_demo1_MainActivity_stringFromJNI “函數名修改成stringFromJNI,這樣的函數名清爽多了。
在這裏插入圖片描述
接下來就是編寫代碼了

JNINativeMethod methods[] = {
        {"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
};
env->RegisterNatives(env->FindClass("com/example/demo1/MainActivity"),methods,1);

這兩句增加到我們JNI_OnLoad中留白的地方,就大功告成了。還有一個困惑我們需要解釋一下,方法簽名的表述似乎和Java語法不太一樣,事實上,這和Smali的語法一致。

我們可以參考一下Android源碼中對JNINativeMethod方法列表的處理
![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20191229152722386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM4ODUxNTM2,size_16,color_FFFFFF,t_70
似乎寫法不太一樣,這是因爲經過了封裝,可以看出,方法數組的長度(jint nMethods)使用NELEM(methods)取得,可以猜測這類似於length(methods)。
查看源碼,NELEM 是一個宏定義,返回length。
在這裏插入圖片描述
我們對代碼稍作修改,向Android源碼靠近。
在這裏插入圖片描述
大功告成,運行試試。
在這裏插入圖片描述
知道動態註冊如何實現後,自然就知道逆向時怎麼做。
首先找JNI_OnLoad,找到其中RegisterNative函數,它的倒數第二個參數是一個大數組。裏面包含了諸多對應表。

三、回到libcms.so

首先找JNI_Onload
在這裏插入圖片描述
在這裏插入圖片描述
我們遇到了錯誤,無法F5反編譯成C代碼,報錯提示是棧不平衡。處理的方法非常多,我們嘗試樸素的幾種。

3.1、棧不平衡就改回去

先打開IDA的通用設置窗口,顯示棧高度
在這裏插入圖片描述
在這裏插入圖片描述
我修改了三處,增加棧高度的顯示,自動註釋彙編指令的意義,以及顯示操作碼的字節(有時候可以幫助我們判斷thumb,arm指令)
在這裏插入圖片描述
接下來按G跳轉地址,到棧出問題的地方
在這裏插入圖片描述
往上找,在第一次出現負數的上一行按alt+k, alt+K是棧指針調節的快捷鍵,我們使用它將上一行的棧高度調整成一樣的負數。
在這裏插入圖片描述
在這裏插入圖片描述
棧高度被調整和擴散成了正數,我們再回到JNI_OnLoad,試一下F5
在這裏插入圖片描述
繼續重複剛纔的工作,G跳轉,往上找遇到的第一個正直棧高度,alt+k修改
在這裏插入圖片描述
在這裏插入圖片描述
再次嘗試F5
在這裏插入圖片描述
修改完再F5
在這裏插入圖片描述
反編譯成功,但反編譯的結果似乎很糟糕,參數個數多達幾十個,代碼也奇怪極了。
這是因爲我們強行平衡了棧,而IDA爲了完成配平堆棧的任務, 會給一些函數增刪錯誤的參數數量, 整體代碼識別混亂極了,我們可以在函數上按Y,把函數的參數個數修改成正確的個數。
除此之外,我們可以在彙編代碼上,alt+p,對函數的方方面面進行調整。
在這裏插入圖片描述
我們不對JNI_OnLoad做過多處理,因爲我們只是想看其中的RegisterNative函數。
———————認真看JNI_OnLoad反編譯後的代碼1分鐘———————————
反彙編代碼非常雜亂,我們想識別出函數中的JNI函數,有些困難,或許這不是一個好主意,這種方法得到的垃圾反彙編代碼還不如看彙編。

3.2、根據特徵找RegisterNative函數

所有的JNI函數的指針在一個大表格中,RegisterNative函數位於860(0x35C)這個位置,正常反彙編後形如(*(v1 + 860))(),然後我們會轉換v1爲env結構體指針,即可得到正確結果。但我們現在得不到正確的反彙編代碼,所以老老實實看彙編的特徵。

在IDA中搜索立即數,ALT+I熱鍵,如圖操作。

在這裏插入圖片描述
Ctrl+F 搜索0x35C(這種信息一般十六進制顯示)
在這裏插入圖片描述
看到有兩處使用了這個函數
先看第一處調用
在這裏插入圖片描述
閱讀此處彙編代碼,這裏涉及到地址重定位和調用約定的問題,off_8A5C4即爲我們需要的methods列表。
Ps.//TODO 補充彙編的資料
在這裏插入圖片描述
大功告成

3.3 、通過字符串搜索調用

我們在動態註冊函數時,需要提供對應關係

typedef struct {
    const char* name; // native 的方法名,比如StringFromJNI
    const char* signature; // 方法簽名,例如 ()Ljava/lang/String;
    void*       fnPtr; // 函數指針
} JNINativeMethod;

因此可以搜索字符串“Java“等,查看其引用。Shift+F12打開字符串窗口,CTRL+F搜索。

在這裏插入圖片描述
在這裏插入圖片描述
任意選擇一個方法簽名進入
在這裏插入圖片描述
字符串值爲“(Ljavaxxxxx“,IDA將值賦給了某個變量,變量名爲aLjavaxxx,在變量名上按x查看其引用,進入。
OK搞定

3.4、使用Hook得到函數地址

使用Hook工具 Hook 得到真實地址,Hook工具的玩兒法太多了。
// TODO
// TODO

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章