1.Dalvik虚拟机
- 特点
- 体积小, 占用内存空间少。
- 专有的DEX ( Dalvik Exe c utable )可执行文件格式, 体积小, 执行速度快。
- 常量池采用32位索引值, 对类方法名、 字段名、 常量的寻址速度快。
- 基于寄存器架构, 同时拥有 一套完整的指令系统。
- 提供了对象生命周期管理、 堆枝管理、 线程管理、 安全和异常管理及垃圾回收等重要功能。
- 所有的Android程序都运行在Android系统进程中, 每个进程都与一个 Dalvik虚拟机实例对应。
- Dalvik虚拟机与Java虚拟机的区别
-
运行的字节码不同
-
Dalvik可执行文件的体积更小。(dx工具:将Java字节码转换为Dalvik字节码)
dx对所有Java类文件中的常量池进行了分解, 消除了其中的冗余信息,然后将它们重新组合形成一个常量池,并让所有类文件共享这个常量池。
-
虚拟机架构不同
java: 基于
栈架构
。Dalvik: 基于
寄存器架构
。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x2slSsm6-1611245855983)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/JVM.png)]
iload_1:i表示int类型,load表示将局部变量存入Java栈,1表示要操作的哪个局部变量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcJk22WZ-1611245855986)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/Dalvik%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81.png)]
add-int v0,v3,v4:将v3与v4相加,并将值存入v0。
- 虚拟机的执行方式
- 即时编译(Just-in-time Compilation ,JIT)又称动态编译, 是一种通过在运行时将字节码翻译为机器码使得程序的执行速度加快的技术。
- 主流的JIT包括两种字节码编译方式
- method方式:以函数或方法为单位进行编译。
- trace方式:以trace为单位进行编译。
2.Dalvik 语言基础
-
Dalvik字节码
-
类型
-
基本类型
-
引用类型
-
Dalvik字节码类型描述符
语法 Z B J L [ 含义 boolean byte long Java类类型 数组 -
Lpackage/name/ObjectName;
L 表示后面眼着一 个Java类,package/name/表示对象所在 的包,ObjectName表示对象的名称,分号表示对象名结束。
-
方法
-
Lpackage/name/ObjectName;->MethodName(III)Z
MethodName为具体的方法名,(III)Z是方法的签名部分, 括号内的III 为方法的参数(在此为三个整型参数),z表示方法的返回类型。
-
-
Dalvik指令集
-
指令类型
-
move-wide/from16 vAA, vBBBB
-
move为基础字节码;
-
-wide为名称后缀,表示指令操作 的数据宽度(64位);
-
from16为字节码后缀,表示源为一个16位的寄存器引用变量;
-
vAA为目的寄存器,取值范围为v0~v255;
-
vBBBB为源寄存器, 取值范围为v0~v65535;
-
-
空操作指令
- 助记符为nop,值为00;
-
数据操作指令
-
move vA,vB 将vB寄存器的值赋予vA寄存器,源寄存器与目的寄存器都为4位 move/from16 vAA,vBBBB 将vBBBB寄存器的值赋予vAA 寄存器,源寄存器为16位,目的寄存器为8位 move/16 vAAAA,vBBBB 将vBBBB寄存器的值赋予vAAAA寄存器, 源寄存器与目的寄存器都为16位。 move-wide vA,vB 为4位的寄存器对赋值, 源寄存器与目的寄存器都为4位 -
move-wide vA, vB
指令用于为4位的寄存器对赋值, 源寄存器与目的寄存器都为4位。
-
move-result vAA
指令用于将上 一 个invoke类型指令操作的单字非对象结果赋予vAA寄存器。
-
-
返回指令
-
return vAA
指令表示函数返回 一 个32位非对象类型的值, 返回值为8位寄存器vAA。
-
-
数据定义指令
-
const/16 vAA, #+BBBB
指令用于将数值符号扩展为32位后赋予寄存器vAA。
-
-
锁指令
-
monitor-enter vAA monitor-exit vAA 为指定对象获取锁 释放指定对象的锁
-
-
实例操作指令
-
check-cast vAA, type@BBBB
指令用于将vAA寄存器中的对象引用转换成指定的类型。
-
new-instance vAA, type@BBBB
指令用于构造一个指定类型对象的新实例, 并将对象引用赋值给vAA寄存器 。 类型符type指定的类型不能是数组类。
-
instance-of vA, vB, type@CCCC
指令用于判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以就为vA寄存器赋值1,否则为vA寄存器赋值为0。
-
-
数组操作指令
-
array-length vA, vB
指令用于获取给定vB寄存器中数组的长度, 并将值赋予vA寄存器。
-
-
arrayop vAA, vBB, vCC
-
指令用于对vBB寄存器指定的数组元素进行取值与赋值;
- vCC寄存器用于指定数组元素的索引;
- vAA寄存器用于存放读取的或需要设置的数组元素的值;
- 读取元素时使用aget类指令, 为元素赋值时使用aput类指令;
-
new-array vA, vB, type@CCCC
指令用于构造指定类型(type@CCCC )和大小(vB)的数组,并将值赋予vA寄存器。
-
-
跳转指令
-
无条件跳转指令goto
goto/16 +AAAA
指令用于无条件跳转到指定偏移处, 偏移量 AAAA不能为0。
-
packed-switch vAA, +BBBBBBBB
- 分支跳转指令,vAA寄存器为switch分支中需要判断的值,BBBBBBBB指向一个packed-switch -payload格式的偏移表, 表中的值是递增的偏移量。
-
-
若packed改为sparse,则表中的值是无规律的偏移量。
-
if-test vA, v8, +CCC
-
-
-
if-test类型指令如下
smali if-eq if-ne if-lt if-ge if-gt if-le java if(vA==vB) if(vA!=vB) if(vA<vB) if(vA>=vB) if(vA>vB) if(vA<=vB) -
if-testz vAA, +BBBB
-
-
指令将vAA寄存器的值与0进行比较,若比较结果满足或值为0,就跳转。
-
比较指令
-
cmpl-float cmpg-float cmpl -double cmpg-double 若vBB>vCC,结果为-1; 若=,则结果为0;若<,则结果为1 若vBB>vCC,结果为1. 若vBB对>vCC对,结果为-1. 若vBB对>vCC对,结果为1.
-
-
字段操作指令
- 普通字段的指令前缀为i。
- 静态字段的指令前缀为s。
-
数据运算指令
binop vAA, vBB, vCC 将vBB 寄存器与vCC 寄存器进行运算, 将运算结果保存到vAA寄存器中 binop/2addr vA, vB 将vA寄存器与 vB寄存器进行运算, 将运算结果保存到vA寄存器中 rem-type vBB % vCC shl-type vBB << vCC
3.Android文件格式
库文件
jar包
- zip格式的压缩包文件,放着编译后Java代码的class文件的集合。
aar包
- 既包含了代码,又包含了开发中所有使用的资源数据,解决了手动复制图片,声音,布局等资源的繁琐。
APK
classes.dex
DEX文件使用的数据类型
自定义类型 原类型 含义 s1 int8_t 8位有符号整型 u1 uint8_t 8位无符号整型 s2 int16_t 16位有符号整型,小端字节序 u2 uint16_t 16位无符号整型, 小端字节序 s4 int32_t 32位有符号整型, 小端字节序 u4 uint32_t 32位无符号整型, 小端字节序 s8 int64_t 64位有符号整型, 小端字节序 u8 uint64_t 64位无符号整型, 小端字节序 sleb128 无 有符号LEB128, 可变长度 ulebl28 无 无符号LEB128,可变长度 ulebl28pl 元 无符号LEB128加1,可变长度
- 每个LEB128由I~5字节组成,所有的字节组合在一起表示一个32位的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gD0hHTSS-1611245855988)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/LEB128.png)]
- 每个字节只有7位为有效位, 如果第l个字节的最高位为1, 表示LEB128需要使用第2个字节, 如果第2个字节的最高位为l, 表示会使用第3个字节, 依此类推, 直到最后 一个字节的最高位为0为止。 当然,LEB128最多使用5字节, 如果读取5字节后下一个字节的最高位仍为1, 则表示该DEX文件无效 , Dalvik虚拟机在验证DEX文件时会失败井返回。
DEX文件的结构
- | dex header | dex文件头部记录整个dex文件的相关属性 |
| :--------------- | ------------------------------------------------------------ |
| string_table | 字符串数据索引,记录了每个字符串在数据区的偏移量 |
| type_table | 类似数据索引,记录了每个类型的字符串索引 |
| proto_ids | 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数列表 |
| field_ids | 字段数据索引,记录了所属类,类型以及方法名 |
| method_ids | 类方法索引,记录方法所属类名,方法声明以及方法名等信息 |
| class_def | 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量 |
| data | 真实的数据 |
| link_data | 静态链接数据区 |
![]()
magic
字段:表示这是一个有效的DEX文件, 目前它的值固定为 “64 65 78 Oa 30 33 35 00”checksum
字段:DEX文件的校验和,可以判断DEX文件是否已经损坏或被篡改signature
字段:识别未经dexopt优化的DEX文件filesize
字段:记录了包括DexHeade在内的整个DEX文件的大小headerSize
字段:记录了DexHeader结构本身占用的字节数endianTag
字段:指定了DEX运行环境的CPU字节序, 预设值ENDIAN_CONSTANT等于0x12345678, 表示默认采用小端字节序linkSize与linkOff
字段:分别指定了链接段的大小与文件偏移, 在大多数情况下它们的值为0mapOff
字段:指定了DexMapList结构的文件偏移- 接下来的字段则分别表示 DexStringId、DexTypeId、DexProtoId、DexFieldId、DexMethodId、DexClassDef及数据段的大小与文件偏移
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPMB7JmR-1611245855991)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/DexCode.png)]
registersSize
字段:指定了方法中使用的寄存器的个数。(对应于smali语法中.registerd的值)insSize
字段:指定了方法的参数的个数(对应于.paramter)outsSize
字段:指定了方法在调用外部方法时使用的寄存器的个数triesSize
字段:指定了方法中try/catch语句的个数insnsSize
字段:指定了接下来的指令的个数insns
字段:真正的代码部分4.AndroidManifest.xml
android:allowBackup= ''true''
android:allowBackup允许系统 在进行备份操作时备份程序的应用数据, 典型的操作是在终端执行 adb backup命令, 或者点击手机设置界面上的 “备份操作 ” 按钮。
android:supportsRtl= ''true''
这个标签的作用是让APK支持 RTL ( Right-to-Left )视图。
AXML文件格式
Android Studio 在编译APK文件时, 会将AndroidManifest.xml处理后打包进去。打包进去的AndroidManifest.xml被编译成了二进制格式的文件,这个打包后的AndroidManifest.xml称为 “AXML”。
AXML文件结构
文件头
ResFileheader
,字符串池ResStringPool
,资源ID块ResIDs
,XML数据内容块ResXMLTree
四部分线性地组成。
ResFileheader
表示文件的头部, 在这里用ResChunk_header
表示,其定义如下:struct ResChunk_header { uint16_t type; uint16_t headerSize; uint32_t size; }
type
字段描述了chunk
所属结构体的类型。header_size
字段表示当前ResChunk_header
结构的大小。file_size
字段表示该chunk
结构体数据的长度。
字符串池ResStringPool ,它包含了AXML中使用的所有字符串。字符串池由
字符串池头ResStringPool_header
,字符串列表
,样式列表
三部分组成。ResStringPool_header定义如下:
struct ResStringPool_header { struct ResChunk_header header; uint32_t stringCount; uint32_t styleCount; enum { SORTED FLAG= 1<<0, UTF8 FLAG = 1<<8 }; uint32_t flags; uint32_t stringsStart; uint32_t stylesStart; };
stringCount与styleCount字段分别表示这个池中字符串的数目与样式的数目。flags字段用于标识字符串的类型是UTF-8还是 16 位编码 。 stringsStart与stylesStart字段分别表示字符串列表与样式列表在文件中的偏移量。
字符串数据是 一个字符串索引列表, 每条索引都使用ResStringPool_string结构体来表示:
struct ResStringPool_string { uint32_t index; };
index字段指向字符串在文件中的具体偏移量,其指向的内容可能是一个UTF-8字符串,也可能是一个16位编码字符串。
资源ID块ResIDs,该部分主要用于 存放AndroidManifest.xml使用的系统属性值所对应的资源ID, 结构定义如下:
typedef struct { ResChunk_header header; int count; uint ids[count]; } ResIDs;
header字段的 type 在这里是 0x180, 即RES_XML_RESOURCE_MAP_TYPE, 表示这是资源表。count字段表示资源ID的个数。ids字段中存放的是一个个资源ID。
每个资源ID都是一个32位的整型值,由三部分组成:
- Package ID :相当于一个命名空间, 用于限定资源的来源。
- Type ID:表示资源的类型ID。
- Entry ID:指明了每一个资源在其所属的资源类型中的索引位置。
ResXMLTree,它用于 表示XML文件的具体内容。它是一个线性的XML节点数据集合,由多个XML节点数据组成, 由多个XML节点数据组成, 每个XML节点数据由基本结构体ResXMLT陀飞 node和扩展结构体组成。 ResXMLTree_node 的定义如下:
struct ResXMLTree node { struct ResChunk_header header; uint32_t lineNumber; structResStringPool_ref comment; };
对第1个节点来说,header的type字段必须是RES_XML_START_NAMESPACE_TYPE, 表示这是一个namespace开始节点。与此对应的是ResxXMLTree 部分的最后一个 ResXMLTr、民_node, 它的header的type字段必须是RES_XML_END_NA问ESPACE_TYPE, 表示 namespace 节点的结束。lineNumber字段表示节点数据在 AndroidManifest 文件中的行号, 占用4字节。
AXML文件的修改
目前,部分APK保护工具及一些厂商的加固方案利用了Android系统解析AXML的漏洞,在编译APK时构造畸形的AXML, 使系统能正常安装APK,但无法运行ApkTool 这类反编译工具 。 在这种情况下 ,需要对AXML进行修改,最直接的修改方式是:配合使用。10Editor及AXML模板查看文件格式,找到异常部分后进行修改。 对一些已经出现的AXML加固方案,可以使用现成的工具来修改, 具体如下。
AmBinaryEditor,下载地址为https://github.com/ele7enxxh/AmBinaryEditor。
AndroidManifestFix,下载地址为https://github.com/zylc369/AroidManifestFix。
5.resources.arsc
ARSC文件格式
一个 ARSC 从整体结构上看, 由文件头ResTableHeader、资源项值字符串池ResStringPool、Package 数据内容块 ResTablePackage 三部分线性地组成。
文件头ResTableHeader:
struct ResTable_header { struct ResChunk_header header; uint32_t packageCount; };
header字段的类型是ResChunk_header,header的type字段指向的类型为RES_TABLE_TYPE,表示这是一个ARSC文件。packageCount字段指明该ARSC中包含多少个Package的资源信息,它对应ARSC文件中Package数据内容块ResTablePackage的个数。
Package数据内容块ResTablePackage,它由数据内容块头ResTable_package、资源类型字符串池 TypeStrings、资源项名称字符串池 KeyStrings、资源表规范ResTable_typeSpec、资源表类型配置ResTable_type五部分组成。ResTable_package的定义如下:
struct ResTable_package { struct ResChunk_header header; uint32_t id; uint16_t name[128]; uint32_t typeStrings; uint32_t lastPublicType; uint32_t keyStrings; uint32_t lastPublicKey; uint32_t typeIdOffset; };
header的 type字段的类型是RES_TABLE_PACKAGE_TYPE。id字段指定了Package的ID, 对用户编译的 APK 来说,它的取值是Ox7F。name字段指定了Package的名称 ,该名称通常就是APK的包名。typeStrings字段是一个偏移量,指的是资源类型字符串池typeStrings在文件中相对ResTable_package结构体的偏移量。lastPublicType字段指的是导出的Public类型的字符串在资源类型字符串池中的索引。keyStrings字段是一个偏移量,指的是资源项名称字符串池 KeyStrings在文件中相对ResTable_package结构体的偏移量。lastPublicKey字段指的是导出的Public资源项名称字符串在资源项名称字符串池中的索引。typeidOffset 字段指的是类型ID的偏移量。
在KeyStrings下面就是ResTable_typeSpec与ResTable_type,它们在文件中可能交叉出现。 ResTable_typeSpec的定义如下:
struct ResTable_typeSpec { struct ResChunk_header header; uint8_t id; uint8_t res0; uint16_t res1; uint32_t entryCount; };
header的type字段在这里是RES_TABLE_TYPE_SPEC_TYPE。id字段指明了类型规范资源的Type ID。内s0与resl字段的值目前必须是0。 entryCount字段指明了接下来的flags的个数(每个flag都是32位整型的)。
6.META-INF目录
CERT.RSA 存放了 APK 的开发者证书与签名信息 MANIFEST.MF 签名的清单文件, 它是一个文本文件 CERT.SF 签名信息文件, 它也是一个文本文件 7.ODEX
ODEX与DEX相比, 多出了如下内容:
DexOptHeader ODEX文件头, 描述了ODEX文件的基本信息 Dependences 依赖库列表 , 描述了ODEX文件加载时可能使用的依赖库 ClassLookups 优化数据块的类索引列表信息, 用于提高类搜索速度 RegisterMaps 优化数据块的寄存器图( Register Map )信息, 主要用于帮助Dalvik虚拟机进行精确的垃圾回收( Garbage Collection ) ODEX文件转换成DEX文件
先将ODEX文件反编译成smali文件, 再将smali文件编译成DEX文件,我们将这个过程称为 “deodex” 。
8.OAT
OAT是优化过的, 用于ART虚拟机执行的DEX文件,类似于Dalvik的ODEX文件。
ART虚拟机
ART使用AOT ( Ahead-of-Time )编译技术,在APK 第一次安装或系统升级、 重启时, 通过调用dex2oat命令将APK 中的DEX文件 静态编译成OAT文件并存放到 Android设备的/data/dalvik-cache 或/data/app/package目录下。AOT的静态编译操作会影响APK的安装效率,于是新版本的Android使用的是基于JIT on AOT的编译技术,可以明显加快安装速度。
OAT文件格式
OAT文件格式完全融入Android所特有的ELF格式,一个OAT文件必须包含 oatdata、oatexec、oatlastword三个符号。
oatdata
符号指向的地址是 OAT 所在 ELF的.rodata 段,这里存放的是 OAi:文件头OATHeader、OAT 的 DEX文件头,OATDexFile、原始的DEX文件 DexFile、OAT的DEX类OatClass等信息。oatexec
符号指向的地址是OAT所在ELF的.text段, 这里存放的是编译生成的Native指令代码。oatlastwo
时符号指向的地址是OAT文件结束处在ELF中的文件偏移, 通过它可以确定OAT文件的内容在哪里结束。![]()
将OAT文件转换成DEX文件
定位OAT文件中的DexFile结构体, 将它的完整数据导出。Android系统提供了 oatdump命令, 使用该命令的–export-dex-to参数可以将OAT中的所有DEX文件导出, 放到指定的目录下。
adb shell 'oatdump --oat-file=/data/dalvik-cache/arm/system@framework@boot.oat --export-dex-to=/data/local/tmp'
如果当前环境中没有合适的Andrid设备,要导出DEX的OAT文件就在本地, 可以尝试自行编写导出工具。传统的方法是解析OAT文件格式, 定位DexFile结构体后导出DEX 文件。 一种更简单的方法是:由于DEX文件有固定的文件头
magic''dex\n035''
, 只需要在OAT文件中搜索它, 就可以快速定位DEX文件的开头(在DEX文件头的0x20字节处保存了DEX文件的完整大小, 这样一来, 要导出的偏移量与文件大小就都有了。 接下来, 只需向下搜索, 即可定位所有的DEX文件)。