Android 逆向基础知识

时间:2024-03-21 08:41:15

一. 初识Apk、Dalvik字节码以及Smali

Dalvik(Android操作系统的虚拟机)

Dalvik VM是基于寄存器的,而JVM是基于栈的。 Dalvik有专属的文件执行格式dex(dalvik executable),而JVM执行的则是Java字节码,Dalvik VM比JVM速度更快,占用空间更少。
通过Dalvik的字节码我们不能直接看到原来的逻辑代码,这时需要借助如ApkTool或dex2jar+jd-gui工具来查看。但是,最终我们修改Apk需要操作的文件是 .smali文件,而不是导出来的Java文件重新编译。

Smali–**的重中之重

Smali、Baksmali分别是指安卓系统里的Java虚拟机(Dalvik)所使用的一种 .dex 格式文件的汇编器、反汇编器。其语法是一种宽松式的Jasmin/dedexer语法,而且它实现了 .dex格式所有功能(注解、调试信息、线路信息等)。
当我们对Apk文件反编译后,便会生成此类的文件。在Dalvik字节码中,寄存器都是32位的,能够支持任何类型,64位类型(Long/Double)用两个寄存器表示;Dalvik字节码有两种类型:原始类型、引用类型(包括对象和数组)。

二. 初识Apk、Dalvik字节码以及Smali

1. 原始类型:

B- - -byte

C- - -char

D- - - double

F- - - float

I- - -int

J- - -long

S- - - short

V- - -void

Z- - -boolean

XXX- - -array

Lxxx/yyy- - -object

