APK
APK(Android Package)是 Android 应用程序的安装包格式,本质上是一个 ZIP 压缩包,包含编译后的代码、资源、清单文件等
文件结构
使用解压工具打开 APK,可看到标准目录结构
1 | APK/ |
AndroidManifest.xml:声明权限、Activity、Service、Receiver、Provider 等组件classes.dex:Dalvik 字节码,包含所有 Java/Kotlin 代码编译结果lib/:armeabi-v7a, arm64-v8a, x86 等原生.so库,通过 JNI 调用META-INF/:签名文件,用于完整性校验
实战案例
简单的 APK 分析(签到)

解包 APK 得到 multiprotocol.tesla.scada 文件
反编译该文件通过对比正版软件,很容易得到 flag(为混淆前的类名)

bang(梆梆壳 + Frida 动态脱壳 + Jadx 反编译)

使用 APKiD 识别出加固过,用的是 SecShell/梆梆壳
[*] signed.apk!classes.dex*:APKiD 开始扫描 APK 包中的classes.dex文件(即 Java/Kotlin 字节码的核心文件)。末尾的*表示该文件在 APK 中处于被压缩状态,APKiD 会在内存中实时解压并扫描*|-> anti_vm : possible ro.secure check:在 DEX 字节码中检测到了反虚拟机(Anti-VM)特征,具体表现为检查系统属性ro.securero.secure:Android 系统的只读属性。在生产环境(真机)中通常为 1
*|-> compiler : dexlib 2.x:检测到 DEX 文件的编译器版本标记为dexlib 2.x[*] signed.apk!lib/arm64-v8a/libSecShell.so:libSecShell.so是百度旗下的 “SecShell” 加固壳(俗称 “百度加固”)的标志性文件名|-> anti_hook : syscalls:在该 SO 库中检测到了反 Hook 机制,具体手段为直接使用系统调用

这里我使用的是 Genymotion 模拟器,安卓版本是 8.1,其自带 adb.exe
1 | # 列出设备 |

1 | # 获取 Android 系统版本 |

将 APK 拖进去运行

然后查看对应的进程
1 | adb shell ps | findstr how |

先查壳有没有释放 dex
1 | # /data/data:Android 系统规定的所有应用程序私有数据的根目录 |

把缓存文件拉回电脑
1 | mkdir dump_cache |

结果发现都是被混淆加密的文件

使用 frida 工具,提前下载好 frida-server-17.17.0-android-x86.xz,解压后把它改名成 fs 放到附件目录中
1 | # adb push:电脑 → 手机的传输命令 |
运行后需要开另一个终端看能不能连接上
1 | # 测试列出进程 |

其中 2458 how_debug 是我们的 APK
1 | # frida-dexdump:基于 Frida 的动态脱壳工具 |
成功 dump 两个 dex,分别是 classes.dex 和 classes2.dex

使用 Jadx 反编译拿到 flag

vulcrack(APK 360 加固 + GDA 脱壳)

APKID 识别出 360 加固保以及还开启了 ProGuard 混淆
1 | packer : Jiagu |

