文章目录

  • 写在前面
  • 回顾
  • 定位
  • 切入点
  • 断点调试
  • 结论
  • 离线解密工具
  • 配置环境
  • 代码
  • 运行
  • 写在后面


写在前面

前面的几篇文章,已经找到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 + 0x6D0689WeChatWin.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

java企业微信通知解密后 企业微信pc版数据库解密_数据库


堆栈窗口里有0和6两个参数,还有两个参数在寄存器中,ecx是数据库绝对路径,edx是保存数据库句柄的指针,目前还是0:

java企业微信通知解密后 企业微信pc版数据库解密_java企业微信通知解密后_02


保留数据窗口,按Ctrl + F9执行到返回,发现获取到了数据库句柄:

java企业微信通知解密后 企业微信pc版数据库解密_微信_03


按照正常的思路,就可以对该地址下一个内存写入断点,分析下句柄怎么来的,以及哪里引用了数据库密码,可是不得不告诉大家,这项工作可能费时且得不到结果(补充一下:openDataBase这里数据库已经解密完成了,如果找引用也是往前找,可是堆栈里根本找不到openDataBase的调用地址,这里可能要参考sqlcipher的解密过程)。

前面吟唱那么久,只是给想继续深入研究的读者提供点思路。接下来提供本文的结论,定位过程取了巧,一段时间后位置就不再适用了。

结论

一开始找到的资料里,有一个比较重要的结论,PC端数据库密码长度是32个字节,换算到16进制是0x20,openDataBase这个位置,数据库密码已经生成了,就在堆栈里,当然,是作为指针保存的,没有那么明显。更为明显的地方在WeChatWin.dll + 0x6D0689处:

java企业微信通知解密后 企业微信pc版数据库解密_sqlite_04


ebx这个寄存器,查看一下数据(注意:xinfo.db这个库直接放过去,因为这个库没有加密;同一个库,只有第一次打开才需要解密):

java企业微信通知解密后 企业微信pc版数据库解密_微信_05


单独看没什么特别的,但是可以再看看0x053E9440处的数据:

java企业微信通知解密后 企业微信pc版数据库解密_逆向_06


这里的数据就很有说法了,053F6818处是数据库密码,下面的0x20是长度,02D6DFB0保存着0x5B(十进制91)大小的信息,作用是校验MD5(可以看看xinfo.db这个数据库,这个数据库没有加密,但是俺也不清楚怎么解析这91个字节信息),打码的地方是自己的wxid。

看看数据库密码:

java企业微信通知解密后 企业微信pc版数据库解密_sqlite_07


选中的32个字节就是,为了找到一个基址,在CE中搜索字节数组:

java企业微信通知解密后 企业微信pc版数据库解密_java企业微信通知解密后_08


053F6818这个地址就不要了,以4字节搜索另外一个地址:

java企业微信通知解密后 企业微信pc版数据库解密_sqlite_09


6488EFE4 == WeChatWin.dll + 0x222EFE4,OD中看一下:

java企业微信通知解密后 企业微信pc版数据库解密_java企业微信通知解密后_10


另外,如果你研究过登录标志位,会发现上图中选中的那个地址是多么的熟悉!

数据库密码定位到这里就结束了,思路可以作为参考,拿来当教程实在不合适,如果哪天那个地方没有数据库密码了,也是很正常的事情。

离线解密工具

仍然是那份拷来拷去的代码,但是看着网上的那些教程还是很头疼啊,只贴代码不讲如何配置环境,本文就把这个坑填上。

配置环境

在第一篇文章中,预先让大家下载了别人编译好的OpenSSL安装版,安装后,在你的安装路径下,就有include和lib两个文件夹:

java企业微信通知解密后 企业微信pc版数据库解密_数据库_11


新建一个VS空项目,添加附加包含目录:

java企业微信通知解密后 企业微信pc版数据库解密_数据库_12


添加附加库目录:

java企业微信通知解密后 企业微信pc版数据库解密_微信_13


添加附加依赖项:

java企业微信通知解密后 企业微信pc版数据库解密_数据库_14


项目配置完毕。

代码

代码如下,注意将数据库密码修改成你自己的,或者从微信内存中读取,这块逻辑我就不写了。

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,感谢感谢~