首页 > 代码库 > Android通用脱壳工具DexHunter的原理分析和使用说明(二)

Android通用脱壳工具DexHunter的原理分析和使用说明(二)

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/53715325

前面的博文《Android通用脱壳工具DexHunter的原理分析和使用说明(一)》中已经记录了很多关于DexHunter脱壳工具的脱壳原理和思考的思路问题并没有涉及到DexHunter脱壳工具的代码的分析,今天我就代码分析、理解和DexHunter脱壳工具的使用以及需要注意的地方进行博文的记录。


在阅读DexHunter的代码之前,复习下几个须知:


1>. POSIX定时器编程相关的知识

struct sigevent

The <signal.h> header shall define the sigevent structure, which shall include at least the following members:

struct sigevent {
    int           sigev_notify;            //Notification type. 
    int           sigev_signo;            //Signal number. 
    union       sigval  sigev_value;             //Signal value. 
    void         (*sigev_notify_function)(union sigval); //Notification function. 
    pthread_attr_t *sigev_notify_attributes;  //Notification attributes. 
}; 

sigev_notify

 sigev_notify 的取值范围如下,只有3种情况(对应的宏在<signal.h>中定义)。

SIGEV_NONE
事件发生时,什么也不做.
SIGEV_SIGNAL
事件发生时,将sigev_signo 指定的信号(A queued signal)发送给指定的进程.
SIGEV_THREAD
事件发生时,内核会(在此进程内)以sigev_notification_attributes为线程属性创建一个线程,并且让它执行sigev_notify_function
传入sigev_value作为为一个参数.

sigev_signo

 在sigev_notify = SIGEV_SIGNAL 时使用,指定信号的种别(number).

sigev_value

sigev_notify = SIGEV_THREAD 时使用,作为sigev_notify_function 即定时器线程回调函数的参数.

union sigval
{
    int sival_int;
    void *sival_ptr;
};  

(*sigev_notify_function)(union sigval)

函数指针(指向通知执行函数即定时器线程回调函数),在sigev_notify = SIGEV_THREAD 时使用, 其他情况下置为NULL.

sigev_notify_attributes

指向线程属性的指针,在sigev_notify = SIGEV_THREAD 时使用,指定创建线程的属性, 其他情况下置为NULL. 


 struct timespec

The <time.h> header shall declare the timespec structure, which shall include at least the following members: 

struct timespec 
{
    time_t tv_sec;        /* Seconds */
    long    tv_nsec;        /* Nanoseconds(纳秒:十亿分之一秒) */
};

struct itimerspec

The <time.h> header shall also declare the itimerspec structure, which shall include at least the following members:

struct itimerspec 
{
    struct timespec it_interval;  /* Timer interval(timer循环时间间隔) */
    struct timespec it_value;     /* Initial expiration(timer初次到期时间间隔) */
};

clockid_t

clockid_t is used for clock ID type in the clock and timer functions, 取值范围如下(前4个是POSIX定义的,灰色部分为Linux的扩展),

/* Identifier for system-wide realtime clock, Setting this clock requires appropriate privileges */
#define CLOCK_REALTIME        0
/* Monotonic system-wide clock, Clock that cannot be set and represents monotonic time since some unspecified starting point  */
#define CLOCK_MONOTONIC        1
/* High-resolution timer from the CPU. (since Linux 2.6.12) */
#define CLOCK_PROCESS_CPUTIME_ID  2    
/* Thread-specific CPU-time clock. (since Linux 2.6.12) */
#define CLOCK_THREAD_CPUTIME_ID    3
/* Monotonic system-wide clock, not adjusted for frequency scaling.  */
#define CLOCK_MONOTONIC_RAW        4
/* Identifier for system-wide realtime clock, updated only on ticks.  */
#define CLOCK_REALTIME_COARSE    5
/* Monotonic system-wide clock, updated only on ticks.  */
#define CLOCK_MONOTONIC_COARSE    6

CLOCK_REALTIME : 这种时钟表示的是绝对时间, 指的是从1970年1月1月0:00到目前经过多少秒, 相当于你的linux系统中显示的时间, 所以这个时间是可以更改的, 当系统的时钟源被改变,或者系统管理员重置了系统时间之后,这种类型的时钟可以得到相应的调整, 对设定为此类型的timer是有影响的.

CLOCK_MONOTONIC : 这种时钟表示的是相对时间, 其值对通过累积时钟节拍(嘀嗒)计算出来的, 不受时钟源等的影响, 从系统启动这一刻起开始计时, 如果你想计算出在一台计算机上不受重启的影响,两个事件发生的间隔时间的话,那么它将是最好的选择。

