Android逆向

Android基础知识

系统架构

安卓(Android)是一种基于Linux内核(不包含GNU组件)的自由及开放源代码的操作系统。主要使用于移动设备,如智能手机和平板电脑,由美国Google公司和开放手机联盟领导及开发。

android框架:image-20240307112450236

从下往上分别是:Linux内核层、系统运行库层(程序库+android运行库)、应用程序框架层、应用层。

Linux内核层: Android 系统是基于 Linux 内核的,它提供了基本的系统功能及与硬件交互的驱动,像图中Display Driver(显示驱动)、Camera Driver(摄像头驱动)、WiFi Driver(WiFi驱动)等

系统运行库层: 像图中内核的上一层就是系统运行库层,程序库(绿色部分) 和 Android运行库(黄色部分) 组成。

  • Libraries(程序库): 由 C、C++ 编写,一系列程序库的集合,供 Android 系统的各个组件使用。

  • Android runtime(Core Librares + Dalvik虚拟机): 翻译过来就是 Android 运行时,Android 应用程序时采用 Java 语言编写,程序在Android运行时中执行,其运行时分为核心库 和 Dalvik虚拟机 两部分。

Application Framework(应用程序框架层): 这一层主要是提供一些组件,搭建框架,方便app 开发人用再次基础上快速开发开发应用程序。而实际上就是一些 Android API。

Applications(应用层):顶层中有所有的 Android 应用程序,如手机中的:电话、文件管理、信息等。

虚拟机

Dalvik 虚拟机

Dalvik 是 google 专门为 Android 操作系统设计的一个虚拟机,简称DVM在 Android 4.4及以前的版本, 所有的 Android 程序都是在 Dalvik 虚拟机环境下去运行的。DVM 的指令是基于寄存器的,运行的是经过转换的 .dex****文件 (.dex 是专为 Dalvik 设计的一种压缩格式,适合内存和处理器速度有限的系统),Dalvik 虚拟机每次应用运行的时候,将代码编译成机器语言执行。

ART 虚拟机

前面说到,当我们使用手机每运行一个程序,Dalvik 虚拟机都会产生一个实例,互不影响,这就可能会出现占用资源过多等问题,随着人们日益增长的需求,Dalvik 无法满足人们对软件运行效率的需要。这是就诞生了 ART 虚拟机。

Android4.4 及以上版本,应运而生的 ART 虚拟机替代了 Dalvik 虚拟机,其处理机制根本上的区别是它采用AOT(Ahead of TIme) 技术,会在应用程序安装时就转换成机器语言,不再在执行时解释,从而优化了应用运行的速度。在内存管理方面,ART 也有比较大的改进,对内存分配和回收都做了算法优化,降低了内存碎片化程度,回收时间也得以缩短。

DVM ART 的区别: 虽说 ART 替换了 Dalvik 虚拟机,并不意味着,其应用程序开发上也要发生改变,像 android 应用程序安装包(apk)中,仍然还是可执行的 .dex 文件。

Apk 编译流程

image-20240307124845628

正向开发

首先我们来写一个 app,并打包成 apk。在这个过程中,我们可以接触到很多安卓逆向中会遇到的概

念。

一个 app 可以理解为由三个基本元件构成:

  1. AndroidManifest.xml:可以想成是 app 的配置文件

  2. resources:各种资源,包括排版、代码中出现的字符串、图片等

  3. 代码:UI 界面及逻辑代码

下面是一个简单的 demo,左边是项目树,右边是 AndroidManifest.xml 的內容:

从这个配置文件,我们可以知道:

  1. 这个 app 的包名(package name)是 com.example.demo

  2. 这个 app 里有一个窗体(activity),名字为 MainActivity ,是程序入口窗体

什么是 activity 呢?你可以把 activity 理解成是一个界面,每个程序界面对应一个 activity。假设 app是个需要注册才能使用的应用,则可能有以下界面:

  • 欢迎界面

  • 注册界面

  • 登录界面

  • 主界面

