APK逆向基础
APK逆向基础
本文适合
CTF 逆向工程 入门学习者。学完你能:拆解 APK 结构,定位 Manifest、Java/Kotlin、资源和 native so 中的校验入口,并完成一次最小验证
APK 是 Android 应用安装包。它本质上是一个压缩包,里面包含代码、资源、配置、签名和可能的 native 库。
一句话判断
拿到 .apk 时,不要先当普通 exe 看;先把它当压缩包和 Android 工程拆开,按 Manifest、dex、资源、assets、native so 五条线定位校验逻辑。
APK 题的关键通常在“输入从 Activity 到校验函数,再到资源或 native 层”的调用链。
题目中常见信号
- 附件是
.apk,或 zip 解开后有AndroidManifest.xml、classes.dex、res/、lib/。 - 页面提示输入 flag、password、serial、activation code。
- jadx 里能搜到
check、verify、encrypt、decrypt、native、System.loadLibrary。 - Manifest 里有非主 Activity、exported 组件、deep link 或 ContentProvider。
assets/、res/raw/、resources.arsc里有 key、表、密文、数据库或配置。- Java 层只有壳逻辑,真正判断在
lib/arm64-v8a/*.so。
核心概念
APK 逆向要分层看:
- 入口层:Manifest 决定应用启动入口和暴露组件。
- Java/Kotlin 层:dex 中保存 Activity、事件处理和大部分业务逻辑。
- 资源层:字符串、布局、raw/assets 可能保存密钥、密文或二阶段数据。
- native 层:JNI 调用
.so,常用于隐藏关键算法。 - 动态层:运行时 hook、调试和日志能验证输入输出。
不要只看 MainActivity。CTF 题常把关键逻辑放在按钮点击、隐藏 Activity、资源文件或 native 方法里。
最小分析流程
- 用
file或直接解压确认 APK 结构。 - 用 apktool 读 Manifest,确定入口 Activity、包名、权限、exported 组件和 deep link。
- 用 jadx 搜索
flag、correct、wrong、check、verify、native。 - 追按钮事件或输入框处理函数,找到校验函数。
- 检查资源和 assets 是否参与校验。
- 若看到
System.loadLibrary或native方法,提取 so 进入 JNI/native 分析。 - 动态运行时用 logcat、Frida 或模拟器验证关键函数输入输出。
最小验证示例
apktool d target.apk -o decoded
grep -R "MAIN\\|LAUNCHER\\|exported\\|scheme" decoded/AndroidManifest.xml
jadx -d jadx_out target.apk
grep -RniE "flag|correct|wrong|check|verify|native|loadLibrary" jadx_out | head -50如果搜索到 MainActivity.checkFlag(String input),先读它的返回条件;如果它调用 nativeCheck(input),继续提取 so:
unzip -o target.apk "lib/*/*.so" -d so_out
find so_out -type f -name "*.so"
strings so_out/lib/arm64-v8a/libnative-lib.so | grep -Ei "flag|check|wrong|correct"这组命令能完成“入口在哪、校验在哪、是否进 native”的最小判断。
常见利用 / 解题路线
路线总览:
- Java 层校验路线:jadx 搜关键字,追按钮事件,直接还原 Java/Kotlin 校验算法。
- 资源联动路线:从
R.string、assets、res/raw读取密文、key、表,再回到代码看解密。 - Manifest 入口路线:分析隐藏 Activity、deep link、exported provider,触发隐藏功能或读取数据。
- smali patch 路线:把失败跳转改成成功、强制返回 true,用于验证路径或取运行时数据。
- JNI 路线:Java 层找 native 方法声明,so 里找静态/动态注册函数,还原 native 算法。
- 动态 hook 路线:用 Frida hook
checkFlag、nativeCheck、String.equals、memcmp,打印参数和返回值。
APK 里有什么
常见内容包括:
AndroidManifest.xml:应用配置、权限、入口 Activity。
classes.dex:Dalvik/ART 字节码,保存 Java/Kotlin 逻辑。
resources.arsc 和 res/:资源文件、字符串、布局、图片。
lib/:native so 库,常见 C/C++ 编译产物。
assets/:程序自带资源,可能藏配置或数据文件。
Java 层和 native 层
很多基础 APK 题的逻辑在 Java 或 Kotlin 层,可以用 jadx 反编译查看。
如果代码调用了 System.loadLibrary 或 native 方法,关键逻辑可能在 .so 里,需要用逆向工具分析 native 层。
JNI 是 Java 层和 native 层之间的桥。
Manifest 为什么重要
Manifest 能告诉你:
- 应用入口 Activity。
- 使用了哪些权限。
- 暴露了哪些组件。
- 是否有 deep link。
- 包名和版本信息。
找入口时,不要只盯着文件列表,先看 Manifest。
资源不只是图片
资源目录里可能有字符串、加密数据、字典、音频、证书、数据库或额外 dex。
有些题会把 flag 的一部分放在代码里,另一部分放在 assets 或 raw 资源里。
多 dex 问题
大型 APK 可能有多个 dex,例如:
classes.dex
classes2.dex
classes3.dex如果只看第一个 dex,可能漏掉真正逻辑。
Jadx 通常会一起加载,但手动处理时要注意。
jadx 反编译工作流
jadx 是 APK 逆向的核心工具:
# 命令行使用
jadx -d output_dir target.apk
# 图形界面
jadx-gui target.apk标准分析流程:
1. 用 jadx-gui 打开 APK
2. 左侧展开包名树,找到主包
3. 搜索字符串(Resources -> Strings)找 flag、key 等关键词
4. 搜索方法名:check、verify、encrypt、decrypt
5. 右键方法 -> 查看用法(Usage)
6. 检查 Manifest 中声明的入口 Activity
7. 追踪 native 方法调用当 jadx 反编译失败(混淆严重或字节码异常)时,可以退回到 smali 层面分析。
smali 基础语法
smali 是 Dalvik 字节码的可读表示,类似汇编:
# 方法定义
.method public checkFlag(Ljava/lang/String;)Z
.locals 4 # 局部变量数量
.param p1, "input" # 参数名
# const-string v0, "flag{"
const-string v0, "flag{"
# invoke-virtual {p1, v0}, Ljava/lang/String;->startsWith(Ljava/lang/String;)Z
invoke-virtual {p1, v0}, Ljava/lang/String;->startsWith(Ljava/lang/String;)Z
move-result v1 # 将返回值放入 v1
# if-eqz v1, :fail
if-eqz v1, :fail # 如果 v1 == 0(false),跳到 fail
# 通过检查
const/4 v0, 0x1
return v0
:fail
const/4 v0, 0x0
return v0
.end method常用 smali 类型描述符:
V - void
Z - boolean
I - int
J - long
F - float
D - double
[B - byte[]
Ljava/lang/String; - String对象
[I - int[]关键 smali 指令:
const/4 v0, 0x1 ; v0 = 1
const-string v0, "abc" ; v0 = "abc"
iget v0, p0, Lcom/example/Foo;->secret:I ; 读取实例字段
iput v0, p0, Lcom/example/Foo;->secret:I ; 写入实例字段
invoke-virtual {p0, p1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0 ; 获取上一个调用的返回值
move-result-object v0 ; 获取返回的对象引用
if-eqz v0, :label ; v0 == 0 则跳转
if-nez v0, :label ; v0 != 0 则跳转
goto :label ; 无条件跳转Manifest 分析实战
# 反编译 Manifest
apktool d target.apk -o decoded/
cat decoded/AndroidManifest.xml关注以下字段:
<!-- 入口 Activity(有 LAUNCHER intent-filter 的) -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 暴露的组件(exported=true 或有 intent-filter) -->
<activity android:name=".SecretActivity" android:exported="true"/>
<!-- 权限声明 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Content Provider(可能泄露数据) -->
<provider android:name=".DataProvider"
android:authorities="com.example.provider"
android:exported="true"/>
<!-- Deep Link -->
<data android:scheme="myapp" android:host="flag"/>Native 库加载分析
当 Java 层调用 native 方法时,需要分析 .so 库:
// Java 层声明 native 方法
public native String decrypt(byte[] input, byte[] key);
// 加载 so 库
static {
System.loadLibrary("native-lib");
}so 文件位于 lib/ 目录下:
lib/
arm64-v8a/libnative-lib.so # ARM 64位
armeabi-v7a/libnative-lib.so # ARM 32位
x86/libnative-lib.so # x86 32位
x86_64/libnative-lib.so # x86 64位分析 native 层的方法:
# 提取 so 文件
unzip target.apk "lib/*"
# 用 IDA/Ghidra 打开 so
# JNI 函数名格式:Java_包名_类名_方法名
# 例如 Java_com_example_MainActivity_decrypt
# 用 jadx 查看 JNI 接口
# 搜索 "native" 关键字找到所有 native 方法声明JNI 常见函数调用模式:
// 读取 Java 传入的 byte[]
jbyteArray input = (*env)->GetObjectField(env, obj, fieldId);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
jsize len = (*env)->GetArrayLength(env, input);
// 返回 String
jstring result = (*env)->NewStringUTF(env, "flag{...}");
return result;常见失败原因
- 把 APK 当成普通 exe 分析:先解包和看 Manifest,再决定 Java、资源还是 native。
- 只看 Java 层:看到
native或System.loadLibrary就要转 JNI/so 分析。 - 忽略 assets 和 raw:很多题把密文、key、模型、数据库放在资源里。
- 只看 MainActivity:真实入口可能是 Manifest 指向的别名 Activity、深链或 exported 组件。
- 看到混淆名就放弃:方法名乱了,字符串、调用关系、资源 ID 仍然可用。
- jadx 反编译异常就停止:退回 smali、apktool 输出或动态 hook。
- 多 dex 漏看:确认
classes2.dex、classes3.dex是否包含关键包。
迷你案例
题目给 check.apk。用 apktool 看 Manifest,入口是 .MainActivity;jadx 搜 Wrong,跳到按钮点击函数:
if (Checker.check(input, getString(R.string.k))) {
show("Correct");
}继续看 R.string.k,发现资源字符串是 Base64 key;Checker.check 对输入逐字节异或后和 assets 中的 enc.bin 比较。把 enc.bin 导出,用 key 逆运算得到 flag。这个案例的闭环是:Manifest 定入口 -> 字符串 xref 找按钮事件 -> 资源参与校验 -> 导出 assets -> 脚本还原。