CLOCK_PROCESS_CPUTIME_ID : 测量调用进程(包括该进程内所有的线程)用户和系统消耗的总CPU时间.

CLOCK_THREAD_CPUTIME_ID : 测量调用线程消耗的CPU时间.


 struct timeval

The <sys/time.h> header shall define the timeval structure, which shall include at least the following members:

struct timeval
{
    time_t         tv_sec;    // Seconds(秒). 
    suseconds_t    tv_usec;   // Microseconds(微秒:千分之一毫秒). 
};

strucitimerval

The <sys/time.h> header shall define the itimerval structure, which shall include at least the following members:

struct itimerval
{
    struct timeval it_interval;  /* Timer interval(timer循环时间间隔) */
    struct timeval it_value;     /* Initial expiration(timer初次到期时间间隔) */
};

POSIX时钟创建、初始化以及删除一个定时器的操作被分为三个不同的函数:timer_create()(创建定时器)、timer_settime()(初始化定时器)以及timer_delete(销毁它)。


使用timer定时器时注意两点

1.定时器创建的线程回调函数,如果不是C函数而是C++的类成员函数,则不能用普通成员函数,必须用静态成员函数,因为普通成员函数含有隐含参数--this指针

2.timer定时器的时间间隔,第一次是ts.it_value这么长,后面每次时间间隔是ts.it_interval这么长

    ts.it_interval.tv_sec = 0;            //定时器第一次之后的每次时间间隔

    ts.it_interval.tv_nsec = 200000000; //200ms 

    ts.it_value.tv_sec = 0;             //定时器第一次定时的时间

    ts.it_value.tv_nsec = 200000000; //200ms 


感谢链接

http://www.cnblogs.com/LubinLew/p/POSIX-DataStructure.html

http://blog.csdn.net/ustcxiangchun/article/details/6339762

http://www.ccvita.com/508.html

http://www.linuxidc.com/Linux/2011-08/41862.htm


2>. Android的Dex文件相关的重要结构体

//Use this to keep track of mapped segments.
struct MemMapping 
{
	void*  addr;		//start of data
	size_t length;		//length of data
	void*  baseAddr;	//page-aligned base address
	size_t baseLength; 	//length of mapping
};
// odex文件在内存中的存放地址memMap->addr和长度memMap->length
struct DvmDex 
{
    DexFile*               pDexFile;            // odex文件的信息
    const  DexHeader*      pHeader;             // dex文件头相关信息
    struct StringObject**  pResStrings;         // dex文件的字符串结构体
    struct ClassObject**   pResClasses;         // 通过DexFile里的ClassDefItem构造出来的一个结构体(类信息)
    struct Method**        pResMethods;         // 通过Method_Item构造出来的结构体(方法信息)
    struct Field**         pResFields;
    struct AtomicCache*    pInterfaceCache;
    bool                   isMappedReadOnly;
    MemMapping             memMap;				// memMap->addr为odex文件的内存存放位置,memMap->length为odex文件的长度
    jobject                dex_object;
    pthread_mutex_t        modLock;
};
// DexFile结构体存储了整个odex文件在内存的一些信息。
struct DexFile 
{
    //directly-mapped "opt" header 
    const DexOptHeader*         pOptHeader;				//指向odex文件的文件头DexOptHeader的信息

    //pointers to directly-mapped structs and arrays in base DEX
    const DexHeader*            pHeader;				// dex文件的文件头DexHeader的信息
    const DexStringId*          pStringIds;
    const DexTypeId*            pTypeIds;
    const DexFieldId*           pFieldIds;
    const DexMethodId*          pMethodIds;
    const DexProtoId*           pProtoIds;
    const DexClassDef*          pClassDefs;				// 指向dex文件的DexClassDef结构体的指针
    const DexLink*              pLinkData;
    const DexClassLookup*       pClassLookup;
    const void*                 pRegisterMapPool;       // RegisterMapClassPool
    const u1*                   baseAddr;				// dex文件在内存中的存放地址
    int                         overhead;
};
// 类成员方法的结构体
struct Method {  
    ClassObject*        clazz;  		// 该方法所在的类
    u4                  accessFlags;	// 该方法对应的访问属性(native对应0x00100)  
    u2                  methodIndex;  	// 该方法在函数表或者接口表中的偏移
  
