Dex文件全解析(C语言实现),后篇


前言(必看)

本篇内容主要解析dex文件中的类以及它的相关所有结构。
主要包括:DexClassDef,DexClassData,DexClassDataHeader,DexField,DexMethod以及DexCode。
书接上文,请读者熟悉一下上篇对其余基础结构的解析,方便本篇直接使用和理解,链接:Dex文件全解析中篇
同时,本文中含有很多很多uleb128的坑,请读者也回顾下uleb128值的计算,本篇直接调用函数,链接:MUTF-8以及uleb128提取计算

小贴士:本篇中对于DexCode这个结构,也就是指示函数真正的方法指令的结构,我们以找到它相对稳健开头的偏移为最终结果,之后的文章中会更新内存运行时修改指令信息的帖子


一、DexClassDef结构

DexClassDef结构官方定义:

(结构意义:描述一个类的大略的全部信息)

struct DexClassDef{
	u4 classIdx;//类类型,指向DexTypeId索引
	u4 accessFlags;//类访问标志,public、interface等
	u4 superclassIdx;//父类类型
	u4 interfacesOff;//指向DexTypeList的偏移,类实现的接口
	u4 sourceFileIdx;//源文件名字符串
	u4 annotationsOff;
	u4 classDataOff;//指向DexClassData的偏移
	u4 staticValuesOff;//静态数据偏移
}

用于提取数据的自定义结构:

struct dexClassDef {
	int classId;//-->typeTable
	int accessFlags;//-->flag
	int superClassId;//-->typeTable
	//注意,使用时加上基址一起,否则直接崩
	struct dexTypeList* interfacesOff;//-->typeList,
	int sourceFileId;//-->stringTable
	int annotationsOff;//-->annotation offset
	//注意,使用时加上基址一起,否则直接崩
	//这个指针需要先拿到四个整数值以及uleb128数字的字节数才可以后续计算,即放进函数getFourCount进行计算
	//指针位置加uleb128长度就是四项中第一个非零项起始地址
	//从此开始,到DexCode之前,全是uleb128
	char* classDataOff;
	int staticValuesOff;
};

同样的,接上篇操作,我们读取文件,赋值dex文件头指针,就可以拿到DexClassDef的个数和偏移。
接着,我们开始操作类,对于一个类,我们拿到它的类名和flags并输出。
这里需要注意一点也是上篇就说过的:文件存储的DexClassDef结构是连续存放的,即便它子结构很多,但是我们也可以使用结构体指针直接加1的方式直接访问下一个类结构数据,嗯,没毛病。

