安卓 APK 逆向

Android 的逆向主要分为两个层面:

  1. Java 层
  2. 原生层

Android 逆向常用工具 jadx 下载地址:GitHub - skylot/jadx: Dex to Java decompiler

首先了解一下 Android:

  • Android 也可以看成是 Linux 的一个发行版,但不是 GNU/Linux
  • Ubuntu、Kali 等也是 Linux 的发行版,但都是基于 GNU/Linux 的发行版,它们的应用层用的是 GNU(glibc、libstdc++、GNU CoreUtils 等)
  • 也就是说,Android 和 Ubuntu、Kali 等基于 GNU/Linux 的 Linux 发行版是不一样的,Android 的应用层是自己独有的,不依赖于 GNU

Java 层

简而言之,就是直接分析 APK 的 MainActivity 方法,不存在其他的链接库调用,一般仅需要掌握 Java 语言即可

用 jadx 打开 apk 程序后,主要方法 MainActivity 通常位于 com.xxx.xxx

CTF - REVERSE_Android 逆向1.png

对于 MainActivity 中的一些字符串变量,例如:

CTF - REVERSE_Android 逆向2.png

这里的 C0535R.string.table 可以在如下路径中找到:资源文件/resources.arsc/res/values/strings.xml

CTF - REVERSE_Android 逆向3.png

jadx 分析 Android 的 Java 层代码和 IDA 分析 C/C++ 程序一样,从 MainActivity 开始一路分析即可

Android 程序 Java 层逆向例题见本站《【楚慧杯 2023】Level_One


原生层

原生层也叫 Native 层,指的是 Android 操作系统的底层,包括 Linux 内核和各种 C/C++ 库

Native 层通常会使用 so 文件来实现相关的方法,有点类似于 Linux 中的动态链接库,一般需要掌握 Java 语言、C/C++ 语言、汇编语言

Android 的 apk 程序实质上也是一个压缩包,我们可以对 apk 程序直接进行解压(使用 7-zip 或者 WinRAR 都可以)

然后会得到如下结构的目录:

CTF - REVERSE_Android 逆向4.png

其实细心一点可以发现,这个目录结构和 jadx 中看到的结构是一样的

so 文件通常在 lib 文件夹下:

CTF - REVERSE_Android 逆向5.png

由于 Android 程序需要考虑适配市面上不同手机的 CPU 架构,因此会生成支持不同平台的 so 文件进行兼容

这里每一个文件夹中的 so 文件就对应了一个 CPU 架构

它们的内容几乎都是一样的,分析其中之一即可,通常是在 IDA 中分析 x86x86_64 架构

Android 程序原生层逆向例题见本站 《【楚慧杯 2023】Level_up


什么是 so

与 Linux 类似,Native 层代码通常存在于 so 文件中,so 文件全称为 Shared Object,使用 so 可以提高开发效率、快速移植

开发 Android 应用时,有时候 Java 层的编码不能满足实现需求,就需要使用 C/C++ 实现,然后生成 so 文件,常见的场景有:加解密算法、音视频编解码等

