首页
社区
课程
招聘
[原创]对一手游的自定义 luajit 字节码的研究
发表于: 2024-1-15 20:41 17164

[原创]对一手游的自定义 luajit 字节码的研究

2024-1-15 20:41
17164

最近闲下来之后无聊研究起了一个unity手游 大量使用了 lua (或者说就是 lua 写的 ) 看到网上已有的一些针对方案 都觉得太不方便 于是深入研究了一下 他自定义的 luajit

首先 这是一个 unity的 传统手游 这里就跳过较为前期的部分

像是 libtersafe . libbugly . libcri_ware 这些都是老熟人了 都跳过

对于 unity游戏来说 特别是商业手游 热更新几乎是必须的

由此诞生很多方案 这里简单介绍几个重点

以下来自 gpt

LuaC 和 LuaJIT 都是与 Lua 编程语言相关的工具,但它们的用途和性能特点有所不同。

LuaC:

LuaJIT:

总的来说,LuaC 更多地关注于代码的移植性和标准化,而 LuaJIT 更专注于提高执行效率和性能。用户可以根据自己的需要选择使用 LuaC 还是 LuaJIT。

luac 和luajit的字节码一致吗 api呢

LuaC 和 LuaJIT 在字节码和 API 方面存在一些差异:

字节码兼容性:

LuaC 和 LuaJIT 生成的字节码不完全一致。LuaC 遵循 Lua 官方的标准,为特定版本的 Lua(如 Lua 5.1, Lua 5.2 等)生成相应的字节码。LuaJIT 也能够编译为字节码,但它的字节码格式是为 JIT 编译器优化的。
由于 LuaJIT 主要与 Lua 5.1 兼容,它的字节码在某种程度上与 Lua 5.1 的字节码相似。然而,由于 JIT 优化的原因,LuaJIT 生成的字节码可能在某些情况下与标准 Lua 字节码不兼容。
这意味着用 LuaC 编译的字节码可能无法在 LuaJIT 上正确运行,反之亦然。
API 兼容性:

在 API 层面,LuaJIT 与 Lua 5.1 高度兼容。它支持 Lua 5.1 的全部标准 API,并且引入了一些自己的扩展。这些扩展主要是为了提高性能和扩展功能。
如果你的 Lua 代码是针对 Lua 5.1 API 编写的,那么它在 LuaJIT 上应该可以无缝运行。但如果你使用了 LuaJIT 的特定扩展,那么这部分代码可能无法在标准 Lua 解释器上运行。
综上所述,虽然 LuaC 和 LuaJIT 在很多方面是兼容的,但在字节码和特定 API 的使用上存在一些差异。如果你的项目需要在不同环境中移植,需要特别注意这些差异。对于大多数应用来说,这些差异并不会造成显著问题,但在高性能或特定的应用场景下,选择合适的工具是非常重要的。

分析之前 我们现确定一下目标

让使用 luajit 的应用能执行我们提供的 lua 代码

luajit 源码

库源码

xlua , tolua 等等都是开源的 而且区别主要在和 c#对接的部分 对于我们需要研究的部分 差别不大

