文章目錄
定位數據庫文件密碼
微信的數據庫使用的是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再告訴你們,最後附上用代碼解密數據庫的工程。
9運行
[外鏈圖片轉存中…(img-bkvnRcVG-1563613749040)]
程序斷下,此時ecx指向數據庫文件的路徑
[外鏈圖片轉存中…(img-ZLt43xxD-1563613749045)]
edx指向一個空的緩衝區,那麼這個就非常像我們要找的call
[外鏈圖片轉存中…(img-xzU5iL5H-1563613749048)]
單步步過這個call,發現緩衝區裏寫入了一個地址,那麼就可以確定這個就是我們要找的call,只要我們HOOK這個地址,那麼就能拿到所有的數據庫文件的句柄了。而數據庫的名稱就在堆棧裏,可以自己去找到偏移然後獲取數據。
至於代碼,等我研究下怎麼調用SQlite再告訴你們,最後附上用代碼解密數據庫的工程。
鏈接:YPSuperKey Unlockedhttps://pan.baidu.com/s/14fqLn8qUt2qr34UIb6Qd1Q
提取碼:t48w
目前微信機器人的成品已經發布,需要代碼請移步Github。還請親們幫忙點個star
https://github.com/TonyChen56/WeChatRobot