Android逆向
Android基础知识
系统架构
安卓(Android)是一种基于Linux内核(不包含GNU组件)的自由及开放源代码的操作系统。主要使用于移动设备,如智能手机和平板电脑,由美国Google公司和开放手机联盟领导及开发。
android框架:
从下往上分别是: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 编译流程
正向开发
首先我们来写一个 app,并打包成 apk。在这个过程中,我们可以接触到很多安卓逆向中会遇到的概
念。
一个 app 可以理解为由三个基本元件构成:
AndroidManifest.xml:可以想成是 app 的配置文件
resources:各种资源,包括排版、代码中出现的字符串、图片等
代码:UI 界面及逻辑代码
下面是一个简单的 demo,左边是项目树,右边是 AndroidManifest.xml 的內容:
从这个配置文件,我们可以知道:
这个 app 的包名(package name)是 com.example.demo
这个 app 里有一个窗体(activity),名字为 MainActivity ,是程序入口窗体
什么是 activity 呢?你可以把 activity 理解成是一个界面,每个程序界面对应一个 activity。假设 app是个需要注册才能使用的应用,则可能有以下界面:
欢迎界面
注册界面
登录界面
主界面
而这每一个界面都是一个 activity,而每一个 activity 可能都有一个 layout。在 Android 开发中,layout 其实就是一个 xml 文档,像这样:
像 layout 就属于资源文件的一种,会被放到 res 文件夹下。
上图中有两点需要注意,一是 android:id=”@+id/textview_first” 代表这个组件对应到一个 id。才能在代码里通过 id 拿到这个组件:
1 | TextView tv = (TextView) findViewById(R.id.textview_first); |
第二个值得注意的地方是 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 | zipalign -v -p 4 demo2.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 文件是怎么得到的呢?
通过 javac 把 .java 编译成 .class
通过 dx 把 .class 转化为 .dex
通过 baksmali 把 .dex 转为 smali
1 | javac Test.java |
Dalvik 寄存器
Dalvik中 的寄存器都是 32 位大小,支持所有类型。对于小于或等于 32 位类型,使用一个寄存器就可
以了,对 64 位(long 和 double) 类型,需要使用两个相邻的寄存器来存储。
寄存器的命名法有两种:****V 命名法和 P 命名法:
v 命名法:局部变量寄存器 V0-Vn,参数寄存器是 Vn-V(n+m)。
p 命名法:参数寄存器 P0-Pn,变量寄存器 V0-Vn。
v 命名法
v0-v2 表示的是局部变量寄存器,v3-v4 表示的参数寄存器。在例一中第一个方框里可以看到 v2 是局部寄存器, v3 是参数寄存器。在图中第二个方框里可以看到 v0 是局部寄存器,v4 是参数寄存器。
p 命名法
p 命名法针对参数寄存器进行了优化,参数寄存器的命名从 p0 开始,使得局部变量寄存器和参数寄存器得以很容易的进行区分。smali 语法中就是用了 p 命名法。我们来看下 add() 方法的 smali 代码:
1 | .method public add(II)I |
这样就很清晰了,4 个寄存器命名如下所示:
v0 局部变量寄存器,存储 a+b 的值
p0 当前引用 this
p1 参数寄存器,存储 a 的值
p2 参数寄存器,存储 b 的值
Dalvik 描述符
在更深入的了解 Dalvik 字节码前,先来看一下 Dalvik 是如何描述字段和方法的,这也有助于我们阅读smali 代码。
类型描述符
Dalvik 字节码中只有两种类型,基本类型和引用类型。除了对象和数组以外,其他的所有 Java 类型都是基本类型。这和 JVM 的类型描述符是基本一致的。基本类型都是使用单个字母来表示。数组类型使用[ 表示。除数组以外的引用类型使用 L 加上全限定名表示。如下表所示:
举一个引用类型的例子。例如 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 | Landroid/content/pm/ActivityInfo;->theme:I |
方法
Dalvik 方法的表现形式:类型(包名+类名)+方法名(+参数类型)+返回值类型
例如:Lpackage/name/ObjectName;->MethodName (III) Z
MethodName 是方法名, (III) Z 是方法的签名信息,由方法参数列表(III)和返回值(Z)构成,(III)表示三个 int 型参数;Z 表示返回值类型为 boolean。
其他例子:
1 | Lcom/test/Test;->add(II)I |
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 指令根据字节码大小与类型不同,后面会跟上不同的后缀,表达的意义也就不同。
总结起来 move 指令有三种作用:
进行赋值操作
move-result 接收返回值操作
处理异常的操作
返回指令
返回指令即 return 指令,指的是函数结尾时运行的最后一条指令。共有以下四条返回指令:
数据定义指令
数据定义指令用来定义程序中用到的常量,字符串,类等数据。它的基础字节码为 const
类型判断指令
数组操作指令
数组操作包括获取数组长度,新建数组,数组赋值,数组元素取值与赋值等操作。
异常指令
Dalvik 指令集有一条指令用来抛出异常:
throw vAA 抛出 vAA 寄存器指定的异常
跳转指令
比较指令
比较指令用于对两个寄存器的值(浮点型或长整型)进行比较。
大于(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 | iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、iget-short |
静态字段
sstaticop vAA,field@BBBB
对已标识的静态字段执行已确定的对象静态字段运算,并将结果加载或存储到值寄存器中
针对不同类型的静态字段,有如下指令:
1 | sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、sget-short |
方法调用指令
方法调用指令的格式为 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB , 具体的有如下指令:
数据转换指令
数据转换指令用于将一种类型的数值转换成另一种类型。它的格式为“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 | public class Main { |
adb shell dumpsys activity top
adb shell am start -D -n 包名/.ui.login.LoginActivity
nkctf9233bg