简介:这份PDF面向CTF竞赛入门与进阶选手,聚焦Android移动端逆向分析这一高频考点,帮助读者建立从APK反编译到漏洞定位的完整解题思路。内容以APKToolBOX与jadx两款工具为主线,串联Android应用逆向工程、Java字节码还原、应用安全测试等核心知识,并通过Androideasy、DD Android Easy等真实赛题演示异或运算还原flag、FlagActivity定位等典型手法,附带可复用的Python解密脚本与排错思路。资源包内仅含1个PDF文件,体积约18KB,轻量便携,适合随时查阅与对照练习。目前已有473人学习下载,可作为CTF安卓逆向专项训练的入门参考与工具速查手册。
1. 从一份 ctf逆向安卓篇.pdf 说起:安卓逆向到底在考什么
很多人第一次打 CTF 的安卓方向,是拿到一个 APK 之后完全不知道从哪下手。jadx 打开一看满屏反编译代码,APKToolBOX 解出来一堆 smali 目录,搜 flag 搜不到,动态调试又连不上,最后只能放弃。这份流传很广的 ctf逆向安卓篇.pdf 之所以被反复找,本质上是因为它把「安卓逆向在 CTF 里到底考哪几类题、每类题用什么工具链、卡住时看哪里」这条线串起来了。它解决的不是「教你安卓开发」,而是让你在面对一个陌生 APK 时有一套可复现的拆解流程:先静态看结构,再定位校验逻辑,最后动态验证。适合已经会一点 Java、想入门 CTF 移动安全方向的人,也适合打过几场 misc 但一遇到 APK 就发怵的选手。下面我按自己实际做题的顺序,把这条链路拆开讲清楚。
2. 拿到 APK 先做什么:静态分析工具链与定位思路
2.1 为什么先静态而不是直接上动态
CTF 安卓题里,绝大多数 flag 校验逻辑都藏在 Java/Kotlin 层,只有少数会下沉到 native so 或者加固壳里。静态分析的成本最低:不需要真机、不需要 root、不需要配 frida,一个 jadx 就能覆盖七八成的题目。我一般的顺序是先用 jadx-gui 打开 APK 看整体结构,重点看 AndroidManifest.xml 里的入口 Activity、有没有注册 Service 或 Receiver、有没有明显的 native 库引用。这一步的目的是建立「这个 App 大概怎么跑起来」的心智模型,而不是急着找 flag。
判断要不要上动态,有个简单的信号:如果 jadx 里搜flag、check、verify、decrypt这些关键词能直接命中一段看着像校验的代码,那就纯静态搞定;如果搜出来全是混淆过的单字母类名、或者关键逻辑在native-lib.so里、又或者代码明显被抽走了(方法体是空的或者只有throw),那就得考虑动态或者脱壳。这个判断能帮你省掉大量无效折腾。
2.2 jadx 与 APKToolBOX 的分工
这两个工具经常被混着用,但定位不一样。jadx 强在反编译成 Java 可读代码,适合读逻辑;APKToolBOX(本质是 apktool 加一堆封装脚本的集成环境)强在解包出 smali、资源文件和原始 manifest,适合改包重打包、看资源里的字符串、分析 so 加载路径。我的习惯是:读逻辑用 jadx,改东西用 apktool。
用 jadx 命令行批量导出,比 GUI 更适合脚本化处理:
# 反编译 APK,输出到 out_dir,--show-bad-code 保留反编译失败的伪代码 jadx -d out_dir --show-bad-code target.apk # 只导出资源,不反编译代码,速度快,适合先看 manifest 和 strings.xml jadx -d res_only --no-src target.apk # 搜索关键词,-r 递归,-i 忽略大小写 grep -rn "flag" out_dir/sources/ | head -50-d指定输出目录,--show-bad-code很关键,很多题目的关键方法 jadx 反编译会失败,但伪代码里往往保留了足够的字符串和调用关系,直接跳过就漏了。--no-src在只想快速看资源时能省时间。搜关键词时别只搜flag,CTF 里常见变体有FLAG、flag{、key、secret、check、input,还有把校验逻辑拆成多个方法的,所以要结合上下文看调用链。
APKToolBOX 解包:
# 解包,-o 指定输出目录,-f 强制覆盖 apktool d -f -o unpacked target.apk # 解包后重点看这几个位置 # unpacked/AndroidManifest.xml 入口和组件 # unpacked/smali/ 所有 smali 代码 # unpacked/res/values/strings.xml 硬编码字符串 # unpacked/lib/ 各架构 so # unpacked/assets/ 常放加密数据或二次加载的 dexassets目录是重灾区,很多题把加密后的 dex 或者数据文件塞在这里,运行时再解密加载。看到 assets 里有不认识的.dat、.bin、.dex就要警觉。lib目录下按架构分文件夹,如果只有armeabi-v7a没有arm64-v8a,说明题目可能针对特定架构,动态调试时要注意模拟器架构匹配。
2.3 定位校验逻辑的三个实用套路
第一,从入口 Activity 顺着onCreate往下读,看它setContentView加载了哪个布局,布局里有哪些输入框和按钮,按钮的onClick绑定了什么方法。这是最笨但最稳的办法,适合代码量不大的题。
第二,全局搜字符串。flag 的提示、错误提示(比如「wrong」「try again」「correct」)往往直接写在代码或资源里,从提示字符串反查引用它的方法,能快速跳到校验点。用 jadx 的搜索功能或者直接 grep 反编译输出都行。
第三,看build.gradle或者 manifest 里的依赖,如果引了frida-gadget、xposed相关的库,或者有System.loadLibrary调用,说明题目设计时就考虑了动态分析,这时候静态可能只是铺垫,真正的逻辑在运行时。
提示:搜关键词时把
const-string也纳入搜索范围,smali 里的字符串常量经常是突破口,jadx 反编译后可能被内联,但 smali 里原样保留。
3. 动态调试怎么搭:从模拟器到 frida 钩子
3.1 环境选择:模拟器还是真机
CTF 安卓题对动态环境的要求没生产环境那么苛刻,但坑一点不少。模拟器首选带 root 的 AOSP 镜像或者 Genymotion,因为要 push frida-server、要能adb shell su。真机的话需要解锁 bootloader 加 root,成本高,除非题目检测模拟器(常见手段是查Build.FINGERPRINT、ro.product.model、传感器数量),否则模拟器够用。
架构匹配是第一个坑:frida-server 的架构必须和 App 运行的架构一致。如果 APK 只带了armeabi-v7a的 so,而模拟器是 x86_64,App 可能跑不起来或者 so 加载失败。解决办法是选支持 ARM 翻译的模拟器,或者用adb shell getprop ro.product.cpu.abi确认架构后下载对应版本的 frida-server。
3.2 frida 环境搭建与最小验证
# 1. 确认设备连接和架构 adb devices adb shell getprop ro.product.cpu.abi # 2. 推送 frida-server(假设架构是 arm64) adb push frida-server-16.x.x-android-arm64 /data/local/tmp/fs adb shell "chmod 755 /data/local/tmp/fs" adb shell "su -c '/data/local/tmp/fs &'" # 3. 宿主机确认 frida 能连上 frida-ps -U | headfrida-ps -U能列出设备上的进程就说明通了。如果报unable to connect,先检查 frida-server 有没有真的跑起来(ps -A | grep fs),再检查宿主机 frida 版本和 server 版本是否匹配,大版本不一致经常连不上。
最小钩子脚本,验证能不能 hook 到目标方法:
// hook.js Java.perform(function () { // 替换成你要 hook 的类和方法 var Target = Java.use("com.example.ctf.MainActivity"); Target.checkFlag.implementation = function (input) { console.log("[*] checkFlag called, input = " + input); var ret = this.checkFlag(input); // 先调用原方法 console.log("[*] return = " + ret); return ret; }; });frida -U -f com.example.ctf -l hook.js --no-pauseJava.perform确保在 Java 虚拟机就绪后执行,Java.use拿到类引用,implementation覆盖原方法。this.checkFlag(input)是调用原始实现,如果你只想看返回值不想改逻辑,这样写;如果想直接绕过校验,可以return true。-f是启动 App 并注入,--no-pause让它不要停在启动阶段。参数input的类型要和原方法签名一致,类型不对会报argument types do not match。
3.3 常见动态场景的处理
如果校验逻辑在 native 层,frida 的 Java hook 就够不着了,要用Interceptor.attach钩 so 里的导出函数:
// hook native 函数 var addr = Module.findExportByName("libnative-lib.so", "Java_com_example_ctf_MainActivity_check"); Interceptor.attach(addr, { onEnter: function (args) { // args[0] 是 JNIEnv*, args[1] 是 jobject, args[2] 开始才是 Java 传入的参数 console.log("[*] native check called"); console.log(hexdump(args[2], { length: 32 })); // 打印传入的字符串 }, onLeave: function (retval) { console.log("[*] retval = " + retval); } });Module.findExportByName按导出符号名找地址,JNI 函数名有固定命名规则Java_包名_类名_方法名,下划线替换点号。args数组里前两个是 JNI 固定参数,真正的业务参数从args[2]开始。hexdump打印内存内容,适合看传入的字节。onLeave里的retval是返回值,可以在这里改。
如果题目用了加固(常见的有几大商业壳),直接 frida 注入会被检测或者附加失败。这时候的思路是先脱壳拿到真实 dex,再用 jadx 静态分析。脱壳工具和方法迭代很快,核心原理是让壳在内存里解密出 dex 后 dump 出来,具体工具这里不展开,遇到时按「内存 dump + dex 修复」这个方向找当前可用的方案。
注意:动态调试时 App 可能检测 frida 的端口、进程名、
/data/local/tmp下的文件。改 frida-server 的文件名和端口是基本操作,fs改成不显眼的名字,默认端口 27042 改成其他。
4. 避坑与排查:安卓逆向里最容易翻车的五件事
4.1 jadx 反编译出来代码是空的或者全是乱码
现象:打开某个类,方法体是空的,或者只有// decompilation failed,或者变量名全是a、b、c。
原因:代码被混淆(ProGuard/R8)或者被加固壳抽走了方法体,jadx 无法还原。混淆只改名字不改逻辑,加固则是运行时才解密真实代码。
解决:混淆的情况,靠字符串和调用关系硬读,或者用 jadx 的--show-bad-code看伪代码;加固的情况,静态基本没戏,转动态脱壳。判断是混淆还是加固,看方法体是不是完全空——混淆的方法体有逻辑只是名字乱,加固的方法体是真的空或者只有throw new RuntimeException。
4.2 frida 注入报Failed to spawn: unable to find process
现象:frida -U -f 包名直接报找不到进程,或者frida-ps -U列不出目标 App。
原因:包名写错、App 没安装、frida-server 没跑、或者 App 有反调试在启动阶段就退出了。
解决:先用adb shell pm list packages | grep 关键词确认包名;frida-ps -U确认 server 活着;如果 App 一启动就退,用adb logcat | grep -i "frida\|debug\|crash"看日志,大概率是反调试。反调试的绕过思路是 hook 掉检测函数,常见检测点有Debug.isDebuggerConnected、android.os.Debug、/proc/self/status里的TracerPid。
4.3 动态 hook 到了方法但参数是乱码
现象:console.log(input)打出来是[object Object]或者一堆看不懂的字节。
原因:参数类型不是 String,可能是 byte[]、ByteBuffer 或者自定义对象;或者字符串被加密了,运行时才解密。
解决:先确认方法签名,用javap或者 jadx 看参数类型。如果是 byte[],用Java.array('byte', input)转成 JS 数组再打印;如果是加密字符串,hook 解密函数而不是校验函数,在解密之后打印明文。
4.4 重打包后 App 闪退
现象:用 apktool 改了点东西重新打包签名安装,一打开就闪退。
原因:签名校验、完整性校验、或者资源引用错乱。很多 App 会校验签名哈希,重打包后签名变了就触发退出。
解决:先看 logcat 定位崩溃点。签名校验的话,hook 掉校验函数或者用原始签名(如果拿得到)。资源问题的话,检查apktool.yml里的版本和--use-aapt2参数,aapt 和 aapt2 混用经常导致资源 ID 错乱。
4.5 搜 flag 搜不到,怀疑题目没放 flag
现象:全局搜flag、key、secret都没有明显结果。
原因:flag 是运行时拼出来的,或者存在 assets 的加密文件里,或者需要特定输入才触发。
解决:搜const-string里的可疑字符串,看有没有 base64、hex 编码的长串;检查 assets 和 res/raw 下的文件,用file命令看类型;动态跑一遍,在关键函数下断点看内存。CTF 里 flag 很少直接明文放着,编码、加密、拼接是常态。
5. 进阶技巧:用脚本批量处理与校验结果
打到后面你会发现,单题手撕效率太低,很多题有共性。我一般会写几个小脚本把重复劳动自动化。比如批量反编译一批 APK 并提取所有字符串常量:
# batch_strings.py import subprocess import os import re def extract_strings(apk_path, out_dir): # 用 jadx 导出资源,再用 aapt 提取字符串 subprocess.run(["jadx", "-d", out_dir, "--no-src", apk_path], check=True) strings_file = os.path.join(out_dir, "resources", "res", "values", "strings.xml") if os.path.exists(strings_file): with open(strings_file, "r", encoding="utf-8", errors="ignore") as f: content = f.read() # 提取所有 string 标签的值 return re.findall(r'<string name="[^"]*">([^<]*)</string>', content) return [] if __name__ == "__main__": apk_dir = "./apks" for apk in os.listdir(apk_dir): if apk.endswith(".apk"): path = os.path.join(apk_dir, apk) out = os.path.join("./out", apk.replace(".apk", "")) strings = extract_strings(path, out) print(f"=== {apk} ===") for s in strings: if any(k in s.lower() for k in ["flag", "key", "secret", "check"]): print(" ", s)这个脚本的逻辑是:对每个 APK 用 jadx 只导出资源,然后从strings.xml里正则提取所有字符串,过滤出含关键词的。--no-src跳过代码反编译,速度快很多,适合先做一轮粗筛。errors="ignore"防止编码问题中断。实际用的时候可以把关键词列表扩展,加上题目相关的提示词。
另一个实用技巧是校验结果。CTF 里 flag 格式通常是flag{...}或者题目自定义的前缀,写个校验函数确认你解出来的东西格式对不对:
import re def check_flag(candidate, prefix="flag"): # 常见格式:flag{xxx} 或 prefix{xxx} pattern = rf"^{prefix}\{{[^}}]+\}}$" if re.match(pattern, candidate): return True # 有些题是纯 hex 或者 base64,长度固定 if re.match(r"^[0-9a-fA-F]{32}$", candidate): return True return Falseprefix按题目实际格式改,[^}}]+匹配花括号内非花括号内容。这个函数不能帮你解题,但能帮你快速排除明显不对的候选,尤其是在暴力枚举或者批量尝试的时候。
最后一个习惯:每道题做完把关键步骤记下来,包括用了什么工具、卡在哪、怎么绕过的。安卓逆向的题型重复率很高,加固方式、反调试手段、加密算法就那么几类,积累下来下次遇到同类题能直接套。我自己的笔记里按「壳类型 / 反调试 / 加密算法 / 动态 hook 点」分了类,比按题目记有用得多。
希望帮到你。
本文还有配套的精品资源,点击获取