00002 贪婪洞窟.004:解密存档文件

来源:互联网 发布:淘宝开店基础 编辑:程序博客网 时间:2024/04/29 10:24


00002贪婪洞窟.004:解密存档文件

贪婪洞窟的存档文件叫做“CaveMaster.db”。看后缀是一个sqlite文件,但直接用sqlite浏览器无法打开。用文本打开,全是乱码。在存档目录下还有其他的db文件,这些倒是可以用sqlite浏览器打开。其中有一个叫做“TopPlayer.db”的,记录着当前的上榜人员。我就开始嘀咕了,在一个游戏中会用两套不同的表格存储系统吗?在网上搜索一番,发现sqlite数据库是可以加密的。那我就看看CaveMaster.db是不是加密的sqlite

祭出ida大法,反编译“\Payload\CaveiOS CN.app\Cave iOS CN”,结果ida告诉我这个文件是加密的,反编译会没啥用,还问我要不要继续。这不废话嘛,既然没啥用我还反编译干嘛。于是转而下了个安卓版的贪婪洞窟,贪婪洞窟是哪个版本的无所谓,只要是安卓的就行。

解压,然后到\lib\armeabi文件夹,发现里面有一个“libcocos2dcpp.so”,哈哈,不由眼睛一亮啊。我的经验告诉我,cocos2d生成的游戏便会有这个动态库。反编译它,耐心等待分析完成。然后查找字符串“CaveMaster.db”及其引用:

好,看看sub_84ABB8的伪码。步骤如下:首先,在函数面板中鼠标右键,选择“Quick filter”;或者在函数面板拥有焦点的情况下按“Ctrl+F”:

然后,在函数面板下方,就会出现一个输入框。在输入框中输入想要查找的函数名称,ida会自动进行过滤。在找到的函数上双击,如果此时右侧的活动窗口是伪码窗口,则直接显示伪码;否则,将切换到“IDA View-A”之类的面板,并跳转到函数对应的汇编代码的起始处。此时,按下F5,则打开函数的伪码窗口。

sub_84ABB8的伪码比较长,此处仅显示“CaveMaster.db”的使用部分:

int __fastcallsub_84ABB8(int result, int a2)

{

 ……

 

 v25 = _stack_chk_guard;

 if ( result == 1 && a2 == 0xFFFF )

 {

   ……

   copyString_Dest_Src_L(&DataController::DB_NAME,"CaveMaster.db", (int)&v19);

   nullsub_7();

   setAtExit((int)&DataController::DB_NAME, (int)sub_1869130);

   nullsub_5();

   copyString_Dest_Src_L(&DataController::DB_TEMP_NAME,"CaveMaster_temp.db", (int)&v19);

   nullsub_7();

   result =setAtExit((int)&DataController::DB_TEMP_NAME, (int)sub_1869130);

 }

 if ( v25 != _stack_chk_guard )

   _stack_chk_fail(result);

 return result;

}

标记为红色的行是我们关心的重点。其中,copyString_Dest_Src_L这个函数的原名是sub_XXX之类的,在阅读它的代码后发现其功能只是拷贝字符串,因此该改成现在这个可读性的名字(好吧,名字起得不怎么样,L让人看不懂,改成copyString_Dest_Src_SrcLenght或许会好点)。

回到“CaveMaster.db”上来,sub_84ABB8函数只是将字符串“CaveMaster.db”赋值给DataController::DB_NAME,而没有对文件“CaveMaster.db”进行任何操作。那就继续跟进,查找DataController::DB_NAME的引用:

……

实在是太多了,图表上都无法显示(ida的图表显示真是太糟糕了,图形一多就很难定位和查看了。我只想让它平铺,结果它给你个智能的这样那样,啥都看不了,怎么换显示方式、怎么放大和缩小都没用。噢,我的ida6.8版)。那就在伪码中显示跳转列表吧。在DataController::DB_NAME上右键,选择“Jump to xref”:

222个引用,真是够“2”的:

默认选中的是“GameController::addDiamond”,函数名称很让人有兴趣。那就跟进去看看:

int __fastcallGameController::addDiamond(GameController *this, int a2)

