比特派官方苹果app下载安卓|dex文件

作者: 比特派官方苹果app下载安卓
2024-03-17 02:35:43

Android逆向笔记 —— DEX 文件格式解析 - 知乎

Android逆向笔记 —— DEX 文件格式解析 - 知乎首发于Android 逆向笔记切换模式写文章登录/注册Android逆向笔记 —— DEX 文件格式解析路遥DEX 文件结构思维导图及解析源码见文末。往期目录:Class 文件格式详解 Smali 语法解析——Hello World Smali —— 数学运算,条件判断,循环 Smali 语法解析 —— 类 Android逆向笔记 —— AndroidManifest.xml 文件格式解析 系列第一篇文章就分析过 Class 文件格式,我们都知道 .java 源文件经过编译器编译会生成 JVM 可识别的 .class 文件。在 Android 中,不管是 Dalvik 还是 Art,和 JVM 的区别还是很大的。Android 系统并不直接使用 Class 文件,而是将所有的 Class 文件聚合打包成 DEX 文件,DEX 文件相比单个单个的 Class 文件更加紧凑,可以直接在 Android Runtime 下执行。对于学习热修复框架,加固和逆向相关知识,了解 DEX 文件结构是很有必要的。再之前解析过 Class 文件和 AndroidManifest.xml 文件结构之后,发现看二进制文件看上瘾了。。后面会继续对 Apk 文件中的其他文件结构进行分析,例如 so 文件,resources.arsc 文件等。DEX 文件的生成在解析 DEX 文件结构之前,先来看看如何生成 DEX 文件。为了方便解析,本篇文章中就不从市场上的 App 里拿 DEX 文件过来解析了,而是手动生成一个最简单的 DEX 文件。还是以 Class 文件解析时候用的例子:public class Hello {

private static String HELLO_WORLD = "Hello World!";

public static void main(String[] args) {

System.out.println(HELLO_WORLD);

}

}

首先 javac 编译成 Hello.class 文件,然后利用 Sdk 自带的 dx 工具生成 DEX 文件:dx --dex --output=Hello.dex Hello.class

dx 工具位于 Sdk 的 build-tools 目录下,可添加至环境变量方便调用。dx 也支持多 Class 文件生成 dex。DEX 文件结构概览关于 DEX 文件结构的学习,给大家推荐两个资料。第一个是看雪神图,出自非虫,第二个是 Android 源码中对 DEX 文件格式的定义,dalvik/libdex/DexFile.h,其中详细定义了 DEX 文件中的各个部分。第三个是 010 Editor,在之前解析 AndroidManifest.xml 文件格式解析 也介绍过,它提供了丰富的文件模板,支持常见文件格式的解析,可以很方便的查看文件结构中的各个部分及其对应的十六进制。一般我在代码解析文件结构的时候都是对照着 010 Editor 来进行分析。下面贴一张 010 Editor 打开之前生成的 Hello.dex 文件的截图:我们可以一目了然的看到 DEX 的文件结构,着实是一个利器。在详细解析之前,我们先来大概给 DEX 文件分个层,如下图所示: 文末我放了一张详细的思维导图,也可以对着思维导图来阅读文章。 依次解释一下:header : DEX 文件头,记录了一些当前文件的信息以及其他数据结构在文件中的偏移量string_ids : 字符串的偏移量type_ids : 类型信息的偏移量proto_ids : 方法声明的偏移量field_ids : 字段信息的偏移量method_ids : 方法信息(所在类,方法声明以及方法名)的偏移量class_def : 类信息的偏移量data : : 数据区link_data : 静态链接数据区从 header 到 data 之间都是偏移量数组,并不存储真实数据,所有数据都存在 data 数据区,根据其偏移量区查找。对 DEX 文件有了一个大概的认识之后,我们就来详细分析一下各个部分。headerDEX 文件头部分的具体格式可以参考 DexFile.h 中的定义:struct DexHeader {

u1 magic[8]; // 魔数

u4 checksum; // adler 校验值

u1 signature[kSHA1DigestLen]; // sha1 校验值

u4 fileSize; // DEX 文件大小

u4 headerSize; // DEX 文件头大小

u4 endianTag; // 字节序

u4 linkSize; // 链接段大小

u4 linkOff; // 链接段的偏移量

u4 mapOff; // DexMapList 偏移量

u4 stringIdsSize; // DexStringId 个数

u4 stringIdsOff; // DexStringId 偏移量

u4 typeIdsSize; // DexTypeId 个数

u4 typeIdsOff; // DexTypeId 偏移量

u4 protoIdsSize; // DexProtoId 个数

u4 protoIdsOff; // DexProtoId 偏移量

u4 fieldIdsSize; // DexFieldId 个数

u4 fieldIdsOff; // DexFieldId 偏移量

u4 methodIdsSize; // DexMethodId 个数

u4 methodIdsOff; // DexMethodId 偏移量

u4 classDefsSize; // DexCLassDef 个数

u4 classDefsOff; // DexClassDef 偏移量

u4 dataSize; // 数据段大小

u4 dataOff; // 数据段偏移量

};

其中的 u 表示无符号数,u1 就是 8 位无符号数,u4 就是 32 位无符号数。magic 一般是常量,用来标记 DEX 文件,它可以分解为:文件标识 dex + 换行符 + DEX 版本 + 0

字符串格式为 dex\n035\0,十六进制为 0x6465780A30333500。checksum 是对去除 magic 、 checksum 以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。signature 是对除去 magic 、 checksum 、 signature 以外的文件部分作 sha1 得到的文件哈希值。endianTag 用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678。其余部分分别标记了 DEX 文件中其他各个数据结构的个数和其在数据区的偏移量。根据偏移量我们就可以轻松的获得各个数据结构的内容。下面顺着上面的 DEX 文件结构来认识第一个数据结构 string_ids。string_idsstruct DexStringId {

u4 stringDataOff;

};

string_ids 是一个偏移量数组,stringDataOff 表示每个字符串在 data 区的偏移量。根据偏移量在 data 区拿到的数据中,第一个字节表示的是字符串长度,后面跟着的才是字符串数据。这块逻辑比较简单,直接看一下代码:private void parseDexString() {

log("\nparse DexString");

try {

int stringIdsSize = dex.getDexHeader().string_ids__size;

for (int i = 0; i < stringIdsSize; i++) {

int string_data_off = reader.readInt();

byte size = dexData[string_data_off]; // 第一个字节表示该字符串的长度,之后是字符串内容

String string_data = new String(Utils.copy(dexData, string_data_off + 1, size));

DexString string = new DexString(string_data_off, string_data);

dexStrings.add(string);

log("string[%d] data: %s", i, string.string_data);

}

} catch (IOException e) {

e.printStackTrace();

}

}

打印结果如下:parse DexString

string[0] data:

string[1] data:

string[2] data: HELLO_WORLD

string[3] data: Hello World!

string[4] data: Hello.java

string[5] data: LHello;

string[6] data: Ljava/io/PrintStream;

string[7] data: Ljava/lang/Object;

string[8] data: Ljava/lang/String;

string[9] data: Ljava/lang/System;

string[10] data: V

string[11] data: VL

