ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CTF安卓逆向入门:APK拆解与Frida动态调试实战指南

CTF安卓逆向入门:APK拆解与Frida动态调试实战指南 简介ctf逆向安卓篇.pdf是一份以 CTF 安卓逆向为核心的 PDF 学习资料读者定位为 CTF 参赛者、移动安全初学者以及想快速掌握 APK 静态分析的开发与安全人员。文档从 Android 应用逆向工程的基本概念讲起结合 Android Easy 与 DD Android Easy 两个典型赛题完整演示了如何用 APKToolBOX 与 jadx 反编译 APK 文件、定位 MainActivity 和 FlagActivity 等关键类、阅读 onCreate 与 onClick 逻辑以及分析 check 函数中的字节数组与异或 23 运算并编写 Python 脚本输出 flag 的过程。除了解题步骤还覆盖 Android 应用安全测试点出常见漏洞、暴露风险与修复建议能够帮助读者建立从“拿到 APK”到“还原密码/Flag”的完整流程也为后续自行分析同类应用打下基础。压缩包内共 1 个 PDF 文件文件大小约 18KB轻量便携适合边读边对照题目练习目前已有 473 人学习下载对希望快速上手安卓逆向真实题目的读者有较好的参考价值。1. CTF逆向安卓篇一份从零到拿flag的完整路径如果你打开过几份CTF安卓逆向的题目附件大概会和我有一样的感受考的不是“会不会写安卓”而是“会不会拆”。APK解压出来之后dex、so、assets、resources.arsc各藏各的东西flag可能躺在Java层明文里也可能被塞进一个native函数用IDA/Frida折腾一整晚才能抠出来。CTF逆向安卓篇这类pdf之所以流传广是因为它恰恰把“从哪里下手、先看哪个文件、用什么工具、卡住看什么”这些血泪经验整理成了可复现的步骤而不像安卓开发教程那样教你怎么写业务代码。这篇博文就把我平时解题的完整方案摊开讲清楚从APK结构、静态分析、动态调试到高频翻车点新手照着走能跑通一道简单题熟手也能在这里对一遍自己的分析节奏。2. 从APK到flagCTF安卓逆向的完整分析链路2.1 先认识你要拆的东西APK结构里藏着flag的三种常见位置APK本质是一个zip包解压之后你会看到AndroidManifest.xml、classes.dex可能有多个、lib目录下的so文件、assets和res目录以及META-INF签名信息。CTF题目最常见的做法有三种第一种是直接把flag明文放在assets或res/raw里考你“找不找得到”第二种是把flag经过简单变换后写进Java层代码比如字符串拼接、移位、异或固定key考你“会不会看smali或jadx的反编译结果”第三种是把校验逻辑写进native层的so文件Java层只是调用一个check函数考你“会不会用GDA、IDA或Frida去逆so”。这三种位置决定了你的工具优先级。我的一般顺序是先全盘搜字符串再静态读Java层最后才上动态调试。搜索这一步用jadx自带的搜索功能或者grep扫描解压出来的文件就能做很多时候flag就躺在某个文件末尾或者资源文件里。注意AndroidManifest.xml在解压出来是二进制格式直接grep看不到明文应用名和权限字符串需要用jadx或apktool反编译之后再看。2.2 为什么先静态后动态分析顺序决定了你的解题速度很多刚入门的选手一上来就开Frida跑hook结果连目标函数在哪都不知道hook了个寂寞。合理的顺序应当是静态分析先把代码结构和关键逻辑摸一遍找出校验函数的入口和参数动态调试只用来确认运行时数据或者绕过反调试。这个顺序在时间有限、题目数量多的CTF比赛里尤其重要少做一个无效操作就多一分钟给下一题。静态分析最快跑通的一条路径是解压APK → 用jadx打开dex生成Java代码 → 搜索“flag”、“key”、“secret”等关键词 → 顺着可疑调用跳转 → 追到校验函数或资源文件。整个过程五分钟内可以完成。动态调试则是静态分析卡住之后的补充手段比如某段代码被混淆、变量名被抹掉、函数被vmp加固静态看逻辑的成本已经高于跑起来hook的成本这时候才切到Frida或objection。2.3 一套CTF安卓逆向的通用流程从file命令到jadx一键反编译拿到一份题目附件之后第一步不是急着解压而是先确认文件类型。CTF里偶尔会把APK改了后缀名比如改成zip或jpg直接用file命令看一眼最稳妥。常见做法是先建一个工作目录把附件复制进去按顺序跑以下几组命令mkdir ctf_apk cd ctf_apk cp ../challenge.apk . file challenge.apk unzip -o challenge.apk -d apk_out这里先说逻辑file确认是不是zip/APK格式避免后缀名误导unzip解压之后能看到资源文件和so库但此时AndroidManifest.xml和dex还是二进制直接看不了。下一步建议直接用jadx把整个APK反编译成可读代码它内部会自动处理manifest和解码dexjadx -d jadx_out challenge.apk参数说明-d指定输出目录不加的话默认生成在当前目录下同名文件夹。jadx跑完以后业务代码会以Java源码形式出现在jadx_out/sources下AndroidManifest.xml也能直接读。到这里静态分析的原材料就齐了。提示如果jadx反编译中途报错或者进度卡住多半是dex有问题可以先用apktool d challenge.apk单独解出smali再用jadx只打开dex文件两种方式互补。3. 静态分析三板斧jadx看逻辑、GDA看native、脚本搜flag3.1 jadx的四个高频操作搜索、跳转、反混淆和导出jadx是CTF安卓逆向里出场率最高的静态分析工具原因很简单它直接把smali还原成Java阅读难度从汇编级降到业务代码级。常用的操作就四个按频率排序全局文本搜索、点击跳转、查看反混淆、导出工程。全局搜索是定位flag的第一步。界面上方的搜索框支持正则我会优先搜这几个模式flag\{、key、secret、base64、encrypt、check、verify。搜索结果会列出命中的类和行号点进去看上下文就能判断是真flag还是干扰项。这里有个值得注意的细节CTF出题人经常把flag拆成几段用字符串拼接的方式分散在多个方法里比如前一半在onCreate里后一半在一个点击事件的回调里。只看单条匹配容易漏遇到拼接逻辑要顺着赋值关系把片段拼回去。方法跳转靠的是jadx对调用关系的索引。看到可疑方法名右键选择Find Usage可以快速找出谁调用了它。有一道经典题目就是MainActivity里有个checkFlag函数入参是用户输入的字符串返回boolean输入正确就弹窗。做法就是点进这个方法看校验逻辑一般是一个异或循环或者一个for循环比对数组。如果方法体特别短才几行通常是纯逻辑题如果方法体特别长或者调用了native方法就需要转向GDA或者动态调试。反混淆选项在jadx的偏好设置里勾选后会把一些无意义的命名如a.a.b尽量还原成人话。不过它只能处理一部分遇到真正的代码混淆器比如ProGuard加固过的题目还原效果有限这时候不要恋战该上动态调试就上。导出工程这个操作容易被忽略但它特别实用。当题目代码量很大、搜索命中很多可疑点时用jadx的File - Export - Gradle project把源码导出就能放到自己熟悉的IDE里全局搜索还能直接编译运行某些独立算法函数。CTF比赛时钟宝贵跳出jadx卡顿的界面用命令行grep你的工作目录搜索速度更快也更适合把多段代码片段快速拼起来读。3.2 遇到native层别慌GDA定位JNI函数和so导出表Java层代码干净清爽反而要警惕——说明逻辑可能压根不在这。CTF里越来越多题目把核心算法放进lib/arm64-v8a目录下的so文件Java层的System.loadLibrary之后通过JNI调用nativeCheck之类的函数。你搜Java层只能看到一个native方法声明点进去没有函数体这就是提示你“去看so”。看so的常见工具有GDA、IDA和readelf。GDA的优势是对安卓逆向专门做了适配打开so文件后能直接列出JNI导出函数、字符串引用还能辅助还原关键算法。适合CTF的场景题目so不大、函数不多GDA足以应付。如果so特别大函数被混淆那还是得请出IDA但那是另一个故事了。在GDA里拿到JNI函数地址后优先看两样东西函数的汇编逻辑、用到的字符串常量。很多题目出题人偷懒直接用strcmp(用户输入, flag{...})字符串就在静态区放着。就算用了异或加密字符串也往往能在汇编里看到异或的常量key。如果手头连GDA都用不习惯还有一个讨巧的办法直接在命令行用strings工具扫so文件strings lib/arm64-v8a/libnative-lib.so | grep -E flag|key|secret|check参数说明strings会提取二进制里的可打印字符串grep -E用正则匹配关键词。这个命令不能保证看到被加密的字符串但能快速判断出题人有没有偷懒。如果strings的输出里直接出现了flag这题就结束了如果只看到几个不明意义的短字符串说明做了编码处理需要进GDA或IDA看算法。3.3 用Python脚本批量搜flag特征xorknown明文模式静态分析新手经常犯一个错误只用jadx的搜索框搜完就结束。实际上CTF题目的flag特征往往能通过脚本批量发现尤其是当你面对一个包含几十个文件的工程时。我一般会写一个小脚本把整个反编译目录跑一遍搜出所有可疑的字符串赋值和异或运算。import os import re root jadx_out pattern re.compile(rbflag\{[^}]\}|key\s*|secret\s*) for dirpath, _, filenames in os.walk(root): for name in filenames: if not name.endswith(.java): continue path os.path.join(dirpath, name) with open(path, rb) as f: content f.read() for match in pattern.findall(content): print(f{path}: {match[:200]})逻辑说明遍历jadx输出目录下所有Java文件用正则去匹配flag{}、key、secret这几种常见特征命中就打印文件路径和匹配片段。这样可以在一分钟内把几十个类文件扫完比手动一个个点开看高效得多。正则里的[^}]匹配到第一个右花括号为止避免flag后面跟了别的代码导致匹配过长。这里有一个CTF解题里非常常见的模式——xor加密。很多题目的算法就是把用户输入的每个字符异或一个固定key再和预先存储的密文数组比对。你在Java代码里看到类似int key 0x5A; while (i input.length) { output[i] input[i] ^ key; }的片段就可以自己在Python里把密文还原一遍enc [0x01, 0x1C, 0x0A, 0x7D, 0x3F] key 0x5A flag .join(chr(c ^ key) for c in enc) print(flag)这段脚本把密文数组逐个和key做异或还原出flag。参数说明enc是从静态分析里看到的密文数组key是反编译代码里读出的异或常量两者替换成你实际题目里的值即可。注意密文数组如果以字节形式存储在smali里通常是用byte[]和const/16指令加载从jadx看到的Java代码会直接以数组字面量呈现复制下来很省事。4. 动态调试frida与objection组合拳4.1 模拟器还是真机环境选择的分水岭动态调试的第一步是决定跑在什么设备上。模拟器启动快、快照方便是我参加CTF时的首选真机适合那些检测模拟器、检测root的题目以及需要读取真实传感器数据的场景。CTF题目通常不会做太重的环境检测所以模拟器足够应对大部分情况。模拟器选择的常见做法是优先用带root的Android虚拟机镜像比如Genymotion或者Android Studio自带的AVD。前者对游戏类应用兼容性好后者更干净、命令行可控。启动之后第一个动作就是确认adb能看到设备跑一下adb devices状态是device而不是offline。很多新人在这里就翻车了设备离线真机和模拟器各有各的坑后面避坑章会专门说。真机调试的话需要先开启开发者选项和USB调试并且保证adb版本和手机系统匹配。部分安卓11以上系统还要注意授权弹窗里勾选“始终允许”。root环境是动态调试的前提因为frida注入系统和进程需要高权限。如果拿到的是root权限但frida一直注入失败优先检查Selinux状态。4.2 frida的三种注入姿势spawn、attach和frida-traceFrida是安卓逆向动态调试的事实标准面试题里常会问它和Xposed、LSPosed的区别结论是Frida注入不需要重启、不需要刷框架对CTF这种短平快场景最合适。常用方式有三种spawn模式启动新进程、attach模式附加到运行中进程、frida-trace做函数调用跟踪。# spawn模式冷启动app适合在onCreate早期逻辑下hook frida -U -f com.example.ctf -l solve.js --no-pause # attach模式附加到已运行进程适合定位运行中触发校验的时机 frida -U -n com.example.ctf -l solve.js # frida-trace直接跟踪特定函数调用快速看参数和返回值 frida-trace -U -n com.example.ctf -i check*参数说明-U指定USB连接的安卓设备-f后跟包名、以spawn方式启动-l指定JavaScript脚本文件--no-pause表示启动后不暂停在入口处、直接继续运行。-i是frida-trace用来匹配函数名的正则check*表示跟踪所有开头为check的函数。输出会显示每个被跟踪函数的参数和返回值用来确认校验逻辑的输入输出非常直接。三种姿势的选择逻辑是要hook的代码如果发生在Application或MainActivity的onCreate里用spawn模式如果用户要操作几步之后才触发校验用attach模式更方便因为进程已经在运行、界面也加载好了对函数名和调用关系完全不确定的时候先跑frida-trace让工具自动枚举候选函数。4.3 objection的快捷指令内存搜索、类和函数定位objection是基于frida的封装工具把高频操作简化成了命令。它在CTF里最实用的三个场景是枚举类和函数、内存搜索字符串、绕过root检测。# 启动objection并注入目标app objection -g com.example.ctf explore # 枚举包含特定关键字的类 android hooking list classes | grep -i flag # 在内存中搜索flag字符串 memory search flag{ --string # 枚举某个类的所有方法定位可疑校验函数 android hooking list class_methods com.example.ctf.CheckUtil命令说明-g后跟包名explore进入交互模式。后面三条都是在交互模式里执行的。android hooking list classes会输出当前进程加载的所有类配合grep -i flag过滤出包含flag的类名memory search的--string表示按字符串搜索会列出所有内存中出现的flag{位置list class_methods查看指定类的全部方法对判断哪个函数是校验入口非常有用。objection的memory搜索比jadx的静态搜索强在“运行时数据”上——有些flag不是硬编码在代码里而是程序运行时才从服务器拉取或者由算法动态生成静态代码里根本看不到。这也是为什么静态分析到瓶颈之后必须先跑一遍objection的memory search再决定要不要逆so。4.4 跑通一个最小动态调试流程从hook到拿到flag把上面的工具串起来一个最小可用的动态调试流程是这样先adb install安装目标APK用objection枚举类和方法确认目标函数再写一个十几行的frida脚本hook掉它拿到入参和返回值。// solve.js: hook核心校验函数打印入参和返回值 Java.perform(function () { var CheckUtil Java.use(com.example.ctf.CheckUtil); CheckUtil.checkFlag.implementation function (input) { console.log([input] input); var result this.checkFlag(input); console.log([result] result); return result; }; });代码逻辑说明Java.use拿到目标类implementation替换原始函数实现。函数入参input就是用户提交的字符串返回值result是校验结果。这个hook的作用是观察当你输入不同字符串时返回值是否变化。如果返回永远是false说明校验逻辑不在这个Java函数里往下走了native层如果返回值跟着输入变说明可以尝试从这里往里追。实际CTF里更常见的用法是hook住checkFlag之后拿到入参的某种状态或对比数组的内容再结合静态分析拼出flag。比如checkFlag内部是逐字符和某个数组比较你直接在hook里打印出那个数组就比静态看代码快得多。frida脚本里访问Java层的字段用Java.use(...).class.getDeclaredField或者直接this._字段名.value这些在题目复杂一点时会用到但对大多数CTF题一个console.log级别的hook已经能解决一大半问题。5. CTF安卓逆向避坑指南6个高频翻车点5.1 现象adb devices能看到设备但frida一直报unable to connect无法注入原因这个坑我至少踩过三次。第一次是模拟器里没开root权限frida-server启动后没有足够的权限去attach系统进程第二次是frida-server版本和电脑端frida版本不匹配两端版本差一位小版本号就会报连接异常第三次是模拟器里frida-server压根没跑起来只是存在文件里。解决先把电脑端和模拟器端的frida版本对齐。电脑端pip show frida看版本模拟器里frida-server --version看版本两个必须一致。然后确认root权限在模拟器终端执行su能切换到root再启动frida-server。启动时用./frida-server 后台运行再配合adb forward tcp:27042 tcp:27042做端口转发frida命令加-H 127.0.0.1:27042连接。这个组合我现在每次都要检查一遍。5.2 现象模拟器无法访问网络App联网请求一直失败抓包什么都抓不到原因CTF题目偶尔会有需要联网加载flag或校验的设定模拟器默认网络模式在某些NAT环境下访问外部受限或者App检测到非移动网络就直接拒绝请求。解决把模拟器的网络模式从NAT改成桥接让虚拟机直接走宿主机所在的局域网。Android Studio的AVD可以在启动配置里选择网络模式Genymotion在设置里也有对应选项。另外检查虚拟机内的DNS设置改成8.8.8.8很多时候能解决域名解析不了的问题。抓包工具我用Charles配合系统证书安装但要注意App可能做了ssl-pinning这就要上frida的ssl绕过脚本。5.3 现象hook代码没问题但frida脚本不生效函数没被调用原因最常见的原因是目标函数有多个重载Java层方法同名但参数类型不同。你用CheckUtil.checkFlag默认匹配的是无参或第一个重载实际被调用的可能是另一个带String参数的重载。另一个原因是类名不对APK里类名带了包名前缀或内部类标记$。解决用objection的list class_methods确认函数签名然后frida里用完整的重载写法CheckUtil.checkFlag.overload(java.lang.String).implementation function (input) { console.log(input); return this.checkFlag(input); };参数说明overload(java.lang.String)指定参数类型为String多个重载就要写多行或者直接遍历所有重载打印签名。类名含$的情况记得在Java.use里原样带上$符号。5.4 现象so文件拖进GDA或IDA看不到明显的校验函数全是乱码原因so文件可能被加固或混淆真正的JNI函数不是通过常规的Java_前缀导出。也可能是架构选错了APK里有armeabi-v7a和arm64-v8a两个目录你拖进IDA的是v7a但真机跑的是v8a。解决先看APK解压后lib目录下有哪些架构只有一种架构就只分析那一种即可但更推荐优先看arm64-v8a这是现在主流真机架构。其次看导出表用readelf -Ws libxxxx.so列出所有动态符号找Java_前缀的函数名。如果没有再考虑用frida的Module.enumerateExports打印so运行时真实导出情况以运行时的符号表为准。如果确认是加固不要在静态分析里死磕。动态调试时frida可以直接hookart::JNI相关的内部函数或者用frida-dexdump从内存dump出完整的dex。这个技术对脱壳特别有效几秒钟就能拿到解开后的dex再丢回jadx看。5.5 现象静态分析找到了flag字符串提交却显示错误flag格式完全不对原因题目可能把flag做了二次编码比如aes加密后的密文字符串存储静态看到的只是密文不是明文flag。也可能是flag不同部分的字符顺序被故意打乱需要按照代码逻辑还原顺序。解决看到类似{...}包裹的字符串别急着复制。先用jadx确认它有没有经过encrypt或decode处理往上追这个字符串的源头。常见做法是直接在frida里hook对应的解密函数让它计算出结果后打出来一锤定音。另外注意flag大小写和花括号是英文还是中文CTF里的flag严格区分大小写中文括号提交必错。5.6 现象运行App后弹窗“检测到模拟器”直接退出frida还没启动进程就没了原因题目故意做了环境检测常见特征包括检测Build.FINGERPRINT是否包含generic、检测ro.kernel.qemu是否有值、检测已知模拟器特征文件。这类题目就是考反调试对抗。解决如果静态分析能定位到检测逻辑直接hook掉对应的检测函数并强制返回正常值。比如检测函数返回booleanhook后改成固定返回false。如果检测点隐蔽、数量多用objection自带的android root disable --no-restart和android sslpinning disable能绕掉一部分标准的root和模拟器检测。再不行就换真机跑真机天然没有模拟器特征是最省心的后悔药。6. 收个尾把flag定位时间压进3分钟的frida脚本模板和一个习惯最后一章分享一个我反复用、现在基本3分钟内解决一道中低级题目的模板。它做的事情是自动搜索Java层所有包含check、verify、flag关键字的类与方法然后hook所有能hook的校验函数打印入参、返回值并在返回值变化时给出标记。Java.perform(function () { Java.enumerateLoadedClasses({ onMatch: function (className) { if (/flag|check|verify/.test(className)) { console.log([class] className); try { var target Java.use(className); var methods target.class.getDeclaredMethods(); methods.forEach(function (m) { console.log([method] m.toString()); }); } catch (e) {} } }, onComplete: function () { console.log([scan done]); } }); });这段脚本不直接拿flag但它能在30秒内把目标App里所有值得hook的位置全部列出来。拿到这个输出后你再精准hook那些返回类型是boolean或String的方法配合输入不同测试字符串观察返回值思路就清晰了。我一般会把这类模板脚本放在一个固定目录里按“枚举类、hook字符串函数、hook校验函数、dump内存字符串”四个用途分开存放遇到新题直接改包名就能用。CTF比赛时间永远是廉价的、试错成本是昂贵的把重复动作脚本化才是从“会逆向”走到“逆向得够快”的那道坎。希望你也能把这套流程跑顺遇到卡住的时候先停下来想想是不是选错了工具再决定硬刚还是绕路走。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进