vs ( 用于分析 c 源码和 c# 源码 )

vsc ( 用于分析编写 lua 和 js / ts )

python ( 自动化工作流 , frida )

node ( 编译 ts )

010editor (分析二进制 lua bc)

ida ( 分析修改后的luajit )

最好吧 unity 也带上 方便需要问题可以用 unity 实际测试一下

在 app 中 我们可以直接看到 libxlua.so , libil2cpp.so

直接用 frida 为了方便使用 frida-il2cpp 我们创建一个 node 项目

添加库 并配置 ts 环境

添加命令

( frida js 运行在手机上 运行麻烦 使用 ts 可以避免语法错误 并享受 js 生态 )

在 index.ts 中开始 hook

我们先使用Il2Cpp.perform(()=>{console.log("OK")}) 确认il2cpp 能够被正常 hook

然后我们就可以使用 il2cpp 获取由元数据的来的c#代码函数签名信息

这一步其实和文章主题关系不大 这里的手游 c# 层也没有特别的内容

说明在 c#层没有修改内容

接着我们看向 luaEnv 类 这里就有由 lua 框架映射而来的多数 lua基础 api

( 在 so 库中也能看到接口 )

这里我们直接尝试使用 DoString 方法来执行我们提供的 lua 代码

faq 我怎么知道是 main.lua 你可以先打印他们的名字啊

faq 为什么要在执行 main.lua 之后执行 因为这样才能获取到他代码注册的内容

题外话 记得去把 log hook 了 才能看到输出

frida-il2cpp 提供了 log Il2Cpp.installExceptionListener("all");

lua 框架大概率也有logger 可以 hook lua框架的 logger 将输出复一份到 frida上面来

然而 很神奇的事情发生了 程序直接崩溃了

在反复排除了各种东西之后 不得不打开 ida 分析 so 库

好在lua 框架是讲他自己的代码链接到 luajit 上的 也就是说我们可以对照 luajit 的源码

题外话 win 上可以使用 msvc 编译 luajit 参考 luajit 官网教程 记得把 -O2 改成 -Od 开启 debug 模式

直接定位到核心的 lua 代码加载函数 lua_loadx -> cpparser

手游的 so 库里面的

编译的 so 库里面的

源码

题外话 手游的 so 库 开了优化 一些没有注明要内联的函数 也被内联了 看着会有些不一样

不难发现 他直接少了 t 选项 查阅 lua 官网 可知

lua加载代码分为 b (从字节码加载) t (从文本加载 )

而进一步的分析发现 这个手游直接把 t 模式整个删了(没绷住)

随后进一步的对比分析 发现不仅仅是加载字节码的模式 而是真个字节码都被加密了 下面的章节会详细介绍

在网上搜索时 发现了另一种思路

即通过 lua 暴露的 c api 来控制 lua

这样也可变相的实现控制逻辑 而且由于 这些暴露的接口对于框架交互来说是必须的 也不用太担心这里会被做手脚 但是这个方案只能进行简单的更改 对于外挂之类的来说可能比较有用

不过这里也提供另类的思路

由于 lua 的特殊性 lua 运行时本身是无状态的 理论上我们可以将 lua_state 直接交给另一个 lua 虚拟机来执行

不过这个方案并不能运用在这里 这里由于是游戏 有大量的网络请求 涉及到协程 lua 会将 协程信息放在 lua 共有的 global 段 中 这样的话 就不是无状态了

另一种思路是 修改一个 lua 虚拟机 将其最终执行的命令记录并转发给我们这里的 lua 虚拟机 得益于 lua 本身的简单 这并非不可能 像 fengari 库 直接在 原始 js 中实现了 lua vm , 如果对他进行一下修改后集成在 frida 中 也许可以实现

最后 我们还是老老实实的分析他加密后的 bytecode 不过在分析加密的之前 我们得先搞清楚原始的

这里感谢 feicong 大佬的文章 https://github.com/feicong/lua_re

有关原始 luajit 的字节码格式与分析请参考他的文章

https://github.com/feicong/lua_re/blob/master/lua/lua_re3.md

https://github.com/feicong/lua_re/blob/master/lua/lua_re4.md

题外话 大佬提供的 010 的 bt 模板 在我这里似乎有版本兼容问题

没有函数 parentof() 即获取节点的父节点

不过这个可以直接在父节点处

把父节点自己作为参数传给子节点 来绕过这个函数

为了更好的分析游戏的 lua bytecode 这里我们需要找一个游戏中有的(加密过后的文件) 同时我们也有源码的 lua 文件(加密前的文件)

这样的文件我们可以去找框架的 lua 代码 让后使用 frida hook loadbuffer 函数 并判断名称 然后 dump 下来

顺带 我们打开一个 python 并编写

这里我们进行超级多开

我们可知原始 luajit 字节码 的结构

GlobalHeader 头部

多个 Proto 函数体

header 头部

insts 指令

constants 常量

upvalue

complex

CHILD = 0

TAB = 1

I64 = 2

U64 = 3

COMPLEX = 4

STR = 5 大于 5 的都是字符串 字符串长度为 值-5

numberic

最后以一个 size 为 0 的 proto 结束

而 luajit 解析这是

其中 bc 开头的函数都是读取对应部分的函数 重点在于lj_bcread_proto 这个函数

包含了分析 proto 这个重要结构的代码

由于编译器将很多子函数的代码内联了进来 导致这个函数很大 不过不要怕 我们有原程序进行对比 这里就不完整将函数代码贴上来了

第一部分 读取 proto 头部

不难发现 他进行了异或 将函数头的参数互相异或了 并修改了部分参数的位置

这里我们直接让 gpt 给出逆函数

当然,让我总结一下我们找到 (fl) 函数反函数的过程:

问题描述: 您提供了一个名为 (fl) 的函数,它对一个四元素的元组 (t)(具体为 (0, 1, 2, 3))进行一系列异或(XOR)操作,生成一个新的四元素元组。您询问如何找到 (fl) 函数的反函数,即如何从 (fl) 函数的输出恢复出原始输入。

(fl) 函数的分析: (fl) 函数通过以下方式操作:

逆函数的构建: 我们尝试了几种不同的方法来构建这个逆函数,关键在于理解 XOR 操作的自反性和如何正确地逆向每一步操作。

最终解决方案: 经过一系列尝试和错误,我们找到了正确的逆函数。这个逆函数通过以下方式恢复原始 (t) 值:

验证和结果: 逆函数成功地验证了,它能够准确地从 (fl) 函数的输出恢复出原始输入 (0, 1, 2, 3)。

这样 我们可以先编写proto 的 python

而对于指令

结合 010 我们可以发现 对于指令的 4 个数 op , a1 ,a2, a3

对于指令 好在他虽然打乱顺序了 但是没有完全打乱

他只是按照 lj_bc.h 中指令的大块打乱了 相邻的指令依然是连续的

结合之后获取的更多的 lua 样本和其他模板的解密 指令基本能够恢复

(就算不能完全恢复 常用指令也能够恢复 对于达成目标并不影响)

接下来对于字符串 我们能在 ida 中看到一串很恐怖的大量代码

这一大串看着多 其实很简单 大部分内容都是由于编译器为了加快异或而生成的代码 下面xmmword这些其实是 SIMD 指令集

整段代码其实就是

将字符串按位求反异或 而这个操作的逆函数就是他自身

还有一些其他大大小小的更改 如更换位置 等

这里就不贴上来了

最后 写一个自动编译生成的工作流 结合之前的 ts 代码

在 frida 中 我们直接 hook dll 的对应函数 使用 frida 创建调用

这里核心就 lua_loadbuffer 一个函数 其他都是为了不让我们插入的代码破坏 lua 的原始堆栈引发程序崩溃的保护措施

在成功植入 lua 代码之后 参考 unlua 写了反编译 我们就可以直接使用 lua 代码来 hook 并插入内容了

文章没有写的很详细 考虑到文章核心是介绍 lua bc 其他部分就都简化了

有什么问题欢迎在文章下提问

@types/node
@types/frida-gum
frida-compile
frida-java-bridge
frida-il2cpp-bridge
@types/node
@types/frida-gum
frida-compile
frida-java-bridge
frida-il2cpp-bridge
*(a1[10] + 196LL) = -1;
  v5 = (loc_43480)();
  if ( !*(a3 + 136) )
  {
    if ( v5 )
      goto LABEL_4;
    return 0LL;
  }
  if ( v5 )
  {
    if ( strchr(*(a3 + 136), 'b') )
    {
LABEL_4:
      v6 = sub_45740(a3);
      v7 = sub_351F0(a1, v6, a1[9]);
      v8 = a1[5];
      a1[5] = v8 + 1;
      *v8 = v7 | 0xFFFB800000000000LL;
      return 0LL;
    }
    goto LABEL_8;
  }
  if ( strchr(*(a3 + 136), 't') )
    return 0LL;
LABEL_8:
  v10 = a1[5];
  a1[5] = v10 + 1;
  *v10 = sub_3142C(a1, 2100LL) | 0xFFFD800000000000LL;
  v11 = sub_3123C(a1, 3LL);
  return lua_loadx(v11, v12, v13, v14, v15);
}
*(a1[10] + 196LL) = -1;
  v5 = (loc_43480)();
  if ( !*(a3 + 136) )
  {
    if ( v5 )
      goto LABEL_4;
    return 0LL;
  }
  if ( v5 )
  {
    if ( strchr(*(a3 + 136), 'b') )
    {
LABEL_4:
      v6 = sub_45740(a3);
      v7 = sub_351F0(a1, v6, a1[9]);
      v8 = a1[5];
      a1[5] = v8 + 1;
      *v8 = v7 | 0xFFFB800000000000LL;
      return 0LL;
    }
    goto LABEL_8;
  }
  if ( strchr(*(a3 + 136), 't') )
    return 0LL;
