PC微信逆向:两种姿势教你解密数据库文件
2019-08-08 08:00:59 Author: mp.weixin.qq.com(查看原文) 阅读量:58 收藏

本文作者:鬼手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);
DWORD dwOldAttr = 0;VirtualProtect(pAddr, 0x20, PAGE_EXECUTE_READWRITE, &dwOldAttr);
memcpy(databasekey, pAddr, 0x20);
VirtualProtect(pAddr, 0x20, dwOldAttr, &dwOldAttr);

定位数据库文件句柄

在拿到数据库密码之后,我们还需要对数据库文件进行解密,解密完成之后才能查询数据库。那么有没有更好的方法可以不需要获取密码也不需要解密数据库文件就能直接进行数据库的查询操作呢?当然是有的,就是通过微信的数据库句柄!

关于微信数据库句柄

微信的数据库句柄在一些地方会经常用到,比如查询好友的详细信息的时候,需要传入一个数据库的句柄。然后通过句柄去查询信息,最后返回好友详细信息。

如果我们直接拿到密码,然后对数据库进行解密,再查询好友信息,这种方法当然也是可以的。但是拿到的数据并不是实时的。

如果我们拿到这个数据库的句柄,就能实时的去查询好友的详细信息了,而且也不需要进行解密和获取数据库密码的操作了。

获取微信数据库句柄的思路

找微信数据库句柄的思路和找数据库密码的思路是一样的,微信在点击登录的时候,肯定是要打开本地的数据库,然后获得一个句柄,所以我们可以通过在CreateFileW下断点,接着单步跟踪,就能找到数据库的句柄

定位微信的数据库句柄

在 CreateFileW 下断,当微信读取数据库文件时让程序断下。

接着来到 CreateFileW 的返回地址处,点击 K 查看调用堆栈

经过排查,这个地址的 call 最像我们需要的找的 call,在这个 call 的地址下断,点击 F9 运行

程序断下,此时 ecx 指向数据库文件的路径

edx 指向一个空的缓冲区,那么这个就非常像我们要找的 call

单步步过这个 call,发现缓冲区里写入了一个地址,那么就可以确定这个就是我们要找的 call,只要我们 HOOK 这个地址,那么就能拿到所有的数据库文件的句柄了。而数据库的名称就在堆栈里,可以自己去找到偏移然后获取数据。

至于代码,等我研究下怎么调用 SQlite 再告诉你们,最后附上用代码解密数据库的工程。

推荐阅读

原创 PC 端微信技术研究之保存聊天语言

原创 微信PC端技术研究(3)-如何找到消息发送接口

原创 PC微信逆向:发送与接收消息的分析与代码实现

原创 PC微信逆向:分析发送xml名片call


文章来源: http://mp.weixin.qq.com/s?__biz=MzI5MDQ2NjExOQ==&amp;mid=2247490990&amp;idx=1&amp;sn=48e13378412610529200c939565f02ed&amp;chksm=ec1e2d86db69a490e6a59ab68a6ea6611c9db948ab2220e3400b7fcb93c5c333b9dd697f4c15#rd
如有侵权请联系:admin#unsafe.sh