解开Windows微信4.0版本的手机聊天记录备份文件
接下来,编写Frida脚本,针对这两个函数进行动态调试,记录密钥和初始化向量等信息。 在Frida脚本中,我们需要: 1. 钩住 EVP_EncryptInit_ex 函数 2. 记录每次调用的参数 3. 特别关注第二次调用 EVP_EncryptInit_ex 时传入的 key 和 iv 参数 编写 Frida 脚本如下: // 加载 Frida 的 Java API Java.perform(function () { // 获取微信进程的主线程 const mainThread = Java.choose('android.os.Looper', { filter: function (instance) { return instance.getMainLooper() !== 2025-11-9 16:0:0 Author: www.freebuf.com(查看原文) 阅读量:8 收藏

Windows微信4.0的"备份与恢复"功能同样可以将手机微信上的聊天记录存储到电脑。但新版本的"备份与恢复"功能是彻底推倒重来的,备份文件的结构也与老版本完全不一样。新版本的"备份与恢复"需配合新版本的手机微信使用。本文使用的版本为: 图片

新版"备份与恢复"的操作步骤与微信3.9版本类似:在电脑微信上进入菜单-"备份与迁移"-"备份与恢复"-"新建备份",接着在手机上设定时间范围等即开始备份。备份文件存储于如下路径: C:\Users\[用户名]\Documents\xwechat_files\Backup\[微信号] 图片

正常操作进行备份,合理选择备份范围使之仅包含一条文字消息,这样得到的备份目录是最简单的,目录结构如下:

wxid_0xt662v10ru629 - 微信号
    │  roam_device_info.dat
    │
    └─73cfbe036d741ddf3 - 设备标识
            │  alt_name.dat
            │  backup.attr *
            │
            └─files
                └─39 - 第多少次备份
                        │  backup_time.dat *
                        │  detail.dat
                        │  phoneid.dat *
                        │  phone_history.dat *
                        │  pkg.attr *
                        │  pkg_info.dat
                        │
                        └─98dffe08c400f2b... 按会话分组
                            ├─ChatPackage
                            │      1760266855000-1760266855000 * 按时间分组
                            └─Index
                                   1760266855000-1760266855000 *
                                   time.dat *
                                   wholetime.dat *

使用十六进制编辑器查看每个文件。树状图中标以星号的九个文件结构类似:文件开头是以RMFH为首的128字节;近结尾处RMFT字样至末尾的长度也为128字节;中间是一些看不出意义的字节,可能被加密过。剩下的非RMFH格式文件包含些许有实际含义的字符,但没有共通的结构,且文件不大,估计也没什么有价值的信息。 图片

备份目录的结构较为复杂。将这份备份恢复到手机,同时使用 FileActivityWatch 软件监视这一过程中微信进程Weixin.exe对备份文件的访问情况。

在电脑上打开备份列表后
    \73cfbe03\alt_name.dat
    \73cfbe03\files\39\pkg_info.dat
    \73cfbe03\files\39\detail.dat
在手机上启动恢复过程后
    \73cfbe03\alt_name.dat
    \73cfbe03\files\39\pkg.lock
    \73cfbe03\files\39\pkg_info.dat
    \73cfbe03\files\39\detail.dat
    \73cfbe03\backup.attr
    \73cfbe03\files\39\pkg.attr
    \73cfbe03\files\39\98dffe08c400f\ChatPackage\1760266855000-1760266855000

在启动恢复过程前,微信访问的文件都是非RMFH文件,而RMFH文件在启动恢复之后才被访问。据此推断聊天记录具体内容保存在ChatPackage 文件夹下的RMFH格式文件中。现需找出密钥和加密算法将RMFH文件解密。 下面顺着生成RMFH文件的路径,探寻文件加密的实现逻辑。

首先想到的还是从电脑微信介入。启动x64dbg并附加到微信进程上,查看所有已经加载的模块。打开"备份与恢复"界面后,发现新加载了两个名字很有意义的库,分别是roma_server.dllroma_immigrate.dll ,从文件名推测后者与"迁移"功能相关,故把关注点主要放在roma_server.dll上面。 到这些RMFH格式文件的结构特征明显,如果程序要实现RMFH文件结构的生成,则程序内部必然会存在RMFH和RMFT这两组字符。使用十六进制编辑器打开 roma_server.dll ,搜索"RMF"这组字符,没有结果。这说明roma_server.dll 没有生成RMFH格式文件的功能,即电脑接收到的就已经就是RMFH格式文件了。进一步猜测文件加密操作可能同样不在电脑上进行。 至此,我们需要深入手机微信程序寻找答案。

