环境搭建与使用

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。

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 密钥,建议对可信任的开发机勾选 “一律……”。

Warning

如果无法进行 USB 连接,请查看 Google USB Driver 是否安装。Windows 用户也在 “设备管理器” 中查看是否有被警告的 “其他设备”。驱动获取参考获取 Google USB 驱动程序
从 Android 11 开始,支持 Wi-Fi 无线调试,详见通过 Wi-Fi 连接到设备

然后就可以通过 adb devices 查看设备的连接情况了。

主要的 ADB 命令有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
adb help  显示 ADB 命令帮助
adb version 显示 ADB 的版本和运行路径
adb start-server 启动 ADB 服务端
adb kill-server 停止 ADB 服务端
adb devices 显示连接的设备列表
adb install <name>.apk 通过 ADB 安装 APP
adb install -r <name>.apk 覆盖安装 APP
adb uninstall <package.name> 通过 ADB 卸载 APP(注意不再是 apk 名,而是 AndroidManifest.xml 中的 package 属性值,可通过 `adb shell pm list packages` 获取)

adb push <local_src> <remote_dest> 将开发机上的文件传送到设备上
adb pull <remote_src> <local_dest> 将设备上的文件传送到开发机上
adb shell 进入设备的 Linux 交互式终端
adb -s <device_name> shell 多设备下进入指定设备的 Linux 终端
adb shell <shell_command> 通过设备的 Linux 终端执行单条命令

adb root 以 Root 权限重启 ADB,即获取(部分)设备 Root。仅限模拟器或“超级 adbd”
adb remount 重新挂载 /system 目录并获得读写权限。需要在获得 Root 权限的 ADB 中使用

完整的 ADB 命令文档请参阅 Android 调试桥 (adb)

Logcat

一个应用在运行过程中少不了各种日志。Android 日志记录系统是系统进程 logd 维护的一组结构化环形缓冲区。这组可用的缓冲区是固定的,并由系统定义。该日志系统会存储应用日志、系统日志和崩溃日志。

Logcat 🐱 是一个命令行工具,用于转储日志,包括应用使用 Log 类写入的消息。它需要在设备的 Linux Shell 中运行,也可以和 ADB 搭配使用(本质上仍然在 Shell 中运行)。Android Studio 提供了带 GUI 的 Logcat

Logcat 的基本命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
adb logcat  常规显示
adb logcat -help 显示帮助信息
adb logcat -c 清除日志
adb logcat -g 显示日志缓冲区大小
adb logcat -G <size> 设置日志缓冲区大小
adb logcat -v time 切换日志显示格式
adb logcat -v color 带颜色的日志显示
adb logcat -v brief output 简短显示
Ctrl + C 中断 Logcat

adb logcat -s <tag> 根据 tag 过滤输出,注意 tag 大小写敏感,没有模糊搜索

ps -A | grep <package.name> # 获取进程 pid
adb logcat | findstr <pid> 根据 pid 过滤输出,注意 findstr 是 Windows 的工具,Linux 用 grep

Android 扫盲

Android 开发历史简述

Android 4.4 之前,使用 Dalvik / DVM 虚拟机执行 DEX(Dalvik 可执行文件),系统中有 libdvm.so 文件
Android 4.4 时,Google 开始使用 ART (Android Runtime),但仍保留 DVM,系统中并存 libdvm.solibart.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):资源文件索引

还有其他文件(夹),但现阶段我们只关注 libAndroidManifest.xmlclasses.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 文件)和 oatodexvdex 等由 DEX 文件转换得到的文件)

  • data/local/tmp 目录:临时目录。该目录的权限比较大,因此也可以将处理好的文件放在这里,不用担心 data/data 的权限问题

  • system/app 目录:存放系统 APP 本体。

  • system/libsystem/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 后就可以开始开发了。

Tip

Gradle 发行版的下载可以换用阿里云镜像,在 gradle-wrapper.properties 文件中的 distributionUrl 修改。注意使用初始提供的 Gradle 版本。

Warning

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 执行的类,一般加固的应用会有这个。

(不是所有属性都有,但大部分属于默认设置)

Note

此处的 @string@mipmap 表示资源位置,分别位于 res 下的 values/strings.xmlmipmap/ 文件夹。

在 Android 应用中,MainActivity 类的 onCreate() 函数扮演着程序入口点的角色。

Note

MainActivityonCreate() 是程序员能够控制的程序入口。在系统层面,一个应用的入口事件执行顺序是:Application.attachBaseContext(), Application.onCreate(), MainActivity.attachBaseContext(), MainActivity.onCreate()

你也可以自己指定入口点。和 LAUNCHER 绑定的 activity 就是入口点所在的类。

