zoukankan      html  css  js  c++  java
  • (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
    ", azColName[i], argv[i] ? argv[i] : "NULL");
       }
       printf("
    ");
       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
    ", sqlite3_errmsg(db));
          exit(0);
       }else{
          fprintf(stderr, "Opened database successfully
    ");
       }
    
       /* 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
    ", zErrMsg);
          sqlite3_free(zErrMsg);
       }else{
          fprintf(stdout, "Operation done successfully
    ");
       }
       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
    ", 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逆向分析

  • 相关阅读:
    Java基础-String、StringBuffer、StringBuilder的区别
    Java基础-String类能被继承吗,为什么
    Java基础-Java中的基本数据类型是什么,各自占用多少字节
    JAVA关键字
    Java 栈(stack)与堆(heap) 详解
    Hibernate--hibernate.hbm.xml详解
    Hhibernate--映射继承关系
    C--c语言模拟java的linkedList
    C--调试基本命令
    C--指针
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14454937.html
Copyright © 2011-2022 走看看