环境搭建与使用
Java 环境
Java 开发环境分为 JRE (Java Runtime Environment) 和 JDK (Java Development Kit),我们一般安装 JDK。
JDK 常用 LTS 版本,有 8、11、17 和 21;常见的发行版本来自 Oracle。
在安装好 JDK 后,需要在环境变量里新增一个 JAVA_HOME
条目,指向需要使用的 JDK 的安装路径,然后在 Path
里增加 %JAVA_HOME%\bin
,便于多版本管理。
JADX
JADX 是 Android Dex 和 Apk 文件的反编译器,可以将文件反编译为 Java 源代码。
如果想查看类与函数定义,可以直接双击该类 / 函数。如果想查找引用,右击选择 “查找用例” 或点击快捷键 X。
注意不要先开反混淆,反混淆会修改一些类名,导致后面 hook 函数的时候 hook 失败。反混淆仅在类名显示为乱码时使用。
GDA
GDA 是国产的反编译器,功能也很强大,不过界面相对来说不那么好看
JEB
建议在 JADX 和 JEB 均无法顺利反编译的情况下用 JEB。
需要定制虚拟机,输出 Smali 代码(Android 的汇编代码)和寄存器数据。
Android Studio 与 ADB
Android Studio 基于 JetBrains IDEA,是 Android 开发的重要工具。
默认的 SDK 路径在 C:\Users\<UserName>\AppData\Local\Android\Sdk
,标准安装会安装最新版的 SDK,在 Language & Frameworks > Android SDK 中可以勾选较旧版本的 SDK
在 SDK Tools 中还需要勾选 NDK (Side by side)、CMake 和 Google USB Driver
我们主要的开发和逆向工作都是在 PC 上进行的,如果需要控制手机应用的运行情况,则要将 PC 和手机关联起来,让 PC 控制手机。这就是 ADB(Android Debug Bridge,调试桥)的作用。
在 C:\Users\<UserName>\AppData\Local\Android\Sdk\platform-tools\
下,我们就可以找到一个 adb
程序。
ADB 的构成和工作方式:
- 客户端 (Client):用于发送命令。客户端在开发机器上运行。可以通过
adb
命令从命令行终端调用客户端。 - 服务器 (server):用于管理客户端与守护程序之间的通信。服务器在开发机器上作为后台进程运行。
- 守护程序 (ADB Daemon, adbd):用于在连接的设备上运行命令。守护程序在每个设备上作为后台进程运行。
graph LR A[ADB 客户端] -->|发送命令| B[ADB 服务端] B -->|返回结果| A B -->|发送命令| C[ADB Daemon] C -->|返回结果| B C -->|执行命令| D[设备] D -->|返回结果| C
当您启动某个 ADB 客户端时,该客户端会先检查是否有 ADB 服务器进程已在运行。如果没有,它会启动服务器进程。所有 ADB 客户端均使用端口 5037 与 ADB 服务器通信。
使用 ADB 前需要在设备上启用 USB 调试(在默认隐藏的 “开发人员选项” 中,请参考启用开发者选项),Android Studio 提供的模拟器默认开启。如果使用真机,开启时请接受用于调试的 RSA 密钥,建议对可信任的开发机勾选 “一律……”。
如果无法进行 USB 连接,请查看 Google USB Driver 是否安装。Windows 用户也在 “设备管理器” 中查看是否有被警告的 “其他设备”。驱动获取参考获取 Google USB 驱动程序
从 Android 11 开始,支持 Wi-Fi 无线调试,详见通过 Wi-Fi 连接到设备
然后就可以通过 adb devices
查看设备的连接情况了。
主要的 ADB 命令有:
1 | adb help 显示 ADB 命令帮助 |
完整的 ADB 命令文档请参阅 Android 调试桥 (adb)
Logcat
一个应用在运行过程中少不了各种日志。Android 日志记录系统是系统进程 logd
维护的一组结构化环形缓冲区。这组可用的缓冲区是固定的,并由系统定义。该日志系统会存储应用日志、系统日志和崩溃日志。
Logcat 🐱 是一个命令行工具,用于转储日志,包括应用使用 Log
类写入的消息。它需要在设备的 Linux Shell 中运行,也可以和 ADB 搭配使用(本质上仍然在 Shell 中运行)。Android Studio 提供了带 GUI 的 Logcat
Logcat 的基本命令:
1 | adb logcat 常规显示 |
Android 扫盲
Android 开发历史简述
Android 4.4 之前,使用 Dalvik / DVM 虚拟机执行 DEX(Dalvik 可执行文件),系统中有 libdvm.so
文件
Android 4.4 时,Google 开始使用 ART (Android Runtime),但仍保留 DVM,系统中并存 libdvm.so
和 libart.so
文件
Android 5.0+ 完全转向 ART,系统架构开始分为 32 位和 64 位
APK 基本结构
apk 本质上是一个具有特殊文件夹结构的 zip 压缩包,其中包括:
-
静态资源文件目录 (assets/):图片、音频、数据库、网页、配置文件等
-
库文件目录 (lib/):保存各种平台下使用对应的
so
文件。-
目录分为以下架构版本的子文件夹:
armeabi-v7a
(通用架构)、arm64-v8a
(64 bit 架构),x86
/x86-64
(模拟器)。存在子文件夹表示支持该平台。 -
如果在 PC 端逆向,先使用 x86 系列,再逆向 arm 系列。
-
-
编译资源文件目录 (res/):编译后的布局文件
layout
、程序图标等。 -
签名文件目录 (META - INF/):验证 APK 完整性。
-
配置清单文件 (
AndroidManifest.xml
):APP 信息,如名称、版本、权限、引用的库文件等。 -
核心代码文件 (
classes.dex
):Java / Kotlin 的 Dalvik 字节码文件,APK 运行的主要逻辑 -
资源映射文件 (
resources.arsc
):资源文件索引
还有其他文件(夹),但现阶段我们只关注 lib
、AndroidManifest.xml
和 classes.dex
即可
Android 文件目录结构
Android 基于 Linux,因此借鉴了 Linux 的文件树系统。
对于软件逆向,我们主要关注这些目录:
-
data/data
目录:存放 APP 数据,每个 APP 的数据又存放在该目录下以包名命名的目录内。这是一个私有目录,除非有 Root 权限,否则各个 APP 仅可访问自己的目录。由于这个私有目录对 APP 自己没有权限限制,因此可以将处理后的 APP 文件放在这类目录里。 -
data/app
目录:存放用户 APP 本体。在 Android 10 上,该目录和data/data
近似,目录名称是包名 + 随机数,但 Android 12 开始,变成双层目录,第一层没有包名只有随机数,第二层才是包名 + 随机数。该目录下一般有base.apk
(APP 的原始 APK,大多数情况下可以直接安装)、lib/
(so
文件)和oat
(odex
、vdex
等由 DEX 文件转换得到的文件) -
data/local/tmp
目录:临时目录。该目录的权限比较大,因此也可以将处理好的文件放在这里,不用担心data/data
的权限问题 -
system/app
目录:存放系统 APP 本体。 -
system/lib
和system/lib64
目录:存放 APP 用到的so
文件
Android 正向开发入门
无论是哪种语言 / 平台,要搞逆向,最好是先了解如何正向开发,在正向开发中知道程序的运行流程和代码特点。这一块有很多教程。
Android 开发流程可以概括为
graph LR A(使用 Java/Kotlin 语法 + Android SDK) --> C(Android Studio 编译成 xx.apk) B(通过 JNI 调用非 Java 代码) --> C C --> D(资源文件/xx.dex/xx.so)
Android Studio 是主要的开发工具。
Android Studio 介绍
现在开始写一个 Hello World 吧!在 “New Project” 中选一个工程模板。
“我们一般比较常用的是 Empty Views Activity 和 Empty Activity 两种,前者是传统的 View 体系界面开发,后者使用 Compose 进行界面开发(固定使用 Kotlin)”
—— 隔壁西二在线的 Android 开发入门
新建工程后,AS 就会提示 “Sync Gradle”。由于需要从境外下载资源,因此需要开🪜(但也还是慢啊啊啊)。同步完 Gradle 后就可以开始开发了。
Gradle 发行版的下载可以换用阿里云镜像,在 gradle-wrapper.properties
文件中的 distributionUrl
修改。注意使用初始提供的 Gradle 版本。
Java 版本不要太高!AS 默认的是 Java 21,就用它自带的或者你自己安装的 Java 21,版本高了构建会报错。
如果 AS 警告说多个 Java,不需要理它。
整个 AS 主要分为三个视图:Project、Code 和 Design 视图(分别位于左、中、右)。
- Project 视图用于显示项目文件和文件夹
- Code 视图用于编辑代码
- Design 视图用于预览应用外观
右上角可以切换 Code 和 Design 视图的可见性。
在 AS 中,我们有两种主要的文件组织方式:Android 和 Project Source Files。前者是 AS 中的标准文件组织方式,后者则是我们在操作系统中使用的组织方式。文件组织方式可以在 Project 视图的左上角进行切换。
好了,基本的工具使用已经介绍完了,接下来介绍一些开发知识。
APP 信息
对于使用 Java 语言开发的项目,我们在 MainActivity.java
中写程序逻辑,在 activity_main.xml
中写程序界面布局。在 Android 中,Activity(活动)可以理解为应用各界面的名称,类似 Windows 中的窗口。应用可以通过 Activity 直接跳转到指定的界面,而无需经过启动界面。AndroidManifest.xml
下有各个 Activity 的索引。
AndroidManifest.xml
声明了 app 的具体信息:
属性 | 定义 |
---|---|
versionCode |
版本号,用于更新 |
versionName |
版本名,给用户看的 |
package |
包名 |
package android:name="xxx" |
APP 引用的第三方包 |
uses-permission android:name="xxx" |
应用需要的 xxx 权限 |
android:label="@string/app_name" |
应用名称 |
android:icon="@mipmap/ic_launcher" |
应用图标路径 |
android:debuggable="true" |
debug 权限,默认关闭 |
android:allowBackup="true" |
备份权限 |
android:supportsRtl="true" |
支持从右往左的文字排列(如阿拉伯文字) |
android:theme="@style/Theme.xxx" |
显示主题 |
application 下的 android:name="xxx" |
先于 MainActivity 执行的类,一般加固的应用会有这个。 |
(不是所有属性都有,但大部分属于默认设置)
此处的 @string
、@mipmap
表示资源位置,分别位于 res
下的 values/strings.xml
和 mipmap/
文件夹。
在 Android 应用中,MainActivity
类的 onCreate()
函数扮演着程序入口点的角色。
MainActivity
的 onCreate()
是程序员能够控制的程序入口。在系统层面,一个应用的入口事件执行顺序是:Application.attachBaseContext()
, Application.onCreate()
, MainActivity.attachBaseContext()
, MainActivity.onCreate()
。
你也可以自己指定入口点。和 LAUNCHER
绑定的 activity
就是入口点所在的类。
1 | <activity |
和 android.intent.category.LAUNCHER
绑定的 com.example.ezandroidpro.MainActivity
就是入口点所在的类。
1 | public class MainActivity extends AppCompatActivity { |
使用 static
块可以在程序开始时先于 OnCreate
方法执行代码块。
1 | static { |
1 | 2025-09-18 08:37:48.335 6568-6568 GK com.example.helloworld D hello |
编译后的 APK 默认存放于 <your_project>/app/build/outputs/apk/{release, debug}
中。具体是 release 还是 debug 要看你在 Build Variants 中的设置,默认是 debug。
组件
在一个 Activity 中,组件 (Component) 是指帮助用户与 Android 应用交互的 UI 元素,比如你看到的按钮、输入框等,都是一种组件。
你可以在 res/layout/
下找到 activity_main.xml
。随着技术的发展,activity_main.xml
的编写已经可视化了,你可以直接在页面上摆放组件,对应的代码就会生成在 activity_main.xml
中。
Button
组件
Button
最基本的 XML 代码长这样:
1 | <Button |
当你在设计界面上放下一个组件时,这个组件并不是固定在这个位置上的。你没有给这个组件设置一个相对关系,导致 Android 不知道要把这个组件放在哪里,于是报错。
AS 的实时提示会给出第一个建议:MissingConstraints
。这算是一个损招了,因为忽略相对关系会让 Android 将你设置的布局全部挤到左上角!
正确的方式是乖乖设置相对关系。在 Design 界面中,点击待设置的组件,会发现有四个空心白点。将水平和垂直方向上的至少一个白点绑定到屏幕边缘或其他组件,则完成相对位置设置,此时移动组件,它的位置也就确定下来了。
如果白点绑定到屏幕边缘,那么在此方向上,组件以绝对位置定位;如果绑定到其他组件上,则以相对于该组件的位置定位。
按钮点击后,需要执行操作(事件函数),操作需要绑定到对应的 Button
上。以下是两种绑定方法:
- 属性绑定
在最新的 AS 中被标记为 “已弃用”
在上面的 XML 标签中写:
1 | android:onClick="test" |
意思是注册一个 onClick
事件,当事件触发时执行 test
方法。
接下来在 MainActivity
类中编写 test
方法:
1 | import android.util.Log; |
编译并安装 APP,点击按钮,打开 Logcat,就可以得到:
1 | watermelonabc com.example.helloworld D Hello World! |
如果 android:onClick
和 MainActivity
类中方法名不符,编译时不会报错,但运行并点击按钮后,APP 就会崩溃,提示 Could not find method
。现在 AS 的实时分析已经可以检查出此错误。
由于程序规模增加后不易维护,此种绑定方式不再建议使用。
- 类内绑定
在 MainActivity
中先创建一个 Button
对象并绑定到 button_id
的按钮,然后创建一个 View.OnClickListener
对象,并调用 setOnClickListener(View.OnClickListener)
以将其分配给按钮。
View.OnClickListener
为 interface
类型,属于抽象类型(C++ 提到过的 “接口类”),我们需要定义这个接口里的 onClick
方法。
1 | Button WelcomeButton = (Button) findViewById(R.id.button_id); |
此处组件的绑定 / 初始化使用的是 findViewById
。如果需要使用 binding
,需要在 build.gradle
中启用 viewBinding
:
1 | android { |
这是 Android 开发者文档的做法,在函数参数里完成接口的实例化。
这个接口类可以单独出来实现:
1 | WelcomeButton.setOnClickListener(new Hello()); |
也可以在已有类内实现方法:
1 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { |
如果有多个按钮需要调用 onClick
方法,但需要不同逻辑,可以用 View.getId
获取当前按钮的 ID:
1 | public void onClick(View v){ |
Toast
组件
Log
的信息输出在 Logcat
中。但一般用户不会接触到 Android 终端,如何将 Button
触发的事件结果呈献给用户呢?Toast
(消息框)就是一个方法。
Toast
可以在一个小型弹出式窗口中提供与操作有关的简单反馈。消息框只会填充消息所需的空间大小,并且当前 activity
会一直显示并供用户与之互动。超时后,消息框会自动消失。
例如,点按电子邮件中的发送会触发 “正在发送邮件…” 消息框。
最基础的 Toast
用法:
1 | // Toast makeText(Context context, CharSequence text, @Duration int duration) |
TextView
组件
TextView
组件用于向用户显示文本,其基本的 XML 为:
1 | <TextView |
一个更改 TextView
组件内文本的示例:
1 | TextView TipText = findViewById(R.id.textView2); |
有时我们想和之前 Button
下的 onClick
联动,此时:
1 | public class MainActivity extends AppCompatActivity { |
EditView
组件
EditView
用于输入和修改文本。基本 XML 为:
1 | <EditText |
定义一个编辑文本小部件时,必须指定 inputType
属性。例如,对于纯文本输入,将 inputType
设置为 "text"
。在 Design 视图中,EditView
就呈现为多个属性变种。inputType
配置了显示的键盘类型、可接受的字符以及编辑文本的外观。
我们主要解读这些用法:
1 | private EditText Pwd; |
getText()
返回 Editable
类型,需要用 toString
转化为 String
类型才能使用。trim
用于删除头尾空格。
TextUtils
字面意思就是 “文本工具”,isEmpty
方法用于检查字符串是否为空。
setText()
就是修改编辑框内容。
字符串定位
涉及到 strings.xml
、R.java
和 public.xml
(不一定有)。
strings.xml
提供了字符串名称和字符串内容的对应关系,是 APP 内字符串定义的集中点。将字符串集中定义在 strings.xml
中,还可以让 AS 辅助实现多语言支持,此时翻译后的字符串就会保存在 values_<lang>/strings.xml
中。当然,翻译需要自行设定,因此逆向一般只关注 values/strings.xml
即可
R.java
是编译时生成的,提供了字符串名称和索引值 id
的对应关系。在 APP 编译后,代码中的 R.string.xxx
就会被替换为对应的索引值。
曾经逆向者需要借助
resources.arsc
才能恢复R.java
,但现在 JADX 等工具已经可以自动完成这一步了。
public.xml
也是编译时生成,提供索引值和字符串名称的对应关系。
R.java
由编译器自动生成。如果资源发生变化,那么 R.java
的索引值也可能发生变化。如果在开发中有需要固定索引值的需求,那么就需要手动编写 public.xml
。
由于当前工具已经可以很好地恢复 R.java
,因此 public.xml
不是必要的。这更多是一个 “路标”,指向高价值的目标(毕竟需要手动编写)。
Android 逆向入门
总的来说,Android 逆向可以分为 Java 层逆向和 Native 层逆向。
Java 层逆向
使用 Jadx、GDA、JEB 等 Android 反编译工具就是在 Java 层进行逆向,你看到的是 Java 代码。Java 层逆向是相对简单的,只需要熟读代码即可。
除了重现算法,我们还可以对 APP 进行 Hook。这种技术允许开发者或黑客在不修改应用程序源代码的情况下,对其进行定制、调试、修改或篡改。
钩子编程 (hooking),也称作 “挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子 (hook)。
——钩子编程
在 Android 平台上,Hook 的目标事件的粒度一般为函数级别,即在指定函数被调用时执行我们 Hook 的代码,根据我们的逻辑拦截修改原有的函数响应。
Android Hook 方案五花八门,不是我们这一篇入门文章可以讲得完的。所以,画饼开始 目前常用的 Hook 框架有 Frida 系和 Xposed 系,但这两种都需要 Root…… 当调试环境有问题时,可以使用 Unidbg 系框架模拟执行或者直接看字节码(汇编)。
graph LR A(拿到要破解的 APP 的 apk) --> B(反编译 Java 代码) B --> C(搜索关键字,阅读 Java 代码逻辑) C --> D(写 Hook 工具) D --> E(Python 模拟发送请求,破解加密) E --> F(获取到我们想要的数据)
Native 层逆向
有些任务使用 Java 完成并不容易,因此 Java 提供了 Native 方法,即调用非 Java 编写的函数。Native 方法通常使用 C / C++ 编写,其实现保存在 so
文件中。一般的 Android 反编译工具无法看到 Native 层的代码,所以高级点的 APP 会将加密逻辑写在 Native 层。但由于 so
文件用 C / C++ 编写,因此可以用 IDA 反编译它。
如果你在 Java 层代码中看到了 native xxx func()
,就需要去看 so
文件了
我在编写 APP 调用 Native 库函数解密 API KEY 看到作者直接调用 Native 库重新写了一个 APP……
当 IDA 无法胜任静态调试且动态调试也受阻的话,就需要上面提到的 Hook 了。
Java 代码和 Native 代码通过 Java Native Interface (JNI) 进行交互。由于被调用的 Native 方法必须被 JNI 注册,因此变相提供了方法索引,有利于逆向分析。
JNI 注册分为静态注册和动态注册。
在静态注册中,Java 方法和 Native 函数的映射关系由 JVM 自动设置,并遵循固定的命名规则:Java_[包名]_[类名]_[方法名]
,例如 Java_com_example_ezandroidpro_MainActivity_check
。由此可见,静态注册暴露了函数的 Native 属性、调用位置及函数名,使得逆向者很容易就能找到 Java 代码和 Native 函数的关联模式。
在动态注册中,Java 方法和 Native 函数的映射关系由开发者通过 JNINativeMethod
注册表和 JNI_OnLoad
函数手动指定。由于是手动指定,因此 Native 函数可以随意命名,只要注册表正确即可。开发者还可以对注册表进行动态加解密,提高破解难度。
一个简单的 JNI_OnLoad
函数:
1 | // 注册函数映射 |
其中 methods
变量属于 JNINativeMethod
结构体,用于映射 Java 方法与 C / C++ 函数的关系,其定义如下:
1 | typedef struct { |
JNI_OnLoad
函数的第一个参数是 JavaVM
指针类型,这里 IDA 不能自动识别,所以需要手动修复一下,这有助于帮你理解代码。
抓包
抓包工具有很多种类,比如全局抓包、代理抓包、VPN 抓包、网卡抓包、手机抓包、Hook 抓包等。我们重点介绍的是 Charles、HttpCanary、rOCapture、Http v7 和 WireShark
Charles
Charles 是一个代理抓包工具。
Charles 安装:https://www.charlesproxy.com/ 和激活码计算器:https://www.zzzmode.com/mytools/charles/
那我的汉化这一块?
Charles 5 暂时找不到公开的汉化耶 o-O
要汉化的话就是 4.x 了,52 上面找找?
反正 IDA 全英文也没碍着你做逆向耶