正文
/data/local/tmp/frida-server
pip install frida-tools
frida -U 微信 -l hook.js
这里使用的Frida脚本
hook.js
需要自己编写。不过,在JADX中右击代码中相关方法的名称可以直接生成Frida代码,简直不要太方便。只要在Jadx给出的代码上添加些
console.log
把密钥打印出来就可以了(把字节按16进制打印出来方便阅读)。
复制代码 隐藏代码
function hookTest1(){
functionprinthex(arr) {
let ss = ''
for(let i=0; i < arr.length; i++){
var num = arr[i]
if (num 0) num = 0xFF + num + 1; // 补码计算
ss += num.toString(16).toUpperCase().padStart(2, '0') + ((i+1)%16 ? ' ' : '\n')
}
console.log(ss)
}
letAesEcb = Java.use("com.tencent.mm.jniinterface.AesEcb");
let C68396j = Java.use("e41.j");
C68396j["h0"].implementation = function (bArr, z15, bArr2) {
console.log(`\n================\nC68396j.m60842h0 is called: z15=${z15}, `)
console.log('【bArr】')
printhex(bArr)
console.log('【bArr2】')
printhex(bArr2)
let result = this["h0"](bArr, z15, bArr2);
console.log(`C68396j.m60842h0 result=${result} \n`);
printhex(result._a.value)
return result;
};
}
functionmain(){
Java.perform(function(){
hookTest1();
});
}
setImmediate(main);
可以看到,这里打印的密钥与Bakup.db的密钥相同。
解密
BAK_0_TXT
文件并提取聊天消息内容
下面就可以使用 ASE-ECB 解密BAK_0_TEXT消息片段了。这里提供一些示例代码。这里,我使用
blackboxprotobuf
(pip install bbpb)
直接解码protobuf。AES解密还是使用
pycryptodome
注意,上面我们分析代码时已经强调过了,安卓微信加密消息片段使用的是16字节截取的密钥,所以这里解密也需要截取前面的16字节。
复制代码 隐藏代码
OFFSET = 356218880 # 片段的偏移
LENGTH = 1008 # 与长度
FILENAME = 'BAK_0_TEXT'
KEY = bytes.fromhex('66 32 64 30 63 35 32 65 32 33 36 39 32 63 30 37') # 密钥
withopen(FILENAME, 'rb') as f:
f.seek(OFFSET)
rawbytes = f.read(LENGTH)
from Crypto.Cipher import AES
cipher = AES.new(KEY, AES.MODE_ECB)
txtbytes = cipher.decrypt(rawbytes)
#print(len(txtbytes), txtbytes[-1], txtbytes[128:256])
#txtbytes = txtbytes[0:-txtbytes[-1]]
print(txtbytes)
import blackboxprotobuf
message,typedef = blackboxprotobuf.decode_message(txtbytes)
from pprint import pprint
pprint(message)
只看
txtbytes
就可以看到正常的聊天消息的样子——正常的数字、字母、XML标记等等。而
decode_message
就是Protobuf反序列化后的字典结构,但是由于我们不知道原始的Protobuf协议文件,所以输出的东西都像下面这样子,字典的键名是些不知所云的数字。要分析实际的字段含义,例如,对下面的这条消息,我们就在电脑微信自己的聊天消息
MicroMsg.db
数据库文件中找到同一条消息对应的行,对比数据库字段值与Protobuf解码出的字段值,尽量把解码字典的字段与
MicroMsg.db
数据库字段逐一匹配起来。
复制代码 隐藏代码
{'1': 1, # type 类型
'10': 0,
'13': {'1': 0},
'14': 0,
'15': 0,
'11': {}, # 媒体文件(如果有)
'16': 739315802669645222, # MsgSvrId
'17': 852963701, # MsgSequence
'18': 1720602127000, # Sequence
'19': 0,
'3': {'1': 'wxid_av0mvrd7aq8er0'}, # 发送者
'4': {'1': 'wxid_8xsk0zv10rut22'}, # 接收人
'5': {'1': '嗯嗯,我刚才去看了也不在[破涕为笑]'}, # 消息内容
'6': 4,
'7': 1720602127, # CreateTime
'8': {},
'9': 0},
。。。
在消息记录中找到媒体文件的名称,经由
MsgMedia
表中的
MediaId
字段和
MsgFileSegments
表的
MapKey
字段定位到文件名(如
BAK_0_MEIDA
)、偏移和长度。将相应的文件片段类似地提取、解密、存为文件即可。长度较长的文件会被分成为几个片段,需要分别解密然后按顺序拼接为完整的文件。注意同一个文件的不同片段只有最末片段才需要unpad。
提取解密
BAK_0_MEIDA
的代码我就不给了,自己写吧
:-)
探寻电脑微信中密钥的流转过程
现在来看看密钥的来源。
把在x64dbg中调试的涉及密钥处理的关键位置"dbKey can't be NULL",同样在IDA中定位。在IDA中打开wechatwin.dll,同样地查找字符串,再 List cross reference to 转到就行。然后,按下F5,进行反编译得到伪C代码,现在可以清晰地看到代码逻辑了。
x64dbg动态调试时,两个je对应伪码中if的两个条件(下图中高亮),判断条件中涉及好几处v64。不知道v64是什么,但v64来自
sub_1827FA350
,不妨进去看看。估计
sub_1827FA350
加载了v64,并通过引用返回到外面。
下面是
sub_1827FA350
的伪码。先看函数定义和参数,上一层的v64就是这里的函数参数
a2
,大概是个指针,
a2
又来自于
sub_18261D750
。
再进入
sub_18261D750
大概扫一眼,其实就是先开一段新的内存空间,然后用标准库
memmove
把老空间的东西复制至新空间。因此,
sub_18261D750(a2, v6, *(_DWORD *)(v3+120))
的意思就是把v6 以长度
*(v3+120)
复制到
a2
。
接着,回到上一层的
sub_1827FA350
,从
sub_18261D750
往前看函数的开头有如下几个变量,它们通过在
sub_1820118A0
返回的指针上进行偏移,获得了两个值:
复制代码 隐藏代码
v3 = sub_1820118A0() + 296;
v6 = *(_QWORD *)(v3 + 112);
if ( v6 && *(_DWORD *)(v3 + 120) )
于是深入
sub_1820118A0
。里面用到了一个变量
qword_185A25D48
,没有看到这个符号的传参或声明,似乎是全局变量。如果这个全局变量为空,则加线程锁并例化对象赋进去,如果已经有值了就直接返回,这是经典的单例模式。那个
qword_185A25D48
双击进去,确实是落在了DLL文件的.data节中,而且地址是固定的,即位于相对DLL基址的固定偏移,无疑是用作全局变量,就叫它pInfo吧。现在知道了
sub_1820118A0
返回pInfo全局变量指针。
因此,猜测QWORD值
*(QWORD *)(v3+112)
是指向密钥字节串的地址,字节串长度为
*(DWORD *)(v3+120)
,
这两个变量又是在固定地址的全局变量之上加以固定偏移得到的
。
。。。
接下来在x64dbg中定位到相应的位置,动态调试查看。
注意看下面有个字符串 "pInfo->m_key is NULL",可以使用这个标志性字符串在x64dbg中定位到上面分析的地方。这个字符串位于错误处理分支,于是把断点加在相关的判断跳转之前。简单分析下面这些断点。A3E5处是错误处理分支的开始,这个分支是从A3BE或A3C4跳转过来的。而上面的"pInfo==NULL"看起来是另一个错误处理分支,所以再把断点打在跨过错误处理分支的
jne
指令上。
这样,我们就打了三个断点,标记有"pInfo->m_key is NULL"的分支打了两个断点,"pInfo==NULL"打了一个断点,分别对应着C代码中的两个if。