什么是 dex 文件
他是Android系统的可执行文件,包含应用程序的全部操作指令以及运行时数据。
由于dalvik是一种针对嵌入式设备而特殊设计的java虚拟机,所以dex文件与标准的class文件在结构设计上有着本质的区别
当java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右
可以看见:
dex将原来class每个文件都有的共有信息合成一体,这样减少了class的冗余
数据结构
类型 | 含义 |
---|---|
u1 | unit8_t,1字节无符号数 |
u2 | unit16_t,2字节无符号数 |
u4 | unit32_t,4字节无符号数 |
u8 | unit64_t,8字节无符号数 |
sleb128 | 有符号LEB128,可变长度1~5 |
uleb128 | 无符号LEB128, |
uleb128p1 | 无符号LEB128值加1, |
dex文件结构
首先从宏观上来说 dex 的文件结构很简单,实际上是由多个不同结构的数据体以首尾相接的方式拼接而成。如下图:
数据名称 | 解释 |
---|---|
header | dex文件头部,记录整个dex文件的相关属性 |
string_ids | 字符串数据索引,记录了每个字符串在数据区的偏移量 |
type_ids | 类似数据索引,记录了每个类型的字符串索引 |
proto_ids | 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数列表 |
field_ids | 字段数据索引,记录了所属类,类型以及方法名 |
method_ids | 类方法索引,记录方法所属类名,方法声明以及方法名等信息 |
class_defs | 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量 |
data | 数据区,保存了各个类的真是数据 |
link_data | 连接数据区 |
header
简单记录了dex文件的一些基本信息,以及大致的数据分布。长度固定为0x70,其中每一项信息所占用的内存空间也是固定的,好处是虚拟机在处理dex时不用考虑dex文件的多样性
字段名称 | 偏移值 | 长度 | 说明 |
---|---|---|---|
magic | 0x0 | 8 | 魔术字段,值为"dex\n035\0" |
checksum | 0x8 | 4 | 校验码 |
signature | 0xc | 20 | sha-1签名 |
file_size | 0x20 | 4 | dex文件总长度 |
header_size | 0x24 | 4 | 文件头长度,009版本=0x5c,035版本=0x70 |
endian_tag | 0x28 | 4 | 标示字节顺序的常量 |
link_size | 0x2c | 4 | 链接段的大小,如果为0就是静态链接 |
link_off | 0x30 | 4 | 链接段的开始位置 |
map_off | 0x34 | 4 | map数据基址 |
string_ids_size | 0x38 | 4 | 字符串列表中字符串个数 |
string_ids_off | 0x3c | 4 | 字符串列表基址 |
type_ids_size | 0x40 | 4 | 类列表里的类型个数 |
type_ids_off | 0x44 | 4 | 类列表基址 |
proto_ids_size | 0x48 | 4 | 原型列表里面的原型个数 |
proto_ids_off | 0x4c | 4 | 原型列表基址 |
field_ids_size | 0x50 | 4 | 字段个数 |
field_ids_off | 0x54 | 4 | 字段列表基址 |
method_ids_size | 0x58 | 4 | 方法个数 |
method_ids_off | 0x5c | 4 | 方法列表基址 |
class_defs_size | 0x60 | 4 | 类定义标中类的个数 |
class_defs_off | 0x64 | 4 | 类定义列表基址 |
data_size | 0x68 | 4 | 数据段的大小,必须4k对齐 |
data_off | 0x6c | 4 | 数据段基址 |
magic
标识一个有效的dex文件,他的固定值为:64 65 78 0a 30 33 35 00,转换为字符串为dex.035.
在电子取证中也称“文件签名”
checksum
他是整个头部的校验和。它被用来校验头部是否损坏
signature
二次打包时的签名
file_size
记录包括dexHeader在内的整个dex文件大小,用来计算偏移和方便定位某区段(section),他也有诸如唯一的标识dex,因为他是dex文件中计算sha-1区段的一个组成部分
header_size
存放整个DexHeadeer结构体的长度,它也可用来计算下一个区段在文件中的起始位置,目前值为0x70
脱壳原理
什么是软件脱壳?
软件脱壳,顾名思义,就是对软件加壳的逆操作,把软件上存在的壳去掉
加固是如何运行起来的?
- -->APP启动
- -->壳dex先加载起来
- -->壳负责把源dex文件读出来
- -->壳把源dex文件解密
- -->把解密后的dex加载进内存 源dex运行起来
原理:
源dex文件最终会加载进内存
Hook加载Dex的函数,把Dex从内存中dump出来
Hook DexFile::OpenMemory()
DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,//nullptr
const OatDexFile* oat_dex_file,
std::string* error_msg)
脱壳方案
方案一:Xposed + Fdex2
方案二:Frida 脱壳
1、启动手机内的 frida-server 并进行端口转发。27043 27042
2、导出 Android 的 /system/lib/libart.so 到本地。然后使用 IDA 查看 OpenMemory 对应的签名函数名。我这里对应的是 _ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_
import frida
import sys
package = 'com.iCitySuzhou.suzhou001'
def on_message(message, data):
if message['type'] == 'send':
print(f"[*] {message['payload']}")
else:
print(message)
# OpenMenory 在 libart.so 中,art 虚拟机(安卓5),davlink 虚拟机(安卓4)
# Hook OpenMemory 的导出方法
# 用 IDA 打开 libart.so ,查看 OpenMemory 的到处方法名
# OpenMemory 的第一个参数是 dex 文件在内存中的其实位置
# 根据 dex 文件格式,从其实位置开始第32个字节是该 dex 文件的大小
# 知道 dex 起始位置和整个文件大小,只需要把这段内存 dump 出来即可
# 使用于 安卓6 7 8 9
src = """
var openMemory_address = Module.findExportByName("libart.so", "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_")
Interceptor.attach(openMemory_address, {
onEnter: function(args){
// dex 文件的起始位置
var dex_begin_address = args[1]
//dex 文件的前 8 个字节是 magic 字段
// 打印 magic (会显示“dex 035”)三个字符,可以验证是否为 dex 文件
console.log("magic:" + Memory.readUtf8String(dex_begin_address))
// 把地址转换成整形,再加32
//因为 dex 文件的第32个字节处存放的是 dex 文件的大小
var address = parseInt(dex_begin_address, 16) + 0x20
// 把 address 地址指向的内存值读出来,该值就是 dex 的文件大小
// ptr(address)转换的原因是 frida 只接受 NativePointer 类型指针
var dex_size = Memory.readInt(ptr(address))
console.log("dex_size:" + dex_size)
//frida 写入文件,把内存中的数据写到本地
var timestamp = new Date().getTime();
var file = new File("/data/data/%s/" + timestamp + ".dex", "wb")
// Memory.readByteArray(begin, length)
// 把内存里的数据读出来,从 begin 开始读,取 length 长度
file.write(Memory.readByteArray(dex_begin_address, dex_size))
file.flush()
file.close()
send("dex begin address: "+parseInt(dex_begin_address,16))
send("dex file size:" +dex_size)
},
onLeave: function (retval) {
if (retval.toInt32() > 0){
}
}
});
"""%(package)
print(f"dex 导出目录为: /data/data/{package}")
device = frida.get_remote_device()
pid = device.spawn(package)
session = device.attach(pid)
script =session.create_script(src)
script.on("message", on_message)
script.load()
device.resume(pid)
sys.stdin.read()
输出:
D:\Python\python.exe E:/wx_h5/frida_dev.py
dex 导出目录为: /data/data/com.iCitySuzhou.suzhou001
magic:dex
035
dex_size:5993048
[*] dex begin address: 3760596376
[*] dex file size:5993048
magic:dex
035
dex_size:292
[*] dex begin address: 4111687680
[*] dex file size:292
magic:dex
035
dex_size:292
[*] dex begin address: 4098458380
[*] dex file size:292
magic:dex
035
dex_size:5542488
[*] dex begin address: 3761046936
[*] dex file size:5542488
magic:dex
035
dex_size:143416
[*] dex begin address: 3729728852
[*] dex file size:143416
magic:dex
035
dex_size:5432740
[*] dex begin address: 3697659272
[*] dex file size:5432740
magic:dex
035
dex_size:74656
[*] dex begin address: 3723183372
[*] dex file size:74656
magic:dex
035
dex_size:358828
[*] dex begin address: 3536693200
[*] dex file size:358828
magic:dex
035
dex_size:358828
[*] dex begin address: 3536693200
[*] dex file size:358828
到手机上看,脱壳成功
然后使用 adb 命令 pull 到本地,用 jadx 打开
需要将手机里的目录加上权限
chmod -R 777 目标文件夹