int classSize = head->class_defs_size;
struct dexClassDef* classPosition = (struct dexClassDef*)((char*)buffer + (head->class_defs_off));
struct dexClassDef* trueClass = (struct dexClassDef*)malloc(sizeof(struct dexClassDef) * classSize);
for (int i = 0; i < classSize; i++) {
		trueClass[i] = classPosition[i];
		char* start = (char*)((char*)buffer + (int)trueClass[i].classDataOff);
		if (trueClass[i].classDataOff==0) {
			printf("abstract/interface class name is %s\n", trueType[trueClass[i].classId]);
			continue;
		}else printf("class name is %s\n",trueType[trueClass[i].classId]);

接着,我们开始解析今天的重头戏:DexClassData
这里有一个注意点:一个类的ClassDataOff字段为0,说明它没有成员变量和方法,我们直接跳过下面的对于成员变量和方法的解析,否则,这,这uleb128横飞的战场会让之后的解析直接崩掉的

二、DexClassData和DexClassDataHeader

1:结构体官方定义

struct DexClassDataHeader{
	uleb128 staticFieldSize;
	uleb128 instanceFieldSize;
	uleb128 directMethodSize;
	uleb128 virtualMethodSize;
}
struct DexClassDex{
	DexClassDataHeader header;
	DexField*  staticFields;
	DexField*  instanceField;
	DexMethod* directMethods;
	DexMethod* vartualMethods;
}

2:重点

给出自定义结构之前需要说明:这里有一个很大的坑。
首先,DexClassDex文件中所有数据都是uleb128,包括之后的偏移,所以,DexClassDataHeader这个结构体它的长度取决于这四个uleb128的长度
同时,接着的几个成员变量,也就是类中的成员和方法的偏移所处位置,就是DexClassDataHeader之后。
什么意思呢?就是说,DexClassDex结构中,位于DexClassDataHeader结构内存之后,拥有这个结构中提取出的四个整数的和的个数个uleb128整数,而每一个整数,就是一个偏移,这里很绕,我们拿图片说话


在这里插入图片描述


大家看着重部分,是DexClassData的数据部分,我们先手工解析出四个uleb128数,分别是1、0、2、0,这四个就对应了静态成员,实例成员,直接方法和虚方法的个数。
重点来了:也就是:我们在四个整数的位置之后需要解析出1+0+2+0个uleb128的整数,而第一个整数对应一个静态成员对应的DexField数据相对文件头的偏移,第二和第三个整数对应直接方法的DexMethod数据相对文件头的偏移
所以,数据的终点我们是未知的,但我们按数量提取,就不会错掉

3:自定义结构类型

struct classDataInfo {
	int countOfstaticFields;
	int countOfinstanceFields;
	int countOfdirectMethods;
	int countOfvirtualMethods;
	int lengthOfUleb128;
};
struct dexClassDef {
	int classId;//-->typeTable
	int accessFlags;//-->flag
	int superClassId;//-->typeTable
	//注意,使用时加上基址一起,否则直接崩
	struct dexTypeList* interfacesOff;//-->typeList,
	int sourceFileId;//-->stringTable
	int annotationsOff;//-->annotation offset
	//注意,使用时加上基址一起,否则直接崩
	//这个指针需要先拿到四个整数值以及uleb128数字的字节数才可以后续计算,即放进函数getFourCount进行计算
	//指针位置加uleb128长度就是四项中第一个非零项起始地址
	//从此开始,到DexCode之前,全是uleb128
	char* classDataOff;
	int staticValuesOff;
};

这里需要注意的点:我直接把ClassDataOff作为一个指针了,它在使用时需要加上缓冲的基址
而char*的用处:我这边直接写了一个函数,可以由char*直接拿到四个uleb128的整数,返回的是上方定义的ClassDataInfo结构体指针,这样可以直接拿到类的成员和方法数量以及知道header的长度,以便之后操作

4:由一个起始地址解析出四个uleb128数

代码如下:(参考第一篇文章中uleb128的计算,我们这里不多介绍,可参考注释食用)

struct classDataInfo* getFourCount(char* start) {
	int count[4] = { 1,1,1,1 };//初始化,四个uleb128字节数都为1
	struct classDataInfo* result = (struct classDataInfo*)malloc(sizeof(struct classDataInfo));
	//每一个uleb128结果临时存放位置,由于for循环,数组方便赋值
	int tmp[4] = { 0 };
	//循环四次,拿到四个uleb128
	for (int i = 0; i < 4; i++) {
	//开始取数,每次运算完毕,指针已经指向下一个uleb128
		unsigned int result = *(start++);
		if (result > 0x0000007f) {
			count[i]++;
			unsigned int cur = *(start++);
			result = (result & 0x0000007f) | ((cur & 0x0000007f) << 7);
			if (cur > 0x0000007f) {
				count[i]++;
				cur = *(start++);
				result |= ((cur & 0x0000007f) << 14);
				if (cur > 0x0000007f) {
					count[i]++;
					cur = *(start++);
					result |= ((cur & 0x0000007f) << 21);
					if (cur > 0x0000007f) {
						cur = *(start++);
						count[i]++;
						result |= (cur << 28);
					}
				}
			}
		}
		tmp[i] = result;
	}
	//结果赋值,返回
	result->countOfstaticFields = tmp[0];
	result->countOfinstanceFields = tmp[1];
	result->countOfdirectMethods = tmp[2];
	result->countOfvirtualMethods = tmp[3];
	result->lengthOfUleb128 = (count[0]+count[1]+count[2]+count[3]);
	return result;
}

好啦,到这里,我们已经可以成功解析出成员和方法的个数啦,接着看一下解析的代码细节:

char* start = (char*)((char*)buffer + (int)trueClass[i].classDataOff);
struct classDataInfo*classInfo=getFourCount(start);
start += classInfo->lengthOfUleb128;

注意指针后移,我们还要取出DexField和DexMethod
这里需要注意:取出四个uleb128之后,指针后移,指向的位置作为新的uleb128数起点,终点未知,但我们按数量提取即可,取出的数作为偏移和字段以及方法一一对应

三:DexField和DexMethod

1)根据起始地址提取一个uleb128数,并返回uleb128数字节数