而这每一个界面都是一个 activity,而每一个 activity 可能都有一个 layout。在 Android 开发中,layout 其实就是一个 xml 文档,像这样:

像 layout 就属于资源文件的一种,会被放到 res 文件夹下。

上图中有两点需要注意,一是 android:id=”@+id/textview_first” 代表这个组件对应到一个 id。才能在代码里通过 id 拿到这个组件:

1
2
3
TextView tv = (TextView) findViewById(R.id.textview_first);

tv.setText("hello");

第二个值得注意的地方是 android:text=”@string/hello_first_fragment” ,这其实就是组件会显示的文字,对应到 res/values/strings.xml :

利用这样的方法,我们可以避免直接在 layout 里写死字符串,这样就可以做成支持多国语言的 app。

拆包与重打包

上面的程序编译成 apk 后,我们可以把它的后缀改成 .zip 直接解压,得到这样的目录结构:

  • META-INF:存放签名文件

  • res:资源文件

  • AndroidManifest.xml:app 配置文件

  • classes.dex:Java 代码编译成 dex 文件

  • resources.arsc:资源文件相关的索引表

java -jar apktool.jar d -f demo.apk

修改 res/values/strings.xml 里的 hello_first_fragment 为 Hacked ,再重打包:

java -jar apktool.jar b –use-aapt2 demo -o demo2.apk

此时的 apk 还无法安装,我们需要对它进行 align 和 sign 才能在手机上安装:

1
2
3
4
5
6
7
zipalign -v -p 4 demo2.apk demo2-aligned.apk

keytool -genkey -v -keystore my-key.jks -keyalg RSA -keysize 2048 -validity
10000 -alias my-alias

apksigner sign --ks my-key.jks --ks-pass pass:123456 --out demo2-signed.apk
demo2-aligned.apk

JNI

JNI(Java Native Interface)是一种在 Java 虚拟机中调用本地(Native)方法的机制。在 Android中,由于 Android 应用是通过 Java 语言编写的,但是某些操作系统底层的功能只能通过本地 C/C++ 代码来实现,所以需要使用 JNI 来实现Java与本地代码的交互。

使用 JNI 可以让 Java 程序调用本地库中的函数,而本地函数可以使用标准 C/C++ 编写。这样可以通过JNI 将 Java 程序与本地代码组合在一起。

常用工具

  • adb

  • apksigner

  • Android killer

  • jadx

  • jeb

smali语法

Java -> smali

smali 文件是怎么得到的呢?

  1. 通过 javac 把 .java 编译成 .class

  2. 通过 dx 把 .class 转化为 .dex

  3. 通过 baksmali 把 .dex 转为 smali

1
2
3
javac Test.java
dx --dex --output=Test.dex Test.class
java -jar baksmali.jar d Test.dex

Dalvik 寄存器

Dalvik中 的寄存器都是 32 位大小,支持所有类型。对于小于或等于 32 位类型,使用一个寄存器就可

以了,对 64 位(long 和 double) 类型,需要使用两个相邻的寄存器来存储。

寄存器的命名法有两种:****V 命名法和 P 命名法:

v 命名法:局部变量寄存器 V0-Vn,参数寄存器是 Vn-V(n+m)。

p 命名法:参数寄存器 P0-Pn,变量寄存器 V0-Vn。

v 命名法

image-20240308224545382

v0-v2 表示的是局部变量寄存器,v3-v4 表示的参数寄存器。在例一中第一个方框里可以看到 v2 是局部寄存器, v3 是参数寄存器。在图中第二个方框里可以看到 v0 是局部寄存器,v4 是参数寄存器。

p 命名法

p 命名法针对参数寄存器进行了优化,参数寄存器的命名从 p0 开始,使得局部变量寄存器和参数寄存器得以很容易的进行区分。smali 语法中就是用了 p 命名法。我们来看下 add() 方法的 smali 代码:

