Dex文件结构
了解dex文件结构可以更好的理解app运行的过程,有一个最大的作用就是加固,加固必须对文件结构还有运行机制比较熟悉才可以写。因为dex文件比较难读,一般都是通过直接修改dex文件来做加固的
他是一个class文件的集合,会把class文件里面的东西分类存放在一块连续的内存区域。比如String还有方法等等这些,都不是根据.class文件来区分的,而是全部放在一起,这样做的好处就是可以过滤掉很多重复的数据,坏处就是比较难查找,
基于dex文件内存地址连续的这个特性,Dalvik虚拟机去解析的时候是采用了Smali的语法,你会看到各种寄存器v0 p0的偏移来代表内存地址。
结构这东西纯靠记忆,没什么可说的,一个文件的规则,都是固定的,还算比较好记
例子
手动对dex添加一些脏代码,算是一种简单的加固。如果复杂一点对dex进行加密,还需要配合安卓的安装机制进行hook,在安装成功的时候让程序对dex先进行解密
import dexlib2
from dexlib2.iface import DexFile
# 打开dex文件
with open("example.dex", "rb") as f:
dex = DexFile.from_bytes(f.read())
# 遍历每一个class
for clazz in dex.get_classes():
# 遍历每一个方法
for method in clazz.get_methods():
# 在每个方法的代码末尾加入一条无用的语句
code = method.get_code()
last_instr = code.get_instructions()[-1]
new_instr = dexlib2.iface.instruction.Instruction10x(
opcode=last_instr.opcode,
start_addr=last_instr.start_addr + last_instr.get_length(),
payload=0x00
)
code.get_instructions().append(new_instr)
# 将修改后的dex文件写回到磁盘
with open("example_modified.dex", "wb") as f:
f.write(dex.to_bytes())
这个例子在smali文件中插入一条无用指令,可能就会导致有些反编译工具无法识别。所以熟悉smali语法就很重要
Smali语法
其实说到这里就比较好理解了,smali就是把java代码变成一条条的指令,下面就是smali提供的一些操作指令,结合例子看两个就明白了。
以下是常见的Smail语言操作指令和操作数的总结:
- move: 移动一个值到另一个寄存器中
- const: 将常量加载到寄存器中
- invoke-XXX: 调用一个方法,其中XXX代表调用的方法类型,例如
invoke-virtual
、invoke-direct
、invoke-static
、invoke-super
、invoke-interface
等 - return-XXX: 返回一个值,其中XXX代表返回值的类型,例如
return-void
、return-object
等 - if-XXX: 条件跳转指令,其中XXX代表跳转条件,例如
if-eq
、if-ne
等 - check-cast: 检查对象类型并转换为指定类型
- sget-XXX: 获取一个静态字段,其中XXX代表字段类型,例如
sget-object
、sget-int
等 - sput-XXX: 设置一个静态字段,其中XXX代表字段类型,例如
sput-object
、sput-int
等 - iget-XXX: 获取一个实例字段,其中XXX代表字段类型,例如
iget-object
、iget-int
等 - iput-XXX: 设置一个实例字段,其中XXX代表字段类型,例如
iput-object
、iput-int
等 - new-instance: 创建一个新的对象
- new-array: 创建一个新的数组
- fill-array: 填充数组
- array-length: 获取数组长度
- aget-XXX: 获取数组元素,其中XXX代表元素类型,例如aget-object、aget-int等
- aput-XXX: 设置数组元素,其中XXX代表元素类型,例如aput-object、aput-int等
- add, sub, mul, div, rem: 数学运算指令,分别代表加、减、乘、除、取余
- and, or, xor, neg, shl, shr, ushr: 位运算指令,分别代表按位与、按位或、按位异或、按位取反、左移、右移、无符号右移
- add/sub:加/减法操作
- cmp:比较操作
- jump:跳转指令,根据条件跳转到不同的位置
- call:调用函数
- ret:返回指令
操作数:
- 寄存器:只需要记住v0和p0这些Dalvik虚拟机中的寄存器就够了,例如rax、rbx、rcx等这些是x86上的寄存器
- 内存地址:在Dalvik虚拟机中是通过寄存器的偏移量来找到内存地址的,一般x86是一个绝对地址会有例如[rbp-4]、[rsp+8]等这些,
- 立即数:例如123、0x1234等。就是直接定义一个值,比如
const/4 v0, 1 # v0 = 1
举例说明
invoke-virtual和invoke-direct
虚方法和非虚方法,是用于区别是多态的时候要调子类的方法还是父类的方法
invoke-virtual {参数列表}, 类名;->方法名(参数类型列表)返回值类型
invoke-direct {参数列表}, 类名;->方法名(参数类型列表)返回值类型
class Shape {
public void draw() {
System.out.println("Drawing a shape.");
}
}
class Circle extends Shape {
public void draw() {
System.out.println("Drawing a circle.");
}
}
Shape s = new Circle();
s.draw();
在这个例子中,s的类型是Shape,但实际上指向的是Circle对象。如果使用invoke-virtual指令来调用draw()方法,那么会根据s的实际类型来调用Circle的draw()方法;而如果使用invoke-direct指令来调用draw()方法,那么会直接调用Shape的draw()方法。
转化为smali代码
.class public LMain;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
// 要用到寄存器的数量
.registers 2
new-instance v0, LCircle;
invoke-direct {v0}, LCircle;-><init>()V
move-object v1, v0
invoke-virtual {v1}, LShape;->draw()V
return-void
.end method
v
开头的寄存器表示虚拟寄存器,p
开头的寄存器表示参数寄存器
iget-object
iget-object v0, p0, Lcom/example/MyClass;->myField:Lcom/example/MyClass$MyInnerClass;
在 Java 代码中,上述 smali 代码对应的 Java 代码如下:
MyInnerClass myInnerClass = myClass.myField;
if和for循环
int a = 10;
if (a > 0) {
a = a * 2;
} else {
a = a / 2;
}
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
// --- if
const/4 v0, 0xa #将常量10放入寄存器v0中
if-gtz v0, :cond_true #如果v0的值大于0,跳转到标签:cond_true处执行
const/4 v0, 0x2 #将常量2放入寄存器v0中
mul-int v0, v0, v1 #将v0乘以v1的值,结果存储到v0中
goto :end #跳转到标签:end处执行
:cond_true
const/4 v0, 0x2 #将常量2放入寄存器v0中
div-int v0, v0, v1 #将v0除以v1的值,结果存储到v0中
:end
// --- for循环
const/4 v0, 0x1 # 初始化i为1
const/4 v1, 0xa # 循环条件为i<=10
const/4 v2, 0x0 # 初始化sum为0
:loop_start
if-ge v0, v1, :loop_end # 判断循环条件是否成立,不成立跳转到loop_end
add-int v2, v2, v0 # 循环体
add-int/lit8 v0, v0, 0x1 # 自增i
goto :loop_start # 继续循环
:loop_end
这一篇属于初学者的见解,可能有些知识点总结的并不正确。希望大神看到能指出,莫笑话。