0x00 前言
有这么一个游戏:
第一天更新策略,红手指闪退;
第二天更新策略,手机脚本闪退;
第三天更新策略,只装了riru模块居然都闪退...
小爷有些不爽,今个就来会会你(•́へ•́╬),说的就是你!Phantomoon!
关键词:Unity,libNetHTProtect,
xlua
,
opcode
,arm64
0x01 隐藏Riru
经测试,开启Magisk Core Only Mode
后游戏可以正常运行,证明安装的其他Magisk
模块会被易盾检测到。但我的手机中只安装了Riru
系的模块,考虑游戏启动时进程空间内会存在libriru.so
等Riru
系的动态库,假设易盾是因为对/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-加入字节码兼容的支持


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

再丢给unluac
试试:

好家伙,报IllegalStateException...基本可以确定游戏对lua的Opcode映射表做了修改,发量-1
0x03 修复Opcode
对于lua5.3来说,一共有47个Opcode,定义于/src/lopcodes.h
中。我们也可以在/src/lopcodes.c
的 luaP_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.java
中case 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)
Comments | 13 条评论
#该评论为私密评论#
@WS
可以参考perfare的riru-il2cppdumper,使用whale框架
@Floeice
#该评论为私密评论#
@Floeice
#该评论为私密评论#
@Floeice
#该评论为私密评论#
@WS
建议你照抄P大的il2cppdumper模块,把用不上的地方注释了,然后看看自己riru的版本,如果riru版本比较高,原有模块的一些api也得对应着升级过来。我记得il2cppdumper模块对应的是riru ver21.3
@Floeice
#该评论为私密评论#
@Floeice
#该评论为私密评论#
@Floeice
#该评论为私密评论#
#该评论为私密评论#
@PP
可能是因为一些防护措施(
#该评论为私密评论#
@xxeh
好久没玩了 这游戏越来越自动化了,感觉也没啥可改的呀(