{

 ……

 

 v7 = this;

 v2 = a2;

 v13 = _stack_chk_guard;

 sub_1869D7C(&v10, (int*)&DataController::DB_NAME);

 v11 = GameController::DB_getDiamond(v7,(int)&v10);

 sub_1869130(&v10);

 v9 = v11 + v2;

 if ( v11 + v2 <= 0 )

   v9 = 0;

 GameController::DB_setDiamond(v7, v9);

 nullsub_5();

 copyString_Dest_Src_L(&v10,"SET_DIAMOND_EVENT", (int)&v8);

 v3 = (cocos2d::EventCustom *)operatornew(0x28u);

 cocos2d::EventCustom::EventCustom();

 v12 = v3;

 sub_1869130(&v10);

 nullsub_7();

 v4 = (cocos2d::Director*)cocos2d::EventCustom::setUserData(v12, &v9);

 v5 = (cocos2d::Director*)cocos2d::Director::getInstance(v4);

 cocos2d::Director::getEventDispatcher(v5);

 result =cocos2d::EventDispatcher::dispatchEvent();

 if ( v12 )

   result = (*(int (__fastcall**)(cocos2d::EventCustom *))(*(_DWORD *)v12 + 4))(v12);

 if ( v13 != _stack_chk_guard )

   _stack_chk_fail(result);

 return result;

}

sub_1869D7C又是干嘛的呢?

int *__fastcallsub_1869D7C(int *a1,int *a2)

{

 int v2; // r4@1

 int *v3; // r5@1

 void *v4; // r0@1

 int *result; // r0@4

 char v6; // [sp+4h] [bp-14h]@5

 

 v2 = *a2;

 v3 = a1;

 v4 = (void *)(*a2 - 12);

 if ( *(_DWORD *)(*a2 - 4) < 0 )

 {

   v2 = sub_18698F0((int)v4, (int)&v6, 0);

 }

 else if ( v4 != &mayEmptyString )

 {

   sub_187B2E4((int *)(v2 - 4), 1);

 }

 result = v3;

 *v3 = v2;

 return result;

}

抱歉,我表示看得懂,但不够理解透彻。但这不要紧。我们把sub_1869D7C稍作整理:参数a1暂时不知道是啥,参数a2dbName,不应当是int*,而是std::string或其引用;局部变量v3只是参数a1的引用,可以去除;局部变量v2只是参数a2的引用,但在某些情况下被赋了新值。函数的最终返回值为result,它就是v3,也就是参数a1。在函数返回之前,对*v3也就是*a1赋了新值,为v2。而在某些情况下,v2的值并没有被改变,就是参数a2。也就是说,*a1=a2。绕了半天,很可能又是一个赋值操作。我们姑且这么定性,如果后面发现不对,再回来深入研究。

回到GameController::addDiamond函数,继续看:

 sub_1869D7C(&v10, (int*)&DataController::DB_NAME);

 v11 =GameController::DB_getDiamond(v7, (int)&v10);

v10就是DB_NAME,进入DB_getDiamond

int __fastcallGameController::DB_getDiamond(cocos2d::FileUtils *a1, int a2)

{

 ……

 

 v2 = a2;

 v15 = _stack_chk_guard;

 v3 = cocos2d::FileUtils::getInstance(a1);

 (*(void (__fastcall **)(int *, int))(*(_DWORD*)v3 + 64))(&v14, v3);

 std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v9,&v14, v2);

 sub_1869130(&v14);

 v4 = (DBUtil *)toString((int)&v9);

 DBUtil::initDB(v4, v5);

 sub_8355FC(&v11, "select ",&COL_NAME_GEMS);

 std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v12,&v11, " from ");

 std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v10,&v12, &TABLE_ROOT_TABLE);

 sub_1869130(&v12);

 sub_1869130(&v11);

 v13 = 0;

 sub_1869D7C(&v14, &v10);

 DBUtil::getDataInfo((int)&v14,(int)select_gamecontroller_calback, (int)&v13);

 v6 = (DBUtil *)sub_1869130(&v14);

 DBUtil::closeDB(v6);

 v7 = v13;

 sub_1869130(&v10);

 sub_1869130(&v9);

 result = v7;

 if ( v15 != _stack_chk_guard )

   _stack_chk_fail(v7);

 return result;

}

看仔细些,很容易发现一些让人感兴趣的东西:

 DBUtil::initDB(v4, v5);

继续跟进:

const char*__fastcall DBUtil::initDB(DBUtil *this, const char *a2)

{

 int v2; // r5@2

 int v3; // r6@2

 const char *result; // r0@2

 

 ::result = (const char *)sqlite3_open();

 if ( ::result )

 {

   result = (const char *)cocos2d::log(

                            (cocos2d *)"鎵撳紑鏁版嵁搴撳け璐ワ紝閿欒%d锛岄敊璇師鍥%s\n",

                            ::result,

                            errMsg);

 }

 else

 {

   v2 = pDB;

   v3 = toString((int)&byte_1C75C78);

   sub_18688F0(&byte_1C75C78);

   result = (const char *)sqlite3_key(v2, v3);

   ::result = result;

   if ( result )

     result = (const char*)cocos2d::log((cocos2d *)"楠岃瘉鏁版嵁搴撳姞瀵嗗け璐%d", ::result);

 }

 return result;

}