最后两项:数组的表示方式是:在基本类型前加上前中括号“[”,例如int数组和float数组分别表示为:[I、[F;对象的表示则以L作为开头,格式是LpackageName/objectName;(注意必须有个分号跟在最后),例如String对象在smali中为:Ljava/lang/String;,其中java/lang对应java.lang包,String就说定义在该包中的一个对象。

类里面的内部类又如何在smali中引用呢?答案是LpackageName/objectNamesubObjectName;subObjectName;。也就是在内部类前加“”符号。

方法的定义一般为:

Func-Name(Para-Type1Para-Typr2Para-Type3…)Return-Type

注意参数与参数之间没有任何分隔符,例如:

1). hello ()V
     没错,这就是void hello()。

2). hello (III)Z
     这个则是boolean hello(int,int,int)。

3). hello (Z[I[ILjava/lang/String;J)Ljava/lang/String;
    上面表示:String hello(boolean,int[],int[],String,long)

2. Smali基本语法

 .field private isFlag:z             定义变量
 .method               方法
 .parameter           方法参数
 .prologue             方法开始
 .line 123               此方法位于第123行
 invoke-super       调用父函数
 const/high16 v0,0x7f03.      把0x7f03赋值给v0
 invoke-direct         调用函数
 return-void            函数返回void
 .end method         函数结束
 new-instance        创建实例
 iput-object            对象赋值
 iget-object            调用对象
 invoke-static         调用静态函数

条件跳转分支:

"if-eq vA,vB,:cond_" 如果vA不等于vB则跳转到:cond_

"if-ne vA,vB,:cond_" 如果vA不等于vB则跳转到:cond_

"if-lt vA,vB,:cond_" 如果vA小于vB则跳转到:cond_

"if-gt vA,vB,:cond_" 如果vA大于vB则跳转到:cond_

"if-le vA,vB,:cond_" 如果vA小于等于vB则跳转到:cond_

"if-eqz vA,:cond_" 如果vA等于0则跳转到:cond_

"if-nez vA,:cond_" 如果vA不等于0则跳转到:cond_

"if-ltz vA,:cond_" 如果vA小于0则跳转到:cond_

"if-gez vA,:cond_" 如果vA大于等于0则跳转到:cond_

"if-gtz vA,:cond_" 如果vA大于0则跳转到:cond_

"if-lez vA,:cond_" 如果vA小于等于0则跳转到:cond_

三. 深入smali文件

Smali中的包信息:

  .class public Lcom/aaaaa;
  .super Lcom/bbbbb;
  .source "ccccc.java"

  这是一个由ccccc.java编译得到的smali文件(第3行)
  它是com.aaaaa这个package下的一个类(第1行)
  继承自com.bbbbb这个类(第2行)

smali中的声明

一般来说在Smali文件中是这个样子的:

Android 逆向基础知识

   这个声明是内部类的声明:aaa这个类它有两个成员内部类——qqq和www。

寄存器知识补充:

在smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、v1、v2、… 参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2、… 特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this”,p1表示函数的第一个参数,p2代表函数中的第二个参数 … 而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)。

寄存器简单实例分析:

    const/4 v0,0x1
    iput-boolean v0,p0,Lcom/aaa;->IsRegistered:Z

  上面两句smali代码,首先它使用了v0本地寄存器,并把值0x1存到v0中,然后第二句用iput-boolean这个指令把v0中的值存放到com.aaa.IsRegistered这个成员变量中。

  即相当于:this.IsRegistered=true;(上面说过,在非static函数中p0代表的是“this”,在这里就是com.aaa实例)。

smali中的成员变量:

 成员变量的格式是:

   .field public/private [static] [final] varName:<类型>

对于不同的成员变量也有不同的指令:

  一般来说,获取的指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object等。

  操作的指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object等。

  没有“-object”后缀的表示操作的成员变量对象是基本数据类型,带“-object”表示操作的成员变量是对象类型,特别地,boolean类型则使用带“-boolean”的指令操作。

Smali成员变量指令简析(一)

 sget-object v0, Lcom/aaa;->ID:Ljava/lang/String;

 sget-object 就是用来获取变量值并保存到紧接着的参数的寄存器中,在本例中,它获取ID这个String类型的成员变量并放到v0这个寄存器中。
 注意:前面需要该变量所属的类的类型,后面需要加一个冒号和该成员变量的类型,中间是“->”表示所属关系。

Smali成员变量指令简析(二)

 iget-object v0, p0, Lcom/aaa;->view:Lcom/aaa/view;

 可以看到iget-object指令比sget-object多了一个参数,就是该变量所在类的实例,在这里就是p0即“this”。
 获取array的话我们用aget和aget-object,指令使用和上述一致。

Smali成员变量指令简析(三)

 put指令的使用和get指令是统一的,如下:

   const/4 v3,0x0

   sput-object v3, Lcom/aaa;->timer:Lcom/aaa/timer;

  相当于:this.timer=null;
  注意:这里因为是赋值object,所以是null

Smali成员变量指令简析(四)

 .local v0, args:Landroid/os/Message;

 const/4 v1,0x12
 
 input v1,v0, Landroid/os/Message;->what:I

 相当于:args.what = 18;(args是Message的实例)

四. Smali函数分析

Smali中的函数调用

smali中的函数和成员变量一样也分为两种类型,分别为direct和virtual。

direct method和virtual method的区别:

简单来说,direct method就是private函数,其余的public和protected函数都属于virtual method。所以在调用函数时,有invoke-direct、invoke-virtual,另外还有invoke-static、invoke-super以及invoke-interface等几种不同的指令。

当然其实还有invoke-XXX/range指令的,这是参数多于4个的时候调用的指令,比较少见,了解下即可。

1)invoke-static:用于调用static函数的

例如:

invoke-static {}, Lcom/aaa;->CheckSignature()Z

这里注意到invoke-static后面有一对大括号“{}”,其实是调用该方法的实例+参数列表,由于这个方法既不需要参数也是static的,所以{}内为空,再看一个:

const-string v0, “NDKLIB”

invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

这个是调用static void System.loadLibrary(String)来加载NDK编译的so库的方法,同样也是这里v0就是参数“NDKLIB”了。

2)invoke-super:调用父类方法用的指令,一般用于调用onCreate、onDestroy等方法。

3)invoke-direct:调用private函数:

  invoke-direct {p0}, Landroid/app/TabActivity;-><init>()V

这里init()就是定义在TabActivity中的一个private函数。

4)invoke-virtual:用于调用protected或public函数,同样注意修改smali时不要错用invoke-direct或invoke-static:

  sget-object v0, Lcom/dddd;->bbb:Lcom/ccc;
  invoke-virtual {v0,v1}, Lcom/ccc;->Messages(Ljava/lang/Object;)V

这里相信大家都很清楚了:

 v0是bbb:Lcom/ccc
 v1是传递给Messages方法的Ljava/lang/Object参数。

5)invoke-xxxxx/range:当方法的参数多于5个时(含5个),不能直接使用以上的指令,而是在后面加上“/range”,range表示范围,使用方法也有所不同:

