[Phantomoon]ver 5.0.12044

发布于 2021-01-03  1.06k 次阅读


0x00 前言

有这么一个游戏:

第一天更新策略,红手指闪退;

第二天更新策略,手机脚本闪退;

第三天更新策略,只装了riru模块居然都闪退...

小爷有些不爽,今个就来会会你(•́へ•́╬),说的就是你!Phantomoon!

关键词:Unity,libNetHTProtect,xluaopcode,arm64

0x01 隐藏Riru

经测试,开启Magisk Core Only Mode后游戏可以正常运行,证明安装的其他Magisk模块会被易盾检测到。但我的手机中只安装了Riru系的模块,考虑游戏启动时进程空间内会存在libriru.soRiru系的动态库,假设易盾是因为对/proc/tid/maps进行检测而导致闪退,那么就得对Riru系的动态库进行隐藏。

恰好,Riru的项目说明中提及了这么一条:How does Hide works?

1.When the file "/data/adb/riru/enable_hide" exists, the hidden mechanism will be enabled (also requires the support of the modules);
2.From v22.0, Riru provides a hidden mechanism (idea from Haruue Icymoon), 
make the memory of Riru and module to anonymous memory to hide from "/proc/maps string scanning".

即:在/data/adb/riru下创建一个名为enable_hide的空文件,即可开启Riru的隐藏自身功能,以规避/proc/tid/maps检测。

完成上述操作后,游戏不再闪退,maps下Riru系的动态库都已经被隐藏了,下面开始谈谈爆破的事儿。

0x02 Hook xlua.so && Dump luac

打开安装包看看,发现是unity+xlua的架构。本来想着赌一把看看具体游戏逻辑在不在libil2cpp.so里面,花了老大功夫,又是用GG dump so文件,dump下来又手修header确保il2cppdumper可以正常使用,结果发现游戏逻辑全在lua里...

想说几点:

1.GG会被检测,45秒闪退,要dump so的话,手要快;

2.如何手修header,以前的文章里有写;

3.Riru-il2cppdumper是神器,但可惜这个游戏没法用

为了通过hook xlua直接得到脚本,直接使用Riru-ModuleTemplate(API v10,Riru ver23.1)编写一个模块,对xluaL_loadbuffer函数下手:

//保存LUA文件到Sdcard
void saveFile(const char *data, size_t data_len, const char *path) {
    FILE* outfile;
    char drive[_MAX_DRIVE];
    char dir[_MAX_DIR];
    char fname[_MAX_FNAME];
    char ext[_MAX_EXT];
    std::string filename(path);
    std::string _path = ("/data/data/com.fantablade.watergun/files/");
    _path.append(filename);
    char *s = new char[100];
    strcpy(s,_path.c_str());
    _splitpath(s, drive, dir, fname, ext);
    //如果文件已经存在直接返回
    if(0 == access(s,F_OK))
    {
        LOGI("[dumpulua]  path:%s Exist",s);
        return;
    } else {
        //创建目录
        createMultiLevelDir(dir);
        LOGI("[dumpulua]  path:%s New file",s);
    }
    FILE *file = fopen(s, "wb+");
    if (file != NULL) {
        fwrite(data , sizeof(unsigned char) , data_len , file);
        fclose(file);
    }
}

int (*old_xluaL_loadbuffer)(void *L, const char *buff, int size, const char *name);
int new_xluaL_loadbuffer(void *L, const char *buff, int size, const char *name) {
    saveFile(buff, size, name);
    return old_xluaL_loadbuffer(L, buff, size, name);
}

void xlua_hook() {
    unsigned long base_addr;
    while (true) {
        base_addr = get_module_base("libxlua.so");
        if (base_addr != 0) {
            LOGD("Detect libxlua.so %lx", base_addr);
            unsigned long hack_addr = base_addr + 0x000000000004C7AC;
            WInlineHookFunction((void *)hack_addr, (void *) new_xluaL_loadbuffer,
                                (void **) &old_xluaL_loadbuffer);
            LOGI("Hook libxlua.so done!");
            break;
        }
        usleep(500000);
    }
}

