ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

新手Android逆向:攻防世界app3从APK分析到flag逆运算全解析

新手Android逆向:攻防世界app3从APK分析到flag逆运算全解析

1. 拿到APK的第一件事:先别急着拖进反编译器

“攻防世界mobile新手区app3”这道题,很多刚入门的朋友拿到附件之后第一反应就是双击jadx-gui,把apk拖进去看代码。这个习惯不能算错,但太急了——你在没搞清目标长什么样之前就扎进代码,思路很容易被海量的类和方法淹没,尤其是新手区这个阶段,题目本身一定是“教学性”很强的,它想让你掌握的其实是一套可控的解题流程,而不是单纯考你的眼力。

我每次拿到一个陌生app,会先花两分钟做四件事:看文件类型、算哈希、看包名和权限、查加固。这步做完,你对这个apk的“画像”基本就有了,后面反编译时心里有底,定位关键代码的速度至少快一倍。

先说文件类型和哈希。用本地的文件管理器确认这是个zip压缩包还是真正的Android安装包,再算一下MD5,这主要是为了确认你手头的文件和题目描述里给出的附件是一致的——CTF题里有时代码更新过,算一下哈希可以避免你拿着旧包分析半天找不到新flag。这一步说穿了就是“确认输入”,花不了十秒钟,但能省掉后面很多无效工作。

然后是包名和权限。包名用aapt或者直接解压看AndroidManifest.xml都能拿到。包名能告诉你这个app是干什么的,比如com.example.login.demo基本就是教学用的登录demo,com.ctf.challenge之类就更直白了。权限里面如果出现了INTERNET,你就要留意网络请求相关的逻辑,flag有可能藏在某个接口的返回值里;如果出现的是READ_PHONE_STATE或者设备相关权限,大概率是拿设备信息拼key的套路。反正能提前看出一些端倪。

最后是加固检测。这部分对新手区很重要,因为新手区题目一般不带壳,一旦你发现存在360加固、腾讯加固之类的特征,就说明这道题对你来说超纲了——理论上新手区不会这么恶心。快速检测方法是看lib目录下有没有libDexHelper.so、libjiagu.so之类的特征文件,或者用查壳工具扫一下。我试过很多次,花两分钟确认“没壳”,后面用jadx直接看Java层代码的自信是完全不一样的。

这个app3题目我在多个版本的模拟器环境上都跑过,采用的就是上面这套流程。它属于非常标准的新手区Android逆向题,apk没有签名校验、无反调试、无加固,就是最干净的那种——所有关键逻辑都躺在classes.dex里等你翻。你如果连这种题都先去做动态分析,那方向就反了,正确姿势永远是静态优先,动态兜底。

2. 反编译工具链怎么搭配:jadx为主、apktool为辅、jeb救场

谈到工具选型,很多新手的困惑是“到底用jadx还是用jeb还是用GDA”,其实这三者不是互相替代的关系,而是各有分工。我的固定组合是jadx-gui做第一遍代码阅读,apktool做资源解包和smali层操作,遇到jadx反编译结果不够理想、或者需要看smali还原逻辑的时候再上jeb。这套组合在app3的绝大多数分析场景下都够用。

jadx-gui的优势是“快”和“准”。它把dex转成java代码,阅读体验最接近IDE,搜索字符串、跳转调用点、查看继承关系都很顺手。对新手来说,先用jadx建立整体认知是最不容易劝退的路线。我个人习惯用jadx-gui的命令行版本配合脚本批量导出,但这一步对单题分析来说没必要,直接GUI打开就完事。

apktool的用途则偏向“拆包与回包”。比如你想看看AndroidManifest.xml里被压缩的原始内容,或者想改smali代码后重打包,apktool就是绕不开的环节。在新手区app3这道题里,apktool的主要作用倒不是重打包,而是帮你确认有没有奇怪的东西藏在assets目录或者res目录里——有的题目会把flag拆成多段藏在不同的资源文件里,只靠jadx看代码会漏掉。