有一些乱码,这是由于ida不认识那些文本所用的编码格式造成的。将焦点定位到乱码文本,按下“Alt+A”,打开字符串样式窗口:

点击“Set default encodings”按钮,弹出默认字符串编码窗口,将8-bit and multibyte strings中的设置更改为合适的值(一个一个试吧,^_^)。此处需要修改为UTF-8。但有时也需要改为GBK。但问题是,默认是没有GBK的。这个时候就需要在字符串样式窗口中点击“Change encoding”按钮,在“Encodings”窗口的列表中右键,选择“Insert”,然后输入GBK,确定即可。

正确显示字符串后的函数如下:

const char*__fastcall DBUtil::initDB(DBUtil *this, const char *a2)

{

 ……

 

 ::result = (const char *)sqlite3_open();

 if ( ::result )

 {

   result = (const char*)cocos2d::log((cocos2d *)"打开数据库失败,错误码:%d,错误原因:%s\n",::result, errMsg);

 }

 else

 {

   v2 = pDB;

   v3 = toString((int)&byte_1C75C78);

   sub_18688F0(&byte_1C75C78);

   result = (const char *)sqlite3_key(v2, v3);

   ::result = result;

   if ( result )

     result = (const char*)cocos2d::log((cocos2d *)"验证数据库加密失败 %d", ::result);

 }

 return result;

}

第二个字符串直接明了地告知了我们解决问题的方法。其实没有这个字符串也是容易解决的,看到sqlite3_key,这还不够吗?

sqlite3_key有两个参数,我们关心的key是第二个参数,在上面的代码中,是v3。而v3 = toString((int)&byte_1C75C78);那就再看看byte_1C75C78。查找其引用,一个叫做“DBUtil::setUpDB”的函数引起了我们的注意。——我一直以为是setup,查了之后才知道是set up,长姿势了。

int __fastcallDBUtil::setUpDB(DBUtil *this, const char *a2)

{

 ……

 

 v8 = (char *)this;

 ::result = (const char *)sqlite3_open();

 if ( ::result )

 {

   result = cocos2d::log((cocos2d *)"尝试打开数据库失败,路径:%s,错误码:%d", v8, ::result);

 }

 else

 {

   ::result = (const char *)sqlite3_exec(pDB,"create table test(tid integer)", 0, 0);

   if ( ::result )

   {

     result = cocos2d::log(

                (cocos2d *)"对数据库创建测试表失败,数据库已加密,错误码:%d,错误原因:%s",

                ::result,

                errMsg);

   }

   else

   {

     cocos2d::log((cocos2d *)"已打开未加密数据库,可进行加密", v2);

     v3 = pDB;

     v4 = toString(0x1C75C78);

     v5 = sub_18688F0(&byte_1C75C78);

     ::result = (constchar *)sqlite3_rekey(v3, v4, v5);

     if ( ::result )

     {

       cocos2d::log((cocos2d *)"数据库加密失败,错误码:%d", ::result);

     }

     else

     {

       v6 = (const char*)toString((int)&byte_1C75C78);

       cocos2d::log((cocos2d *)"数据库加密成功,%s", v6);

     }

     sqlite3_exec(pDB, "drop tabletest", 0, 0);

     result = sqlite3_close(pDB);

   }

 }

 return result;

}

除了再次确定我们的判断外,setUpDB函数未能起到任何用处,根本看不到对byte_1C75C78的操作。这没关系,很多时候都是这样,排除了一个之后再找下一个。好在对byte_1C75C7的引用不多。

经过几次查找之后,我们发现了:

int __fastcallsub_E38AB8(int result, int a2)

{

 ……

   copyString_Dest_Src_L(&byte_1C75C78,"好吧,这儿的值被我替换了",(int)&v2);

 ……

}

Ok,数据库加密密码成功Get!但别高兴得太早,因为似乎还没有直接支持加密的sqlite浏览器。经过在玩上一番搜索,得知sqlite的加密功能是通过第三方扩展来完成的。而作这个扩展的并不止一家。好吧,一家一家试吧。

试,是自己编程手动解码。sqlite的操作,在CC++中进行比较方便。虽然在Xcode中,可以通过桥接的方式,在Swift中使用CC++代码,但我的直觉告诉我,用VC捣鼓捣鼓更有优势。