    u2                  registersSize;  // 该方法总共用到的寄存器个数
    u2                  outsSize;  		// 当该方法要调用其它方法时,用作参数传递而使用的寄存器个数
    u2                  insSize;  		// 作为调用该方法时,参数传递而使用到的寄存器个数
  
    const char*         name;  			// 该方法的名称
    DexProto            prototype;  	// 对该方法调用参数类型、顺序还有返回类型的描述
    const char*         shorty;  		// 方法对应协议的短表示法,一个字符代表一种类型
    const u2*           insns;  		// 该方法对应的实现的字节码的内存地址
  
    int                 jniArgInfo;  	
    DalvikBridgeFunc    nativeFunc;  	// 本地方法的内存地址
  
    bool                fastJni;  
    bool                noRef;  
    bool                shouldTrace;  
    const RegisterMap*  registerMap;  
    bool                inProfile;  
};  
// DexOrJar->fileName当前apk进程的文件路径
typedef struct DexOrJar {
    char*          fileName;	// 当前apk文件的文件路径
    bool           isDex; 		// 是dex文件还是jar包
    bool           okayToFree;
    RawDexFile*    pRawDexFile;
    JarFile*       pJarFile;  	// 如果是jar包,则指向JarFile结构
    u1* 		   pDexMemory; 	// 如果是dex文件,则指向dex文件存放的内存区
} DexOrJar;


3>. Android的Dex文件重要的结构体在内存中的对应关系图

Dex文件与Odex文件结构对比图

技术分享

DexClassLookup数据结构的定义

技术分享

struct DexFile 结构体成员变量

技术分享

Dex 文件与 DexFile 数据结构映射关系图

技术分享

struct ClassObject 结构体的定义

技术分享

类加载的工作流程以及 ClassObject 结构体的结构图

技术分享

findClassNoInit 函数执行流程图

技术分享


DexClassDef 数据结构的定义

技术分享


loadClassFromDex 函数的执行流程图

技术分享


Method 数据结构的定义

技术分享


类成员方法的method->insns字段的说明:

技术分享


感谢连接

http://blog.csdn.net/roland_sun/article/details/38640297

http://blog.csdn.net/zhangyun438/article/details/17193411

http://blog.csdn.net/qq1084283172/article/details/53584495

http://www.blogfshare.com/defineclassnative.html

Android软件安全与逆向分析

Android Dalvik虚拟机结构及机制剖析(第2卷):Dalvik虚拟机各模块机制分析(有不少小错误,推荐)


4>. DexHunter脱壳工具代码分析

dvm情况下的代码分析,见下面,整个DexHunter脱壳工具代码大分部都理解清楚了,极个别函数的实现不是很理解,后期有时间在看看,代码的注释很全,但是有些地方表述的不是很明白,将就着看吧。art模式下的修改的class_linker.cpp文件的大部分代码一样,但是有一些细节的地方处理不一样,后期再分析了。dvm情况下的DexHunter脱壳工具代码只要是修改Android源码文件/dalvik/vm/native/dalvik_system_DexFile.cpp里的Dalvik_dalvik_system_DexFile_defineClassNative函数的实现,具体操作是Android系统代码调用函数dvmDefineClass进行类加载之前主动地一次性加载并初始化Dex文件所有的类

//------------------------added begin----------------------//

#include <asm/siginfo.h>
#include "libdex/DexClass.h"
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>


// 保存加固类型的特征字符串,其实是由pDexOrJar->fileName决定的
static char dexname[100]={0};
// 保存需要脱壳的apk文件的内存dump出来的part1、data等文件的保存路径
static char dumppath[100]={0};

static bool readable=true;

static pthread_mutex_t read_mutex;

static bool flag=true;

static pthread_mutex_t mutex;

static bool timer_flag=true;

static timer_t timerId;

/***
 * dexname-----特征字符串,由pDexOrJar->fileName决定的(the feature string)
 * dumppath----内存dump文件的保存路径(the data path of the target app )
 * /data/dexname文件的示例:
 * /data/data/com.example.seventyfour.tencenttest/files/libmobisecy1.zip
 * /data/data/com.example.seventyfour.tencenttest/
 * 
 * dexname(the feature string)的引用,具体参见作者zyq8709给出的slide.pptx
 * 重要的提醒:
 * Its line ending should be in the style of Unix/Linux
 * 意思就是/data/dexname文件中的字符串的结尾换行必须是Unix/Linux格式0A的不能是windows的0D0A
 ***/

struct arg
{
    DvmDex* pDvmDex;
    Object * loader;
} param;

