安卓逆向基础

APK结构

APK 是 Android Package 的缩写,是 Android 平台上的应用程序包文件格式。APK 文件是一个 ZIP 压缩格式的文件,其中包含了应用程序的代码、资源文件和 META-INF 目录等文件。APK 文件的结构如下:

文件 注释
assets目录 存放APK的静态资源文件,比如视频,音频,图片等
lib 目录 armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件
META-INF目录 保存应用的签名信息,签名信息可以验证APK文件的完整性,相当于APK的身份证(验证文件是否又被修改)
res目录 res目录存放资源文件,包括图片,字符串等等,APK的脸蛋由他的layout文件设计
AndroidMainfest.xml文件 APK的应用清单信息,它描述了应用的名字,版本,权限,引用的库文件等等信息
classes.dex文件 classes.dex是java源码编译后生成的java字节码文件,APK运行的主要逻辑
resources.arsc文件 resources.arsc是编译后的二进制资源文件,它是一个映射表,映射着资源和id,通过R文件中的id就可以找到对应的资源

初识AndroidManifest.xml

AndroidManifest.xml文件是整个应用程序的信息描述文件,它描述了应用程序的名称、版本、图标、权限、组件等信息。AndroidManifest.xml文件是一个 XML 格式的文件,它的根元素是 manifest 元素,manifest 元素的几个常见属性如下:

  • package:应用程序的包名,每个应用程序都有一个唯一的包名。如QQ的包名是com.tencent.mobileqq。
  • versionCode:应用程序的版本号,用于区分不同版本的应用程序。
  • versionName:应用程序的版本名称,用于显示给用户。
  • uses-permission android:name=“”:声明应用程序需要的权限。例如,如果应用程序需要访问网络,则需要声明 android.permission.INTERNET 权限。
  • application:应用程序的信息,包括应用程序的名称、图标、主题、启动 Activity 等信息。
  • activity:应用程序的 Activity 组件,每个 Activity 都需要在 AndroidManifest.xml 文件中进行声明。
  • android:lable=“@string/app_name”:Activity 的名称,用于显示在应用程序的标题栏中。
  • android:icon=“@drawable/ic_launcher”:Activity 的图标,用于显示在应用程序的标题栏中。
  • intent-filter:Activity 的过滤器,用于指定 Activity 的启动方式。例如,通过指定 MAIN 和 LAUNCHER,可以将 Activity 设置为应用程序的启动 Activity。
  • android:debuggable=“true”:是否允许调试应用程序。
    img
    每个应用程序在根目录下必须包含一个AndroidManifest.xml文件,且文件名不能修改。它描述了package中暴露的组件,他们各自的实现类,各种能被处理的数据和启动位置。

简单Android逆向举例——双开

双开软件是一种可以在同一台手机上安装多个同一应用的软件,比如可以在同一台手机上安装两个微信,两个QQ等。双开有多种实现方式:

  • 多开分身:通过修改应用程序的包名和签名,实现多个应用程序的安装。
  • 修改Framework:对于有系统修改权限的厂商,可以修改 Framework 来实现双开的目的,例如:小米自带多开
  • Xposed框架:通过 Xposed 框架实现双开,通过 Hook 应用程序的启动逻辑,实现多个应用程序的安装。
  • 虚拟化技术:通过虚拟化技术实现双开,虚拟 Framework 层、虚拟文件系统、模拟 Android 对组件的管理、虚拟应用进程管理等一整套虚拟技术,将 APK 复制一份到虚拟空间中运行。
  • 以插件机制运行:利用反射替换,动态代(过)(滤)理,hook了系统的大部分与system—server进程通讯的函数,以此作为“欺上瞒下”的目的,欺骗系统“以为”只有一个apk在运行,瞒过插件让其“认为”自己已经安装。例如:VirtualApp

由于其他几种方式比较复杂,涉及到系统层面的修改,这里主要介绍第一种方式:多开分身。多开分身的原理是通过修改应用程序的包名和签名,实现多个应用程序的安装。这里用 NP管理器简单演示,具体步骤如下:
先在菜单栏选择安装包提取,然后选择要提取的应用,
img
提取成功后,点击定位,定位到安装包位置
点击安装包,能看到安装包的一些基本信息,我们点击功能
img
选择 APK 共存,然后会在当前目录下生成一个新的 APK 文件,这个 APK 文件就是多开分身的 APK 文件。
img
点击生成的安装包,可以看到生成的 APK 文件的包名和签名状态已经修改了,这样就实现了多开分身。这时候就可以点击安装了。
img

Dalvik虚拟机和smali语言

