微信聊天记录解密
前言:该文是从网上各文章中东拼西凑出来的一篇纯复现文章,分享该文章目的是为了让没基础的朋友也能够复现。也请看到该篇文章的朋友严禁将该方法用于非授权的测试及非法用途,否则后果自负。
目录
0x01 逆向获取密钥
0x02 解密本地数据库
0x03 CE寻找密钥的基址
0x04 实现获取动态密钥的代码
0x05 实战
0x06 总结
0x07 参考链接
0x01 逆向获取密钥
微信的数据库存储在本地
好友微信id等各种信息存储在该目录下
C:\Users\xxxxxx\Documents\WeChat Files\wxid_xxxxxxxxxx\Msg
和微信好友的聊天记录存储在multi目录下的MSG0.db数据库里
C:\Users\xxxxxx\Documents\WeChat Files\wxid_xxxxxxxxxx\Msg\Multi
解密这些数据库需要密钥,密钥是所有数据库共用同一个密钥,所以逆向的时候找到解密其中一个数据库时用的密钥即可。
至于如何找到这个密钥,因为这些数据库都是通过AES加密,而AES的密钥是32位的,32的十六位进制是20,所以在逆向的过程中重点关注20这个数字。
下面开始详细的寻找密钥过程,因为水平有限,所以跟着前辈的路线走,目的就是为了找到密钥是多少
从下面这篇文章中知道可以通过 open db fail=%d ,error=%s 这个字符串取定位到密钥附近的代码
于是使用x32dbg去跟踪
使用x32dbg打开wechat.exe程序
会弹出微信的登录框
右键–>搜索–>所有模块–>字符串
在引用的搜索框中输入 open db fail=%d ,error=%s
选中这两行按F2设置断点
根据文章中的说明对红框中的代码设置断点
于是双击第一个断点,进入CPU页面,对je wechatwin.57A3DCDB设置断点
接下来重启
双击登录
此时跳到了刚才断点处
按F7单步步进,进入到 cmp dword ptr ds:[ebx+30],0
往下翻,对红框的内容设置断点
按F9重新回到了一开始的断点处
继续按F9跳到了push eax,再按F9跳到push esi,然后查看eax的值
选中EAX的值,右键–>在内存窗口中转到,可以看到下一个字节是数字20
得到了密钥的地址:88 4D 88 04
接下来跳转到地址04884D88获取密钥的值
右键–>转到–>表达式
输入刚才得到的地址04884D88
跳转过去后,取32个字节,即红框里的内容
04884D88 6F 50 9B 68 38 4C 46 68 8B A0 8C 54 E1 0A DA 74 oP.h8LFh. .Tá.Út
04884D98 21 30 09 87 52 84 43 AF 8E CF C9 14 F4 DF 95 19 !0..R.C¯.ÏÉ.ôß..
此时密钥拿到了,接下来尝试解密
0x02 解密本地数据库
需要安装老版本的openssl,下载地址:https://slproweb.com/download/Win32OpenSSL-1_0_2u.exe
默认安装就行
接下来需要配置解密代码的项目,用Visual Studio创建C++控制台应用
根据顺序将openssl的include添加进去
同理添加lib目录
如上增加两个库名称
配置如上设置就可以了,接下来编译代码,代码如下。每次修改pass里的密钥即可。
using namespace std;
#include <Windows.h>
#include <iostream>
#include <openssl/rand.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <openssl/hmac.h>
#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得到的32位pass。
unsigned char pass[] = { 0xba,0x9f,0x56,0x04,0xc7,0x6b,0x4a,0xd5,0x8c,0xf7,0xaa,0x46,0x78,0x2c,0x7f,0xb7,0x2b,0x03,0xc5,0xfd,0x00,0x10,0x41,0xdb,0x9a,0xd6,0x6b,0x5f,0x6b,0x16,0x28,0xe1 };
char dbfilename[50];
int Decryptdb();
int CheckKey();
int CheckAESKey();
int main(int argc, char* argv[])
{
if (argc >= 2) //第二个参数argv[1]是文件名
strcpy_s(dbfilename, argv[1]); //复制
//没有提供文件名,则提示用户输入
else {
cout << "请输入文件名:" << endl;
cin >> dbfilename;
}
Decryptdb();
return 0;
}
int Decryptdb()
{
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
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");
return 0;
}
编译出来exe,运行的时候跟上要解密的db文件即可
解密结束后会在当前目录生成dec_MicroMsg.db文件。然后用打开sqlite3数据库的连接工具打开即可。
0x03 CE寻找密钥的基址
下一步是如何获取动态密钥
方法是使用CE取寻找密钥的基址
先下载CE,下载地址
https://d7qe0znl1rfet.cloudfront.net/installer/575474003088649/859880
再下载中文语言包
https://www.cheatengine.org/download/ch_cn.zip
安装CE后,将中文语言包解压后放到该目录下
编辑language,ini文件
此时打开CE即使中文版,选择打开进程
选择微信进程
通过之前找到的密钥去定位基址
6F509B68384C46688BA08C54E10ADA7421300987528443AF8ECFC914F4DF9519
字节数组–>搜索数组–>Hex–>密钥值
点击首次扫描,得到地址05E4BE68
新的扫描–>所有类型–>精确数值–>Hex–>上一步的地址–>首次扫描
得到了偏移量,即:WeChatWin.dll+1AD1F8C
0x04 实现获取动态密钥的代码
最后就可以通过代码去获取,步骤则是
通过WeChatMainWndForPC获取微信的句柄,通过该句柄获取到微信的进程id,再找到微信的安装目录,拼接路径得到WeChatWin.dll的路径,通过上一步的偏移量得到密钥的值。
同理获取微信id和微信名的偏移量
字符串搜索微信id,找到了很多地址,选择05E开头的,因为密钥的偏移量是05E开头的,那么猜测微信id可能也存在同一个dll里,即WeChatWin.dll里
得到偏移量
同理可以获取微信名称的偏移量。
核心代码如下:
getKey.h
#pragma once
#include <stdio.h>
#include <Windows.h>
#include <tchar.h>
#include <direct.h>
#include <stdlib.h>
#include <TlHelp32.h>
#include <string>
#include <strstream>
#include <list>
#include <Psapi.h>
#pragma comment (lib,"Psapi.lib")
#pragma comment(lib, "Version.lib")
using namespace std;
const string wxVersoin1 = "2.9.0.123";
#define version1 0x16B4D50
#define wxid_addr1 0x16B4914
#define wxname_addr1 0x16B498C
const string wxVersoin2 = "2.9.0.112";
#define version2 0x16B4C70
#define wxid_addr2 0x16B4834
#define wxname_addr2 0x16B48AC
const string wxVersoin3 = "2.9.5.56";
#define version3 0x17744A8
#define wxid_addr3 0x1774054
#define wxname_addr3 0x17740CC
const string wxVersoin4 = "3.2.1.154";
#define version4 0x1AD1F8C
#define wxid_addr4 0x1AC2100
#define wxname_addr4 0x1AD1BAC
DWORD GetProcessIDByName(const char* pName);
PVOID GetProcessImageBase(DWORD dwProcessId, const char* dllName);
DWORD IsWxVersionValid(WCHAR* VersionFilePath);
int GetdbKey(unsigned char* databasekey, unsigned char* wxid);
getKey.cpp
#include "getKey.h"
#include <iostream>
//判断微信版本,确定偏移
DWORD IsWxVersionValid(WCHAR* VersionFilePath)
{
string asVer = "";
VS_FIXEDFILEINFO* pVsInfo;
unsigned int iFileInfoSize = sizeof(VS_FIXEDFILEINFO);
int iVerInfoSize = GetFileVersionInfoSizeW(VersionFilePath, NULL);
if (iVerInfoSize != 0) {
char* pBuf = new char[iVerInfoSize];
if (GetFileVersionInfoW(VersionFilePath, 0, iVerInfoSize, pBuf)) {
if (VerQueryValue(pBuf, TEXT("\\"), (void**)&pVsInfo, &iFileInfoSize)) {
//主版本2.9.0.123
//2
int s_major_ver = (pVsInfo->dwFileVersionMS >> 16) & 0x0000FFFF;
//9
int s_minor_ver = pVsInfo->dwFileVersionMS & 0x0000FFFF;
//0
int s_build_num = (pVsInfo->dwFileVersionLS >> 16) & 0x0000FFFF;
//123
int s_revision_num = pVsInfo->dwFileVersionLS & 0x0000FFFF;
//把版本变成字符串
strstream wxVer;
wxVer << s_major_ver << "." << s_minor_ver << "." << s_build_num << "." << s_revision_num;
wxVer >> asVer;
}
}
delete[] pBuf;
}
printf("var = %s\n", asVer.c_str());
if (asVer == wxVersoin1)
return version1;
else if (asVer == wxVersoin2)
return version2;
else if (asVer == wxVersoin3)
return version3;
else if (asVer == wxVersoin4)
return version4;
else
return 0;
return version4;
}
//通过pid和模块名获取基址
PVOID GetProcessImageBase(DWORD dwProcessId, const char* dllName)
{
PVOID pProcessImageBase = NULL;
MODULEENTRY32 me32 = { 0 };
me32.dwSize = sizeof(MODULEENTRY32);
// 获取指定进程全部模块的快照
HANDLE hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if (INVALID_HANDLE_VALUE == hModuleSnap)
{
return pProcessImageBase;
}
// 获取快照中第一条信息
BOOL bRet = ::Module32First(hModuleSnap, &me32);
while (strcmp((char*)me32.szModule, dllName) != 0)
{
Module32Next(hModuleSnap, &me32);
}
pProcessImageBase = (PVOID)me32.modBaseAddr;
// 关闭句柄
::CloseHandle(hModuleSnap);
return pProcessImageBase;
}
//获取数据库密钥
int GetdbKey(unsigned char* databasekey, unsigned char* wxid)
{
HWND phandle = FindWindow("WeChatMainWndForPC", NULL);//获取句柄
if (!phandle)
{
printf("%d\n", GetLastError());
return 0;
}
DWORD pid;
GetWindowThreadProcessId(phandle, &pid);//获取进程id
if (!pid)
{
return 0;
}
printf("wechat pid = %d\n", pid);
HANDLE mProc = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
if (mProc == NULL)
{
return 0;
}
wchar_t path[MAX_PATH];
if (!GetModuleFileNameExW(mProc, NULL, path, MAX_PATH))
return 0;
else
{
// cout << "path" << path << endl;
for (int i = 0; i < sizeof(path); ++i) {
cout << (char)path[i];
if (path[i] == '.')
{
wcscpy_s(&path[i], sizeof(L"Win.dll"), L"Win.dll");
break;
}
}
}
cout << endl;
for (int i = 0; i < sizeof(path); ++i) {
cout << (char)path[i];
}
//DWORD WxDatabaseKey = IsWxVersionValid(path);
//if (!WxDatabaseKey)
// return 0;
DWORD WxDatabaseKey = version4;
//获取WeChatWin的基址
DWORD base_address = (DWORD)GetProcessImageBase(pid, "WeChatWin.dll");
// printf("dllbase_address = %p\n", base_address);
DWORD dwKeyAddr = base_address + WxDatabaseKey;
//printf("Addr = %p\n", dwKeyAddr);
int addr = 0;
DWORD dwOldAttr = 0;
//获取数据库密钥
ReadProcessMemory(mProc, (LPCVOID)dwKeyAddr, &addr, 4, NULL);
//printf("key addr = %x\n", addr);
ReadProcessMemory(mProc, (LPCVOID)addr, databasekey, 0x20, NULL);
unsigned char wxname[100] = { 0 };
//获取微信昵称
DWORD wxAddr = base_address + wxname_addr4;
//printf("wxAddr = %x\n", wxAddr);
ReadProcessMemory(mProc, (LPCVOID)wxAddr, wxname, 100, NULL);
printf("\nname: %s\n", wxname);
//获取微信id
wxAddr = base_address + wxid_addr4;
//printf("wxAddr = %x\n", wxAddr);
ReadProcessMemory(mProc, (LPCVOID)wxAddr, &addr, 4, NULL);
//printf("wxid addr = %x\n", addr);
ReadProcessMemory(mProc, (LPCVOID)addr, wxid, 100, NULL);
printf("wxid: %s\n", wxid);
//打印密钥
printf("key: ");
for (int i = 0; i < 0x20; i++)
{
printf("%02x", databasekey[i]);
}
return 1;
}
wechatKey.cpp
// wechatKey.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include "Getkey.h"
unsigned char pass[33] = { 0 };
unsigned char filepath[256] = { 0 };
unsigned char wxid[256] = { 0 };
int main()
{
std::cout << "Hello World!\n";
GetdbKey(pass, wxid);
}
运行工具得到密钥
0x05 实战
下载MSG0.db文件
获取密钥
本地解密得到聊天数据内容
总结
每一个微信号在每一台机器上面的密钥是固定的,所以只要目标机器登录了微信,那么就可以通过该工具动态获取密钥。然后拖回MSG0.db聊天记录的数据库,解密即可。
参考链接
核心思路 https://www.xuenixiang.com/thread-2409-1-1.html
https://bbs.pediy.com/thread-251303.htm
https://www.dongzt.cn/archives/495.html
https://www.52pojie.cn/thread-1084703-1-1.html
https://blog.csdn.net/weixin_30230009/article/details/105100181
配置openssl教程 https://bbs.pediy.com/thread-251303-1.htm
基址寻找 https://zhuanlan.zhihu.com/p/95932843
核心代码 https://github.com/A2kaid/Get-WeChat-DB