背景描述

在学习 frida 的时候,从一位大佬的博客过来进行实战。本文复现了参阅博客的过程,并根据现学的知识进行了额外的操作。以此文记录学习的过程。

本文是对一 Android 备份文件 .ab 文件入手,到得到其中的 apk 文件和数据库,并通过分析此 apk 得到数据库的密钥,进而得到数据库中的 flag。

本文中用到的文件可以从这里直接下载。1.ab

解决过程

后缀分析

题目文件后缀名是 .ab ,通过属性查看,没有其他信息。

属性信息

什么是 .ab 文件呢?网上查阅一下,可以知道这玩意是安卓系统的备份文件。

一般情况下,.ab 的二进制数据中,前 24 个字节充当文件头,其中记录了一些必要的参数。如果文件头中有 none 字样,则此文件是没有加密的,如果存在如 AES-256 标志,则代表此文件是加密的。

使用 010Editor 查看文件,如下所示,可以看到 none 字样,因此此文件是没有经过加密的。

查看未加密

数据提取

确定了目标文件是未加密的,就可以使用工具进行提取。android-backup-extractor 可以将 .ab 文件转化为 .tar 文件。然后解压即可。

打开终端输入以下命令。

1
java -jar ade.jar unpack [path_to_db] [output_path_to_tar]

如下图所示

ade使用

然后将 .tar 压缩文件解压,可以发现里面存在一个 apk 文件(base.apk)和两个 sqlite 数据库文件(Encryto.db, Demo.db)。

压缩包预览

数据分析

apk 运行

将解压出来的 apk 文件安装到手机/模拟器上,发现是一个登录页面,于是随便在输入框输入了弱口令,但是提示 “Wait a Minute.What was happened”,其他并没有什么有价值的信息。

测试运行

数据库文件查看

既然 apk 直接打开无法观察到有用的信息,那就尝试查看一下数据库,说不定能看到账号觅马。

直接使用 sqlitebrowser 狂暴轰入打开 Demo.db ,但是失败了,再打开 Encryto.db 也是一样的效果。结合这个名字,多半是加密数据库了。

打卡数据库失败

反编译 APK

使用 jeb 反编译 Demo.apk ,搜索一下在模拟器中运行时弹出的字符串 “Wait a Minute.What was happened” ,可以发现是在 AnotherActivity 中。

AnotherActivity查看

交叉引用一下,发现在 MainActivity 中的 onClick() 中绑定了监听。然后通过 Intent 开启了 AnotherActivity

而对按钮的设计都是在 onCreate() 方法中,但其中除了对按钮、布局的设定外和调用了一个加密数据库相关的操作后就直接调用了私有方法 a() 。 所以关键就在这个 a 方法中。

MainActivity

为了方便分析,这里将其中的私有方法 a() 单独列举拖出来。

1
2
3
4
5
6
7
8
9
10
11
private void a() {
SQLiteDatabase.loadLibs(this);
this.b = new a(this, "Demo.db", null, 1);
ContentValues v0 = new ContentValues();
v0.put("name", "Stranger");
v0.put("password", Integer.valueOf(0x1E240));
com.example.yaphetshan.tencentwelcome.a.a v1 = new com.example.yaphetshan.tencentwelcome.a.a();
String v2 = v1.a(v0.getAsString("name"), v0.getAsString("password"));
this.a = this.b.getWritableDatabase(v1.a(v2 + v1.b(v2, v0.getAsString("password"))).substring(0, 7));
this.a.insert("TencentMicrMsg", null, v0);
}

第五、六两行放置了一个 usernameStrangerpassword123456

第七行实例化了一个类,可以暂时不管。

第八行计算了一个字符串 v2。通过 v1.a(String, String) 。定位到其中可以看到该方法只是将两个参数的前四位进行了拼接。事实上,我们只需要知道它本质是一个 z = f(x,y) 的二到一的映射就好。

a(string, string)