书接上文,我们为什么需要这么一个函数呢,因为静态字段,实例字段,直接方法和虚方法个数未知,我们只能根据数量,提取数与数量对应的偏移,方法代码如下:

此处由于多定义结构体的繁琐,我直接定义参数作为另一个返回值也就是uleb128数字节数,传入指针,对指针赋值,外部的值同时改变

//由uleb128起始地址拿到整数值的方法
//#define OUT    #define IN表示参数需要传出
int calculateUleb128(char* start,OUT int *length) {
	int count = 1;
	unsigned int result = *(start++);
	if (result > 0x0000007f) {
		count++;
		unsigned int cur = *(start++);
		result = (result & 0x0000007f) | ((cur & 0x0000007f) << 7);
		if (cur > 0x0000007f) {
			count++;
			cur = *(start++);
			result |= ((cur & 0x0000007f) << 14);
			if (cur > 0x0000007f) {
				count++;
				cur = *(start++);
				result |= ((cur & 0x0000007f) << 21);
				if (cur > 0x0000007f) {
					cur = *(start++);
					count++;
					result |= (cur << 28);
				}
			}
		}
	}
	(*length) = count;
	return result;
}

result就是我们的uleb128的值啦

2)结构官方定义:

结构意义:类字段和方法的细节描述,包括访问标志,方法DexCodeOff等细节

struct DexField{
	uleb128 fieldIdx;//指向DexFieldId索引
	uleb128 axxessFlags;//字段访问标志
}
struct DexMethod{
	uleb128 methodIdx;//指向DexMethodId索引
	uleb128 accessFlags;//方法访问标志
	uleb128 codeOff;//指向DexCode结构偏移
}

这里我们就不自定义多余类型了。
需要注意的是,这两个结构中数也是uleb128,所有会有些许麻烦
我们使用循环,依次解析静态字段,实例字段,直接方法和虚方法
解析静态字段:(注意uleb128的计算和指针后移)

int tmpCount=classInfo->countOfstaticFields;
		if (tmpCount > 0) {
			printf("	static Fields \n");
			while (tmpCount--) {
				int tmpLength = 0;
				int fieldIndexOfField = calculateUleb128(start, &tmpLength);
				//指针后移,后续使用tmpLength
				start += tmpLength;
				//按照field输出格式输出这个字段,输出field的更多细节
				printf("	%d %s %d %s %d %s ", trueField[fieldIndexOfField].classId, trueType[trueField[fieldIndexOfField].classId],
					trueField[fieldIndexOfField].typeId, trueType[trueField[fieldIndexOfField].typeId], trueField[fieldIndexOfField].nameId, 
					trueString[trueField[fieldIndexOfField].nameId]);
				int accFlags= calculateUleb128((char*)start, &tmpLength);
				printf(" accflag=0x%X\n", accFlags);
				start += tmpLength;
			}
		}

解析实例字段:(代码相同,只是输出部分不同)

tmpCount = classInfo->countOfinstanceFields;
		if (tmpCount > 0) {
			printf("	instance Field \n");
			while (tmpCount--) {
				int tmpLength = 0;
				int fieldIndexOfField = calculateUleb128(start, &tmpLength);
				//指针后移,后续使用tmpLength
				start += tmpLength;

				printf("	%d %s %d %s %d %s ", trueField[fieldIndexOfField].classId, trueType[trueField[fieldIndexOfField].classId],
					trueField[fieldIndexOfField].typeId, trueType[trueField[fieldIndexOfField].typeId], trueField[fieldIndexOfField].nameId,
					trueString[trueField[fieldIndexOfField].nameId]);
				int accFlags = calculateUleb128((char*)start, &tmpLength);
				printf(" accflag=0x%x\n", accFlags);
				start += tmpLength;
			}
		}

解析直接方法:

tmpCount = classInfo->countOfdirectMethods;
		if (tmpCount > 0) {
			printf("	%d direct method \n",tmpCount);
			while (tmpCount--) {
				int tmpLength = 0;
				//因为懒所以没换变量名
				int fieldIndexOfField = calculateUleb128(start, &tmpLength);
				//指针后移,后续使用tmpLength
				start += tmpLength;

				printf("method table index is %d ", fieldIndexOfField);
				int accFlags = calculateUleb128((char*)start, &tmpLength);
				printf(" accflag=%x ", accFlags);
				start += tmpLength;
				int offsetOfCodeItem = calculateUleb128((char*)start, &tmpLength);
				printf("codeitem offset is %x \n", offsetOfCodeItem);
				start += tmpLength;
			}
		}