LABEL_8:
  v10 = a1[5];
  a1[5] = v10 + 1;
  *v10 = sub_3142C(a1, 2100LL) | 0xFFFD800000000000LL;
  v11 = sub_3123C(a1, 3LL);
  return lua_loadx(v11, v12, v13, v14, v15);
}
*(a1[10] + 196LL) = -1;
  v5 = (loc_426F8)();
  if ( !*(a3 + 136) )
  {
    if ( v5 )
      goto LABEL_4;
    goto LABEL_6;
  }
  if ( !v5 )
  {
    if ( !strchr(*(a3 + 136), 't') )
      goto LABEL_9;
LABEL_6:
    v6 = sub_49D8C(a3);
    goto LABEL_7;
  }
  if ( strchr(*(a3 + 136), 'b') )
  {
LABEL_4:
    v6 = (loc_4A8A8)(a3);
LABEL_7:
    v7 = sub_349F0(a1, v6, a1[9]);
    v8 = a1[5];
    a1[5] = v8 + 1;
    *v8 = v7 | 0xFFFB800000000000LL;
    return 0LL;
  }
LABEL_9:
  v10 = a1[5];
  a1[5] = v10 + 1;
  *v10 = sub_30C08(a1, 2100LL) | 0xFFFD800000000000LL;
  sub_30A10(a1, 3LL);
  v12 = v11;
  v15 = v13;
  if ( !feof(*v13) && (v14 = fread(v15 + 1, 1uLL, 0x400uLL, *v15), (*v12 = v14) != 0) )
    result = v15 + 1;
  else
    result = 0LL;
  return result;
}
*(a1[10] + 196LL) = -1;
  v5 = (loc_426F8)();
  if ( !*(a3 + 136) )
  {
    if ( v5 )
      goto LABEL_4;
    goto LABEL_6;
  }
  if ( !v5 )
  {
    if ( !strchr(*(a3 + 136), 't') )
      goto LABEL_9;
LABEL_6:
    v6 = sub_49D8C(a3);
    goto LABEL_7;
  }
  if ( strchr(*(a3 + 136), 'b') )
  {
LABEL_4:
    v6 = (loc_4A8A8)(a3);
LABEL_7:
    v7 = sub_349F0(a1, v6, a1[9]);
    v8 = a1[5];
    a1[5] = v8 + 1;
    *v8 = v7 | 0xFFFB800000000000LL;
    return 0LL;
  }
