Dalvik笔记

71 阅读8分钟

Dalvik虚拟机与Java虚拟机的区别:

  • Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码
  • Java虚拟机基于栈架构,Dalvik虚拟机基于寄存器架构
  • Dalvik可执行文件体积更小

Smali语言是对Dalvik虚拟机字节码的一种解释,但它并非一种官方标准语言。它是由一个名叫JesusFreke对Dalvik字节码的翻译,因为Dalvik虚拟机名字来源于冰岛一个小渔村的名字,JesusFreke便把smali和baksmali取自了冰岛语中的“汇编器”和“反编器”。assmali和baksmali则是针对DEX执行文件格式的汇编器和反汇编器,相关的资源位于地址为github.com/JesusFreke/…, 使用方法如下:

  • 代码编译:javac -source 1.6 -target 1.6 <javafile>
  • 代码反编:javap -c classpath ./ <classfile>
  • dex编译:dx --dex --output=<dexname> <classfile>
  • dex查看:dexdump -d <dex1> [dex2...]
  • dex反编:java -jar baksmali.jar d <dex> -o <dir>
  • dex重编:java -jar asssmali.jar a <dir> -o <dex>

注意:建议安装java2samli、samlidea插件帮助进行学习和逆向工作。调试步骤为反编文件、开启DDMS、设置remote的attach的task、开始debug

Samli语法

在 Dalvik 虚拟机字节码中寄存器的命名法中主要有2种:v命名法和p命名法。v命名法表示函数中用到的局部变量与参数,命名从v0开始而依次递增。p命名法表示函数的入参,命名从p0开始命名而依次递增。在调用非静态方法时需要传入该方法所在对象的引用,因此此时p0表示的是传入的隐式对象引用,从p1开始才是实际传入的入参。在调用静态方法时,由于静态方法不需要构建对象的引用,因此从p0开始就是实际传入的入参。

每个Dalvik寄存器都是32位大小,对于小于或者等于32位长度的类型来说,一个寄存器就可以存放该类型的值,而像J、D 等64位的类型,它们的值是使用相邻两个寄存器来存储的。每条指令使用的寄存器索引范围都有限制,除非指令通过标识len标明,都则一般均指为地址为256(一个字节大小)内的寄存器。

下面是针对smali语法的一个汇总,便于快速记忆和上手。更详细的语法可上网搜索smali语法中文参考文档进行查看。

2.1、助记

type:表示保存数据的类型,间接表示目的寄存器的使用个数

  • none:表示32位内的基本对象,间接表示使用一个寄存器存储数值,在get\put时需要细化类型boolean\char等
  • wide:表示64位内的基本对象(long、double类型),间接表示使用两个寄存器进行存储
  • object:表示32位内的引用对象地址,间接表示使用一个寄存器存储地址,在const时需要细化string\class等

leng:表示来源数据的地址长度,或来源寄存器的寻址范围

  • none:表示使用256地址内的寄存器
  • destNum:表示目的寄存器的地址位于2^Num内,dest为了理解而写(dest四个字母省略)
  • fromNum:表示来源寄存器的地址位于2^Num内,或者数据长度为num位

opt:表示对应指令特有的选项。如if中的gt\ge\eq\ne\le\lt\eqz等

2.2、指令

定义:

  • const[-type]/[leng] vx,val:存入数值到寄存器vx,其中tag要细分

  • new-instance vx,类型:根据类型新建一个对象实例,并将新建的对象的引用存入寄存器vx

  • new-array vx,vy,类型:根据类型新建一个数组,vy存入数组的长度,vx存入数组的引用

    filled-new-array-range {vx..vy},类型:根据类型新建一个寄存器长度的数组,寄存器存储对应位置元素

赋值:

  • move[-type]/[leng] vx,vy:将寄存器vy的内容赋值给寄存器vx。注意的是形式还有move-result[tag] vx
  • sput[-type] vx,字段ID:将vx的值作为tag型写入到静态字段中
  • sget[-type] vx,字段ID:将静态字段的值作为tag型存储到vx
  • iput[-type] vx,vy,字段ID:将vx的值作为tag型写入到实例的字段中,vy中是该实例的引用
  • iget[-type] vx,vy,字段ID:将实例中字段的值作为tag型存储到vx,vy中是该实例的引用
  • aput[-type] vx,vy,vz:将vx的值作为tag型写入数组中,vy中是该对象的引用,vz是的元素的索引
  • aget[-type] vx,vy,vz:将数组中某元素的值作为tag型存储到vx,vy中是该对象的引用,vz是的元素的索引

