初步认识cocos2d-x
先clone到本地
git clone https://github.com/cocos2d/cocos2d-x.git
Cocos2d-x是一个开源的移动2D游戏框架,底层支持各种平台,核心用c++封装了各种库,外面给了lua和c++的接口,所以关键代码可能在lua中,很多安卓游戏的逻辑也基本都在lua脚本里,盗用官网这张图
从c++进入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
applicationDidFinishLaunching函数
应用结束加载中进入lua虚拟机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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;
}
|
这句engine->executeScriptFile("main.lua")调用了cocos2d-x\cocos\scripting\lua-bindings\manual\CCLuaEngine.cpp的executeScriptFile调用了CCLuaStack.cpp的executeScriptFile
LuaStack::executeScriptFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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;
}
|
LuaStack::luaLoadBuffer
luaLoadBuffer里调用xxtea_decrypt解密了lua脚本,然后调用luaL_loadbuffer加载解密后的脚本,所以直接hook 这个函数luaL_loadbuffer把(char*)content这个字符dump出来就得到解密过的lua脚本了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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;
}
|
而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世界的调用逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | AppDelegate::applicationDidFinishLaunching
{
setXXTEAKeyAndSign
executeScriptFile
{
getDataFromFile
luaLoadBuffer
{
xxtea_decrypt
luaL_loadbuffer
{
luajit
}
}
executeFunction
}
}
|
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这个字符串进一步验证了正确性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int __fastcall AppDelegate::applicationDidFinishLaunching(AppDelegate * this)`
`{`
/ / `前面是一堆初始化啥的没用的`
....
`( * * (v7 + 1 ) + 116 ))( * (v7 + 1 ), "ZX_01RdsF~@!R8" , 14 , "ZX_CS@56#D~d@dud" , 16 );`
` if ( ( * ( * v7 + 28 ))(v7, "base/src/main.lua" ) )`
` return 0 ;`
`v26 = GetMCKernel();`
` if ( !v26 )`
` return 0 ;`
`v27 = * (this + 145 );
if ( v27 )
v27 + = 580 ;
v28 = ( * ( * v26 + 20 ))(v26, v27);`
`v29 = * (cocos2d::Director::getInstance(v28) + 152 );`
`v32[ 0 ] = AppDelegate::GlobalUpdate;`
`v32[ 1 ] = - 8 ;`
`cocos2d::Scheduler::schedule(v29, AppDelegate::GlobalUpdate, - 8 , this + 4 , 0.0 , 0xFFFFFFFE , 0.0 , 0 );`
` return v25;`
`}`
|
有了key和sign就可以直接解密luac脚本了,照着写,很简单,可以看到sign的作用就是被忽略,只有长度有用,尴尬,解密用的主要是key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | unsigned char * result = xxtea_decrypt((unsigned char * )chunk + _xxteaSignLen,
(xxtea_long)chunkSize - _xxteaSignLen,
(unsigned char * )_xxteaKey,
(xxtea_long)_xxteaKeyLen,
& len );
unsigned char * xxtea_decrypt(unsigned char * data, xxtea_long data_len, unsigned char * key, xxtea_long key_len, xxtea_long * ret_length)
{
unsigned char * result;
* ret_length = 0 ;
if (key_len < 16 ) {
unsigned char * key2 = fix_key_length(key, key_len);
result = do_xxtea_decrypt(data, data_len, key2, ret_length);
free(key2);
}
else
{
result = do_xxtea_decrypt(data, data_len, key, ret_length);
}
return result;
}
|
或者直接pip install xxtea-py
然后直接python,自己写个循环实现批量解密吧
1 2 3 4 5 6 7 8 9 10 11 12 13 | import xxtea
import os
orig_path = “” / / 初始路径
new_path = “” / / 存储路径
xxtea_sign = “”
xxtea_key = “”
orig_file = open (orig_path, "rb" )
encrypt_bytes = orig_file.read()
orig_file.close()
decrypt_bytes = xxtea.decrypt(encrypt_bytes[ len (xxtea_sign):], xxtea_key)
new_file = open (new_path, "wb" )
new_file.write(decrypt_bytes)
new_file.close()
|
下面开始分析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就可以加金币了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function BulletLayer:on_bullet_crash_fish(bullet_obj, t_fish_list)
local scene = self :get_scene()
local master_id = bullet_obj:get_master_id()
local bullet_uid = bullet_obj:get_bullet_uid()
local self_player_id = self :get_self_player_id()
local num =
if (num > = 2 ) then
local fish_layer = self :get_fish_layer()
table.sort(t_fish_list, function(uid1, uid2)
local fish_obj1 = fish_layer:get_fish_by_uid(uid1);local fish_obj2 = fish_layer:get_fish_by_uid(uid2)
local zorder1 = fish_obj1:getLocalZOrder();local zorder2 = fish_obj2:getLocalZOrder()
if (zorder1 > zorder2) then return true end
end)
end
local fish_uid = t_fish_list[ 1 ]
if (master_id = = self_player_id) then
scene:on_self_bullet_crash_fish(master_id, bullet_uid, fish_uid)
- - scene:on_self_bullet_crash_fish_test(master_id, bullet_uid, fish_uid)
end
scene:on_bullet_crash_fish(master_id, bullet_uid, t_fish_list)
end
|
在定位一下加钱的函数,fish_gold是鱼的钱,fish_odds是鱼的剩余 ,这句 local one_add_gold = math.floor(fish_gold/12)就是一个鱼加的钱,我们把这个/12改为*12就可以修改倍数了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function AnimationLayer:play_catch_fish_earn_money_self(fish_gold, fish_odds)
local earn_money_node = self :reuse_new_earn_money_view_node( 'zhuanqianle_buyu' , self , 0 )
local pos = cc.p(sizeVisble.width / 2 , sizeVisble.height / 2 )
local one_add_gold = math.floor(fish_gold / 12 )
local min = self :get_earn_money_one_add_gold_min()
if (one_add_gold < min ) then one_add_gold = min end
earn_money_node:setPosition(pos); earn_money_node:set_gain_gold(fish_gold)
earn_money_node:set_one_add_gold(one_add_gold); earn_money_node:set_cur_gold( 0 )
local delay_time = cc.DelayTime:create( 4 )
local call_back = cc.CallFunc:create(handler( self , self .call_back_reuse_obj))
local seq = cc.Sequence:create(delay_time, call_back);
earn_money_node:setScale( 1 )
earn_money_node:runAction(seq);
earn_money_node:play_ani()
end
|
入口代码在AnimationLayer.lua里,直接看接收到玩家捕到鱼的函数,分为网或者炸弹,里面还有一网打尽和大转盘之类的特效,这里可以把它全部改成高倍的特效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | function AnimationLayer:on_recv_player_catch_fish(player_id, fish_uid, fish_gold)
local fish_layer = self :get_fish_layer()
local fish_obj = fish_layer:get_fish_by_uid(fish_uid)
if (fish_obj = = nil) then return end
local data_center = self :get_data_center()
local fish_id = fish_obj:get_fish_id()
local fish_info = data_center:get_fish_info(fish_id)
if (fish_info = = nil) then return end
local fish_type = fish_info. type
if (fish_type = = GameDefine.fish_type.wang) then
local other_fish_obj_list = self :get_other_fish_yi_wang_da_jin_list(fish_obj, GameDefine.fish_type.wang)
local num =
if (num > 0 ) then
local fish_odds = self :get_catch_fish_odds(fish_obj, other_fish_obj_list)
self :on_player_catch_fish_yi_wang_da_jin(player_id, fish_obj, fish_gold, fish_odds, other_fish_obj_list)
end
self :on_player_catch_fish_drop_fish_gold(player_id, fish_obj, 0 , 0 )
return
end
if (fish_type = = GameDefine.fish_type.bomb) then
play_drop_gold = 0
local other_fish_list = self :get_bomb_fish_effect_fish_list()
local num =
if (num > 0 ) then
local fish_odds = self :get_catch_fish_odds(fish_obj, other_fish_list)
self :on_player_catch_fish_bomb(player_id, fish_obj, fish_gold, fish_odds, other_fish_list)
end
self :on_player_catch_fish_drop_fish_gold(player_id, fish_obj, 0 , 0 )
return
end
local fish_odds = fish_info.mulriple_max;local fish_name = fish_info.name
self :on_player_cath_fish_da_zhuan_pan(player_id, fish_type, fish_gold, fish_name)
self :on_player_catch_fish_drop_fish_gold(player_id, fish_obj, fish_gold, fish_odds)
end
|
src\models\DataCenter.lua里存储着鱼和子弹等参数,可以看到子弹最高9级,odds_min与odds_max应该就代表威力,改这里可以改子弹威力
1 2 3 4 5 6 7 8 9 10 11 12 | {
room_style = 3 ,
cannon_id = 109 ,
odds_min = 7 ,
odds_max = 10 ,
level = 3 ,
res = "pao3_buyu" ,
bullet = "zd9" ,
net = "yuwang3_buyu" ,
time = 200 ,
sound = 109
}
|
self.fish_client存储不同鱼的参数,不同id对应不同鱼,mulriple_min与mulriple_max存储着金币的倍数最大与最小值,一般鱼都是2-3倍,大章鱼为300倍,改这里可以直接改鱼的倍数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {
id = 302 ,
name = "60倍组合鱼" ,
packet = "4lian_buyu" ,
crash_model = 101 ,
mulriple_min = 60 ,
mulriple_max = 60 ,
type = 101 ,
zoder = 21 ,
die_sound = "fish14_1" ,
die_type = 4 ,
copy_num = 4
},
{
id = 402 ,
name = "大章鱼" ,
packet = "yu22_buyu" ,
crash_model = 16 ,
mulriple_min = 300 ,
mulriple_max = 300 ,
type = 2 ,
zoder = 30 ,
bron_sound = "f_wb_3" ,
die_sound = "fish33_1" ,
die_type = 5 ,
copy_num = 4
},
|
其他的功能逻辑获得源码后都很清晰,改倍数改鱼改子弹什么都很简单,改完之后重新用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解密反编译
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2021-7-20 20:58
被挤蹭菌衣编辑
,原因: 图挂了