Dalvik 虚拟机是 Android 平台上的虚拟机,它是专门为 Android 平台设计的虚拟机,用于执行 Android 应用程序的字节码。它有专属的字节码格式 dex,Dalvik 虚拟机的字节码文件是以 .dex 为后缀的文件。Dalvik 虚拟机的字节码文件是通过将 Java 字节码文件转换为 Dalvik 字节码文件生成的,这个过程叫做 dex 编译。
smali 是一种基于 Dalvik 字节码的汇编语言,它是 Dalvik 字节码的文本表示形式,用于描述 Dalvik 字节码的指令。smali 语言的语法和 Java 语言的语法有很大的不同,smali 语言的指令是 Dalvik 字节码的指令,它是一种基于寄存器的指令集,每个指令都是一个操作码和操作数的组合。smali 语言的指令和 Java 语言的指令是一一对应的,通过 smali 语言可以很方便地查看和修改 Dalvik 字节码。我们在反编译 Android 应用程序时,无法直接修改 Java 源码,但可以通过修改 smali 代码来修改 Dalvik 字节码,从而实现对应用程序的修改。
这里仅对 smali 语言做简单介绍,更多语法内容遇到了再自行查询。
这里分享一个吾爱大佬写的smali语法查询:smali语法查询
首先是关键字,smali 语言的关键字和 Java 语言的关键字有很大的不同,smali 语言的关键字主要包括 .class、.super、.source、.field、.method、.register、.end method、public、protected、private、.parameter、.prologue、.line xxx 等,这些关键字用于描述类、方法、变量等信息。

名称 注释
.class 类名
.super 父类名,继承的上级类名名称
.source 源名
.field 变量
.method 方法名
.register 寄存器
.end method 方法名的结束
public 公有
protected 半公开,只有同一家人才能用
private 私有,只能自己使用
.parameter 方法参数
.prologue 方法开始
.line xxx 位于第xxx行

然后是数据类型,smali 语言的数据类型和 Java 语言的数据类型有很大的不同,smali 语言的数据类型主要包括 V、Z、B、S、C、I、J、F、D、string、Lxxx/xxx/xxx 等,这些数据类型用于描述变量的类型。

smali类型 java类型 注释
V void 无返回值
Z boolean 布尔值类型,返回0或1
B byte 字节类型,返回字节
S short 短整数类型,返回数字
C char 字符类型,返回字符
I int 整数类型,返回数字
J long (64位 需要2个寄存器存储) 长整数类型,返回数字
F float 单浮点类型,返回数字
D double (64位 需要2个寄存器存储) 双浮点类型,返回数字
string String 文本类型,返回字符串
Lxxx/xxx/xxx object 对象类型,返回对象

最后是指令集,smali 语言的指令集和 Dalvik 字节码的指令集是一一对应的,通过 smali 语言可以很方便地查看和修改 Dalvik 字节码。smali 语言的指令集主要包括常量指令、加载指令、存储指令、算术指令、逻辑指令、控制指令、异常指令等,这些指令用于描述 Dalvik 字节码的操作。

指令 注释
const 重写整数属性,真假属性内容,只能是数字类型
const-string 重写字符串内容
const-wide 重写长整数类型,多用于修改到期时间。
return 返回指令
if-eq 全称equal(a=b),比较寄存器ab内容,相同则跳
if-ne 全称not equal(a!=b),ab内容不相同则跳
if-eqz 全称equal zero(a=0),z即是0的标记,a等于0则跳
if-nez 全称not equal zero(a!=0),a不等于0则跳
if-ge 全称greater equal(a>=b),a大于或等于则跳
if-le 全称little equal(a<=b),a小于或等于则跳
goto 强制跳到指定位置
switch 分支跳转,一般会有多个分支线,并根据指令跳转到适当位置
iget 获取寄存器数据

smali语法实例——VIP会员绕过

接下来就通过一个简单的实例来学习 smali 语法。实例安装包
首先先看看功能,点击获取硬币,硬币数就会增加,单击一键三连提示请长按完成一键三连,长按一键三连就提示请先充值大会员哦!
img
根据关键词大会员,我们在jadx-gui中搜索大会员,找到相关代码
img
找到对应 Java 代码,我们可以看到在点击一键三连时,会调用一个名为isVip的方法,如果不是大会员则会提示请先充值大会员。我们可以通过修改 smali 代码来绕过这个检测。
img
找到对应的 smali 代码,
img
分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//一个私有、静态、不可变的方法   方法名lambda-2
.method private static final onCreate$lambda-2(Lkotlin/jvm/internal/Ref$IntRef;Lcom/zj/wuaipojie/ui/ChallengeSecond;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/view/View;)Z //方法名,参数,返回值Z表示返回值为布尔类型
.registers 7 //7个寄存器

.line 33 //第33行
iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I //获取寄存器element属性,存入p0

const/4 p5, 0x1 //将1存入p5

const/16 v0, 0xa //将10存入v0

