Android逆向与动态调试
安卓 APK 逆向
Android 的逆向主要分为两个层面:
- Java 层
- 原生层
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
下
对于 MainActivity
中的一些字符串变量,例如:
这里的 C0535R.string.table
可以在如下路径中找到:资源文件/resources.arsc/res/values/strings.xml
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 都可以)
然后会得到如下结构的目录:
其实细心一点可以发现,这个目录结构和 jadx 中看到的结构是一样的
而 so
文件通常在 lib
文件夹下:
由于 Android 程序需要考虑适配市面上不同手机的 CPU 架构,因此会生成支持不同平台的
so
文件进行兼容
这里每一个文件夹中的 so
文件就对应了一个 CPU 架构
它们的内容几乎都是一样的,分析其中之一即可,通常是在 IDA 中分析 x86
或 x86_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 的实现示例如下:
这里通过 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
目录下有许多远程调试用的服务程序:
调试安卓用到的主要是上图红框中的程序,但它们也有区别:
真实的安卓手机通常是 ARM 架构,对应
android_server
和android_server_64
模拟器(如:雷电模拟器)通常是 x86 架构,对应
android_x86_server
和android_x64_server
我这里主要以雷电模拟器作为例子
以《2024 WIDC 天融信杯》的《Day2-debug 算法逆向》这道题来说明
用 jadx 打开 apk,定位到 MainActivity
,主要与 getFlag()
函数有关:
函数的定义在 libread.so
文件中:
关键加密逻辑在于:
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
进行了处理,分别对应 v62
为 0 ~ 9
、a ~ z
和 A ~ Z
的情况,循环次数为 v6
,最后将加密后的数据与 buff
比较,显然 v5
是明文,buff
是密文
但是 buff
处并没有内容,因此可能是动态生成的,必须动态调试才能拿到密文
下载雷电模拟器:雷电安卓模拟器-手游模拟器安卓版_android手机模拟器电脑版_雷电模拟器官网
雷电模拟器要开启 root 模式,否则 IDA 找不到要附加的进程
然后在雷电模拟器的安装路径下,有一个 adb.exe
程序,我这里是 D:\leidian\LDPlayer9
:
将该路径加入环境变量,便可以在 CMD 中直接使用 adb
命令:
将 apk 程序安装到雷电模拟器,并保证其可以正常运行:
在 IDA 的 dbgsrv
目录下打开 CMD
测试一下是否能连接到雷电模拟器: (一般只要安卓设备连接正确,会自动启动 adb server)
adb devices
以 root 权限运行:
adb root
将 IDA 的 Android 调试服务程序推送到雷电模拟器:
adb push android_x64_server /data/local/tmp # 也可以选择推送到其他路径
注意这里 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
运行 android_x64_server
:
./android_x64_server
如果 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
中查看
雷电模拟器会弹出等待调试的弹窗:
在 IDA 中使用远程 Linux 调试器:
为了防止 apk 反调试,勾选下图中三个调试选项:
Hostname
设置为 127.0.0.1,Port
设置为前面转发到本机的端口号,我这里是 23946
如果报错显示拒绝连接,检查转发端口号是否正确,或者重新转发一次
由于 so 文件无法单独运行,因此我们需要 attach
附加进程
如果前面没出错的话,在 IDA 的 attach
列表里是可以看到该进程的:
找到 buff
存放的地址:0x7FFF5A3772A0
,初始时未定义
设置 jdwp 调试端口
首先查看一下雷电模拟器中该程序的端口号:
adb shell ps -ef | grep com
adb forward tcp:8700 jdwp:3047 # 注意将 3047 端口号修改为自己的
jdb -connect "com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700"
然后 IDA 图标会闪烁,回到 IDA 就可以正常 F9、正常下断点了
运行后,buff
存放的地址:0x7FFF5A3772A0
处生成了数据
提取出来得到密文:jm0g3{djyalj{4og3k1vequwbi:f61:6f;36:;2dkkfAWRjSv2UFDukk
根据加密逻辑暴力破解:
#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