对一手游的自定义 luajit 字节码的研究

这篇具有很好参考价值的文章主要介绍了对一手游的自定义 luajit 字节码的研究。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

对一手游的自定义 luajit 字节码的研究

前言

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

情况研究

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

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

unity 的 lua 通信方案

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

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

 hybridclr

    • c# 原生热更新
  • xlua
    • 代表新兴 lua 框架
    • 有详细的文档 !
  • tolua
    • 代表老一代 lua 框架

luac 与 luajit 的关系

以下来自 gpt

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

  1. LuaC:

    • LuaC 是 Lua 的标准字节码编译器。它是 Lua 解释器的一部分,用于将 Lua 源代码编译成字节码。这个过程通常是为了提高代码的加载速度和执行效率。
    • LuaC 生成的字节码可以在任何 Lua 虚拟机上运行,确保了跨平台的兼容性。
    • 它是 Lua 发行版的标准组成部分,通常用于开发环境或当脚本需要预编译以隐藏源代码时。
  2. LuaJIT:

    • LuaJIT 是一个独立的 Lua 编译器,它使用即时编译(JIT)技术来提高 Lua 代码的执行速度。LuaJIT 能够在运行时将 Lua 字节码转换成机器码,这通常可以显著提高性能。
    • LuaJIT 与 Lua 5.1 版本高度兼容,但也引入了一些自己的扩展和优化。
    • 它特别适合于性能敏感的应用,如高性能服务器、游戏、或实时系统。
    • LuaJIT 的性能通常远超标准 Lua 解释器,尤其是在复杂或计算密集型任务中。

总的来说,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 分析

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

frida / frida-il2cpp

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

添加库 并配置 ts 环境

1

2

3

4

5

@types/node

@types/frida-gum

frida-compile

frida-java-bridge

frida-il2cpp-bridge

添加命令

frida-compile src/index.ts -o dist/_agent.js -c
frida -Uf xxx -l dist/_agent.js

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

在 index.ts 中开始 hook

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

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

            const destination = 		`${Il2Cpp.applicationDataPath}/${dirName}`;

        for (const assembly of Il2Cpp.Domain.assemblies) {
            const path = `${destination}/${assembly.name}.cs`
            const file = new File(path, "w");
            for (const klass of assembly.image.classes) {
                file.write(`${klass}\n\n`);
            }
            file.flush();
            file.close();
        }

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

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

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

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

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

