1. Android 开发基础
1.1 Android 四大组件
- Activity:面向用户的应用组件或用户操作的可视化界面,由ActivityManager统一管理;也负责处理应用内或应用间发的Intent消息
- Broadcast Receiver:接受并过滤广播消息
- Service:处理后台耗时逻辑
- Content Provider:应用程序间数据共享
1.2 APK 文件结构
meta-inf
- manifest.mf:清单文件
- cert.rsa:应用签名
- cert.sf:资源列表及对应的SHA-1签名
lib
- armeabi:所有arm处理器相关文件
- armeabi-v7a:armv7以上处理器相关文件
- arm64-v8a:所有armv8处理器下的arm64相关文件
- x86:所有x86处理器相关文件
- x86_64:所有x86_64处理器相关文件
- mips:MIPS处理器相关文件
res:没有编译至resource.arsc中的其他资源文件
assets:可以通过AssetMannager访问到的资源文件
AndroidManifest.xml:安卓组件清单文件,以二进制形式存储在apk中,可以通过apktool、AXMLPrinter2等工具转换成明文文件
classed.dex:安卓运行时的可执行文件
resources.arsc:包含编译好的部分资源文件
2. APK 逆向工具
先做一个简单罗列,之后针对题目进行练习的时候再进行详细的补充~
2.1 JEB
支持Android APK反编译,还支持MIPS、ARM、ARM64、x86、x86-64、WebAssembly、EVM等反编译
2.2 IDA
遇到Native(本地服务)逆向是,IDA优于JEB等其他逆向工具
2.3 Xposed Hook
在root设备下可以不修改源码的情况下影响程序运行的Android Hook
2.4 Frida Hook
跨平台Hook框架,支持IOS、Android
3. APK逆向 - 反调试
为了保护应用关键代码,开发者会采用一些技术增加关键代码逆向的难度。调试技术是逆向人员理解代码的重要手段,对此开发者衍生出很多“反调试技术”。Android下的反调试技术大多从Windows平台衍生而来。
- 检测调试器特征
- 检测调试器端口,如IDA调试默认占用23946端口
- 检测常用调试器进程明,如android_server、gdbserver等
- 检测/proc/pid/status、/proc/pid/task/pid/status 下的 Tracepid 是否为0
- 检测/proc/pid/stat、/proc/pid/task/pid/stat 的第二个字段是否为t
- 检测/proc/pid/wchan、/proc/pid/task/pid/wchan 是否为 trace_stop
- 检测自身进程运行状态
- 检测父进程是否为 zygote
- 利用系统自带检测函数 android.os.Debug.isDebuggerConnected
- 检测自身是否被 ptrace
- 检测自身代码是否包含软件断点
- 主动发出异常信号并捕获,如果没有被正常接收说明被调试器捕获
- 检测某段程序代码运行时间是否超过预期
攻击者绕过上述检测方法最便捷的是定制 Android ROM,从Android源码层面隐藏调试器特征。比如通过 ptrace函数检测是否被 ptrace时,可以修改源码让ptrace函数永远返回非调试状态等。
4. APK逆向 - 脱壳
在软件开发和逆向工程中,“壳”是指一种 软件保护技术,通常用于保护应用程序的代码和资源,防止被未经授权地分析、修改或使用。壳通常通过加密、混淆或动态加载等手段来隐藏应用的核心逻辑,使逆向工程变得更加困难。
4.1 注入进程Dump内存
首先使用adb命令获取应用包名,再启动目标应用,确保应用的壳加载解密过程完成
1 | adb shell pm list packages | grep targetapp |
找到目标进程,获取其pid
1 | adb shell ps | grep targetapp |
使用Frida注入进程
1 | frida -U -n com.targetapp # 通过 USB 或网络连接 Frida 到设备 |
编写dump脚本,用于 Hook DEX 加载函数并提取内存中的数据(以下为一个简单例子)
1 | var Module = Process.getModuleByName("libart.so"); // 获取 ART 虚拟机的模块 |
操作目标应用的关键功能,使其加载核心 DEX 文件。此时,脚本会捕获到 DEX 文件的解密内容,并自动将其保存到设备的 /data/local/tmp/ 路径下。
导出dump的文件(DEX),并对其反编译
1 | adb pull /data/local/tmp/dumped.dex . # 使用 ADB 将导出的 DEX 文件拉到本地 |
🙋:为什么内存中会存储解密后的代码(DEX文件)?
在 Android 应用的运行过程中,应用加固壳会将加密的代码(通常是 DEX 文件)在启动时或使用到某些功能时解密到内存中,然后由 Android 的 ART/Dalvik 虚拟机执行。
因为运行时解密是必须的。加壳的目的是保护代码,使其在静态分析时不可读取。但在运行时,虚拟机需要能读取和解析解密后的 DEX 文件才能执行,否则应用无法正常运行。
4.2 修改源码脱壳
使用 apktool 解包 APK 文件,查找壳的代码文件(壳代码通常在 smali 文件夹中,如 smali_classes2 或其他 obfuscation 的命名目录下),用 JADX 查看壳逻辑。
1 | apktool d target.apk -o target_apk |
根据壳中的关键代码,用 Smali 替换相关逻辑。例如有如下反调试代码:
1 | if (Debug.isDebuggerConnected() || Debug.waitingForDebugger()) { |
可以编写如下smali代码
1 | .line 25 |
壳逻辑可能会在 DEX 文件解密加载后清理内存,常见代码如下:
1 | Arrays.fill(decryptedDex, (byte) 0); // 用 0 覆盖解密后的 DEX |
我们可以修改Smali代码,保持解密后的数据完整
1 | .line 50 |
最后重新打包和签名
1 | apktool b target_apk -o modified_target.apk # 使用 apktool 重新打包修改后的 APK |
🙋:Smali是什么,如何通过Smali代码来修改app源代码?
Smali 是 Android 的字节码语言,修改 Smali 文件后生成的 DEX 文件会被加载到内存中,因此最终效果是间接“修改了内存中的代码”。通过对 APK 的反编译、修改、重编译和重新打包的过程,可以
间接改变运行时的代码逻辑
。
4.3 类重载和DEX重组
对于不连续壳,在内存中不存在完整的DEX文件,此时不能通过Dump内存来完成完整脱壳,需要在运行时对DEX进行重建。
推荐使用FUPK3(是在Android4.4下通过修改源码实现的DEX重组方式)
- 动态监控 DEX 加载:
- 修改 ART/Dalvik 虚拟机的源码,在每次加载 DEX 文件的碎片时拦截数据。
- 记录每一段 DEX 数据的内存地址和大小。
- 数据拼接:
- 收集所有的 DEX 碎片,按照 DEX 文件格式重新拼接。
- 将拼接后的完整 DEX 文件写入到文件系统。
- 自动化:
- 修改虚拟机源码后,所有运行的应用都会被自动监控,不需要手动 Hook 或注入。