0
点赞
收藏
分享

微信扫一扫

【Docker】Docker容器实战部署多个Nginx实现负载均衡和高可用

颜路在路上 2024-01-19 阅读 7

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

前言

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

情况研究

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

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

unity 的 lua 通信方案

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

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

 hybridclr

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

luac 与 luajit 的关系

以下来自 gpt

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`))
}
}

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

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

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

直接定位到核心的 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;

}

不难发现 他直接少了 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 不过在分析加密的之前 我们得先搞清楚原始的

自定义 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 给出逆函数

这样 我们可以先编写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 其他部分就都简化了

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

举报

相关推荐

0 条评论