invoke-direct/range {v0…v5}, Lcmb/pb/ui/PBContainerActivity;->h(ILjava/lang/CharSequence;Ljava/lang/String;Landroid/content/Intent;I)Z

需要传递v0到v5一共6个参数,这时候大括号内的参数采用省略形式,且需要连续。

Smali中函数返回的结果的操作:

在Java代码中调用函数和返回函数结果可以用一条语句完成,而在Smali里则需要分开来完成,在使用上述指令后,如果调用的函数返回非void,那么还需要用到move-result(返回基本数据类型)和move-result-object(返回对象)指令。

const-string v0, “Eric”
   
invoke-static {v0}, Lcmb/pbi;->t(Ljava/lang/String;)Ljava/lang/String;

move-result-object v2

v2保存的就是调用t方法返回的String字符串。

Smali中函数实体分析—if函数分析:

.method private ifRegistered()Z

.locals 2         //在这个函数中本地寄存器的个数

.prologue          //指定程序的开始处,混淆过后的代码可能会没有这一说明

const/4 v0, 0x1.       //v0赋值为1

.local v0, tempFlag:Z

if-eqz v0, :cond_0             //判断v0是否等于0,等于0则跳到cond_0执行

const/4 v1, 0x1              //符合条件分支

:goto_0                         //标签

return v1                 //返回v1的值

:cond_0                 //标签

const/4 v1, 0x0         //cond_0分支

goto :goto_0            //跳到goto_0执行 即返回v1的值 这里可以改成return v1,也是一样的

.end method

Smali中函数实体分析—for函数分析:

const/4 v0, 0x0              //v0=0

.local v0, i:I

:goto_0

if-lt v0, v3, :cond_0      //v0小于v3 则跳转到cond_0并执行分支:cond_0

return-void

:cond_0                 //标签

iget-object v1, p0, Lcom/aaa/MainActivity;->listStrings:Ljava/util/List;      //引用对象

const-string v2, “Eric”

