Android学习笔记3--逆向1--Dex

来源:互联网 发布:zigbee实战演练 源码 编辑:程序博客网 时间:2024/05/19 12:41

DEX文件

Dalvik虚拟机作为Android平台的核心组件,拥有如下几个特点:
1) 原理上运行Dalvik字节码,常量池采用32位索引值,基于32位寄存器架构并拥有一套完整的指令系统,采用了DEX可执行文件格式。
2) 功能上提供对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能;
3) 运行上所有app都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例。

odex是一种经过优化的dex文件格式,大部分厂商发布的手机上都会做odex处理。odex处理过的镜像包,jar/apk包中是不存在dex文件的,其系统jar/apk包的可执行文件是存在同目录下的同名.odex 文件。对于odex文件,只需知道两点:
1) odex文件是dex文件的一个超集,会在dex文件基础上附加一些数据;
2) odex文件对镜像包中的/system/framework(可具体到目录中core.jar、ext.jar、framework.jar、services.jar和android.policy.jar这5个jar包)具有强依赖性,odex文件在不同的framework上是无法被dalvik虚拟机加载的。

构建文件

构建过程

看一下这篇文章
APK构建流程

构件流程如下:
1) 用appt和aidl和ndk将源码和文件进行整理。
2) 用javac将java源文件编译为class文件。
3) 用dx将class文件转为dex文件(还有消除冗余,压缩常量池)。
4) 用apkbuilder将上一步的所有资源打包成apk文件。
5) 用signapk将apk进行签名。
6) 用zipalign对签名后的apk进行对齐。

安装方式如下:
1) 通过系统程序安装。开机时由PMS安装,无安装界面。所在位置为system/app目录。
2) 通过市场或者SD卡安装。手动由PI.apk安装,有安装界面。
3) 由ADB安装。手动由PI.apk安装,有安装界面。

工具介绍

工具汇总

smali和baksmali是针对DEX执行文件格式的第三方汇编器和反汇编器。apktool是一个编译与反编译apk的第三方工具,该工具中使用了smail和baksmail的代码。反汇编后DEX文件会产生smali后缀的代码文件。smali代码拥有特定的格式与语法,smali语言是对Dalvik虚拟机字节码的一种解释。

# 反编译apk时可能需要资源apk,这时需要先安装再反编译.# -t指定安装到本机资源存储目录中apk的名字扩展部分。-p指定安装到本机资源存储目录的子位置apktool if res.apk -t ui -p test/1 #安装资源apk到本地目录(if:install-framework)# > I: Framework installed to: test/1/res-ui.apkapktool -f d ./test.apk [-o ./spy] # 反编译apk并将反编译文件输出到spy文件夹中(f:force,d:decode)apktool -f b ./spy [-o ./test.apk] # 编译修改后的spy文件夹并打包成apk文件(f:force,b:build)

使用apktool是无法直接对odex文件进行反编译,我们必须先使用baksmali进行deodex处理。

# 通过aapt命令获取apk使用的SDK的版本号targetSdkVersion为15aapt d badging Calculator.apk | grep targetSdkVersion # 把apk文件中其他的资源等文件反编译出来apktool d Calculator.apk -o ./Calculator # 用baksmali对Calculator.odex去odex并反编译成smali代码文件# -x指定需deodex的odex文件,-d指明依赖的framework目录,-a指明sdk版本,-o 指定去输出目录。baksmali -x Calculator.odex -d framework -a 15 -o Calculator/smali#修改完相应的smali文件和资源文件后,重新编译打包成apk文件apktool b Calculator -o Calculator.apk 

当生成dex后可以通过adb命令push到手机上,并使用dalvikvm命令运行这个dex文件,执行命令如下:

adb push Test.dex /data/local/adb shell dalvikvm -cp /data/local/Test.dex Test

文件结构

DEX数据类型

点这里看sleb128讲解。

