0x00 前言
一不留神,近乎一年没有产出文章了,摸鱼摸得令人发指..趁着元旦假期,写写最近玩过的小游戏。本文使用Frida对游戏进行attach,通过替换lua来达到修改的目的。下一篇将使用il2cppdumper开展另一个方向的逆向尝试
关键词:Unity, libnesec, xlua
0x01 环境配置
Frida,一款基于python + javascript 的hook框架,非常好用以至于被各路加固厂商针对,检测Frida的方式也有了很多成熟方案,例如frida-server文件名检测、27042端口检测、D-Bus协议检测、/proc/*/maps检测。为了防止一次性踩中太多坑,我们本次使用strongR-frida 来进行hook,下载15.2.2的latest release版本即可。测试环境:红米K40,MIUI 12.5.18,Android 11
将hluda-server-15.2.2-android-arm64 push到/data/local/tmp 目录下,并赋予777权限,通过adb su执行如下命令:
./hluda-server-15.2.2-android-arm64 -l 0.0.0.0:9999
新建一个cmd窗口,执行如下命令,进行端口转发:
adb forward tcp:9999 tcp:9999
同窗口内使用frida-ps查看进程:
frida-ps -H 127.0.0.1:9999
0x02 Maps Check Bypass
在没有编写js脚本的情况下,尝试直接attach,并以no pause形式启动:
frida -H 127.0.0.1:9999 -f com.shangyoo.neon --no-pause

不出所料,直接被干掉了,想想这是易盾加固,倒也正常。按照惯例,先写一个脚本打印/proc/*/maps
内容,并保存到 /data/user/0/com.shangyoo.neon/maps
中:
function mapsRedirect() {
var fakePath = "/data/user/0/com.shangyoo.neon/maps";
var file = new File(fakePath, "w");
var buffer = Memory.alloc(512);
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readCString(pathnameptr);
var realFd = open(pathnameptr, flag);
if (pathname.indexOf("/proc/") >= 0 && pathname.indexOf("maps") >= 0) {
while (parseInt(read(realFd, buffer, 512)) !== 0) {
var oneLine = Memory.readCString(buffer);
file.write(oneLine);
}
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
}
return realFd;
}, 'int', ['pointer', 'int']));
}
打开 /data/user/0/com.shangyoo.neon/maps
,尝试搜索frida、/data/local/tmp
之类的关键词,发现:

这个*-64.so
是frida-server释出的注入到进程空间内的so,猜测易盾对其作了检测。为了证明猜想,我们继续写一个脚本hook open并打印:
function hook_open() {
Interceptor.attach(openPtr, {
onEnter:function(args) {
var filename = Memory.readCString(args[0]);
console.log("", filename);
},onLeave:function(retval) {
}
})
}

在打印出*-64.so
后游戏闪退,同时脚本退出,猜测得到了证实。将我们的function mapsRedirect()
稍作修改,重定向maps的同时过滤掉包含"/tmp"的内容:
function mapsRedirect() {
var fakePath = "/data/user/0/com.shangyoo.neon/maps";
var file = new File(fakePath, "w");
var buffer = Memory.alloc(512);
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readCString(pathnameptr);
var realFd = open(pathnameptr, flag);
if (pathname.indexOf("/proc/") >= 0 && pathname.indexOf("maps") >= 0) {
while (parseInt(read(realFd, buffer, 512)) !== 0) {
var oneLine = Memory.readCString(buffer);
if (oneLine.indexOf("/tmp") === -1) { //过滤/data/local/tmp
file.write(oneLine);
}
}
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
}
return realFd;
}, 'int', ['pointer', 'int']));
}
0x03 libart.so Check Bypass
一般来说,上述工作做完之后,就可以收工了,但往往现实是残酷的..
运行修改后的脚本再跑一次(注:此时mapsRedirect
与hook_open
同时启用):

