首页
社区
课程
招聘
[原创]Android 取证之微信8.0.38版本数据库解密分析
2023-7-20 10:22 25468

[原创]Android 取证之微信8.0.38版本数据库解密分析

2023-7-20 10:22
25468

0x01 前言

本文以教学为基准、本文提供的可操作性不得用于任何商业用途和违法违规场景。
本人对任何原因在使用本人中提供的代码和策略时可能对用户自己或他人造成的任何形式的损失和伤害不承担责任。

0x02 软硬件环境

app 版本:8.0.38
inject:frida 12.8.0
设备:Pixel 2 XL 已 ROOT
反汇编工具:JEB、JADX、IDA

0x03 Android数据库:SQLite

想要进行微信数据库的逆向必须先了解其采用的是何种数据库及采用的防护手段!
Android中的SQLite是一种轻量级的关系型数据库,它是Android平台中默认的本地数据库存储解决方案。SQLite在Android系统中广泛使用,可用于存储和检索应用程序中的数据。
SQLite的优点包括:
1、轻量级:SQLite非常轻便,它的库文件非常小,可以轻松嵌入到Android应用程序中。
2、高效:由于SQLite是一个本地数据库,它可以快速地执行读写操作,并且具有非常高的性能。
3、可靠性:SQLite是一个可靠的数据库解决方案,它可以确保数据的完整性和一致性。
4、跨平台:SQLite可以在各种不同的操作系统和平台上运行,因此可以轻松地将数据从一个平台移植到另一个平台。
but,SQLite却有一个致命的缺陷:不支持加密。
因此存储在SQLite中的数据可以被任何人轻易地查看。如果是普通的数据还好,但是当涉及到一些账号密码,聊天内容或者个人信息的时候,我们的应用就会面临严重的安全漏洞隐患。
所以,需要在开发应用时对数据库进行加密,目前对SQLite有两种加密方式:
1、对写入数据库的数据进行加密
2、对整个数据库文件进行加密
而微信所采取的方式便是第二种:对整个数据库文件进行加密。

0x04 需求分析

微信的数据库,位于本地的/data/data/com.tencent.mm/MicroMsg/921dxxxxxxxxxx4d/路径下,名称为:EnMicroMsg.db,最后一个文件夹每个手机可能不同,需自己进入定位下:
图片描述
将其导出后拖入sqlcipher软件中,可以看到需要输入密钥才能进行查看数据:
图片描述
ok,那么我们的需求就是获取到解密数据库的密钥。

0x05 密钥获取

将apk拖入jadx,等待反汇编完成,腾讯有着自己的加壳方案(腾讯乐固),但并未在微信中使用,应是加壳后会影响使用性能,而微信这么一个全国几亿人在用的软件对性能的要求出不了一丝偏差。
对SQLite有所了解的应该都知道,他有着自己所定义的api,要想对其进行操作必定会用到SQLiteDatabase这个类,搜索可得:
图片描述
双击进入后,查找open函数,查看open函数的具体实现:
图片描述
要想打开数据库,那么必定需要密钥,看open函数传入三个参数,其中第一个bArr数组无疑是最可疑的。
使用frida编写hook脚本:

1
2
3
4
5
6
7
8
9
Java.perform(function(){
    var SQLiteDatabase = Java.use("com.tencent.wcdb.database.SQLiteDatabase");
    SQLiteDatabase["open"].implementation = function (bArr, sQLiteCipherSpec, i15) {
        console.log('open is called' + ', ' + 'bArr: ' + JSON.stringify(bArr) + ', ' + 'sQLiteCipherSpec: ' + sQLiteCipherSpec + ', ' + 'i15: ' + i15);
        var ret = this.open(bArr, sQLiteCipherSpec, i15);
        console.log('open ret value is ' + ret);
        return ret;
    };
});

打开微信进行hook,结果:

[Pixel 2 XL::微信]->open is called, bArr: [48,48,57,102,97,56,51], sQLiteCipherSpec: com.tencent.wcdb.database.SQLiteCipherSpec@2eb6bd9, i15: 0
openInner ret value is undefined