1
2
3
4
5
6
7
8
9
10
11
12
<activity
android:name="com.example.ezandroidpro.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<!-- LAUNCHER 是 APP 的启动界面 -->
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value=""/>
</activity>

android.intent.category.LAUNCHER 绑定的 com.example.ezandroidpro.MainActivity 就是入口点所在的类。

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}

使用 static 块可以在程序开始时先于 OnCreate 方法执行代码块。

1
2
3
4
5
6
7
8
9
static {
Log.d("GK", "hello"); // 用于发送日志输出,输出的信息可以用 Logcat 查看。d 表示 debug 级别
}

@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("OC", "it's me");
// ...
}
Logcat
1
2
2025-09-18 08:37:48.335  6568-6568  GK  com.example.helloworld  D  hello
2025-09-18 08:37:48.406 6568-6568 OC com.example.helloworld D it's me

编译后的 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
2
3
4
5
<Button
android:id="@+id/button_id"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Button" />
按钮创建后出现错误

当你在设计界面上放下一个组件时,这个组件并不是固定在这个位置上的。你没有给这个组件设置一个相对关系,导致 Android 不知道要把这个组件放在哪里,于是报错。

AS 的实时提示会给出第一个建议:MissingConstraints。这算是一个损招了,因为忽略相对关系会让 Android 将你设置的布局全部挤到左上角!

正确的方式是乖乖设置相对关系。在 Design 界面中,点击待设置的组件,会发现有四个空心白点。将水平和垂直方向上的至少一个白点绑定到屏幕边缘或其他组件,则完成相对位置设置,此时移动组件,它的位置也就确定下来了。

如果白点绑定到屏幕边缘,那么在此方向上,组件以绝对位置定位;如果绑定到其他组件上,则以相对于该组件的位置定位。

按钮点击后,需要执行操作(事件函数),操作需要绑定到对应的 Button 上。以下是两种绑定方法:

  • 属性绑定
Warning

在最新的 AS 中被标记为 “已弃用”

在上面的 XML 标签中写:

1
android:onClick="test"

意思是注册一个 onClick 事件,当事件触发时执行 test 方法。

接下来在 MainActivity 类中编写 test 方法:

1
2
3
4
5
6
import android.util.Log;
// ...
public void test(View view)
{
Log.d("watermelonabc", "Hello World!");
}

编译并安装 APP,点击按钮,打开 Logcat,就可以得到:

Logcat
1
watermelonabc           com.example.helloworld               D  Hello World!
Warning

如果 android:onClickMainActivity 类中方法名不符,编译时不会报错,但运行并点击按钮后,APP 就会崩溃,提示 Could not find method。现在 AS 的实时分析已经可以检查出此错误。

由于程序规模增加后不易维护,此种绑定方式不再建议使用。

  • 类内绑定

MainActivity 中先创建一个 Button 对象并绑定到 button_id 的按钮,然后创建一个 View.OnClickListener 对象,并调用 setOnClickListener(View.OnClickListener) 以将其分配给按钮。

View.OnClickListenerinterface 类型,属于抽象类型(C++ 提到过的 “接口类”),我们需要定义这个接口里的 onClick 方法。

1
2
3
4
5
6
7
Button WelcomeButton = (Button) findViewById(R.id.button_id);

WelcomeButton.setOnClickListener(new View.OnClickListener(){
public void onClick(View v){
Log.d("Watermelonabc", "Get Your Second FLAG!");
}
});
Warning

此处组件的绑定 / 初始化使用的是 findViewById。如果需要使用 binding,需要在 build.gradle 中启用 viewBinding

build.gradle.kts(:app)
1
2
3
4
5
android {
buildFeatures {
viewBinding = true
}
}

这是 Android 开发者文档的做法,在函数参数里完成接口的实例化。

这个接口类可以单独出来实现:

1
2
3
4
5
6
7
8
9
  WelcomeButton.setOnClickListener(new Hello());

// ...

class Hello implements View.OnClickListener {
public void onClick(View v){
Log.d("Watermelonabc", "Get Your Third FLAG!");
}
}

也可以在已有类内实现方法:

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public void onClick(View v){
Log.d("Watermelonabc", "Get Your Third FLAG!");
}

// ...

Button WelcomeButton = (Button) findViewById(R.id.button2);
WelcomeButton.setOnClickListener(this); // OR WelcomeButton.setOnClickListener(new MainActivity())

如果有多个按钮需要调用 onClick 方法,但需要不同逻辑,可以用 View.getId 获取当前按钮的 ID:

1
2
3
4
5
6
7
8
9
10
11
public void onClick(View v){
switch(v.getId()){
case R.id.button1:
Log.d("Watermelonabc", "Get Your First FLAG!");
break;
case R.id.button2:
Log.d("Watermelonabc", "Get Your Second FLAG!");
break;
}

}

