Cocos2dx游戏逆向实战
搞过cocos2dx游戏逆向的都知道lua的解密函数是下面这个:
int luaL_loadbuffer (lua_State *L,
const char *buff,
size_t sz,
const char *name);
网上有很多帖子,大多讲解了如何去解密lua代码和资源,然后回编译,重新签名打包apk,但是本文将采用一种新思路—通过对上述函数的hook,动态加载我们自己编写的lua脚步,实现劫持lua加载,这样就不需要回编译和打包apk的操作,实现外挂功能。目前cocos2dx的脚本类型主要有两种,一种是luac,另外一种是luajit.。本文讲解的是第一种luac,游戏的例子是这个:https://jltx.175game.com/ 可直接去官网下载apk。
Hook使用到的技术是Xposed hook 游戏so库加载 libgame.so。对于Android 9, xposed的核心代码如下所示:
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.qtz.game.jltx"))
return;
// It uses the class loader of the caller, which is usually the app, but would be Xposed as soon as you hook it. Please hook Runtime.loadLibrary() instead.
// loadLibrary0(VMStack.getCallingClassLoader(), libname); Android 9
XposedHelpers.findAndHookMethod("java.lang.Runtime", lpparam.classLoader,
"loadLibrary0", ClassLoader.class,String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
ClassLoader fromClass = (ClassLoader) param.args[0];
String soName = (String) param.args[1];
XposedBridge.log("afterHookedMethod System.loadLibrary0("+soName+")");
// 不可hook多次,so可能会加载多次
// https://www.jianshu.com/p/93828be3ff58
if(soName.contains("game")){
// 将hook-lib.so拷贝到应用程序库目录
// cp /data/app/com.example.hellondk-mE2vo3RWkDDrlWW-T8gywA\=\=/lib/arm64/libhook-lib.so /data/app/com.qtz.game.jltx-NJFXwyKPB3COyuej66PaOQ\=\=/lib/arm64/
XposedBridge.log("find "+soName);
Object[] newArgs = new Object[]{fromClass, "hook-lib"};
XposedBridge.invokeOriginalMethod(param.method, param.thisObject, newArgs);
}
}
});
}
逻辑就是游戏刚加载完so库时是我们比较好的hook时机。此时加载我们的hook-lib.so实现对luaL_loadbuffer()
的动态hook。
hook框架使用了Dobby,一个安全大佬写的hook库。接下来是hook-lib.so的核心代码:
bool saveFile(const void* addr,int len,const char*outFileName) {
bool success = false;
FILE* file = fopen(outFileName,"wb+");
if(file!=nullptr){
fwrite(addr,len,1,file);
fflush(file);
fclose(file);
success=true;
chmod(outFileName,S_IRWXU|S_IRWXG|S_IRWXO);
}else{
MY_LOG_ERROR("[%s] fopen failed,error: %s.",__FUNCTION__ ,dlerror());
}
return success;
}
/*
int luaL_loadbuffer (lua_State *L,
const char *buff,
size_t sz,
const char *name);
*/
// original function copy
int (*luaL_loadbuffer_orig) (void* L,const char *buff,int size,const char* name) =nullptr;
// local function
int lual_loadbuffer_mod(void* L,const char* buff,int size,const char* name) {
//MY_LOG_INFO("[dumplua] loadL_loadbuffer name: %s",name);
std::string path = "/storage/emulated/0/Download/jltx";
std::string modPath = "/storage/emulated/0/Download/jltx_modified";
if(name[0]!='/'){
path+='/';
modPath+='/';
}
path+=name;
modPath+=name;
// get the file name
std::string fileName = path;
std::string modFileName = modPath;
// get the path
int pos = path.rfind("/");
path = path.substr(0,pos);
pos = modPath.rfind("/");
modPath = modPath.substr(0,pos);
//MY_LOG_INFO("[dumplua] path: %s",path.c_str());
// create the directories
std::__fs::filesystem::create_directories(path);
std::__fs::filesystem::create_directories(modPath);
// judgement the file's existence ,if not exist , save it
bool isExist = std::__fs::filesystem::is_regular_file(fileName);
if (!isExist)
saveFile(buff,size,fileName.c_str());
else{
MY_LOG_INFO("[dumplua] %s has saved.",fileName.c_str());
}
isExist = std::__fs::filesystem::is_regular_file(modFileName);
if(isExist){
FILE* fp = fopen(modFileName.c_str(),"rb");
MY_LOG_INFO("[hook] hijack the %s.",fileName.c_str());
if(fp!=nullptr){
fseek(fp,0,SEEK_END);
long fileSize = ftell(fp);
unsigned char* buffer = (unsigned char*)malloc(fileSize);
if(buffer!=nullptr){
fseek(fp,0,SEEK_SET);
int readBytes = fread(buffer,fileSize,1,fp);
if(readBytes>0){
int ret = luaL_loadbuffer_orig(L,(const char*)buffer,fileSize,modFileName.c_str());
free(buffer);
MY_LOG_INFO("[hook] ret: %d.",ret);
return ret;
}
}
}
}
return luaL_loadbuffer_orig(L,buff,size,name);
}
void Hook(){
//DobbyHook((void*)&Java_com_example_hellondk_MainActivity_print,(void*)new_print,(void**)&orig_print);
MY_LOG_INFO("[dumplua] hook begin");
void* handle = dlopen("libgame.so",RTLD_NOW);
if(handle == nullptr){
MY_LOG_INFO("[dumplua] dlopen err: %s.",dlerror());
return;
}else{
MY_LOG_INFO("[dumplua] libgame.so dlopen OK!");
}
void *pluaL_loadbuffer = dlsym(handle,"luaL_loadbuffer");
if(pluaL_loadbuffer == nullptr) {
MY_LOG_ERROR("[dumplua] lua_loadbuffer not found!");
MY_LOG_ERROR("[dumplua] dlsym err: %s.", dlerror());
}else{
MY_LOG_DEBUG("[dumplua] lual_loadbuffer found!");
DobbyHook(pluaL_loadbuffer,(void*)&lual_loadbuffer_mod,(void**)&luaL_loadbuffer_orig);
}
}
JNIEXPORT void JNICALL Java_com_example_hellondk_MainActivity_Unhook(JNIEnv* env,jobject thiz) {
//DobbyDestroy((void*)&Java_com_example_hellondk_MainActivity_print);
}
}
jint JNI_OnLoad(JavaVM* vm,void* reserved){
MY_LOG_INFO("Start Hook");
Hook();
return JNI_VERSION_1_6;
}
上面的代码逻辑就是在JNI_OnLoad时完成hook操作。同时我们备份原始的luac脚本到Download目录。然后通过LuacGUI.exe(这个工具可以在网上下载到)反编译成lua脚步源码。
在拿到源码后,我们可以进行修改,分析,阅读相关游戏逻辑。
在阅读上面的相关模块后,我们进行一定的修改,再使用luac.exe,回编译,放到jltx_modified的目录下,就能实现劫持加,实现作弊检测的移除,游戏对象的修改等等。
.\luac.exe -o K:\Reverse\Android\jltx_decrypt\data\user\0\com.qtz.game.jltx\files\q2.game.qtz.com\scripts\module\login\login.lua.out K:\Reverse\Android\jltx_decrypt\data\user\0\com.qtz.game.jltx\files\q2.game.qtz.com\scripts\module\login\login.lua
以下是一些作弊检测分析结果:
-- 字符串特征码
--\230\136\152\230\150\151\231\187\147\230\158\156
--module\battle\battle.lua 1287
--
-- 发送视频回放
battle.sendBattleRecord()
--
battle.sendResult(battleConst.STATE_OVER)
server.rpc_server_fight_end(battle.battleType, effectMax, effectLv)
MissionData.sendCachedRpcRequest()
if battle.verifyStatus == battleConst.VERIFY_STATUS_CHEAT then
tellme.show(T("\230\136\152\230\150\151\231\187\147\230\158\156\230\160\161\233\170\140\229\164\177\232\180\165!!!"))
end
battle.sendResult 里有检测代码
function battle.checkCheat()
battle.sendVerifyInfo()
battle.sendImpeachInfo()
battle.verifyBattleData()
if battle.cheatInfo then
battle.sendCheatInfo(battle.cheatInfo)
battle.cheatInfo = nil
end
end
最终统计后发送这个
server.rpc_server_fight_verify(tostring(checksum))
local infoStr = json.encode(info)
server.rpc_server_fight_impeach(infoStr)
rpc_server_fight_result
rpc_server_pve_fight_result
触发作弊事件
gEvent.trigEvent(gEvent.BATTLE_CHEAT, verifyInfo)
发送作弊信息
battle.sendCheatInfo(info)
server.rpc_server_client_error(infoStr)
battle.onCheat
fight_obj.lua
699
function Obj:recordVerifyInfo(info)
self.cheatInfo = info
gEvent.trigEvent(gEvent.BATTLE_CHEAT, info)
end
function Obj:verifyAttr(k, v, randomKey)
local clones = self.fightAttrClones
for i, clone in ipairs(clones) do
if not clone[k] then
return nil
end
local cv
if i == 1 then
cv = randomKey - clone[k] / 373
elseif i == 2 then
cv = clone[k] - randomKey
cv = cv - math_floor(v / 20)
cv = cv <= 1 and cv >= -1 and v or v + 2
else
cv = clone[k] / i
end
local diff = v - cv
if not (diff > 0) or not diff then
diff = -diff
end
return nil
-- if diff > 1 then
-- logger.warn("It is cheating!!!", i, self:getName(), self:getFightObjId(), diff)
-- print("It is cheating!!!", self:getName(), self:getFightObjId(), diff)
-- return {
-- k = k,
-- v = v,
-- cv = cv,
-- diff = diff,
-- name = self:getName(),
-- clone1 = clones[1][k],
-- clone2 = clones[2][k],
-- id = self:getFightObjId(),
-- rkey = randomKey
-- }
-- end
end
return nil
end
function Obj:isVerify()
return false
end
function Obj:isVerify()
return true
end
local VERIFY_ATTRS = {
[FAConst.FA_HP] = true,
[FAConst.FA_DAMAGE] = true,
[FAConst.FA_ATTACK_SPEED] = true
}
检测血量,伤害,攻击速度
tellme.show 显示文字的代码
检测特征码 rpc_server_fingerprint
checkTool
特征码 \232\167\166\229\143\145\229\164\150\230\140\130\230\163\128\230\159\165
checkTool
battle.sendCheatInfo(info)
battle.checkCheat()
self:recordVerifyInfo(cheatInfo)
针对游戏对象的检测原则上过这个事件就ok了
触发的就是这个事件
function Obj:recordVerifyInfo(info)
self.cheatInfo = info
--gEvent.trigEvent(gEvent.BATTLE_CHEAT, info)
end
在阅读源码的过程中,发现游戏采用了UTF8编码字符串,
为了方便分析特意写了一个转换工具。
以下是代码涉及的仓库地址:
https://github.com/BeneficialCode/utf8_tool
https://github.com/BeneficialCode/XposedDev
https://github.com/BeneficialCode/HelloNDK
解密后的大部分lua源码地址
https://github.com/BeneficialCode/jltx_decrypt
已通知官方客服,由于研究游戏安全的过程中,被其他用户举报,导致账户被封。反馈给客服后,不予解封。对方客服以“如果对我们游戏造成恶劣影响或者损失,我们会保留相应的法律权利并进行追责行为。”搪塞,以上内容仅供学习,勿用于非法用途!