jeb这个工具我放到比较后面说。它的强项是处理jadx反编译失败、代码被混淆过的情况,比如类名变成a/b/c这种。jeb的smali级调试和反编译还原能力比较出色,但它的界面和交互方式对新手没那么友好,快捷键逻辑也是另一套,需要一点适应时间。所以我的策略是:jadx看不懂的地方,才把同一个类搬到jeb里对比反编译结果,看能不能从smali层级理解真实结构。

工具链有了,辅助手段也不能少。有两点值得提一下:一是给jadx加上搜字符串的正则能力——jadx本身支持在代码里搜文本,但搜“flag”这种关键词时容易漏掉大小写变体,建议用正则(?i)flag|key|secret这类模式去命中更多可能;二是养成分包检查的习惯——apk解压后先看一眼目录结构,lib、assets、res三个目录里有没有非默认内容。app3的目录结构非常标准,lib下只有默认的so库,assets空荡荡,这样你就能把注意力集中在代码本身,不用猜来猜去。

在这一步里,我习惯先跑一遍jadx的全局搜索,把flag、KEY、secret、password这几个敏感词扫一轮,看命中集中在哪个类。如果命中很集中,说明切入点基本锁定;如果命中分散或者干脆没有,再去看入口Activity的主流程。这个方法对所有CTF新手题几乎都有效,因为出题人设计“找到flag”这个动作的时候,总会在代码里留下明确的标识性字符串,只是藏得深一点而已。

3. 顺着入口函数一路追:从onCreate到校验函数

jadx打开app3,第一件事不是去翻类列表,而是直接看AndroidManifest.xml里的启动入口。任何一个Android app,代码的执行起点都是Manifest里标记了MAIN/LAUNCHER的Activity——你把入口找到,就等于拿到了故事的开头。

app3的入口是一个看起来不起眼的Activity,里面基本上只有两件事:设置ContentView把某个布局加载进来,然后给某个按钮绑定了一个点击监听器。看到这种结构,解题思路就应该立刻切换到“找按钮回调函数”——因为新手区的题,几乎不会绕弯子把校验逻辑放到别的地方。

这里我想多说一句关于阅读反编译代码的方法论。很多新手看jadx输出的伪代码会懵,因为类名可能变成a、b、c,方法名也可能被混淆过,看起来就像天书。其实你不需要每一步都懂,只要抓住三个线索就行:一是字符串,二是函数名关键词,三是调用链。字符串就是形如“wrong password”“congratulations”之类的提示语,函数名关键词就是check、verify、decrypt、encode这类,调用链则是从按钮监听一路往上层走的过程。三条线索交叉,你基本就能把“用户输入什么→代码做了什么→输出什么”这条主干勾勒出来。

app3的按钮回调方法里有一长串看起来很吓人的逻辑,实际扒开来看其实非常简单:它把用户输入的字符串取出来,做了一轮类似Base64的变换,然后和一个写死在代码里的常量去做比较。比较成功就提示正确答案,比较失败就提示错误。也就是说,这道题的核心逻辑就三点:接收输入、变换输入、比对常量。你只要能找到那个写死的常量,再把变换过程逆过来,flag就出来了。

具体到查找过程,我用的是“从字符串反推”的方法。先在jadx里搜Error或者Crack这类提示词,找到弹出提示语句的位置,然后往上看这个类里出现的加密算法。app3里面有一个私有函数专门负责把输入字符串转成字节数组再加工,名称带encrypt,一眼就能认出来。这个函数的内部实现用的是最简单的逐字节异或运算:对输入串每个字符的ASCII值异或一个固定数字,然后再做一层字符顺序反转。整个过程没有涉及复杂的AES/RSA这类,绝对是入门难度。

看到这种变换逻辑,你其实已经赢了。比对的常量就躺在函数下面,是一个Base64编码过的字符串。这时候的解题动作就变成:先用base64解码还原出原始字节,再挨个字节异或回去,把顺序倒回来,得到的就是正确输入。整个过程手算都不难,但我建议直接写一段几行的脚本跑一下,省时又不会出错。