u1 # 等同于uint8_t,表示1字节的无符号数u2 # 等同于uint16_t,表示2字节的无符号数u4 # 等同于uint32_t,表示4字节的无符号数u8 # 等同于uint64_t,表示8字节的无符号数sleb128 # 有符号LEB128,可变长度1~5字节uleb128 # 无符号LEB128,可变长度1~5字节uleb128p1 # 无符号LEB128值加1,可变长1~5字节

dex文件结构

参考网上的这篇文章。DEX是十六进制的小端字节。
dex文件结构

struct DexFile{    const DexOptHeader* pOptHeader;    const DexHeader* pHeader;    const DexStringId* pStringIds;    const DexTypeId* pTypeIds;    //...    const DexLink* pLinkData;    const DexClassLoopkup* pClassLookup;    const void* pRegisterMapPool;    const u1* baseAddr;    int overead;}

头部(DexHeader)段

DexHeader:是DEX文件头,其结构很固定,占用0x70个字节。

magic[8]:dex版本标志,共1个字节。目前为固定值64 65 78 0a 30 33 35 00。
checksum:文件校验码,使用alder32算法校验文件除去magic、checksum外余下的所有文件区域,用于检查文件错误。
signature[20]:共1个字节。使用SHA-1算法hash除去magic,checksum和signature外余下的所有文件区域 ,用于唯一识别本文件 。
fileSize:DEX文件的长度,包括DexHeader。
headerSize:DexHeader本身的长度,一般固定为0x70字节。
endianTag:指定了DEX运行环境的cpu字节序,预设值ENDIAN_CONSTANT等于0x12345678(00 bc 61 4e),表示默认采用Little-Endian字节序。

mapOff:索引段之DexMapList文件的偏移。
stringIdsSize和stringIdsOff:索引段之DexStringId文件的大小与偏移。
typeIdsSize和typeIdsOff:索引段之DexTypeId文件的大小与偏移。
protoIdsSize和protoIdsSize:索引段之DexProtoId文件的大小与偏移。
fieldIdsSize和fieldIdsSize:索引段之DexFieldId文件的大小与偏移。
methodIdsSize和methodIdsSize:索引段之DexMethodId文件的大小与偏移。
classDefsSize和classDefsOff:索引段之DexClassDef文件的大小与偏移。

dataSize和dataOff:数据段文件的大小与偏移。
linkSize和linkOff:链接段文件的大小与偏移,大多数情况下它们的值都为0。

link_size:LinkSection大小,如果为0则表示该DEX文件不是静态链接。
link_off:LinkSection距离DEX头的偏移地址,如果LinkSize为0,此值也会为0。

索引(DexMapList)段

详情参考这里。Dalvik虚拟机解析DEX文件的内容,最终将其映射成DexMapList数据结构,它实际上包含所有其他区段的结构大纲。DexHeader中的mapOff字段指明了DexMapList结构在DEX文件中的偏移。

struct DexMapList {    u4 size;               /* DexMapItem的个数 */    DexMapItem list[1];    /* DexMapItem的结构 */};struct DexMapItem {    u2 type;    /* 类型 */    u2 unused;  /* 未使用,用于字节对齐 */    u4 size;    /* type指定类型的个数,它们在dex文件中连续存放 */    u4 offset;  /* 指定类型数据的文件偏移 */};/* type字段为一个枚举常量,通过类型名称很容易判断它的具体类型 */enum {    kDexTypeHeaderItem               = 0x0000,    kDexTypeStringIdItem             = 0x0001,    kDexTypeTypeIdItem               = 0x0002,    kDexTypeProtoIdItem              = 0x0003,    kDexTypeFieldIdItem              = 0x0004,    kDexTypeMethodIdItem             = 0x0005,    kDexTypeClassDefItem             = 0x0006,    kDexTypeMapList                  = 0x1000,    kDexTypeTypeList                 = 0x1001,    kDexTypeAnnotationSetRefList     = 0x1002,    kDexTypeAnnotationSetItem        = 0x1003,    kDexTypeClassDataItem            = 0x2000,    kDexTypeCodeItem                 = 0x2001,    kDexTypeStringDataItem           = 0x2002,    kDexTypeDebugInfoItem            = 0x2003,    kDexTypeAnnotationItem           = 0x2004,    kDexTypeEncodedArrayItem         = 0x2005,    kDexTypeAnnotationsDirectoryItem = 0x2006,};

1 DexStringId

// 在DexMapList中(在header中也可以找到)找到其偏移和个数struct DexStringId{    u4 stringDataOff;       //  字符串数据偏移}

2.1 DexTypeId

// 在DexMapList中(在header中也可以找到)找到其偏移和个数struct DexTypeId{    u4 descriptorIdx;       //  指向stringIds的索引}struct DexTypeList{    u4 size;                /* DexTypeItem的个数 */    DexTypeItem list[1];    /* DexTypeItem的结构 */}struct DexTypeItem{    u2 typeIdx;             //  指向DexTypeId的索引}

2.2 DexProtoId

// 在DexMapList中(在header中也可以找到)找到其偏移和个数struct DexProtoId{    u4 shortyIdx;           //  指向DexStringId的索引,方法名类型    u4 returnTypeIdx;       //  指向DexTypeId的索引,方法返回类型    u4 parametersOff;       //  指向DexTypeList的偏移,方法参数签名}

3.1 DexFieldId

// 在DexMapList中(在header中也可以找到)找到其偏移和个数struct DexProtoId{    u2  classIdx;           // 指向DexTypeId for defining class    u2  typeIdx;            // 指向DexTypeId for method prototype    u4  nameIdx;            // 指向DexStringId for method name}

3.2 DexMethodId

// 在DexMapList中(在header中也可以找到)找到其偏移和个数struct DexMethodId {    u2  classIdx;           // 指向DexTypeId for defining class    u2  protoIdx;           // 指向DexProtoId for method prototype    u4  nameIdx;            // 指向DexStringId for method name};

3.3 DexClassDef
ClassDefs以4字节对齐,即总大小为4 * 8 * ClassDefsSize。详情请看网上博客和谷歌官方。

// 在DexMapList中(在header中也可以找到)找到其偏移和个数struct DexClassDef {    u4  classIdx;           // 指向DexTypeId的索引,表示类的类型    u4  accessFlags;        // 类的访问标识,如public、final等。    u4  superclassIdx;      // 指向DexTypeId的索引,表示父类类型(如没值为NO_INDEX)    u4  interfacesOff;      // 指向DexTypeList的文件偏移,表示接口(如没值为0)    u4  sourceFileIdx;      // 指向DexStringId的索引,表示源文件名(如没值为NO_INDEX)    u4  annotationsOff;     // 指向某文件偏移,表示注解(如没值为0)    u4  classDataOff;       // 指向DexClassData文件的偏移    u4  staticValuesOff;    // 指向DexEncodedArray文件的偏移};typedef struct DexClassData {     DexClassDataHeader header;     // DexClassDataBody部分     DexField*          staticFields;     DexField*          instanceFields;     DexMethod*         directMethods;     DexMethod*         virtualMethods; };typedef struct DexClassDataHeader {     u4 staticFieldsSize;     u4 instanceFieldsSize;     u4 directMethodsSize;     u4 virtualMethodsSize;};// DexClassDataBody部分typedef struct DexField {     u4 fieldIdx;    // 指向DexFiledId的索引     u4 accessFlags;};// DexClassDataBody部分typedef struct DexMethod {     u4 methodIdx;    // 指向DexMethodId的索引     u4 accessFlags;     u4 codeOff;      // 指向DexCode结构};typedef struct DexCode {     u2  registersSize;      // 使用寄存器的个数     u2  insSize;            // 自己运行的使用的寄存器(参数)个数     u2  outsSize;           // 调用其它方法时使用的寄存器个数     u2  triesSize;          // try的个数     u4  debugInfoOff;       // 指向调试信息的偏移     u4  insnsSize;          // 指令集个数,以2字节为单位     u2  insns[1];           // 指令集,即真正的代码段};

odex文件结构

参考这里,odex生成通过dexopt-wrapper程序。odex文件的结尾肯定是以64 65 79 0a 30 33 36 00开头,以44 4E 45 41 00 00 00 00结尾的,odex有两种存在形式:

一种是从apk程序中提取出来,与apk存放的同级目录,文件后缀为odex的文件。这类型多是AndroidROM的系统程序。
一种是dalvik-cache缓存,文件后缀为dex的文件。这类型多存放在cache/dalvik-cache目录下。保存的形式为app路径@apk名@classes.dex。app路径中的/也用@表示,apk名包含.apk后缀。

由于安卓程序的apk文件为zip压缩包形式,虚拟机每次加载它们时需要从apk中读取而太消耗CPU,而odex是dex的超集,因此利用缓存即可更快的读取运行dex。


// 抽象的看是如下的定义struct ODexFile{    DexOptHeader* header;// odex头部    DexFile* dexfile;// dex文件    Dependences deps;// 依赖库列表    ChunkDexClassLookup lookup;// 类查询结构    ChunkRegisterMapPoll mappool;// 映射池    ChunkEnd end;// 结束标志}

头部(DexOptHeader)段

DexOptHeader:是ODEX文件头,其结构很固定,占用0x27个字节。

struct DexOptHeader {    u1  magic[8];        // odex版本标志,目前固定值为64 65 79 0a 30 33 36 00    u4  dexOffset;       // dex文件头的偏移,目前固定值为0x28    u4  dexLength;       // dex文件总长度    u4  depsOffset;      // dex依赖库列表偏移    u4  depsLength;      // dex依赖库列表总长度    u4  optOffset;       // 辅助段偏移    u4  optLength;       // 辅助段总长度    u4  flags;           // 标志,标识了㣈虚拟机加载odex的优化与验证选项    u4  checksum;        // 依赖库与辅助数据的校验和,标识odex是否合法};

依赖段

DexDependences
Dependences不会加载进内存,并且也没有相关的官方资料,通过系统源码整理如下:

struct DexDependences{    u4 timestamp;// 时间戳,用来记录优化前的classes.dex的时间戳    u4 crc;      // 校验值,用来记录优化前的classes.dex的校验值    u4 version;  // 虚拟机版本号    u4 size;  // 依赖库个数    struct {        u4 len;  // name字符串的长度        u1 name[len];  // 依赖库的名称        SHA1DigeslLen signature; // 当前指定依赖库的SHA1校验值    }} table[size];

辅助段

Dependences下面为3个Chunk块,他们被虚拟机加载到一个叫auxillay的段中。3个块由DexPrepare.cpp文件的writeOptData()函数写入,实际writeOptData调用了writeChunk写入。writeChunk函数中定义了一个header(占用8个字节),writeChunk函数写入时会先填充这个结构:

union{    char raw[8];    struct{        u4 type; // 指下面三种类型的某个的类型常量        u4 size; // 需要为指定的类型结构填充数据的字节数    } ts;} header;

ChunkDexClassLookup

虚拟机通过ChunkDexClassLookup检索dex文件中的所有类。
写入时会向writeChunk函数传递一个DexClassLookup指针。

struct ChunkDexClassLookup{    Header header;    struct{        int len;  // 本结构的字节数        int size; // 下面struct的个数,通常值为2        struct{            u4 classDescriptorHash;    // 类的哈希            int classDescriptorOffset; // 类的描述            int classDefOffset;        // classDef的偏移        } table[1];    } DexClassLookup;}

ChunkRegisterMapPoll

写入时会向writeChunk函数传递一个RegisterMapPoll指针。

struct ChunkRegisterMapPoll{    Header header;    struct{        struct{            u4 numClasses;            u4 classDataOffset[1];        } classpool;        struct{            u2 numMethod;            u4 methodDataOffset[1];        } lookup;    } RegisterMapPoll;}

ChunkEnd

写入时会向writeChunk函数传递一个NULL指针。

struct ChunkEnd{    Header header;}

dexopt的验证与优化

暂时省略,参看《Android软件安全与逆向分析》的第四章的4.5小节。