网络安全编程:开发Dex文件格式解析工具
2021-07-30
来源:计算机与网络安全
解析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);
}
在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解析后的输出