将数据转换后得到结果:009fa83
在sqlcipher软件输入该密钥点击确定:
图片描述
可以看到整个数据库的内容已经可以正常查看了,那么接下来需要关心的就是密钥的生成方式了!

0x06 密钥生成分析

6.1 堆栈分析

得到了密钥,但是不知道密钥如何生成的,这种奇耻大辱岂是我辈逆向人员可以忍受的,必须搞它!
简单更改下hook代码,加入堆栈打印,已便于观测其执行流程:

1
2
3
4
5
6
7
8
9
10
Java.perform(function(){
    var SQLiteDatabase = Java.use("com.tencent.wcdb.database.SQLiteDatabase");
    SQLiteDatabase["open"].implementation = function (bArr, sQLiteCipherSpec, i15) {
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));  //java层打印堆栈
        console.log('open is called' + ', ' + 'bArr: ' + JSON.stringify(bArr) + ', ' + 'sQLiteCipherSpec: ' + sQLiteCipherSpec + ', ' + 'i15: ' + i15);
        var ret = this.open(bArr, sQLiteCipherSpec, i15);
        console.log('open ret value is ' + ret);
        return ret;
    };
});

hook结果:

[Pixel 2 XL::微信]-> java.lang.Throwable
at com.tencent.wcdb.database.SQLiteDatabase.open(Native Method)
at com.tencent.wcdb.database.SQLiteDatabase.openDatabase(SourceFile:3)
at com.tencent.wcdb.database.SQLiteDatabase.openDatabase(SourceFile:4)
at ir3.e.r(Unknown Source:185)
at ir3.a.f(Unknown Source:240)
at ir3.f.n(Unknown Source:55)
at ir3.f.m(Unknown Source:10)
at gi.bb.<init>(Unknown Source:80)
at se1.i$a.invokeSuspend(Unknown Source:185)
at q74.a.resumeWith(Unknown Source:8)
at ta4.a1.run(Unknown Source:122)
at l34.b$b.run(Unknown Source:63)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:457)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at x34.j.run(Unknown Source:246)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at q34.c.run(Unknown Source:2)
at java.lang.Thread.run(Thread.java:764)
open is called, bArr: [48,48,57,102,97,56,51], sQLiteCipherSpec: com.tencent.wcdb.database.SQLiteCipherSpec@8ea336a, i15: 0
open ret value is undefined

可以看到在进入SQLiteDatabase类中最后一个方法为:ir3.e.r,跟进查看:
图片描述
编写hook代码对e.r方法进行hook查看:

1
2
3
4
5
6
7
8
9
Java.perform(function(){
    var e = Java.use("ir3.e");
    e["r"].implementation = function (str, str2, i15, z15) {
        console.log('r is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2 + ', ' + 'i15: ' + i15 + ', ' + 'z15: ' + z15);
        var ret = this.r(str, str2, i15, z15);
        console.log('r ret value is ' + ret);
        return ret;
    };
});

hook结果:

[Pixel 2 XL::微信]-> r is called, str: /data/user/0/com.tencent.mm/MicroMsg/921dfcc503355cc212099213345ddc4d/enFavorite.db, str2: 009fa83, i15: 0, z15: true
r is called, str: /data/user/0/com.tencent.mm/MicroMsg/921dfcc503355cc212099213345ddc4d/enFavorite.db, str2: 009fa83, i15: 0, z15: true
r ret value is ir3.e@a3302f8
r ret value is ir3.e@41156a4

打开的另一个数据库,这个无所谓,可以看到密码bArr是由e方法的参数str2通过str2.getBytes()生成,那么就需要继续分析调用com.tencent.mm.ir3.e.r()方法的地方。
查看堆栈上一个调用位置为a.f()方法,跟进查看:
图片描述
没有什么可分析的代码,继续往上分析:com.tencent.mm.ir3.f.n()和com.tencent.mm.ir3.f.m()
图片描述
参数很多,hook看看,hook代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function(){
    var f = Java.use("ir3.f");
    f["m"].implementation = function (str, str2, j15, str3, hashMap, z15) {
        console.log('m is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2 + ', ' + 'j15: ' + j15 + ', ' + 'str3: ' + str3 + ', ' + 'hashMap: ' + hashMap + ', ' + 'z15: ' + z15);
        var ret = this.m(str, str2, j15, str3, hashMap, z15);
        console.log('m ret value is ' + ret);
        return ret;
    };
 
    var f = Java.use("ir3.f");
    f["n"].implementation = function (str, str2, str3, j15, str4, hashMap, z15) {
        console.log('n is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2 + ', ' + 'str3: ' + str3 + ', ' + 'j15: ' + j15 + ', ' + 'str4: ' + str4 + ', ' + 'hashMap: ' + hashMap + ', ' + 'z15: ' + z15);
        var ret = this.n(str, str2, str3, j15, str4, hashMap, z15);
        console.log('n ret value is ' + ret);
        return ret;
    };
});

hook结果:

[Pixel 2 XL::微信]-> m is called, str: , str2: /data/user/0/com.tencent.mm/MicroMsg/921dfcc503355cc212099213345ddc4d/enFavorite.db, j15: 1721974820, str3: 1234567890ABCDEF, hashMap: {-403906948=gi.bba@d405a6a,268557265=gi.bba@d405a6a, 268557265=gi.bbb@7eefb5b, -491946003=gi.bbe@4b8eff8,1810537579=gi.bbe@4b8eff8, 1810537579=gi.bbf@dabecd1, 1692712704=gi.bbd@f01b336,20610547=gi.bbd@f01b336, 20610547=gi.bbc@a17f237, -1687968802=gi.bb$g@58913a4}, z15: true
n is called, str: , str2: /data/user/0/com.tencent.mm/MicroMsg/921dfcc503355cc212099213345ddc4d/enFavorite.db, str3: , j15: 1721974820, str4: 1234567890ABCDEF, hashMap: {-403906948=gi.bba@d405a6a,268557265=gi.bba@d405a6a, 268557265=gi.bbb@7eefb5b, -491946003=gi.bbe@4b8eff8,1810537579=gi.bbe@4b8eff8, 1810537579=gi.bbf@dabecd1, 1692712704=gi.bbd@f01b336,20610547=gi.bbd@f01b336, 20610547=gi.bbc@a17f237, -1687968802=gi.bb$g@58913a4}, z15: true
m is called, str: , str2: /data/user/0/com.tencent.mm/MicroMsg/921dfcc503355cc212099213345ddc4d/enFavorite.db, j15: 1721974820, str3: 1234567890ABCDEF, hashMap: {-403906948=gi.bba@d405a6a,268557265=gi.bba@d405a6a, 268557265=gi.bbb@7eefb5b, -491946003=gi.bbe@4b8eff8,1810537579=gi.bbe@4b8eff8, 1810537579=gi.bbf@dabecd1, 1692712704=gi.bbd@f01b336,20610547=gi.bbd@f01b336, 20610547=gi.bbc@a17f237, -1687968802=gi.bb$g@58913a4}, z15: true
n is called, str: , str2: /data/user/0/com.tencent.mm/MicroMsg/921dfcc503355cc212099213345ddc4d/enFavorite.db, str3: , j15: 1721974820, str4: 1234567890ABCDEF, hashMap: {-403906948=gi.bba@d405a6a,268557265=gi.bba@d405a6a, 268557265=gi.bbb@7eefb5b, -491946003=gi.bbe@4b8eff8,1810537579=gi.bbe@4b8eff8, 1810537579=gi.bbf@dabecd1, 1692712704=gi.bbd@f01b336,20610547=gi.bbd@f01b336, 20610547=gi.bbc@a17f237, -1687968802=gi.bb$g@58913a4}, z15: true
n ret value is true
m ret value is true
n ret value is true
m ret value is true

可以看到有两个参数是固定的,j15: 1721974820, str4: 1234567890ABCDEF。

6.2 ir3.f.m()函数分析

查看m的调用,分析这两个参数的来源,定位到com.tencent.mm.gi.bb()函数中:
图片描述
看参数二:r50.z.b().g(),跟进查看:
图片描述
继续跟进:C4827e.a方法:
图片描述
貌似是动态生成的,可反复操作n次,其值总是1721974820,这不经让我想到一种可能它会不会是从某个文件中取出的?
在jadx中搜索该值,把所有选项都勾选了,可以看到并无任何结果:
图片描述
我又把方向转向apk的私有目录中,将其导出都搜索,果不其然在auth_info_key_prefs.xml文件中找到了该值:
图片描述

看参数三:pj.r.f(true),跟进查看:
图片描述
三元表达式,可以看出是写死"1234567890ABCDEF"。

6.3 加密方式分析

那么是怎么通过这两个参数变化生成了密钥值呢?
必然是经过某些算法得出的密钥,至于是什么算法,可以有两种方式确认:
一:继续分析apk源码,耗时久。
二:上算法通杀脚本,只要是常见的算法全给它hook一边从而确定采用了何种算法。
那我肯定是采用性价比最高的方法,直接上算法通杀脚本hook一遍,可得:
图片描述
标准MD5算法得出的值,只是密钥只采用了值的前7位,先把hook脚本给一下,太多了有限制,这边仅把MD5的脚本贴上:

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
function stack_print() {
    console.log(
        Java.use("android.util.Log")
            .getStackTraceString(
                Java.use("java.lang.Throwable").$new()
            )
    );
}
 
Java.perform(function(){
    var messageDigest = Java.use("java.security.MessageDigest");
    var ByteString = Java.use("com.android.okhttp.okio.ByteString");
    //tag为标签,data为数据
    function toBase64(tag, data) {
        console.log(tag + " Base64: ", ByteString.of(data).base64());
    }
    function toHex(tag, data) {
        console.log(tag + " Hex: ", ByteString.of(data).hex());
    }
    function toUtf8(tag, data) {
        console.log(tag + " Utf8: ", ByteString.of(data).utf8());
    }
    messageDigest.update.overload('byte').implementation = function (data) {
        console.log("MessageDigest.update('byte') is called!");
        return this.update(data);
    }
    messageDigest.update.overload('java.nio.ByteBuffer').implementation = function (data) {
        console.log("MessageDigest.update('java.nio.ByteBuffer') is called!");
        return this.update(data);
    }
    messageDigest.digest.overload().implementation = function () {
        console.log("MessageDigest.digest() 被调用了!");
        var result = this.digest();
        var algorithm = this.getAlgorithm();
        var tag = algorithm + " 调用digest返回输出的数据:";
        toHex(tag, result);
        toBase64(tag, result);
        console.log("=======================================================");
        return result;
    }
    messageDigest.digest.overload('[B').implementation = function (data) {
        console.log("MessageDigest.digest('[B') 被调用了!");
        var algorithm = this.getAlgorithm();
        var tag = algorithm + " 调用digest得到的数据:";
        toUtf8(tag, data);
        toHex(tag, data);
        toBase64(tag, data);
        var result = this.digest(data);
        var tags = algorithm + " 调用digest返回输出的数据:";
        toHex(tags, result);
        toBase64(tags, result);
        console.log("=======================================================");
        return result;
    }
    messageDigest.digest.overload('[B', 'int', 'int').implementation = function (data, start, length) {
        console.log("MessageDigest.digest('[B', 'int', 'int') 被调用了!");
        var algorithm = this.getAlgorithm();
        var tag = algorithm + " 调用digest得到的数据:";
        toUtf8(tag, data);
        toHex(tag, data);
        toBase64(tag, data);
        var result = this.digest(data, start, length);
        var tags = algorithm + " 调用digest返回输出的数据:";
        toHex(tags, result);
        toBase64(tags, result);
        console.log("=======================================================", start, length);
        return result;
    }
});

6.4 算法还原

再将MD5算法得出密钥的过程用python还原:

1
2
3
4
5
6
7
8
9
10
11
12
13
from hashlib import md5
 
def get_encode_mes(mes):
    # 创建 MD5 对象
    new_md5 = md5()
    # 这里必须用encode()函数对字符串进行编码,不然会报 TypeError: Unicode-objects must be encoded before hashing
    new_md5.update(mes.encode(encoding='utf-8'))
    # 加密
    return new_md5.hexdigest()
 
if __name__ == '__main__':
    print(get_encode_mes('17219748201234567890ABCDEF'))
    print(get_encode_mes('1234567890ABCDEF1721974820'))

结果:

d33514c7133ec8ddeeb741db284c3b62
009fa83591e1b8d1655857d83b03b71d

由于不知道两个参数的拼接顺序,所以两种方式都进行了尝试,看加密后的值,显然1234567890ABCDEF在前,_auth_uin在后。

0x07 总结

现今各大厂商对数据的保护也是是越来越强,路漫漫其修远兮,吾辈当上下求索。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2023-7-22 10:16 被行简编辑 ,原因:
收藏
点赞13
打赏
分享
最新回复 (17)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_yptsvehl 2023-7-22 01:26
2
0
雪    币: 19439
活跃值: (29125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-7-22 16:31
3
1
也是不能收藏
雪    币: 41
活跃值: (382)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
大雪花 2023-8-1 23:14
4
0
感谢分享!但是你讲的和老版本不一样。密钥的生成变过吗?
雪    币: 2671
活跃值: (3839)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
行简 2023-8-2 10:37
5
0
大雪花 感谢分享!但是你讲的和老版本不一样。密钥的生成变过吗?
老版本的是用imei加上uid再经过md5加密取前7位数做密钥,这点是不一样的,大致分析流程都一样。
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_早安 2023-8-10 22:03
6
0
作者使用的SQLCipher是什么版本的呢?我用4.4.3的解密不了
雪    币: 574
活跃值: (257)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
miyuecao 2023-8-14 17:11
7
0
雪    币: 2671
活跃值: (3839)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
行简 2023-8-16 23:56
8
0
wx_早安 作者使用的SQLCipher是什么版本的呢?我用4.4.3的解密不了
sqlcipher2
雪    币: 19439
活跃值: (29125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-8-17 09:20
9
1
雪    币: 0
活跃值: (29)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
llllnaonao 2023-9-19 15:49
10
0
多谢大佬的分享。但是对微信8.0.38的EnMicroMsg.db,使用md5(“1234567890ABCDEF”+uin) 的前7位解密失败。是打开工具的原因吗?
雪    币: 2395
活跃值: (2257)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
gtict 2023-9-20 09:30
11
0
求新版qq解密。搞了一段时间,密钥都有了,就是解不开
雪    币: 2671
活跃值: (3839)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
行简 2023-9-20 17:31
12
0
gtict 求新版qq解密。搞了一段时间,密钥都有了,就是解不开
qq貌似是对数据库中的数据进行加密的,方法不一样。
雪    币: 2671
活跃值: (3839)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
行简 2023-9-20 17:31
13
0
llllnaonao 多谢大佬的分享。但是对微信8.0.38的EnMicroMsg.db,使用md5(“1234567890ABCDEF”+uin) 的前7位解密失败。是打开工具的原因吗?
你换个工具,用我上面提到的那个,之前我也遇到过。
雪    币: 145
活跃值: (85)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
winnip 1 2023-11-16 11:54
14
0
索印数据库 没有尝试一下吗?
雪    币: 62
活跃值: (566)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-11-16 17:00
15
0
mark
雪    币: 243
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
蛋散h h 2024-3-19 12:05
16
0
你好,给个联系,有需求,有预算,
雪    币: 47
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_mzboxhri 2024-3-21 18:15
17
0
gtict 求新版qq解密。搞了一段时间,密钥都有了,就是解不开

有思路了么老铁

最后于 2024-3-21 18:16 被mb_mzboxhri编辑 ,原因:
雪    币: 2395
活跃值: (2257)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
gtict 2024-3-24 01:11
18
0
mb_mzboxhri gtict 求新版qq解密。搞了一段时间,密钥都有了,就是解不开 有思路了么老铁
已经解开了
游客
登录 | 注册 方可回帖
返回