DEX文件混淆加密
0x00 前言
混淆加密主要是为了隐藏 dex 文件中关键的代码,力度从轻到重包括:静态变量的隐藏、函数的重复定义、函数的隐藏、以及整个类的隐藏。混淆后的 dex 文件依旧可以通过 dex2jar jade 等工具的反编译成 Java 源码,但是里面关键的代码已经看不到了。
java 效果图:
源码地址和使用说明在 github 上 hidex-hack
0x01 dex格式分析
dex 文件格式在上一篇有进行了比较详细的介绍,具体可看dex文件格式分析,这里简单的介绍一下整个 dex 文件的布局。
1.header(dex头部)
header 概述了整个 dex 文件的分布情况,包括了:magic, checksum, signature, file_size, header_size, endian_tag, link, map, string_ids, type_ids, proto_ids, field_ids, method_ids, class_defs, data。
- checksum 和 signature 是校验值,修改后需要对其进行修复
- string_ids, type_ids, proto_ids, field_ids, method_ids 作为类型数组节区(我瞎起的)保存了不同类型的值
- class_defs 存储了类的定义也是我们修改的重点
- data 是数据存储区,包括所有的数据
2.类型数组节区
类型数组节区包括了string_ids, type_ids, proto_ids, field_ids, method_ids。分别表示:字符串,类型,函数签名,属性,函数。每个节区都保存了对应类型数据数组,可以用 010Editor 分析二进制文件数据。
属性示例:
3.类定义
类定义是修改的重点,这里保存了所有类的结构,也是整个 dex 文件中结构最复杂的部分。其中包括了:静态属性变量、成员数形变量,虚函数,直接函数,静态函数等数据。
0x02 实现功能
通过分析 dex 文件格式,现在可以实现的混淆加密主要包括四种:
- 静态变量隐藏
- 函数重复定义
- 函数隐藏
- 类定义隐藏
四种混淆加密的实现方式都是通过修改 class_def 结构体中字段实现的。可以通过 json 格式了解一下 class_def 的结构(这里只列出来要用到的字段):
|
|
字段含义:
- class_idx: 类名序号,值是type_ids的一个index
- class_def: 类定义结构体
- static_values_off: 静态变量值偏移
- class_data_off: 类定义偏移
- class_data: 类定义结构体
- direct_methods_size: 直接函数个数
- virtual_methods_size: 虚函数个数
- virtual_methods: 虚函数结构体
- code_off: 函数代码偏移
通过上面的字段介绍其实很容易得到四个功能的实现方案,下面一个一个介绍。
1.静态变量隐藏
static_vaules_off 保存了每个类中静态变量的值的偏移量,指向 data 区里的一个列表,格式为 encode_array_item,如果没有此项内容,该值为0。所以要实现静态变量赋值隐藏只需要将 static_values_off 值修改为0。
实现效果:
这里的静态数组数据没有成功隐藏,因为我也不知道怎么搞。?
2.函数重复定义
class_def -> class_data -> virtual_methods -> code_ff 表示的是某个类中某个函数的代码偏移地址。这里需要提到一个概念:Java 中所有函数实现都是虚函数,这一点和 C++ 是不一样的,所有这里修改的都是 virtual_methods 中 code_off。
实现方式:读取第一个函数的代码偏移地址,将接下来的函数偏移地址都修改为第一的值。
3.函数隐藏
class_def -> class_data -> virtual_methods_size 和 class_def -> class_data -> direct_methods_size 记录了类定义中函数的个数,如果没有定义函数则该值为0。所以只要将该值改为0,函数定义就会被隐藏。
4.类定义隐藏
class_def -> class_data_off 保存了具体类定义的偏移地址,也就是 class_def -> class_data 的地址,如果该值为0则所有实现将被隐藏。隐藏后会把类定义的所有东西都隐藏包括成员变量,成员函数,静态变量,静态函数。
0x03 数据读取
上面一个章节主要介绍了功能实现的原理,接下来要介绍具体实现了。要实现修改 class_def 中字段,首先要把整个 dex 文件结构解析出来,当然可以只是我们需要的字段。在工具中我定义的 dex 结构如下,因为 class_def 结构比较复杂所以独立了一个包定义:
|
|
也许你可能会疑问,我们功能实现时候只需要修改 class_def 为什么还需要读取 string_ids 这些区段。这是因为像上面提到的 class_def -> class_idx 保存的其实是 type_ids 中的序号,而 type_ids 中保存的是 string_ids 的序号。
为了灵活配置,运行工具的时候我们只需要配置好要隐藏的类名,比如需要隐藏某个类的实现 hack_me_size:
cc.gnaixx.samp.core.EntranceImpl
, 配置文件的具体实现下个章节介绍。
DexFile.java 定义了整个 dex 文件结构, 实现比较简单只有一个 read(byte[] dexBuff) 函数读取整个 dex 文件格式。
DexFile.java:
|
|
第一步要先读取 header 因为它保存了其他节区的偏移地址和个数。
Header.java:
|
|
知道了各个节区的偏移地址和个数接下来的读取就比较简单了,比如 string_ids 节区的读取。
StringIds.java:
|
|
其他节区的读取和 string_ids 类似,但是 class_def 节区结构比较复杂,读取起来可能比较麻烦。但是其实我们要用的值并不是很多,只需要关注那几个字段就好了。
ClassDefs.java:
|
|
这里需要介绍一下 dex 特有的一种数据类型 LEB128 官方介绍如下:
LEB128 (“Little-Endian Base 128”) is a variable-length encoding for arbitrary signed or unsigned integer quantities. The format was borrowed from the DWARF3 specification. In a .dex file, LEB128 is only ever used to encode 32-bit quantities.
Each LEB128 encoded value consists of one to five bytes, which together represent a single 32-bit value. Each byte has its most significant bit set except for the final byte in the sequence, which has its most significant bit clear. The remaining seven bits of each byte are payload, with the least significant seven bits of the quantity in the first byte, the next seven in the second byte and so on. In the case of a signed LEB128 (sleb128), the most significant payload bit of the final byte in the sequence is sign-extended to produce the final value. In the unsigned case (uleb128), any bits not explicitly represented are interpreted as 0.
也就是说 LEB128 是基于 1 个 Byte 的一种不定长度的编码方式 。若第一个 Byte 的最高位为 1 ,则表示还需要下一个 Byte 来描述 ,直至最后一个 Byte 的最高 位为 0 。每个 Byte 的其余 Bit 用来表示数据。
代码中用 ULeb128.java(unsigned 无符号) 表示是该结构,通过分析Android源码 Leb128.h可以知道 LEB128 虽然表示的是不定长格式,但是在 Android 中只用到了4 个byte,所以只需要用int表示就可以了。
ULeb128.java:
|
|
Bytes to ULEB128:
|
|
Integer to ULEB128:
|
|
0x04 HackPoint格式
HackPoint 表示修改后的数据结构,代码中把所有要修改的的字段都用 HackPoint 类型表示。HackPoint 类型有三个字段 type、offset、value,都是 int 类型分别表示:类型、偏移地址、原始值。类型主要有三种 uint(unsigned int)、ushort(unsigned
short 2byte)、uleb128。这三种数据用 int 存储都足够了。
HackPoint.java:
|
|
在修改完后会把所有的 HackPoint 数据写在 dex 文件的末尾。本来 dex 文件末尾是 map_list 区段,数据格式是 :
|
|
刚好是 12 byte, 所以 HackPoint 写入 dex 文件的格式为:
0x05 配置文件
配置文件的定义比较简单看一下示例就知道了:
|
|
当多个类需要实现同一个功能的时候只需要用空格分隔就可以了
配置文件读取代码:
|
|
0x06 dex混淆隐藏
dex 文件混淆隐藏主要包括三个步骤:
- 修改 HackPoint 并保存到 dex 文件末尾
- 修复Header
1.修改 HackPoint
通过取得的配置文件中的配置类遍历 class_def_item:
|
|
隐藏静态变量值:
|
|
函数重复定义:
|
|
函数隐藏:
|
|
隐藏类:
|
|
添加 HackPoint 数据到 dex 文件:
|
|
dex 文件都是以小端数据保存
2.修复Header
Header 中修复的数据有三个:
- 文件长度
- checksum
- signature
修改代码:
|
|
该部分代码地址: HidexHandle.java
0x07 dex还原
相对于加密解密过程简单了很多,只要根据 HackPoint 数据一一修复就好了。这里简单的说下修复步骤:
- 读取 Header 中 map_list 的偏移地址和个数,因为 HackPoint 数据保存在 map_list 之后
- 读取 HackPoint 数据并修复 dex 文件
- 修复 Header 中的 file_size、checksum、signature
java 实现
修复关键源码:
|
|
c++ 实现
工具本身就是为了实现安全加固,那么用 java 实现意义就小了很多,所以工具包里面的实现我是用 NDK 开发的。
修复关键源码:
|
|
完整源码地址: hidex.cpp
0x09 总结
整体功能还是比较简单,实现的代码也不是很复杂,但是这些都需要基于对 dex 文件格式的了解的前提下。
另外该工具存在一个缺点,dex 的加载问题。Android中加载 dex 的 DexClassLoad 只支持文件路径加载,不像 java 中的 ClassLoad 可以支持二进制流加载,所以在加载 dex 是就存在加密后的 dex 缓存,这是非常危险的。所以下个研究的点也就是自定义 DexClassLoad 实现不落地加载。(很多安全加固厂商老早就实现了?)。
虽然功能不算强大,也有不少缺点,不过也花了自己不少时间研究,对 dex 文件格式也有点了解,也算值得了。