高手谈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(链接[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<<typeid(int).name()<<endl;
     cout<<typeid(Base).name()<<endl;
     cout<<typeid(base).name()<<endl;
     Base *pBase = &deriver1;
     cout<<typeid(pBase).name()<<endl;
     cout<<typeid(*pBase).name()<<endl;

cout << pBase << endl;
     Driver1 *pDeriver1 = dynamic_cast<Deriver1*>(pBase);
     cout << pDeriver1 << endl;

Driver2 *pDeriver2 = dynamic_cast<Deriver2*>(pBase); //正确,返回NULL
     cout << pDeriver2 << endl;

pDeriver2 = (Deriver2*)pBase;//错误
     cout << pDeriver2 << endl;

pDeriver2 = static_cast<Deriver2*>(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文件中有定义这个类:
class type_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 : 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的对象的内存布局如下:

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信息:

各个类的虚函数表结构:

       细心的朋友可能有疑问,为什么会产生两个构造函数?对于这个问题,可以参考链接[4]。
可见,从反编译的代码看,虚表、typeinfo信息关系与第3节中描述一致。
       对于通常的逆向分析,都没有没有上面的符号信息的。所以我们可以通过RTTI信息来恢复类名及其类间关系,为逆向工作提供便利。可以按以下步骤进行:

  1. 定位__class_type_info, __si_class_type_info, __vmi_class_type_info虚函数表。
  2. 查找对这些虚函数表的引用,我们可以得到这些typeinfo派生类的实例地址。而这些实例中type_name字段就表示原始类名。
  3. 根据引用这些实例地址,就可以得到相关类的虚表地址,此处我们可以根据上一步得到的原始类名重命名虚表指针。
  4. 查找引用这些虚表指针的代码,通过都是类的构造函数,于是我们又可以重命名这些构造函数了。

以上步骤我们都可以通过IDAPython脚本自动完成。

四、小结
       其实上面只是分析了最简单的单继承情景,还有诸如多继承、虚继承等情景待分析,由于相关typeinfo类已经例出,相信分析难度不大。
       另外需要注意的一个地方,在反汇编后的代码中,并不是直接引用虚表地址,而是引用虚表地址-8的位置,用这个位置+8写入当作虚拟指针。
       以上分析过程与结论都来自个人认知,如有错误,欢迎指正。

网易云捕-高效的APP质量跟踪平台

时间: 2024-11-01 06:09:30

高手谈Android NDK C++ RTTI 分析的相关文章

Android NDK 编译选项设置[zhuan]

http://crash.163.com/#news/!newsId=24 在Android NDK开发中,有两个重要的文件:Android.mk和Application.mk,各尽其责,指导编译器如何编译程序,并决定编译结果是什么.本文将详细说明几个常见的NDK选项的配置,帮助大家理解相应的配置选项. 一.Application.mk Application.mk实际上是轻量级Makefile,通常在$PROJECT/jni目录下,用于配置所有modules的编译变量,例子如下: APP_AB

Android NDK编译选项设置

在Android NDK开发中,有两个重要的文件:Android.mk和Application.mk,各尽其责,指导编译器如何编译程序,并决定编译结果是什么.本文将详细说明几个常见的NDK选项的配置,帮助大家理解相应的配置选项. 一.Application.mk Application.mk实际上是轻量级Makefile,通常在$PROJECT/jni目录下,用于配置所有modules的编译变量,例子如下: APP_ABI := armeabi arm64-v8a x86_64 x86 arme

转:android实时语音问题分析

转:http://ticktick.blog.51cto.com/823160/1746136 PigeonCall:一款Android VoIP网络电话App架构分析 2016-02-29 20:12:19 标签:Android Pigeoncall 飞鸽电话 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://ticktick.blog.51cto.com/823160/1746136 1.概述 PigeonCall,中文名“

Android NDK编程浅入深出之--Android.mk

    Android.mk Android.mk是一个向Android NDK构建系统描述NDK项目的GUN Makefile片段.它是每一个NDK项目的必备组件.构建系统希望它出现在jni子目录中.下面是hello-jni项目中Android.mk文件的内容. # Copyright (C) 2009 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License

深入理解Android NDK日志符号化

为了进行代码及产品保护,几乎所有的非开源App都会进行代码混淆,这样当收集到崩溃信息后,就需 要进行符号化来还原代码信息,以便开发者可以定位Bug.基于使用SDK和NDK的不同,Android的崩溃分为两类:Java崩溃和C/C++崩溃.Java崩溃通过mapping.txt文件进行符号化,比较简单直观,而C/C++崩溃的符号化则需要使用Google自带的一些NDK工具,比如ndk-stack.addr2line.objdump等.本文不去讨论如何使用这些工具,有兴趣的朋友可以参考同事写的另一篇

Android NDK开发指南---Application.mk文件和android.mk文件

https://android.googlesource.com/platform/development/+/donut-release/ndk/docs/OVERVIEW.TXT https://android.googlesource.com/platform/ndk/+/4e159d95ebf23b5f72bb707b0cb1518ef96b3d03/docs/ANDROID-MK.TXT https://android.googlesource.com/platform/ndk/+/4

Android NDK环境搭建与简单实例

一.NDK与JNI简介 NDK全称为native development kit本地语言(C&C++)开发包.而对应的是经常接触的Android-SDK,(software development kit)软件开发包(只支持java语言开发). 简单来说利用NDK,可以开发纯C&C++的代码,然后编译成库,让利用Android-SDK开发的Java程序调用.NDK开发的可以称之为底层开发或者jni(java  native interface)层开发,SDK开发可以称为上层开发. Andr

Android &lt;uses-sdk&gt; 和 target 分析

Android中<uses-sdk>属性和target属性分析 1. 概要 <uses-sdk> 用来描述该应用程序可以运行的最小和最大API级别,以及应用程序开发者设计期望运行的平台版本.通过在manifest清单文件中添加该属性,我们可以更好的控制应用在不同android 系统版本上的安装和兼容性体验问题.                                                                                    (图

Android ndk下用AssetManager读取assets的资源

转自:http://www.cppblog.com/johndragon/archive/2012/12/28/196754.html 在使用 cocos2dx 在 Android 上进行游戏开发时,遇到了奇怪的事情,无论什么代码,都无法读资源文件.不得以只好寻求更高版本的Api. 在Android ndk api level 9 之后,提供了一套称为 AssetManager 的api. 这个api 的工作原理是 - Java通过JNI把getAssets得到的AssetManager传递给一