[Neon]ver 3.2.95(Frida)

发布于 2022-12-31  157 次阅读


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

一般来说,上述工作做完之后,就可以收工了,但往往现实是残酷的..

运行修改后的脚本再跑一次(注:此时mapsRedirecthook_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