解压缩文件查看 assets/cmd.txt,内容是 hfns?qozt<ihxKrhgGkuwyY{eq=jiyLsahUhgttkSugs
将给定的字节串中的每个字节,减去其所在位置索引对 8 取模的值(即 i % 8),生成一个新的字节串,最后以 UTF‑8 编码解码并打印
1 | data = b"hfns?qozt<ihxKrhgGkuwyY{eq=jiyLsahUhgttkSugs" |
得到 help;list;getFlagFirstStep;getFlagSecondStep
这一步非常关键,它告诉你真实代码里大概率有这几个命令
1 | help |
首先,我们按下 Dump 按钮启动程序 GDA Device Dumper,然后点击 Install APK 安装 APK 文件,并按下按钮在设备上运行(需要 root 权限)。之后,右键单击 “刷新” 菜单刷新软件包列表,我们将看到 PID 为 的新软件包 5165
双击进程(软件包名称)即可查看该进程的内存空间分布

接下来,点击单选 REF 按钮显示与进程名称最相关的模块(如下图所示)。oat 文件实际上包含原始应用程序的 DEX(如果您不确定,可以导出所有模块并进行检查,有时原始 DEX 会存在于堆中,我们稍后会讨论这一点)

我们需要完整地导出模块。怎么做呢?例如,导出模块时 classes.oat module,首先用鼠标左键点击模块的第一行 classes.oat,然后拖动到模块的最后一行 classes.oat module,右键单击 Dump Module 并稍等片刻,对话框就会弹出

单击 OK 查看转储文件,如图所示

转储的文件是一个 OAT 文件,解密的 DEX 文件(实际上 DEX 头部的魔法信息被擦除了,GDA 在解析 OAT 时会自动修复它)就在这个文件中

在这里,修改文件后缀并将 .oat 其拖到 GDA 中以反编译解密的 dex 文件(如果要将其转换为 DEX,可以使用菜单工具 > oat2dex 将其转换为 dex)

经过初步检查,我们很快找到了可疑的 Flag 类,它很可能就是答案。该类中有两个关键函数:calcFlagFirstStep() 和 calcFlagSecondStep(),它们似乎通过 Flag.keyFirst 和 Flag.keySecond 来计算 flag
这两个函数(或其返回值)经过 Base64 解码后,会传递给 Flag.comm() 方法。双击 Flag.comm() 查看详情

如果您不想 try-catch 在这里观看,可以右键单击将其禁用,如图所示

此功能由 GDA 的灵活结构化算法实现。尤其是在嵌套结构较多的情况下 try-catch,我们通常不需要处理异常,因此关闭此功能 try-catch 可以大大简化代码的复杂度
这段代码更清晰。首先对两个 flag 进行 Base64 解码,然后进行解密 Flag.comm(),最后连接成我们需要的 flag

在这里,我们使用 GDA Encryption&Decryption 来解码两个标志,然后使用 GDA java plugin 来解密和连接
首先,打开该 GDA Encryption &Decryption 工具 menu>tools>ALG Tool,选择 Base64 快速解码这两个字符串


获取到的两个解码后字符串如下:
第一个解码结果:
fmcj.97;A7G462H;2F/7<I9496EH1,对应的 Base64 编码(方括号内)为:[Zm1jan85NztBN0c0NjJIOzJGLzc8STk0OTZFSDE=]第二个解码结果:
A:HI9E4I1E6<}对应的 Base64 编码为:[QTpISTlFNEkxRTY8fQ==]
下来对 Flag.comm() 的处理步骤:
将
Flag.comm()方法的实现代码直接复制到您的脚本文件中从这里下载 GDA 的 java 插件案例
建议:直接在 GDA 示例目录(example 文件夹)下创建新的 Java 文件,并注意 Java 类名必须与文件名完全相同
完成上述准备后,按照官方提供的调用方式,在脚本中调用
comm()方法即可
1 |
|

最后,点击 Menu>file>Run Java/Class 运行 .java/.class 文件,Flag 将直接打印在 GDA 上,如图所示

java(APK + Base64 + AES + XOR)

jadx 反编译,在 Congratulations 之前调用了 c() 类的 a() 方法

c 类里有两个关键静态字符串以及一个整数数组
1 | public static String a = "aes_check_key!@#"; |
反编译代码中的 android.support.v7.a.a.j.AppCompatTheme_textColorAlertDialogListItem 是 jadx 把某个整数常量显示成了 Android 支持库里的资源常量名

在这里面呢又是调用了 e.a() 方法

查看这个 e 类明显发现是 AES-ECB 加密

再去看这个 d 类,每个字节异或 22,然后每个字节再异或 c[i]

最后返回与 Base64 编码的字符串做比较
1 | return new String(b.a(bArrA)).equals(b); |

同时,APP 启动时会被 App 类修改 key,所以真正的 key 是 aos_chock_koy!@#

1 | import base64 |
得到 flag:flag{67587AAF-C20A-4B6D-991B-A40FD3C2098E}