/*******删除定时器********/ 
void timer_thread(sigval_t)
{
    timer_flag = false;
    timer_delete(timerId);
    ALOGI("GOT IT time up");
}

/*****读取配置文件/data/dexname中的数据信息并创建初始化定时器******/
void* ReadThread(void *arg) 
{
	
    FILE *fp = NULL;
    while (dexname[0]==0 || dumppath[0]==0) 
    {
		// 打开脱壳的配置文件/data/dexname
		// 脱壳的时,需要adb push dexname /data到Andrid系统里
		// 配置文件/data/dexname是可以自定义的,一些加固如梆梆可能会检测这个路径
        fp=fopen("/data/dexname", "r");
        if (fp==NULL) 
        {
            sleep(1);
            continue;
        }
        
        // 读取文件中的第1行字符串---加固的特征字符串
        fgets(dexname, 99, fp);
        dexname[strlen(dexname)-1]=0;
        
        // 读取文件中的第2行字符串---需要脱壳的apk文件的数据路径
        fgets(dumppath,99,fp);
        dumppath[strlen(dumppath)-1]=0;
        
        fclose(fp);
        fp=NULL;
    }

    struct sigevent sev;
    // 定时器事件类型为创建线程
    sev.sigev_notify=SIGEV_THREAD;
    // 设置事件的线程回调函数的传入参数
    sev.sigev_value.sival_ptr=&timerId;
    // 设置事件的线程回调函数--删除定时器
    sev.sigev_notify_function=timer_thread;
    // 设置事件的线程回调函数的属性
    sev.sigev_notify_attributes = NULL;

    // 创建定时器
    timer_create(CLOCK_REALTIME, &sev, &timerId);

    struct itimerspec ts;
    // 定时器的第一次时间间隔
    ts.it_value.tv_sec=5;
    ts.it_value.tv_nsec=0;
    // 定时器的第一次之后的每次时间间隔
    ts.it_interval.tv_sec=0;
    ts.it_interval.tv_nsec=0;

	// 初始化定时器
    timer_settime(timerId, 0, &ts, NULL);

    return NULL;
}

/****获取一个类中静态字段,实例字段,直接方法和虚方法的个数*****/
void ReadClassDataHeader(const uint8_t** pData,
        DexClassDataHeader *pHeader) 
{
	// 静态成员变量的个数
    pHeader->staticFieldsSize = readUnsignedLeb128(pData);
    // 实例成员变量的个数
    pHeader->instanceFieldsSize = readUnsignedLeb128(pData);
    // 直接成员方法的个数
    pHeader->directMethodsSize = readUnsignedLeb128(pData);
    // 虚成员方法的个数
    pHeader->virtualMethodsSize = readUnsignedLeb128(pData);
}

/****获取一个类中成员变量的信息****/
void ReadClassDataField(const uint8_t** pData, DexField* pField) 
{
	// 成员变量的DexFieldId索引
    pField->fieldIdx = readUnsignedLeb128(pData);
    // 成员变量的访问权限 
    pField->accessFlags = readUnsignedLeb128(pData);
}

/***获取一个类中成员方法的信息****/
void ReadClassDataMethod(const uint8_t** pData, DexMethod* pMethod) 
{
	// 成员方法的DexMethodId索引
    pMethod->methodIdx = readUnsignedLeb128(pData);
    // 成员方法的访问权限
    pMethod->accessFlags = readUnsignedLeb128(pData);
    // 成员方法的实现字节码DexCode的数据结构
    pMethod->codeOff = readUnsignedLeb128(pData);
}

