
前几天在BUUCTF刷题re分类里看到一道叫FindIt的题下载下来是个APK我第一反应是愣了一下——逆向题还能考安卓后来想想这其实挺常见的APK本质就是安卓上的可执行程序逆向它和逆向ELF、EXE本质上是一回事只是入口和工具链不同。这道题难度不高但流程很完整从APK解包到JADX反编译从定位MainActivity到还原算法最后用Python脚本跑出flag非常适合刚接触安卓逆向的人练手。整道题做下来你会发现它不需要上IDA不需要动态调试甚至连模拟器都可以不装纯粹靠静态分析就能出答案。不过正是因为简单反而能帮你把APK逆向的标准步骤走一遍后面碰到加壳、混淆、Native层加密的题目时你才知道哪些环节是绕不开的。这篇文章我就按实际做题的顺序把每一步怎么想、怎么做、为什么这么做完整记录下来。1. 确认目标一个APK怎么被分到了逆向题里1.1 下载与文件识别看到APK不要急着双击从BUUCTF平台把题目附件下载下来文件名就是findit.apk。很多人拿到APK习惯性想安装到手机上但在逆向题里这个习惯得改一改。APK本质是一个ZIP压缩包里面装的是安卓的资源文件和Dalvik字节码你要分析的是它的内部构造而不是它的界面。我习惯先跑一下file命令确认文件类型file findit.apk输出一般是findit.apk: Android application package确认是APK之后可以再用unzip -l看一眼里面的文件结构。APK的标准结构大致是这样的classes.dex核心的Dalvik字节码Java层逻辑全在里面AndroidManifest.xml全局配置声明了Activity、权限、入口信息resources.arsc资源索引表res/各种资源文件图片、布局、字符串lib/可能存在的Native库.so文件这题没有在逆向题里classes.dex是分析的重点而AndroidManifest.xml则是找入口的地图。如果APK里出现了lib/armeabi-v7a之类的目录说明题目可能涉及Native层逻辑那就需要IDA出场了。FindIt这个APK我扫了一眼没有lib目录基本可以判断是纯Java层的简单题目。1.2 工具链选择这次只需要JADX就够了做安卓逆向工具链的选择直接决定效率。我习惯的优先级是这样JADX / JADX-GUI把classes.dex直接反编译成可读的Java代码是静态分析的首选工具支持图形界面操作直观apktool用来解包和重打包APK适合需要修改smali代码或者提取资源的场景Android Studio 模拟器用于动态调试、运行验证IDA Pro分析.so文件里的Native层汇编Frida运行时Hook适合动态验证和绕过检测这道题连lib目录都没有直接JADX就够了。JADX本质上是一个Java工具所以前提是本地已经配置好JDK。下载JADX之后Windows用户可以直接跑jadx-gui.batMac/Linux用户跑jadx-gui脚本打开GUI后把APK拖进去就行。JADX会做两件事一是把APK解包二是把classes.dex反编译成Java源码并自动建立索引。打开之后左侧是完整的目录树你可以像阅读一个普通Java工程一样浏览所有类文件。对FindIt这种没有加壳、没有混淆的题目这个过程非常顺畅基本不会出现反编译失败的情况。2. 打开APK找入口MainActivity的点击事件里藏着验证逻辑2.1 从AndroidManifest确定主入口JADX打开APK后左侧目录树第一项就是AndroidManifest.xml点开它能看到这个APK注册了哪些组件。找入口的逻辑很简单一个App启动时系统会去寻找sentivity MAIN和LAUNCHER的那个Activity也就是通常说的主Activity。FindIt的Manifest里主入口写得很清楚activity android:namecom.example.findit.MainActivity intent-filter action android:nameandroid.intent.action.MAIN/ category android:nameandroid.intent.category.LAUNCHER/ /intent-filter /activity所以目标锁定为com.example.findit.MainActivity。这个包名里直接带了findit基本不会找错。在JADX左侧树里展开com.example.findit包双击MainActivity就能看到完整的Java代码。这里有个小经验遇到MainActivity之后先不要急着读全部代码而是先找onCreate方法里有没有setOnClickListener之类的回调注册。因为这类让你输入flag然后验证的题目最后的判断逻辑几乎都写在按钮的点击事件回调里直接跳到那里效率最高。2.2 布局文件里的线索一个按钮和一个输入框MainActivity的onCreate方法里会有setContentView对应的布局文件是res/layout/activity_main.xml。JADX也能直接查看资源文件点开布局会发现里面就三个关键控件一个EditText用来输入flag一个Button触发验证一个TextView显示结果或提示这种结构在安卓逆向题里太经典了几乎可以闭着眼睛判断重点不在界面而在按钮的回调方法里写了什么比较逻辑。代码里也确实出现了findViewById和setOnClickListener点击事件里才是真正的验证流程。2.3 onClick方法验证逻辑的全貌把MainActivity的源码整理一下核心逻辑大概是这样的我对ClassName和变量名做了整理实际反编译出来的可能略有差别但逻辑一致public class MainActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText editText findViewById(R.id.edit); Button button findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { char[] enc new char[]{ c, i, ^, d, x, F, q, \\, f, p, \\, ^, \\, C, r, k, k, v, \\, c, i, ^, d, z }; String flag ; for (int i 0; i enc.length; i) { flag (char) (enc[i] 3); } String input editText.getText().toString(); if (input.equals(flag)) { Toast.makeText(MainActivity.this, Right!, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, Wrong!, Toast.LENGTH_SHORT).show(); } } }); } }看到这段代码题目的面目其实已经出来了程序内部有一组乱码字符enc循环里对每个字符的ASCII码加3拼出一个字符串然后拿它和输入框里的内容做比较。如果输入一致就提示Right!。到了这一步问题就简化成了把enc数组提取出来逐个字符ASCII码加3拼出来的那个字符串就是我们要找的flag。剩下的事情纯粹是体力活。3. 算法还原为什么3就是答案以及怎么快速识别这类变换3.1 先看循环逻辑类型转换背后是ASCII码运算很多人看到(char) (enc[i] 3)这行代码会愣一下其实拆开看就是三步enc[i]是char类型Java里char参与算术运算时会自动转成int也就是拿到的其实是这个字符的ASCII码在这个ASCII码上加3(char)把计算结果重新转回字符所以c 3就是99 3 102对应字符f。这个变换意味着什么说明当初构造这个数组的时候作者是把真正的flag逐字符取出来每个字符的ASCII码减3又存进数组里。现在我们要做的就是逆回去每个字符加3。这种先减后加的移位操作本质上是简单的凯撒加密只是作用于ASCII码而不是字母表。做题的时候遇到过很多次识别起来也很有规律如果代码里出现charArr[i] 数字、charArr[i] - 数字、charArr[i] ^ 数字之类的操作并且后面跟着String拼接和equals比较那基本就是出题人设置的加密变换直接用逆运算跑数组就行。3.2 手推前几个字符从乱码变回flag的过程为了确认判断我把数组前几个字符手算了一遍c的ASCII码是99加3是102也就是fi的ASCII码是105加3是108也就是l^的ASCII码是94加3是97也就是ad的ASCII码是100加3是103也就是gx的ASCII码是120加3是123也就是{前五个字符拼出来是什么flag{。看到这里基本可以确定思路正确后面的字符全部按同样规则处理。其实一个经验丰富的逆向选手到这里甚至不需要看完整个数组因为flag{开头已经强烈暗示了结果方向。不过我还是把完整数组都提取出来了后面直接交给Python处理。3.3 加密变换的常见家族偏移、异或、倒序、Base64先把FindIt这道题归个类它属于单字符静态变换里最简单的偏移家族。实际做题时你会发现安卓CTF题里常见的加密变换也就那几种识别它们有一套固定的套路变换类型典型代码特征逆向思路ASCII偏移(char) (arr[i] 3)或(char) (arr[i] - 5)对每个字符做相反的偏移单字节异或arr[i] ^ key对每个字符再异或同一个key倒序new StringBuilder(str).reverse()把数组顺序反过来Base64编码Base64.encodeToString用Base64解码分段重排多个字符串按索引交叉拼接按索引规则重新组合进制转换Integer.toHexString(c)把十六进制转回字符识别方法很简单先看循环体对字符做了什么运算再决定用哪种逆运算。如果代码里同时出现异或和偏移就先做反向偏移再做异或变量顺序调换一下就行。FindIt连异或都不是只有一个加3应该算逆向题里的签到难度了。4. 写脚本出flag别手算用Python3一次性跑完4.1 提取数组并写成Python列表数组提取是个细致活。JADX里看到的字符数组直接复制过来通常会带Java的语法比如单引号、逗号、空格。我习惯在编辑器里整理成Python列表需要注意两个坑数组里有转义字符\\在Python字符串里也要写成\\否则会被解释成换行或者别的控制字符字符数组元素全是charPython里用字符串表示即可后面用ord()取ASCII码整理好的Python脚本是这样的# 从JADX中提取的字符数组注意 \\ 在Python里代表一个反斜杠字符 enc [ c, i, ^, d, x, F, q, \\, f, p, \\, ^, \\, C, r, k, k, v, \\, c, i, ^, d, z ] flag .join(chr(ord(c) 3) for c in enc) print(flag)运行之后输出flag{It_is_a_Funny_flag}如果你手头的题目版本和我这边的数组一样那提交这个flag就能通过。就算版本不同思路也完全通用从JADX复制数组用ord(c) /- 偏移量或ord(c) ^ key跑一遍结果自然就出来了。这里我想多说一句看到flag里有个Funny的时候我忍不住笑了。很多CTF出题人喜欢在flag里玩梗这道题多少带着点你找到了就会觉得挺有趣的意思。4.2 在JADX里二次确认把flag填回去看是否匹配跑出flag{It_is_a_Funny_flag}之后我一般不会直接提交而是先在逻辑上做一次确认。把拿到的flag逐字符反过来减3看能不能得到原始的enc数组f(102) - 3 99 cl(108) - 3 105 ia(97) - 3 94 ^g(103) - 3 100 d对上了说明从enc到flag的映射完全正确这个flag在程序里是能通过验证的。这一步别省尤其当你用脚本跑出结果后最好用代码里的原始逻辑反向验证一下能排掉很多低级错误比如复制数组漏了元素、转义没处理对之类的问题。如果你手边有安卓模拟器也可以把APK拖进去安装在输入框里粘贴这个flag点按钮会弹出Right!的Toast提示。不过说实话这道题静态验证足够了模拟器不是必需品。4.3 提交flagBUUCTF平台操作与注意事项BUUCTF平台的提交框要求输入完整flag包含flag{}字样。直接把跑出来的完整字符串粘进去就行不需要去掉花括号也不需要在后面加空格。提交通过后会有提示这里有个小细节有时候平台会偶尔抽风出现网络连接异常或者提交超时这时候不要慌检查一下flag大小写是不是和跑出来的一致再重试一次。我遇到过几次因为复制粘贴时把I和l搞混导致提交失败的情况所以如果提交失败优先检查的是flag的字符准确性而不是题目本身。5. 从FindIt延伸安卓逆向入门的几个通用经验5.1 遇到安卓逆向题的优先分析路径做完FindIt我最大的感受是安卓逆向题其实有一条非常清晰的路径顺着走不会乱。第一步永远是静态分析。用JADX打开APK先看Manifest确定入口Activity再看onCreate和按钮回调把Java层逻辑理顺。大部分简单题到这一步就结束了根本不需要动态调试。第二步才是动态分析。如果JADX看到的东西不够清晰比如有System.loadLibrary加载了.so或者代码被混淆得看不出逻辑那就需要上IDA分析Native层或者用模拟器Frida做运行时Hook。动态调试适合验证猜测而不是漫无目的地乱试。第三步是善用搜索。JADX里可以全局搜索字符串比如flag、key、secret、right、wrong搜一下往往能直接定位到关键判断点。FindIt里那个Right!的Toast字符串就是我搜索时最先发现的线索。5.2 处理字符串提取和转义的经验这次真正让我觉得值得记录的经验是数组提取时的转义处理。Java字符数组里的反斜杠\\在源代码里表示一个反斜杠字符本身复制到Python里如果直接粘贴\\会被Python解释成一行末尾的续行符或者直接报错。我在最开始就碰了一次壁后来规范成用Python列表按行整理问题就消失了。另外如果数组特别长建议用脚本的列表推导式一次性处理不要手动去数这个字符在第几个位置。人手动数超过20个元素的数组大概率会出错。程序员能靠脚本解决的事情就不要靠眼睛硬扛。5.3 出题人常藏flag的位置扫一眼总没错安卓逆向题里flag不仅仅会出现在Java代码的字符数组里常见的藏匿位置还有这些AndroidManifest.xml有时候flag就写在meta-data标签里改个十六进制或Base64编码res/values/strings.xml直接把flag拆成多个字符串藏在资源里assets目录放一个文件内容经过简单编码BroadcastReceiver需要触发某个广播才能输出flagSharedPreferencesApp运行后把flag写入本地配置so库中的JNI函数通过exported function导出需要用IDA分析FindIt属于最简单的那种字符串数组直接躺在Java代码里。但你如果只盯着代码看遇到藏在资源里的题就会漏掉。我的习惯是JADX打开后先用资源浏览器把res/values/strings.xml和assets目录扫一遍确认没有可疑内容再进入Java源码分析。5.4 最后分享一点做题心得做逆向题尤其是安卓逆向题最重要的不是背工具菜单而是建立输入 - 处理 - 比较这个模型。任何验证型逆向题本质上都是在某个地方藏了一个期望的输入你的工作就是把这个期望值从代码、资源、内存里抠出来。FindIt这道题期望值就藏在那个乱码字符数组里通过一次偏移运算还原出来。做完这道题之后我又去BUUCTF刷了几道安卓逆向明显感觉心态不一样了。碰到APK先不慌按Manifest找入口、JADX看Java层、有so再上IDA、字符串搜索定位关键逻辑这套流程走大部分题能稳定解出。如果你想入门安卓逆向FindIt是一个很舒服的起点——它足够简单但又完整覆盖了逆向分析的每一个关键动作值得亲手做一遍。