通过ida查看xlua.so的字符串,可以确定lua版本为5.3,通过dump得到的实际上是编译后的luac文件。使用official版本的unluac随便反编译一个luac试试:

意料之中的报错

这里提示的“non-standard lua format”实际上是xlua项目自己弄出来的...详见xlua-加入字节码兼容的支持

启用“字节码兼容”后会设置字节码格式为1
此处size_t=uint32_t,长度0x04,如果启用”字节码兼容“,luac头部将少一个"04"字节

了解上述后,手修luac header,将字节码格式从0x01改为0x00,并添加0x04字节:

再丢给unluac试试:

好家伙,报IllegalStateException...基本可以确定游戏对lua的Opcode映射表做了修改,发量-1

0x03 修复Opcode

对于lua5.3来说,一共有47个Opcode,定义于/src/lopcodes.h中。我们也可以在/src/lopcodes.cluaP_opnames数组中得到其对应关系,通过ida搜索字符串的方式可以找到映射表。以米哈游的未定事件簿为例,这款游戏安装包里的xlua.so是official版本,lua版本5.3,Opcode映射表未作修改:

而Phantomoon这款游戏的Opcode如下,显然是经过自定义的:

/src/lvm.c中的 luaV_execute 函数得知,通过 GET_OPCODE 函数获取到Opcode后,再跳转执行对应操作:

void luaV_execute (lua_State *L) {
  ...
    vmdispatch (GET_OPCODE(i)) {
      vmcase(OP_MOVE) {
        setobjs2s(L, ra, RB(i));
        vmbreak;
      }
      vmcase(OP_LOADK) {
        TValue *rb = k + GETARG_Bx(i);
        setobj2s(L, ra, rb);
        vmbreak;
      }
  ...
}

一旦改变了 Opcode映射,对应跳转的case也会发生相应改变。比较直接的方法是将原版和修改后的 luaV_execute 函数在ida中进行对比,手动找到每个case和Opcode之间的对应顺序关系,缺点就是工作量大了亿点点。原版xlua中,case 0x00对应vmcase(OP_MOVE),而在Phantomoon的xlua中,vmcase(OP_MOVE) 对应的是case 0x10

通过逐一对比,可以得到一份 Phantomoon xlua中Opcode顺序:

LUAI_DDEF const char *const luaP_opnames[NUM_OPCODES+1] = {
  "ADD", //orig 13 p 0
  "SUB", //orig 14 p 1
  "MUL", //orig 15 p 2
  "MOD", //orig 16 p 3
  "POW", //orig 17 p 4
  "DIV", //orig 18 p 5
  "IDIV", //orig 19 p 6
  "BAND", //orig 20 p 7
  "BOR", //orig 21 p 8
  "BXOR", //orig 22 p 9
  "SHL", //orig 23 p 10
  "SHR", //orig 24 p 11
  "UNM", //orig 25 p 12
  "BNOT", //orig 26 p 13
  "NOT", //orig 27 p 14
  "LEN", //orig 28 p 15
  "MOVE", //orig 0 p 16
  "CONCAT", //orig 29 p 17
  "JMP", //orig 30 p 18
  "EQ", //orig 31 p 19
  "LT", //orig 32 p 20
  "LE", //orig 33 p 21
  "TEST", //orig 34 p 22
  "TESTSET", //orig 35 p 23
  "CALL", //orig 36 p 24 
  "TAILCALL", //orig 37 p 25 
  "RETURN", //orig 38 p 26 
  "FORLOOP", //orig 39 p 27
  "FORPREP", //orig 40 p 28
  "TFORCALL", //orig 41 p 29
  "TFORLOOP", //orig 42 p 30
  "SETLIST", //orig 43 p 31
  "CLOSURE", //orig 44 p 32
  "VARARG", //orig 45 p 33
  "EXTRAARG", //
  "LOADK", //orig 1 p 35
  "LOADKX", //orig 2 p 36
  "LOADBOOL", //orig 3 p 37
  "LOADNIL", //orig 4 p 38
  "GETUPVAL", //orig 5 p 39
  "GETTABUP", //orig 6 p 40
  "GETTABLE", //orig 7 p 41
  "SETTABUP", //orig 8 p 42
  "SETUPVAL", //orig 9 p 43
  "SETTABLE", //orig 10 p 44
  "NEWTABLE", //orig 11 p 45
  "SELF", //orig 12 p 46
  NULL
};

0x04 编译自定义unluac && lua5.3.0

1.编译unluac

源码下载地址:unluac_HgCode,使用Intellij IDEA进行编译。

修改/src/unluac/decompile/OpcodeMap.javacase LUA53为对应Opcode顺序,编译输出jar即可使用。

2.编译lua5.3.0

源码下载地址:lua_ftp,使用VS 2019进行编译。

分别修改lopcodes.h中165行的enum Opcode lopcodes. c中的luaP_opnames/luaP_opmodes为对应Opcode顺序,尔后将lopcodes.h中219行的 NUM_OPCODES 修改为:

#define NUM_OPCODES	(cast(int, OP_SELF) + 1)

打开 developer command prompt for VS2019,切换到lua源码所在src文件夹,依次输入如下命令(每次一行):

cl /MD /O2 /c /DLUA_BUILD_AS_DLL *.c 
ren lua.obj lua.o 
ren luac.obj luac.o 
link /DLL /IMPLIB:lua5.3.lib /OUT:lua5.3.dll *.obj 
link /OUT:lua.exe lua.o lua5.3.lib 
lib /OUT:lua5.3-static.lib *.obj 
link /OUT:luac.exe luac.o lua5.3-static.lib 

确认编译无误后,src文件夹下会生成lua.exe(解释器)、luac.exe(编译器)、lua5.3.dll(动态库)三个文件。

0x05 修改 && 编译lua

@Logic.CharacterActorManager文件为例,这是游戏中跟角色逻辑相关的lua文件。关注一下246行的criteffect_tt这个参数:

...
self.rolelv = mastery
self.armorArray = armor
self.armor = 0
self.critrate = 0
self.criteffect_tt = 15000
self.mass = 108
self.movespeed = 10
self.damage = 0
self._moveTime = 0
...

联系上下文猜测,人物的初始暴击伤害均为150% ,这个 criteffect_tt指的应该就是暴击伤害,上面的critrate则是暴击率。将 criteffect_tt 一项修改为30000,人物初始暴击伤害将变为300%。还有很多其他可以修改的地方,这里就不一一赘述了。

lua文件修改完毕后,使用如下命令生成luac文件:

luac -o out.luac in.lua

0x06 替换lua

为了使游戏在读取 @Logic.CharacterActorManager 文件时改为读取已修改过的 Logic.CharacterActorManager.luac文件,需要对Hook代码做一些调整:

int new_xluaL_loadbuffer(void *L, const char *buff, int size, const char *name) {
    //saveFile(buff, size, name);
    const char *wp = "@Logic.CharacterActorManager";
    if (strstr(name, wp)) {
        LOGI("Detect CharacterActorManager");
        const char *lua = "/data/data/com.fantablade.watergun/files/Logic.CharacterActorManager.luac";
        FILE *file = fopen(lua, "r");
        if (file != NULL) {
            fseek(file, 0, SEEK_END);
            size_t new_size = ftell(file);
            fseek(file, 0, SEEK_SET);
            char *new_buffer = (char *) malloc(new_size + 1);
            fread(new_buffer, new_size, 1, file);
            fclose(file);
            return old_xluaL_loadbuffer(L, new_buffer, new_size, name);
        }
    }
    return old_xluaL_loadbuffer(L, buff, size, name);
}

使用gradle-build-assemble命令将模块打包成zip文件后,使用Magisk进行刷入。重启后运行游戏,输出log如下,证明已经Hook成功:

0x07 备忘

self.criteffect_tt // 人物暴击率/100
local water_recover = 1 + self._get_energy_recover() // 能量回复
self._hurtTime // 受击后无敌时间(s)