string[12] data: [Ljava/lang/String;

string[13] data: main

string[14] data: out

string[15] data: println

其中包含了变量名,方法名,文件名等等,这个字符串池在后面其他结构的解析中也会经常遇到。type_idsstruct DexTypeId {

u4 descriptorIdx;

};

type_ids 表示的是类型信息,descriptorIdx 指向 string_ids 中元素。根据索引直接在上一步读取到的字符串池即可解析对应的类型信息,代码如下:private void parseDexType() {

log("\nparse DexTypeId");

try {

int typeIdsSize = dex.getDexHeader().type_ids__size;

for (int i = 0; i < typeIdsSize; i++) {

int descriptor_idx = reader.readInt();

DexTypeId dexTypeId = new DexTypeId(descriptor_idx, dexStringIds.get(descriptor_idx).string_data);

dexTypeIds.add(dexTypeId);

log("type[%d] data: %s", i, dexTypeId.string_data);

}

} catch (IOException e) {

e.printStackTrace();

}

}

解析结果:parse DexType

type[0] data: LHello;

type[1] data: Ljava/io/PrintStream;

type[2] data: Ljava/lang/Object;

type[3] data: Ljava/lang/String;

type[4] data: Ljava/lang/System;

type[5] data: V

type[6] data: [Ljava/lang/String;

proto_idsstruct 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 */

};

proto_ids 表示方法声明信息,它包含以下三个变量:shortyIdx : 指向 string_ids ,表示方法声明的字符串returnTypeIdx : 指向 type_ids ,表示方法的返回类型 parametersOff : 方法参数列表的偏移量方法参数列表的数据结构在 DexFile.h 中用 DexTypeList 来表示:struct DexTypeList {

u4 size; /* #of entries in list */

DexTypeItem list[1]; /* entries */

};

struct DexTypeItem {

u2 typeIdx; /* index into typeIds */

};

size 表示方法参数的个数,参数用 DexTypeItem 表示,它只有一个属性 typeIdx,指向 type_ids 中对应项。具体的解析代码如下:private void parseDexProto() {

log("\nparse DexProto");

try {

int protoIdsSize = dex.getDexHeader().proto_ids__size;

for (int i = 0; i < protoIdsSize; i++) {

int shorty_idx = reader.readInt();

int return_type_idx = reader.readInt();

int parameters_off = reader.readInt();

DexProtoId dexProtoId = new DexProtoId(shorty_idx, return_type_idx, parameters_off);

log("proto[%d]: %s %s %d", i, dexStringIds.get(shorty_idx).string_data,

dexTypeIds.get(return_type_idx).string_data, parameters_off);

if (parameters_off > 0) {

parseDexProtoParameters(parameters_off);

}

dexProtos.add(dexProtoId);

}

} catch (IOException e) {

e.printStackTrace();

}

}

解析结果:parse DexProto

proto[0]: V V 0

proto[1]: VL V 412

parameters[0]: Ljava/lang/String;

proto[2]: VL V 420

parameters[0]: [Ljava/lang/String;

field_idsstruct DexFieldId {

u2 classIdx; /* index into typeIds list for defining class */

u2 typeIdx; /* index into typeIds for field type */

u4 nameIdx; /* index into stringIds for field name */

};

field_ids 表示的是字段信息,指明了字段所在的类,字段的类型以及字段名称,在 DexFile.h 中定义为 DexFieldId , 其各个字段含义如下:classIdx : 指向 type_ids ,表示字段所在类的信息typeIdx : 指向 ype_ids ,表示字段的类型信息nameIdx : 指向 string_ids ,表示字段名称代码解析很简单,就不贴出来了,直接看一下解析结果:parse DexField

field[0]: LHello;->HELLO_WORLD;Ljava/lang/String;

field[1]: Ljava/lang/System;->out;Ljava/io/PrintStream;

method_idsstruct DexMethodId {

u2 classIdx; /* index into typeIds list for defining class */

u2 protoIdx; /* index into protoIds for method prototype */

u4 nameIdx; /* index into stringIds for method name */

};

method_ids 指明了方法所在的类、方法声明以及方法名。在 DexFile.h 中用 DexMethodId 表示该项,其属性含义如下:classIdx : 指向 type_ids ,表示类的类型protoIdx : 指向 type_ids ,表示方法声明nameIdx : 指向 string_ids ,表示方法名解析结果:parse DexMethod

method[0]: LHello; proto[0]

method[1]: LHello; proto[0]

method[2]: LHello; proto[2] main

method[3]: Ljava/io/PrintStream; proto[1] println

method[4]: Ljava/lang/Object; proto[0]

class_defstruct DexClassDef {

u4 classIdx; /* index into typeIds for this class */

u4 accessFlags;

u4 superclassIdx; /* index into typeIds for superclass */

u4 interfacesOff; /* file offset to DexTypeList */

u4 sourceFileIdx; /* index into stringIds for source file name */

u4 annotationsOff; /* file offset to annotations_directory_item */

u4 classDataOff; /* file offset to class_data_item */

u4 staticValuesOff; /* file offset to DexEncodedArray */

};

class_def 是 DEX 文件结构中最复杂也是最核心的部分,它表示了类的所有信息,对应 DexFile.h 中的 DexClassDef :classIdx : 指向 type_ids ,表示类信息accessFlags : 访问标识符superclassIdx : 指向 type_ids ,表示父类信息interfacesOff : 指向 DexTypeList 的偏移量,表示接口信息sourceFileIdx : 指向 string_ids ,表示源文件名称annotationOff : 注解信息classDataOff : 指向 DexClassData 的偏移量,表示类的数据部分staticValueOff :指向 DexEncodedArray 的偏移量,表示类的静态数据DefCLassData重点是 classDataOff 这个字段,它包含了一个类的核心数据,在 Android 源码中定义为 DexClassData ,它不在 DexFile.h 中了,而是在 DexClass.h 中:struct DexClassData {

DexClassDataHeader header;

DexField* staticFields;

DexField* instanceFields;

DexMethod* directMethods;

DexMethod* virtualMethods;

};

DexClassDataHeader 定义了类中字段和方法的数目,它也定义在 DexClass.h 中:struct DexClassDataHeader {

u4 staticFieldsSize;

u4 instanceFieldsSize;

u4 directMethodsSize;

u4 virtualMethodsSize;

};

staticFieldsSize : 静态字段个数instanceFieldsSize : 实例字段个数directMethodsSize : 直接方法个数virtualMethodsSize : 虚方法个数在读取的时候要注意这里的数据是 LEB128 类型。它是一种可变长度类型,每个 LEB128 由 1~5 个字节组成,每个字节只有 7 个有效位。如果第一个字节的最高位为 1,表示需要继续使用第 2 个字节,如果第二个字节最高位为 1,表示需要继续使用第三个字节,依此类推,直到最后一个字节的最高位为 0,至多 5 个字节。除了 LEB128 以外,还有无符号类型 ULEB128。那么为什么要使用这种数据结构呢?我们都知道 Java 中 int 类型都是 4 字节,32 位的,但是很多时候根本用不到 4 个字节,用这种可变长度的结构,可以节省空间。对于运行在 Android 系统上来说,能多省一点空间肯定是好的。下面给出了 Java 读取 ULEB128 的代码:public static int readUnsignedLeb128(byte[] src, int offset) {

int result = 0;

int count = 0;

int cur;

do {

cur = copy(src, offset, 1)[0];

cur &= 0xff;

result |= (cur & 0x7f) << count * 7;

count++;

offset++;

DexParser.POSITION++;

} while ((cur & 0x80) == 128 && count < 5);

return result;

}

继续回到 DexClassData 中来。header 部分定义了各种字段和方法的个数,后面跟着的分别就是 静态字段 、实例字段 、直接方法 、虚方法 的具体数据了。字段用 DexField 表示,方法用 DexMethod 表示。DexFieldstruct DexField {

u4 fieldIdx; /* index to a field_id_item */

u4 accessFlags;

};

fieldIdx : 指向 field_ids ,表示字段信息accessFlags :访问标识符DexMethodstruct DexMethod {

u4 methodIdx; /* index to a method_id_item */

u4 accessFlags;

u4 codeOff; /* file offset to a code_item */

46};

method_idx 是指向 method_ids 的索引,表示方法信息。accessFlags 是该方法的访问标识符。codeOff 是结构体 DexCode 的偏移量。如果你坚持看到了这里,是不是发现说到现在还没说到最重要的东西,DEX 包含的代码,或者说指令,对应的就是 Hello.java 中的 main 方法。没错,DexCode 就是用来存储方法的详细信息以及其中的指令的。struct DexCode {

u2 registersSize; // 寄存器个数

u2 insSize; // 参数的个数

u2 outsSize; // 调用其他方法时使用的寄存器个数

u2 triesSize; // try/catch 语句个数

u4 debugInfoOff; // debug 信息的偏移量

u4 insnsSize; // 指令集的个数

u2 insns[1]; // 指令集

/* followed by optional u2 padding */ // 2 字节,用于对齐

/* followed by try_item[triesSize] */

/* followed by uleb128 handlersSize */

/* followed by catch_handler_item[handlersSize] */

};

我们打开 010 Editor,定位到 main() 方法对应的 DexCode,对照进行分析:public class Hello {

private static String HELLO_WORLD = "Hello World!";

public static void main(String[] args) {

System.out.println(HELLO_WORLD);

}

}

main() 方法对应的 DexCode 十六进制表示为 :03 00 01 00 02 00 00 00 00 00 79 02 00 00 08 00

62 00 01 00 62 01 00 00 6E 20 03 00 01 00 0E 00

使用的寄存器个数是 3 个。参数个数是 1 个,就是 main() 方法中的 String[] args。调用外部方法时使用的寄存器个数为 2 个。指令个数是 8 。终于说到指令了,main() 函数中有 8 条指令,就是上面十六进制中的第二行。尝试来解析一下这段指令。Android 官网就有 Dalvik 指令的相关介绍,链接。第一个指令 62 00 01 00,查询文档 62 对应指令为 sget-object vAA, field@BBBB,AA 对应 00 , 表示 v0 寄存器。BBBB 对应 01 00 ,表示 field_ids 中索引为 1 的字段,根据前面的解析结果该字段为 Ljava/lang/System;->out;Ljava/io/PrintStream,整理一下,62 00 01 00 表示的就是:sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

接着是 62 01 00 00。还是 sget-object vAA, field@BBBB, AA 对应 01 ,BBBB 对应 0000, 使用的是 v1 寄存器,field 位 field_ids 中索引为 0 的字段,即 LHello;->HELLO_WORLD;Ljava/lang/String,该句完整指令为:sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

接着是 6E 20 03 00, 查看文档 6E 指令为 invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB。6E 后面一个十六位 2 表示调用方法是两个参数,那么 BBBB 就是 03 00,指向 method_ids 中索引为 3 方法。根据前面的解析结果,该方法就是 Ljava/io/PrintStream;->println(Ljava/lang/String;)V。完整指令为:invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

最后的 0E,查看文档该指令为 return-void,到这 main() 方法就结束了。将上面几句指令放在一起:62 00 01 00 : sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

62 01 00 00 : sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

6E 20 03 00 : invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

OE OO : return-void

这就是 main() 方法的完整指令了。还记得我之前的一篇文章 Smali 语法解析——Hello World,其实这个解析结果和 Hello.java 对应的 smali 代码是一致的:.method public static main([Ljava/lang/String;)V

.registers 3

.prologue

.line 6

sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

.line 7

return-void

.end method

总结这种文章真的是又臭又长,但是耐下心去看,还是会有很大收货的。最后来一张思维导图总结一下:Java 版本 DEX 文件格式解析源码,点我 DexParser 文章首发微信公众号: 秉心说 , 专注 Java 、 Android 原创知识分享,LeetCode 题解。 更多 JDK 源码解析,扫码关注我吧! 编辑于 2019-05-23 22:40Android软件逆向工程​赞同 22​​1 条评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录Android 逆

Android Dex文件详解 - 掘金

Android Dex文件详解 - 掘金

首页 首页

沸点

课程

直播

活动

竞赛

商城

APP

插件 搜索历史

清空

创作者中心

写文章 发沸点 写笔记 写代码 草稿箱 创作灵感

查看更多

会员

登录

注册

Android Dex文件详解

西瓜king

2022-03-23

1,760

前言

相信大家都熟悉dex文件,把一个apk给解压缩,就会得到一堆dex文件,但是这些dex文件是怎么来的,又有什么用,为什么这样设计,有进行思考过吗

俗话说知其然,知其所以然,本篇文章开始探究一下这些底层实现细节。

正文

不同的虚拟机

JVM

JVM是Java Virtual Machine的简称,即Java虚拟机,它本质是一层软件抽象,在这之上才可以运行Java程序。Java文件经过编译后会生成JVM字节码,和C语言编译后生成的汇编语言不同,C编译成的汇编语言可以直接在硬件上跑,但是Java编译生成的字节码是在JVM上跑,需要由JVM把字节码翻译成机器指令。

也是由于这个JVM在操作系统上屏蔽了底层实现的差异,从而有了Java的跨平台特性。

DVM

DVM是Dalvik Virtual Machine的简称,是Android4.4及以前使用的虚拟机,所有Android程序都运行在Android系统进程中,每个进程对应着一个Dalvik虚拟机实例。

JVM和DVM都提供了对对象生命周期管理,堆栈管理,安全和异常管理及垃圾回收等重要功能。

但是DVM却不能和JVM一样能直接运行Java字节码,它只能运行.dex文件,而这个.dex文件则是由Java字节码通过Android的dx工具生成的文件。

ART

ART是Android Runtime,在Android5.0开始使用ART虚拟机来替代Dalvik虚拟机,为什么Google要换Android程序运行的虚拟机呢 因为ART虚拟机更优秀。

前面说了Dalvik虚拟机会在APP打开时去运行.dex文件,而这个是实时的,也就是JIT特性(Just In Time),这也就会导致在启动APP时会先将.dex文件转换成机器码,这就导致了APP启动慢的问题。

而ART虚拟机有个很好的特性叫做AOT(ahead of time),这个特性可以在安装APK的时候将dex直接处理成可直接供ART虚拟机使用的机器码,ART虚拟机将.dex文件转换成可直接运行的.oat文件,而且ART虚拟机天生支持多dex,所以ART虚拟机可以很大提升APP的冷启动速度。

除了这个优点外,ART还提升了GC速度,提供功能更全面的Debug特性,但是缺点也就是APK安装速度慢,占用的空间多。

生成和查看dex文件

前面说了dex文件是给Android手机的虚拟机来使用的,所以我们来看看如何生成和查看一个dex文件。

先编写一个简单的.java文件:

public class HelloWorld {

int a = 0;

static String b = "HelloDalvik";

public int getNumber(int i, int j) {

int e = 3;

return e + i + j;

}

public static void main(String[] args) {

int c = 1;

int d = 2;

HelloWorld helloWorld = new HelloWorld();

String sayNumber = String.valueOf(helloWorld.getNumber(c, d));

System.out.println("HelloDex!" + sayNumber);

}

}

然后使用javac命令来编译.java文件为.class,注意这里必须使用Java 8,而不能使用Java 11,如下图专门使用Java 8编译的结果(原来Windows环境变量是Java 11,后续的dex解析有误):

有了.class文件后,就是Android的dx工具,该工具一般在下面目录:

//也就是sdk目录下的build-tools文件夹中

D:\Users\wayee\AppData\Local\Android\sdk\build-tools\30.0.3\dx.bat

使用dx工具对.class文件进行处理:

然后会生成一个.dex文件,直接打开这个dex文件它是十六进制编码的文件,看不出任何有用信息,这时就需要一个专门来看这个的工具,这里推荐使用 010 Editor 这个工具,直接把.dex文件拖入工具:

注意这里选择的模板就是DEX.bt,然后就可以按照DEX的格式来分析这些字节是什么意思了,所以看懂dex文件必须要了解DEX文件的格式。

Dex文件格式

看到这里就必须要清楚一个基本概念了,也就是平时使用Java编写的文件,这里给编译打包成dex文件,那这个dex文件就必须要包含这个Java文件的所有信息,那是按照Java文件顺序一行一行保存为字节码还是其他什么方式呢

所以想知道编译器是如何在编译Java文件后保存信息的,就必须要清楚Dex文件格式。

我们可以直接在刚刚010 Editor软件中看到Dex.bt即Dex文件的格式,其格式如下:

当然也可以去Android源码官网看一下Dex的格式:

source.android.com/devices/tec…

看了上面dex文件的格式,其大致可以分为3个区域,分别是文件头、索引区和数据区,那我们就来挨个分析这几个区域有什么作用,以及是如何保存编译后的Java文件。

header 文件头

头文件它包含了这个dex文件的几乎所有信息,所以它的信息非常多,其格式如下:

然后这时就直接点击010 Editor下面的dex_header部分:

其中上面的红框就是文件头的数据,而下面的红框就是文件头的格式,我们来挨个分析一下。

1、Magic value,即魔数,这个就是用来失败dex这种文件的,可以判断当前的dex文件是否有效,其值是固定死的:

转换成ASCII也就是dex.035, 所以凡是dex文件都是这个开头,否则就是错误的dex文件。

2、checksum,dex文件的校验和,它可以判断dex文件是否损坏或者篡改,占用4个字节,注意这里是采用小字节序的编码方式,即低位上存储的就是低字节内容,可以看一下:

会发现这里的值和二进制保存是相反的。

3、SHA1签名,也就是把整个dex文件用SHA-1签名得到的一个值,占用20个字节。

4、fileSize,整个文件的大小,占用4个字节,看一下值这里是:

十进制是1204,换算成16进制就是4B4,我们再来看看这个dex文件的长度:

这里长度也是4B4。

5、headerSize,表示头结构的大小,占用4个字节,这个就不截图看了。

6、endian_tag,表示字节序,这里具体的值就2个,标准的.dex格式采用小段字节序,但具体实现可能会选择执行字节交换,所以这个改变就由这个tag来判断。

7、linkSize和linkOff,这2个字段指定了链接段的大小和文件偏移,通常情况下他们都是0,linkSize为0表示为静态链接。

从这里开始就会发现有off这个字段,这是啥意思呢,其实也就是文件偏移量,也就是从这个文件第多少位置开始表示的值。

8、mapOff,这个字段表示DexMapList的文件偏移,这里我们先不多介绍,后面再说,这里值是:

换成16进制就是414h。

9、stringIdsSize和stringIdsOff,这2个字段指定了dex文件中所有用到的字符串的个数和位置偏移,注意这里指的是位置偏移,而不是真正的字符串值。

我们来看看size是多少:

会发现一共有28个字符串,而其值的偏移从112开始,而这个112是不是有点熟悉,112是整个dex头的大小,也就说明在头部之后第一部分就是字符串索引,这里之所以叫做索引也很合理,从112开始的n个字节保存的是程序用到的字符串的偏移量,注意这里不是字符串,只是各个字符串的偏移量。

这时你可能会疑惑,这28个字符串的偏移量该如何存放以及值是多少,我们完全不用担心,还是打开010 Editor软件,选中数据结构是dex_string_ids即可:

会发现从70h开始,开始保存的每个字符串的偏移量,而这个偏移量对应的就是最后面部分的值,我们还是拿第一个字符串来说:

会发现这里的偏移量,我们转到偏移量会发现:

这里是用了10个字节来保存了一个字符串,这个字符串是"clinit",我们暂时不考虑这个字符串是啥意思,这里采用了一种叫做uleb的数据结构,来动态保存字符串长度,这里我们暂时不考虑细节。

其实从这个字符串保存的方法来看,我们已经能大概看出是如何保存的了。首先在头部保存字符串大小,以及字符串索引的偏移量,然后再遍历索引找到每个字符串。

比如上面我们的代码,在这里保存的字符串是如下:

10、typeIds和typeIdsOff,有了上面字符串保存的逻辑,这个就是类的类型的数量和位置偏移,也都是占用4个字节,我们还是来看看值:

一共用到了9个类型,但是注意这里就没必要像保存字符串一样了,记录每个类型的偏移量,再去偏移的地方取值,这里类型的描述符已经在前面字符串变量中都进行描述过了,所以这里保存的是字符串的索引,我们来看看:

找到上面对应的偏移位置,我们发现第一个类型值是0x5,然后我们再去前面的字符串索引找到下标为5的字符串:

会发现这里的值是I,也就是第一个类型,依次类推,所有的类型如下:

会发现这几个类型的字符串描述在前面字符串列表中都保存过了,这样设计也可以减小查询操作、节省内存。

11、protoIdSize和protoIdOff,这个表示的是方法原型的个数和位置偏移,会发现上面dex文件中有7个方法原型,这里图就不截了,来看一下这7个方法原型都保存了哪些数据:

其实不难理解,想表示一个方法原型不外乎就是方法名、返回值和参数,其中参数可能是多个,所以会有多个类型索引,这里具体的数据结构就不细说了,大体意思理解即可,来看看7个方法原型:

这里的方法也就是前面java文件中有使用到的。

12、fieldSize和fieldOff,这2个字段就比较简单了,表示java文件中字段的信息,从头部数据结构中会发现有3个字段,我们直接看一下字段索引的数据:

会发现到这里时,信息表示就变的简单了,因为你想表示一个字段,不外乎就是类型、其类的类型、以及自己描述的字符串,而由于前面我们已经得到了字符串索引和类型索引,所以这里数据结构中的值直接使用前面定义过的索引即可。

还是看一下定义的所有字段的值:

13、methodSize和methodOff,这2个字段就比较熟悉了,表示了方法,而方法的表示也是需要几个要点,比如方法所在的类、方法的声明以及方法名,而类在之前类型索引定义过了,方法声明也声明过了,以及方法名也就是之前定义的字符串索引,所以这里我们就不细看其数据结构了,直接看一下我们前面写的java文件有多少个方法:

这里一共有10个方法。

14、classDefsSize和classDefsOff,这2个字段表示类定义的相关信息,类的信息就比较多了,包括类的修饰符、父类、接口、注解、静态元素等等,我们也还是通过010 Editor来看一下class都保存了哪些信息:

可以发现还是有不少信息的。

到这里我们基本就可以把一个类的信息都整清楚了,我们使用一张图来表示:

上图虽然只是表示了文件头的信息,但是我们知道有了这些文件头的信息,根据偏移量便可以获取到其保存的值。

Dex文件格式总结

看了文件头的定义,并且明白其值的意义,便也就熟悉了整个Dex格式的保存原理,我们这里看一张图:

这里除了文件头还有索引区和数据区,其中索引区的偏移量已经在文件头中定义,而数据区则保存着类的定义以及索引区中的数据,而最下面的链接数据区则是一些静态库或者动态库的链接。

总结

本篇内容有点多,但是还是很好理解的,首先就是虚拟机,在Android系统的虚拟机需要读取dex文件,而这个dex文件是由我们编写的.java文件编译而来,所以dex文件应当保存.java文件的所有信息。

而保存这些信息的方法就像是文件头保存大致地址,索引区保存具体地址,数据区是真的地方,通过这种方式就可以完整的保存一个java文件的信息。

西瓜king

嵌入式开发工程师

145

文章

264k

阅读

367

粉丝 目录 收起

前言

正文

不同的虚拟机

JVM

DVM

ART

生成和查看dex文件

Dex文件格式

header 文件头

Dex文件格式总结

总结

友情链接:

java 设置label大小

sql 判断是不是英文

山西java程序员考试

sql 组合外键有什么用

一文读懂 DEX 文件格式解析-腾讯云开发者社区-腾讯云

DEX 文件格式解析-腾讯云开发者社区-腾讯云信安之路一文读懂 DEX 文件格式解析关注作者腾讯云开发者社区文档建议反馈控制台首页学习活动专区工具TVP最新优惠活动文章/答案/技术大牛搜索搜索关闭发布登录/注册首页学习活动专区工具TVP最新优惠活动返回腾讯云官网信安之路首页学习活动专区工具TVP最新优惠活动返回腾讯云官网社区首页 >专栏 >一文读懂 DEX 文件格式解析一文读懂 DEX 文件格式解析信安之路关注发布于 2020-07-17 11:43:585.6K0发布于 2020-07-17 11:43:58举报文章被收录于专栏:信安之路信安之路曾有人问我,为什么要去干解析 dex 文件这种麻烦的事?

我想说的是写个解析脚本不是为了模仿着 apktools 造轮子,而是在解析过程中寻找逆向的道路,方法会变,工具会变,但一切都建立在 dex 上的安卓不会变一、什么是 Dex 文件dex 文件是 Android 平台上可执行文件的一种文件类型。它的文件格式可以下面这张图概括:二、文件头解析1、文件头简介dex 文件头一般固定为 0x70 个字节大小,包含标志、版本号、校验码、sha-1 签名以及其他一些方法、类的数量和偏移地址等信息。如下图所示:2、dex 文件头各字段解析dex 文件头包含以下各个字段:magic: 包含了 dex 文件标识符以及版本,从 0x00 开始,长度为 8 个字节 checksum: dex 文件校验码,偏移量为: 0x08,长度为 4 个字节。 signature: dex sha-1 签名,偏移量为 0x0c, 长度为 20 个字节 file_szie: dex 文件大小,偏移量为 0x20,长度为 4 个字节 header_size: dex 文件头大小,偏移量为 0x24,长度为 4 个字节,一般为 0x70 endian_tag: dex 文件判断字节序是否交换,偏移量为 0x28,长度为 4 个字节,一般情况下为 0x78563412 link_size: dex 文件链接段大小,为 0 则表示为静态链接,偏移量为 0x2c,长度为 4 个字节 link_off: dex 文件链接段偏移位置,偏移量为 0x30,长度为 4 个字节 map_off: dex 文件中 map 数据段偏移位置,偏移位置为 0x34,长度为 4 个字节 string_ids_size: dex 文件包含的字符串数量,偏移量为 0x38,长度为 4 个字节 string_ids_off: dex 文件字符串开始偏移位置,偏移量为 0x3c,长度为 4 个字节 type_ids_size: dex 文件类数量,偏移量为 0x40,长度为 4 个字节 type_ids_off: dex 文件类偏移位置,偏移量为 0x44,长度为 4 个字节 photo_ids_size: dex 文件中方法原型数量,偏移量为 0x48,长度为 4 个字节 photo_ids_off: dex 文件中方法原型偏移位置,偏移量为 0x4c,长度为 4 个字节 field_ids_size: dex 文件中字段数量,偏移量为 0x50,长度为 4 个字节 field_ids_off: dex 文件中字段偏移位置,偏移量为 0x54,长度为 4 个字节 method_ids_size: dex 文件中方法数量,偏移量为 0x58,长度为 4 个字节 method_ids_off: dex 文件中方法偏移位置,偏移量为 0x5c,长度为 4 个字节 class_defs_size: dex 文件中类定义数量,偏移量为 0x60,长度为 4 个字节 class_defs_off: dex 文件中类定义偏移位置,偏移量为 0x64,长度为 4 个字节data_size: dex 数据段大小,偏移量为 0x68,长度为 4 个字节 data_off: dex 数据段偏移位置,偏移量为 0x6c,长度为 4 个字节3、dex 文件头代码解析示例 (python)dex 使用 open 函数以二进制打开文件,然后使用 seek 函数移动文件指针,例如 magic 就是f.seek(0x00),然后读取相应信息的字节数即可,例如读取版本号f.seek(0x04) f.read(4),然后做相应打印操作就行,dex 文件头较简单,不涉及编码等,所以解析起来感觉脑子都不用带。。。。。具体代码可以看下面或者 github,下面附上代码运行图:4、dex 文件头解析实现代码(python 实现)ps:只截取关键代码,完整代码请参考文末 github 链接或网盘链接三、checksum (校验和)解析1、checksum 介绍checksum(校验和)是 DEX 位于文件头部的一个信息,用来判断 DEX 文件是否损坏或者被篡改,它位于头部的0x08偏移地址处,占用 4 个字节,采用小端序存储。在 DEX 文件中,采用Adler-32校验算法计算出校验和,将 DEX 文件从0x0C处开始读取到文件结束,将读取到的字节数组使用Adler-32 校验算法计算出结果即是校验和即 checksum 字段!!!2、Adler-32 算法Adler-32算法如下步骤实现:a、定义两个变量varA、varB,其中varA初始化为1,varB初始化为0。b、 读取字节数组的一个字节(假设该字节变量名为byte),计算varA = (varA + byte) mod 65521,然后可以计算出varB = (varA + varB) mod 65521。c. 重复步骤,直到字节数组全部读取完毕,得到最终varA、varB两个变量的结果。d. 根据第三步得到的varA、varB两个变量,可得到最终校验和checksum =(varB << 16)+ varA。下面是官方 WIKI 给的例子:3、python 实现 Adler-32 算法先给出 Dex 文件头部信息以及代码跑出的结果python 代码实现如下(python 3.6 版本):四、字符串解析1、DEX 文件中的字符串a、DEX 文件大致上可以粗略的分为 3 个部分:文件头、索引区以及数据区。而文件头一般来说占了整个 DEX 文件 0x70 个字节(还不了解 DEX 文件头的可以看一下我前面两篇文章),在文件头中,关于字符串的相关信息一共有 8 个字节,分别位于 0x38(4 Bytes) 和 0x3c(4 Bytes) 处,前者说明了该 DEX 文件包含了多少个字符串,后者则是字符串索引区的起始地址,但是需要注意的是,DEX 存储是以小端序存储的(通俗一点的说就是从后往前读),如下所示:b、前面我们通过文件头知道了字符串数量和字符串索引区起始地址等信息,接下来我们就来具体看一下字符串索引区。字符串索引区存储的是字符串真正存储在数据区的偏移地址,以 4 个字节为一组,表示一个字符串在数据区的偏移地址,所以索引区一个占字符串数量 X 4个字节那么多,同样的,索引区也采用的是小端序存储,所以我们在读取地址时,需要与小端序的方式来读取真正的地址,如下所示:c、从上面我们已经知道了如何找到字符串在数据区的偏移地址,接下来我们需要做的就是解析这些数据区的字节。通过偏移地址我们可以在数据区找到代表字符串的这些字节,在 DEX 文件中,字符串是通过MUTF-8编码而成的(至于 mutf-8 是什么编码,我会将一些相关博客链接贴在文末),在MUTF-8编码中,第一个字节代表了这个字符串所需要用到的字节数目(不包括最后一个代表终结的字节),最后一个字节为0x00,表示这个字符串到此结束,跟 c 语言有点类似,中间部分才是一个字符串的具体内容,如下所示:(PS:mutf-8第一个字节还经过uleb128编码,所以简单的进行进制换算得到的字节数很多人奇怪对不上,由于比较复杂,就不过多解释了,想进一步了解更深的可以去看一下安卓源码中对 DEX 文件解析出字符串这一部分)2、解析代码:PS:我电脑运行环境-- python3.6代码关键截图如下:运行截图:五、类的类型解析1、DEX 文件中的类的类型a、Dex 文件中关于类的类型需要知道字符串是怎么解析的,如果不知道的,可以看一下前面部分。好了,切入正题,关于类的类型,就是一个对象的所属的类(大概这么理解吧。。。),例如在 java 中一个字符串,它的类型就是java/lang/String。在 Dex 文件头中,跟类的类型有关的一共有八个字节,分别是位于0x40处占四个字节表示类的类型的数量和位于0x44处占四个字节表示类的类型索引值的起始偏移地址,如下所示:b、关于类的类型数量,没什么好说的,只需要注意它是以小端序存储的,读取的时候注意即可。对于类的类型偏移地址,找到偏移地址后,它是以四个字节为一组,对应了在解析出来的字符串数组中的索引值,例如下图中的第一组,它的数据是BE 04 00 00,我们读取出来就是0x04BE(同样采用的小端序存储),对应的类的类型就是字符串数组 [0x04be]。2、解析脚本PS:我电脑上脚本运行环境 python3.6运行效果:代码关键截图如下:六、方法原型解析1、DEX 文件中的方法原型a、关于 dex 文件中方法原型的解析,需要知道怎么解析出字符串和类的类型,不明白的可以看前面解析。DEX 文件中的方法原型定义了一个方法的返回值类型和参数类型,例如一个方法返回值为void,参数类型为int,那么在 dex 文件中该方法原型表示为V(I)(smali中V表示void,I表示int)。在 dex 文件头部中,关于方法原型有两处,第一处位于0x48处,用 4 个字节定义了方法原型的数量,在0x4C处用 4 个字节定义了方法原型的偏移地址,如下所示:b、在上面我们知道了方法原型的起始偏移地址,接下来我们根据这个偏移地址找到方法原型,同样的,跟解析类的类型比较类似,一个方法原型所占字节数为 12 个字节,第一个字节到第四个字节表示了定义方法原型的字符串,这四个字节按小端序存储,读取出来为在字符串列表的索引,例如一个方法原型返回值为void,参数为boolean,那么定义该方法原型的字符串即为VZ;第 5 个字节到第八个字节表示该方法原型的返回值类型,读取出来的值为前面解析出来的类的类型列表的索引;第 8 个字节到第十二给字节表示该方法原型的参数,读取出来为一组地址,通过该地址可以找到该方法原型的参数,跳转到该地址去,首先看前 4 个字节,前四个字节按照小端序存储,读取出来的值为该方法原型参数的个数,接着根据参数个数,读取具体的参数类型,每个参数类型占 2 个字节,这两个字节读取出来的值为前面解析出来的类的类型列表的索引,如下所示:2、解析代码运行环境:我电脑环境为 python3.6运行截图:解析代码关键截图:七、字段解析1、dex 文件中的字段a、在 dex 文件头中,关于字段(ps:字段可以简单理解成定义的变量或者常量)相关的信息有 8 个字节,在0x50~0x53这四个字节,按小端序存储这 dex 文件中的字段数量,在0x54~0x57这四个字节,存储这读取字段的起始偏移地址,如下所示:b、根据上面的字段起始偏移地址,我们可以找到字段,表示一个字段需要用八个字节,其中,前两个字节为我们在前面解析出来类的类型列表的索引,通过该索引找到的类的类型表示该字段在该类中被定义的(ps:我是这么理解的,如有不对,还请纠正);第三个字节和第四个字节,也是类的类型列表的索引,表示该字段的类型,例如我们在 java 某个类中定义了一个变量int a,那么我们此处解析出来的字段类型就是int;最后四个字节,则是我们前面解析出来字符串列表的索引,通过该索引找到的字符串表示字段的,例如我们定义了一个变量String test;,那么我们在这里解析出来的就是test,如下图所示:2、解析代码解析代码运行截图:解析代码关键截图:八、方法定义解析1、Dex 文件中的方法定义a、在 dex 文件头中,关于方法定义的信息同样是八个字节,分别位于0x58处和0x5c处。在0x58处的四个字节,指明了 dex 文件中方法定义的数量,在0x5c处的四个字节,表明了 dex 文件中的方法定义的起始地址(ps:都是以小端序存储的),如下图所示:b、在上面的一步以及找到了方法定义的起始地址,跟字段类似的,一个方法定义也需要八个字节。其中,在前两个字节,以小端序存储着解析出来的类的类型列表的索引,表示该方法属于哪个类;第三个字节和第四个字节,以小端序存储这解析出来的方法原型列表的索引,通过该索引值找到的方法原型声明了该方法的返回值类型和参数类型;最后四个字节则以小端序存储着前面解析出来的字符串列表的索引,声明了该方法的方法名。如下图所示:2、解析代码解析代码运行截图:解析代码关键截图:九、类解析PS:Dex 文件解析到现在,终于到了最重要也是结构最复杂的部分了,这里分析的 dex 样本来自一个复杂 apk 的 dex 文件,但是代码运行时使用的样本是一个在网上找的很简单的 dex 样本,原因很简单,分析使用的 dex 涉及的 smali 指令太多了,大概有 200 多个,挨个解析起来工作量太大了,有时间我会写一个通用的 python 解析模块,完成了我会上传到 github 仓库,有兴趣的完成后可以看一下,用简单的 dex 只涉及到 5 个指令,代码写起来就没那么麻烦了!!!(tips:Dex 类数据这里解析起来有种俄罗斯套娃的感觉,多看几篇就很容易理解了。)1、uleb128 编码PS:本来关于 uleb128 编码网上一大堆,没必要写这个,但是网上的你抄我的我抄你的,能找的的相关资料基本都一样。。。。或者干脆贴个官方代码,官方代码的位运算写的很巧妙,但是直接去看的化,反正我是没看懂到底是怎么解码出来的。uleb128 编码,是一种可变长度的编码,长度大小为1-5字节,uleb128 通过字节的最高位来决定是否用到下一个字节,如果最高位为 1,则用到下一个字节,直到某个字节最高位为 0 或已经读取了 5 个字节为止,接下来通过一个实例来理解 uleb128 编码。假设有以下经过 uleb128 编码的数据(都为 16 进制)--81 80 04,首先来看第一个字节81,他的二进制为10000001,他的最高位为1,则说明还要用到下一个字节,它存放的数据则为0000001;再来看第二个字节80,它的二进制为10000000,它的最高位为1,则说明还需要用到第三个字节,存放的数据为0000000;再来看第三个字节04,它的二进制为00000100,最高位为0,说明一共使用了三个字节,它存放的数据为0000100;通过上面的数据我们已经获取了存放的数据,接下来就是把这些 bit 组合起来获取解码后的数据,dex 文件里面的数据都是采用的小端序的方式,uleb128 也不例外,在这三个字节,也不例外,第三个字节04存放的数据0000100作为解码后的数据的高 7 位,第二个字节80存放的数据0000000作为解码后的数据的中 7 位,第一个字节81存放的数据0000001作为解码后的数据的低 7 位;那么解码后的数据二进制则为0000100 0000000 0000001,转换为 16 进制则为0x10001。其他使用 5 个字节、4 个字节照此类推即可,下面是 python 读取 uleb128 的代码(ps:该代码是最终类数据解析代码的一共函数,无法单独运行,仅供参考,采用的是官方提供的位运算算法):复制2、类解析第一层结构:class_def_itema、在 dex 文件头0x60-0x63这四个字节,指明了class的数量,在0x64-0x67这四个字节,指明的class_def_item的偏移地址。如下所示:b、通过上面的偏移地址,我们可以找到 class_def_item 的起始地址,class_def_item 包含了一个类的类名、接口、父类、所属 java 文件名等信息。一个 class_def_item 结构大小为 32 字节,分别包含 8 个信息,每个信息大小为 4 字节(小端序存储):第 1-4 字节 -- class_idx(该值为前面解析出来的类的类型列表的索引,也就是这个类的类名); 第 5-8 字节 -- access_flags(类的访问标志,也就是这个类是 public 还是 private 等,这个通过官方的文档查表得知,具体算法在最后面说明); 第 9-12 字节 -- superclass_idx(该值也为前面解析出来的类的类型列表的索引,指明了父类的类名) 第 13-16 字节 -- interfaces_off(该值指明了接口信息的偏移地址,所指向的地址结构为 typelist,前面的文章有说过,这里不再多说,如果该类没有接口,该值则为 0) 第 17-20 字节 -- source_file_idx(该值为 dex 字符串列表的的索引,指明了该类所在的 java 文件名) 第 21-24 字节 -- annotations_off(该值为注释信息的偏移地址,由于注释信息不是我要解析的重点,要查看注释信息具体结构的可以参考官方文档,官方文档地址粘贴在文末) 第 25-28 字节 -- class_data_off(该值是这个类数据第二层结构的偏移地址,在该结构中指明了该类的字段和方法) 第 29-32 字节 -- static_value_off(该值也是一个偏移地址,指向了一个结构,不是重点,感兴趣的参考官方文档,如果没相关信息,则该值为 0) 具体分析过程,如下图所示:3、类解析第二层结构:class_data_itema、通过上面 class_def_item 的分析,我们知道了类的基本信息,例如类名、父类等啊,接下来就是要找到类里面的字段和方法这些信息,而这些信息,在 class_def_item 里面的 class_data_off 字段给我们指明class_data_item就包含这些信息并给出了偏移地址,即现在需要解析class_data_iem结构获取字段和方法信息。(ps:以下的数据结构不做特别说明都为 uleb128 编码格式)b、class_data_item结构包含以下信息:第一个uleb128编码--static_field_size,指明了该类的静态字段的数量第二个uleb128编码--instance_field_size,指明了该类的实例字段的数量(实例字段不知道是啥的建议百度) 第三个uleb128编码--direct_method_size,指明了该类的直接方法的个数第四个uleb128编码--virtual_method_size,指明了该类的虚方法的个数(虚方法理解不清楚的建议百度一下) encoded_field--static_fields,该结构指明了具体的静态字段信息,该结构的存在前提是static_field_size >0,该结构包含两个 uleb128 编码,第一个 uleb128 编码为前面解析出来的字段列表的索引,第二个 uleb128 编码指明了该字段的访问标志 encoded_field--instance_fields,跟上面类似,不再多说,值得注意的是,该结构存在的前提是instance_field_size > 0encoded_method--direct_methods,该结构指明了直接方法具体信息,该结构存在的前提同样是direct_method_size > 0,该结构包含 3 个 uleb128 编码,第一个 uleb128 为前面文章解析出来的方法原型列表的索引值,第二个 uleb128 编码为该方法的访问标志,第三个 uleb128 为 code_off,也就是该方法具体代码的字节码的偏移地址,对应的结构为 code_item,code_item 结构里面包含了该方法内部的代码,这里是字节码,也就是 smali(ps: 如果该方法为抽象方法,例如 native 方法,这时 code_off 对应的值为 0,即该方法不存在具体代码) encoded_method--virtual_methods,该结构指明了该类的虚方法的具体信息,存在前提为virtual_method_size > 0,具体结构和上面一样,不再多说 具体分析过程,如下图所示:4、类解析的第三层结构:code_itema、在上面的 class_data_item 结构中的encoded_method结构的第三个 uleb128 编码中,指出了一个类中的方法具体代码的偏移地址,也就是 dv 虚拟机在执行该方法的具体指令的偏移地址,该值指向的地址结构为code_item,里面包含了寄存器数量、具体指令等信息,下面来分析一下该结构。b、code_item结构包含以下信息:第 1-2 字节 -- registers_size,该值指明了该方法使用的寄存器数量,对应的 smali 语法中的.register的值 第 3-4 字节 -- ins_size,该值指明了传入参数的个数 第 5-6 字节 -- outs_size,该值指明了该方法内部调用其他函数用到的寄存器个数 第 7-8 字节 -- tries_size,该值指明了该方法用到的try-catch语句的个数 第 9-12 字节 -- debug_info_off,该值指明了调试信息结构的偏移地址,如果不存在调试信息,则该值为 0 第 13-16 字节 -- insns_size,该值指明了指令列表的大小,可以这么理解:规定了指令所用的字节数大小--2 x insns_sizeushort[insns_size] -- insns,这个是指令列表,包含了该方法所用到的指令的字节,每个指令占用的字节数可以参考官方文档,这个没什么算法,就是一个查表的过程,例如invoke-direct指令占用 6 个字节,return-void指令占用 2 个字节 2 个字节 -- padding,该值存在的前提是tries-size > 0,作用用来对齐代码 try_item--tries,该值存在的前提是tries-size > 0,作用是指明异常具体位置和处理方式,该结构不是解析重点,重点是解析指令,感兴趣的查看官方文档 encoded_catch_handler_list--handlers, 该结构存在前提为tries-size > 0,同样不是解析重点,感兴趣的查看官方文档 具体分析过程,如下图所示:5、access_flags算法access_flags 访问标志具体值可以去查看官方文档,下图只截了一部分。如果 access_flags 的算法为access_flags = flag1 | flag2 | ...,如果访问标志只有一共,直接查表即可,如果是两个,按照算法对比值即可,下面举个例子来理解该算法。例如我有一个类的访问标志为public static,经过查表得知public对应的值为0x01,static对应的值为0x8,那么public static对应的访问标志为0x01 | 0x08 = 0x9,如果读取出来的 access_flags 为 0x09,那么对应的访问标志则为public static,其余的照此算法计算即可!!!6、解析代码PS:代码运行环境推荐 3.6 及其以上,需要模块binascii,运行样本为Hello.dex,样本附在文末网盘链接中!!!运行截图通过脚本解析出来的和通过 apktools 反编译出来的 smali 文件对比图(ps:左侧为 apktools 反编译出来的,右侧为脚本解析出来的,可以发现基本差不多)解析代码关键截图:十、一些总结有人可能会问没事做这么多复杂的工作干啥,不是有 apktools 可以直接反编译就完了嘛,但我想说的是,做这么做解析工作不是想替换 apktools,而是想在这个解析过程中弄明白 dex 文件格式,只有先理解了这些文件的格式,才能写一下加固方案和分析一些可能市面上的一些小众的壳。例如 java 层指令抽取壳,就是在最后的方法指令上做保护。逆向方法会变,工具会变,但这些最根本的东西,不会变,理解这些,是一名搞安卓安全的必备知识吧!!!十一、参考资料和样本代码地址参考资料:1、Android 逆向之旅—解析编译之后的 Dex 文件格式:http://www.520monkey.com/archives/5792、一篇文章带你搞懂 DEX 文件的结构:https://blog.csdn.net/sinat_18268881/article/details/558327573、官方文档:https://source.android.google.cn/devices/tech/dalvik/dex-format#embedded-in-class_def_item,-encoded_field,-encoded_method,-and-innerclass样本及代码下载:github 链接:https://github.com/windy-purple/parserDex本文参与 腾讯云自媒体分享计划,分享自微信公众号。原始发表:2020-07-16,如有侵权请联系 cloudcommunity@tencent.com 删除java存储编程算法本文分享自 信安之路 微信公众号,前往查看如有侵权,请联系 cloudcommunity@tencent.com 删除。本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!java存储编程算法评论登录后参与评论0 条评论热度最新登录 后参与评论推荐阅读LV.关注文章0获赞0目录一、什么是 Dex 文件二、文件头解析1、文件头简介2、dex 文件头各字段解析3、dex 文件头代码解析示例 (python)4、dex 文件头解析实现代码(python 实现)三、checksum (校验和)解析1、checksum 介绍2、Adler-32 算法3、python 实现 Adler-32 算法四、字符串解析1、DEX 文件中的字符串2、解析代码:五、类的类型解析1、DEX 文件中的类的类型2、解析脚本六、方法原型解析1、DEX 文件中的方法原型2、解析代码七、字段解析1、dex 文件中的字段2、解析代码八、方法定义解析1、Dex 文件中的方法定义2、解析代码九、类解析1、uleb128 编码2、类解析第一层结构:class_def_item3、类解析第二层结构:class_data_item4、类解析的第三层结构:code_item5、access_flags算法6、解析代码十、一些总结十一、参考资料和样本代码地址参考资料:样本及代码下载:相关产品与服务对象存储对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。免费体验产品介绍产品文档COS新用户专享存储包低至1元,新老同享存储容量低至0.02元/GB/月,立即选购!

领券社区专栏文章阅读清单互动问答技术沙龙技术视频团队主页腾讯云TI平台活动自媒体分享计划邀请作者入驻自荐上首页技术竞赛资源技术周刊社区标签开发者手册开发者实验室关于社区规范免责声明联系我们友情链接腾讯云开发者扫码关注腾讯云开发者领取腾讯云代金券热门产品域名注册云服务器区块链服务消息队列网络加速云数据库域名解析云存储视频直播热门推荐人脸识别腾讯会议企业云CDN加速视频通话图像分析MySQL 数据库SSL 证书语音识别更多推荐数据安全负载均衡短信文字识别云点播商标注册小程序开发网站监控数据迁移Copyright © 2013 - 2024 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有 深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569腾讯云计算(北京)有限责任公司 京ICP证150476号 |  京ICP备11018762号 | 京公网安备号11010802020287问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档Copyright © 2013 - 2024 Tencent Cloud.All Rights Reserved. 腾讯云 版权所有登录 后参与评论00

聊聊 APK(一) —— 直接运行 Dex - 知乎

聊聊 APK(一) —— 直接运行 Dex - 知乎首发于Beyond mPaaS切换模式写文章登录/注册聊聊 APK(一) —— 直接运行 DexmPaaS高效、灵活、稳定的移动研发、管理平台因为近期的工作接触了许多 android 工具链的东西,所以我们就来介绍下 APK 这个耳熟能详的文件。首先,我们先看看如何使用 Dex 文件在手机终端上输出一个 HelloWorld编译和运行工具学习过 Android 的人一定知道,在 Android OS 上跑的虚拟机曾经叫 dalvik,现在叫 ART (Android Runtime),为了方便,下文不再区分两者差别,暂时统称 dalvik。如果把 dalvik 当作一个黑盒,无视细节,我们就能拿他和 jvm 进行类比。那么,在学习 java 语言之初,使用 IDE 进行 java 开发之前,我们一定知道有两个二进制文件叫做 javac 和 java,一个是将 xxx.java 源代码编译成 xxx.class 字节码,一个是启动虚拟机加载运行字节码。那么在 Android 中,dx 类似 javac,但是它的输入不是 java 源代码,而是 class 字节码,输出是大名鼎鼎的dex文件,今天我们不探讨dex和class文件的区别,我们只要知道,把class文件和dex文件分别指向给不同的二进制做输入,就可以执行里面的逻辑。jvm 里面运行class的是java,那么 Android 里面运行dex的二进制文件,是dalvikvm> adb shell

> dalvikvm -version一如既往令人讨厌的单横杠我的手机是一台运行 Android 9 的手机,输出的结果是:> ART version 2.1.0 arm64如果我们在 jvm 的环境下,运行> java -version那么输出的结果是>  ~/Desktop/ java -version> java version "1.8.0_77"> Java(TM) SE Runtime Environment (build 1.8.0_77-b03)> Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode)可以看见我的机器上运行的是 java 8,好,运行工具暂时介绍到这里,接下来我们看下如何让 jvm 和 dalvik 运行 HelloWorld 程序。Compile HelloWorld.java首先,我们需要写代码,写一个简单的 HelloWorld.java 文件:public class HelloWorld {

public static void main(String[] args) {

System.out.println("Hello World!");

}

}这四行 java 代码不能更简单了,我们应该不能更熟悉了。我们从上一个章节知道dx的输入格式是class文件,javac的输入格式是 java 源代码,输出是`class`文件,也就是说,不管怎么样,我们都需要生成`class`文件,那么,生成的方式很简单,只需要运行javac HelloWorld.java即可,在当前目录下,就会出现一个HelloWorld.class文件,jvm 上需要的文件就准备好了,接下来看看 dalvik 上需要准备的东西。学习过 Android 的人可能会了解到,class -> dex 需要的工具是dx,它属于 Android Platform Build Tools 的一部分,会随着 SDK 的分发更新而更新,在我这使用的是 28.0.3 版本,所以它的路径就是`$ANDROID_HOME/build-tools/28.0.3/dx`,以下简称dx,这个二进制文件平常我们虽然天天会用,但是不会直接接触,所以对于我们来说是陌生的,知道这个二进制文件所在的路径,第一步我的习惯是使用`--help`命令看一下它能做什么工作(又要吐槽下垃圾 java 的单横杠),执行dx --help,我们看见如下输出(省略暂时不重要的部分)dx --dex [--debug] [--verbose] [--positions=