运算:

  • [type1-]to[-type2] vx,vy:转换vy寄存器中的tag1型值为tag2型值存入vx
  • cmp[l|g][-long|double] vx,vy,vz:比较vy和vz的值,并在vx存入int型返回值(根据[l|g]进行规则返回)
  • instance-of vx,vy,类型:检查寄存器vy中的对象引用是否是类型对应的实例,是vx存入非0值,否则vx存入0
  • neg[-type] vx,vy:对vy取负值后存到vx中
  • add[-type]/[leng] vx,vy,vz:计算vy+vz并将结果作为tag型存入vx
  • sub[-type]/[leng] vx,vy,vz:计算vy-vz并将结果作为tag型存入vx
  • mul[-type]/[leng] vx,vy,vz:计算vy*vz并将结果作为tag型存入vx
  • div[-type]/[leng] vx,vy,vz:计算vy\vz并将结果作为tag型存入vx
  • rem[-type]/[leng] vx,vy,vz:计算vy%vz并将结果作为tag型存入vx
  • and[-type]/[leng] vx,vy,vz:计算vy&vz并将结果作为tag型存入vx
  • or[-type]/[leng] vx,vy,vz:计算vy|vz并将结果作为tag型存入vx
  • xor[-type]/[leng] vx,vy,vz:计算vy⊕vz并将结果作为tag型存入vx
  • shl[-type]/[leng] vx,vy,vz:计算vy<<vz并将结果作为tag型存入vx
  • shr[-type]/[leng] vx,vy,vz:计算vy>>vz并将结果作为tag型存入vx

控制:

  • goto/[leng] 目标:通过偏移量无条件跳转到目标
  • if[opt] vx,vy,目标:如果vx和vy符合opt定义,则跳转到目标。vx和vy是int型值
  • packed-switch vx,索引表目标:vx存储的是要查找具体case的值。查找时通过vx减去表case初始值得到target,进而确定执行代码地址。索引表case常量是连续的(一般用于连续整数)
  • sparse-switch vx,查询表目标:vx存储的是要查找具体case的值。查找时通过vx比较表case的数值得到target,进而确定执行代码地址。查询表case常量是不连续(一般用于枚举定义)
// Dalvik从指令起始字节起计算偏移量,计算偏移是以两个字节为单位
// 偏移量可以是正或负,负偏移量用二进制补码格式存储
// 参考:https://www.ituring.com.cn/book/miniarticle/26960
// 索引表是有数据结构定义的

struct  packed-switch-payload {
    ushort ident; // 表类型,值固定为0x0100,代表packed-switch
    ushort size;  // case的个数
    int init_key; // 初始case的值,相减后得到具体case的target
    int[] targets;// 每个case代码地址相对switch指令处的偏移
};
// 查询表是有数据结构定义的
struct sparse-switch-payload {
    ushort ident; // 表类型,值固定为0x0200,代表sparse-switch
    ushort size;  // case的个数
    int[] keys;   // 每个case的值,比较后得到具体case的target
    int[] targets;// 每个case代码地址相对switch指令处的偏移
};

方法:

  • invoke-[opt]/[range] {参数},方法名:调用方法。opt有interface\super\direct\virtual\static等
  • execute-inline {参数},内联ID:根据内联ID执行内联方法
  • return[-type] vx:返回vx寄存器的值

Smali结构

无论普通类、抽象类、接口类还是内部类,反编译的时候会为每个类单独生成一个Smali文件,但是内部类相存在相对比较特殊的地方。

  • 内部类的文件是[外部类]$[内部类].smali来命名的,匿名内部类文件以[外部类]$[数字].smali来命名
  • 编译器会把外部类的引用作为第一个参数插入到会内部类的构造器参数列表
  • 编译器会在外部类中对私有成员和私有方法生成对应访问的静态方法
  • 内部类访问外部类的私有变量和私有方法时,都要通过编译器对外部类生成的“合成方法”来间接访问
// 基本信息
.class <访问权限> [修饰关键字] <类名> // 指定了当前类的类名
.super <父类名>                    // 指定了当前类的父类
.source <源文件名>                 // 指定了当前类的源文件名
.implements <接口名>               // 接口信息(如果有的话)
.annotation [注解属性] <注解类名>    // 注解信息(如果有的话)
  [注解字段=值]
.endannotation
// 字段信息(如果有的话)
#fields
.field <访问权限> [static] [修饰关键字] <字段名>:<字段类型>
.annotation [注解属性] <注解类名>;// 注解信息(如果有的话)
  [注解字段=值]
.end annotation
.end field
// 方法信息(如果有的话)
#methods
.method <访问权限> [static] [修饰关键字] <方法原型>
.annotation [注解属性] <注解类名>; // 注解信息(如果有的话)
    [注解字段=值]
  .end annotation
  <.locals>     // 指定了使用的局部变量寄存器的个数
  [.parameter]  // 指定了方法的一个参数(可有多个)
  [.prologue]   // 指定了代码的开始处(混淆过的代码可能去掉了该指令)
  [.line]       // 指定了该处指令在源代码中的行号(混淆过的代码可能去掉了该指令)
  <代码体>       // 
.end method