1
2
3
4
5
6
7
8
9
10
11
.method public add(II)I
.registers 4
.param p1, "a" # I
.param p2, "b" # I
.prologue
.line 6
add-int v0, p1, p2
.line 7
.local v0, "c":I
return v0
.end method

这样就很清晰了,4 个寄存器命名如下所示:

  • v0 局部变量寄存器,存储 a+b 的值

  • p0 当前引用 this

  • p1 参数寄存器,存储 a 的值

  • p2 参数寄存器,存储 b 的值

Dalvik 描述符

在更深入的了解 Dalvik 字节码前,先来看一下 Dalvik 是如何描述字段和方法的,这也有助于我们阅读smali 代码。

类型描述符

Dalvik 字节码中只有两种类型,基本类型和引用类型。除了对象和数组以外,其他的所有 Java 类型都是基本类型。这和 JVM 的类型描述符是基本一致的。基本类型都是使用单个字母来表示。数组类型使用[ 表示。除数组以外的引用类型使用 L 加上全限定名表示。如下表所示:

image-20240308224712573

举一个引用类型的例子。例如 String 对象,其全限定名是 java/lang/String; ,在 Dalvik 中就表示为 Ljava/lang/String; 。对于数组,又可以分为基本类型数组和引用类型数组,其格式都是 [ 加上类型描述符。 int[] 就是 [I , String[] 就是 [java/lang/String; 。多维数组就是多个 [ ,例如int[][] 就是 [[I 。

字段

Dalvik 表示字段的格式:类型(包名+类名)+字段名称+字段类型

例如:

1
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

Lpackage/name/ObjectName; 是当前这个字段所在的类,其中,L 是 java类类型, package/name/是包名, ObjectName 是类名。

FieldName 是字段名称, Ljava/lang/String; 是字段类型。字段名称和字段类型之间要用 : 隔

开。

其他例子:

1
2
Landroid/content/pm/ActivityInfo;->theme:I
Lorg/cocos2dx/lua/AppActivity;->handler:Landroid/os/Handler;

方法

Dalvik 方法的表现形式:类型(包名+类名)+方法名(+参数类型)+返回值类型

例如:Lpackage/name/ObjectName;->MethodName (III) Z

MethodName 是方法名, (III) Z 是方法的签名信息,由方法参数列表(III)和返回值(Z)构成,(III)表示三个 int 型参数;Z 表示返回值类型为 boolean。

其他例子:

1
2
3
Lcom/test/Test;->add(II)I
Landroid/os/Handler;-><init>()V
Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

Dalvik 指令集

参考资料:Dalvik opcodes

有了上面的知识储备之后,就可以具体的学习 Dalvik 指令集了。

Dalvik 指令格式为:基础字节码 - 名称后缀**/**字节码后缀 目的寄存器 源寄存器

注:连接符号 - 在有的指令里是可以不存在的

例子:move-wide/from16 vAA,VBBBB

move 为基础字节码:即 opcode。

wide 为名称后缀:标识指令操作的数据宽度为 64 位。

from16 为字节码后缀:标识源为一个 16 位的寄存器引用变量。

vAA 为目的寄存器:它始终在源的前面,取值范围为 v0~v255。

vBBBB 为源寄存器:取值范围为 v0~v65535。

Dalvik 指令集中大多数指令用到了寄存器作为目的操作数或源操作数,其中 A/B/C/D/E/F/G/H 代表一个 4 位的数值,AA/BB/…/HH 代表一个 8 位的数值,AAAA/BBBB/…/HHHH 代表一个 16 位的数值。

一些常见的指令。

空指令

nop 空指令,通常用于对齐

数据操作指令

数据操作指令为 move。move 指令根据字节码大小与类型不同,后面会跟上不同的后缀,表达的意义也就不同。

image-20240308225456588

总结起来 move 指令有三种作用:

  • 进行赋值操作

  • move-result 接收返回值操作

  • 处理异常的操作

返回指令

返回指令即 return 指令,指的是函数结尾时运行的最后一条指令。共有以下四条返回指令:

image-20240308225608978

数据定义指令

数据定义指令用来定义程序中用到的常量,字符串,类等数据。它的基础字节码为 const

image-20240308225624563

类型判断指令

image-20240308225636083

数组操作指令

数组操作包括获取数组长度,新建数组,数组赋值,数组元素取值与赋值等操作。

image-20240308225648985

异常指令

Dalvik 指令集有一条指令用来抛出异常:

throw vAA 抛出 vAA 寄存器指定的异常

跳转指令

image-20240308225717003

比较指令

比较指令用于对两个寄存器的值(浮点型或长整型)进行比较。

  • 大于(1)/等于(0)/小于(-1)=>cmpg、cmp

  • 大于(-1)/等于(0)/小于(1)=>cmpl

指令如下:

  • cmp-long vAA, vBB, vCC 比较两个长整型数。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于则结果为-1。

  • cmpl-float vAA, vBB, vCC****:比较两个单精度浮点数。如果vBB寄存器大于vCC寄存器,结果为-1,相等则结果为0,小于的话结果为1。

  • cmpl-double vAA, vBB, vCC****:比较两个双精度浮点数。如果vBB寄存器大于vCC寄存器,结果为-1,相等则结果为0,小于的话结果为1。

  • cmpg-float vAA, vBB, vCC****:比较两个单精度浮点数。如果vBB寄存器大于vCC寄存器,结果为1,相等则结果为0,小于的话结果为-1。

  • cmpg-double vAA, vBB, vCC****:比较两个双精度浮点数。如果vBB寄存器大于vCC寄存器,结果为1,相等则结果为0,小于的话结果为-1。

字段操作指令

字段操作指令用来对对象实例的字段进入读写操作。字段的类型可以是 Java 中有效的数据类型,对普

通字段与静态字段操作有两中指令集。

  • 普通字段 => iget读 / iput 写

  • 静态字段 => sget读 / sput 写

普通字段

iinstanceop vA, vB,field@CCCC

对已标识的字段执行已确定的对象实例字段运算,并将结果加载或存储到值寄存器中

针对不同类型的普通字段,有如下指令:

1
2
iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、iget-short
iput、iput-wide、iput-object、iput-boolean、iput-byte、iput-char、iput-short
静态字段

sstaticop vAA,field@BBBB

对已标识的静态字段执行已确定的对象静态字段运算,并将结果加载或存储到值寄存器中

针对不同类型的静态字段,有如下指令:

1
2
sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、sget-short
sput、sput-wide、sput-object、sput-boolean、sput-byte、sput-char、sput-short

方法调用指令

方法调用指令的格式为 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB , 具体的有如下指令:

image-20240308225931362

数据转换指令

数据转换指令用于将一种类型的数值转换成另一种类型。它的格式为“opcode vA, vB”,vB寄存器存放需

要转换的数据,转换后的结果保存在vA寄存器中。

常见的数据转换指令:

  • **neg-**数据类型:求补

  • **not-**数据类型:求反

  • *数据类型***1-to-数据类型2**:将数据类型1转换为数据类型2

数据运算指令

数据运算指令包括算术运算指令与逻辑运算指令。算术运算指令主要进行数值间如加、减、乘、除、

模、移位等运算,逻辑运算主要进行数值间与、或、非、异或等运算。

常见的数据运算指令:

  • add/sub/mul/div/rem:加/减/乘/除/模

  • and/or/xor:与/或/异或

  • shl/shr/ushr:有符号左移/有符号右移/无符号右移

示例分析

通过演示,让我们来看看下面的这个 Java 程序在 smali 层面是怎样实现的:

1
2
3
4
5
6
public class Main {
private static String HELLO_WORLD = "Hello World!";
public static void main(String[] args) {
System.out.println(HELLO_WORLD);
}
}

adb shell dumpsys activity top

adb shell am start -D -n 包名/.ui.login.LoginActivity

nkctf9233bg