LABEL_9:
  v10 = a1[5];
  a1[5] = v10 + 1;
  *v10 = sub_30C08(a1, 2100LL) | 0xFFFD800000000000LL;
  sub_30A10(a1, 3LL);
  v12 = v11;
  v15 = v13;
  if ( !feof(*v13) && (v14 = fread(v15 + 1, 1uLL, 0x400uLL, *v15), (*v12 = v14) != 0) )
    result = v15 + 1;
  else
    result = 0LL;
  return result;
}
// lj_load.c
LUA_API int lua_loadx(lua_State *L, lua_Reader reader, void *data,
              const char *chunkname, const char *mode)
{
  LexState ls;
  int status;
  ls.rfunc = reader;
  ls.rdata = data;
  ls.chunkarg = chunkname ? chunkname : "?";
  ls.mode = mode;
  lj_buf_init(L, &ls.sb);
  status = lj_vm_cpcall(L, NULL, &ls, cpparser);
  lj_lex_cleanup(L, &ls);
  lj_gc_check(L);
  return status;
}
 
static TValue *cpparser(lua_State *L, lua_CFunction dummy, void *ud)
{
  LexState *ls = (LexState *)ud;
  GCproto *pt;
  GCfunc *fn;
  int bc;
  UNUSED(dummy);
  cframe_errfunc(L->cframe) = -1;  /* Inherit error function. */
  bc = lj_lex_setup(L, ls);
  if (ls->mode && !strchr(ls->mode, bc ? 'b' : 't')) {
    setstrV(L, L->top++, lj_err_str(L, LJ_ERR_XMODE));
    lj_err_throw(L, LUA_ERRSYNTAX);
  }
  pt = bc ? lj_bcread(ls) : lj_parse(ls);
  fn = lj_func_newL_empty(L, pt, tabref(L->env));
  /* Don't combine above/below into one statement. */
  setfuncV(L, L->top++, fn);
  return NULL;
}
// lj_load.c
LUA_API int lua_loadx(lua_State *L, lua_Reader reader, void *data,
              const char *chunkname, const char *mode)
{
  LexState ls;
  int status;
  ls.rfunc = reader;
  ls.rdata = data;
  ls.chunkarg = chunkname ? chunkname : "?";
  ls.mode = mode;
  lj_buf_init(L, &ls.sb);
  status = lj_vm_cpcall(L, NULL, &ls, cpparser);
  lj_lex_cleanup(L, &ls);
  lj_gc_check(L);
  return status;
}
 
