专栏名称: 吾爱破解论坛
吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调而奢华的技术交流与探索却
目录
相关文章推荐
豫法阳光  ·  河南知识产权专业调解组织化解著作权侵权纠纷 ·  16 小时前  
豫法阳光  ·  河南知识产权专业调解组织化解著作权侵权纠纷 ·  16 小时前  
浙江经信  ·  这214场活动,促成融资34.67亿元! ·  昨天  
浙江经信  ·  这214场活动,促成融资34.67亿元! ·  昨天  
幸福东台  ·  位于东台的重点工程再迎关键节点 ·  昨天  
全国网安标委  ·  关于举办2025年人工智能安全国家标准贯标应 ... ·  2 天前  
51好读  ›  专栏  ›  吾爱破解论坛

解开Windows微信备份文件

吾爱破解论坛  · 公众号  · 互联网安全  · 2025-04-08 11:22

正文

请到「今天看啥」查看全文



/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。







请到「今天看啥」查看全文