1. Android 开发基础

1.1 Android 四大组件

  1. Activity:面向用户的应用组件或用户操作的可视化界面,由ActivityManager统一管理;也负责处理应用内或应用间发的Intent消息
  2. Broadcast Receiver:接受并过滤广播消息
  3. Service:处理后台耗时逻辑
  4. Content Provider:应用程序间数据共享

1.2 APK 文件结构

  1. meta-inf

    • manifest.mf:清单文件
    • cert.rsa:应用签名
    • cert.sf:资源列表及对应的SHA-1签名
  2. lib

    • armeabi:所有arm处理器相关文件
    • armeabi-v7a:armv7以上处理器相关文件
    • arm64-v8a:所有armv8处理器下的arm64相关文件
    • x86:所有x86处理器相关文件
    • x86_64:所有x86_64处理器相关文件
    • mips:MIPS处理器相关文件
  3. res:没有编译至resource.arsc中的其他资源文件

  4. assets:可以通过AssetMannager访问到的资源文件

  5. AndroidManifest.xml:安卓组件清单文件,以二进制形式存储在apk中,可以通过apktool、AXMLPrinter2等工具转换成明文文件

  6. classed.dex:安卓运行时的可执行文件

  7. 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平台衍生而来。

  1. 检测调试器特征
    • 检测调试器端口,如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
  2. 检测自身进程运行状态
    • 检测父进程是否为 zygote
    • 利用系统自带检测函数 android.os.Debug.isDebuggerConnected
    • 检测自身是否被 ptrace
    • 检测自身代码是否包含软件断点
    • 主动发出异常信号并捕获,如果没有被正常接收说明被调试器捕获
    • 检测某段程序代码运行时间是否超过预期

攻击者绕过上述检测方法最便捷的是定制 Android ROM,从Android源码层面隐藏调试器特征。比如通过 ptrace函数检测是否被 ptrace时,可以修改源码让ptrace函数永远返回非调试状态等。

4. APK逆向 - 脱壳

在软件开发和逆向工程中,“壳”是指一种 软件保护技术,通常用于保护应用程序的代码和资源,防止被未经授权地分析、修改或使用。壳通常通过加密、混淆或动态加载等手段来隐藏应用的核心逻辑,使逆向工程变得更加困难。

4.1 注入进程Dump内存

首先使用adb命令获取应用包名,再启动目标应用,确保应用的壳加载解密过程完成

1
2
adb shell pm list packages | grep targetapp
adb shell monkey -p com.targetapp -c android.intent.category.LAUNCHER 1

找到目标进程,获取其pid

1
adb shell ps | grep targetapp

使用Frida注入进程

1
2
frida -U -n com.targetapp  # 通过 USB 或网络连接 Frida 到设备
frida -U -n com.targetapp -s dumpdex.js # 执行注入脚本(脚本内容见下)

编写dump脚本,用于 Hook DEX 加载函数并提取内存中的数据(以下为一个简单例子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Module = Process.getModuleByName("libart.so"); // 获取 ART 虚拟机的模块
var addr = Module.findExportByName("art::DexFile::OpenMemory"); // 查找函数地址

Interceptor.attach(addr, {
onEnter: function (args) {
var dexAddr = args[1]; // DEX 文件的内存地址
var dexSize = args[2].toInt32(); // DEX 文件的大小
console.log("Dex Dump Addr: " + dexAddr + ", Size: " + dexSize);

// 读取内存数据
var dex = Memory.readByteArray(dexAddr, dexSize);

// 保存到设备上的文件
var file = new File("/data/local/tmp/dumped.dex", "wb");
file.write(dex);
file.close();
}
});

操作目标应用的关键功能,使其加载核心 DEX 文件。此时,脚本会捕获到 DEX 文件的解密内容,并自动将其保存到设备的 /data/local/tmp/ 路径下。

导出dump的文件(DEX),并对其反编译

1
2
3
adb pull /data/local/tmp/dumped.dex .  # 使用 ADB 将导出的 DEX 文件拉到本地
dexdump dumped.dex # 使用工具如 dexdump 检查文件是否有效
jadx-gui dumped.dex # 使用 JADX GUI 打开提取的 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
2
3
if (Debug.isDebuggerConnected() || Debug.waitingForDebugger()) {
System.exit(0); // 检测到调试器,直接退出
}

可以编写如下smali代码

1
2
3
4
5
6
7
.line 25
invoke-static {}, Landroid/os/Debug;->isDebuggerConnected()Z
move-result v0

if-nez v0, :exit // 原逻辑:检测到调试器时退出
// 修改为:
const/4 v0, 0x0 // 强制将结果设为 "未检测"

壳逻辑可能会在 DEX 文件解密加载后清理内存,常见代码如下:

1
Arrays.fill(decryptedDex, (byte) 0); // 用 0 覆盖解密后的 DEX

我们可以修改Smali代码,保持解密后的数据完整

1
2
3
4
5
.line 50
invoke-static {v0, 0x0}, Ljava/util/Arrays;->fill([BB)V
// 修改为:
# invoke-static {v0, 0x0}, Ljava/util/Arrays;->fill([BB)V
# 注释掉销毁代码

最后重新打包和签名

1
2
3
apktool b target_apk -o modified_target.apk  # 使用 apktool 重新打包修改后的 APK
jarsigner -verbose -keystore my-release-key.keystore modified_target.apk mykey # 签名
adb install -r modified_target.apk # 安装到设备进行测试

🙋:Smali是什么,如何通过Smali代码来修改app源代码?

Smali 是 Android 的字节码语言,修改 Smali 文件后生成的 DEX 文件会被加载到内存中,因此最终效果是间接“修改了内存中的代码”。通过对 APK 的反编译、修改、重编译和重新打包的过程,可以间接改变运行时的代码逻辑

4.3 类重载和DEX重组

对于不连续壳,在内存中不存在完整的DEX文件,此时不能通过Dump内存来完成完整脱壳,需要在运行时对DEX进行重建。

推荐使用FUPK3(是在Android4.4下通过修改源码实现的DEX重组方式)

  1. 动态监控 DEX 加载
  • 修改 ART/Dalvik 虚拟机的源码,在每次加载 DEX 文件的碎片时拦截数据。
  • 记录每一段 DEX 数据的内存地址和大小。
  1. 数据拼接
  • 收集所有的 DEX 碎片,按照 DEX 文件格式重新拼接。
  • 将拼接后的完整 DEX 文件写入到文件系统。
  1. 自动化
  • 修改虚拟机源码后,所有运行的应用都会被自动监控,不需要手动 Hook 或注入。