this.AssemblyCSharp = Il2Cpp.Domain.assembly('Assembly-CSharp').image;
this.AssemblyCSharp.class("LuaEnv").method("DoString").implementation = function (bytes: Il2Cpp.Array<Il2Cpp.Object>, name: Il2Cpp.String) {
if(name !== undefined && name?.content=="@main.lua"){
 this.method("DoString").Invoke(Il2Cpp.String.from(`print("我的 lua 代码。。。")`),Il2Cpp.String.from(`@test.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 库里面的

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

*(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);

}

编译的 so 库里面的

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

*(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;

}

源码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

// 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;

}

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

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

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

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

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

已有的破解分析

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

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

1

2

3

4

5

lua_gettop

lua_pop

lua_pushvalue

lua_pcall

lua_pushstring

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

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

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

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

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

原始 luajit bytecode 分析

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

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

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

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

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

自定义 bytecode 分析 与 对应实现

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

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

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

这里我们进行超级多开

  • ida - 目标游戏的 so 库
  • ida - 自编译的 so 库
  • vs - luajit 源码
  • vsc - python 代码 (编写加解密脚本)
  • 010 - 游戏的lua字节码
  • 010 - 原始的 lua字节码

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

  • GlobalHeader 头部

  • 多个 Proto 函数体

    • header 头部

      • size
      • flags
      • arg_count
      • framesize
      • upvalue_count
      • complex_constants_count
      • numeric_constants_count
      • instructions_count
    • insts 指令

      • 4 个字节一组 详见上一张大佬的文章
    • constants 常量

      • upvalue

      • complex

        • CHILD = 0

          TAB = 1

          • tab 还会进一步细分为键和值 或仅值 (lua特色)

          I64 = 2

          U64 = 3

          COMPLEX = 4

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

      • numberic

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

而 luajit 解析这是

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

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);

}

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

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

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

第一部分 读取 proto 头部

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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;

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

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

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

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

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

    • (o1 = t[0])
    • (o2 = t[1] \oplus o1)
    • (o3 = t[2] \oplus o2)
    • (o4 = t[3] \oplus o3)
  3. 逆函数的构建: 我们尝试了几种不同的方法来构建这个逆函数,关键在于理解 XOR 操作的自反性和如何正确地逆向每一步操作。

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

    • 从 (o4) 开始,因为 (o4 = t[3] \oplus o3),所以 (t3 = o4 \oplus o3)。
    • 依此类推,我们逆向每一步操作,最终恢复出 (t0, t1, t2, t3)。
  5. 验证和结果: 逆函数成功地验证了,它能够准确地从 (fl) 函数的输出恢复出原始输入 (0, 1, 2, 3)。

1

2

3

4

5

6

7

8

9

10

11

12

13

def fl(t):

    o1 = t[0]

    o2 = t[1] ^ o1

    o3 = t[2] ^ o2

    o4 = t[3] ^ o3

    return o1 , o2 , o3 , o4

def fl_inverse(t):

    t3 = t[3] ^ t[2]

    t2 = t[2] ^ t[1]

    t1 = t[1] ^ t[0]

    t0 = t[0

    return t0, t1, t2, t3

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

1

2

3

4

5

6

7

8

9

10

11

12

= (self.framesize[0],self.flags[0],self.argcount[0],self.upvc[0])

            a1 , a2 , a3 , a4 = fl_inverse(t)

            assert t==fl((a1 , a2 , a3 , a4))

            WriterUtil.write_byte(stream,a1)

            WriterUtil.write_byte(stream,a2)

            WriterUtil.write_byte(stream,a3)

            WriterUtil.write_byte(stream,a4)

            WriterUtil.ULeb128Write(stream,self.ncc)

            WriterUtil.ULeb128Write(stream,self.ccc)

            WriterUtil.ULeb128Write(stream, self.instc)

而对于指令

1

2

3

4

5

6

7

8

9

do

    {

      v21 = *(v18++ + 3);

      v22 = v20++ ^ v21;

      v23 = *(v18 - 3);

      *(v18 - 1) = v22;

      *(v18 - 3) = ~v23;

    }

    while ( v20 != sizebc_1 );

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

  • 首先 他 opcode 都更改了 但是是一一对应的
  • 其次 根据代码 不难 发现
    • a1 为 ~a1&0xff
    • a2 不变
    • a3 为 a3^idx&0xff 其中 idx 为指令个数

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class LuaJitInstruction:

    def __init__(self,stream,idx) -None:

        self.idx = idx

        self.op = ReaderUtil.read_byte(stream)

        self.a1 = ReaderUtil.read_byte(stream)

        self.a2 = ReaderUtil.read_byte(stream)

        self.a3 = ReaderUtil.read_byte(stream)

     

    def dump(self,stream):

        if not NEED_OBUF:

            WriterUtil.write_byte(stream,self.op)

            WriterUtil.write_byte(stream,self.a1)

            WriterUtil.write_byte(stream,self.a2)

            WriterUtil.write_byte(stream,self.a3)

        else:

            WriterUtil.write_byte(stream,OP().OPtoOBOP(self.op[0]))

            WriterUtil.write_byte(stream,~self.a1[0]&0xff)

            WriterUtil.write_byte(stream,self.a2)

            WriterUtil.write_byte(stream,(self.a3[0]^self.idx)&0xff)

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

if ( _loop_next_len - 6 <= 14 )

          {

            v40 = 0;

LABEL_28:

            ls_p_1[v40 / 0x10].n128_u8[0] = ~ls_p_1[v40 / 0x10].n128_u8[0] ^ v40;

            if ( loop_len > v40 + 1 )

            {

              ls_p_1[v40 / 0x10].n128_u8[1] = ~ls_p_1[v40 / 0x10].n128_u8[1] ^ (v40 + 1);

              if ( loop_len > v40 + 2 )

              {

                ls_p_1[v40 / 0x10].n128_u8[2] = ~ls_p_1[v40 / 0x10].n128_u8[2] ^ (v40 + 2);

                if ( v40 + 3 < loop_len )

                {

                  ls_p_1[v40 / 0x10].n128_u8[3] = ~ls_p_1[v40 / 0x10].n128_u8[3] ^ (v40 + 3);

                  if ( v40 + 4 < loop_len )

                  {

                    ls_p_1[v40 / 0x10].n128_u8[4] = ~ls_p_1[v40 / 0x10].n128_u8[4] ^ (v40 + 4);

                    if ( loop_len > v40 + 5 )

                    {

                      ls_p_1[v40 / 0x10].n128_u8[5] = ~ls_p_1[v40 / 0x10].n128_u8[5] ^ (v40 + 5);

                      if ( loop_len > v40 + 6 )

                      {

                        ls_p_1[v40 / 0x10].n128_u8[6] = ~ls_p_1[v40 / 0x10].n128_u8[6] ^ (v40 + 6);

                        if ( loop_len > v40 + 7 )

                        {

                          ls_p_1[v40 / 0x10].n128_u8[7] = ~ls_p_1[v40 / 0x10].n128_u8[7] ^ (v40 + 7);

                          if ( loop_len > v40 + 8 )

                          {

                            ls_p_1[v40 / 0x10].n128_u8[8] = ~ls_p_1[v40 / 0x10].n128_u8[8] ^ (v40 + 8);

                            if ( loop_len > v40 + 9 )

                            {

                              ls_p_1[v40 / 0x10].n128_u8[9] = ~ls_p_1[v40 / 0x10].n128_u8[9] ^ (v40 + 9);

                              if ( loop_len > v40 + 10 )

                              {

                                ls_p_1[v40 / 0x10].n128_u8[10] = ~ls_p_1[v40 / 0x10].n128_u8[10] ^ (v40 + 10);

                                if ( loop_len > v40 + 11 )

                                {

                                  ls_p_1[v40 / 0x10].n128_u8[11] = ~ls_p_1[v40 / 0x10].n128_u8[11] ^ (v40 + 11);

                                  if ( loop_len > v40 + 12 )

                                  {

                                    v50 = v40 + 13;

                                    ls_p_1[v40 / 0x10].n128_u8[12] = ~ls_p_1[v40 / 0x10].n128_u8[12] ^ (v40 + 12);

                                    if ( loop_len > v40 + 13 )

                                    {

                                      v51 = v40 + 14;

                                      ls_p_1->n128_u8[v50] = ~ls_p_1->n128_u8[v50] ^ v50;

                                      if ( loop_len > v51 )

                                        ls_p_1->n128_u8[v51] = ~ls_p_1->n128_u8[v51] ^ v51;

                                    }

                                  }

                                }

                              }

                            }

                          }

                        }

                      }

                    }

                  }

                }

              }

            }

          }

          else

          {

            v41 = ls_p_1;

            v42 = 0;

            v43 = xmmword_45730;

            do

            {

              v44.n128_u64[0] = 0x400000004LL;

              v44.n128_u64[1] = 0x400000004LL;

              v45.n128_u64[0] = 0xC0000000CLL;

              v45.n128_u64[1] = 0xC0000000CLL;

              ++v42;

              v46 = vaddq_s32(v43, v44);

              v44.n128_u64[0] = 0x800000008LL;

              v44.n128_u64[1] = 0x800000008LL;

              v47 = vaddq_s32(v43, v45);

              v48 = vmovn_hight_s32(vmovn_s32(v43), v46);

              v45.n128_u64[0] = 0x1000000010LL;

              v45.n128_u64[1] = 0x1000000010LL;

              v49 = vaddq_s32(v43, v44);

              v43 = vaddq_s32(v43, v45);

              *v41 = veorq_s8(vmovn_hight_s16(vmovn_s16(v48), vmovn_hight_s32(vmovn_s32(v49), v47)), vmvnq_s8(*v41));

              ++v41;

            }

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

整段代码其实就是

1

2

def stringOBUF(s: bytes) -> bytes:

    return bytes([( (~e & 0xff) ^ i) for i, e in enumerate(s)])

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

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

这里就不贴上来了

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

LUAJIT_PATH = os.path.join("LuaJIT""luajit64.exe")

LUA_PATH = os.path.join("HookScript""LuaCode""main.lua")

LUA_BC_PATH = os.path.join("HookScript""dist""_agent.lua.bc")

OUTDATA_PATH = os.path.join("HookScript""src""hooks""luadata.ts")

subprocess.run([LUAJIT_PATH, "-b",os.path.join("..",LUA_PATH),os.path.join("..",LUA_BC_PATH)], cwd='LuaJIT'# f"{LUAJIT_PATH} -b {LUA_PATH} {LUA_BC_PATH}"

with open(LUA_BC_PATH,'rb') as f:

    data = LuaJitBC(f.read()).dump()

luats = ["""export const luadata = [\n"""]

luats.append(",".join([hex(i) for in data])+"\n")

luats.append("]\n")

with open(OUTDATA_PATH,'w') as f:

    f.writelines(luats)

subprocess.run(["pnpm","i"],cwd="./HookScript",shell=True)

subprocess.run(["pnpm","run","phone"],cwd="./HookScript",shell=True)

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

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

public LuaLoad(data:Array<number>,chunkName:string){
        
        this.lua_pushtraceback(this.lua_state)
        
        const oldTop = this.lua_gettop(this.lua_state)

        const prtbuffer = Memory.alloc(data.length+1)
        prtbuffer.writeByteArray(data)
        const ckn = Memory.allocUtf8String(chunkName)
        console.log(`allocing ${data.length} mem`)
        
        console.log(`[lua] LuaLoad execing as oldTop:${oldTop} and buffer at ${prtbuffer.toString(16)}`)
        
        if(this.lua_loadbuffer(this.lua_state,prtbuffer,data.length,ckn)==0){
            if(this.lua_pcall(this.lua_state,0,-1,0)==0){
                this.lua_settop(this.lua_state,oldTop-1);
                console.log("[lua] LuaLoad exec finished!")
                return;
            }
        }

        const errlen = new NativePointer(0)
        const errptr = this.lua_tolstring(this.lua_state,-1,errlen)
        let errmsg:string
        try{
            if(errlen.isNull()){
                errmsg = errptr.readUtf8String() || ""
            }else{
                errmsg = errptr.readUtf8String(errlen.toInt32()) || ""
            }
            console.error(errmsg)
            console.error("[lua] failed to exec")
        }catch(e){
            console.error(e)
            console.log("[lua] failed to load errmsg")
        }
        this.lua_settop(this.lua_state,oldTop-1);
        return
    }

尾声

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

local org_func = target_class.target_func
target_class.target_func = function (self, args)
  -- doxxx before
    org_func(self,args)
  -- doxxx after
end

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

有什么问题欢迎在文章下提问文章来源地址https://www.toymoban.com/news/detail-814481.html

到了这里,关于对一手游的自定义 luajit 字节码的研究的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 从零开发基于ASM字节码的Java代码混淆插件XHood

    因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施 例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低 经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴

    2024年02月06日
    浏览(34)
  • 入门篇-其之六-附录一-以Java字节码的角度分析i++和++i

    前言:众所周知, i++ 和 ++i 的区别是: i++ 先将 i 的值赋值给变量,再将 i 的值自增1;而 ++i 则是先将 i 的值自增1,再将结果赋值给变量。因此,二者最终都给 i 自增了1,只是方式不同而已。 当然,如果在面试过程中面试官问你这个问题,只回答出上述内容,只能说明你对

    2024年02月08日
    浏览(38)
  • 深入理解JVM虚拟机第二篇:虚拟机概念和JVM整体架构以及字节码的执行路线

      😉😉 学习交流群: ✅✅1:这是孙哥suns给大家的福利! ✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 🥭🥭3:QQ群:583783824   📚📚  工作微信:BigTreeJava 拉你进微信群,免费领取! 🍎🍎4:本文章内容出自上述:Spring应用课程!💞💞

    2024年02月09日
    浏览(56)
  • MyBatis的自定义插件

    MyBatis 可以拦截的四大组件 Executor - 执行器 StatementHandler - SQL 语句构造器 ParameterHandler - 参数处理器 ResultSetHandler - 结果集处理器 效果如下 创建四大对象的代码如下 首先在创建 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四个对象时,将插件(plugins)注入 调用 Intercep

    2024年02月07日
    浏览(38)
  • Java的自定义注解

            自定义注解包括注解声明、元注解、运行时处理器三个部分。注解声明指定了注解的名称、作用域、成员等信息;元注解则用来对注解进行修饰;运行时处理器则负责在程序运行过程中处理注解,并根据注解提供的信息执行相应的逻辑。自定义注解在编写框架、插

    2024年02月03日
    浏览(38)
  • gogs的自定义配置

    在 GOGS 下载并安装后,在程序目录下建立一个 custom/conf/app.ini 的配置文件,内容如下: nginx的转发配置

    2024年02月17日
    浏览(44)
  • 自定义loadbalance实现feignclient的自定义路由

    服务A有多个同事同时开发,每个同事都在dev或者test环境发布自己的代码,注册到注册中心有好几个(本文nacos为例),这时候调用feign可能会导致请求到不同分支的服务上面,会出现一些问题,本文重点在于解决该问题 解决方案 调用流程 [外链图片转存失败,源站可能有防盗链机

    2024年02月11日
    浏览(40)
  • hive的自定义函数以及自定义加密函数

    hive对于敏感数据的加密还不够完善,现在开发一个udf函数,自己设置密钥(hive的加密函数等级比较低,也没有集成自己加密的密钥函数,所以自己开发一个),如果要加密一些数据则可以自己使用特定的密钥进行加密解密,这样很好的方便数据的加密下面将实现过程如下:

    2024年02月08日
    浏览(37)
  • Avalonia的自定义用户组件

    Avalonia中的自定义用户控件 Avalonia是一个跨平台的.NET UI框架,它允许开发者使用C#和XAML来构建丰富的桌面应用程序。 自定义用户控件(UserControl)是Avalonia中一种重要的组件,它允许我们将多个控件组合成一个可重用的单元。 本文将介绍如何在Avalonia中定义和使用自定义用户

    2024年04月08日
    浏览(42)
  • Spring中的自定义注解

    在Spring中,注解是一种非常使用的工具。 因其强大的功能,极大的提高了我们开发效率。 但是当遇到一些特殊业务时,框架自有的注解已经不能满足我们的需求了,这时我们就可以添加自定义注解来满足我们的业务需求。 我们用 @interface 来声明这是一个注解类。 另外需要在

    2024年02月11日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包