原文链接:网络安全编程:开发Dex文件格式解析工具
解析Dex文件的工作应该是自动化的,由工具去完成。本文通过VS2012来新建一个控制台的工程,然后完成一个Dex文件的解析工具。
对于解析Dex文件而言,需要准备一些头文件,这些头文件都可以从安卓系统的源代码中获取到,首先要有common.h、uleb128.h,因为common.h中存放了相应的数据类型(这里所说的数据类型是u1、u2),uleb128.h中存放了读取uleb128数据类型的相关函数。接着要准备的是DexFile.h、DexFile.cpp、DexClass.h和DexClass.cpp 4个文件。
为了使用方便,将这4个文件中的代码都复制到了DexParse.h中,为了能够编译通过,在函数的定义部分进行了删除,或者对某些函数的参数进行了修改,对函数体的一些内容也进行了删减。
在自己准备相关内容时,可以在编译时通过报错信息自己进行修改。在这里,将DexParse.h文件添加到了新建的控制台工程当中。
解析Dex文件也按照Dex的格式逐步进行即可,当然在解析文件前请不要忘记,对文件的操作首先是要打开文件。
1. 打开与关闭文件
打开与关闭文件的代码如下: - int _tmain(int argc, _TCHAR* argv[])
- {
- HANDLE hFile = CreateFile(DEX_FILE, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_
- EXISTING, FILE_ACTION_ADDED, NULL);
- HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
- LPVOID hView = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
- UnmapViewOfFile(hView);
- CloseHandle(hMap);
- CloseHandle(hFile);
- return 0;
- }
复制代码
在上面的代码中,首先要打开文件,然后创建文件映射,在MapViewOfFile函数和UnmapViewOfFile函数之间,来添加关于解析DEX文件的代码。
2. Dex文件头部
在解析Dex文件时,需要对Dex文件的头部进行解析,解析Dex文件的头部时,安卓系统提供了一个函数,函数定义如下: DexFile* dexFileParse(const u1* data, size_t length, int flags); 该函数有3个参数,第一个参数是Dex文件数据的起始位置,第二个参数是Dex文件的长度,第三个参数是用来告诉dexFileParse函数是否需要进行验证的。对于目前阶段而言,我们不需要第三个参数,因此将该函数进行删减后的代码如下: - DexFile* dexFileParse(const u1* data, size_t length)
- {
- DexFile* pDexFile = NULL;
- const DexHeader* pHeader;
- const u1* magic;
- int result = -1;
- pDexFile = (DexFile*) malloc(sizeof(DexFile));
- if (pDexFile == NULL)
- goto bail;
- memset(pDexFile, 0, sizeof(DexFile));
- /*
- * 去掉优化的头部
- */
- if (memcmp(data, DEX_OPT_MAGIC, 4) == 0) {
- magic = data;
- if (memcmp(magic+4, DEX_OPT_MAGIC_VERS, 4) != 0) {
- goto bail;
- }
- /* 忽略可选的头部和在这里追加的数据
- data += pDexFile->pOptHeader->dexOffset;
- length -= pDexFile->pOptHeader->dexOffset;
- if (pDexFile->pOptHeader->dexLength > length) {
- goto bail;
- }
- length = pDexFile->pOptHeader->dexLength;
- }
- dexFileSetupBasicPointers(pDexFile, data);
- pHeader = pDexFile->pHeader;
- /*
- * Success!
- */
- result = 0;
- bail:
- if (result != 0 && pDexFile != NULL) {
- dexFileFree(pDexFile);
- pDexFile = NULL;
- }
- return pDexFile;
- }
复制代码 该函数首先判断Dex文件的合法性,然后将Dex文件的一些基础的指针进行了初始化,在dexFileParse函数中调用了另外一个函数,即dexFileSetupBasicPointers函数,该函数的函数体如下:
- void dexFileSetupBasicPointers(DexFile* pDexFile, const u1* data) {
- DexHeader *pHeader = (DexHeader*) data;
- pDexFile->baseAddr = data;
- pDexFile->pHeader = pHeader;
- pDexFile->pStringIds = (const DexStringId*) (data + pHeader->stringIdsOff);
- pDexFile->pTypeIds = (const DexTypeId*) (data + pHeader->typeIdsOff);
- pDexFile->pFieldIds = (const DexFieldId*) (data + pHeader->fieldIdsOff);
- pDexFile->pMethodIds = (const DexMethodId*) (data + pHeader->methodIdsOff);
- pDexFile->pProtoIds = (const DexProtoId*) (data + pHeader->protoIdsOff);
- pDexFile->pClassDefs = (const DexClassDef*) (data + pHeader->classDefsOff);
- pDexFile->pLinkData = (const DexLink*) (data + pHeader->linkOff);
- }
复制代码
从dexFileSetupBasicPointers函数中可以看出,对于其他各个结构体的索引及数量已经在这里全部读取出来,在后面具体解析其他数据结构时,它会很方便地被使用。
在dexFileParse中使用malloc函数申请了一块空间,这块空间在解析完成以后需要手动地进行释放,在安卓系统的源码中也定义了一个函数以方便使用,函数名是dexFileFree,函数的定义如下:- void dexFileFree(DexFile* pDexFile)
- {
- if (pDexFile == NULL)
- return;
- free(pDexFile);
- }
复制代码
很简单的函数,判断指针是否为NULL,不为NULL则直接调用free函数释放空间。
有了上面的代码,那么就可以完成解析Dex文件的第一步了,具体代码如下:- DWORD dwSize = GetFileSize(hFile, NULL);
- DexFile *pDexFile = dexFileParse((const u1 *)hView, (size_t)dwSize);
- dexFileFree(pDexFile);
复制代码 这样就得到了指向DexFile结构体的指针pDexFile,DexFile结构体的定义如下:
- struct DexFile {
- /* 直接映射的"opt"头部 */
- const DexOptHeader* pOptHeader;
- /* 指向基础 DEX 中直接映射的结构体和数组的指针 */
- const DexHeader* pHeader;
- const DexStringId* pStringIds;
- const DexTypeId* pTypeIds;
- const DexFieldId* pFieldIds;
- const DexMethodId* pMethodIds;
- const DexProtoId* pProtoIds;
- const DexClassDef* pClassDefs;
- const DexLink* pLinkData;
- /*
- * 这些不映射到"auxillary"部分,可能不包含在该文件中
- */
- const DexClassLookup* pClassLookup;
- const void* pRegisterMapPool; // RegisterMapClassPool
- /* 指向 DEX 文件开始的指针 */
- const u1* baseAddr;
- /* 跟踪辅助结构的内存开销 */
- int overhead;
- /* 与 DEX 相关联的其他数据结构 */
- //void* auxData;
- };
复制代码
对于我们而言,在写程序时只需要关心结构体中DexHeader到DexClassDef之间的字段即可。
之后解析的代码中都会使用到返回的pDexFile指针,因此之后缩写的代码都必须写在调用dexFileFree函数之前。3. 解析DexMapList相关数据
DexMapList是在DexHeader的mapOff给出的,不过在程序中不用直接从DexHeader结构体中去取,因为在安卓系统中已经给出了相关的函数,函数代码如下:- DEX_INLINE const DexMapList* dexGetMap(const DexFile* pDexFile) {
- u4 mapOff = pDexFile->pHeader->mapOff;
- if (mapOff == 0) {
- return NULL;
- } else {
- return (const DexMapList*) (pDexFile->baseAddr + mapOff);
- }
- }
复制代码
dexGetMap函数通过前面返回的DexFile指针来定位DexMapList在文件中的偏移位置。
在实际的代码中,我们需要将DEX_INLINE宏删掉,或者按照安卓系统的源代码中的定义去定义一下。
通过dexGetMap函数获得了DexMapList的指针,那么接下来就可以对DexMapList进行遍历了,这里定义一个自定义函数来进行遍历,代码如下:
- void PrintDexMapList(DexFile *pDexFile)
- {
- const DexMapList *pDexMapList = dexGetMap(pDexFile);
- printf("DexMapList:\r\n");
- printf("TypeDesc\t\t type unused size offset\r\n");
- for ( u4 i = 0; i < pDexMapList->size; i ++ )
- {
- switch (pDexMapList->list[i].type)
- {
- case 0x0000:printf("kDexTypeHeaderItem");break;
- case 0x0001:printf("kDexTypeStringIdItem");break;
- case 0x0002:printf("kDexTypeTypeIdItem");break;
- case 0x0003:printf("kDexTypeProtoIdItem");break;
- case 0x0004:printf("kDexTypeFieldIdItem");break;
- case 0x0005:printf("kDexTypeMethodIdItem");break;
- case 0x0006:printf("kDexTypeClassDefItem");break;
- case 0x1000:printf("kDexTypeMapList");break;
- case 0x1001:printf("kDexTypeTypeList");break;
- case 0x1002:printf("kDexTypeAnnotationSetRefList");break;
- case 0x1003:printf("kDexTypeAnnotationSetItem");break;
- case 0x2000:printf("kDexTypeClassDataItem");break;
- case 0x2001:printf("kDexTypeCodeItem");break;
- case 0x2002:printf("kDexTypeStringDataItem");break;
- case 0x2003:printf("kDexTypeDebugInfoItem");break;
- case 0x2004:printf("kDexTypeAnnotationItem");break;
- case 0x2005:printf("kDexTypeEncodedArrayItem");break;
- case 0x2006:printf("kDexTypeAnnotationsDirectoryItem");break;
- }
- printf("\t %04X %04X %08X %08X\r\n",
- pDexMapList->list[i].type,
- pDexMapList->list[i].unused,
- pDexMapList->list[i].size,
- pDexMapList->list[i].offset);
- }
- }
复制代码
在main函数中调用该函数时,只要将前面得到的指向DexFile结构体的指针传给该函数即可。查看该部分解析的输出,如图1所示。
图1 DexMapList解析后的输出
4. 解析StringIds相关数据
对于StringIds的解析也非常简单,这里直接给出一个自定义函数,代码如下:
- void PrintStringIds(DexFile *pDexFile)
- {
- printf("DexStringIds:\r\n");
- for ( u4 i = 0; i < pDexFile->pHeader->stringIdsSize; i ++ )
- {
- printf("%d.%s \r\n", i, dexStringById(pDexFile, i));
- }
- }
复制代码
在该自定义函数中,它调用了dexStringById函数,也就是通过索引值来得到字符串,该函数的定义如下:
- /* 通过特定的 string_id index 返回 UIF-8 编码的字符串 */
- DEX_INLINE const char* dexStringById(const DexFile* pDexFile, u4 idx) {
- const DexStringId* pStringId = dexGetStringId(pDexFile, idx);
- return dexGetStringData(pDexFile, pStringId);
- }
复制代码
/* 通过特定的 string_id index 返回 UIF-8 编码的字符串 */DEX_INLINE const char* dexStringById(const DexFile* pDexFile, u4 idx) { const DexStringId* pStringId = dexGetStringId(pDexFile, idx); return dexGetStringData(pDexFile, pStringId);}
在dexStringById函数中又调用了两个其他的函数,分别是dexGetStringId和dexGetStringData,大家可以自行查看。
在main函数中调用笔者的自定义函数,输出如图2所示。
图2 StringIds解析后的输出
5. 解析TypeIds相关数据
解析TypeIds也是非常简单的,直接上代码即可,代码如下:
- void PrintTypeIds(DexFile *pDexFile)
- {
- printf("DexTypeIds:\r\n");
- for ( u4 i = 0; i < pDexFile->pHeader->typeIdsSize; i ++ )
- {
- printf("%d %s \r\n", i, dexStringByTypeIdx(pDexFile, i));
- }
- }
复制代码
代码中调用了一个关键的函数dexStringByTypeIdx,该函数也是安卓系统源码中提供的函数,该函数的实现如下:
- /*
- * 获取与指定的类型索引相关联的描述符字符串
- * 调用者不能释放返回的字符串
- */
- DEX_INLINE const char* dexStringByTypeIdx(const DexFile* pDexFile, u4 idx) {
- const DexTypeId* typeId = dexGetTypeId(pDexFile, idx);
- return dexStringById(pDexFile, typeId->descriptorIdx);
- }
复制代码
在dexStringByTypeIdx函数中调用了dexGetTypeId和dexStringById两个函数,请大家自行在源码中查看。
在main函数中调用自定义函数,输出如图3所示。
图3 TypeIds解析后的输出
6. 解析ProtoIds相关数据
Proto是方法的原型或方法的声明,也就是提供了方法的返回值类型、参数个数,以及参数的类型。对于ProtoIds的解析,首先是对原始数据的解析,然后再将它简单地还原为可以直接阅读的方法原型。
先来看一下代码,代码如下:
- void PrintProtoIds(DexFile *pDexFile)
- {
- printf("DexProtoIds:\r\n");
- // 对数据的解析
- for ( u4 i = 0; i < pDexFile->pHeader->protoIdsSize; i ++ )
- {
- const DexProtoId *pDexProtoId = dexGetProtoId(pDexFile, i);
- // 输出原始数据
- printf("%08X %08X %08X \r\n", pDexProtoId->shortyIdx, pDexProtoId->returnTy
- peIdx, pDexProtoId->parametersOff);
- // 输出对应的 TypeId
- printf("%s %s\r\n",
- dexStringById(pDexFile, pDexProtoId->shortyIdx),
- dexStringByTypeIdx(pDexFile, pDexProtoId->returnTypeIdx));
- // 获得参数列表
- const DexTypeList *pDexTypeList = dexGetProtoParameters(pDexFile, pDexProtoId);
- u4 num = pDexTypeList != NULL ? pDexTypeList->size : 0;
- // 输出参数
- for ( u4 j = 0; j < num; j ++ )
- {
- printf("%s ", dexStringByTypeIdx(pDexFile, pDexTypeList->list[j].typeIdx));
- }
- printf("\r\n");
- }
- printf("\r\n");
- // 对解析数据的简单还原
- for ( u4 i = 0; i < pDexFile->pHeader->protoIdsSize; i ++ )
- {
- const DexProtoId *pDexProtoId = dexGetProtoId(pDexFile, i);
- printf("%s", dexStringByTypeIdx(pDexFile, pDexProtoId->returnTypeIdx));
- printf("(");
- // 获得参数列表
- const DexTypeList *pDexTypeList = dexGetProtoParameters(pDexFile, pDexProtoId);
- u4 num = pDexTypeList != NULL ? pDexTypeList->size : 0;
- // 输出参数
- for ( u4 j = 0; j < num; j ++ )
- {
- printf("%s\b, ", dexStringByTypeIdx(pDexFile, pDexTypeList->list[j].typeIdx));
- }
- if ( num == 0 )
- {
- printf(");\r\n");
- }
- else
- {
- printf("\b\b);\r\n");
- }
- }
- }
复制代码
在该自定义函数中有两个for循环,其内容基本一致。第一个循环完成了数据的解析,第二个循环是将数据简单地解析成了方法的原型。
这里只对第一个for循环进行说明。ProtoIds是方法的原型,看一下DexProtoId的定义,定义如下:
- /*
- * Direct-mapped "proto_id_item".
- */
- struct DexProtoId {
- u4 shortyIdx; /* index into stringIds for shorty descriptor */
- u4 returnTypeIdx; /* index into typeIds list for return type */
- u4 parametersOff; /* file offset to type_list for parameter types */
- };
复制代码
第一个字段是方法原型的短描述,第二个字段是方法原型的返回值,第三个字段是指向参数列表的。因此,可以看到,在两个for循环中,仍然嵌套着一个for循环,外层的循环是用来解析方法原型的,内层的循环是用来解析方法原型中的参数的。
首先,通过dexGetProtoId函数来获得ProtoIds,然后通过dexGetProtoParameters函数来得到相应ProtoIds的参数。
在main函数中调用自定义函数,输出如图4所示。
图4 ProtoIds解析后的输出
从图4中可以看出,该Dex文件中有3个方法原型,这里来说一下ProtoIds中的shortyIdx这个简短描述的意思,用第二个方法原型来说明。
第二个方法原型是V(Ljava/lang/String);这种形式,它的简短描述是VL。V表示返回值类型,就是V,而L就是第一个参数的类型。再举个例子,如果简短描述是VII,那么返回值类型是V,然后有两个参数,第一个参数是I类型,第二个参数也是I类型。
7. 解析FieldIds相关数据
FieldIds的解析相对于ProtoIds的解析就简单了,直接上代码:
- void PrintFieldIds(DexFile *pDexFile)
- {
- printf("DexFieldIds:\r\n");
- for ( u4 i = 0; i < pDexFile->pHeader->fieldIdsSize; i ++ )
- {
- const DexFieldId *pDexFieldId = dexGetFieldId(pDexFile, i);
- printf("%04X %04X %08X \r\n", pDexFieldId->classIdx, pDexFieldId->typeIdx,
- pDexFieldId->nameIdx);
- printf("%s %s %s\r\n",
- dexStringByTypeIdx(pDexFile, pDexFieldId->classIdx),
- dexStringByTypeIdx(pDexFile, pDexFieldId->typeIdx),
- dexStringById(pDexFile, pDexFieldId->nameIdx));
- }
- }
复制代码
Field是类中的属性,在DexFieldId中对于类属性有3个字段,分别是属性所属的类、属性的类型和属性的名称。
在main函数中调用自定义函数,输出如图5所示。
图5 FieldIds解析后的输出
8. 解析MethodIds相关数据
MethodIds的解析也分为两部分,第一部分是解析数据,第二部分是简单的还原方法。在DexMethodId中给出了方法所属的类、方法对应的原型,以及方法的名称。在解析ProtoIds的时候,只是方法的原型,并没有给出方法的所属的类,还有方法的名称。在还原方法时,就要借助ProtoIds才能完整地还原方法。
解析MethodIds的代码如下:
- void PrintMethodIds(DexFile *pDexFile)
- {
- printf("DexMethodIds:\r\n");
- // 对数据的解析
- for ( u4 i = 0; i < pDexFile->pHeader->methodIdsSize; i ++ )
- {
- const DexMethodId *pDexMethodId = dexGetMethodId(pDexFile, i);
- printf("%04X %04X %08X \r\n", pDexMethodId->classIdx, pDexMethodId->protoIdx,
- pDexMethodId->nameIdx);
- printf("%s %s \r\n",
- dexStringByTypeIdx(pDexFile, pDexMethodId->classIdx),
- dexStringById(pDexFile, pDexMethodId->nameIdx));
- }
- printf("\r\n");
- // 根据 protoIds 来简单还原方法
- for ( u4 i = 0; i < pDexFile->pHeader->methodIdsSize; i ++ )
- {
- const DexMethodId *pDexMethodId = dexGetMethodId(pDexFile, i);
- const DexProtoId *pDexProtoId = dexGetProtoId(pDexFile, pDexMethodId->protoIdx);
- printf("%s ", dexStringByTypeIdx(pDexFile, pDexProtoId->returnTypeIdx));
- printf("%s\b.", dexStringByTypeIdx(pDexFile, pDexMethodId->classIdx));
- printf("%s", dexStringById(pDexFile, pDexMethodId->nameIdx));
- printf("(");
- // 获得参数列表
- const DexTypeList *pDexTypeList = dexGetProtoParameters(pDexFile, pDexProtoId);
- u4 num = pDexTypeList != NULL ? pDexTypeList->size : 0;
- // 输出参数
- for ( u4 j = 0; j < num; j ++ )
- {
- printf("%s\b, ", dexStringByTypeIdx(pDexFile, pDexTypeList->list[j].typeIdx));
- }
- if ( num == 0 )
- {
- printf(");");
- }
- else
- {
- printf("\b\b);");
- }
- printf("\r\n");
- }
- }
复制代码
在解析数据时,只是将数据对应的字符串进行了输出,而还原方法时,则是借助ProtoIds来完整地还原了方法。
同样,在main函数中调用自定义函数,输出如图6所示。
图6 MethodIds解析后的输出
在解析ProtoIds的时候是有3个方法原型,在解析方法时是4个方法,第一个方法与第四个方法的方法原型是相同的。
用第二个方法来进行一个简单说明,V LHelloWorld.main([Ljava/lang/String]);。V表示方法的返回值类型,LHelloWorld是方法所在的类,main是方法的名称,Ljava/lang/String是该方法参数的类型。
9. 解析DexClassDef相关数据
解析DexClassDef是最复杂的部分了,因为它会先解析类相关的内容,类相关的内容包含类所属的文件、类中的属性、类中的方法、方法中的字节码等内容。虽然复杂,但是它只是前面每个部分和其余部分的组成,因此只是代码比较多,没有什么特别难的地方,具体代码如下:
- void PrintClassDef(DexFile *pDexFile)
- {
- for ( u4 i =0; i < pDexFile->pHeader->classDefsSize; i ++ )
- {
- const DexClassDef *pDexClassDef = dexGetClassDef(pDexFile, i);
- // 类所属的源文件
- printf("SourceFile : %s\r\n", dexGetSourceFile(pDexFile, pDexClassDef));
- // 类和父类
- // 因为我们的 Dex 文件没有接口所以这里就没写
- // 具体解析的时候需要根据实际情况而定
- printf("class %s\b externs %s\b { \r\n",
- dexGetClassDescriptor(pDexFile, pDexClassDef),
- dexGetSuperClassDescriptor(pDexFile, pDexClassDef));
- const u1 *pu1 = dexGetClassData(pDexFile, pDexClassDef);
- DexClassData *pDexClassData = dexReadAndVerifyClassData(&pu1, NULL);
- // 类中的属性
- for ( u4 z = 0; z < pDexClassData->header.instanceFieldsSize; z ++ )
- {
- const DexFieldId *pDexField = dexGetFieldId(pDexFile, pDexClassData->
- instanceFields[z].fieldIdx);
- printf("%s %s\r\n",
- dexStringByTypeIdx(pDexFile, pDexField->typeIdx),
- dexStringById(pDexFile, pDexField->nameIdx));
- }
- // 类中的方法
- for ( u4 z = 0; z < pDexClassData->header.directMethodsSize; z ++ )
- {
- const DexMethodId *pDexMethod = dexGetMethodId(pDexFile, pDexClassData->
- directMethods[z].methodIdx);
- const DexProtoId *pDexProtoId = dexGetProtoId(pDexFile, pDexMethod->
- protoIdx);
- printf("\t%s ", dexStringByTypeIdx(pDexFile, pDexProtoId->returnTypeIdx));
- printf("%s\b.", dexStringByTypeIdx(pDexFile, pDexMethod->classIdx));
- printf("%s", dexStringById(pDexFile, pDexMethod->nameIdx));
- printf("(");
- // 获得参数列表
- const DexTypeList *pDexTypeList = dexGetProtoParameters(pDexFile, pDexProtoId);
- u4 num = pDexTypeList != NULL ? pDexTypeList->size : 0;
- // 输出参数
- for ( u4 k = 0; k < num; k ++ )
- {
- printf("%s\b v%d, ", dexStringByTypeIdx(pDexFile, pDexTypeList->
- list[k].typeIdx), k);
- }
- if ( num == 0 )
- {
- printf(")");
- }
- else
- {
- printf("\b\b)");
- }
- printf("{\r\n");
- // 方法中具体的数据
- const DexCode *pDexCode = dexGetCode(pDexFile, (const DexMethod *)&pDex
- ClassData->directMethods[z]);
- printf("\t\tregister:%d \r\n", pDexCode->registersSize);
- printf("\t\tinsnsSize:%d \r\n", pDexCode->insSize);
- printf("\t\tinsSize:%d \r\n", pDexCode->outsSize);
- // 方法的字节码
- printf("\t\t// ByteCode ...\r\n\r\n");
- printf("\t\t//");
- for ( u2 x = 0; x < pDexCode->insnsSize; x ++ )
- {
- printf("%04X ", pDexCode->insns[x]);
- }
- printf("\r\n");
- printf("\t}\r\n\r\n");
- }
- printf("}\r\n");
- }
- }
复制代码
在代码中逐步地对类进行了解析,从类所属的源文件、类的名称、类的父类、类的属性,到类的方法以及类的字节码。除了方法中的数据在前面的代码中没有,其余的代码在前面都有过介绍了。对于类方法中的数据只要按照DexCode进行解析即可,这里请参考前面给出的DexCode结构体即可。
最后,在main函数中调用自定义函数,输出如图7所示。
图7 DexClassDef解析后的输出
|