so 文件的加载通常有两种方式:

  • loadLibrary 加载(主要使用
System.loadLibrary("xxx")   // 调用项目中 lib 目录下的 libxxx.so 文件

一般通过 JNI 来实现

JNI 全名 Java Native Interface,是 Java 本地接口

JNI 是 Java 调用 Native 语言的一种特性,通过 JNI 可以使 Java 与 C/C++ 机型交互,简单点说就是 JNI 是 Java 中调用 C/C++ 的统称

在 Android 中 JNI 的实现示例如下:

CTF - REVERSE_Android 逆向6.png

这里通过 JNI 从 libSecret_entrance.so 文件中调用了两个方法:

Java_com_example_re11113_jni_getiv(__int64 a1)
Java_com_example_re11113_jni_getkey(__int64 a1)

  • load 加载(主要用于在插件中加载 so 文件)
System.load("xxx")   // xxx 对应 lib 的绝对路径

IDA 动态调试安卓 so

使用 IDA 动态调试意味着我们要将 apk 运行起来,可以使用模拟器(如:雷电模拟器),也可以使用真实的安卓手机(建议拥有 root 权限)

当然有 root 的真机最好,用 Android 模拟器来动态调试 so 文件可能无法进行某些步骤

adb 命令使用方法:ADB 命令大全 - 知乎

在 IDA 的 dbgsrv 目录下有许多远程调试用的服务程序:

CTF - REVERSE_Android 逆向7.png

调试安卓用到的主要是上图红框中的程序,但它们也有区别:

  • 真实的安卓手机通常是 ARM 架构,对应 android_serverandroid_server_64

  • 模拟器(如:雷电模拟器)通常是 x86 架构,对应 android_x86_serverandroid_x64_server

我这里主要以雷电模拟器作为例子

以《2024 WIDC 天融信杯》的《Day2-debug 算法逆向》这道题来说明

用 jadx 打开 apk,定位到 MainActivity,主要与 getFlag() 函数有关:

CTF - REVERSE_Android 逆向17.png

函数的定义在 libread.so 文件中:

CTF - REVERSE_Android 逆向18.png

关键加密逻辑在于:

  do
  {
LABEL_16:
    v62 = v3[v7];
    if ( (v62 - 48) <= 9u )
    {
      v62 = (v62 - 45 - 5 * (((v62 - 45) / 5u) & 0xFE)) | 0x30;
    }
    else if ( (v62 - 97) > 0x19u )
    {
      if ( (v62 - 65) <= 0x19u )
        v62 = (v62 - 62) % 0x1Au + 65;
    }
    else
    {
      v62 = (v62 - 94) % 0x1Au + 97;
    }
    v5[v7++] = v62 ^ 3;
  }
  while ( v6 != v7 );
  return strcmp(v5, buff) == 0;

这里对 v62 进行了处理,分别对应 v620 ~ 9a ~ zA ~ Z 的情况,循环次数为 v6,最后将加密后的数据与 buff 比较,显然 v5 是明文,buff 是密文

但是 buff 处并没有内容,因此可能是动态生成的,必须动态调试才能拿到密文

CTF - REVERSE_Android 逆向19.png

下载雷电模拟器:雷电安卓模拟器-手游模拟器安卓版_android手机模拟器电脑版_雷电模拟器官网

雷电模拟器要开启 root 模式,否则 IDA 找不到要附加的进程

然后在雷电模拟器的安装路径下,有一个 adb.exe 程序,我这里是 D:\leidian\LDPlayer9

CTF - REVERSE_Android 逆向9.png

将该路径加入环境变量,便可以在 CMD 中直接使用 adb 命令:

CTF - REVERSE_Android 逆向8.png

将 apk 程序安装到雷电模拟器,并保证其可以正常运行:

CTF - REVERSE_Android 逆向10.png

在 IDA 的 dbgsrv 目录下打开 CMD

测试一下是否能连接到雷电模拟器: (一般只要安卓设备连接正确,会自动启动 adb server

adb devices

CTF - REVERSE_Android 逆向11.png

以 root 权限运行:

adb root

CTF - REVERSE_Android 逆向12.png

将 IDA 的 Android 调试服务程序推送到雷电模拟器:

adb push android_x64_server /data/local/tmp   # 也可以选择推送到其他路径

CTF - REVERSE_Android 逆向13.png

注意这里 IDA 的 server 要选对,64 位雷电模拟器一般选择 android_x64_server

通过 shell 连接雷电模拟器:

adb shell

/data/local/tmp 目录下赋予 android_x64_server 执行权限:

cd /data/local/tmp && ls
chmod 777 android_x64_server

CTF - REVERSE_Android 逆向14.png

运行 android_x64_server

./android_x64_server

CTF - REVERSE_Android 逆向15.png

如果 android_x64_server 在 23946 端口正常开启监听,说明一切正常

另外开启一个 CMD,将雷电模拟器的端口转发到本机:

adb forward tcp:23946 tcp:23946   # 前面是电脑本机的端口,后面是手机的端口

为了让 IDA 能够发现该 APP,在调试模式打开 APP:

adb shell am start -D -n  com.ctf.read/com.ctf.read.MainActivity

具体名称可以在 资源文件/AndroidManifest.xml 中查看

CTF - REVERSE_Android 逆向16.png

雷电模拟器会弹出等待调试的弹窗:

CTF - REVERSE_Android 逆向20.png

在 IDA 中使用远程 Linux 调试器:

CTF - REVERSE_Android 逆向21.png

为了防止 apk 反调试,勾选下图中三个调试选项:

CTF - REVERSE_Android 逆向22.png

Hostname 设置为 127.0.0.1,Port 设置为前面转发到本机的端口号,我这里是 23946

CTF - REVERSE_Android 逆向23.png

如果报错显示拒绝连接,检查转发端口号是否正确,或者重新转发一次

由于 so 文件无法单独运行,因此我们需要 attach 附加进程

如果前面没出错的话,在 IDA 的 attach 列表里是可以看到该进程的:

CTF - REVERSE_Android 逆向24.png

找到 buff 存放的地址:0x7FFF5A3772A0,初始时未定义

CTF - REVERSE_Android 逆向25.png

设置 jdwp 调试端口

首先查看一下雷电模拟器中该程序的端口号:

adb shell ps -ef | grep com

CTF - REVERSE_Android 逆向26.png

adb forward tcp:8700 jdwp:3047   # 注意将 3047 端口号修改为自己的
jdb -connect "com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700"

CTF - REVERSE_Android 逆向27.png

然后 IDA 图标会闪烁,回到 IDA 就可以正常 F9、正常下断点了

运行后,buff 存放的地址:0x7FFF5A3772A0 处生成了数据

提取出来得到密文:jm0g3{djyalj{4og3k1vequwbi:f61:6f;36:;2dkkfAWRjSv2UFDukk

CTF - REVERSE_Android 逆向28.png

根据加密逻辑暴力破解:

#include <iostream>
#include <string>
#include <stdio.h>

using namespace std;

int main()
{
    char enc[] = "jm0g3{djyalj{4og3k1vequwbi:f61:6f;36:;2dkkfAWRjSv2UFDukk";

    char dec;
    for (int i = 0; i < 56; i++) {
        for (int j = 32; j < 127; j++) {
            if ( (j - 48) <= 9 )
                dec = (j - 45 - 5 * (((j - 45) / 5) & 0xFE)) | 0x30;
            else if ( (j - 97) > 0x19 && (j - 65) <= 0x19)
                    dec = (j - 62) % 0x1A + 65;
            else
                dec = (j - 94) % 0x1A + 97;
            dec = dec ^ 3;

            if (dec == enc[i] && ((48 <= j && j <= 57) || (65 <= j && j <= 90) || (97 <= j && j <= 122))) {
                printf("%c", j);
                break;
            }
        }
    }

    return 0;
}

得到 flag:fk0a7udfwylfu4ia7e9rcosqDg6b2962b572658deebQNfMr8Ssee