先clone到本地
git clone https://github.com/cocos2d/cocos2d-x.git
Cocos2d-x是一个开源的移动2D游戏框架,底层支持各种平台,核心用c++封装了各种库,外面给了lua和c++的接口,所以关键代码可能在lua中,很多安卓游戏的逻辑也基本都在lua脚本里,盗用官网这张图
lua虚拟机相关代码在cocos2d-x\cocos\scripting\lua-bindings\manual里
CCLuaEngine.h lua引擎相关
CCLuaStack.h lua栈相关
进入虚拟机
cocos2d-x\templates\lua-template-default\frameworks\runtime-src\Classes\AppDelegate.cpp\AppDelegate::applicationDidFinishLaunching
应用结束加载中进入lua虚拟机
这句engine->executeScriptFile("main.lua")调用了cocos2d-x\cocos\scripting\lua-bindings\manual\CCLuaEngine.cpp的executeScriptFile调用了CCLuaStack.cpp的executeScriptFile
luaLoadBuffer里调用xxtea_decrypt解密了lua脚本,然后调用luaL_loadbuffer加载解密后的脚本,所以直接hook 这个函数luaL_loadbuffer把(char*)content这个字符dump出来就得到解密过的lua脚本了
而luaL_loadbuffer的源码没有,只有编译过的库cocos2d-x\external\lua\luajit\prebuilt\android\armeabi-v7a\libluajit.a,
要找它的实现需要下载luajit源代码分析了,这就完全进入了lua虚拟机的实现
a Just-In-Time Compiler for Lua. 采用C语言写的Lua的解释器的代码
1.从c++进入lua世界的调用逻辑
2.加密算法为xxtea,如果没有修改,sign为XXTEA,KEY为2dxlua,如果有修改,可以ida打开libxxluaxx.so在applicationDidFinishLaunching里找到
3.无论是否加密,解密后都会调用luaL_loadbuffer函数,所以直接hook这个函数把(char*)content这个字符dump出来就是解密过的lua脚本了,缺点是要把游戏运行一遍,只能搞出执行过的代码
4.cocos2d-x\external\xxtea\xxtea.cpp里有完整的加密解密算法,逻辑清晰,可以写个python脚本直接本地解密,也可以在这里hook获取key和sign或者解密后脚本
某捕鱼游戏,下载安装apk后,再其内部内置了捕鱼、麻将等十几款小游戏,嘿嘿,懂得都懂这是干啥的,直接点击捕鱼游戏下载,
下载后的游戏源码在/data/data/com.q8wdw6.gyll9spfo.nycatp9/files/download/里
adb pull 出来,files文件夹里面已经暴露很多信息了,配置,下载的游戏,更新等等
进入files\download\107\res\就可以看到luac,很明显crash里面存储了所有碰撞,model_path_crash和path里存储的是移动路径,我们可以看到所有的鱼的移动路径都是tm设定好的,models里面存储着游戏逻辑,views里面存储着界面显示逻辑
随机打开一个luac加密了,开头的ZX_CS@56#D~d@dud明显就是加密sign
根据之前对cocos2d引擎的分析,找到apk下面带有lua也是最大的一个so libqpry_lua.so,用ida直接打开,定位到AppDelegate::applicationDidFinishLaunching函数,直接拖到最下面,看到这句
(*(v7 + 1) + 116))((v7 + 1), "ZX_01RdsF~@!R8", 14, "ZX_CS@56#D~d@dud", 16);很明显是调用了stack->setXXTEAKeyAndSign
对比源码stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));
解密key为ZX_01RdsF~@!R8,sign为ZX_CS@56#D~d@dud
也可以直接搜索字符串,因为key和sign在一个函数调用,所以一般离的很近,base/src/main.lua这个字符串进一步验证了正确性
有了key和sign就可以直接解密luac脚本了,照着写,很简单,可以看到sign的作用就是被忽略,只有长度有用,尴尬,解密用的主要是key
或者直接pip install xxtea-py
然后直接python,自己写个循环实现批量解密吧
下面开始分析lua,这个最简单,我们想分析子弹打到鱼的逻辑,直接找src\views\layer\BulletLayer.luac
直接找子弹打到鱼的函数,很清晰,第一个参数为子弹对象,第二个为鱼列表,这里看到只有master_id == self_player_id才会调用
on_self_bullet_crash_fish当自己的子弹击中了鱼最后会调用加金币等,否则只调用on_bullet_crash_fish显示效果,这里可以也改成on_self_bullet_crash_fish就可以加金币了
在定位一下加钱的函数,fish_gold是鱼的钱,fish_odds是鱼的剩余 ,这句 local one_add_gold = math.floor(fish_gold/12)就是一个鱼加的钱,我们把这个/12改为*12就可以修改倍数了
入口代码在AnimationLayer.lua里,直接看接收到玩家捕到鱼的函数,分为网或者炸弹,里面还有一网打尽和大转盘之类的特效,这里可以把它全部改成高倍的特效
src\models\DataCenter.lua里存储着鱼和子弹等参数,可以看到子弹最高9级,odds_min与odds_max应该就代表威力,改这里可以改子弹威力
self.fish_client存储不同鱼的参数,不同id对应不同鱼,mulriple_min与mulriple_max存储着金币的倍数最大与最小值,一般鱼都是2-3倍,大章鱼为300倍,改这里可以直接改鱼的倍数
其他的功能逻辑获得源码后都很清晰,改倍数改鱼改子弹什么都很简单,改完之后重新用sign和key加密之后push到相应文件夹下面就实现了破解
如何实现cocos2d反逆向,我的一些不成熟想法,由浅到深分层如下
1.修改xxtea的key和sign,就像这个游戏,需要分析so才能找到key
2.直接修改xxtea算法为其他可逆算法,这样逆向的时候还得逆加密算法
3.在进一步,修改luajit源码改动lua虚拟机,比如改变字节码指令顺序,或者改变数据读取顺序等,这样逆向的时候必须分析lua虚拟机的改动
4.把key,加密算法,虚拟机改动等的关键函数封装放到其他cpp或者其他so里,在加密一层,调用的时候在解密,这样需要先动态调试先分析调用逻辑
5.关键代码加入ollvm再编译或者直接vm掉,这样需要先去掉ollvm混淆或者分析vm
cocos2d 3.3 lua 代码加密 luac
安卓逆向之Luac解密反编译
bool
AppDelegate::applicationDidFinishLaunching()
{
/
/
set
default FPS
Director::getInstance()
-
>setAnimationInterval(
1.0
/
60.0f
);
/
/
设置fps刷新率
/
/
register lua module
auto engine
=
LuaEngine::getInstance();
/
/
创建lua虚拟机引擎
ScriptEngineManager::getInstance()
-
>setScriptEngine(engine);
/
/
设置脚本引擎为lua引擎
lua_State
*
L
=
engine
-
>getLuaStack()
-
>getLuaState();
/
/
创建lua虚拟机环境lua_State
lua_module_register(L);
/
/
分配网络,控制台,ui界面等相关联的寄存器
register_all_packages();
LuaStack
*
stack
=
engine
-
>getLuaStack();
stack
-
>setXXTEAKeyAndSign(
"2dxLua"
, strlen(
"2dxLua"
),
"XXTEA"
, strlen(
"XXTEA"
));
/
/
这一步很关键,获取栈结构并调用setXXTEAKeyAndSign设置加密算法为xxtea,sign为XXTEA,KEY为
2dxlua
,很多游戏lua脚本都用的默认的sign和key
/
/
如果没有用默认的,ida打开libxxxluaxxx.so直接搜索applicationDidFinishLaunching导出函数也基本都能直接找到
/
/
register custom function
/
/
LuaStack
*
stack
=
engine
-
>getLuaStack();
/
/
register_custom_function(stack
-
>getLuaState());
FileUtils::getInstance()
-
>addSearchPath(
"src/64bit"
);
FileUtils::getInstance()
-
>addSearchPath(
"src"
);
/
/
lua源码在src文件夹,资源在res文件夹
FileUtils::getInstance()
-
>addSearchPath(
"res"
);
if
(engine
-
>executeScriptFile(
"main.lua"
))
/
/
直接通过lua引擎调用main.lua进入lua的世界
{
return
false;
}
return
true;
}
bool
AppDelegate::applicationDidFinishLaunching()
{
/
/
set
default FPS
Director::getInstance()
-
>setAnimationInterval(
1.0
/
60.0f
);
/
/
设置fps刷新率
/
/
register lua module
auto engine
=
LuaEngine::getInstance();
/
/
创建lua虚拟机引擎
ScriptEngineManager::getInstance()
-
>setScriptEngine(engine);
/
/
设置脚本引擎为lua引擎
lua_State
*
L
=
engine
-
>getLuaStack()
-
>getLuaState();
/
/
创建lua虚拟机环境lua_State
lua_module_register(L);
/
/
分配网络,控制台,ui界面等相关联的寄存器
register_all_packages();
LuaStack
*
stack
=
engine
-
>getLuaStack();
stack
-
>setXXTEAKeyAndSign(
"2dxLua"
, strlen(
"2dxLua"
),
"XXTEA"
, strlen(
"XXTEA"
));
/
/
这一步很关键,获取栈结构并调用setXXTEAKeyAndSign设置加密算法为xxtea,sign为XXTEA,KEY为
2dxlua
,很多游戏lua脚本都用的默认的sign和key
/
/
如果没有用默认的,ida打开libxxxluaxxx.so直接搜索applicationDidFinishLaunching导出函数也基本都能直接找到
/
/
register custom function
/
/
LuaStack
*
stack
=
engine
-
>getLuaStack();
/
/
register_custom_function(stack
-
>getLuaState());
FileUtils::getInstance()
-
>addSearchPath(
"src/64bit"
);
FileUtils::getInstance()
-
>addSearchPath(
"src"
);
/
/
lua源码在src文件夹,资源在res文件夹
FileUtils::getInstance()
-
>addSearchPath(
"res"
);
if
(engine
-
>executeScriptFile(
"main.lua"
))
/
/
直接通过lua引擎调用main.lua进入lua的世界
{
return
false;
}
return
true;
}
int
LuaStack::executeScriptFile(const char
*
filename)
{
CCAssert(filename,
"CCLuaStack::executeScriptFile() - invalid filename"
);
std::string buf(filename);
/
/
/
/
remove .lua
or
.luac
/
/
size_t pos
=
buf.rfind(BYTECODE_FILE_EXT);
/
/
BYTECODE_FILE_EXT就是lua字节码,NOT_BYTECODE_FILE_EXT就是lua脚本源码
/
/
static const std::string BYTECODE_FILE_EXT
=
".luac"
;
if
(pos !
=
std::string::npos)
{
buf
=
buf.substr(
0
, pos);
/
/
截取前缀
}
else
{
pos
=
buf.rfind(NOT_BYTECODE_FILE_EXT);
if
(pos
=
=
buf.length()
-
NOT_BYTECODE_FILE_EXT.length())
{
buf
=
buf.substr(
0
, pos);
}
}
FileUtils
*
utils
=
FileUtils::getInstance();
/
/
/
/
1.
check .luac suffix
/
/
2.
check .lua suffix
/
/
std::string tmpfilename
=
buf
+
BYTECODE_FILE_EXT;
if
(utils
-
>isFileExist(tmpfilename))
{
buf
=
tmpfilename;
}
else
{
tmpfilename
=
buf
+
NOT_BYTECODE_FILE_EXT;
if
(utils
-
>isFileExist(tmpfilename))
{
buf
=
tmpfilename;
}
}
std::string fullPath
=
utils
-
>fullPathForFilename(buf);
/
/
获取绝对路径
Data data
=
utils
-
>getDataFromFile(fullPath);
/
/
通过getDataFromFile读取lua文件到data
int
rn
=
0
;
if
(!data.isNull())
{
if
(luaLoadBuffer(_state, (const char
*
)data.getBytes(), (
int
)data.getSize(), fullPath.c_str())
=
=
0
)
/
/
通过luaLoadBuffer加载data
{
rn
=
executeFunction(
0
);
}
}
return
rn;
}
int
LuaStack::executeScriptFile(const char
*
filename)
{
CCAssert(filename,
"CCLuaStack::executeScriptFile() - invalid filename"
);
std::string buf(filename);
/
/
/
/
remove .lua
or
.luac
/
/
size_t pos
=
buf.rfind(BYTECODE_FILE_EXT);
/
/
BYTECODE_FILE_EXT就是lua字节码,NOT_BYTECODE_FILE_EXT就是lua脚本源码
/
/
static const std::string BYTECODE_FILE_EXT
=
".luac"
;
if
(pos !
=
std::string::npos)
{
buf
=
buf.substr(
0
, pos);
/
/
截取前缀
}
else
{
pos
=
buf.rfind(NOT_BYTECODE_FILE_EXT);
if
(pos
=
=
buf.length()
-
NOT_BYTECODE_FILE_EXT.length())
{
buf
=
buf.substr(
0
, pos);
}
}
FileUtils
*
utils
=
FileUtils::getInstance();
/
/
/
/
1.
check .luac suffix
/
/
2.
check .lua suffix
/
/
std::string tmpfilename
=
buf
+
BYTECODE_FILE_EXT;
if
(utils
-
>isFileExist(tmpfilename))
{
buf
=
tmpfilename;
}
else
{
tmpfilename
=
buf
+
NOT_BYTECODE_FILE_EXT;
if
(utils
-
>isFileExist(tmpfilename))
{
buf
=
tmpfilename;
}
}
std::string fullPath
=
utils
-
>fullPathForFilename(buf);
/
/
获取绝对路径
Data data
=
utils
-
>getDataFromFile(fullPath);
/
/
通过getDataFromFile读取lua文件到data
int
rn
=
0
;
if
(!data.isNull())
{
if
(luaLoadBuffer(_state, (const char
*
)data.getBytes(), (
int
)data.getSize(), fullPath.c_str())
=
=
0
)
/
/
通过luaLoadBuffer加载data
{
rn
=
executeFunction(
0
);
}
}
return
rn;
}
int
LuaStack::luaLoadBuffer(lua_State
*
L, const char
*
chunk,
int
chunkSize, const char
*
chunkName)
{
int
r
=
0
;
if
(_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen)
=
=
0
)
/
/
这里判断是否开启xxtea加密,如果开启就需要解密
{
/
/
decrypt XXTEA
xxtea_long
len
=
0
;
unsigned char
*
result
=
xxtea_decrypt((unsigned char
*
)chunk
+
_xxteaSignLen,
(xxtea_long)chunkSize
-
_xxteaSignLen,
(unsigned char
*
)_xxteaKey,
(xxtea_long)_xxteaKeyLen,
&
len
);
/
/
调用xxtea_decrypt解密脚本,这个函数在cocos2d
-
x\external\xxtea\xxtea.cpp里,加解密都在这个cpp里
unsigned char
*
content
=
result;
xxtea_long contentSize
=
len
;
skipBOM((const char
*
&)content, (
int
&)contentSize);
/
/
忽略utf8的bom
r
=
luaL_loadbuffer(L, (char
*
)content, contentSize, chunkName);
/
/
无论是否加密,解密后都会调用luaL_loadbuffer函数,所以直接hook这个函数把(char
*
)content这个字符dump出来就是解密过的lua脚本了
free(result);
}
else
{
skipBOM(chunk, chunkSize);
r
=
luaL_loadbuffer(L, chunk, chunkSize, chunkName);
/
/
这个返回值r会反映加载失败的类型,在下面的switch中打印出来
}
if
(r)
{
switch (r)
{
case LUA_ERRSYNTAX:
CCLOG(
"[LUA ERROR] load \"%s\", error: syntax error during pre-compilation."
, chunkName);
break
;
case LUA_ERRMEM:
CCLOG(
"[LUA ERROR] load \"%s\", error: memory allocation error."
, chunkName);
break
;
case LUA_ERRFILE:
CCLOG(
"[LUA ERROR] load \"%s\", error: cannot open/read file."
, chunkName);
break
;
default:
CCLOG(
"[LUA ERROR] load \"%s\", error: unknown."
, chunkName);
}
}
return
r;
}
int
LuaStack::luaLoadBuffer(lua_State
*
L, const char
*
chunk,
int
chunkSize, const char
*
chunkName)
{
int
r
=
0
;
if
(_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen)
=
=
0
)
/
/
这里判断是否开启xxtea加密,如果开启就需要解密
{
/
/
decrypt XXTEA
xxtea_long
len
=
0
;
unsigned char
*
result
=
xxtea_decrypt((unsigned char
*
)chunk
+
_xxteaSignLen,
(xxtea_long)chunkSize
-
_xxteaSignLen,
(unsigned char
*
)_xxteaKey,
(xxtea_long)_xxteaKeyLen,
&
len
);
/
/
调用xxtea_decrypt解密脚本,这个函数在cocos2d
-
x\external\xxtea\xxtea.cpp里,加解密都在这个cpp里
unsigned char
*
content
=
result;
xxtea_long contentSize
=
len
;
skipBOM((const char
*
&)content, (
int
&)contentSize);
/
/
忽略utf8的bom
r
=
luaL_loadbuffer(L, (char
*
)content, contentSize, chunkName);
/
/
无论是否加密,解密后都会调用luaL_loadbuffer函数,所以直接hook这个函数把(char
*
)content这个字符dump出来就是解密过的lua脚本了
free(result);
}
else
{
skipBOM(chunk, chunkSize);
r
=
luaL_loadbuffer(L, chunk, chunkSize, chunkName);
/
/
这个返回值r会反映加载失败的类型,在下面的switch中打印出来
}
if
(r)
{
switch (r)
{
case LUA_ERRSYNTAX:
CCLOG(
"[LUA ERROR] load \"%s\", error: syntax error during pre-compilation."
, chunkName);
break
;
case LUA_ERRMEM:
CCLOG(
"[LUA ERROR] load \"%s\", error: memory allocation error."
, chunkName);
break
;
case LUA_ERRFILE:
CCLOG(
"[LUA ERROR] load \"%s\", error: cannot open/read file."
, chunkName);
break
;
default:
CCLOG(
"[LUA ERROR] load \"%s\", error: unknown."
, chunkName);
}
}
return
r;
}
AppDelegate::applicationDidFinishLaunching
{
setXXTEAKeyAndSign
executeScriptFile
{
getDataFromFile
luaLoadBuffer
{
xxtea_decrypt
luaL_loadbuffer
{
luajit
}
}
executeFunction
}
}
AppDelegate::applicationDidFinishLaunching
{
setXXTEAKeyAndSign
executeScriptFile
{
getDataFromFile
luaLoadBuffer
{
xxtea_decrypt
luaL_loadbuffer
{
luajit
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2021-7-20 20:58
被挤蹭菌衣编辑
,原因: 图挂了