本文意在说明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(链接[3])?)。 所以本文将选择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文件:
class Base
{
public:
Base();
virtual ~Base();
virtual void Func();
private:
int mMember;
};
class Deriver1 : public Base
{
public:
Deriver1();
virtual ~Deriver1();
virtual void Func();
private:
int mDeriver1Member;
};
class Deriver2 : public Base
{
public:
Deriver2();
virtual ~Deriver2();
virtual void Func();
private:
int mDeriver2Member;
};
main.cpp文件:
int main()
{
Base base;
Deriver1 deriver1;
Deriver2 deriver2;
cout<(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(),="" 类名="" typeid(base).name(),="" 变量="" p4base="" typeid(pbase).name(),="" base的指针类型="" 8deriver1="" typeid(*pbase).name(),="" pbase实际指向一个deriver1="" 0xbec87a20="" <-----="" 正确的转换,指向deriver1的基类指针可以转换为deriver1类型指针="" 0x00000000="" 正确的转换,因为指向deriver1的基类指针并不能转换为deriver2类型指针="" 错误,若继续使用,可能会导致内存访问出错,即将dervier1当deriver2用="" 错误,若继续使用,可能会导致内存访问出错<="" code="">
main.cpp文件:
class type_info
{
public:
virtual ~type_info();
//....
private:
//....
const char *__type_name; // 这个字段记录改写过后的类名
};
编译成可执行文件,push到android 手机上运行,输出:
i <------- typeid(int).name(),="" 变量类型="" 4base="" <-------="" typeid(base).name(),="" 类名="" typeid(base).name(),="" 变量="" p4base="" typeid(pbase).name(),="" base的指针类型="" 8deriver1="" typeid(*pbase).name(),="" pbase实际指向一个deriver1="" 0xbec87a20="" <-----="" 正确的转换,指向deriver1的基类指针可以转换为deriver1类型指针="" 0x00000000="" 正确的转换,因为指向deriver1的基类指针并不能转换为deriver2类型指针="" 错误,若继续使用,可能会导致内存访问出错,即将dervier1当deriver2用="" 错误,若继续使用,可能会导致内存访问出错="" p.s.="" 上面看到显示的类名与我们定义的不完全一样,是因为为了保证每个类名称在程序中的唯一性,编译器会通过一定的规则对原始类名进行改写,如想了解这一规则,可以以name="" mangling为关键词进行搜索。="" 二、rtti="" 相关数据结构="" ="" 上文说到typeid将返回一个typeinfo对象的const引用,rtti就是依赖typeinfo类及其派生类来实现的,下面介绍下这些类。="" 在ndk路径下\android-ndk-r10c\sources\cxx-stl\gabi++\include\typeinfo文件中有定义这个类:="" class __shim_type_info : public std::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();
unsigned int __flags;
unsigned int __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的对象的内存布局如下:
-
定位__class_type_info, __si_class_type_info, __vmi_class_type_info虚函数表。
-
查找对这些虚函数表的引用,我们可以得到这些typeinfo派生类的实例地址。而这些实例中type_name字段就表示原始类名。
-
根据引用这些实例地址,就可以得到相关类的虚表地址,此处我们可以根据上一步得到的原始类名重命名虚表指针。
-
查找引用这些虚表指针的代码,通过都是类的构造函数,于是我们又可以重命名这些构造函数了。
以上步骤我们都可以通过IDAPython脚本自动完成。 四、小结 其实上面只是分析了最简单的单继承情景,还有诸如多继承、虚继承等情景待分析,由于相关typeinfo类已经例出,相信分析难度不大。 另外需要注意的一个地方,在反汇编后的代码中,并不是直接引用虚表地址,而是引用虚表地址-8的位置,用这个位置+8写入当作虚拟指针。 以上分析过程与结论都来自个人认知,如有错误,欢迎指正。
更多资讯请关注网易云捕微信公众号,网易云捕官方微博~~