PC微信逆向--定位sqlite3_exec和数据库句柄

写在前面

最近在做PC端微信逆向,搞定了基本的收发消息,通讯录获取等,这期间遇到一个小小的问题,从通讯录获取到的内容不全,除非登录后手动点击过某个好友,不然获取不到头像、V3等,所以产生了解密数据库的想法。

收集资料

首先要明确一个想法,这个世界很大,想做的事情,至少90%都可能是别人做过的,有些人会把他们的经历分享到互联网上,我们可以利用这些知识,让自己不需要从0开始。
在这个日新月异的时代,技术存在时效性,找到的资料可以作为参考,我们需要做的,是更新替换那些过期的内容,让别人分享的东西在重新跑起来。
下面是我找到的一些资料,讲的都很详细:

  1. 一个系列教程,通过已打开的数据库句柄完成在线备份
    pc版微信 数据库备份
  2. 调用sqlite3_exec来执行SQL
    绕过加密来访问SQLite数据库
  3. 定位微信数据库密码
    解密数据库文件

信息提炼

通过阅读上面的文章,有些东西基本明确了:

  1. 微信使用的数据库是SQLite,并且使用AES加密
  2. 解密需要用到以下工具或开源库
    2.1 OllyDbg
    2.2 IDA Pro
    2.3 Visual Studio(2019或更高版本)
    2.4 OpenSSL
    2.5 SQLite3

工具下载

OllyDbg建议使用吾爱破解版,如果无法通过杀软,也可以去看雪工具下载。
OpenSSL使用别人编译好的1.1.1n安装版,尽量使用32位。
SQLite3需要3.28.0的源码包,可以去官网下载
IDA也可以在看雪工具找到。
VS需要去微软官网下载安装工具,完成在线安装。
此外,最好安装CE,可以在吾爱破解的工具包中找到,方便进行内存搜索。

如果已准备好上述工具,就让我们开始吧。

目标

需要分步实现下面的目标:

  1. 定位sqlite3_exec函数
  2. 获取微信打开的数据库句柄
  3. 调用微信内部的sqlite3_exec执行SQL
  4. 定位sqlite3_open、sqlite3_backup_init等函数
  5. 完成数据库在线备份
  6. 定位数据库密码的保存位置
  7. 编写数据库离线解密工具

定位sqlite3_exec函数

思路比较简单,先看一下sqlite3_exec的原型:

int sqlite3_exec(
        sqlite3*,                                  /* An open database */
        const char *sql,                           /* SQL to be evaluated */
        int (*callback)(void*,int,char**,char**),  /* Callback function */
        void *,                                    /* 1st argument to callback */
        char **errmsg                              /* Error msg written here */
);
 sqlite3*:通过sqlite3_open打开的数据库句柄 
 const char *sql:要执行的sql语句
 int (*callback)(void*,int,char**,char**):执行sql语句时对应的回调函数
 void *:回调函数的参数
 char **errmsg:存放错误信息

这里还没有必要分析它,只需要知道这个函数需要五个参数就够了,其中第二个参数是要执行的SQL,其他的,暂时当成DWORD类型。
站在正向的角度去思考,当一个新用户下载并开始使用微信,他还没有任何数据库文件,微信一定要对数据库进行初始化,相应的建表语句是:

CREATE TABLE IF NOT EXISTS `table_name` 表结构;

当然,为了方便,这些命令每次登录都要执行一遍,因为微信也不知道你会不会把库给删了。
打开IDA,按Shift+F12打开字符串窗口,Ctrl+F搜索CREATE TABLE IF NOT EXISTS(下图):
在这里插入图片描述
找一个比较完整的SQL点进去,在变量名上按下x查看交叉引用(下图):

查看push这个变量的位置(下图):

sub_106D11D0需要两个参数,一个是SQL,另外一个(圈出来的地方)不出意外是数据库句柄,但是这个CALL不是sqlite3_exec,因为参数太少。
双击sub_106D11D0看一下这个CALL的实现(下图):

不远处发现了这样一段汇编,传递了五个参数,末尾的add esp,14h也符合cdecl调用约定;静态调试不是特别明显,可以结合OD进行动态调试,会发现其中一个参数是SQL命令,这里就是在执行数据库的初始化,所以sub_11356570就是sqlite3_exec。
也可以查看CALL的内部(下图):

对比这些特征也能确定(比对sqlite3的源码)。

获取微信打开的数据库句柄

IDA静态分析

刚才定位sqlite3_exec是从引用SQL的地方(下图)向下追溯:

猜测[esi+0x64]是数据库句柄,不过这里没有数据库名,也找不到保存句柄的容器,所以需要向上追溯,找到句柄的来源。回到函数头查看交叉引用(下图):

什么都没有,惊喜又意外,这时候需要使用OD进行动态调试,在这之前,先说一下怎么算地址。
如上图所示,在IDA中,sub_106646500的地址是0x10664650,我的IDA基址是0x10000000(可能跟你的不同),那么sub_106646500对应的偏移就是0x664650,在OD中,需要跳转下断的位置是WeChatWin.dll + 0x664650

OD动态调试

明确之后,我们继续,打开OD,通过OD启动微信,暂时不要点登录,按Ctrl+G,输入地址后回车(下图):

在函数头下断,然后扫码登录并在手机上点击确认,断下后查看堆栈窗口(下图):

反汇编窗口跟随(下图):

是通过寄存器调用的,IDA中找不到交叉引用也很正常。
在回到上层调用之前,先分析下数据库初始化之前的代码:

0FD3465D    56              push esi
0FD3465E    8BF1            mov esi,ecx                              ; esi赋值ecx
...
0FD34672    FFD2            call edx                                 ; WeChatWi.0FD34650
...
0FD346DF    68 50545311     push WeChatWi.11535450                   ; SQL
0FD346E4    FF76 64         push dword ptr ds:[esi+0x64]             ; 数据库句柄
0FD346E7    8BCE            mov ecx,esi
0FD346E9    E8 E2CA0600     call WeChatWi.0FDA11D0                   ; 数据库初始化

猜测esi+0x64是保存数据库句柄的指针,在函数开始时,为esi赋值了ecx,查看下ecx+0x64处的数据(下图):

现在还没有东西,按F8执行单步,看看哪个CALL执行完毕得到的数据库句柄,当步过call edx时,esi+0x64处的数据发生了变化(下图):

从0变成了0x9996AB8,再往下esi+0x78保存数据库名,esi+0x8C处保存表名,esi来自ecx,(如果想通过Hook的方式取得句柄,就要进入call edx具体分析了,本文暂不赘述)接下来回到上一层去分析ecx的来源:

0FAD721C    E8 2F820600     call WeChatWi.0FB3F450
0FAD7221    8B08            mov ecx,dword ptr ds:[eax]
0FAD7223    8B51 4C         mov edx,dword ptr ds:[ecx+0x4C]
0FAD7226    8D8D B0FBFFFF   lea ecx,dword ptr ss:[ebp-0x450]
0FAD722C    51              push ecx
0FAD722D    8BC8            mov ecx,eax                              ; ecx == eax
0FAD722F    FFD2            call edx

这里只能得到ecx来自eax,eax这个寄存器比较特殊,可能是前一个CALL执行完毕的返回值,所以要回到函数头下断,看看eax是怎么来的(下图):

断下后开始单步,注意给eax赋值的指令和每个CALL执行后eax的变化。发现最后改变eax的CALL:

...
1038720F    E8 3C820600     call WeChatWi.103EF450                   ; 最后改变eax的CALL
...
1038721C    E8 2F820600     call WeChatWi.103EF450
...
1038722C    51              push ecx
1038722D    8BC8            mov ecx,eax                              ; ecx == eax
1038722F    FFD2            call edx

追进去(F7或Ctrl+G直接跳)看一下,建议从retn往前找:

0FAEF4DE    A1 FCF38A11     mov eax,dword ptr ds:[0x118AF3FC]
0FAEF4E3    83C0 28         add eax,0x28
...
0FAEF4F7    C3              retn

这里将一个基址保存的数据赋值给eax,然后再加0x28,查看之后也确实是接下来要用到的参数,但是不知道细心的你有没有发现一个问题,刚才我们在函数头下的断点,一直都只断下来一次。
难道微信只有一个数据库?或者为每个数据库写了单独的打开函数?
正常来说,为了提高代码的复用性,应该会对sqlite3_exec进行包装,数据库路径、表名、初始化用的SQL作为参数,写一个循环来初始化所有要用到的数据库。
所以要在刚才的函数头(下图),再向上追溯(因为多次重启微信,所以地址会不一样,可根据地址后四位判断):

断下后反汇编窗口跟随返回地址(下图):

然后怎么办呢,回函数头or单步,其实都可以,回函数头也是要单步的,就先往下走吧,后面不远处就是一个循环:

0FE5E2DF    8BB7 88180000   mov esi,dword ptr ds:[edi+0x1888]
0FE5E2E5    83C4 18         add esp,0x18
0FE5E2E8    8B87 8C180000   mov eax,dword ptr ds:[edi+0x188C]
0FE5E2EE    3BF0            cmp esi,eax
...
0FE5E301    C745 CC 0000000>mov dword ptr ss:[ebp-0x34],0x0
0FE5E30A    83C0 78         add eax,0x78
0FE5E30D    50              push eax
0FE5E30E    E8 6DF9C2FF     call WeChatWi.0FA8DC80
...
0FE5E322    FF50 4C         call dword ptr ds:[eax+0x4C]
...
0FE5E32E    83C6 04         add esi,0x4
0FE5E331    3BF7            cmp esi,edi
0FE5E333  ^ 75 CC           jnz short WeChatWi.0FE5E301

上面这段汇编,遍历[edi+0x1888]到[edi+0x188C],还会引用[esi]+0x78处的数据,这里跟前面讲的一样,保存着数据库的名字,那[esi]+0x64处会保存,或者说,将会保存什么呢?当然是数据库句柄。
在这个循环内,步过每一个CALL后观察下[esi]+0x64的变化,发现执行call dword ptr ds:[eax+0x4C]之后得到了数据库句柄,那么问题就转化为找到edi的来源,这个就很简单了,往上找发现了这个赋值指令:

0FE5E07A    8B3D FCF3C111   mov edi,dword ptr ds:[0x11C1F3FC]

刚才找eax的时候后四位也是F3FC,其实就是同一个地方,不过加的数值不一样,这一次要加0x1888,看看数据(下图):

(0x654 - 0x5F0) / 4 = 0x19 = 25,这里初始化了25个表,至于有多少库,就得去重后再统计,看一下成果:

计算偏移

理清楚之后,算一算偏移,方便后面查找句柄,此时WeChatWin.dll的基址是0x0F9F0000

Executable modules, 条目 83
 基址=0F9F0000
 大小=02680000 (40370176.)
 入口=11337B60 WeChatWi.<ModuleEntryPoint>
 名称=WeChatWi
 文件版本=3.6.0.18
 路径=D:\WeChat\[3.6.0.18]\WeChatWin.dll

则edi的计算公式是WeChatWin.dll + 0x11C1F3FC - 0x0F9F0000
开始遍历的地址:[[WeChatWin.dll + 0x222F3FC] + 0x1888]
结束遍历的地址:[[WeChatWin.dll + 0x222F3FC] + 0x188C]
设单个元素为esi,数据库句柄在esi+0x64,数据库名在esi+0x78

写在后面

本篇文章整理了资料和工具,并通过IDA和OD找到了微信中sqlite3_exec函数位置以及保存数据库句柄的地址,下一篇文章,介绍下如何调用sqlite3_exec函数。如果你已经迫不及待,可以参考本文提供的资料,尝试动手实现。

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
sqlite3_exec() 函数和 sqlite3_get_table() 函数都是 SQLite 库中用于执行 SQL 语句的函数,但是它们的应用场景有所不同。 sqlite3_exec() 函数用于执行一条或多条 SQL 语句,并且可以通过回调函数处理查询结果集。它适用于执行各种类型的 SQL 语句,包括 SELECT、INSERT、UPDATE、DELETE 等。 sqlite3_get_table() 函数则用于执行 SELECT 语句,并将查询结果存储在一个二维数组中。它的函数原型如下: ``` int sqlite3_get_table( sqlite3* db, /* Database handle */ const char* zSql, /* SQL statement */ char*** pazResult, /* Results of the query */ int* pnRow, /* Number of result rows written here */ int* pnColumn, /* Number of result columns written here */ char** pzErrmsg /* Error msg written here */ ); ``` 它的参数和返回值如下: - db:SQLite 数据库句柄。 - zSql:要执行的 SELECT 语句,必须是以 NULL 结尾的字符串。 - pazResult:指向一个二维数组的指针,用于存储查询结果。该数组的第一行存储查询结果的列名,后面的行存储查询结果的数据。 - pnRow:指向一个整数变量的指针,用于存储查询结果的行数。 - pnColumn:指向一个整数变量的指针,用于存储查询结果的列数。 - pzErrmsg:如果在执行过程中发生错误,则返回错误信息。 sqlite3_get_table() 函数执行 SQL 语句,并返回一个整数值,表示执行结果。如果返回值为 SQLITE_OK,则表示执行成功。如果返回值为其他值,则表示执行失败,并且错误信息将存储在 pzErrmsg 指针所指向的字符串中。 因此,sqlite3_exec() 函数适用于执行各种类型的 SQL 语句,而 sqlite3_get_table() 函数则适用于执行 SELECT 查询,并将查询结果存储在一个数组中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值