文章目录
- 写在前面
- 回顾
- 定位
- 切入点
- 断点调试
- 结论
- 离线解密工具
- 配置环境
- 代码
- 运行
- 写在后面
写在前面
前面的几篇文章,已经找到sqlite3_exec函数并调用,也实现了数据库在线备份,本篇文章,尝试定位保存数据库密码的基址,并编写一个离线解密工具。
回顾
在开始之前,要先回顾一下之前得到的结果:
数据库初始化
785BE313 8D55 CC lea edx, dword ptr [ebp-34]
785BE316 52 push edx
785BE317 8B08 mov ecx, dword ptr [eax]
785BE319 8B06 mov eax, dword ptr [esi]
785BE31B 8948 60 mov dword ptr [eax+60], ecx
785BE31E 8B0E mov ecx, dword ptr [esi]
785BE320 8B01 mov eax, dword ptr [ecx]
785BE322 FF50 4C call dword ptr [eax+4C] ; 单个数据库初始化
call dword ptr [eax+4C]
这条指令会初始化数据库,数据库句柄是在其内部获取到的,里面应该有调用sqlite3_open
,往里面追的话,可以找到一个更具体的位置:
7882067F 57 push edi
78820680 8D46 78 lea eax, dword ptr [esi+78]
78820683 B9 D8DD387A mov ecx, 7A38DDD8
78820688 50 push eax ; 数据库名
78820689 E8 A2BBF9FF call 787BC230 ; 获取数据库句柄
7882068E 8946 64 mov dword ptr [esi+64], eax ; 数据库句柄存储在eax中
再看一下sqlite3_open
函数:
sqlite3_open原型
/*
** Open a new database handle.
*/
SQLITE_API int sqlite3_open(
const char *zFilename,
sqlite3 **ppDb
){
return openDatabase(zFilename, ppDb,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0);
}
sqlite3_open汇编
794DACD0 55 push ebp
794DACD1 8BEC mov ebp, esp
794DACD3 8B55 0C mov edx, dword ptr [ebp+C]
794DACD6 8B4D 08 mov ecx, dword ptr [ebp+8]
794DACD9 6A 00 push 0
794DACDB 6A 06 push 6
794DACDD E8 4EF7FFFF call 794DA430 ; openDataBase
794DACE2 83C4 08 add esp, 8
794DACE5 5D pop ebp
794DACE6 C3 retn
获取句柄的动作是在openDataBase这个函数中完成的。
定位
切入点
计划的切入点是call 787BC230
这条指令和sqlite3_open
函数,在微信中的基址分别为:WeChatWin.dll + 0x6D0689
、WeChatWin.dll + 0x138ACD0
。
断点调试
首先用OD启动微信,暂时不要登录,在上述两处地址下断,然后正常登录即可,会在WeChatWin.dll + 0x6D0689
处断下,参数如下:
06F78920 06F359C0 UNICODE "xInfo.db"
06F78924 00000008
06F78928 00000008
06F7892C 00000000
06F78930 00000000
06F78934 06C0A6C8 UNICODE "wxid_xxxxxxInfo"
06F78938 00000017
06F7893C 00000017
06F78940 00000000
06F78944 00000000
这里引用的是句柄结构体+0x78处的地址。另外一个参数是NULL指针,不知道有什么用。
点击运行,如果提示你已退出微信
,就重新来过吧,注意不要停滞太久。
按预期,点击运行以后应该会断在sqlite3_open
,也就是WeChatWin.dll + 0x138ACD0
处,然而并没有,而是断在了WeChatWin.dll + 0x6D0689
,直接去初始化下一个库(表)了。
看来并不是调用sqlite3_open
打开的数据库,那试试openDatabase
,下断位置:WeChatWin.dll + 0x138A430
:
6308A430 55 push ebp ; openDataBase
6308A431 8BEC mov ebp, esp
6308A433 83E4 F8 and esp, FFFFFFF8
6308A436 83EC 1C sub esp, 1C
6308A439 53 push ebx
6308A43A 33C0 xor eax, eax
6308A43C 895424 10 mov dword ptr [esp+10], edx
6308A440 56 push esi
...
下断后重启微信,点击登录,断下后点击运行,成功断在openDataBase
:
堆栈窗口里有0和6两个参数,还有两个参数在寄存器中,ecx是数据库绝对路径,edx是保存数据库句柄的指针,目前还是0:
保留数据窗口,按Ctrl + F9
执行到返回,发现获取到了数据库句柄:
按照正常的思路,就可以对该地址下一个内存写入断点,分析下句柄怎么来的,以及哪里引用了数据库密码,可是不得不告诉大家,这项工作可能费时且得不到结果(补充一下:openDataBase
这里数据库已经解密完成了,如果找引用也是往前找,可是堆栈里根本找不到openDataBase
的调用地址,这里可能要参考sqlcipher
的解密过程)。
前面吟唱那么久,只是给想继续深入研究的读者提供点思路。接下来提供本文的结论,定位过程取了巧,一段时间后位置就不再适用了。
结论
一开始找到的资料里,有一个比较重要的结论,PC端数据库密码长度是32个字节,换算到16进制是0x20,openDataBase
这个位置,数据库密码已经生成了,就在堆栈里,当然,是作为指针保存的,没有那么明显。更为明显的地方在WeChatWin.dll + 0x6D0689
处:
ebx这个寄存器,查看一下数据(注意:xinfo.db
这个库直接放过去,因为这个库没有加密;同一个库,只有第一次打开才需要解密):
单独看没什么特别的,但是可以再看看0x053E9440
处的数据:
这里的数据就很有说法了,053F6818
处是数据库密码,下面的0x20是长度,02D6DFB0
保存着0x5B(十进制91)大小的信息,作用是校验MD5(可以看看xinfo.db这个数据库,这个数据库没有加密,但是俺也不清楚怎么解析这91个字节信息),打码的地方是自己的wxid。
看看数据库密码:
选中的32个字节就是,为了找到一个基址,在CE中搜索字节数组:
053F6818
这个地址就不要了,以4字节搜索另外一个地址:
6488EFE4
== WeChatWin.dll + 0x222EFE4
,OD中看一下:
另外,如果你研究过登录标志位,会发现上图中选中的那个地址是多么的熟悉!
数据库密码定位到这里就结束了,思路可以作为参考,拿来当教程实在不合适,如果哪天那个地方没有数据库密码了,也是很正常的事情。
离线解密工具
仍然是那份拷来拷去的代码,但是看着网上的那些教程还是很头疼啊,只贴代码不讲如何配置环境,本文就把这个坑填上。
配置环境
在第一篇文章中,预先让大家下载了别人编译好的OpenSSL安装版,安装后,在你的安装路径下,就有include和lib两个文件夹:
新建一个VS空项目,添加附加包含目录:
添加附加库目录:
添加附加依赖项:
项目配置完毕。
代码
代码如下,注意将数据库密码修改成你自己的,或者从微信内存中读取,这块逻辑我就不写了。
using namespace std;
#include <Windows.h>
#include <iostream>
#include <openssl/rand.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <openssl/hmac.h>
#pragma warning(disable:6385)
#undef _UNICODE
#define SQLITE_FILE_HEADER "SQLite format 3"
#define IV_SIZE 16 //16或者改成 0x30
#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
// 数据库密码,替换成你自己的,一共32位,我省略了一些
unsigned char pass[] = { 173,240,179,128,199 };
char dbfilename[50];
int Decryptdb();
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("打开文件出错!程序已退出!");
return 0;
}
fseek(fpdb, 0, SEEK_END);
long nFileSize = ftell(fpdb);
fseek(fpdb, 0, SEEK_SET);
unsigned char* pDbBuffer = new unsigned char[nFileSize];
cout << nFileSize << endl;
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_new();
HMAC_CTX_reset(hctx);
HMAC_Init_ex(hctx, mac_key, sizeof(mac_key), EVP_sha1(), NULL);
HMAC_Update(hctx, pTemp + offset, DEFAULT_PAGESIZE - (size_t)reserve - (size_t)offset + IV_SIZE);
HMAC_Update(hctx, (const unsigned char*)&nPage, sizeof(nPage));
HMAC_Final(hctx, hash_mac, &hash_len);
HMAC_CTX_free(hctx);
if (0 != memcmp(hash_mac, pTemp + DEFAULT_PAGESIZE - reserve + IV_SIZE, sizeof(hash_mac)))
{
printf("\n 哈希值错误! 程序已退出!\n");
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+");
{
if (fp) {
fwrite(pDecryptPerPageBuffer, 1, DEFAULT_PAGESIZE, fp);
fclose(fp);
}
}
nPage++;
offset = 0;
pTemp += DEFAULT_PAGESIZE;
}
printf("\n 解密成功! \n");
return 0;
}
然后编译即可。
运行
运行时如果报错DLL不存在,请将OpenSSL的bin目录下两个dll文件拷贝到exe同级目录。当然,也可以使用静态库进行编译。
写在后面
数据库系列的文章总算是写完了,希望对大家有所帮助。如有错误欢迎指正。
最后附一下github地址:ComWeChatRobot 有用的话麻烦给个star,感谢感谢~