发现输出停在了libart.so
,让我不禁想起了这篇文章:简单的frida检测思路,描述了一种通过遍历符号表,获得一个so中所有函数首地址来检测是否使用了frida的方式。 仔细看了看,没有发现很好的hook点,尝试了将libart.so 保存到其他地址进行open也不可行,那么咱们换个思路,既然检测的是/lib64/下的libart.so,那么替换成其他同目录的so是否可行呢?以/lib64/libz.so为例进行尝试:
function libCheckBypass() {
var fakelib = "/apex/com.android.art/lib64/libz.so";
var fakelibAddr = Memory.allocUtf8String(fakelib);
Interceptor.attach(openPtr, {
onEnter:function(args) {
var filename = Memory.readCString(args[0]);
//console.log("", filename);
if (filename.indexOf("libart.so") !== -1) {
//console.log(" -----libart.so has been opened-----");
args[0] = fakelibAddr;
}
},onLeave:function(retval) {
}
})
}
经过n次不明所以的崩溃,成功进入到游戏登录界面
注意:这不是一种优雅的hook方式。已知会在libkxqpplatform.so、libmain.so加载过程中发生随机崩溃,推测与游戏内集成的乐变热更新有关,如果有小伙伴有fix crash的思路,请联系我
0x04 Dump lua
由于易盾自定义linker,导致例如libil2cpp.so、libxlua.so
均不通过系统的 dlopen
或 android_dlopen_ext
进行加载,hook dlopen是无法获得so地址的,那么我们通过读取/proc/*/maps
来判断libxlua.so 是否已经加载,继而获得地址并hook xluaL_loadbuffer
。同时注意 /proc/*/maps
会被多次打开,但xlua.so 仅需hook一次,我们需要额外添加is_can_hook
用于判断:
var is_can_hook = 0;
function mapsRedirect() {
var fakePath = "/data/user/0/com.shangyoo.neon/maps";
var file = new File(fakePath, "w");
var buffer = Memory.alloc(512);
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readCString(pathnameptr);
var realFd = open(pathnameptr, flag);
if (pathname.indexOf("/proc/") >= 0 && pathname.indexOf("maps") >= 0) {
while (parseInt(read(realFd, buffer, 512)) !== 0) {
var oneLine = Memory.readCString(buffer);
if (oneLine.indexOf("xlua.so") >= 0) {
//console.log("[hook]find xlua.so");
is_can_hook++;
if (is_can_hook === 1) {
hook_xluaDumpFile();
//hook_xluaReplaceFile();
}
}
if (oneLine.indexOf("/tmp") === -1) { //过滤/data/local/tmp
file.write(oneLine);
}
}
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
}
return realFd;
}, 'int', ['pointer', 'int']));
}
hook xluaL_loadbuffer
没什么好说的,直接上代码。注意lua文件夹需要手动创建:
function hook_xluaDumpFile() {
if (xluaL_loadbuffer_ptr) {
Interceptor.attach(xluaL_loadbuffer_ptr, {
onEnter: function (args) {
this.fileout = "/sdcard/lua/" + Memory.readCString(args[3]).split("/").join(".");
console.log("[dumpxlua]" + this.fileout);
var tmp = Memory.readByteArray(args[1], args[2].toInt32());
var file = new File(this.fileout, "w");
file.write(tmp);
file.flush();
file.close();
}
});
}
}
0x05 Modify lua
dump下来的都是luac文件,opnames也没有修改过顺序,参考[Phantomoon]ver 5.0.12044,使用官方的unluac反编译即可。游戏的核心lua文件为OutGameData.lua
,看了看其中的逻辑,比较关键的地方在 function OutGameData.FixroleDetailDataOut(outId, BaseFixId, PercentFixId)
:
function OutGameData.FixroleDetailDataOut(outId, BaseFixId, PercentFixId)
if not OutGameData.roleDetailData[BaseFixId] then
local _BaseFixAsset = CS.XmlMgr.Query_AttributeNode(BaseFixId)
if BaseFixId == 105 then
OutGameData.roleDetailData[BaseFixId] = {
id = BaseFixId,
name = _BaseFixAsset.name,
para = _BaseFixAsset.para,
totalAddValue = OutGameData.roleDetailData_Out[103] and OutGameData.roleDetailData_Out[103].finalValue or 0
}
else
if BaseFixId == 107 then
OutGameData.roleDetailData[BaseFixId] = {
id = BaseFixId,
name = _BaseFixAsset.name,
para = _BaseFixAsset.para,
totalAddValue = OutGameData.roleDetailData_Out[103] and OutGameData.roleDetailData_Out[103].finalValue or 0
}
else
end
end
end
PercentFixId = PercentFixId or 0
local _outAsset = CS.XmlMgr.Query_AttributeNode(outId)
OutGameData.roleDetailData_Out[outId] = {}
OutGameData.roleDetailData_Out[outId].name = OutGameData.roleDetailData[outId] and OutGameData.roleDetailData[outId].name or _outAsset.name
OutGameData.roleDetailData_Out[outId].para = OutGameData.roleDetailData[outId] and OutGameData.roleDetailData[outId].para or _outAsset.para
OutGameData.roleDetailData_Out[outId].finalValue = (OutGameData.roleDetailData[BaseFixId] and OutGameData.roleDetailData[BaseFixId].totalAddValue or 0) * ((outId == 105 and 0 or 1) + (OutGameData.roleDetailData[PercentFixId] and OutGameData.roleDetailData[PercentFixId].totalAddValue or 0))
if OutGameData.roleDetailData[BaseFixId] then
end
end
重点关注一下 OutGameData.roleDetailData_Out[outId].finalValue
中的 (outId == 105 and 0 or 1)
,这是一句三目运算符,这里我们不深究具体含义,直接将其改为固定数字例如"3",即可实现人物全属性3倍的效果。但具体get_Data的函数并未在lua中找到,金币、钥匙、蛋等参数也没法修改,应该是在il2cpp中进行了相关实现,有机会的话放到下篇文章再说
0x06 Replace lua
一样没啥好说的,直接贴代码,将 mapsRedirect()
中执行的hook_xluaDumpFile()
替换为 hook_xluaReplaceFile()
即可。lua文件的编译可以直接使用官方版本的lua53,传送门:
var fopenPtr = Module.findExportByName("libc.so", "fopen");
var fopen = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);
var fclosePtr = Module.findExportByName("libc.so", "fclose");
var fclose = new NativeFunction(fclosePtr, 'int', ['pointer']);
var fseekPtr = Module.findExportByName("libc.so", "fseek");
var fseek = new NativeFunction(fseekPtr, 'int', ['pointer', 'int', 'int']);
var ftellPtr = Module.findExportByName("libc.so", "ftell");
var ftell = new NativeFunction(ftellPtr, 'int', ['pointer']);
var freadPtr = Module.findExportByName("libc.so", "fread");
var fread = new NativeFunction(freadPtr, 'int', ['pointer', 'int', 'int', 'pointer']);
function hook_xluaReplaceFile() {
var xluaL_loadbuffer_ptr = Module.findExportByName("libxlua.so", "xluaL_loadbuffer");
//Module.findBaseAddress("libxlua.so").add(0x74908);
var xluaL_loadbuffer = new NativeFunction(xluaL_loadbuffer_ptr, 'int', ['pointer', 'pointer', 'int', 'pointer']);
Interceptor.replace(xluaL_loadbuffer_ptr, new NativeCallback(function (L, buffer, size, name) {
let luaName = Memory.readCString(name);
//console.log("[dumpxlua]" + luaName);
if (luaName.indexOf("OutGameData") >= 0) {
console.log("Detect OutGameData");
let newLuaPath = Memory.allocUtf8String('/storage/emulated/0/dump/OutGameData');
let openMode = Memory.allocUtf8String('r');
let file = fopen(newLuaPath, openMode);
if (file != null) {
fseek(file, 0, 2);
let new_size = ftell(file);
fseek(file, 0, 0);
let new_buffer = Memory.alloc(new_size + 1);
fread(new_buffer, new_size, 1, file);
fclose(file);
return xluaL_loadbuffer(L, new_buffer, new_size, name);
}
}
return xluaL_loadbuffer(L, buffer, size, name);
}, 'int', ['pointer', 'pointer', 'int', 'pointer']));
}
0x07 Final results

Comments | 1 条评论
学到了