2014 年,我在第一家公司接到了一个非常规的项目:对一款 Android APK 做安全评估。说"非常规",是因为那时部门的项目周期动辄两三个月到两三年,而这个只有一周。领导把任务交给我,大概是觉得我平时喜欢折腾各种新东西, 当时业余时间在研究树莓派。
背景
客户是一家制造电梯的公司,他们有一套电梯维保工单系统,安装在配发给维修工程师的 Android 平板上。工程师通过这个 APK 查看维护保养订单、处理故障报修。这个 APK 由另一家第三方公司(vendor-A)开发。
客户委托我所在的公司(vendor-H)对这个 APK 做安全评估,核心问题是:这个 APK 能不能被破解?里面内置的密钥等敏感数据能不能被提取出来?
这本质上是一次白盒逆向测试,只是用了"安全评估"这个更正式的说法。
第一次评估:一周内出报告
拿到 APK 之后,我先去查了 Android APK 逆向的相关资料。
APK 文件本质上是一个 zip 包,解压后可以看到 classes.dex——这是 Android Dalvik 虚拟机的字节码文件,而不是标准 Java 的 .class 文件。逆向的第一步是用 dex2jar 把 classes.dex 转换成普通的 jar 包,再用 jd-gui 等 Java 反编译工具将 jar 里的 .class 文件反编译成可读的 Java 源码。具体流程可以参考我当时整理的 Android APK 反编译。
第一版 APK 虽然有一些保护措施——把部分逻辑封装成单独的 jar 包作为依赖,密钥也不是直接明文写死,而是在运行时动态加载/生成——但通过反编译拿到全部源码之后,这些都可以被分析清楚。我新建了一个 Java 工程,把 APK 里的密钥加载逻辑复制进去单独执行,成功还原出了被保护的密钥。
此外,代码里也存在硬编码的敏感信息。
第一次评估报告在一周内交付给了客户,列出了几个可被利用的安全漏洞。
第二次评估:加壳技术
几个月后,客户再次找到我们,带来了 vendor-A 升级后的新版 APK。
拿到手之后能明显感觉到保护措施升级了。推测是 vendor-A 根据第一次评估报告做了针对性加固。新版 APK 采用了一种类似病毒为躲避查杀而使用的加壳技术。关于 Android APK 加壳的具体实现原理可以参考这篇文章:APK 本身携带了一个加密的核心 dex 包,安装后核心逻辑并不直接以可执行形式存在于硬盘。只有在运行时输入正确的注册码,APK 才会触发解密流程,把核心逻辑还原成 dex 文件,再通过 DexClassLoader 动态加载执行。
这个防护的安全性依赖于 Android 文件系统的权限隔离:在非 root 的设备上,其他进程无法访问 APK 运行时还原出来的临时文件,因此无法在运行时截取解密后的 dex。而一旦设备有 root 权限,这道屏障就不存在了——攻击者可以在 APK 运行时直接读取已还原的 class 文件,完成逆向。
第二次评估的结论是:在没有 root 权限的设备上,这个 APK 的安全防护是充分的。
最终解决方案
客户针对第二次报告给出的处置方案是行政控制:平板是公司统一配发给维修工程师的,从 IT 管理层面可以确保设备不被 root,从制度上堵住了这个风险口。
延伸思考
写到这里想到一个问题:APK 是否可以在启动时主动检测设备是否已经 root,如果检测到就直接拒绝运行?这在技术上是可行的。Android 上有一些常用的 root 检测手段,比如检查 su 二进制文件是否存在、检查常见 root 管理工具的包名、尝试执行特权命令等。不过这类检测并不是无懈可击——有经验的攻击者可以通过 hook 或伪装绕过检测。所以 root 检测更多是增加攻击门槛,而不能作为唯一的防线。结合注册码机制和行政管控,才是更完整的纵深防御。
事后回顾
这个项目是我在完全没有 Android 开发经验的情况下接手的。能做下来,一方面是因为 Android APK 的底层是 Java,而我有多年 Java 开发背景;另一方面,逆向工程本身的思路——解包、反编译、分析逻辑——在技术上是清晰的,陌生的只是 Android 特有的 dex 格式和 ClassLoader 机制,这些查资料可以快速补上。
两次评估前后,对方的安全防护从基本可以被轻易突破,到达到了一个可接受的水平。某种程度上,这也算是通过两轮攻防对话推动了 APK 安全性的实质提升。