解析虚方法:(代码和解析直接方法基本相同)

tmpCount = classInfo->countOfdirectMethods;
		if (tmpCount > 0) {
			printf("	%d virtual method \n",tmpCount);
			while (tmpCount--) {
				int tmpLength = 0;
				//因为懒所以没换变量名
				int fieldIndexOfField = calculateUleb128(start, &tmpLength);
				//指针后移,后续使用tmpLength
				start += tmpLength;

				printf("method table index is %d ", fieldIndexOfField);
				int accFlags = calculateUleb128((char*)start, &tmpLength);
				printf(" accflag=%x ", accFlags);
				start += tmpLength;
				int offsetOfCodeItem = calculateUleb128((char*)start, &tmpLength);
				printf("codeitem offset is %x \n", offsetOfCodeItem);
				start += tmpLength;
			}
		}

好啦,到这里,我们就解析的差不多啦
注意:int offsetOfCodeItem = calculateUleb128((char*)start, &tmpLength);
这里我们已经拿到方法对应的偏移了。接下来是硬核阶段喽

四:DexCode或称为CodeItem

1)结构体官方定义:

struct DexCode{
	u2 registersSize;//方法使用寄存器个数
	u2 insSize;//参数个数
	u2 outsSize;//调用其他方法使用参数个数
	u2 triesSize;
	u4 debugInfoOff;
	u4 insnsSize;//方法指令长度,二字节为单位
	u2 insns[1];//方法指令
	//接下来结构可能有,可能没有,最后两个由是否有try和catch决定
	ushort paddding; //二字节用于对齐
	try_item tries [ tyies_size ]; 
	encoded_catch_handler_list handlers; 
}

本篇我们不对这个结构做过多解析。读者可自行解析,根据偏移+16即为方法指令位置
我们只是把一个方法的细节做一个文件输出,拼接类名,方法名和偏移作为方法的唯一标识以便后续文章中找到方法并置空指令完成函数抽取模拟的操作。
本篇中涉及部分的文件操作,方便我们看到结果,代码如下:

FILE* methodDetail = NULL;
test = fopen_s(&methodDetail, "methos.txt", "wt+");
if (test)printf("txt打开失败\n");
tmpCount = classInfo->countOfdirectMethods;
		if (tmpCount > 0) {
			printf("	%d direct method \n",tmpCount);
			while (tmpCount--) {
				int tmpLength = 0;
				int fieldIndexOfField = calculateUleb128(start, &tmpLength);
				//指针后移,后续使用tmpLength
				start += tmpLength;

				//拼接字符串,类名加方法名
				fwrite(trueType[trueClass[i].classId], strlen(trueType[trueClass[i].classId]), 1, methodDetail);
				fprintf(methodDetail, "#");
				fwrite(trueString[trueMethod[fieldIndexOfField].nameId], strlen(trueString[trueMethod[fieldIndexOfField].nameId]), 1, methodDetail);
				fprintf(methodDetail, "--->fileOff 0x");
				printf("method table index is %d ", fieldIndexOfField);
				int accFlags = calculateUleb128((char*)start, &tmpLength);
				printf(" accflag=%x ", accFlags);
				start += tmpLength;
				int offsetOfCodeItem = calculateUleb128((char*)start, &tmpLength);
				//拼接字符串,加方法偏移
				char offsetString[16];
				_itoa_s(offsetOfCodeItem, offsetString, 16, 16);
				fwrite(offsetString, strlen(offsetString), 1, methodDetail);
				fprintf(methodDetail, "\n");

				printf("codeitem offset is %x \n", offsetOfCodeItem);
				start += tmpLength;
			}
		}

(虚方法中抽取的代码相同,我们直接看结果的截图,解析的一个较大的dex文件)
在这里插入图片描述


五:总结

好啦,本篇中对于类的详细解析到此就结束喽,想对方法codeItem做更多操作的可自行实现。
需要源码或有问题的可私聊,源码较长,就不贴出来啦
小贴士:自己写出来真的很重要很重要
感谢大家的观看,浏览,如有错误,欢迎指正,指导

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值