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;
};
});
|
[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.bb
a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb
e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb
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
a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb
e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb
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
a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb
e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb
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
a@d405a6a,268557265=gi.bbb@7eefb5b, -491946003=gi.bb
e@4b8eff8,1810537579=gi.bbf@dabecd1, 1692712704=gi.bb
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()函数中:
看参数二: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):
new_md5
=
md5()
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 总结
想要在取证领域有所建设对各类数据库的结构及基础知识点的了解是必不可少的,现今各类厂商对数据的保护也是是越来越强,我也碰到过许多自定义的文件格式搭配繁琐的加解密方式进行数据保护,相对而言微信在这一块做得防护措施并不是很强,但也需要一定的知识积累,感谢各位看官看完,免费不花钱,点个赞加个关注再走吧。