逆向-Dex文件结构与Smali语法

903 阅读5分钟

Dex文件结构

了解dex文件结构可以更好的理解app运行的过程,有一个最大的作用就是加固,加固必须对文件结构还有运行机制比较熟悉才可以写。因为dex文件比较难读,一般都是通过直接修改dex文件来做加固的

他是一个class文件的集合,会把class文件里面的东西分类存放在一块连续的内存区域。比如String还有方法等等这些,都不是根据.class文件来区分的,而是全部放在一起,这样做的好处就是可以过滤掉很多重复的数据,坏处就是比较难查找,

基于dex文件内存地址连续的这个特性,Dalvik虚拟机去解析的时候是采用了Smali的语法,你会看到各种寄存器v0 p0的偏移来代表内存地址。

image.png

结构这东西纯靠记忆,没什么可说的,一个文件的规则,都是固定的,还算比较好记

例子

手动对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-virtualinvoke-directinvoke-staticinvoke-superinvoke-interface
  • return-XXX: 返回一个值,其中XXX代表返回值的类型,例如return-voidreturn-object
  • if-XXX: 条件跳转指令,其中XXX代表跳转条件,例如if-eqif-ne
  • check-cast: 检查对象类型并转换为指定类型
  • sget-XXX: 获取一个静态字段,其中XXX代表字段类型,例如sget-objectsget-int
  • sput-XXX: 设置一个静态字段,其中XXX代表字段类型,例如sput-objectsput-int
  • iget-XXX: 获取一个实例字段,其中XXX代表字段类型,例如iget-objectiget-int
  • iput-XXX: 设置一个实例字段,其中XXX代表字段类型,例如iput-objectiput-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

这一篇属于初学者的见解,可能有些知识点总结的并不正确。希望大神看到能指出,莫笑话。