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.前言
主要用到的工具和環境:
在學習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。
luajit:

一般有安全意識的游戲廠商都不會直接把lua源碼腳本打包到APK中發布,所以一般對lua腳本的保護有下面3種:
這種情況是指打包在APK中的lua代碼是加密過的,程序在加載lua腳本時解密(關鍵函數luaL_loadbuffer ),解密后就能夠獲取lua源碼。如果解密后獲取的是luac字節碼的話,也可以通過反編譯得到lua源碼,反編譯主要用的工具有unluac和luadec51,后面會具體分析。
因為反編譯的結果並不容易查看,所以這種情況能夠較好的保護lua源碼。這個情況主要是先解密后反編譯,反編譯主要是通過luajit-decomp項目,它能夠將luajit字節碼反編譯成偽lua代碼。
這種情況主要是修改lua虛擬機源碼,再通過修改過的虛擬機將lua腳本編譯成luac字節碼,達到保護的目的。這種情況如果直接用上面的反編譯工具是不能將luac反編譯的,需要在程序中分析出相對應的opcode,然后修改lua項目的opcode的順序並重新編譯生成反編譯工具,就能反編譯了,后面會具體分析。
一般上面的情況都會交叉遇到。
這里主要介紹4種方法,都會在第5節中用實例說明。
這種方法需要把解密的過程全部分析出來,比較費時費力,主要是通過ida定位到luaL_loadbuffer函數,然后往上回溯,分析出解密的過程。
這里主要通過ida動態調試so文件,然后是定位到luaL_loadbuffer地址,游戲會在啟動的時候通過調用luaL_loadbuffer函數加載必要的lua腳本,通過在luaL_loadbuffer下斷點 ,斷下后就可以運行idc腳本將lua代碼導出(程序調用一次luaL_loadbuffer加載一個lua腳本,不寫idc腳本的話需要手動導N多遍.....)。
跟4.2原理一樣,就是通過hook函數luaL_loadbuffer地址,將代碼保存,相比4.2的好處是有些lua腳本需要在玩游戲的過程中才加載,如果用了4.2的方法,游戲過程中 中斷一次就需要手動運行一次idc腳本,而且往往每次只加載一個lua文件,如果是hook的話,就不需要那么麻煩,直接玩一遍游戲,全部lua腳本就已經保存好了。
這里主要是opcode的順序被修改了,需要用ida定位到虛擬機執行luac字節碼的地方,然后對比原來lua虛擬機的執行過程,獲取修改后的opcode順序,最后還原lua腳本。
好了,下面用3個例子來說明上面的情況。
一直向上回溯(交叉引用 ),來到下圖,發現解密的密鑰和簽名,其中xiaoxian為密鑰,XXFISH為簽名
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; }
解密后的文件如下:
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文件夾,運行 jitdecomp.exe,反編譯的結果在out目錄下,結果如下:

5.2 捕魚達人4
接着,ida加載libcocos2dlua.so文件,定位到函數luaL_loadbuffer,可以在函數中直接搜索,也可以字符串搜索"[LUA ERROR]"來定位到函數中,函數分析如下:
所以在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腳本獲取的部分數據如下:

5.3.夢幻西游手游


這里需要實現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結構如下:
#define DATA_SIZE 256 struct Lrc4 { unsigned char pData[DATA_SIZE]; //初始化時計算得到的256個字節 int nIndex; //記錄下標 int nPreIndex; //記錄前一個下標 };
其他函數的具體實現請看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就可以快速對比出opcode,因為opmode不相等,那么opcode也肯定不相等,到這一步,已經能還原部分opcode了,因為有一些opmode是唯一的。比如下面幾個:

接下來就需要定位到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代碼反編譯失敗的修復 請點這里)
參考文章
Kaitiren的專欄《Quick-cocos2d-x 與Cocos2dx 區別》http://blog.csdn.net/kaitiren/article/details/35276177
littleNA《夢幻手游部分Luac反編譯失敗的解決方法》 https://litna.top/2018/07/08/夢幻手游部分Luac反編譯失敗的解決方法/