zoukankan      html  css  js  c++  java
  • (xxxx)十一:SQLite3的db数据库解密(三)数据库在线备份

       前面两篇文章分别介绍了sqlite数据库句柄和sqlite3_exec函数调用来查找数据库内容。通过这种方式来查询,需要一直hook目标软件。如果目标软件有检测程序,就有可能被检测到。本文分享另一种读取数据库内容的办法:在线备份!

         db文件存储的数据,本质上都是二进制的。db文件由于被加密,在磁盘上的那个文件必须先解密,所以要先找到密钥,这是个麻烦事!通过前面的分享可以看出:执行sqlite3_exec时其实db文件已经解密,所以才能查出来明文!这时的数据库已经存在于内存,sqlite官方提供了备份数据库的整套API(注意:完整的备份功能需要好几个API,不止一个,这也为后续我们自己写代码备份带来了很多麻烦事!),链接在这里: https://www.sqlite.org/backup.html  ,根据官网的接口,自己写一个备份的demo,先在自己本机试试看行不行!,如下:

    #include <stdio.h>
    #include "sqlite3.h"
    
    int backupDb(
        sqlite3* pDb,               /* Database to back up */
        const char* zFilename,      /* Name of file to back up to */
        void(*xProgress)(int, int)  /* Progress function to invoke */
    );
    
    void XProgress(int a, int b);
    
    int main()
    {
        printf("Hello World!
    ");
        sqlite3* db = NULL;
        int result = sqlite3_open("testDB.db", &db);
    
        const char* zFilename = "testDB_back.db";
        backupDb(db, zFilename, XProgress);
    
        sqlite3_close(db);
    }
    
    int backupDb(
        sqlite3* pDb,               /* Database to back up */
        const char* zFilename,      /* Name of file to back up to */
        void(*xProgress)(int, int)  /* Progress function to invoke */
    ) {
        int rc;                     /* Function return code */
        sqlite3* pFile;             /* Database connection opened on zFilename */
        sqlite3_backup* pBackup;    /* Backup handle used to copy data */
    
        /* Open the database file identified by zFilename. */
        rc = sqlite3_open(zFilename, &pFile);
        if (rc == SQLITE_OK) {
    
            /* Open the sqlite3_backup object used to accomplish the transfer */
            pBackup = sqlite3_backup_init(pFile, "main", pDb, "main");
            if (pBackup) {
    
                /* Each iteration of this loop copies 5 database pages from database
                ** pDb to the backup database. If the return value of backup_step()
                ** indicates that there are still further pages to copy, sleep for
                ** 250 ms before repeating. */
                do {
                    rc = sqlite3_backup_step(pBackup, 5);
                    xProgress(
                        sqlite3_backup_remaining(pBackup),
                        sqlite3_backup_pagecount(pBackup)
                    );
                    if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
                        sqlite3_sleep(250);
                    }
                } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED);
    
                /* Release resources allocated by backup_init(). */
                (void)sqlite3_backup_finish(pBackup);
            }
            rc = sqlite3_errcode(pFile);
        }
    
        /* Close the database connection opened on database file zFilename
        ** and return the result of this function. */
        (void)sqlite3_close(pFile);
        return rc;
    }
    
    void XProgress(int a, int b)
    {
        printf("%d,%d
    ",a,b);
    }

         确实生成了db文件:用navicat也能顺利查到表和数据,说明备份是成功的!先在新的问题来了:怎么才能在线备份xxxx的db文件了? 从内存备份完整的db文件后,也能用这些专业的软件在本地轻松打开了!

      

          上两篇文章分析了通过openDataBase找到db句柄,在那里hook的话可以顺利得到数据库句柄。但是从上面的备份代码看,还涉及到很多sqlite3开头API的调用,这就麻烦了:

    • 因为在目标进程空间执行,需要这些API在目标进程的地址,而不是上面那么备份demo进程的地址,这就要像上次找sqlite3_exec函数的入口地址一样从新开始查找了,真麻烦!
    • 原xxxx软件大概率是没用到备份功能的,所以这些备份的代码应该是没有的。如果要备份,这些备份的代码都要自己在dll里面添加进去,里面涉及到sqlite3的API函数都要挨个从目标dll里面找到!

          因为IDA分析PE文件时会增加引用、F5反编译等功能,相对OD这种动态调试工具会方便一些,所以这里用IDA静态查找这些关键函数的偏移位置;先用IDA打开关键的dll(这个dll放了很多函数,打开非常慢):dll原本只有27M,经过IDA分析,添加了好多查找功能,最后膨胀到650M了!

           

        下面是需要挨个找的关键函数:

        DWORD address_sqlite3_open = wxBaseAddress + ;
        DWORD address_sqlite3_backup_init = wxBaseAddress + ; 
        DWORD address_sqlite3_backup_step = wxBaseAddress + ; 
        DWORD address_sqlite3_sleep = wxBaseAddress + ;
        DWORD address_sqlite3_backup_finish = wxBaseAddress + ;
        DWORD address_sqlite3_close = wxBaseAddress + ;
        DWORD address_sqlite3_backup_remaining = wxBaseAddress + ;
        DWORD address_sqlite3_backup_pagecount = wxBaseAddress + ;
        DWORD address_sqlite3_errcode = wxBaseAddress + ;
    •         先看第一个sqlite3_open: 前面已经找到了openDataBase的偏移,这里先根据偏移定位到openDataBase调用的地方,然后选择jmp to xref,如下:

         

         这里能看到所有的引用,根据sqlite3_open的源码,其实就是简单粗暴直接调用了openDataBase,所以第4、5两个引用最像(距离最近嘛,偏移只有D和F);先看看这个,和sqlite3.c中的源码极其类似,应该就是它了,先把函数改名标记一下,同时记住其偏移:0xA895B0

         

        同理可以标记出另一个sqlite3_open_v2函数(其实就是紧接着上面这个函数,C语言里面是挨着的,编译器大概率也会挨着翻译成机器码,shellcode也是根据这个原理生成的!后续也会根据这个原理查找其他关键的sqlite3函数),这里不再赘述;

    •      接着看第二个sqlite3_backup_init函数:先在sqlite3.c源文件中找到这个函数   找到了一个比较明显的特征:字符串:source and destination must be distinct

           

           立马在IDA中查找这个字符串:还真找到了!

         

            跳转到引用这里:

          

           和源码一比对,参数是能符合的,应该就是了:call sub_10A083F0应该就是sqlite3ErrorWithMsg了,这里先标记一下;

    sqlite3ErrorWithMsg(
            pDestDb, SQLITE_ERROR, "source and destination must be distinct"
        );

          往上溯源,找到函数入口,把函数名改成sqlite3_backup_init即可,记下这里的偏移:0xA26980

    •     接着找sqlite3_backup_step函数

           从sqlite3.c通读额整个函数,没有找到任何字符串,看来直接用字符串定位是不行的,只能换个思路:要么通过其他函数找(比如这个函数调用了很多其他函数,如果我们先找到了其他函数,就能根据调用关系、顺藤摸瓜找到这个函数了),要么通过机器码定位!;这里我们先用机器码试试。由于是开源的,我们在自己本地先在sqlite3_backup_step函数入口下个断点,再运行,然后转到反汇编,提取一些机器码,比如下面这个:先用前面6个byte的机器码试试;

    #ifdef SQLITE_ENABLE_API_ARMOR
      if( p==0 ) return SQLITE_MISUSE_BKPT;
    #endif
      sqlite3_mutex_enter(p->pSrcDb->mutex);
    010603C0 8B 45 08             mov         eax,dword ptr [p]  
    010603C3 8B 48 14             mov         ecx,dword ptr [eax+14h]  
    010603C6 8B 51 0C             mov         edx,dword ptr [ecx+0Ch]  
    010603C9 52                   push        edx  
    010603CA E8 C8 40 FF FF       call        _sqlite3_mutex_enter (01054497h)  
    010603CF 83 C4 04             add         esp,4  

        在IDA中菜单中选择search->sequence of byte, 输入8B 45 08 8B 48 14,找到了这3个地方:

     

       和C的源码比对,明显不是,放弃;这里暂时没有更好的思路了,暂时放弃,先找其他的函数;

    •        sqlite3_backup_finish:这里面也没找到字符串,还是根据特征码查找。同样先在本地的工程下断点,然后调试;由于没有字符串,特征码也没有匹配上(可能是xxxx用的sqlite版本和我本地的不一样,也有可能是编译器翻译成机器码不一样,总之是没匹配上),和刚才那个一样,暂时方式,继续找其他函数;
    •      sqlite3_sleep: 既然是sleep,肯定涉及到时间的计算;从源码看,有几行比较明显,比如下面的这行:有乘法,先转换成毫秒,再除以1000,所以先根据69 45 08 E8 03 00 00 这一串特征码在IDA中查找:
        rc = (sqlite3OsSleep(pVfs, 1000*ms)/1000);
      0105C66F 69 45 08 E8 03 00 00 imul        eax,dword ptr [ms],3E8h  
      0105C676 50                   push        eax  
      0105C677 8B 4D F8             mov         ecx,dword ptr [pVfs]  
      0105C67A 51                   push        ecx  
      0105C67B E8 D0 94 07 00       call        sqlite3OsSleep (010D5B50h)  
      0105C680 83 C4 08             add         esp,8

       还真找到了:和本地汇编代码比虽说不完全一样,但逻辑结果是一样的;再结合前面的代码对比,就是这里了!记住函数入口的偏移:0xA89C80

            

    •  sqlite3_errcode:从源码看,函数比较简单,没有字符串,但是调用了sqlite3SafetyCheckSickOrOk这个函数;
    SQLITE_API int sqlite3_errcode(sqlite3 *db){
      if( db && !sqlite3SafetyCheckSickOrOk(db) ){
        return SQLITE_MISUSE_BKPT;
      }
      if( !db || db->mallocFailed ){
        return SQLITE_NOMEM_BKPT;
      }
      return db->errCode & db->errMask;
    }

     继续进入sqlite3SafetyCheckSickOrOk函数,发现有invalid字符串了,但是比较短,感觉不够;继续进入logBadConnection函数:

    SQLITE_PRIVATE int sqlite3SafetyCheckSickOrOk(sqlite3 *db){
      u32 magic;
      magic = db->magic;
      if( magic!=SQLITE_MAGIC_SICK &&
          magic!=SQLITE_MAGIC_OPEN &&
          magic!=SQLITE_MAGIC_BUSY ){
        testcase( sqlite3GlobalConfig.xLog!=0 );
        logBadConnection("invalid");
        return 0;
      }else{
        return 1;
      }
    }

        这次就有明显的字符串了:API call with %s database connection pointer

    static void logBadConnection(const char *zType){
      sqlite3_log(SQLITE_MISUSE, 
         "API call with %s database connection pointer",
         zType
      );
    }

       放入IDA搜查,一路跟踪到这里:从参数来看,实锤就是这里了;

        

          先把函数名改了,再往上层层追溯,再和源代码比对,发现基址在这:0xA885D0;

    •     sqlite3_close:从C源码看,是直接调用了sqlite3Close,遂进入sqlite3Close函数,发现了一个字符串:unable to close due to unfinalized;如法炮制,继续用这个字符串在IDA里面找: 从参数和函数调用来看,确实是这里,实锤了!

          

          往上找到函数入口,函数名改为sqlite3Close;这个函数又被调用了好多次,只有标红的这两个最接近入口,和在C源码看到的接近,先看看这两个函数:

         

          第一个:从对比来看应该就是sqlite3_close了,在IDA中标记,并记录下偏移: 0xA871F0;IDA中紧接这下面就是sqlite3_close_v2,也顺便标记下!

         

        至此,还有sqlite3_backup_step、sqlite3_backup_finish、sqlite3_backup_remaining、sqlite3_backup_pagecount 4个函数没找到,原因都一样:(1)没有字符串  (2)特征码没匹配上(可能是xxxx用的sqlite版本和我本地做demo的sqlite不一样,也有可能是编译器翻译成机器码不一样);这该怎么办了?继续从C源码入手,找打了一个新的突破口:

        sqlite3_backup_init已经找到了,剩下这4个函数互相挨着的,sqlite3_backup_step在最前面,sqlite3_backup_pagecount在最后面;sqlite3_backup_init和sqlite3_backup_step之间只间隔了4个函数;前面说过了:编译器会按照顺利编译(可以利用此特性生成shellcode),也就是说这sqlite3_backup_init后面第5个函数很有可能就是sqlite3_backup_step,然后紧接着就是sqlite3_backup_finish、sqlite3_backup_remaining、sqlite3_backup_pagecount;在只读的数据段找到sqlite3_backup_init,如下:

         

             我们顺着先看前两个函数: 这两个函数代码几乎是一样的,不同的仅仅是返回值,分别是[eax+20h]和[eax+24h];没有其他任何代码了,看起来和sqlite3_backup_remaining、sqlite3_backup_pagecount很像,那么这两个是不是了? 就需要进一步验证参数了!

        

        参数类型如下:非常凑巧的是nRemaining偏移在0x20处,nPagecount偏移在0x24h处,那么这里实锤了这两个就是sqlite3_backup_remaining、sqlite3_backup_pagecount;偏移分别是0xA275C0、0xA275D0;

    struct sqlite3_backup {
      sqlite3* pDestDb;        /* Destination database handle */
      Btree *pDest;            /* Destination b-tree file */
      u32 iDestSchema;         /* Original schema cookie in destination */
      int bDestLocked;         /* True once a write-transaction is open on pDest */
    
      Pgno iNext;              /* Page number of the next source page to copy */
      sqlite3* pSrcDb;         /* Source database handle */
      Btree *pSrc;             /* Source b-tree file */
    
      int rc;                  /* Backup process error code */
    
      /* These two variables are set by every call to backup_step(). They are
      ** read by calls to backup_remaining() and backup_pagecount().
      */
      Pgno nRemaining;         /* Number of pages left to copy */
      Pgno nPagecount;         /* Total number of pages to copy */
    
      int isAttached;          /* True once backup has been registered with pager */
      sqlite3_backup *pNext;   /* Next backup associated with source pager */
    };

            先在只剩sqlite3_backup_step、sqlite3_backup_finish这两个函数没找到了;既然这个函数自身没有字符串,特征码也不对,那我们先在源码看看这些函数都在哪些地方被引用了,说不定能从这些引用的函数找到突破口了!很明显红框那个才是引用,其他的都是申明或我们自己的代码;

          

          进入引用,发现一个有趣的现象: 我们还缺的sqlite3_backup_step、sqlite3_backup_finish居然在同一个函数被调用了,这个函数就是sqlite3BtreeCopyFile;也就是说只要找到sqlite3BtreeCopyFile,就找到了我们想要的函数;

     /* 0x7FFFFFFF is the hard limit for the number of pages in a database
      ** file. By passing this as the number of pages to copy to
      ** sqlite3_backup_step(), we can guarantee that the copy finishes 
      ** within a single call (unless an error occurs). The assert() statement
      ** checks this assumption - (p->rc) should be set to either SQLITE_DONE 
      ** or an error code.  */
      sqlite3_backup_step(&b, 0x7FFFFFFF);
      assert( b.rc!=SQLITE_OK );
    
      rc = sqlite3_backup_finish(&b);
      if( rc==SQLITE_OK ){
        pTo->pBt->btsFlags &= ~BTS_PAGESIZE_FIXED;
      }else{
        sqlite3PagerClearCache(sqlite3BtreePager(b.pDest));
      }

        继续查找sqlite3BtreeCopyFile的引用:发现在sqlite3RunVacuum函数内,更让人惊喜的是,这个函数内部有大量的sql查询语句:

    rc = execSqlF(db, pzErrMsg,
          "SELECT sql FROM "%w".sqlite_master"
          " WHERE type='index' AND length(sql)>10",
          zDbMain
      );
      if( rc!=SQLITE_OK ) goto end_of_vacuum;
      db->init.iDb = 0;
    
      /* Loop through the tables in the main database. For each, do
      ** an "INSERT INTO vacuum_db.xxx SELECT * FROM main.xxx;" to copy
      ** the contents to the temporary database.
      */
      rc = execSqlF(db, pzErrMsg,
          "SELECT'INSERT INTO vacuum_db.'||quote(name)"
          "||' SELECT*FROM"%w".'||quote(name)"
          "FROM vacuum_db.sqlite_master "
          "WHERE type='table'AND coalesce(rootpage,1)>0",
          zDbMain
      );
      assert( (db->flags & SQLITE_Vacuum)!=0 );
      db->flags &= ~SQLITE_Vacuum;
      if( rc!=SQLITE_OK ) goto end_of_vacuum;
    
      /* Copy the triggers, views, and virtual tables from the main database
      ** over to the temporary database.  None of these objects has any
      ** associated storage, so all we have to do is copy their entries
      ** from the SQLITE_MASTER table.
      */
      rc = execSqlF(db, pzErrMsg,
          "INSERT INTO vacuum_db.sqlite_master"
          " SELECT*FROM "%w".sqlite_master"
          " WHERE type IN('view','trigger')"
          " OR(type='table'AND rootpage=0)",
          zDbMain
      );
      if( rc ) goto end_of_vacuum;
    
      /* At this point, there is a write transaction open on both the 
      ** vacuum database and the main database. Assuming no error occurs,
      ** both transactions are closed by this block - the main database
      ** transaction by sqlite3BtreeCopyFile() and the other by an explicit
      ** call to sqlite3BtreeCommit().
      */
      {
        u32 meta;
        int i;
    
        /* This array determines which meta meta values are preserved in the
        ** vacuum.  Even entries are the meta value number and odd entries
        ** are an increment to apply to the meta value after the vacuum.
        ** The increment is used to increase the schema cookie so that other
        ** connections to the same database will know to reread the schema.
        */
        static const unsigned char aCopy[] = {
           BTREE_SCHEMA_VERSION,     1,  /* Add one to the old schema cookie */
           BTREE_DEFAULT_CACHE_SIZE, 0,  /* Preserve the default page cache size */
           BTREE_TEXT_ENCODING,      0,  /* Preserve the text encoding */
           BTREE_USER_VERSION,       0,  /* Preserve the user version */
           BTREE_APPLICATION_ID,     0,  /* Preserve the application id */
        };
    
        assert( 1==sqlite3BtreeIsInTrans(pTemp) );
        assert( 1==sqlite3BtreeIsInTrans(pMain) );
    
        /* Copy Btree meta values */
        for(i=0; i<ArraySize(aCopy); i+=2){
          /* GetMeta() and UpdateMeta() cannot fail in this context because
          ** we already have page 1 loaded into cache and marked dirty. */
          sqlite3BtreeGetMeta(pMain, aCopy[i], &meta);
          rc = sqlite3BtreeUpdateMeta(pTemp, aCopy[i], meta+aCopy[i+1]);
          if( NEVER(rc!=SQLITE_OK) ) goto end_of_vacuum;
        }
    
        rc = sqlite3BtreeCopyFile(pMain, pTemp);

         继续如法炮制,根据这些语句先找到sqlite3RunVacuum函数,再进一步找到sqlite3BtreeCopyFile函数(从源码看,这个函数调用了memset,这也是比较明显的特征之一);

    text:10A276E8 E8 13 FA 86 00                          call    _memset
    .text:10A276ED 8B 07                                   mov     eax, [edi]
    .text:10A276EF 83 C4 0C                                add     esp, 0Ch
    .text:10A276F2 89 45 DC                                mov     [ebp+var_24], eax
    .text:10A276F5 8B 47 04                                mov     eax, [edi+4]
    .text:10A276F8 89 7D E0                                mov     [ebp+var_20], edi
    .text:10A276FB 89 75 CC                                mov     [ebp+var_34], esi
    .text:10A276FE C7 45 D8 01 00 00 00                    mov     [ebp+var_28], 1
    .text:10A27705 8B 08                                   mov     ecx, [eax]
    .text:10A27707 8B 46 04                                mov     eax, [esi+4]
    .text:10A2770A 8B 10                                   mov     edx, [eax]
    .text:10A2770C 0F B7 81 8E 00 00 00                    movzx   eax, word ptr [ecx+8Eh]
    .text:10A27713 66 39 82 8E 00 00 00                    cmp     [edx+8Eh], ax
    .text:10A2771A 74 24                                   jz      short loc_10A27740
    .text:10A2771C 8B 8A D4 00 00 00                       mov     ecx, [edx+0D4h]
    .text:10A27722 66 89 82 8E 00 00 00                    mov     [edx+8Eh], ax
    .text:10A27729 85 C9                                   test    ecx, ecx
    .text:10A2772B 74 13                                   jz      short loc_10A27740
    .text:10A2772D 98                                      cwde
    .text:10A2772E 50                                      push    eax
    .text:10A2772F FF B2 98 00 00 00                       push    dword ptr [edx+98h]
    .text:10A27735 FF B2 DC 00 00 00                       push    dword ptr [edx+0DCh]
    .text:10A2773B FF D1                                   call    ecx
    .text:10A2773D 83 C4 0C                                add     esp, 0Ch
    .text:10A27740
    .text:10A27740                         loc_10A27740:                           ; CODE XREF: sqlite3BtreeCopyFile+BA↑j
    .text:10A27740                                                                 ; sqlite3BtreeCopyFile+CB↑j
    .text:10A27740 8D 45 C8                                lea     eax, [ebp+var_38]
    .text:10A27743 68 FF FF FF 7F                          push    7FFFFFFFh
    .text:10A27748 50                                      push    eax
    .text:10A27749 E8 E2 F5 FF FF                          call    sub_10A26D30
    .text:10A2774E 8D 45 C8                                lea     eax, [ebp+var_38]
    .text:10A27751 50                                      push    eax
    .text:10A27752 E8 69 FD FF FF                          call    sub_10A274C0

       再对比源码,根据0x7FFFFFFF很容易找到sqlite3_backup_step和sqlite3_backup_finish,偏移分别是0xA26D30、0xA274C0

     memset(&b, 0, sizeof(b));
      b.pSrcDb = pFrom->db;
      b.pSrc = pFrom;
      b.pDest = pTo;
      b.iNext = 1;
    
    #ifdef SQLITE_HAS_CODEC
      sqlite3PagerAlignReserve(sqlite3BtreePager(pTo), sqlite3BtreePager(pFrom));
    #endif
    
      /* 0x7FFFFFFF is the hard limit for the number of pages in a database
      ** file. By passing this as the number of pages to copy to
      ** sqlite3_backup_step(), we can guarantee that the copy finishes 
      ** within a single call (unless an error occurs). The assert() statement
      ** checks this assumption - (p->rc) should be set to either SQLITE_DONE 
      ** or an error code.  */
      sqlite3_backup_step(&b, 0x7FFFFFFF);
      assert( b.rc!=SQLITE_OK );
    
      rc = sqlite3_backup_finish(&b);

       至此,所有关键函数的偏移都已经找到,总结如下:

        //.text:10A895B0
        DWORD address_sqlite3_open = wxBaseAddress + 0xA895B0;
        //.text:10A26980 
        DWORD address_sqlite3_backup_init = wxBaseAddress + 0xA26980;
        //.text:10A89C80
        DWORD address_sqlite3_sleep = wxBaseAddress + 0xA89C80;
        //.text:10A871F0 
        DWORD address_sqlite3_close = wxBaseAddress + 0xA871F0;
        //.text:10A26D30
        DWORD address_sqlite3_backup_step = wxBaseAddress + 0xA26D30;
        //.text:10A274C0 
        DWORD address_sqlite3_backup_finish = wxBaseAddress + 0xA274C0;
        //.text:10A275C0
        DWORD address_sqlite3_backup_remaining = wxBaseAddress + 0xA275C0;
        //.text:10A275D0
        DWORD address_sqlite3_backup_pagecount = wxBaseAddress + 0xA275D0;
        //.text:10A885D0
        DWORD address_sqlite3_errcode = wxBaseAddress + 0xA885D0;

       效果展示:能hook到所有的db数据库:

       

       选择msg0导出,然后放入sqlite export:所有表、字段和数据都能看到了!用户的隐私荡然无存!

       

    最后,做了这么久的逆向,自己总结的要点如下:关于调试和反调试,xxxx逆向时并未遇到,后续逆向过TP时再分享!

    参考:

    1、https://github.com/zmrbak  2019 PC xxxx探秘/SQLite_L37; 注意:不同版本中函数的偏移是不一样的,不能直接照抄,需要重新找偏移!

  • 相关阅读:
    出差兰州
    出差乌鲁木齐(3)
    依赖倒置原则
    .NET 打印界面
    出差乌鲁木齐2
    出差乌鲁木齐(2)
    出差乌鲁木齐3
    IStyleGallery.AddItem、UpdateItem、RemoveItem用法
    Shapefile记录检索
    c#利用最小二乘法拟合任意次函数曲线(转)
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14492894.html
Copyright © 2011-2022 走看看