PC 微信逆向:破解聊天記錄文件!

作者:newx

鏈接:https://bbs.pediy.com/thread-251303.htm

聲明:本文僅用於技術交流

在電子取證過程中,也會遇到提取 PC 版微信數據的情況,看雪、52 破解和 CSDN 等網上的 PC 版微信數據庫破解文章實在是太簡略了,大多數只有結果沒有過程。經過反覆試驗終於成功解密了數據庫,現在把詳細過程記錄下來,希望大家不要繼續在已經解決的問題上過度浪費時間,以便更投入地研究尚未解決的問題。

通過查閱資料得知,與安卓手機版微信的 7 位密碼不同,PC 版微信的密碼是 32 字節(64 位),加密算法沒有說明,但是可以通過 OllyDbg 工具從內存中獲取到這個密碼,然後通過一段 C++ 代碼進行解密。

 首先下載 OllyDbg 2.01 漢化版,我用的版本如下圖所示:

運行 OllyDbg,然後運行 PC 版微信(需要下載客戶端的,不是網頁版)。先不要點擊登錄按鈕。

切換到 Ollydbg 界面:

點擊文件菜單,選擇 “附加”,在彈出的對話框中找到名稱爲 WeChat 的進程,其窗口名稱爲 “登錄”。然後點擊 “附加”。

附加成功後 OllyDbg 開始加載,成功加載後可以看到最上面 OllyDbg 後面有 WeChat.exe 的字樣:

在查看菜單中選擇 “可執行模塊”:

找到名稱爲 WeChatWin 的模塊,雙擊選中。爲了方便觀察,在窗口菜單中選擇水平平鋪。在 CPU 窗口標題欄可以看到 “模塊 WeChatWin” 字樣。

在插件中選擇 “StrFinder 字符查找” 中的“查找 ASCII 字符串”(注意如果下載的 OllyDbg 版本不對,可能沒有相關插件,因此一定要找對版本),要稍微等一會兒,會出現搜索結果的窗口。

在此窗口點擊鼠標右鍵,選擇 “Find”,在搜索框中輸入 “DBFactory::encryptDB”。

會自動定位在第一處,但我們需要的是第二處,即 “encryptDB %s DBKey can’t be null” 下面這一處。可以用鼠標點擊滾動條向下,找到第二處,用鼠標雙擊此處。

 在 CPU 窗口中可以看到已經定位到了相應的位置。用鼠標點擊滾動條向下翻。

下面第六行應該是 TEST EDX,EDX,就是用來比對密碼的彙編語言代碼。在最前面地址位置(本文中是 0F9712BA)雙擊設置斷點(設置斷點成功則地址會被標紅,而且可以在斷點窗口中看到設置成功的斷點)

點擊 “運行” 按鈕(或者在調試菜單中選擇“運行”),這時寄存器窗口中的 EDX 的值應該是 00000000。

切換到微信登錄頁面,點擊登錄,然後到手機端確認登錄。這是 OllyDbg 界面中的數據不斷滾動,直到 EDX 不再爲全 0 並且各個窗口內容停止滾動爲止。

在 EDX 的值上面點擊鼠標右鍵,在彈出的菜單裏面選擇 “數據窗口中跟隨”,則數據窗口中顯示的就是 EDX 的內容。

圖示中從 0B946A80(這個數值是變化的,不但每臺電腦不同,每次調試也可能完全不同)到 0B946A9F 共 32 個字節就是微信的加密密碼,本圖中就是:

“53E9BFB23B724195A2BC6EB5BFEB0610DC2164756B9B4279BA32157639A40BB1”

一共 32 個字節,共 64 位。

得到這個之後,就可以關閉 OllyDbg 了,微信也會自動被關閉。

接下來就是解密過程。在看雪、52 破解等多個論壇中都有相關的 C++ 源碼,開始企圖使用 Dev-C++ 或者 C-Free 等輕量級 IDE 進行編譯,也使用過 Visual C++ 6.0 綠色精簡版,結果多次嘗試出現各種錯誤,反覆失敗,最終不得已使用 Visual Studio,並對代碼進行了一定的修正,終於調試成功。

正好 Visual Studio 2019 剛剛發佈直接到官方網站下載了社區版。

根據查到的資料,需要先安裝 openssl,爲了省事直接下載了最新的 Win64OpenSSL-1_1_1b,安裝後發現各種報錯,繼續查找資料發現原來 sqlcipher 使用的是低版本的 openssl,之後找到了一個 Win64OpenSSL-1_0_2r 也報錯,最後發現還是官方這個直接解壓縮的版本靠譜:

https://www.openssl.org/source/openssl-1.0.2r.tar.gz

把壓縮包直接解壓到任意目錄,比如 c:\openssl-1.0.2r

啓動 Visual Studio 2019 社區版(估計 Visual Studio 2008 以後的都應該可以,懶得找就直接官網下載最新的吧)

在啓動界面右下方選擇 “創建新項目”

滾動下拉條,在窗口中選擇 C++ 控制檯應用:

       給項目隨便起個名字,選擇保存位置:

然後點擊 “創建”,即可完成新項目創建。生成默認的 Hello World 代碼:

先要做好項目的基礎配置,之前調試失敗主要問題就出在這裏了。

在項目菜單中最下面選擇項目屬性 “dewechat 屬性”(這個跟設置的項目名稱一致)

對話框最左上角的配置後面,可以選擇配置的是 Debug 模式還是 Release 模式(Release 模式不包含調試信息,編譯完成的 exe 文件更小一些,但如果是自己用,這兩個模式沒有區別,配置了哪個,後面就要用哪個模式編譯,否則會報錯)

先選擇 C/C++ 下面的 “常規” 選項:

右邊第一條是 “附加包含目錄”,點擊右側空白處。在下拉框裏選擇 “編輯…”,在對話框中點擊四個圖標按鈕最左側的“新行” 按鈕,會生成一個空白行,點擊右側的“…”:

在彈出的對話框裏選擇剛剛安裝的 openssl 目錄(本文是 c:\openssl-1.0.2r)中的 include 目錄。

設置完成後如下:

然後選擇左側 “鏈接器” 下面的“常規”:

在中間位置,有一個 “附加庫目錄”,點擊右側空白處,選擇 openssl 目錄下的 lib 目錄,設置完成後如下:

最後點擊鏈接器下面的 “輸入”:

右側最上面有 “附加依賴項”,默認已經有一些系統庫,點擊右側內容,選擇 “編輯…”

這個沒有增加新行的按鈕,只能手工錄入或者拷貝文件名進去,需要增加上圖所示的兩個庫名稱。

設置完成後如下:

現在所有的設置都 OK 了,可以把代碼放進來編譯了。

由於太多網站轉載,而且很多有錯漏,已經搞不清原始代碼是哪位大神寫的了,其中有一些已經被廢棄的代碼,根據系統報錯提示進行了替換,另外做了一個主要的變化就是之前的代碼是把數據庫名寫在變量中,但由於需要解密很多庫,爲了靈活,改爲輸入參數的方法,即在運行時帶參數運行或者根據提示輸入需要解密的數據庫文件名。

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[] = { 0x53,0xE9,0xBF,0xB2,0x3B,0x72,0x41,0x95,0xA2,0xBC,0x6E,0xB5,0xBF,0xEB,0x06,0x10,0xDC,0x21,0x64,0x75,0x6B,0x9B,0x42,0x79,0xBA,0x32,0x15,0x76,0x39,0xA4,0x0B,0xB1 };
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;
}

將之前默認的代碼全部清除,將以上代碼拷貝進去,保存。然後在工具條欄中選擇是 Debug 還是 Release 模式,是 x86 還是 x64(需要跟之前配置匹配,如果選了沒配置的模式會報錯。測試發現幾個選項沒有太大區別,建議默認),之後點擊 “本地 windows 調試器”(或者按 F5 鍵),如果前面的步驟操作都正確,應該可以完成編譯並自動運行,彈出一個命令行窗口,提示需要輸入文件名:

最下方顯示了生成的 exe 文件路徑,將這個文件拷貝到微信數據庫所在的目錄,一般是:

C:\Users\Administrator\Documents\WeChat Files********\Msg

其中 ******** 位置爲需要解密的微信 id,目錄內容如下:

如果要解密 ChatMsg.db,則在命令行窗口輸入指令 dewechat ChatMsg.db 回車即可。

解密成功後,會在目錄中生成 de_ChatMsg.db,用 sqlite 數據庫管理軟件打開即可。

本文主要是個驗證過程,沒有做什麼突破工作,目前的解密只能算是半自動過程,密碼算法部分的獲得是下一步需要研究的內容,希望大家共同努力!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/viAMP8xl3B9_ly7qsKdRKA