if-ge p0, v0, :cond_15 //如果p0大于等于v0,则跳转到cond_15

.line 34 //第34行,以下是常见的Toast弹窗
move-object p0, p1 //将p1存入p0

check-cast p0, Landroid/content/Context; //强制转换p0为Context类型

const-string v0, "\u8bf7\u5148\u83b7\u53d610\u4e2a\u786c\u5e01\u54e6"//弹窗文本"请先获取10个硬币哦"赋值给v0

check-cast v0, Ljava/lang/CharSequence; //强制转换v0为CharSequence类型

invoke-static {p0, v0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; //调用makeText方法,生成一个Toast对象,将弹窗文本和弹窗时长传入p1

move-result-object p0 //将返回值存入p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //调用show方法,显示弹窗

.line 36 //第36行
:cond_15 //cond_15标签
invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //调用isvip方法,返回值存入p1

move-result p0 //将返回值存入p0

if-eqz p0, :cond_43 //如果p0为0,则跳转到cond_43

.line 37
check-cast p1, Landroid/content/Context; //强制转换p1为Context类型

const-string p0, "\u5f53\u524d\u5df2\u7ecf\u662f\u5927\u4f1a\u5458\u4e86\u54e6\uff01" //弹窗文本"当前已经是大会员了哦!"赋值给p0

check-cast p0, Ljava/lang/CharSequence; //强制转换p0为CharSequence类型

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; //调用makeText方法,生成一个Toast对象,将弹窗文本和弹窗时长传入p1

move-result-object p0 //将返回值存入p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //调用show方法,显示弹窗

const p0, 0x7f0d0018 //在arsc文件中查找id为0x7f0d0018的资源,赋值给p0

.line 38
invoke-virtual {p2, p0}, Landroid/widget/ImageView;->setImageResource(I)V //调用setImageResource方法,设置ImageView的资源为p0

const p0, 0x7f0d0008 //在arsc文件中查找id为0x7f0d0008的资源,赋值给p0

.line 39
invoke-virtual {p3, p0}, Landroid/widget/ImageView;->setImageResource(I)V //调用setImageResource方法,设置ImageView的资源为p0

const p0, 0x7f0d000a //在arsc文件中查找id为0x7f0d000a的资源,赋值给p0

.line 40
invoke-virtual {p4, p0}, Landroid/widget/ImageView;->setImageResource(I)V //调用setImageResource方法,设置ImageView的资源为p0

.line 41
sget-object p0, Lcom/zj/wuaipojie/util/SPUtils;->INSTANCE:Lcom/zj/wuaipojie/util/SPUtils; //获取SPUtils的INSTANCE属性,存入p0

const/4 p2, 0x2 //将2存入p2

const-string p3, "level" //将"level"存入p3

invoke-virtual {p0, p1, p3, p2}, Lcom/zj/wuaipojie/util/SPUtils;->saveInt(Landroid/content/Context;Ljava/lang/String;I)V //调用saveInt方法,保存数据

goto :goto_50 //强制跳转到goto_50

.line 44
:cond_43 //cond_43标签
check-cast p1, Landroid/content/Context; //强制转换p1为Context类型

const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01" //弹窗文本"请先充值大会员哦!"赋值给p0

check-cast p0, Ljava/lang/CharSequence; //强制转换p0为CharSequence类型

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; //调用makeText方法,生成一个Toast对象,将弹窗文本和弹窗时长传入p1

move-result-object p0 //将返回值存入p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //调用show方法,显示弹窗

:goto_50 //goto_50标签
return p5 //返回p5
.end method //方法结束


# virtual methods
//一个公有、最终、不可变的方法 方法名isvip
.method public final isvip()Z
.registers 2 //2个寄存器

const/4 v0, 0x0 //将0存入v0

return v0 //返回v0
.end method //方法结束

可以看到,isvip方法默认返回值为0,即不是大会员,我们可以通过修改这个方法的返回值来绕过大会员检测。我们可以将返回值改为1,即是大会员,这样就可以绕过大会员检测了。
其次我们需要先获取10个硬币,才能点亮一键三连,我们可以通过修改 onCreate$lambda-2 方法中的 if-ge p0, v0, :cond_15 这一行代码,将 v0 的值改为 0,这样就可以绕过硬币检测了。但是这个不是很必要,因为我们可以直接修改硬币的数量,这里只是为了演示 smali 语法。
接下来就是修改环节,用 NP管理器提取安装包后查看 classes.dex 文件
选择 DEX编辑PLUS,搜索 isvip,找到对应的方法,将返回值改为1
img
img
定位到 isvip 方法的 smali 代码,将返回值改为 1
修改完成后保存退出,NP管理器会自动重新打包签名,点击安装即可
重新安装后就可以点亮一键三连了
img