第九行,将 v2 放入了一堆函数中又放入另外的函数中。看起来有很多的操作,我们可以从内而外一层一层慢慢进行分析。具体分析过程没有兴趣的同志可以跳过。此行就是将 v2 进行一系列计算得到了数据库的密钥,再传入打开了数据库并将实例赋值给 this.a, 并且可以忽略其中的变化过程,直接正向复用其代码得到 key

  1. 首先,查看第一层 v1.b(v2, v0.getAsString("password")) , 定位到 v1.b(String, String) ,进入函数中进行查看,发现与第二个参数无关,实际上只与第一个参数有关。

    1
    2
    3
    4
    public String b(String arg2, String arg3) {
    new b();
    return b.a(arg2); // 只与 arg2 有关,所以此函数本质是一个 z=f(x,y) -> z=f(x) 的函数
    }

    然后,研究上面第三行的 b.a(String) ,进入函数中查看,这个函数本质就是一个 z=f(x) 的一到一的映射。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static final String a(String arg9) {
    int v0 = 0;
    char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    try {
    byte[] v1 = arg9.getBytes();
    MessageDigest v3 = MessageDigest.getInstance("MD5"); // 想要计算 md5 哈希值
    v3.update(v1); // 将变相后的 arg9 传递进去
    byte[] v3_1 = v3.digest(); // 完成一次hash计算,实际就是计算了一个只与传入参数相关的哈希,可以认为 v3_1 = f(arg9) 一到一映射
    char[] v5 = new char[v3_1.length * 2];
    int v1_1 = 0;
    while(v0 < v3_1.length) {
    int v6 = v3_1[v0];
    int v7 = v1_1 + 1;
    v5[v1_1] = v2[v6 >>> 4 & 15]; // 把高4位挪到低4位,抹掉低4位,作下标,填入 v5 的偶数下标位置
    v1_1 = v7 + 1;
    v5[v7] = v2[v6 & 15]; // 只使用低4位,抹掉高4位,作下标,填入 v5 的奇数下标位置
    ++v0; // hash 值的每一个 byte 经过计算,都会变成 v5 的两个 byte(s)
    }

    return new String(v5); // 最终返回 string v5 可以理解整个函数的功能就是 z=f(x) 的一对一映射
    }
    catch(Exception v0_1) {
    return null;
    }
    }

    然后回到前面的 v1.b(String, String) 中。可以发现此函数接受两个参数,但是仅仅传入第一个参数到一个 z=f(x) 的函数,所以此函数本质也是一个 z=g(x) 的一对一的映射。

  2. 然后查看第二层, v1.a(v2 + v1.b(v2, v0.getAsString("password"))).substring(0, 7) ,内层的参数是将 v2 进行了一个 z=f(x) 的映射后再与 v2 自身进行相加,故这个操作相当于另一个一对一的映射, z=g(x) ,然后 定位到 v1.a(String) 中,此方法将一个固定字符串 yaphetshan 拼接到参数 arg3 的尾部,故此操作仍然是相当于 z=f(x)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    private String a;

    public a() {
    this.a = "yaphetshan";
    }

    public String a(String arg3) {
    new b();
    return b.b(arg3 + this.a);
    }

    然后再查看 b.b(arg3 + this.a) ,定位到 b.b(String) ,稍微细心一点的同学已经发现,这本质也是一个 z=f(x) 的一对一映射,与前面的区别在于一个使用的哈希算法是 md5 ,另一个是 sha-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static final String b(String arg9) {
    int v0 = 0;
    char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    try {
    byte[] v1 = arg9.getBytes();
    MessageDigest v3 = MessageDigest.getInstance("SHA-1");
    v3.update(v1);
    byte[] v3_1 = v3.digest();
    char[] v5 = new char[v3_1.length * 2];
    int v1_1 = 0;
    while(v0 < v3_1.length) {
    int v6 = v3_1[v0];
    int v7 = v1_1 + 1;
    v5[v1_1] = v2[v6 >>> 4 & 15];
    v1_1 = v7 + 1;
    v5[v7] = v2[v6 & 15];
    ++v0;
    }

    return new String(v5);
    }
    catch(Exception v0_1) {
    return null;
    }
    }

    于是回到前面, v1.a(v2 + v1.b(v2, v0.getAsString("password"))).substring(0, 7) 还是将 v2 放入了一个 z=f(x) 的一对一的映射(包含了取前 7 位的操作,因为这也是一对一的)计算出密钥。再用于连接数据库。

  3. 层层深入下去,直到找到了关键方法,确信了计算出来的就是密钥。
    确定 key