/***获取一个类的DexClassData和所有成员变量以及个数、成员方法以及个数的结构体的信息*****/
DexClassData* ReadClassData(const uint8_t** pData) 
{
	// 类信息的数据头
    DexClassDataHeader header;

    if (*pData =http://www.mamicode.com/= NULL) >


5>. DexHunter脱壳工具的需要的配置文件dexname的说明

DexHunter脱壳工具的在脱壳的时候需要构造配置文件dexname并且dexname文件中的字符换行必须是Unix/Linux格式0A的不能是windows的0D0A,特别是在windows平台上使用DexHunter作者提供的system.img镜像的同学要注意了。DexHunter脱壳工具的配置文件的dexname在使用的时候,需要adb push 到Android设备的/data/路径下[/data/dexname]。

技术分享


脱壳配置文件/data/dexname的路径和文件名称是可以自定修改的,具体的使用位置见下面的代码截图。dexname文件中包含2行字符串,第一行字符串(the feature string)--特征字符串,是有加固app的pDexOrJar->fileName决定的,第2行字符串是用于内存dump文件如:part1、data、classdef和extra以及最终脱壳文件whole.dex存放位置也即目标加固app的数据目录。特别说下,DexHunter工具的作者在github上说特征字符串the feature string在其演讲的ppt--slide.pptx上,但是呢,我觉的不能死板,还是要根据作者的DexHunter中的代码引用为准,按照加固app的实际pDexOrJar->fileName来确定。

技术分享


脱壳配置文件/data/dexname中的2行字符串内容被引用的位置:技术分享


DexHunter作者在slide.pptx上给出的特征字符串the feature string):

技术分享


DexHunter脱壳工具内存dump文件产生的文件part1、data、classdef和extra合成脱壳后的whole.dex文件的过程:

技术分享


重建加固前的app的dex文件

技术分享


6>. DexHunter脱壳工具的个人DIY

DexHunter脱壳工具的作者提供给读者的DexHunter工具的Android镜像文件是基于Android 4.4.3的源码编译的。根据作者zyq8709提供的有关DexHunter脱壳工具的详细信息,我们可以自己按照的作者的脱壳思路和实现手法修改自己版本的Android源码/dalvik/vm/native/dalvik_system_DexFile.cpp里的Dalvik_dalvik_system_DexFile_defineClassNative函数的实现,来打造自己的DexHunter脱壳工具,有能力的大牛甚至可以根据最近的加固对抗思路,扩展DexHunter的代码,打造终极Android通用脱壳工具。DIY自己的DexHunter工具的时,既可以使用make命令编译一遍所有的Android源码产生system.img镜像文件部署到Android真机或者Android模拟器上测试,也可以使用Android源码的模块编译命令mm或者mmm(使用前提Android源码已经make编译一次成功)只编译Android源码的dalvik模块产生libdvm.so库文件然后通过修改Android系统的内存分区属性替换掉Android真机或者模拟器上原来的system/lib/libdvm.so文件,然后进行DexHunter的脱壳测试。


7>. DexHunter工具的几点说明

1.DexHunter作者zyq8709在github上给的建议:

技术分享

技术分享


2.DexHunter脱壳工具的第1行---特征字符串(the feature string)一直是在随着加固的对抗在变化的,具体是由当前加固app的pDexOrJar->fileName决定的,按照实际情况来;一些加固像梆梆什么的,会对DexHunter进行检测,检测的方法是检测Android系统的/data/目录下是否有文件/data/dexname,过掉的方式也比较简单,将DexHunter脱壳需要的配置文件dexname改为其他的名称就行;一些加固像梆梆什么的,会Hook一些系统的调用函数如fread、fwrite、fopen等,过掉方式是直接使用系统调用替代掉如:open、write等;一些加固像梆梆、百度什么的,还会采用类成员方法的实现代码抽取的方式对抗脱壳工具(只有代码在需要执行的时候才会被还原,执行完成以后又会被抽去掉)可能会采用比Dalvik_dalvik_system_DexFile_defineClassNative函数更深层的调用函数的Hook,如Hook掉libdvm.so库中dvmResolveClass 函数什么的,导致DexHunter内存dump获取不到dex文件的原代码,因此需要对Hook的dvmResolveClass 函数进行修改才能使DexHunter工具发挥作用。


在dvmResolveClass中进行修改的代码片段如下(以directMethod为例):

技术分享


感谢链接

https://github.com/zyq8709/DexHunter

http://www.liuhaihua.cn/archives/377032.html

http://blog.csdn.net/zhangyun438/article/details/17192787

http://www.cnblogs.com/vendanner/p/4844235.html

http://blog.csdn.net/qq1084283172/article/details/53710357

http://blog.csdn.net/zhangyun438/article/details/17193411

http://blog.csdn.net/justfwd/article/details/51164281

http://www.cnblogs.com/vendanner/p/4884969.html

http://bbs.pediy.com/showthread.php?t=203776



3.其他的大牛对DexHunter工具的弱点的总结:

技术分享


感谢链接:

https://my.oschina.net/cve2015/blog/734360


OK,完成了,格式有点乱。DexHunter脱壳工具的DVM模式下的原理代码已经搞得比较清楚了,后面再补补课分析一下ART模式下DexHunter脱壳工具的原理的代码及其细节。





Android通用脱壳工具DexHunter的原理分析和使用说明(二)