浅析android手游lua脚本的加密与解密
2018.05.02更新
? ? ? ? 這段時間在翻備份的硬盤,突然發現了以前的分析項目和代碼,從里面提取了之前附件的內容,現在上傳給大家,真是柳暗花明又一村啊。附件包括201703版本的夢幻手游里面提取的so文件和一些加密后的資源文件(包括lua腳本),并包括了2個撲魚APK文件,最后還打包了解密代碼,供大家參考。
? ? ? ? 附件太大,快100MB,上傳不來論壇,我又放到百度網盤了......
? ? ? ? 鏈接:https://pan.baidu.com/s/1DVgH0qHYPkiHBIiV2UsU7g 密碼:ipt3
?
2018.04.09更新
附件是真的找不到了, 大家主要理解思路吧。百度網盤的附件好多朋友都下載和保存過,能不能發一份到論壇上傳?感謝感謝~
?
2017.04.15更新
1. 在編輯過程中,5.1后半段內容(解密和反編譯部分)被刪除了,現在補上。
2. 在3.3里說到,“修改lua項目中的opcode后,編譯生成lua.exe再替換到反編譯目錄下,就可以反編譯”,這一句是錯誤的,正確是“修改lua項目中的opcode后,重新編譯反編譯工具luadec51項目,就可以反編譯了”,已經修改。
?
0.前言
這篇文章是本人在學習android手游安全時總結的一篇關于lua的文章,不足之處歡迎指正,也歡迎大牛前來交流。本文目錄如下: 目錄 0. 前言 1. lua腳本在手游中的現狀 2. lua、luac、luaJIT三種文件的關系 3. lua腳本的保護 3.1 普通的對稱加密,在加載腳本之前解密 3.2 將lua腳本編譯成luaJIT字節碼并且加密打包 3.3 修改lua虛擬機中opcode的順序 4. 獲取lua代碼的一般方法 4.1 靜態分析so解密方法 4.2 動態調試:ida + idc + dump 4.3 hook so 4.4 分析lua虛擬機的opcode的順序 5. 三個游戲的lua腳本解密過程 5.1 54捕魚 5.2 捕魚達人4 5.3 夢幻西游手游 6. 總結 參考文章主要用到的工具和環境:
1 win7系統一枚 2 quick-cocos2d-x的開發環境(弄一個開發環境方便學習,而且大部分lua手游都是用的cocos2d-x框架,還有一個好處,可以查看源碼關鍵函數中的特征字符串,然后在IDA定位到關鍵函數,非常方便) 3 IDA6.8(分析so文件+動態調試so) 4 vs2015(編寫解密代碼)這里建議用vs2013來編譯運行cocos2d-x,vs2015太多坑要填了..... 5 AndroidKiller 1.3.1(反編譯apk,其中apktool.exe是最新版) 6 luadec51(反編譯luac) 7 luajit-decomp(反編譯luaJIT) 等等... 1.lua腳本在手游中的現狀 略。 2.lua、luac、luaJIT三種文件的關系?????在學習lua手游過程中,本人遇到的lua文件大部分是這3種。其中lua是明文代碼,直接用記事本就能打開,luac是lua編譯后的字節碼,文件頭為0x1B 0x4C 0x75 0x61 0x51,lua虛擬機能夠直接解析lua和luac腳本文件,而luaJIT是另一個lua的實現版本(不是原作者寫的),JIT是指Just-In-Time(即時解析運行),luaJIT相比lua和luac更加高效,文件頭是0x1B 0x4C 0x4A。
?
luac:????
?
? ? ?luajit:
? ? ? 3.lua腳本的保護?????一般有安全意識的游戲廠商都不會直接把lua源碼腳本打包到APK中發布,所以一般對lua腳本的保護有下面3種:
?
3.1 普通的對稱加密,在加載腳本之前解密? ? ?這種情況是指打包在APK中的lua代碼是加密過的,程序在加載lua腳本時解密(關鍵函數luaL_loadbuffer ),解密后就能夠獲取lua源碼。如果解密后獲取的是luac字節碼的話,也可以通過反編譯得到lua源碼,反編譯主要用的工具有unluac和luadec51,后面會具體分析。
?
3.2 將lua腳本編譯成luaJIT字節碼并且加密打包? ?? 因為反編譯的結果并不容易查看,所以這種情況能夠較好的保護lua源碼。這個情況主要是先解密后反編譯,反編譯主要是通過luajit-decomp項目,它能夠將luajit字節碼反編譯成偽lua代碼。
?
3.3 修改lua虛擬機中opcode的順序? ? ?這種情況主要是修改lua虛擬機源碼,再通過修改過的虛擬機將lua腳本編譯成luac字節碼,達到保護的目的。這種情況如果直接用上面的反編譯工具是不能將luac反編譯的,需要在程序中分析出相對應的opcode,然后修改lua項目的opcode的順序并重新編譯生成反編譯工具,就能反編譯了,后面會具體分析。 ? ??
?
?????一般上面的情況都會交叉遇到。
?
4.獲取lua源碼的一般方法?????這里主要介紹4種方法,都會在第5節中用實例說明。
?
4.1 靜態分析so解密方法? ? ?這種方法需要把解密的過程全部分析出來,比較費時費力,主要是通過ida定位到luaL_loadbuffer函數,然后往上回溯,分析出解密的過程。
?
4.2 動態調試:ida + idc + dump? ? ?這里主要通過ida動態調試so文件,然后是定位到luaL_loadbuffer地址,游戲會在啟動的時候通過調用luaL_loadbuffer函數加載必要的lua腳本,通過在luaL_loadbuffer下斷點 ,斷下后就可以運行idc腳本將lua代碼導出(程序調用一次luaL_loadbuffer加載一個lua腳本,不寫idc腳本的話需要手動導N多遍.....)。
?
4.3 hook so? ? ?跟4.2原理一樣,就是通過hook函數luaL_loadbuffer地址,將代碼保存,相比4.2的好處是有些lua腳本需要在玩游戲的過程中才加載,如果用了4.2的方法,游戲過程中 中斷一次就需要手動運行一次idc腳本,而且往往每次只加載一個lua文件,如果是hook的話,就不需要那么麻煩,直接玩一遍游戲,全部lua腳本就已經保存好了。
?
4.4 分析lua虛擬機的opcode的順序? ? ?這里主要是opcode的順序被修改了,需要用ida定位到虛擬機執行luac字節碼的地方,然后對比原來lua虛擬機的執行過程,獲取修改后的opcode順序,最后還原lua腳本。
?
5.三個游戲的lua腳本解密實例?????好了,下面用3個例子來說明上面的情況。
?
5.1 54捕魚 首先用AndroidKiller 加載,然后查看lib目錄下的so文件,發現libcocos2dlua.so文件,基本可以確定是lua腳本編寫的了。這里有個小技巧,當有很多so文件的時候,一般最大的文件是我們的目標(文件大是因為集成了lua引擎)。既然有lua引擎,肯定有lua腳本了,接著找lua腳本。資源文件和lua腳本文件都是在assets目錄下。發現游戲的資源文件和配置文件都是明文,這里直接修改游戲的配置文件就可以作弊(比如修改升級炮臺所需的金幣和鉆石,就可以達到快速升級炮臺的目的),然后并沒有發現類似lua腳本的文件。 順手解壓了一下res目錄下的liveupdate_precompiled.zip,發現解壓失敗,看來是加密了(看名字就知道是更新游戲的代碼)這里說明一下,一般遇到xxxx_precompiled.zip的這種文件,都是quick-cocos2d-x框架(quick簡單來說就是對lua的拓展實現),在quick-cocos2d-x框架下可以用compile_scripts命令將lua文件加密打包成xxxx_precompiled.zip,游戲運行時再解密加載。注意,這種方式打包的lua腳本一般都會被編譯成luaJIT,加載的關鍵函數是loadChunksFromZIP,可以在IDA中直接搜索該函數,如果找不到可以搜索字符串luaLoadChunksFromZIP來定位到函數 OK,了解了原理接下來開始動手分析,將libcocos2dlua.so拖到IDA中加載,函數中直接搜索loadChunksFromZIP,定位后F5。????
? ? ?一直向上回溯(交叉引用 ),來到下圖,發現解密的密鑰和簽名,其中xiaoxian為密鑰,XXFISH為簽名
? ??
進去函數里面看看,其實會發現調用了XXTea算法,這里我們也可以直接分析loadChunksFromZIP函數的源碼(所以配置一個cocos2d的開發環境還是非常有必要的)。查看源碼里的lua_loadChunksFromZIP函數的原型: int CCLuaStack::lua_loadChunksFromZIP(lua_State *L) {if (lua_gettop(L) < 1){ // 這里可以發現用字符串也可以定位到目標函數CCLOG("lua_loadChunksFromZIP() - invalid arguments");return 0;} ...if (isXXTEA){// decrypt XXTEA// 這里調用了解密函數xxtea_long len = 0;buffer = xxtea_decrypt(zipFileData + stack->m_xxteaSignLen,(xxtea_long)size - (xxtea_long)stack->m_xxteaSignLen,(unsigned char*)stack->m_xxteaKey,(xxtea_long)stack->m_xxteaKeyLen,&len);delete []zipFileData;zipFileData = NULL;zip = CCZipFile::createWithBuffer(buffer, len);} ... }? ?
接下來直接寫解密函數(在cocos2d-x項目里面寫的解密函數,很多工具直接可以調用)
void decryptZipFile_54BY(string strZipFilePath) {CCFileUtils *utils = CCFileUtils::sharedFileUtils();unsigned long lZipFileSize = 0;unsigned char *szBuffer = NULL;unsigned char *zipFileData = utils->getFileData(strZipFilePath.c_str(), "rb", &lZipFileSize);xxtea_long xxBufferLen = 0;szBuffer = xxtea_decrypt(zipFileData + 6, //6為簽名XXFISH的長度(xxtea_long)lZipFileSize - (xxtea_long)6, //減去簽名的長度(unsigned char*)"xiaoxian", //xiaoxian為密鑰(xxtea_long)8, //密鑰的長度&xxBufferLen);//獲取zip里面的所有文件CCZipFile *zipFile = CCZipFile::createWithBuffer(szBuffer, xxBufferLen);int count = 0;string strFileName = zipFile->getFirstFilename();while (strFileName.length()){cout << "filename:" << strFileName << endl;unsigned long lFileBufferSize = 0;unsigned char *szFileBuffer = zipFile->getFileData(strFileName.c_str(), &lFileBufferSize);if (lFileBufferSize){++count;ofstream ffout(strFileName, ios::binary);ffout.write((char *)szFileBuffer, sizeof(char) * (lFileBufferSize));ffout.close();delete[] szFileBuffer;}strFileName = zipFile->getNextFilename();}delete[] zipFileData; }?
? ? 解密后的文件如下:? ??
這幾個都是更新游戲的代碼,是luajit的文件,所以接下來需要反編譯。IDA中查看下lua版本和luajit版本,字符串分別搜索lua+空格和luajit+空格:?
????lua版本為5.1
?
? ? luajit版本為2.1.0
?
? ? 反編譯本人用到的是luajit-decomp,這里需要注意,luajit-decomp默認的lua版本為5.1,luajit版本為2.0.2,我們需要下載對應lua和luajit的版本,編譯后替換luajit-decomp下的lua51.dll、luajit.exe、jit文件夾。反編譯時需要注意的文件和文件夾:
? ??
? ? 這里需要下載版本為2.1.0-beta2的luajit,并且編譯生成文件后,復制LuaJIT-2.1.0-beta2\src路徑下的lua51.dll、luajit.exe文件和jit文件夾覆蓋到luajit-decomp目錄中。luajit-decomp用的是autolt3語言,原腳本默認是只反編譯當前目錄下的test.lua文件,所以需要改一下decoder.au3文件的代碼。修改后的代碼另存為jitdecomp.au3文件,編譯后為jitdecomp.exe。并且增加了data目錄,目錄下有3個文件夾,分別為:
luajit:待反編譯的luajit文件 asm:反匯編后的中間結果 out:反編譯后的結果?
?????將解密后的文件放到luajit文件夾,運行 jitdecomp.exe,反編譯的結果在out目錄下,結果如下:
這個反編譯工具寫得并不好,反編譯后的文件閱讀起來挺困難的,而且反編譯的lua格式有問題,所以不能用lua編輯器格式化代碼。?
5.2 捕魚達人4
這個游戲主要是用ida動態調試so文件,然后用idc腳本把lua文件全部dump下來的方法。首先用AndroidKiller加載apk,在lib目錄下有3個文件夾,不同的手機cpu型號對應不同的文件夾 。本人的手機加載的目標so文件在armeabi-v7a文件下:?
? ? ?接著,ida加載libcocos2dlua.so文件,定位到函數luaL_loadbuffer,可以在函數中直接搜索,也可以字符串搜索"[LUA ERROR]"來定位到函數中,函數分析如下:
LUALIB_API int luaL_loadbuffer (lua_State *L, const char *buff, size_t size,const char *name)?
?????所以在ARM匯編中,參數R0為lua_State指針,參數R1為腳本內容,R2為腳本大小,R3為腳本的名稱,寫一段IDC腳本dump數據即可:
#include <idc.idc> static main() {auto code, bp_addrese,fp,strPath,strFileName;bp_addrese = 0x7573022C; // luaL_loadbuffer函數地址 ,靜態分析獲取的函數地址+so文件的地址得到AddBpt(bp_addrese); // 下斷點,也可以手動下斷while(1){code = GetDebuggerEvent(WFNE_SUSP|WFNE_CONT, 15); // 等待斷點發生,等待時間為15秒if ( code <= 0 ){Warning("錯誤代碼:%d",code);return 0;}Message ("地址:%a, 事件id:%x\n", GetEventEa(), GetEventId()); // 斷點發生,打印消息strFileName = GetString(GetRegValue("R3"),-1,0); // 獲取文件路徑名strFileName = substr(strFileName,strrstr(strFileName,"/")+1,-1); // 獲取最后一個‘/’后面的名字(文件的名字)去掉路徑strPath = sprintf("c:\\lua\\%s",strFileName); // 保存lua的本地路徑fp = fopen(strPath,"wb");savefile(fp,0,GetRegValue("R1"),GetRegValue("R2"));fclose(fp);Message("保存文件成功: %s\n",strPath);} } //字符串查找函數,從后面向前查找,返回第一次查找的字符串下標 static strrstr(str,substr1) {auto i,index;index = -1;while (1){i = strstr(str,substr1);if (-1 == i) return index;str = substr(str,i+1,-1);index = index+i+1;}; }?
? ? ?ida動態調試so文件網上有很多文章,這里就不詳細說明了。通過idc腳本獲取的部分數據如下:?
雖然文件的后綴名是.luac,但其實都是明文的lua腳本。?
5.3.夢幻西游手游
AndroidKiller反編譯apk,查看lib下存在libcocos2dlua.so,基本上確定是lua寫的:?
在assets\HashRes目錄下,存在很多被加密的文件,這里存放的是lua腳本和游戲的其他資源文件?
接著找lua腳本的解密過程,用ida加載libcocos2dlua.so文件,搜索luaL_loadbuffer函數,定位到關鍵位置,這里就是解密的過程了: 分析解密lua文件過程如下:?
????這里需要實現Lrc4解密的相關函數,還有Lzma解壓函數需要自己實現,其他幾個都是cocos2d平臺自帶的函數,直接調用就可以了。上面的流程圖實現的函數如下:
bool decryptLua_Mhxy(string strFilePath, string strSaveDir) {bool bResult = false;char *szBuffer = NULL;int nBufferSize = 0;CCFileUtils *utils = CCFileUtils::sharedFileUtils();unsigned long ulFileSize = 0;char *szFileData = (char*)utils->getFileData(strFilePath.c_str(), "rb", &ulFileSize);if (strncmp(szFileData, "L:grxx", 6)){if (!strncmp(szFileData, "__sign_of_g18_enc__", 0x13)){szBuffer = szFileData + 0x13;nBufferSize = ulFileSize - 0x13;bResult = decrypt((unsigned char*)szBuffer, nBufferSize);}}else if (!strncmp(szFileData + 6, "__sign_of_g18_enc__", 0x13)){unsigned char *pData = (unsigned char *)szFileData + 0x19;int nLen = ulFileSize - 0x19;bResult = decrypt(pData, nLen);if (ZipUtils::isGZipBuffer(pData, nLen)){nBufferSize = ZipUtils::ccInflateMemory(pData, nLen, (unsigned char**)&szBuffer);}else if (ZipUtils::isCCZBuffer(pData, nLen)){nBufferSize = ZipUtils::inflateCCZBuffer(pData, nLen, (unsigned char**)&szBuffer);}else if (LzmaUtils::isLzmaBuffer(pData, nLen)){nBufferSize = LzmaUtils::inflateLzmaBuffer(pData, nLen, (unsigned char**)&szBuffer);}else{bResult = false;}}if(bResult)saveLuaData(szBuffer, nBufferSize, strSaveDir);return bResult; }?
? ? ?解密函數過程如下:?
?
?????decrypt()實現代碼如下:
bool decrypt(unsigned char *pData, int nLen) {Lrc4 *pLrc4 = new Lrc4;Lrc4_lrc4(pLrc4);Lrc4_s(pLrc4, pData, nLen);return true; }?
? ? Lrc4結構如下:?
?
? ? ?其他函數的具體實現請看DecryptData_Mhxy.cpp文件,這里就不貼代碼了。解密后的文件如下:?
?
?????可以看出,解密后的文件為luac字節碼,但是這里直接用反編譯工具是不能反編譯luac字節碼的,因為游戲的opcode被修改過了,我們需要找到游戲opcode的順序,然后生成一個對應opcode的luadec.exe文件才能反編譯。下表為修改前后的opcode:
?
?????lua虛擬機的相關內容就不說明了,百度很多,這里說明下如何還原opcode的順序。首先需要定位到opmode的地方,IDA搜索字符串"LOADK",定位到opname的地方,交叉引用到代碼,找到opmode:
?
?????off_B02CEC為opname的地址,byte_A67C00為opmode的地址,進入opmode地址查看:
這里沒有把全部數據截圖出來,可以看出,這里的opmode跟原opmode是不對應的。原opmode在lua源碼中的lopcodes.c文件中:?
?????源碼用了宏,計算出來的結果就是上表中opmode的結果。這里對比opmode就可以快速對比出opcode,因為opmode不相等,那么opcode也肯定不相等,到這一步,已經能還原部分opcode了,因為有一些opmode是唯一的。比如下面幾個:
如SETLIST,原opcode為34,opmode為0x14,找到的opmode的第8個字節也為0x14,則實際上SETLIST的opcode為8。?
?????接下來就需要定位到luaV_execute函數,然后對比源碼來還原其他的opcode,直接IDA搜索字符串"initial value must be a number"可以定位到luaV_execute 函數,再F5一下。接著打開lua源碼中的lvm.c文件,找到luaV_execute函數,就可對比還原了。lua源碼和IDA F5后的代碼其實差別還是有的,而且源碼用了大量的宏,所以源碼只是用來參考、理解lua虛擬機的解析過程,本人在還原的過程中,會再打開一個沒有修改opcode的libcocos2dlua.so文件,這樣對比查找就方便多了。
?????最后修改lua源碼 lopcodes.h中的opcode、lopcodes.c的opname和opmode,重新編譯并生成luadec51 .exe(需要將lua源碼中的src目錄放到luadec51的lua目錄下才能編譯),就OK了,寫個批處理文件就可以批量反編譯。一個文件反編譯的結果:
?
6.總結
? ? ?總結一下解密lua的流程,拿到APK,首先反編譯,查看lib目錄下是否有libcocos2dlua.so,存在的話很大可能這個游戲就是lua編寫,lib目錄下文件最大的就是目標so文件,一般情況就是libcocos2dlua.so。接著再看assets文件夾有沒有可疑的文件,cocos2dx框架都會把游戲的資源文件放到這個文件夾下,包括lua腳本。其次分析lua加密的方式并選擇解密腳本的方式,如果可以ida動態調試,本人一般都會選擇用idc腳本dump代碼。最后如果得到的不是lua明文,還需要再反編譯一下。
? ? ?不足之處:第一個是此文是本人逆向lua手游時的總結,而且本人逆向的手游可能不是很多,所以有些觀點比較片面,不足之處請指正。第二個就是文章是事后寫的,并且寫文章的時間比較倉促,所以有些步驟寫得可能不詳細,歡迎討論。如果有必要,會寫一篇《如何一步一步還原夢幻手游opcode》,但是如果看過lua源碼,對lua比較熟悉的話,找出來我想應該不是問題的。第三個就是luajit的反編譯并不完美,用的是luajit-decomp反編譯工具,工具作者也說只是滿足了他自己的需求,所以如果可以的話,想自己實現一個luajit的反編譯工具,而且夢幻luac的反編譯好像部分代碼也反編譯失敗了,可能自己遺漏了點什么吧,就先這樣吧.....(2018/07/10 增加:夢幻西游手游lua代碼反編譯失敗的修復?請點這里)
?
參考文章
騰訊游戲安全中心《Lua游戲逆向及破解方法介紹》?http://gslab.qq.com/portal.php?mod=view&aid=173 云風《Lua源碼欣賞》http://download.csdn.net/download/nomoonon/8551481Kaitiren的專欄《Quick-cocos2d-x 與Cocos2dx 區別》http://blog.csdn.net/kaitiren/article/details/35276177
littleNA《夢幻手游部分Luac反編譯失敗的解決方法》?https://litna.top/2018/07/08/夢幻手游部分Luac反編譯失敗的解決方法/
轉載于:https://www.cnblogs.com/dmeng2009/p/11329258.html
總結
以上是生活随笔為你收集整理的浅析android手游lua脚本的加密与解密的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu 移动硬盘复制小文件可以,复
- 下一篇: java爬虫抓取起点小说,手把手带你爬虫