密钥生成

至此,我们知道了如何得到打开数据库的密钥。根据以上的分析过程,我们没有必要深入分析其中的计算过程,只需要知道下面的式子:

1
2
3
key=f(username, password)
# username = Stranger, password = 123456
# 函数 f 程序中有,我们不必自己实现,直接 copy 就好。

所以可以直接编写密钥生成脚本,只用写第九行,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.security.MessageDigest;
import java.util.*;

public class b {
public b() {
super();
}

public static void main(String[] args)
{
String v2 = "Stra1234"; // 我们已经知道是怎么拼接的就直接写死了
String varV1B = a(v2);
String varKey = v2 + varV1B + "yaphetshan"; // 这里也不用去调用了,直接写死
System.out.print("KEY = ");
System.out.print(b(varKey).substring(0,7));
}

// 直接 copy
public static final String a(String arg9) {
int v0 = 0;
char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] v1 = arg9.getBytes();
MessageDigest v3 = MessageDigest.getInstance("MD5");
v3.update(v1);
byte[] v3_1 = v3.digest(); // 完成一次hash计算
char[] v5 = new char[v3_1.length * 2];
int v1_1 = 0;
while(v0 < v3_1.length) {
int v6 = v3_1[v0];
int v7 = v1_1 + 1;
v5[v1_1] = v2[v6 >>> 4 & 15]; // 把高4位挪到低4位,抹掉低4位,作下标,填入 v5 的偶数下标位置
v1_1 = v7 + 1;
v5[v7] = v2[v6 & 15]; // 就用低4位,抹掉高4位,作下标,填入 v5 的奇数下标位置
++v0; // hash 值的每一个 byte 经过计算,都会变成 v5 的两个 byte
}

return new String(v5);
}
catch(Exception v0_1) {
return null;
}
}
// 直接 copy
public static final String b(String arg9) {
int v0 = 0;
char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] v1 = arg9.getBytes();
MessageDigest v3 = MessageDigest.getInstance("SHA-1");
v3.update(v1);
byte[] v3_1 = v3.digest();
char[] v5 = new char[v3_1.length * 2];
int v1_1 = 0;
while(v0 < v3_1.length) {
int v6 = v3_1[v0];
int v7 = v1_1 + 1;
v5[v1_1] = v2[v6 >>> 4 & 15];
v1_1 = v7 + 1;
v5[v7] = v2[v6 & 15];
++v0;
}

return new String(v5);
}
catch(Exception v0_1) {
return null;
}
}
}