static TValue *cpparser(lua_State *L, lua_CFunction dummy, void *ud)
{
  LexState *ls = (LexState *)ud;
  GCproto *pt;
  GCfunc *fn;
  int bc;
  UNUSED(dummy);
  cframe_errfunc(L->cframe) = -1;  /* Inherit error function. */
  bc = lj_lex_setup(L, ls);
  if (ls->mode && !strchr(ls->mode, bc ? 'b' : 't')) {
    setstrV(L, L->top++, lj_err_str(L, LJ_ERR_XMODE));
    lj_err_throw(L, LUA_ERRSYNTAX);
  }
  pt = bc ? lj_bcread(ls) : lj_parse(ls);
  fn = lj_func_newL_empty(L, pt, tabref(L->env));
  /* Don't combine above/below into one statement. */
  setfuncV(L, L->top++, fn);
  return NULL;
}
lua_gettop
lua_pop
lua_pushvalue
lua_pcall
lua_pushstring
lua_gettop
lua_pop
lua_pushvalue
lua_pcall
lua_pushstring
GCproto *lj_bcread(LexState *ls)
{
  lua_State *L = ls->L;
  lj_assertLS(ls->c == BCDUMP_HEAD1, "bad bytecode header");
  bcread_savetop(L, ls, L->top);
  lj_buf_reset(&ls->sb);
  /* Check for a valid bytecode dump header. */
  if (!bcread_header(ls))
    bcread_error(ls, LJ_ERR_BCFMT);
  for (;;) {  /* Process all prototypes in the bytecode dump. */
    GCproto *pt;
    MSize len;
    const char *startp;
    /* Read length. */
    if (ls->p < ls->pe && ls->p[0] == 0) {  /* Shortcut EOF. */
      ls->p++;
      break;
    }
    bcread_want(ls, 5);
    len = bcread_uleb128(ls);
    if (!len) break/* EOF */
    bcread_need(ls, len);
    startp = ls->p;
    pt = lj_bcread_proto(ls);
    if (ls->p != startp + len)
      bcread_error(ls, LJ_ERR_BCBAD);
    setprotoV(L, L->top, pt);
    incr_top(L);
  }
  if ((ls->pe != ls->p && !ls->endmark) || L->top-1 != bcread_oldtop(L, ls))
    bcread_error(ls, LJ_ERR_BCBAD);
  /* Pop off last prototype. */
  L->top--;
  return protoV(L->top);
}
GCproto *lj_bcread(LexState *ls)
{
  lua_State *L = ls->L;
  lj_assertLS(ls->c == BCDUMP_HEAD1, "bad bytecode header");
  bcread_savetop(L, ls, L->top);
  lj_buf_reset(&ls->sb);
  /* Check for a valid bytecode dump header. */
  if (!bcread_header(ls))
    bcread_error(ls, LJ_ERR_BCFMT);
  for (;;) {  /* Process all prototypes in the bytecode dump. */
    GCproto *pt;
    MSize len;
    const char *startp;
    /* Read length. */
    if (ls->p < ls->pe && ls->p[0] == 0) {  /* Shortcut EOF. */
      ls->p++;
      break;
    }
    bcread_want(ls, 5);
    len = bcread_uleb128(ls);
    if (!len) break/* EOF */
    bcread_need(ls, len);
    startp = ls->p;
    pt = lj_bcread_proto(ls);
    if (ls->p != startp + len)
      bcread_error(ls, LJ_ERR_BCBAD);
    setprotoV(L, L->top, pt);
    incr_top(L);
  }
  if ((ls->pe != ls->p && !ls->endmark) || L->top-1 != bcread_oldtop(L, ls))
    bcread_error(ls, LJ_ERR_BCBAD);
  /* Pop off last prototype. */
  L->top--;
  return protoV(L->top);
}
ls_p = *(ls + 32);
 *(ls + 32) = ls_p + 1;
 ph_b1_framesize = *ls_p;
 *(ls + 32) = ls_p + 2;
 ph_b2 = ls_p[1];
 *(ls + 32) = ls_p + 3;
 flags = ph_b2 ^ ph_b1_framesize;
 ph_b3 = ls_p[2];
 *(ls + 32) = ls_p + 4;
 v6 = (ls + 32);
 numparams = ph_b3 ^ ph_b2 ^ ph_b1_framesize;
 ph_b4 = ls_p[3];
 sizekn = bcread_uleb128((ls + 32));
 sizeuv = ph_b4 ^ numparams;
 sizekgc = bcread_uleb128(v6);
 sizebc_1 = bcread_uleb128(v6);
 sizebc = sizebc_1 + 1;