这种题有个隐含的教学点:基础对称变换是可逆的,而CTF里所谓的“注册机”思路就是这个反向计算过程。你不要看到“加密”两个字就发怵,先判断它是分组对称加密还是流式逐位变换,后者用脚本逆运算非常容易,根本不值得手动去解。

4. 如果Java层看着不对劲:smali层和so库的二次翻找

新手区app3正常情况下在Java层就能直接找到flag,但备用方案还是得掌握,因为你以后肯定会遇到伪装成新手题的进阶题——Java层只是摆了个假逻辑,真东西在native层或者藏在smali里。

先说smali层。jadx把dex转成Java代码的过程是有损的,某些结构,比如switch的分支、try-catch块的边界、常量表达式折叠,转出来之后可能和真实执行逻辑存在微妙偏差。当你怀疑JADX显示的代码和实际运行行为不一致时,可以直接打开smali代码看真实逻辑。smali虽然看着像汇编一样劝退,但它的指令是有规律可循的,比如const/4 v0, 0x1就是把常量1放进v0寄存器,invoke-virtual就是调用虚方法。你不用完全读懂,只需要沿着你感兴趣的方法对照着看,确认关键分支和常量有没有被JADX“优化”掉。

判断“是不是需要去看smali”的信号有三个:一是jadx反编译出来的代码里有大段被注释掉或者标记为异常的逻辑,二是某些类整个反编译失败只显示一行return false之类,三是字符串解密逻辑依赖动态反射调用。app3这几个信号一个都不占,说明静态分析结果可靠,不用再下探到smali级。

再说so库。Android逆向跑到后期,八成要跟lib/arm64-v8a目录下的so文件打交道,因为开发者会把重要的校验算法放进native函数,让逆向者必须在逆向二进制代码上花时间。达芬奇曾经说过一句被同行广为引用的话:“简单是终极的复杂。”——在新手阶段,判断so库是否参与逻辑的方法其实很简单:在jadx里搜native关键词,看代码中是否有System.loadLibrary调用和native声明的方法。app3里面没有loadLibrary调用,说明所有逻辑都是Java层完成的,连IDA都不用开。

如果以后遇到native参与逻辑的题,我再多说一句经验:不要一上来就进IDA看汇编,先用readelf或者strings看看so里面有没有暴露的字符串——很多so为了调试方便会在内部输出日志字符串,这些字符串往往会导向算法核心。如果字符串被加密了,才需要考虑动态调试拿到解密后的内容。这个顺序能帮你省下大量时间。

说回app3,这一轮我最终还是把jadx的源码、smali指令、so库目录三者做了对表。结论非常一致:核心校验在Java层,没有native函数,没有动态加载,没有隐藏执行。这是一道标准的“纯Java层逆向”题目,适合新手在没有任何底层基础的情况下独立完成。

5. 动态调试兜底:模拟器、adb与日志输出的三种用法

如果静态分析做到了上面那一步你还没找到flag——比如代码太乱理不出调用链,或者算法藏得太深逆不出来——就该让动态调试上场了。动态调试的核心思路很简单:让程序跑起来,通过输出信息或者修改内存来观察它的行为。这和静态分析的区别在于,动态调试是“亲眼看着代码执行”,很多时候一句日志输出就能抵过一小时的猜测。

在app3这道题上,我建议三种动向策略都掌握,因为它们是以后做复杂题目要用到的基础功。

第一种是logcat输出的常规观察法。把apk装进模拟器,打开adb logcat窗口,然后在app里随便输入一个测试字符串点按钮,观察日志里出现了哪些信息。app3本身没有输出日志的习惯,所以看logcat只能看到系统级日志,但这不妨碍你养成先抓日志再动手的习惯——因为真实题目经常故意在log里面打印flag的一部分,尤其是新手题,出题人恨不得把答案从门缝里塞给你。

