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.dll 和 roma_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-server 程序和电脑上的Python包 frida-tools。这里写得再详细些。 在手机上运行 frida-server
adb shell 进入Android终端后执行su命令,并在手机上"总是允许"bash的管理员权限。adb push 将 frida-server 传入手机的 /data/local/tmp 目录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
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_9D5490和sub_A0061C。

先来看sub_9D5490。我们在sub_9D5490的EVP_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