借助Android设置-"开发者工具"-"显示应用程序的包名"功能,得知微信备份界面的类名为 CreateRoamLitePkgUI。在Jadx中打开apk文件,定位到类 CreateRoamLitePkgUI,从此处着手层层深入分析逻辑。

定位到类 CreateRoamLitePkgUI
        button.setOnClickListener(new ViewOnClickListenerC93344f(this));
ViewOnClickListenerC93344f
        "begin save new package"
        C27130x0.f81265a.m27781h 里面的日志提示 GetAllBackupPackage
                        其中的调用 getAllPackagesAsync 是 JNI原生方法
        使用 countDownLatch.await() 与上面getAllPackagesAsync 的调用等待同步
        "WXGBACKUPPACKAGEPREFIX_" 是类似ID的东西,在电脑上的备份文件中也有相关内容
        sourceDeviceId.setBackupRange 设定备份范围,一种链式调用的编码风格
        下面着重分析一下下面这一行
        ((C99054b3) AbstractC99266l.m79336d(
          AbstractC3350d0.m3824a(createRoamLitePkgUI),
          null,
          null,
          new C93354k(build, createRoamLitePkgUI, null),
          3,
          null))
        .m79161N(
          new C93348h(
            c106441d,
            createRoamLitePkgUI,
            ProgressDialogC63600q3.m59059f
          );
        build存储了备份指令的一些信息,非常关键,进入调用它的C93354k
        C93348h可能涉及UI更新的一些功能,而m79161N可能是回调的一种写法,类似JavaScript中的Promise链式调用的风格
        AbstractC99266l.m79336d 进入看看里面的变量和枚举的命名,不是业务代码,而像是线程池之类的东西很抽象。
        我们还是进入C93354k一探究竟进入C93354k,重点关注 invokeSuspend
        其中的kotlin.coroutines.Continuation指明了这是个协程
        反复出现的c63598q12.m59041h 都是更新UI,与失败退出的处理分支联系在一起
        可以识别出正确处理的分支  if (i16 == 0)
        C27123v.f81245a.m27771e().createPackagesAsync 是 JNI原生方法包装,它的参数:
                backupPackage = this.f265414e 是上面提到的备份范围信息,外面包了一层AbstractC0787c0.m707c
                具体到这段代码 AbstractC0787c0.m707c(backupPackage) 代表的是仅包含一个元素的数组,这唯一一个元素是上面讲到的备份范围信息
                new C27135z(c72310n) C27135z是业务代码作为createPackagesCallbackcreatePackagesAsync 的形参
        通过 CreatePackagesCallbackBridge 实现回调
        真正的JNI方法 jniCreatePackagesAsync
        jniCreatePackagesAsync(
           this.nativeHandler,
           ZidlUtil.mmpbListSerializeToBasic(arrayList),
           createPackagesCallbackBridge)
        Native Handler 是什么
        ZidlUtil.mmpbListSerializeToBasic 是MicroMsgProtobuf的意思吗
                返回二维字节数组,其中的每个子数组都是原始数组对应元素的序列化表示

最后定位到原生方法jniCreatePackagesAsync 。在压缩软件中打开apk文件,提取位于lib/arm64-v8a的全部so动态库文件,接着不区分大小写地搜索函数名CreatePackagesAsync。搜索结果指向了 libaff_biz.so 这个动态链接库。使用IDA打开,继续深入分析备份文件的生成逻辑。 图片

等待IDA分析完成(转为idle小绿灯)。函数导出表("Exports"视图)中搜不到CreatePackagesAsync或类似的函数,说明这个原生方法是动态注册而非静态绑定的。 进入 JNI_OnLoad 函数,按F5反编译,尝试找出CreatePackagesAsync函数的入口。 JNI_OnLoad 似乎使用了静态数组结合遍历的写法,各种数据段绕来绕去,完全理不清其中逻辑,暂时放弃从正面逐层深入的做法。 那尝试在"String"视图中搜索与加密相关的字符串呢,比如说"key size"、 "key empty"什么的。似乎也没有什么发现。线索难道就在这里断了吗?我们需要进一步的思考...... 进入"Imports"视图搜索导入表,在尝试了"key"、"size"等多个字符串后,搜索"enc"(encrypt)出现了一些有意思的结果。 图片

这些"EVP"开头的函数显然来自OpenSSL库,指明 libaff_biz.so 调用了 OpenSSL 的加密功能接口。进一步搜索,发现还导入了更多EVP开头的函数。查阅OpenSSL加密函数的资料@,基本上这些导入的EVP函数涵盖了完整的OpenSSL加密过程。另外,注意到 EVP_aes_128/192/256_gcm 的导入,联想到使用EVP函数前必须导入算法描述字,推测OpenSSL库进行的加密只涉及 AES-GCM 算法。 我们不知道这些 GCM 加密是否就是用于生成RMFH文件,也不能确定 libaff_biz.so 中是否还存在其它加密函数。无论如何,不妨先动态调试一下这部分的加密逻辑。

Frida 带有 so 动态库注入功能,这次还是用它。使用的Frida版本是16.6.1。(我也试过用IDA远程调试器在so上打断点,成功过几次,但更多时候还是以微信崩溃告终。因此本文只介绍Frida注入so的调试方法。) 用一台root过的旧手机登录微信。使用真机有一个好处,就是可以原生执行ARM指令。(在模拟器中,ARM会先被转译为x86指令,故须提取内部生成的x86指令重新做静态分析。)(手机原来的系统基于Android 6.0,微信中按下"开始备份"按钮就失去响应,所以刷入了 Havoc-OS 3.12,基于 Android 10 的三方系统。刷系统也折腾了好久。root用的是 Magisk。)

配置Frida系列工具

首先配置Frida环境,包括手机上运行的 frida-server 程序和电脑上的Python包 frida-tools。这里写得再详细些。 在手机上运行 frida-server

  1. 从 Frida 的 Github Release 下载 ARM64 平台的 frida-server 可执行文件。
  2. 下载 Android Platform Tools 并解压出platform-tools文件夹。配置PATH环境变量,使adb.exe可运行。
  3. 手机里开发者选项打开ADB调试,USB连接到电脑。
  4. 电脑PowerShell运行 adb shell 进入Android终端后执行su命令,并在手机上"总是允许"bash的管理员权限。
  5. 使用 adb push 将 frida-server 传入手机的 /data/local/tmp 目录
  6. 执行 adb shell - su 为 frida-server 添加可执行权限并运行
PS > adb push E:\Downloads\frida-server-16.6.1-android-arm64 /data/local/tmp
PS > adb shell
markw:/ $ su
markw:/ # cd /data/local/tmp
markw:/data/local/tmp # chmod +x frida-server-16.6.1-android-arm64   # 指定执行权限
markw:/data/local/tmp # ./frida-server-16.6.1-android-arm64          # 运行 frida server

在电脑上配置 frida-tools

  1. 创建新目录,准备 Python 虚拟环境并激活
  2. 安装 frida-tools
  3. 执行命令 frida-ps -U 确定微信的进程名
PS > python -m venv .                       # 在当前目录创建虚拟环境
PS > .\Scripts\activate                     # 激活虚拟环境
(FridaTemp) PS > pip install frida==16.6.1  # 手动指定要安装的版本
(FridaTemp) PS > pip install frida-tools==13.6.0
(FridaTemp) PS > frida-ps -U                # 列出手机中活动的进程

选定注入位置

回到IDA,从EVP_EncryptInit_ex函数切入,寻找合适的注入位置。从导入表开始,层层查找交叉引用,定位到四处调用,前两处在一个函数内紧邻,后两处在另一个函数内紧邻。这两个大函数就是目标,分别是sub_9D5490sub_A0061C图片

先来看sub_9D5490。我们在sub_9D5490EVP_EncryptInit_ex函数处按F5反编译,分析具体逻辑。反编译所得代码简要摘录如下:

  v45 = EVP_aes_256_gcm(v43);
if ( (unsigned int)EVP_EncryptInit_ex(v42, v45, 0LL, 0LL, 0LL) == 1 )
  {
    v46 = (*(_BYTE *)(a1 + 16) & 1) != 0 ? *(_QWORD *)(a1 + 32) : a1 + 17;
    v47 = (v85 & 1) != 0 ? v87 : (char *)&v85 + 1;
if ( (unsigned int)EVP_EncryptInit_ex(v42, 0LL, 0LL, v46, v47) == 1
      && (unsigned int)EVP_EncryptUpdate(v42, v39, dest, v36, v38) == 1
      && (unsigned int)EVP_EncryptFinal_ex(v42, &v39[SLODWORD(dest[0])], dest) == 1
      && (unsigned int)EVP_CIPHER_CTX_ctrl(v42, 16LL, 16LL, &v110) == 1 )
    {
EVP_CIPHER_CTX_free(v42);
      v48 = 0;
// 省略 ... ...
goto LABEL_93;
    }
  }

这是调用 OpenSSL EVP 加密接口的典型套路,结合EVP_EncryptInit_ex 的函数定义来看,第一次调用指定加密算法,第二次调用才指定密钥key和初始化向量iv。

int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher,
                       ENGINE *impl, const unsigned char *key, const unsigned char *iv)
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl)
int EVP_EncryptFinal_ex(EVP_C

文章来源: https://www.freebuf.com/articles/456327.html
如有侵权请联系:admin#unsafe.sh