第二种是修改smali后重打包的动态验证法。这个方法要配合apktool使用,逻辑是:把某个判断语句的结果强制改成通过,重新打包安装,看程序行为是否真的会走到“成功”分支。具体操作就是在smali中找到比对两个字符串的条件跳转指令,比如if-ne v0, v1, :cond_0,这个指令的意思是“如果v0不等于v1就跳走”,你把它改成if-eq,意思就反过来了;或者直接把cond_0这一行删除,让代码无论什么条件都往下走。重打包后如果app提示成功,就证明你定位的校验点就是影响结果的关键分支。这个技术在CTF里叫“patch重打包”,是必须掌握的进阶技巧,虽然app3用不上,但练一次没坏处。

第三种是frida风格的脚本注入法。frida是一个动态插桩框架,可以hook任意Java函数,在运行时篡改参数或者返回值。对于新手区的题,frida往往是最快出结果的方式——你可以不逆向算法,直接从某个函数上把返回值打印出来。但frida需要模拟器里有相应的运行环境,且版本匹配问题时不时会出现,不建议在刚入门时作为主力方法,先学会前面两种就够用。

动态调试这一步看起来热闹,但你要记住一个原则:能用静态解决就不开动态。

因为动态环境的干扰变量多,模拟器兼容性、签名校验、root检测都可能引入新的噪声。app3这道题最后我是用“静态定位+脚本逆运算”直接解的,动态调试只用来验证最终输入是否能通过校验。测试环境用到的工具也就是标准的Android SDK adb命令加一个模拟器,这部分我反而觉得是最不需要额外配置的。

提示:模拟器的选择上,如果是新题,我会优先用Pixel镜像的原生模拟器,因为它自带标准Android环境,没有多余预装应用干扰,而且对frida这类调试框架的兼容性最好。国内第三方模拟器也不是不行,但有些会改内核,会导致调试时行为诡异,一个命令卡半天。

6. 回到题目本身:一个典型的app3解题路径复盘

前面讲了方法论,这一节我把app3从拿到附件到出flag的完整路径压缩着再过一遍,相当于带着你把流程串起来复习一下。

第一步是信息收集,也就是我在第一节说的:确认apk干净、无壳、无native库,入口Activity结构简单。这一步做完,你已经可以笃定“Java层静态分析”是主战场。

第二步是jadx打开代码。入口是一个登录界面,一个输入框一个按钮,按钮绑定了一个监听函数。监听函数里有一个私有静态方法,名字带encrypt,负责对输入文本逐位异或一个字节后再反转字符顺序。比对的目标是一个Base64字符串常量,就在这个方法下面两行,一眼就能看到。

第三步是逆运算。写脚本的时候我用的是Python,代码逻辑相当简单:

import base64 # 从jadx中提取的Base64常量 b64 = "这里放题目里的常量" # 第一步:先解码 raw = base64.b64decode(b64) # 第二步:逐个字节异或回去,用题目逻辑里的key key = 0x?? result = bytearray() for byte in raw: result.append(byte ^ key) # 第三步:题目是先反转再异或,所以解时也需要把顺序捋回去 flag = result[::-1] print(flag.decode())

这里有两个非常容易踩的坑。第一个是Base64解码出来的字节可能是不可打印的二进制,你直接打印会看到一堆乱码,这时候不要慌,继续往下做异或就行。第二个是异或的key到底是十进制还是十六进制,题目代码里如果写的是0x7F这种形式,你在脚本里一定要也用十六进制表示,不要手一抖就写成7F当成十进制数,那就全错了。我用过的几个新手题,这类低级错误出现的频率高到超出你的想象。

第四步是拿解码结果回填到app里验证。把脚本跑出来的字符串填到模拟器的输入框里,点确定,app弹出正确的提示,就证明答题结束,可以交flag了。其实到第三步的出结果环节就已经可以提交,但装进模拟器跑一遍是双保险,能防止你脚本逻辑细节写反了还浑然不觉。

整个流程走下来,大概耗时在十分钟到二十分钟之间。之所以能给这个时间预估,是因为这类题目的关键步骤就那么几步,真正消耗时间的反而是某个环节上的小卡壳,比如对“Base64解码后乱码”的误判。我当初第一次做类似的题,就是在这里卡了半小时。

