当前位置: 首页 > news >正文

对一手游的自定义 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:stringtry{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 beforeorg_func(self,args)-- doxxx after
end

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

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

相关文章:

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

对一手游的自定义 luajit 字节码的研究 前言 最近闲下来之后无聊研究起了一个unity手游 大量使用了 lua &#xff08;或者说就是 lua 写的 &#xff09; 看到网上已有的一些针对方案 都觉得太不方便 于是深入研究了一下 他自定义的 luajit 情况研究 首先 这是一个 unity的 传…...

1125. 牛的旅行 (Floyd算法,最短路)

1125. 牛的旅行 - AcWing题库 农民John的农场里有很多牧区&#xff0c;有的路径连接一些特定的牧区。 一片所有连通的牧区称为一个牧场。 但是就目前而言&#xff0c;你能看到至少有两个牧区不连通。 现在&#xff0c;John想在农场里添加一条路径&#xff08;注意&#xff…...

oracle “Interested Transaction List”(ITL)的概念

“Interested Transaction List”&#xff08;ITL&#xff09;的概念。让我们逐点理解&#xff1a; 块头和ITL&#xff1a; 每个数据库段块的块头都包含一个Interested Transaction List&#xff08;ITL&#xff09;。ITL用于确定数据库开始修改块时某个事务是否未提交。 ITL的…...

kali下-MSF-ftp_login模块破解FTP账号及密码

一、环境准备 两台设备在同一个网络内 一台kali系统&#xff1a;192.168.10.128 一台winserver2016&#xff1a;192.168.10.132 二、MSF介绍 metasploit 全称是The Metasploit Framework&#xff0c;又称MSF&#xff0c;是Kali 内置的一款渗透测试框架&#xff0c;也是全球…...

ELK之Filebeat输出日志格式设置及输出字段过滤和修改

一、Filebeat输出日志格式设置 1.1 编辑vim filebeat.yml文件,修改输出格式设置 # output to console output.console:codec.format: string: %{[@timestamp]} %{[message]}pretty: true### 1.2 测试 执行 ./filebeat -e 可以看到/tmp/access.log(目前文件里只有140.77.188…...

【开源】基于JAVA的河南软件客服系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统管理人员2.2 业务操作人员 三、系统展示四、核心代码4.1 查询客户4.2 新增客户跟进情况4.3 查询客户历史4.4 新增服务派单4.5 新增客户服务费 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的河…...

基于SpringBoot的社区帮扶对象管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…...

uniapp踩坑之项目:canvas第一次保存是空白图片

在ctx.draw()回调生成图片&#xff0c;参考canvasToTempFilePath接口文档 // data imgFilePath: null,// 缓存二维码图片canvas路径//js // 首先在draw&#xff08;&#xff09;里进行本地存储 ...... ctx.draw(false, () >{uni.canvasToTempFilePath({ // 把画布转化成临时…...

es-删除字段-实测

es字段一旦创建是无法删除的&#xff0c;本案的方案是复制新老索引的方法 一、背景 现有索引 index1,待删除字段field1和extendMap.field2&#xff0c;es版本6.3.2 二、步骤 1、删除index1中的filed1和extendMap.field2两个字段的数据 POST index1/_update_by_query {&qu…...

24秋招,百度测试开发工程师三面

前言 大家好&#xff0c;我是chowley&#xff0c;今天来回顾一下&#xff0c;我当时参加百度秋招补录&#xff0c;测试开发工程师的第三面-leader面 到面试开始的时间&#xff0c;面试官打电话表示让我等十分钟&#xff0c;随后跳过自我介绍&#xff0c;直接开面 时间&#…...

YOLOv8改进 | 主干篇 | 低照度增强网络PE-YOLO改进主干(改进暗光条件下的物体检测模型)

一、本文介绍 本文给大家带来的改进机制是低照度图像增强网络PE-YOLO中的PENet,PENet通过拉普拉斯金字塔将图像分解成多个分辨率的组件,增强图像细节和低频信息。它包括一个细节处理模块(DPM),用于通过上下文分支和边缘分支增强图像细节,以及一个低频增强滤波器(LEF),…...

【VUE】记录一次 VUE中配置生产环境和开发环境方法

前言 我这里 使用Vue CLI&#xff08;Vue Command Line Interface&#xff09;创建Vue.js项目是一种简单的方式&#xff0c;它提供了一个交互式的命令行工具来帮助你初始化和管理Vue.js项目。 并且我这个项目需要区分生产环境和开发环境。这里具体完整记录下&#xff0c;整个…...

tessreact训练字库

tessreact主要用于字符识别&#xff0c;除了使用软件自带的中英文识别库&#xff0c;还可以使用Tesseract OCR训练属于自己的字库。 一、软件环境搭建 使用Tesseract OCR训练自己的字库&#xff0c;需要安装Tesseract OCR和jTessBoxEditor(配套训练工具)。jTessBoxEditor需要…...

超时配置(OpenFeign)

默认值 连接超时&#xff0c;默认2s读取超时&#xff0c;默认5s单位&#xff1a; 毫秒 全局配置 package com.learning.springcloud.order.feign.config;import feign.Contract; import feign.Logger; import feign.Request; import org.springframework.context.annotation…...

C++设计模式(李建忠)笔记2

C设计模式&#xff08;李建忠&#xff09; 本文是学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。 参考链接 Youtube: C设计模式 Gtihub源码与PPT&#xff1a;https://github.com/ZachL1/Bilibili-plus 豆瓣: 设计模式–可复用面向对象软件的基础 文章目录 C设计模…...

【机器学习】DBSCAN算法

参考链接&#xff1a; https://blog.csdn.net/haveanybody/article/details/113092851 https://www.jianshu.com/p/dd6ce77bfb8a 1 介绍 DBSCAN(Density-Based Spatial Clustering of Applica tion with Noise)算法是于1996年提出的一种简单的、有效的基于密度的聚类算法&…...

Uniapp软件库源码-全新带勋章等

测试环境&#xff1a;php7.1。ng1.2&#xff0c;MySQL 5.6 常见问题&#xff1a; 配置好登录后转圈圈&#xff0c;检查环境及伪静态以及后台创建好应用 上传图片不了&#xff0c;检查php拓展fileinfo 以及public文件权限 App个人主页随机背景图&#xff0c;在前端uitl文件夹里面…...

Microsoft Excel 直方图

Microsoft Excel 直方图 1. 数据示例2. 打开 EXCEL3. settings4. 单击直方图柱&#xff0c;右键“添加数据标签”References 1. 数据示例 2. 打开 EXCEL 数据 -> 数据分析 -> 直方图 3. settings 输入区域样本值、接受区域分类间距&#xff0c;输出选项选择“新工作表组…...

如何录制屏幕视频?让视频制作更简单!

随着数字化时代的来临&#xff0c;录制屏幕视频成为一种常见的传播和教学方式。无论是制作演示文稿、教学视频&#xff0c;还是记录游戏操作&#xff0c;屏幕录制为用户提供了强大而灵活的工具。可是您知道如何录制屏幕视频吗&#xff1f;本文将深入介绍两种常见的屏幕录制方法…...

【JavaEE进阶】 关于应用分层

文章目录 &#x1f38b;序言&#x1f343;什么是应⽤分层&#x1f38d;为什么需要应⽤分层&#x1f340;如何分层(三层架构)&#x1f384;MVC和三层架构的区别和联系&#x1f333;什么是高内聚低耦合⭕总结 &#x1f38b;序言 在我们进行项目开发时我们如果一股脑将所有代码都…...

【已解决】c语言const/指针学习笔记

本博文源于笔者正在复习const在左与在右&#xff0c;指针优先级、a,&a,*a的区别。 1、const在左与在右 int const *p const int *p int * const p int const * const p const int * const p* 在const右边&#xff0c;指向的数据不可以改变&#xff0c;可以改变地址 * 在c…...

Android 系统启动过程纪要(基于Android 10)

前言 看过源码的都知道&#xff0c;Launcher系统启动都会经过这三个进程 init ->zygote -> system_server。今天我们就来讲解一下这三个进程以及Launcher系统启动。 init进程 准备Android虚拟机环境&#xff1a;创建和挂载系统文件目录&#xff1b;初始化属性服务&…...

【Docker实用篇】一文入门Docker(4)Docker-Compose

目录 1.Docker-Compose 1.1.初识DockerCompose 1.2.安装DockerCompose 1.2.1 修改文件权限 1.2.2 Base自动补全命令&#xff1a; 1.3部署微服务集群 1.3.1.compose文件 1.3.2.修改微服务配置 1.3.3.打包 1.3.4.拷贝jar包到部署目录 1.3.5.部署 1.Docker-Compose Doc…...

neo4j 图数据库 py2neo 操作 示例代码

文章目录 摘要前置NodeMatcher & RelationshipMatcher创建节点查询获取节点节点有则查询&#xff0c;无则创建创建关系查询关系关系有则查询&#xff0c;无则创建 Cypher语句创建节点 摘要 利用py2neo包&#xff0c;实现把excel表里面的数据&#xff0c;插入到neo4j 图数据…...

从uptime看linux平均负载

从前遇到系统卡顿只会top。。top看不出来怎么搞呢&#xff1f; Linux系统提供了丰富的命令行工具&#xff0c;以帮助用户和系统管理员监控和分析系统性能。在这些工具中&#xff0c;uptime、mpstat和pidstat是非常有用的命令&#xff0c;它们可以帮助你理解系统的平均负载以及资…...

经典数据库练习题及答案

数据表介绍 --1.学生表 Student(SId,Sname,Sage,Ssex) --SId 学生编号,Sname 学生姓名,Sage 出生年月,Ssex 学生性别 --2.课程表 Course(CId,Cname,TId) --CId 课程编号,Cname 课程名称,TId 教师编号 --3.教师表 Teacher(TId,Tname) --TId 教师编号,Tname 教师姓名 --4.成绩…...

架构篇06-复杂度来源:可扩展性

文章目录 预测变化应对变化小结 复杂度来源前面已经讲了高性能和高可用&#xff0c;今天来聊聊可扩展性。 可扩展性指系统为了应对将来需求变化而提供的一种扩展能力&#xff0c;当有新的需求出现时&#xff0c;系统不需要或者仅需要少量修改就可以支持&#xff0c;无须整个系…...

flowable流程结束触发监听器 flowable获取结束节点 flowable流程结束事件响应监听器

flowable流程结束触发监听器 | flowable流程结束获取结束节点 | flowable流程结束事件响应监听器 下面代码是该监听器是对每个到达结束事件后执行的。 原本的流程定义是如果其中任意某个节点进行了驳回&#xff0c;则直接结束流程。 所以在每个节点的驳回对应的排他网关都设…...

【Python3】【力扣题】389. 找不同

【力扣题】题目描述&#xff1a; 【Python3】代码&#xff1a; 1、解题思路&#xff1a;使用计数器分别统计字符串中的元素和出现次数&#xff0c;两个计数器相减&#xff0c;结果就是新添加的元素。 知识点&#xff1a;collections.Counter(...)&#xff1a;字典子类&#x…...

【从0上手cornerstone3D】如何加载nifti格式的文件

在线演示 支持加载的文件格式 .nii .nii.gz 代码实现 npm install cornerstonejs/nifti-volume-loader// ------------- 核心代码 Start------------------- // 注册一个nifti格式的加载器 volumeLoader.registerVolumeLoader("nifti",cornerstoneNiftiImageVolu…...