高手談Android NDK C++ RTTI 分析

本文意在說明Android NDK 在實現C++ RTTI時的相關數據結構,并從匯編角度分析其內存布局,以幫助理解RTTI的實現原理,同時,分析在逆向過程中如何利用RTTI恢復C++類名信息。

用ndk-build編譯C++代碼時,默認的C++運行時庫(libstdc++)是不支持RTTI的, 需要在Application.mk與Android.mk中進行配置。其它可以選擇的C++運行時庫有GAbi++、STLport、GNU STL、LLVM libc++, 各種庫又分靜態鏈接庫與動態鏈接庫。其中中STLport的RTTI是借用了GAbi++中的實現,另外GNU STL、LLVM libc++的實現也與GAbi++非常相似(相關數據結構的命名、結構都相似, 可能是因為都是基于Itanium C++ ABI。

所以本文將選擇STLPort為C++運行時庫, 在Application.mk中配置:

APP_STL := stlport_static

在Android.mk中配置:

LOCAL_CPP_FEATURES := rtti

另外,本文使用 Android NDK 10c編譯,編譯abi為armeabi,編譯32位代碼時其默認使用GCC 4.8。若使用其它版本NDK或者其它編譯器,可能與本文分析結果有差異。

一、C++ RTTI 簡介

RTTI是Runtime Type Identification的縮寫,即運行時類型識別。程序能夠借此使用基類的指針或引用,來檢查這些指針或引用所指的對象的實際派生類型。C++通過typeid與dynamic_cast來提供RTTI。typeid返回一個typeinfo對象的引用,它記錄了與類型相關的信息,后文將詳細分析這個結構;dynamic_cast用于安全而有效地進行向下轉型(down_cast),即安全地將一個基類指針轉換為一個派生類指針。

它們的基本使用方法如下:

classes.h文件:

classBase

{

public:

Base();

virtual ~Base();

virtualvoidFunc();

private:

intmMember;

};

classDeriver1 :publicBase

{

public:

Deriver1();

virtual ~Deriver1();

virtualvoidFunc();

private:

intmDeriver1Member;

};

classDeriver2 :publicBase

{

public:

Deriver2();

virtual ~Deriver2();

virtualvoidFunc();

private:

intmDeriver2Member;

};

main.cpp文件:

intmain()

{

Base base;

Deriver1 deriver1;

Deriver2 deriver2;

cout<

cout<

cout<

Base *pBase = &deriver1;

cout<

cout<

cout << pBase << endl;

Driver1 *pDeriver1 = dynamic_cast(pBase);

cout << pDeriver1 << endl;

Driver2 *pDeriver2 = dynamic_cast(pBase);//正確,返回NULL

cout << pDeriver2 << endl;

pDeriver2 = (Deriver2*)pBase;//錯誤

cout << pDeriver2 << endl;

pDeriver2 = static_cast(pBase);//錯誤

cout << pDeriver2 << endl;return 0;

}

編譯成可執行文件,push到android 手機上運行,輸出:

i <------- typeid(int).name(), 變量類型

4Base <------- typeid(Base).name(), 類名

4Base <------- typeid(base).name(), 變量

P4Base <------- typeid(pBase).name(), Base的指針類型

8Deriver1 <------- typeid(*pBase).name(), pBase實際指向一個Deriver1

0xbec87a20

0xbec87a20 <----- 正確的轉換,指向deriver1的基類指針可以轉換為Deriver1類型指針

0x00000000 <----- 正確的轉換,因為指向deriver1的基類指針并不能轉換為Deriver2類型指針

0xbec87a20 <----- 錯誤,若繼續使用,可能會導致內存訪問出錯,即將Dervier1當Deriver2用

0xbec87a20 <----- 錯誤,若繼續使用,可能會導致內存訪問出錯

P.S. 上面看到顯示的類名與我們定義的不完全一樣,是因為為了保證每個類名稱在程序中的唯一性,編譯器會通過一定的規則對原始類名進行改寫,如想了解這一規則,可以以name mangling為關鍵詞進行搜索。

二、RTTI 相關數據結構

上文說到typeid將返回一個typeinfo對象的const引用,RTTI就是依賴typeinfo類及其派生類來實現的,下面介紹下這些類。

在NDK路徑下\android-ndk-r10c\sources\cxx-stl\gabi++\include\typeinfo文件中有定義這個類:

classtype_info

{public:

virtual ~type_info();

//....

private:

//....

const char*__type_name;// 這個字段記錄改寫過后的類名

};

在NDK路徑下\android-ndk-r10c\sources\cxx-stl\gabi++\src\cxxabi_defines.h有定義一些typeinfo的派生類,此處挑一些我們感興趣的類列舉:

class__shim_type_info :publicstd::type_info{....}

// 無基類的類的typeinfo類型

class__class_type_info :public__shim_type_info{.....}

//只有一個public非虛基類,且基類偏移為0的類的typeinfo

class__si_class_type_info :public__class_type_info{

public:

virtual ~__si_class_type_info();const__class_type_info *__base_type;

//......

}

// 有基類但不滿足 __si_class_type_info 約束條件的其它類的typeinfo

class__vmi_class_type_info :public__class_type_info{

public:

virtual ~__vmi_class_type_info();

unsignedint__flags;

unsignedint__base_count;

__base_class_type_info __base_info[1];

//......

}

// Used in __vmi_class_type_info

struct __base_class_type_info{

public:

const__class_type_info *__base_type;long__offset_flags;

// .......

}

以第1小節中的程序為例,Base、Driver1的對象的內存布局如下:

deriver2的內存布局與deriver1相似,這里沒有重復畫出。從上圖可以看到,每一個類的虛表索引為-1的位置存放著typeinfo的指針,并根據類的不同,該指針指向不同的typeinfo派生類實例。比如Base類無基類,所以其typeinfo指針指向__class_type_info的實例;而Deriver1繼承自Base, deriver1在其偏移為0的位置包含一個public非虛基類實例,所以它的typeinfo指針指向__si_class_type_info實例。使用dynamic_cast的時候,正是根據這些typeinfo指針來判斷一個基類指針是否可以轉換為一個派生類指針。而且由上可見,若一個待操作的類沒有虛函數表, typeid也只能返回其靜態類型。

下面我們通過反編譯代碼來驗證上面的關系圖。

三、逆向過程中利用RTTI恢復類名

將第1小節中生成的可執行程序用IDA Pro打開,此處選用obj\local\armeabi\目錄下未經過strip的程序,以方便分析。

根據相關字符串,可以很快定位各個類的typeinfo信息:

各個類的虛函數表結構:

可見,從反編譯的代碼看,虛表、typeinfo信息關系與第3節中描述一致。(細心的朋友可能有疑問,為什么會產生兩個析構函數?對于這個問題,可以以Itanium C++ ABI為關鍵字搜索了解)

對于通常的逆向分析,都沒有沒有上面的符號信息的。所以我們可以通過RTTI信息來恢復類名及其類間關系,為逆向工作提供便利。可以按以下步驟進行:

定位__class_type_info, __si_class_type_info, __vmi_class_type_info虛函數表。

查找對這些虛函數表的引用,我們可以得到這些typeinfo派生類的實例地址。而這些實例中type_name字段就表示原始類名。

根據引用這些實例地址,就可以得到相關類的虛表地址,此處我們可以根據上一步得到的原始類名重命名虛表指針。

查找引用這些虛表指針的代碼,通常都是類的構造函數,于是我們又可以重命名這些構造函數了。

以上步驟我們都可以通過IDAPython腳本自動完成。

四、小結

其實上面只是分析了最簡單的單繼承情景,還有諸如多繼承、虛繼承等情景待分析,由于相關typeinfo類已經例出,相信分析難度不大。

另外需要注意的一個地方,在反匯編后的代碼中,并不是直接引用虛表地址,而是引用虛表地址-8的位置,用這個位置+8寫入當作虛擬指針。

以上分析過程與結論都來自個人認知,如有錯誤,歡迎指正。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 上篇請看:C++編程思想重點筆記(上) 宏的好處與壞處 宏的好處:#與##的使用 三個有用的特征:字符串定義、字符...
    小敏紙閱讀 641評論 0 2
  • Introduction to C++ (Season 1) Unit 1: Overview of C++ 第1...
    我是阿喵醬閱讀 2,771評論 0 7
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 再讀高效c++,頗有收獲,現將高效c++中的經典分享如下,希望對你有所幫助。 1、盡量以const \enum\i...
    橙小汁閱讀 1,241評論 0 1
  • 小小年紀無煩惱,手舞足蹈樂開懷。 調皮活潑又可愛,幸福天使美照帥。
    文采樂閱讀 291評論 16 8