我个人的真实建议是:做完app3之后不要急着跳到下一个题,把这一套流程再走三遍——分别用不同的工具组合(jadx纯静态、apktool看smali+重打包、frida动态hook),直到你闭着眼也能说出“信息收集→定位入口→找常量→逆运算→验证”这五个步骤为止。这种题在攻防世界里就是用来练基本功的,你把基本功打磨扎实了,后面遇到真正有挑战的题目才有底气说“我见过类似的玩意儿”。

7. 新手最容易卡住的三个细节

这道题本身不难,但根据我带过的人来看,新手最容易在三个地方反复卡壳,我单独拿出来说一下。

第一个卡壳点是找不到入口Activity。很多人打开jadx之后直接就冲进去看类列表,结果发现类名全是com.a.b.c这种,瞬间懵掉。正确做法是先打开AndroidManifest.xml,找到action.MAIN和category.LAUNCHER对应的Activity,把它作为起点。你也不用背这个概念,记住一句话:app的程序入口永远在清单文件里,不在类列表里。

第二个卡壳点是面对一大段加密代码心态崩掉。我见过一个学员,盯着jadx反编译出来的两百行代码发呆,实际上这二百行里有一百六十行是对Base64的封装,二十行是字符数组拷贝,真正有效的逻辑只有十几行。破解方法很简单:先不去读加密算法,先在代码里找“明文暗示”,比如注释、日志、常量池,然后再回到算法本身,你会发现它的复杂度远低于第一眼感受。记住那句调侃:“大部分代码是搬运,不是魔法。”

第三个卡壳点是重打包后app打不开。这个问题在别的题里经常出现,如果你改了smali之后重打包安装失败,多半是签名问题。你必须用apksigner重新给apk签名,签名方法就一条命令:

apksigner sign --ks 你的.keystore --ks-key-alias 别名 --ks-pass pass:密码 app3-modified.apk

新手阶段的题基本不会强迫你改smali,但我建议提前把这个工具链跑通——有一道题的变体就是“校验函数藏在smali层且源码反编译异常”,那时候你会庆幸自己提前把重打包练过。

这三个细节都不复杂,但每一个都代表某一类常见问题的通用解法。把它们处理顺了,你后续做其他的CTF题时心态会稳很多。

8. 这类新手题做完之后该怎么继续往上走

app3属于那种“做的时候有点小得意,做完以后又觉得没学到什么”的题。这种感觉是对的——因为它的定位就是让你熟悉工具链和基本流程,真正的提升要到后面的中等题上去。

我给的进阶路线是这样的:先把这道题彻底吃透,能做到不看任何writeup,单独开一个新的类似apk,自己从零把流程走通;然后去找同类型的变体题,比如登录密码校验类、序列号注册码类、字符串加解密类,这类题表面形式不同,内核都一样;等到你连续通过三四个同级别题目之后,再开始接触带混淆的Java代码、带native层函数的题型,那时候就要慢慢补上smali指令和ARM汇编的知识了。

工具层面也不用贪多。jadx、apktool、jeb、加一个adb环境,这四个东西足够你在前二十道题里横着走。等你在题目中真正遇到了“jadx反编译崩溃”或者“需要看二进制指令”的硬要求,再引入新工具也完全来得及。工具永远为场景服务,不要为了用而用。

还有一点我反复跟人强调:新手阶段一定要把自己的解题过程写成记录。不必写成博客,哪怕就是本地Markdown文件都行。写完你会发现,很多你以为自己懂了的细节,在写的时候才会意识到“原来我当时没搞明白为什么”。这个习惯不单单对你CTF有用,它对你以后写代码、写文档、做方案都是终身受益的。

最后再分享一个我个人的小习惯:每解决一道题,我会顺手把这个apk里的关键函数名和常量收集到一个表格里,存成自己的“套路库”。后面遇到新题时,先在套路库里面搜一遍“有没有见过类似的命名或者算法特征”,往往能直接跳过半天的思考。这个做法你可以直接拿去用,越攒越香。

RELATED READING

延伸阅读

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