網上各種教程、各種工具用不了,才會有這個文章。附件是我修改的支持luajit 2.1.0-beta2反編譯的LJD
lua bytecode解密
知己知彼很重要,搜索“cocos2dx lua 加密”大概可以找到類似下面的代碼。
bool AppDelegate::applicationDidFinishLaunching() { ... CCLuaStack *pStack = pEngine->getLuaStack(); // 如果設置了 -e 和 -ek 要加上下面這句 // pStack->setXXTEAKeyAndSign("aaa", 3); // 如果設置了 -e 和 -ek -es 則要加上下面這句 pStack->setXXTEAKeyAndSign("aaa", 3, "XT", 2); // load framework pStack->loadChunksFromZip("res/framework_precompiled.zip"); pStack->loadChunksFromZip("res/game.zip"); pStack->executeString("require 'main'"); return true; }
我的目標是一個ANDROID游戲,APK文件直接解壓。一般情況是libcocos2dlua.so,IDA 打開,函數窗口直接搜索applicationDidFinishLaunching,就能帶你飛,可惜只有loadChunksFromZip,沒有setXXTEAKeyAndSign,這保存解密KEY的被編譯優化了。怎么辦?IDA字符串窗口幫你忙,編譯器編譯代碼的時候都是就近原則,只要是差不多地方出現的字符串,都會被放在一起。加密的ZIP文件,文件頭幾個字符就是setXXTEAKeyAndSign的SIGN參數,加上ZIP文件本身的路徑,搜索目標ZIP文件就可以了。如圖,IDA顯示附近也就幾個字符串,剩下的KEY參數是哪個,一個一個試就可以了。
加密的ZIP文件:
IDA中的字符串,第一個黑塊是SIGN參數,非常幸運,嘗試第二個黑塊就是KEY:
https://github.com/cocos2d/cocos2d-x/search?p=1&q=loadChunksFromZip&type=&utf8=%E2%9C%93 找到cocos2dx的loadChunksFromZip的源碼
if (isXXTEA) { // decrypt XXTEA xxtea_long len = 0; buffer = xxtea_decrypt(bytes + stack->_xxteaSignLen, (xxtea_long)size - (xxtea_long)stack->_xxteaSignLen, (unsigned char*)stack->_xxteaKey, (xxtea_long)stack->_xxteaKeyLen, &len); zip = ZipFile::createWithBuffer(buffer, len); } else { if (size > 0) { zip = ZipFile::createWithBuffer(bytes, (unsigned long)size); } }
這里我遇到一點困難,cocos2dx的源碼項目,沒有xxtea的源碼!不過還是讓我在github搜索出來了,源碼在此:
https://github.com/xxtea/xxtea-c
直接寫個C++代碼,就把加密的ZIP解出來了。順帶一提,Sign就是用來跳過,跟什么PE,MZ是一樣的。
HANDLE hFileOUT = CreateFile(L"e:\\tmp\\y1\\xxx\\assets\\res\\de_xxx.zip", GENERIC_ALL, 0, 0, CREATE_ALWAYS, 0, 0); HANDLE hFile = CreateFile(L"e:\\tmp\\y1\\xxx\\assets\\res\\xxx.zip",GENERIC_ALL, 0,0,OPEN_EXISTING,0,0); if (hFile != INVALID_HANDLE_VALUE) { DWORD dwSize = GetFileSize(hFile, 0); size_t dwOut = 0; DWORD dwRead = 0; LPBYTE p = new BYTE[dwSize]; ReadFile(hFile, p, dwSize, &dwRead, 0); DWORD dwSignLen = 3; LPBYTE p2 = (LPBYTE)xxtea_decrypt((void *)(p + dwSignLen), dwSize - dwSignLen,(void *) "key", &dwOut); WriteFile(hFileOUT, p2, dwOut, &dwRead, 0); delete p; CloseHandle(hFileOUT); CloseHandle(hFile); }
de_xxx.zip的頭兩個字節變成喜聞樂見的PK,解壓,搞定。
Luajit ASM
解壓出來的文件,都是LuaJIT— a Just-In-Time Compiler for Lua,文件頭長這樣:
http://luajit.org/download.html 可以下載到LuaJIT的源碼,不過兼容性有點糟糕,需要找到正確的版本才有效。回到IDA字符串窗口搜索luajit,可以確認目標APK使用的是2.1.0-beta2。下載源碼,編譯之后,使用類似下面的命令行:
luajit.exe -bl xxx.lua.bytecode xxx.lua.asm
可以看到LUA的ASM代碼了,LUA ASM長這樣:
0013 TGETV 4 4 1
0014 CALL 3 4 2
0015 ISNEXT 6 => 0019
0016 => MOV 8 7
0017 MOV 9 2
0018 CALL 8 1 2
0019 => ITERN 6 3 3
0020 ITERL 6 => 0016
0021 RET0 0 1
BYTECODE的定義在http://wiki.luajit.org/Bytecode-2.0 。不過我智商不夠,看不懂,
Luajit decompiler
搜索了無數反編譯文章,基本就是https://github.com/NightNord/ljd ,然而這貨2014年之后就再沒更新了,並不能用,沒有選擇,只能自己動手改。
1. 由於新Opcode導致的失敗
luajit 2.1.0-beta2源碼的lj_bc.h里的#define BCDEF,下方就是長長一串Opcode定義。
Ljd源碼的bytecode\instructions.py 和 rawdump\code.py的兩張Opcode表必須跟luajit 2.1.0-beta2 的lj_bc.h一一對比,少了的就要補上。
這里不得不吐槽一下luajit,新增Opcode其實並沒有人用,Opcode是通過數組維護的,隨便加個新的Opcode都會導致原有的Opcode編碼改變。。。
2. 由於assert導致的失敗,注釋掉就好了。雖說有點不負責,但是反編譯各種語法處理我也不懂。
LJD改好之后,main.py xxx.lua.bytecode > xxx.lua,終於可以看到正常的代碼:
DeprecatedNetworkClass = {} or DeprecatedNetworkClass local function deprecatedTip(old_name, new_name) print("\n********** \n" .. old_name .. " was deprecated please use " .. new_name .. " instead.\n**********") return end DeprecatedNetworkClass.WebSocket = function () deprecatedTip("WebSocket", "cc.WebSocket") return cc.WebSocket end _G.WebSocket = DeprecatedNetworkClass.WebSocket() return
完畢!
20170417補充說明:
請大家先確認自己的目標,再使用代碼。如果版本不一致,你們需要先對比luajit源碼中的lj_bc.h和https://github.com/NightNord/ljd的bytecode\instructions.py 和 rawdump\code.py中的OPCODE表,對得上,才能正確反編譯,對不上就自己參照着來改。