编译运行上述脚本可以成功得到 KEY=ae56f99 。(笔者不常用 java,此处由于里面的主类名字的 b ,所以文件命名必须的 b.javaPython YES!

编译运行脚本

flag 获取

得到密钥之后,使用 sqlitebrowser (SQLCipher.exe) 来打开加密数据库 Encryto.db。 将文件拖入窗口中,会弹出来一个窗,让输入口令。

解密数据库

输入之后就可以看到其中的内容了。 flag 字样很显眼,查看一下。

查看表结构

在表内容中可以看到一个 flag 的值为 “VGN0ZntIM2xsMF9Eb19ZMHVfTG92M19UZW5jM250IX0=” 。我觉得是 base64,解码一下看看。

得到 flag base64数据

python 走一波,拿到 flag 为 “Tctf{H3ll0_Do_Y0u_Lov3_Tenc3nt!}”

解码得到 flag

frida 获取 key

由于笔者是从学习 frida 过来的,在前面分析到密钥生成那一步时,笔者就觉得了本题也可以不用写解密脚本,而是通过 hook 的方式,或者直接调用 app 中的函数,让其帮忙计算得到密钥。

直接调用 instance

由前面的逻辑可以知道,密钥 KEY 是通过一些列计算得到的,如果我们主动调用 instance 并获取其返回值则可以直接得到密钥 KEY

frida_rpc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function callFun() {
Java.perform(function fn() {
Java.choose("com.example.yaphetshan.tencentwelcome.MainActivity", {
onMatch: function (x) {
var password = Java.use("java.lang.String").$new("123456"); // password

// 找到类 com.example.yaphetshan.tencentwelcome.a.a 并调用其构造函数 实例化了一个 v1
// 相当于 com.example.yaphetshan.tencentwelcome.a.a v1 = new com.example.yaphetshan.tencentwelcome.a.a();
var v1 = Java.use("com.example.yaphetshan.tencentwelcome.a.a").$new();

// v2 的拼接就不去调用了直接写死
var v2 = Java.use("java.lang.String").$new("Stra1234");

// 和 getWritableDatabase 的参数写的一模一样
var key = Java.use("java.lang.String").$new(v1.a(v2 + v1.b(v2, password))).substring(0, 7);

console.log("KEY = " + key); // 打印 KEY
},
onComplete: function () {
//
}
})
})
}
rpc.exports = {
callfun: callFun
};

loader.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
import frida


device = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
pid = device.spawn(["com.example.yaphetshan.tencentwelcome"])
device.resume(pid)

time.sleep(1)

session = device.attach(pid)
with open("frida_rpc.js", "r", encoding="utf-8") as f:
script = session.create_script(f.read())

script.load()

script.exports.callfun()

运行脚本,可以看到觅马直接打印出来了。

加载js注入

hook getWritableDatabase

由前面的逻辑可以知道,方法 getWritableDatabase(String) 中收到的字符串就是密钥 KEY,因此我们可以通过 hook 这个方法,然后观察他的参数就可以知道密钥 KEY 了。

在一通上网查询资料后,终于配置好了 adbobjectionfrida 与 夜神模拟器的连接。然后输入命令连接上模拟器。

1
objection -d -g com.example.yaphetshan.tencentwelcome explore

效果如下图所示,一般情况下,如果我们不知道这个方法所属的类,我们需要先查找这个方法,如下输入命令查找方法。

1
android hooking search methods getWritableDatabase

使用 objection

但是由于方法数量实在太过于巨大,往往都会崩溃掉,更别说电脑还是开的模拟器了。于是,这里有一个更好的方法——利用 jeb 。将鼠标放到我们需要查找的方法上悬停, jeb 就会显示目标方法的详细信息,然后我们直接 copy 出来即可。

jeb查看类

有了方法的全路径名称,下面就是给方法设置 hook 从而查看觅马。由于我们只需要该方法的参数即可,因此我们只选择 hook 参数。输入下面的命令,如下图所示,即可完成 hook

1
android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args [--dump-backtrace --dump-return]

进行 hook

然后在模拟器中点击 back (返回键)再重新进入应用。关于这一点,会一点点 Android 开发的应该都明白原理,涉及 Activity 的生命周期。等此方法被重新执行,我们就可在终端中看到我们想要的信息了,如下图所示,打印了我们想要的密钥 KEY。

得到 key

总结

此次算是第一次带有一定目的地进行逆向分析,虽然是在学习 frida 的路上中途过来实战练习的。总的来讲收获颇多,包括 .ab 文件是啥,又学会了用一些本文中的工具(sqlitebrowserabe 等)。幸好在实验之前,有学习过了 jeb 的使用和操作,Android 的开发,不然可能抄教程都抄不懂。

且今后遇到不熟悉的题目或者资料也不应该慌乱,甚至放弃。应该像参考连接的博主学习,可以利用搜索引擎现学(粗学)相关的知识,再精学可能有用的知识来解决问题,事后再进一步学习相关的知识。

虽然还有很多东西没有细细品味,如 .ab 文件的构造和 ssqlitecipher 等,但是, 鉴于本次是旁道而来的实战,就不在此处进行研究了。

参考

参考链接1:学习 frida 的使用 https://eternalsakura13.com/2020/07/04/frida/

参考链接2:XCTF app3 题目的 wp https://www.52pojie.cn/forum.php?mod=viewthread&tid=1082706

参考链接3:Android backup extractor https://github.com/nelenkov/android-backup-extractor

参考链接4:jadx https://github.com/skylot/jadx

参考链接5: .ab 文件 https://blog.csdn.net/qq_33356474/article/details/92188491

参考链接6:sqlitebrowser https://sqlitebrowser.org/dl/