kaiyun官方注册
您所在的位置: 首页> 通信与网络> 业界动态> 网络安全编程:开发Dex文件格式解析工具

网络安全编程:开发Dex文件格式解析工具

2021-07-30
来源:计算机与网络安全
关键词: 安全 编程 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);

  }

  在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解析后的输出




电子技术图片.png

本站内容除特别声明的原创文章之外,转载内容只为传递更多信息,并不代表本网站赞同其观点。转载的所有的文章、图片、音/视频文件等资料的版权归版权所有权人所有。本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如涉及作品内容、版权和其它问题,请及时通过电子邮件或电话通知我们,以便迅速采取适当措施,避免给双方造成不必要的经济损失。联系电话:010-82306116;邮箱:aet@chinaaet.com。
Baidu
map