下载各家的含加密扩展的sqlite开发包就不说了,打开VS创建VC工程并添加相关#include也不说了。此处直接给出加密和解密的关键代码(请原谅,函数的名称不那么正规):

int BackupDb(sqlite3 *fromDb,constchar*toFile)

{

        sqlite3*toDb = NULL;

        intresultCode = sqlite3_open(toFile, &toDb);

        if(resultCode != SQLITE_OK)returnresultCode;

 

        sqlite3_backup*backup = sqlite3_backup_init(toDb, "main",fromDb,"main");

        if(backup)

        {

                  sqlite3_backup_step(backup,-1);

                  sqlite3_backup_finish(backup);

        }

        resultCode= sqlite3_errcode(toDb);

        sqlite3_close(toDb);

 

        returnresultCode;

}

int RestoreDb(constchar*fromFile, sqlite3 *toDb)

{

        sqlite3*fromDb = NULL;

        intresultCode = sqlite3_open(fromFile, &fromDb);

        if(resultCode != SQLITE_OK)returnresultCode;

 

        sqlite3_backup*backup = sqlite3_backup_init(toDb, "main",fromDb,"main");

        if(backup)

        {

                  sqlite3_backup_step(backup,-1);

                  sqlite3_backup_finish(backup);

        }

        resultCode= sqlite3_errcode(toDb);

        sqlite3_close(fromDb);

 

        returnresultCode;

}

CString GetErrorCodeString(intcode)

{

        CStringt;

        t.Format(_T("%x|%d"), code, code);

        returnt;

}

CString testContent(sqlite3 *db)

{

        char**dbResult = NULL;

        intnRow = 0;

        intnColumn = 0;

        char*pzErrmsg = NULL;

        if(sqlite3_get_table(db,"SELECT * FROM sqlite_masterwhere type='table'", &dbResult, &nRow,&nColumn, &pzErrmsg) != SQLITE_OK) returnpzErrmsg;

 

        CStringcontent;

        for(inti = 0; i < nColumn; ++i)//第一行数据是字段名称,从 nColumn 索引开始才是真正的数据

        {

                  content= content + dbResult[i] + _T("\t");

        }

        intindex = nColumn;

        for(introw = 1; row <= nRow; ++row)

        {

                  content+= _T("\r\n");

                  for(inti = 0; i < nColumn; ++i)

                  {

                           content= content + dbResult[index++] + _T("\t");

                  }

        }

 

        sqlite3_free_table(dbResult);

        returncontent;

}

voidCSQLite3KeyRemoverDlg::OnBnClickedButtonI2o()

{

        this->UpdateData();

 

        sqlite3*db = NULL;

        intresult = sqlite3_open(this->valueOfDB,&db);

        if(result != SQLITE_OK)MessageBox(GetErrorCodeString(result));

 

        result= sqlite3_key(db, this->valueOfKey,this->valueOfKey.GetLength());//使用密码,第一次为设置密码

        if(result != SQLITE_OK) MessageBox(GetErrorCodeString(result));

        

        //TRACE(testContent(db));

        result= sqlite3_rekey(db,NULL,0); //清空密码

        if((result = BackupDb(db,this->valueOfOutput))== SQLITE_OK) MessageBox("ok!");

        elseMessageBox(GetErrorCodeString(result));

 

        result= sqlite3_close(db);

}

 

voidCSQLite3KeyRemoverDlg::OnBnClickedButtonO2i()

{

        this->UpdateData();

 

        sqlite3*db = NULL;

        intresult = sqlite3_open(this->valueOfDB,&db);

        if(result != SQLITE_OK) MessageBox(GetErrorCodeString(result));

 

        result= sqlite3_key(db, this->valueOfKey,this->valueOfKey.GetLength());//使用密码,第一次为设置密码

        if(result != SQLITE_OK) MessageBox(GetErrorCodeString(result));

 

        if((result=RestoreDb(this->valueOfOutput, db)) ==SQLITE_OK) MessageBox("ok!");

        elseMessageBox(GetErrorCodeString(result));

 

        result= sqlite3_close(db);

}

解密存档文件到此告一段落,但也只是告一段落,还没有结束。我们还没试试能不能成功修改存档呢。万一修改了却不生效,这说明我们肯定在什么地方搞错了。也许这个存档文件只是开发者用来迷惑我们的,谁知道呢。另外,即使修改成功生效了,我说过,我也要将这事做得完美一点,写个专用的存档修改器吧。

在下篇文章中,我就会介绍贪婪洞窟的存档修改器的编写。希望贪婪洞窟的运营方能够看到我的文章,加强一下对程序和存档的保护工作。在这一点上应当向《放置江湖》学习。

0 0
原创粉丝点击