(xxxx)九:SQLite3的db数据库解密(一)句柄位置查找
1、不管在windwos、android、ios或其他平台,应用程序在运行时肯定会产生或读取很多数据(个人观点:数据才是核心,所有的代码都是为数据服务的),这些数据肯定要找个地方存放。比如之前破解的xxxx图片,加密后以bat格式存放在磁盘某个特定的目录。除了图片,还是有其他很多数据,比如聊天记录、语音/视频消息等。由于这些数据涉及个人隐私,T家官方答复是用户的这些数据都是点对点传输的,从来都没经过T家的服务器,那么我们平时用xxxx软件看到的聊天记录肯定存放在客户端本地,而不是从服务器下载的!这些数据都存放在本地哪了?以什么形式存放的?
简单的数据,比如软件参数配置等,一般都以ini格式存放在文件。但聊天记录、语音/视频涉及到大量的非结构化数据,这些是没法直接存文件的。同时文件的数据组织形式很简单,就是顺序存储,检索的时候效率也低,不适合大数据量的查询。这些数据只能以另一种形式存储了,这就是: 数据库!
说起数据库,IT行业肯定是无人不知、无人不晓!大家听说和用的最多的就关系型数据库,比如MySQL、sqlserver、oracle; 近些年大数据和AI火热,催生了mpp、nosql类的数据库,比如gbase、vertica、hbase等;这些所有数据库都有一个同样的特点,就是很“重”!一般的生产环境,都需要单独的服务器装这些数据库,还会配备专门的运维人员来维护。所以对于普通的客户端APP,用这些数据库存放和查询数据肯定是不行的(不可能让客户端的用户去运维数据库吧),那么那种数据库最适合相对轻量的APP来存放和查询数据了? SQLite孕育而生!
2、SQLite是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库,用户不需要在系统中配置。SQLite 引擎不是一个独立的进程,可以按应用程序需求进行静态或动态连接,SQLite 直接访问其存储文件,主要优点终结如下:
-
不需要一个单独的服务器进程或操作的系统(无服务器的)。
-
SQLite 不需要配置,这意味着不需要安装或管理。
-
一个完整的 SQLite 数据库(就是.db文件)是存储在一个单一的跨平台的磁盘文件。
-
SQLite 是非常小的,是轻量级的,完全配置时小于 400KiB,省略可选功能配置时小于250KiB。
-
SQLite 是自给自足的,这意味着不需要任何外部的依赖。
-
SQLite 事务是完全兼容 ACID 的,允许从多个进程或线程安全访问。
-
SQLite 支持 SQL92(SQL2)标准的大多数查询语言的功能。
-
SQLite 使用 ANSI-C 编写的,并提供了简单和易于使用的 API。
-
SQLite 可在 UNIX(Linux, Mac OS-X, Android, iOS)和 Windows(Win32, WinCE, WinRT)中运行。
对于轻量级的客户端APP,最需要功能莫过于前4点了:无需单独服务器、不需要安装、db文件跨平台、轻量级别不占空间!
3、xxxx存放数据的db文件都在哪了?下面这个目录:从db文件的字面意思看,有存放聊天记录的,有存放表情包的,有存放收藏的,有存放媒体的。
先看ChatMsg.db文件,应该是存放聊天记录的。要不先试试看能不能打开?结果却是这样的!
从提示来看,这个文件都不是database文件,这是怎么回事了?难道我们找错文件了?为了验证这个想法, 自己先生成一个db文件,里面写少量数据,比如:
然后和ChatMsg.db比对一下,问题就很明显了:我自己生成的db文件开头就是SQLite format 3,这是个明显的文件头信息,说明这个文件的格式(其他格式文件诸如jpg、gif等都这样,在文件头就标注了文件的格式);而ChatMsg.db头部都是“乱码”,其他地方也都是乱码!这么看就很明显了:ChatMsg.db是加密的!其实回过头来想想:xxxx这种装机量几十亿的国民级的超级APP,怎么可能会明文存储客户的关键数据了?要是发生了大规模泄露,T厂的股价不得直接腰斩啊!
既然都加密了,为了看到db的内容,肯定是要解密的!解密涉及到两个关键的信息:密钥和加密方法!目前生产环境下流行的对称加密方法(非对称加密算法效率低一些,这里应该不会用的,而且也没必要保公私钥,导致维护成本高):XOR和AES;XOR前面加密图片时用过了,这里大概率会用AES,原因:(1)XOR用明文和密文能得到密钥,没有AES安全 (2)聊天记录这些都是用户的绝对隐私,安全级别比图片高多了! 接下来还有一个问题:AES密钥的长度是多少了?
4、解密数据库前,先简单学习一下sqlited的使用接口。在https://www.runoob.com/sqlite/sqlite-c-cpp.html 这里有简单的C或c++接口的demo代码,如下:
#include <stdio.h> #include <stdlib.h> #include <sqlite3.h> static int callback(void *data, int argc, char **argv, char **azColName){ int i; fprintf(stderr, "%s: ", (const char*)data); for(i=0; i<argc; i++){ printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0; } int main(int argc, char* argv[]) { sqlite3 *db; char *zErrMsg = 0; int rc; char *sql; const char* data = "Callback function called"; /* Open database */ rc = sqlite3_open("test.db", &db); if( rc ){ fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); exit(0); }else{ fprintf(stderr, "Opened database successfully\n"); } /* Create SQL statement */ sql = "SELECT * from COMPANY"; /* Execute SQL statement */ rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg); if( rc != SQLITE_OK ){ fprintf(stderr, "SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); }else{ fprintf(stdout, "Operation done successfully\n"); } sqlite3_close(db); return 0; }
上面代码很多,除开各种容错的代码,核心代码就这么几句:先声明一个sqlite3的指针对象db,再连接数据库,并且把连接的相关信息保存到db指针(也有些地方叫句柄)。后续执行各种sql语句(select、delete、update等)都通过这个db指针,所有sql代码执行完后调用cloesAPI关闭指针对象!在调用cloes函数之前,db这个指针(句柄)一直都有效,通过这个指针能调用exec方法执行任何sql语句!
#include <stdio.h> #include <sqlite3.h> static int callback(void *NotUsed, int argc, char **argv, char **azColName){ for(int i=0; i<argc; i++){ printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } return 0; } int main(int argc, char **argv){ sqlite3 *db; sqlite3_open(argv[1], &db); sqlite3_exec(db, argv[2], callback, 0, &zErrMsg); sqlite3_close(db); return 0; }
上述代码经过vs2019编译后(注意选择release模式,减少其他代码的干扰),用IDA打开时main的代码如下:可以看出call sqlite3_open时push的两个参数:db是 lea eax, [ebp-8] ;push eax;这个是非常重要的特征:db是个局部变量,本身在栈内,所以可以通过ebp-立即数的方式找到;db本身又是指针,可以保存函数执行的结果,所以可以把地址传给open函数;后续分析xxxx打开db库是会用到!
.text:00401090 ; int __cdecl main(int argc, const char **argv, const char **envp) .text:00401090 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p .text:00401090 .text:00401090 zErrMsg = dword ptr -0Ch .text:00401090 var_8 = dword ptr -8 .text:00401090 var_4 = dword ptr -4 .text:00401090 argc = dword ptr 8 .text:00401090 argv = dword ptr 0Ch .text:00401090 envp = dword ptr 10h .text:00401090 .text:00401090 push ebp .text:00401091 mov ebp, esp .text:00401093 sub esp, 0Ch .text:00401096 mov eax, ___security_cookie .text:0040109B xor eax, ebp .text:0040109D mov [ebp+var_4], eax .text:004010A0 push esi .text:004010A1 mov esi, [ebp+argv] .text:004010A4 lea eax, [ebp-8] ; sqlite3* db; .text:004010A7 push eax .text:004010A8 mov [ebp+zErrMsg], 0 .text:004010AF push dword ptr [esi+4] ; argv[1] .text:004010B2 call ds:__imp__sqlite3_open .text:004010B8 lea eax, [ebp+zErrMsg] .text:004010BB push eax .text:004010BC push 0 .text:004010BE push offset callback .text:004010C3 push dword ptr [esi+8] .text:004010C6 push [ebp+var_8] .text:004010C9 call ds:__imp__sqlite3_exec .text:004010CF push [ebp+var_8] .text:004010D2 call ds:__imp__sqlite3_close .text:004010D8 mov ecx, [ebp+var_4] .text:004010DB add esp, 20h .text:004010DE xor ecx, ebp ; cookie .text:004010E0 xor eax, eax .text:004010E2 pop esi .text:004010E3 call @__security_check_cookie@4 ; __security_check_cookie(x) .text:004010E8 mov esp, ebp .text:004010EA pop ebp .text:004010EB retn .text:004010EB _main endp
为了进一步熟悉lite相关API,也可以通过调试体验一下;在IDA中看到main被__scrt_common_main_seh调用了,所以也可以在OD中这里下个断点方便观察:
进一步:可以根据call main代码在这函数内部的偏移,在OD找到main的真正入口后下断点:
为了方便静态分析,也可以在edit->segment->rebase program这里把整个exe默认的基址改成OD中实际加载的基址0xBC0000,支持IDA和OD的地址就完全对齐了!
吐槽一下OD这里的写法: 直接用local.2来表示了,平时都用epb-立即数表示局部变量的,这里都有些不习惯了!
open函数执行完后,ebp-8、也就是db指针的地方出现了一个句柄,这个大概率就是sqlite3的对象了! 这个句柄很重要,后续对db数据库所有的操作都要通过这个句柄来实现!
继续顺着这个句柄跟踪内存,这里明显可以发现单个对象的大小是D4-80=0x54字节;把内存网上滑,还能看到好几个同样结构的句柄!
上面铺垫了这么多,都是为了后续在xxxx中快速找到sqlite3_open函数,这个函数有两个非常明显的特征参数:(1)db文件的路径 (2)sqlite3 *db这个句柄是由lea 的方式保存到寄存器,再传递给函数使用的! 这是两个非常重要的特征,一定要牢记!
5、现在正式开始分析xxxx的sqlite数据库!数据库以db文件形式存放在磁盘。xxxx刚开始运行时,肯定会从磁盘读出来、解密,用户才能看到聊天记录等关键内容。从磁盘读文件,win32编程必然涉及到CreateFileW(注意:xxxx全球通用,为了兼容肯定会用w方法),这里先在CreateFileW下断点:(注意: 本人调试期间失败了无数次,并非一气呵成,所以关键数据的地址有可能每次都不一样)
已经开始读磁盘的文件了,不过这个是log日志,明显不是我们想要的,直接放过!
用OD的时候出了一些bug,这里换成x32dbg继续:找到db文件,就是这里了!
从调用堆栈看,有10来个函数。这里没有啥特殊的技巧,只能挨个都看看,直到第4个函数:
这里非常可疑:lea edx, [ebp-0x14]像极了传sqlite3 *db的句柄对象;这个句柄经过call处理后,又付给了esi,进一步说明句柄是有用的;先下个断点试试:
这明显是这个dll里面的,可以记住偏移:0x7a494cbe-0x79f80000=0x514CBE, 以后调试直接一步到位!
x32dbg出现了异常,继续换回OD调试:ebp-0x14这里已经生成了句柄,栈上不远处还有db文件的完整路径,这里越看越像;
继续追踪这个句柄:怎么样,和我们自己写的demo的sqlite3 *db句柄是不是很像啊!!! 0x64-0x10=0x54,连长度都是一样的,这里就可以实锤这就是sqlite3 *db句柄了!后续写代码hook的时候,可以直接在0x7A494CC3(也就是call的下一行代码,已经生成了句柄!写代码时需要动态获取,hook点相对dll基址的偏移是0x7A494CC3 - 0x79f80000 = 0x514CC3)这里下断点,然后取[ebp-0x14]就是sqlite3的句柄了!还有另一个重要的信息:[ebp-0x24] 数据库路径了! 偏移、db句柄的位置、db文件的位置这3个信息非常重要,后续写代码要用到!
现在已经确认call 7AA090A0初始化了sqlite3 *db句柄,那么进一步跟踪进入这个函数瞅瞅:一行一行地跟踪太累;由于我们需要找到哪些代码改写了[ebp-0x14]、也就是sqlite3* db句柄的值,所以对这个地址下个写入断点,断到了这里:这里eax存放了句柄地址,先给地址清零,然后调用7AA0959A函数,所以这个函数有重大“嫌疑”:放过后发现并未改变句柄指针的值,无奈继续;
当走到这里时:句柄的值被改变了,就是图中标红的这两行代码!esi存放了句柄的值,此时重点变成了回溯esi是怎么来的了!这里的偏移0x7AA09577-0x79F80000=0xA89577可以记住,下次直接到这里!
继续往上回溯: 发现好多地方都在读写esi,如果下个要搞定出到底是那行代码生成和句柄,需要继续逐行分析esi值的改变!
其实到此为止,利用找到的句柄位置完全可以通过调用sqlite3_open、sqlite3_exec函数读写db文件了!下次继续分享怎么通过代码远程调用句柄读写db的数据!
查找小技巧:
1、右边是栈视图:栈本质上也是一块内存,里面存的都是各种二进制数据,这些数据都有可能是什么数据了?
- 数字从几到几万、几十万的:这些数字比较常规,游戏中可能是血量、魔法、距离、坐标、药品数量等;其他软件大概率都是常规的数字,具体含义根据软件业务意义确定;
- 数字很大,4字节至少占用了3个字节表示,这种数字大概率是地址、指针或句柄,怎么区分这3者了?
- 如果是地址:OD会标记返回到xxxxx来自xxxxx:通过栈回溯找call就是这个原理!
- 如果是字符串指针:OD会标时出字符串
- 如果是句柄(或则说结构体/对象指针):OD栈视图中双击这个句柄,内存视图也会如下标记
心得:
要想逆向做的好,首先要有正向开发的经验和思维,逆向的时候才知道去哪找关键的call!逆向分析目标exe前,最好自己写个简单的demo,分析一下核心函数被编译器翻译成汇编时的指令,找到这些指令的特征后再去逆向,事半功倍!
参考:
1、https://www.runoob.com/sqlite/sqlite-intro.html SQLIite简介
2、https://blog.csdn.net/qq_38474570/article/details/96606530 PC xxxx逆向:两种姿势教你解密数据库文件
3、https://bbs.pediy.com/thread-257028.htm PC xxxx逆向分析