PC微信逆向:两种姿势教你解密数据库文件
本文作者: 鬼手56 (信安之路病毒分析小组成员 & 信安之路 2019 年度优秀作者)
定位数据库文件密码
微信的
数据库
使用的是 sqlite3,数据库文件在
C:\Users\XXX\Documents\WeChat Files\微信账号\Msg
这个路径下,
所有的数据库文件都是经过 AES 加密的,AES 的密钥是 32 位,而且所有数据库文件共用一个密钥,我们需要找到那个 AES 密钥才能进行解密,然后才能对数据库文件进行操作。
定位数据库密钥的思路
微信在登录时肯定要从数据库文件中获取历史聊天记录加载到程序中,然后我们才能看到之前的聊天记录。那么在微信读取数据库文件之前肯定要先打开数据库文件,所以 CreateFile 这个 API 就是我们的切入点。
在 API 断下之后怎么去找数据库的密码呢?可以根据 AES 的密钥长度为 32 位这个线索,32 也就是十六进制的 20,时刻注意 20 这个数字!
另外,在解密数据库的 call 中至少需要两个参数,一个是 AES 的密钥,另外一个是需要解密的数据库文件的路径。
还有一种方法是在内存中搜索数据库文件的名字,然后下访问断点。这种方案也是可行的。
获取数据库密钥的实战分析
CreateFileW 断点
打开微信,手机不要点击登录,用 OD 附加微信,在
CreateFileW
函数下断点,下好断点之后在手机上确认登录
在
CreateFileW
的参数中找一个 FileName 为 xxx.db 的,我们要在微信访问这个数据库文件的时候断下,然后从这里开始往下跟。一直跟到有数据库的密码的地方
常见错误
如果出现了这个错误,需要修改一下设置
将 StrongOD 和 OD 本身取消忽略所有异常,这个错误是因为多线程访问冲突引起的。
排查堆栈
在 CreateFileW 的返回地址下断,直接 F9 运行,CreateFileW 这个 API 我们是不需要看的
CreateFileW 断点断下来,那么现在应该怎么跟呢?肯定不能一直往下单步,虽然单步也能达到目标。
分析一下现在的状况,这个时候微信的数据库处于一个还未初始化,但是即将初始化的状态,我们可以在堆栈或者堆栈附近的地址找到关于数据库初始化相关的函数。然后在微信初始化完数据库之后单步往下跟。这样能省去很多麻烦
排查堆栈地址
直接找到第四个返回地址
这个函数传入了三个参数,虽然三个参数都没有什么价值。但是这个 call 稍微往下拉,你会发现一个字符串
这个函数的作用应该就是用来提示错误的,一般比较大的工程都会将错误提示信息写成一个函数,报错的时候会提示哪一个模块的哪一个 cpp 的哪一行出错了,以便最快定位到错误点。
再往上看会发现一个 je,用来跳过这个错误
根据这个错误提示的内容,我们现在可以百分百的确定打开数据库的操作已经完成!
单步跟踪
因为微信的数据库文件不止一个,所以我们不需要重启微信。直接在这个函数下断点,然后取消剩下的所有断点,按 F9 运行,程序断下。然后 F8 单步,
这里是我们遇见的第一个函数,看参数就知道不是我们想要的了,跳过 继续往下
第二个函数将数据库名和一个保存零的指针入栈,也跳过
第三个函数就很可疑了,这个 call 将三个参数压入堆栈,其中 eax 是一个结构体,里面保存一个地址和 0x20 这个数字,AES 的密钥正好是 32 位的,也就是十六进制的 0x20。
数据窗口跟随,前两行 0x20 个字节就是数据库的密钥了
各个参数含义如下:
用代码实现解密数据库
编译选项
工程需要包含 OpenSSL 的相关文件
解密代码
这份代码原作者是谁我已经不记得了 反正被拷来拷去拷了很多次了
#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <openssl/hmac.h>
using namespace std;
#pragma comment(lib, "ssleay32.lib")
#pragma comment(lib, "libeay32.lib")
#if _MSC_VER>=1900
#include "stdio.h"
_ACRTIMP_ALT FILE* __cdecl __acrt_iob_func(unsigned);
#ifdef __cplusplus
extern "C"
#endif
FILE* __cdecl __iob_func(unsigned i) {
return __acrt_iob_func(i);
#endif /* _MSC_VER>=1900 */
#undef _UNICODE
#define SQLITE_FILE_HEADER "SQLite format 3"
#define IV_SIZE 16
#define HMAC_SHA1_SIZE 20
#define KEY_SIZE 32
#define SL3SIGNLEN 20
#ifndef ANDROID_WECHAT
#define DEFAULT_PAGESIZE 4096 //4048数据 + 16IV + 20 HMAC + 12
#define DEFAULT_ITER 64000
#else
#define NO_USE_HMAC_SHA1
#define DEFAULT_PAGESIZE 1024
#define DEFAULT_ITER 4000
#endif
//pc端密码是经过OllyDbg得到的64位pass,是64位,不是网上传的32位,这里是个坑
unsigned char pass[] = { 0xc7,0x99,0x26,0xc0,0x36,0x6b,0x4f,0xee,0xb8,0xc7,0x48,0x83,0xaa,0xc9,0x6c,0x7e,0x0b,0x0a,0xda,0x3a,0x56,0x71,0x48,0xac,0xb9,0xda,0x4f,0x37,0x5c,0x4d,0x0b,58};
int Decryptdb();
int main() {
Decryptdb();
return 0;
int Decryptdb() {
const char* dbfilename = "ChatMsg.db";
FILE* fpdb;
fopen_s(&fpdb, dbfilename, "rb+");
if (!fpdb) {
printf("打开文件错!");
getchar();
return 0;
fseek(fpdb, 0, SEEK_END);
long nFileSize = ftell(fpdb);
fseek(fpdb, 0, SEEK_SET);
unsigned char* pDbBuffer = new unsigned char[nFileSize];
fread(pDbBuffer, 1, nFileSize, fpdb);
fclose(fpdb);
unsigned char salt[16] = { 0 };
memcpy(salt, pDbBuffer, 16);
#ifndef NO_USE_HMAC_SHA1
unsigned char mac_salt[16] = { 0 };
memcpy(mac_salt, salt, 16);
for (int i = 0; i < sizeof(salt); i++) {
mac_salt[i] ^= 0x3a;
#endif
int reserve = IV_SIZE; //校验码长度,PC端每4096字节有48字节
#ifndef NO_USE_HMAC_SHA1
reserve += HMAC_SHA1_SIZE;
#endif
reserve = ((reserve % AES_BLOCK_SIZE) == 0) ? reserve : ((reserve / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE;
unsigned char key[KEY_SIZE] = { 0 };
unsigned char mac_key[KEY_SIZE] = { 0 };
OpenSSL_add_all_algorithms();
PKCS5_PBKDF2_HMAC_SHA1((const char*)pass, sizeof(pass), salt, sizeof(salt), DEFAULT_ITER, sizeof(key), key);
#ifndef NO_USE_HMAC_SHA1
//此处源码,怀凝可能有错,pass 数组才是密码
//PKCS5_PBKDF2_HMAC_SHA1((const char*)key, sizeof(key), mac_salt, sizeof(mac_salt), 2, sizeof(mac_key), mac_key);
PKCS5_PBKDF2_HMAC_SHA1((const char*)key, sizeof(key), mac_salt, sizeof(mac_salt), 2, sizeof(mac_key), mac_key);
#endif
unsigned char* pTemp = pDbBuffer;
unsigned char pDecryptPerPageBuffer[DEFAULT_PAGESIZE];
int nPage = 1;
int offset = 16;
while (pTemp < pDbBuffer + nFileSize) {
printf("解密数据页:%d/%d \n", nPage, nFileSize / DEFAULT_PAGESIZE);
#ifndef NO_USE_HMAC_SHA1
unsigned char hash_mac[HMAC_SHA1_SIZE] = { 0 };
unsigned int hash_len = 0;
HMAC_CTX hctx;
HMAC_CTX_init(&hctx);
HMAC_Init_ex(&hctx, mac_key, sizeof(mac_key), EVP_sha1(), NULL);
HMAC_Update(&hctx, pTemp + offset, DEFAULT_PAGESIZE - reserve - offset + IV_SIZE);
HMAC_Update(&hctx, (const unsigned char*)& nPage, sizeof(nPage));
HMAC_Final(&hctx, hash_mac, &hash_len);
HMAC_CTX_cleanup(&hctx);
if (0 != memcmp(hash_mac, pTemp + DEFAULT_PAGESIZE - reserve + IV_SIZE, sizeof(hash_mac))) {
printf("\n 哈希值错误! \n");
getchar();
return 0;
#endif
if (nPage == 1) {
memcpy(pDecryptPerPageBuffer, SQLITE_FILE_HEADER, offset);
EVP_CIPHER_CTX* ectx = EVP_CIPHER_CTX_new();
EVP_CipherInit_ex(ectx, EVP_get_cipherbyname("aes-256-cbc"), NULL, NULL, NULL, 0);
EVP_CIPHER_CTX_set_padding(ectx, 0);
EVP_CipherInit_ex(ectx, NULL, NULL, key, pTemp + (DEFAULT_PAGESIZE - reserve), 0);
int nDecryptLen = 0;
int nTotal = 0;
EVP_CipherUpdate(ectx, pDecryptPerPageBuffer + offset, &nDecryptLen, pTemp + offset, DEFAULT_PAGESIZE - reserve - offset);
nTotal = nDecryptLen;
EVP_CipherFinal_ex(ectx, pDecryptPerPageBuffer + offset + nDecryptLen, &nDecryptLen);
nTotal += nDecryptLen;
EVP_CIPHER_CTX_free(ectx);
memcpy(pDecryptPerPageBuffer + DEFAULT_PAGESIZE - reserve, pTemp + DEFAULT_PAGESIZE - reserve, reserve);
char decFile[1024] = { 0 };
sprintf_s(decFile, "dec_%s", dbfilename);
FILE * fp;
fopen_s(&fp, decFile, "ab+");
fwrite(pDecryptPerPageBuffer, 1, DEFAULT_PAGESIZE, fp);
fclose(fp);
nPage++;
offset = 0;
pTemp += DEFAULT_PAGESIZE;
printf("\n 解密成功! \n");
system("pause");
return 0;
}
实际效果
运行程序
最后生成的
dec_ChatMsg.db
就是解密出来的文件,对比一下解密前后的文件
解密前
解密后 看到这个 MAGIC 头,不用验证我就知道已经解密成功了。接下来还是验证一下结果
用 Navicat 新建一个 SQLite 连接,
选择解密后的数据库
可以看到所有的表数据已经出现了。解密完成
动态获取数据库密钥
找到了密钥之后就结束了吗?这个密钥目前是写死的,如果变化的话,我们又要重新找,然后再次输入。所以我们需要动态获取到数据库密钥。想要动态获取数据库密钥,就必须定位到数据库密钥的基址。步骤如下:
直接在 CE 中搜索之前找到的密钥
接着依次搜索这两个地址,找到了一个绿色的基址
这个基址以指针的形式保存了微信数据库的密钥,这个地址就是我们要的微信密钥的基址了。
动态获取数据库密钥的代码如下:
char databasekey[0x20] = { 0 };
//获取WeChatWin的基址
DWORD dwKeyAddr = (DWORD)GetModuleHandle(L"WeChatWin.dll")+ WxDatabaseKey;
LPVOID* pAddr =(LPVOID*)(*(DWORD*)dwKeyAddr);