首页 > 代码库 > 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(微秒:千分之一毫秒).
};
struct itimerval
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;
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的原理分析和使用说明(二)