ls_p = *(ls + 32);
 *(ls + 32) = ls_p + 1;
 ph_b1_framesize = *ls_p;
 *(ls + 32) = ls_p + 2;
 ph_b2 = ls_p[1];

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 12
支持
分享
最新回复 (9)
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享,期待楼主新的作品
2024-1-15 21:08
0
雪    币: 2428
活跃值: (10698)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
看不懂,只能先点赞收藏了
2024-1-16 09:27
0
雪    币: 3535
活跃值: (31011)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2024-1-16 14:31
1
雪    币: 3712
活跃值: (1441)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
5
感谢分享,方便告知是哪个游戏嘛
2024-1-16 16:47
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
虽然看不太懂 但我大受震撼
2024-1-18 15:54
0
雪    币: 14
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
似乎是碧蓝航线?
2024-1-18 17:12
1
雪    币: 1922
活跃值: (4165)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
功力不一般,厉害
2024-2-25 14:37
0
雪    币: 175
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
大佬大佬,我最近也在研究一个类似的游戏,但是卡在了获取lua的位置,能请教请教吗
2024-6-29 21:50
0
雪    币: 175
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10

大佬,我搞的那个游戏dump下来的lua相比正常luajit编译的文件都带了一个头,这个咋处理

最后于 2024-7-1 11:07 被小绿鸽编辑 ,原因:
2024-7-1 11:04
0
游客
登录 | 注册 方可回帖
返回
//