invoke-interface {v1, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z     //List是接口,执行接口方法add

add-int/lit8 v0, v0, 0x1          //将第二个v0寄存器中的值,加上0x1的值放入第一个寄存器中,实现自增长

goto :goto_0                 //回去:goto_0标签

六. 了解JAVA反编译工具

总的来说Java程序和Android程序的区别在于Android程序是基于组件、基于配置的。

常量池是由常量项(cp_info)组成的。常量池类似于结构体,主要有两个元素。一个是tag,还有一个是info[]数组。tag用来判定info[]数组的类型。tag对应的类型图如下:

Android 逆向基础知识

class_index的作用就是指向CONSTANT_class_index

name_and_type_index的作用就是指向CONSTANT_NameAndType_info。

sget-object 就是用来获取变量值并保存到紧接着的参数的寄存器中。

invoke-direct:没有被覆盖方法的调用,即不用动态根据实例所引用的调用,编译时,静态确认的,一般是private或方法。

invoke-virtual:虚方法调用,调用的方法运行时确认实际调用,和实例引用的实际对象有关,动态确认的,一般是带有修饰符protected或public的方法。

Move-result-object v2 就是将上一条指令的结果存放在v2寄存器中。

ARM汇编

ARM具有31个通用寄存器,6个状态寄存器

ARM处理器支持以下运行模式

1. 用户模式:ARM处理器正常的程序执行状态。
2. 快速中断模式:用于高速数据传输或通道处理。
3. 外部中断模式:用于通用的中断处理。
4. 管理模式:操作系统使用的保护模式。
5. 数据访问终止模式:当数据或指令预取终止时进入该模式,可用于模拟存储及存储保护。
6. 系统模式:运行具有特权的操作系统任务。
7. 未定义指令中止模式:当未定义的指令执行时进入该模式。

ARM汇编语言程序结构

#####(1)处理器架构定义

.arch armv5te  @处理器架构
.fpu softvfp   @协处理器类型
.ebi_attribute 20,1    @接口属性
.ebi_attribute 21,1
.ebi_attribute 23,1
.ebi_attribute 24,1
.ebi_attribute 25,1
.ebi_attribute 26,1
.ebi_attribute 30,1
.ebi_attribute 18,1

.arch指定了ARM处理器架构。

.armv5te表示本程序在armv5te架构处理器上运行。

.fpu指定了协处理器的类型。

softvfp表示使用浮点运算库来模拟协处理器运算。

.ebi_attribute 指定了一些接口属性。

(2)段定义
.section    定义只读数据,属性是默认
.text       定义了代码段
(3)注释与标号

注释方法:

/*...*/    多行注释
@          单行注释

标号:<标号名>:

例如:

loop:
...
end loop
(4)汇编器指令

程序中所有以"."开头的指令都是汇编指令,它们不属于ARM指令集。

部分汇编器指令:

.file: 指定了源文件名
.align: 代码对齐方式
.ascii: 声明字符串
.global: 声明全局变量
.type: 指定符号的类型
(5)字符串与参数传递

声明函数的方法:

.global 函数名
.type 函数名,%function
函数名:
    <...函数体...>
声明一个实现两个数相加的函数的代码:
.global MyAdd
.type MyAdd,%function
MyAdd:
    add r0,r0,r1
    mov pc,lr

ARM汇编规定:R0-R3这4个寄存器用来传递函数调用的第1到第4个参数,超过的参数通过堆栈来传递。

(6)ARM处理器寻址方式
  1. 立即寻址

    mov R0,#1234  @# 作为前缀,表示十六进制时以“0x”开头
    
  2. 寄存器寻址

    mov R0,R1
    
  3. 寄存器位移寻址

    五种位移操作:

    (1)LSL:逻辑左移,移位后寄存器空出的低位补0

    (2)LSR:逻辑右移,移位后寄存器空出的高位补0

    (3)ASR:算数右移,移动过程中符号位不变。如果操作数是整数,则移位后空出的高位补0,否则补1

    (4)ROR:循环右移,移位后移出的低位填入移位空出的高位

    (5)RRX:带扩展的循环右移,操作数右移移位,移位空出的高位用C标志的值填充。

    例如:

    mov R0,R1,LSL #2
    
  4. 寄存器间接寻址

    LDR R0,[R1]
    
  5. 基址寻址

    LDR R0,[R1,#-4]
    
  6. 多寄存器寻址

    LDMIN R0,{R1,R2,R3,R4}
    LDM是数据加载指令
    指令的后缀IA表示每次执行完成加载操作后R0寄存器的值自增1
    ARM中,字表示的是一个32位
    R1=[R0]
    R2=[R0+#4]
    R3=[R0+#8]
    R4=[R0+#12]
    

    注:+#4是因为32位占4个字节

  7. 堆栈寻址

    STMFD SP!,{R1-R7,LR}  入栈,多用于保存子程序“现场”
    LDMFD SP!,{R1-R7,LR}  出栈,多用于回到子程序现场
    
  8. 块拷贝寻址

    块拷贝可实现连续地址数据从存储器的某一位置拷贝到另一位置。

    LDMIN R0! , {R1-R3}  @从寄存器指向的存储单元中读取3个字到R1-R3寄存器。
    
  9. 相对寻址

    程序计数器PC的当前值为基地址,指令中的地址标号作为偏移量,将两者相加之后得到操作数的有效地址。

ARM和Thumb指令

Thumb是16位的ARM汇编。

如同样的beq,bne这两个汇编指令,用ARM的4个HEX数表示时,其HEX值为0A,1A,而当用2个HEX数表示时,其HEX值为D0,D1。

在动态调试的时候,IDA无法分辨ARM和Thumb指令。所以需要人工去进行纠正和调整。

ARM寄存器

R0-R7:    通用寄存器
R8-R10:   不常用的通用寄存器
R11:      基址寄存器(FP)
R12:      暂时寄存器(IP)
R13:      堆栈指针(SP)
R14:      链接寄存器(LR)
CPSR:     状态寄存器

ARM指令集

B      无条件跳转
BL     带链接的无条件跳转
BLX    带状态的无条件跳转
BNE    不相等跳转
BEQ    相等跳转

寄存器交互指令

LDR                    #从存储器中加载数据到寄存器
LDR R1, [R2]           #把R2指向的地址的数据给R1

STR                    #把寄存器的数据存储到存储器
STR R1, [R2]           #在R2指向的地址存储R1

LDM                    #将存储器的数据加载到一个寄存器列表
LDM R0, {R1,R2,R3}     #把R0中的数据一次加载到R1,R2,R3

SDM                    #将一个寄存器列表的数据存储到指定的存储器
SDM R0, {R1,R2,R3}     #把R1,R2,R3加载到R0单元

PUSH                   #入栈
POP                    #出栈

数据传送指令

MOV       #将立即数或寄存器的数据传送到目标寄存器