Toast 组件

Log 的信息输出在 Logcat 中。但一般用户不会接触到 Android 终端,如何将 Button 触发的事件结果呈献给用户呢?Toast(消息框)就是一个方法。

Toast 可以在一个小型弹出式窗口中提供与操作有关的简单反馈。消息框只会填充消息所需的空间大小,并且当前 activity 会一直显示并供用户与之互动。超时后,消息框会自动消失。

例如,点按电子邮件中的发送会触发 “正在发送邮件…” 消息框。

最基础的 Toast 用法:

1
2
3
// Toast makeText(Context context, CharSequence text, @Duration int duration)
// makeText 只是初始化,要显示必须调用 show
Toast.makeText(this, "The flag hides where you can't see~", Toast.LENGTH_SHORT).show(); // 链式调用,这样不需要再显式创建对象

TextView 组件

TextView 组件用于向用户显示文本,其基本的 XML 为:

1
2
3
4
5
6
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
/>

一个更改 TextView 组件内文本的示例:

1
2
TextView TipText = findViewById(R.id.textView2);
TipText.setText("I want a CAT");

有时我们想和之前 Button 下的 onClick 联动,此时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends AppCompatActivity {

private TextView TipText; // 声明为成员变量,便于整个类中访问

@Override
protected void onCreate(Bundle savedInstanceState) {
// ...

Button WelcomeButton = (Button) findViewById(R.id.button2);
TipText = findViewById(R.id.textView2);

WelcomeButton.setOnClickListener(this);
}

@Override
public void onClick(View v){
Log.d("Watermelonabc", "Get Your Third FLAG!");
Toast.makeText(this, "The flag hides where you can't see~", Toast.LENGTH_SHORT).show();
TipText.setText("I want a CAT");
}
}

EditView 组件

EditView 用于输入和修改文本。基本 XML 为:

1
2
3
4
5
<EditText
android:id="@+id/plain_text_input"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:inputType="text"/>

定义一个编辑文本小部件时,必须指定 inputType 属性。例如,对于纯文本输入,将 inputType 设置为 "text"。在 Design 视图中,EditView 就呈现为多个属性变种。inputType 配置了显示的键盘类型、可接受的字符以及编辑文本的外观。

我们主要解读这些用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private EditText Pwd;

// ...

String password = Pwd.getText().toString().trim();

if (TextUtils.isEmpty(password)) {
Toast.makeText(this, "Password is Empty!", Toast.LENGTH_SHORT).show();
}

//...

Pwd.setText("fakeflag{You_are_poor!!!}");

getText() 返回 Editable 类型,需要用 toString 转化为 String 类型才能使用。trim 用于删除头尾空格。

TextUtils 字面意思就是 “文本工具”,isEmpty 方法用于检查字符串是否为空。

setText() 就是修改编辑框内容。

字符串定位

涉及到 strings.xmlR.javapublic.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。这种技术允许开发者或黑客在不修改应用程序源代码的情况下,对其进行定制、调试、修改或篡改。

Note

钩子编程 (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 文件了

Note

我在编写 APP 调用 Native 库函数解密 API KEY 看到作者直接调用 Native 库重新写了一个 APP……

Tip

当 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
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
// 注册函数映射
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;

// 获取环境
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

// 找到类。为了实现这一点,JNI_OnLoad 是从正确的类加载器上下文中被调用的。
jclass c = env->FindClass("com/example/app/package/MyClass");
if (c == nullptr) return JNI_ERR;

// 在 {} 里面进行方法映射编写,第一个是 Java 端方法名,第二个是方法签名,第三个是 C 语言形式签名(括号内是方法返回值类型)
static const JNINativeMethod methods[] = {
{"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
{"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
};

// 动态注册本地方法
// RegisterNatives(JNI 对象, 目标类, 方法映射表, 方法个数),此处 JNI 对象作为 env 的 this 指针被隐藏了,是 C++ 的语法糖
int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
if (rc != JNI_OK) return rc;

//返回 Java 版本
return JNI_VERSION_1_6;
}

其中 methods 变量属于 JNINativeMethod 结构体,用于映射 Java 方法与 C / C++ 函数的关系,其定义如下:

1
2
3
4
5
typedef struct {  
const char* name; // Java 层对应的方法名称
const char* signature;// 该方法的返回值类型和参数类型
void* fnPtr; // Native 中对应的函数指针
} JNINativeMethod;

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 全英文也没碍着你做逆向耶


©2025-Present Watermelonabc | 萌 ICP 备 20251229 号

Powered by Hexo & Stellar 1.33.1 & Vercel & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅

中文独立博客列表 | 博客录 随机博客

AI 参与指数(IIIA)2 级

猫猫🐱 发表了 61 篇文章 · 总计 255.8k 字