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

移动安全 9个月前 admin
206 0 0




前言


本文以教学为基准、本文提供的可操作性不得用于任何商业用途和违法违规场景。

本人对任何原因在使用本人中提供的代码和策略时可能对用户自己或他人造成的任何形式的损失和伤害不承担责任。






软硬件环境


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






Android数据库:SQLite


想要进行微信数据库的逆向必须先了解其采用的是何种数据库及采用的防护手段!

Android中的SQLite是一种轻量级的关系型数据库,它是Android平台中默认的本地数据库存储解决方案。SQLite在Android系统中广泛使用,可用于存储和检索应用程序中的数据。


SQLite的优点包括:

1、轻量级:SQLite非常轻便,它的库文件非常小,可以轻松嵌入到Android应用程序中。

2、高效:由于SQLite是一个本地数据库,它可以快速地执行读写操作,并且具有非常高的性能。

3、可靠性:SQLite是一个可靠的数据库解决方案,它可以确保数据的完整性和一致性。


4、跨平台:SQLite可以在各种不同的操作系统和平台上运行,因此可以轻松地将数据从一个平台移植到另一个平台。

but,SQLite却有一个致命的缺陷:不支持加密。

因此存储在SQLite中的数据可以被任何人轻易地查看。如果是普通的数据还好,但是当涉及到一些账号密码,聊天内容或者个人信息的时候,我们的应用就会面临严重的安全漏洞隐患。

所以,需要在开发应用时对数据库进行加密,目前对SQLite有两种加密方式:

1、对写入数据库的数据进行加密。
2、对整个数据库文件进行加密。

而微信所采取的方式便是第二种:对整个数据库文件进行加密。






需求分析


微信的数据库,位于本地的/data/data/com.tencent.mm/MicroMsg/921dxxxxxxxxxx4d/路径下,名称为:EnMicroMsg.db,最后一个文件夹每个手机可能不同,需自己进入定位下:


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


将其导出后拖入sqlcipher软件中,可以看到需要输入密钥才能进行查看数据:


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

ok,那么我们的需求就是获取到解密数据库的密钥。






密钥获取


将apk拖入jadx,等待反汇编完成,腾讯有着自己的加壳方案(腾讯乐固),但并未在微信中使用,应是加壳后会影响使用性能,而微信这么一个全国几亿人在用的软件对性能的要求出不了一丝偏差。

对SQLite有所了解的应该都知道,他有着自己所定义的api,要想对其进行操作必定会用到SQLiteDatabase这个类,搜索可得:


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


双击进入后,查找open函数,查看open函数的具体实现:


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

要想打开数据库,那么必定需要密钥,看open函数传入三个参数,其中第一个bArr数组无疑是最可疑的。


使用frida编写hook脚本:


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软件输入该密钥点击确定:


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

可以看到整个数据库的内容已经可以正常查看了,那么接下来需要关心的就是密钥的生成方式了!






密钥生成分析


6.1 堆栈分析


得到了密钥,但是不知道密钥如何生成的,这种奇耻大辱岂是我辈逆向人员可以忍受的,必须搞它!


简单更改下hook代码,加入堆栈打印,已便于观测其执行流程:


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,跟进查看:


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

编写hook代码对e.r方法进行hook查看:


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()方法,跟进查看:
Android 取证之微信8.0.38版本数据库解密分析

没有什么可分析的代码,继续往上分析:com.tencent.mm.ir3.f.n()和com.tencent.mm.ir3.f.m()
Android 取证之微信8.0.38版本数据库解密分析

参数很多,hook看看,hook代码:


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.bb@4056,268557265=.a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb@488,1810537579=.e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb@01336,20610547=.d@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.bb@4056,268557265=.a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb@488,1810537579=.e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb@01336,20610547=.d@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.bb@4056,268557265=.a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb@488,1810537579=.e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb@01336,20610547=.d@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.bb@4056,268557265=.a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb@488,1810537579=.e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb@01336,20610547=.d@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()函数中:
Android 取证之微信8.0.38版本数据库解密分析

看参数二:r50.z.b().g(),跟进查看:
Android 取证之微信8.0.38版本数据库解密分析

继续跟进:C4827e.a方法:
Android 取证之微信8.0.38版本数据库解密分析

貌似是动态生成的,可反复操作n次,其值总是1721974820,这不经让我想到一种可能它会不会是从某个文件中取出的?


在jadx中搜索该值,把所有选项都勾选了,可以看到并无任何结果:


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

我又把方向转向apk的私有目录中,将其导出都搜索,果不其然在auth_info_key_prefs.xml文件中找到了该值:


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


看参数三:pj.r.f(true),跟进查看:


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

三元表达式,可以看出是写死”1234567890ABCDEF”。


6.3 加密方式分析


那么是怎么通过这两个参数变化生成了密钥值呢?


必然是经过某些算法得出的密钥,至于是什么算法,可以有两种方式确认:

一:继续分析apk源码,耗时久。

二:上算法通杀脚本,只要是常见的算法全给它hook一边从而确定采用了何种算法。


那我肯定是采用性价比最高的方法,直接上算法通杀脚本hook一遍,可得:


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

标准MD5算法得出的值,只是密钥只采用了值的前7位,先把hook脚本给一下,太多了有限制,这边仅把MD5的脚本贴上:


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还原:


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在后。






总结


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




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


看雪ID:行简

https://bbs.kanxue.com/user-home-945390.htm

*本文为看雪论坛优秀文章,由 行简 原创,转载请注明来自看雪社区

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

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复


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


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

球分享

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

球点赞

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

球在看

原文始发于微信公众号(看雪学苑):Android 取证之微信8.0.38版本数据库解密分析

版权声明:admin 发表于 2023年8月4日 下午6:00。
转载请注明:Android 取证之微信8.0.38版本数据库解密分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...