概览

本题基于 MarioKing 仓库构建了一个单机马里奥手游,以顶计数砖块的形式接收玩家输入:

get_input

以顶问号块的形式进行输入校验:

start_check

如果输入正确,玩家可以获得一个增强力量的蘑菇,从而击败守护 flag 的板栗仔。本题的 flag 即为玩家的正确输入(上下两行的拼接)。

出题视角

游戏采用 cocos2d-x 引擎与 lua 语言开发,其中大部分的加密和混淆方式均取材自我不久前研究的一款国产手游。因为这些逆向对抗手段都比较有意思,也较为经典,所以就拿过来出题了。

环境检测

相信这个部分大家都见怪不怪了,加不加影响不大,所以不如加上(

首先是 Java 层,在 org.cocos2dx.lua.AppActivity onCreate 函数中调用的 Cocos2dxUtil.lowEngineVersion 用于检测设备是否被 root。

其次是 Native 层,JNI_OnLoad 中通过 pthread_create 创建环境监测线程,检查进程是否被调试、是否存在 IDA server 和 frida server。

lua 脚本加密

采用 cocos2d-x 引擎 lua-binding 开发的游戏逻辑代码并不直接存在于 Java 层或 Native 层中,而是作为资源文件存放在 apk 的 assets 目录下,因此有必要对明文的 lua 脚本进行加密处理。

脚本编译

本题使用的 cocos2d-x 引擎版本为 3.17.2,该版本的 lua 引擎为 LuaJIT。将 lua 脚本预编译为 LuaJIT 可解释的 luac 字节码文件以防止源码泄露,是一种对抗逆向工程的经典策略。

本题也采用了这种策略,但是为了防止 luac 能被现有工具直接反编译,我还修改了 LuaJIT 解释器的 opcode 顺序。

内容加密

传统的 cocos2d-x luac 加密是通过游戏引擎自带的 xxtea 算法完成的,因为密钥找起来太容易,所以这种方式自然被舍弃。

在实现时,我定义了一种以 FF FF DB EE 为文件头的加密文件结构,并修改 cocos2d-x 引擎源码的FileUtilsAndroid::getContents,加入了相应的解密函数。

名称混淆

cocos2d-x 的游戏脚本一般存放在 apk 的 assets/src 目录下,为了防止有意义的符号泄露信息,我将该目录下的所有文件名、目录名修改为它们原本名字的 xxhash 结果,并修改 cocos2d-x 引擎源码的 FileUtils::fullPathForFilename,加入了相应的字符串映射函数。

其他处理

  • 输入校验逻辑虚拟化(小型虚拟机)
  • Native 层去符号

获取明文 luac64 文件

在 IDA 字符串窗口搜索 cocos2d-x- 可以发现程序使用的 cocos2d-x 版本为 3.17.2

cocos2d_version

搜索 LuaJIT 可以发现程序使用的 LuaJIT 版本为 2.1.0-beta3

luajit_version

LuaJIT-v2.1.0-beta3 仓库下载源码,参考 LuaJIT 官方文档Cross-compiling LuaJIT 部分,使用 android-ndk-r20b 编译一份 arm64 架构的 libluajit.so,再借助 IDA bindiff 插件即可还原 LuaJIT 引擎的关键符号。

完成上述操作后,在函数列表中搜索可以发现 luaL_loadbuffer 的地址为 0xAC4E9C,该函数的原型为 LUALIB_API int luaL_loadbuffer(lua_State *L, const char *buf, size_t size, const char *name),第二个参数指向当前加载字节码文件的二进制内容,第三个参数指明 buf 的大小,第四个参数为模块路径名。

hook 该函数入口,打印第四个参数 name:

1
2
3
4
5
6
7
8
9
var lib_base = Module.findBaseAddress("libgame.so");

Interceptor.attach(lib_base.add(0xAC4E9C), {
onEnter: function(args) {
var chunk_name = args[3].readCString();
console.log("Load: " + chunk_name);
},
onLeave: function(retval) {}
})

程序打开后(游戏主菜单处)注入上方 js 脚本,并点击开始游戏,frida 端的输出如下:

1
2
3
4
Load: scene/GameScene.pyc
Load: core/GameMap.pyc
Load: entity/Enemy.pyc
Load: entity/Mario.pyc

顶问号方块时,还会输出 Load: core/Util.pyc,因此输入校验逻辑应该与其相关。这里可以看到,所有的脚本后缀都是 .pyc,但其实通过搜索文件头前四个字节,可以发现其为 LuaJIT 字节码文件。

先将所有加载了的脚本 dump 出来,在 dump 的过程中,手动将文件后缀改为 .luac64(指代 LuaJIT 64 位字节码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var base_dir = "/data/data/cn.org.xctf.flagio/";
var lib_base = Module.findBaseAddress("libgame.so");

Interceptor.attach(lib_base.add(0xAC4E9C), {
onEnter: function(args) {
var chunk = args[1];
var chunk_size = args[2].toInt32();
var chunk_name = args[3].readCString();

var new_name = chunk_name.slice(chunk_name.lastIndexOf('/') + 1, -3) + "luac64";
var file = new File(base_dir + new_name, "wb");
file.write(chunk.readByteArray(chunk_size));
file.close();
},
onLeave: function(retval) {}
})

将文件移动到 /data/local/tmp 并赋予 777 权限后拉到本地即可获得明文的 luac64 文件:

dump_bytecodes

为什么说这些文件是 64 位而不是 32 位的呢?我们用 16 进制编辑器随便打开一个文件,它的前五个字节都是 1B 4C 4A 02 0A。而第五个字节的 0x0A 是 LuaJIT 文件结构中的 GlobalHeader.flags 字段,其二进制形式为 1010, 置位的两个比特位分别表示文件被去掉了符号(FLAG_IS_STRIPPED = 0b00000010)和采用 2-slot frame info 模式(FLAG_FR2 = 0b00001000),后者是 64 位引入的新特性,详情请参考 Finish LJ_GC64 mode

当然,你可以通过打印 luaL_loadbuffer 的调用栈往上追溯到关键解密函数 sub_5035CC,分析它的实现再写脚本解密 apk 的 assets/src 目录下的所有加密文件(或用其他方法主动调用)。不过这样做就复杂了,所以这里不作展开,仅给出 python 版的解密脚本:

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import os
import zlib
import struct

block_size = 4
file_magic = b"\xFF\xFF\xDB\xEE"

enc_tbl = [
0xc6bfb437, 0x8601dd6b, 0x9f6bdbea, 0xe3ec6fea, 0x9f6283a8, 0xd21da726, 0xc15ff083, 0x3dec6868, 0x53f4c551, 0x8cddfaeb, 0xf3c858de, 0xa5a3995d, 0x8ced646b, 0xde783524, 0x5f83b518, 0x20b1a8dc, 0x0095b96a, 0x1a7d641c, 0xf0b529e1, 0x17abf4ef,
0x4ba44454, 0x24477765, 0x4f477d43, 0x13163da6, 0x1cfdc4b9, 0xc98feb0c, 0x1263e38c, 0x9214113b, 0x5530b57b, 0x97c9c907, 0x59c62012, 0x8ef987d2, 0xe9bc51c2, 0xde70b2f2, 0x3118de7a, 0x66dda446, 0xbabd9012, 0x881f794f, 0x52741f20, 0xdfc43ad9,
0x16aff5ff, 0x4c133c12, 0x8594ac3e, 0xbf95602c, 0xd00badc9, 0x608c398d, 0x46ff9161, 0x3f00ecbd, 0xed3c0bdf, 0x77e37cc9, 0xe3c35119, 0x66d486fc, 0x3ee1a90d, 0x7ee04048, 0xbe254e4e, 0x9b7c0584, 0xe8b5d0f1, 0x168f6f7d, 0x8fa08863, 0xf734d73e,
0x58e56b08, 0x6f179538, 0x47e0c609, 0x522e3e48, 0xb71b908f, 0x1db04b1a, 0x0dfb2e29, 0x9df3f71e, 0x73072a55, 0xe9d6d17c, 0xef17f00a, 0x4ff4a7ed, 0x6f11d3dd, 0xe57d4240, 0x730bdaf2, 0xbeb18cb2, 0x23f2a7b9, 0x88edc6e6, 0x7219fb97, 0x41152194,
0x76cc5eb1, 0x22c7de91, 0x4186e4b3, 0x87efa1ae, 0x173a54e0, 0x8e49b7b4, 0xa3390d88, 0x9499f12e, 0x1e2f362a, 0xf92bda22, 0x133dcd2f, 0xb5bf7704, 0x133007a4, 0x677ea91c, 0x7ca51393, 0xb271661b, 0xb961496b, 0x7d37c903, 0x8f1baf38, 0x69a2ca9f,
0xdeebcb61, 0x596bce09, 0x0a610760, 0x410896ce, 0x803f612a, 0xe17166f7, 0x6329e37e, 0x69676067, 0xcd15b586, 0x28a8262d, 0xe55f85c8, 0xe9b68a7e, 0xfe84fccf, 0xa568180b, 0x0a776072, 0x1fcc5ea0, 0x5237f259, 0x655cb945, 0x0ed0a769, 0x2ddfb75f,
0x02ab9014, 0x60829de3, 0xce7d8582, 0xc7a0840e, 0x2db226de, 0x2f4d65b1, 0x3562407e, 0x6f60bb04, 0xd5bf4fac, 0x5c1b419a, 0x2e2aed20, 0x4ba278be, 0xe5826e22, 0xf1719ad8, 0x8355c16a, 0x7f9c5099, 0xf81d43e8, 0x96d68a16, 0x51b4fbd6, 0xc5aba1a9,
0x2906faaf, 0x08df650b, 0x8c2cf238, 0x47660236, 0x21284442, 0x1be465d3, 0x7b2f6118, 0xdd2c089d, 0xc70ab20d, 0x2164a83a, 0x6e718555, 0xc75ddbf1, 0x89fd68c1, 0xe4b4911e, 0x80d2f2b8, 0xd7e03857, 0xc40fc98c, 0x47a30b80, 0x94589a48, 0x3a55dd73,
0xf935cd28, 0xa9ba2903, 0x078f8563, 0x3151d61b, 0xacde3d3e, 0xb8ecd154, 0x87c24b49, 0xb10a0678, 0x35cf9006, 0x2ae5a60b, 0xd7b0ef0e, 0xdab27594, 0x5407ef84, 0xe12ade07, 0xc3bb788b, 0x193e6187, 0xbe2d59b0, 0xf5f845eb, 0x52366e4f, 0x5135869b,
0x343869c2, 0xec048a46, 0x0a0440c8, 0xafb3a1cc, 0x04db06f7, 0x1455dde2, 0x8b42b7a5, 0x33e04cba, 0xb6412b66, 0x729ee85c, 0xfadf7cde, 0xa14fcaf4, 0xa7b3121e, 0x2b01d813, 0x1be0c240, 0xc0a73eb8, 0xcc90e00f, 0x014b45c9, 0x86cbc26c, 0xb91f7d5e,
0xb5172fd6, 0x9fe38ef7, 0x9b7f0bff, 0x6ae221c4, 0x69846e79, 0x8090ea82, 0x31e71cb4, 0x83e8e898, 0xab099975, 0x389f7b0c, 0xf0235da1, 0xa69496fa, 0xd19e910f, 0x1a44d0de, 0x163d1c84, 0x071de1bf, 0x9f827f9f, 0x13dcb3bc, 0x32f65cf8, 0x09082987,
0xb6e29776, 0xb3d15d21, 0x02669d17, 0x4bd68fb6, 0xbe618152, 0x4cdbf269, 0x0e5e5c74, 0x6b505a5a, 0x31cf50fd, 0xa9cce485, 0x14518715, 0xc920e57c, 0xca7f0f8b, 0x7ab47b24, 0x2502b1bd, 0xcbe725d7, 0x1686ae0b, 0x76e8d3cc, 0x03e562cd, 0x47c27f90,
0x0bf1ac9f, 0x7203e668, 0x12660dbe, 0xfa1754a0, 0x955de065, 0x1c5fcd6b, 0xeb5a16ac, 0x93a8f58a, 0x757c8449, 0x086943af, 0x9e099367, 0x04d88f41, 0xf14da8c6, 0x5b014a71, 0x9f08ab6f, 0x546e02c5, 0xaa839216, 0x163623d3, 0xb77e2262, 0x1ec0d6af,
0x322e2556, 0xd8efe2a8, 0xa84791fb, 0x6579b782, 0x76ec0831, 0x563924e4, 0x52a367d9, 0x8e4f672f, 0xe7f0190a, 0xca685659, 0x5c558d20, 0xc6609bf8, 0xa2fe64eb, 0xcc6c308c, 0x3b5f7fb3, 0x6f113528, 0xd150f04c, 0x1db84f0d, 0x9f0c40d0, 0xff9dda06,
0x40b986f7, 0x652f5c98, 0x2d5d424f, 0xd508779c, 0xce2cefea, 0x408c40c1, 0xac2add9b, 0x401bf9e5, 0x4ec635fa, 0xbe2039a7, 0x2fa7b7c3, 0xe8d5e764, 0x94685963, 0x287d0a85, 0xd37e1c7e, 0xc50e60ce, 0xd936baa0, 0x60d2f38b, 0x0a631af7, 0xe6098986,
0x736621df, 0x5ab72072, 0x1b3ccc30, 0xece32f2d, 0xcb7e8e6a, 0x46accdc1, 0xb3ee9a55, 0x651b7664, 0xb8dc21a3, 0x59eb5f92, 0x0d719c43, 0x0177bbbc, 0xd8b65e50, 0xe49e3639, 0x8f43577a, 0xbf5fbaa2, 0x40529e13, 0x9ab9bc17, 0xe737dcac, 0xd0ffebba,
0x1bd139b0, 0xbfb6b530, 0x7f119a22, 0x77b6ac03, 0x3da04c35, 0x3e5a8446, 0x4e1c5123, 0x14dba10e, 0xae8227a4, 0x7b7d1c1f, 0xd28cb72a, 0xfae9dabe, 0x8eeaec66, 0xadea7dad, 0x5ba21551, 0x15f1e4bb, 0x6cdc9cbc, 0xd1b50569, 0xa9fda62c, 0xb4a69d8e,
0x8552f7b0, 0x0e5ab05d, 0x7afda900, 0x7160f49a, 0x9cd74a63, 0x6c8bd299, 0xaf11f097, 0x69baa295, 0xdcd99b1d, 0x36467f3a, 0xbb8a82a6, 0x300ccd43, 0x78961a0c, 0xef6d7ab8, 0x20477213, 0x15b36dfb, 0x878d11e7, 0x7c026830, 0x923400a6, 0x68f97cfc,
0xcc8492f8, 0x21c02cb1, 0xe6190991, 0xf0a15763, 0xb789f74b, 0x7cdef308, 0xd8452eef, 0x8f700ba2, 0x1f34c45e, 0xaeb19b7f, 0x6e4b1460, 0x6a77b90b, 0x168067b2, 0x739f3d63, 0x164a0031, 0xe5d9af44, 0x9fe6950f, 0xb3877e24, 0x787893b5, 0x52eeebbf,
0x8119aab3, 0x9d71c82e, 0xaeddb512, 0xb2739605, 0x6de257c9, 0x2583e4ca, 0xae3efb2a, 0xcf70e8d8, 0x5f3bf373, 0x93720ba0, 0x3d7356f4, 0x074a5bfa, 0x86705c32, 0xc750b7e7, 0x1172dded, 0xf3691a33, 0x2ead9e77, 0x99c6d776, 0x971b630d, 0x3119628c,
0x71b7175b, 0x3c7fc6b5, 0x7176c0bc, 0xfdfa123c, 0x6869b5e1, 0x567d3e0a, 0xb63f1c8e, 0x86a509fb, 0x0350e36a, 0xe683b1ba, 0xc8573667, 0xb5d68412, 0x0f0ca321, 0x1b2f93c4, 0xc366d743, 0xa75f5f1f, 0x6bd30415, 0x578e1228, 0x9ad7e1de, 0xdd923625,
0xb5c6960d, 0x839e8c6b, 0xb79da6a8, 0x5d516c96, 0x3e031138, 0xc26a69b5, 0x71e02b2a, 0x83d5badf, 0x3f10bcda, 0x5cd27b7b, 0xc1b7327b, 0xb2b1fc19, 0xb5da9c7c, 0x9e6c6788, 0x2c48de6c, 0x0acbf365, 0x901e38fe, 0x76301365, 0x22ee70e6, 0x76b31faf,
0x86c404b6, 0xb233216d, 0x3031b902, 0x61cce2a6, 0xebaa334b, 0x7ca20c9c, 0x0925a9cf, 0xc6ba2e40, 0xa39f3b34, 0x7da5c1a1, 0xa6e137ae, 0xdc517135, 0x29e9606b, 0x7ba0e387, 0x30511df0, 0xa1c85fc9, 0xdf3a9378, 0x60cd580c, 0x375adb50, 0x6ddc4bc0,
0x2cd369d4, 0x8c6af18d, 0x65849d8a, 0x1badd1c3, 0x5318ad5c, 0xefcf5a8c, 0x353c1838, 0xa1ee6742, 0xff578c6d, 0x6feed6cc, 0x916cdd96, 0x2207e0d3, 0xb509a85c, 0xac18ddf6, 0xe6035b44, 0x58f262b5, 0x255ee071, 0x7e4fbd36, 0x5701bb1c, 0x70d05c02,
0x6b04c60a, 0x119ff091, 0x7de77c28, 0x01ea0beb, 0xffa2012e, 0xcd289b5f, 0x8cb81969, 0x38ae5b48, 0x2de87535, 0x2c5f4e7a, 0xdd37d2e0, 0x7b321056, 0x073afded, 0xddba0425, 0x103f5785, 0x9c7751dd, 0x5b2f0227, 0x96d6a165, 0x8aa4f0c1, 0x4a9c9e92,
0x8a09df65, 0x706839b6, 0x7e148267, 0x07c2b26f, 0x50573df4, 0x27ec1fe2, 0xddbed1b6, 0x3025b059, 0x5ea770b6, 0x6f36fccb, 0xfaca270c, 0x8aa5738f, 0xb43322a9, 0xeb081caa, 0x4ec2902a, 0x25cd3eea, 0xf7fbc7c8, 0x7df412f0, 0x65bbff82, 0x3df15d68,
0xa2952aed, 0x385845c4, 0xcd00c3b2, 0xcb041ee3, 0xc067ba13, 0xc1a00e99, 0x18b3ecfa, 0xe05be99e, 0x2b5793b7, 0xf8c1e25b, 0xff4d0214, 0x184f1919, 0x48c62d8a, 0x54696b77, 0xeb358643, 0x7b26489e, 0x74f64703, 0x20581ac2, 0x86a3a152, 0xd91e6385,
0x0ddf82cf, 0x1b2b8787, 0x9d656c97, 0x2b35f5a4, 0x8009457c, 0x34b5625b, 0x91f6bf53, 0x092c2469, 0x49bb878c, 0x67bddbdd, 0x7808a8f4, 0x06bab5d8, 0xf1052269, 0x1c4dc31c, 0x41f4cd77, 0x7428fdef, 0x35ddabda, 0xae48e659, 0xde331397, 0xa75ef097,
0x8ee510a2, 0x3f1a42e3, 0x36411ad6, 0x5c3bb01d, 0xb64090a1, 0x89e36f7b, 0x623708aa, 0xfdf4ed1c, 0xf714506f, 0x2a582445, 0x2fcf3401, 0x5d6ab83f, 0xa685d2af, 0xd59b0022, 0x158f52da, 0x8795a624, 0x0f069f3d, 0x01ea335b, 0x9fa22d71, 0x486a4e95,
0x256ca29a, 0x84e10604, 0x3c60c902, 0xc6b2092c, 0xb684090e, 0x4989b96a, 0x96bce509, 0xfaa0f229, 0x3220aae8, 0x5cb9ced1, 0x75e864b0, 0xdd116efd, 0xa608ec2d, 0xf51d3e1c, 0xfc548a6e, 0x08175bd2, 0x044729d5, 0x4d1bed73, 0xd8b9c5e3, 0x52ae7c1b,
0x946622c5, 0x512bb583, 0xd3c1d5f5, 0x412c4de0, 0xdb6a7e5e, 0xfbe6fee0, 0x59fcd7c7, 0x59132cc9, 0xf38ea2df, 0xf114c426, 0x8e686f80, 0x1b8c0c33, 0x2e954e26, 0xfeac08ad, 0x70c35e70, 0x52442023, 0xed58707c, 0x7839e1b8, 0xf7944491, 0xbdbf75dc,
0x6b68cb1b, 0xde45390b, 0x9b4b98ed, 0x10f33556, 0xdb7ec13f, 0xbd359844, 0x35c36bdb, 0x1e6a6a68, 0xeedc3b19, 0xc0ce9ebd, 0xda077d0c, 0x97513627, 0xbe2ab4a8, 0x82416ff9, 0x0cd5bd78, 0xad41b27f, 0xa3b4c5e8, 0x4dd1e832, 0x53c6bdbe, 0xcc6ab880,
0xce947057, 0xb8f9161a, 0x668eda53, 0xa85834eb, 0xa6df8924, 0x1da9e9ae, 0x052a3c9d, 0x65aad5f4, 0xacdcf2cf, 0x98974b21, 0xee583d8b, 0xd7686039, 0x69c32a05, 0x807ef23a, 0x22d9756b, 0x4854feaf, 0x720e05e0, 0xe6175b72, 0x9149f2bd, 0xa4482834,
0xe2fe8618, 0x4ca97bdc, 0x3a609ceb, 0xa2c60972, 0x2fc23750, 0x5ba701d5, 0x4c86d7b7, 0x19b87d79, 0xc92ca13b, 0x334795a8, 0x64b38fd4, 0x97273086, 0x34bdfed6, 0x88f62637, 0x46914256, 0x2f04690b, 0xa5a3edaa, 0x3185b7ab, 0x35790aad, 0xda58400d,
0xd54655e4, 0x4a4ac79c, 0x139d026e, 0xce9b6d44, 0x8651fa21, 0xb8000ecf, 0x87988316, 0x1541eec9, 0xb5fb5189, 0x689af174, 0xbd2eeb22, 0x72051645, 0xaed24488, 0xc6794fbb, 0x00f2e376, 0x54f9e4ec, 0xede0cba2, 0xea613503, 0xcb433dc0, 0xc58bda4f,
0x365b983e, 0x1451bc20, 0xc2b18a7d, 0xc65ed5bd, 0x33374784, 0xfdf37490, 0x07bff8d0, 0x5e118f5c, 0xfb18b6fb, 0xe09af302, 0x496b0dc1, 0x5235e5d7, 0x6ecb2112, 0xd0183177, 0x783720c3, 0xa185067c, 0xdc9b4381, 0x4ad1cf10, 0x433ad32c, 0x1aa35a53,
0xb3460aac, 0xd12b6892, 0x44349f2d, 0x1f3ffb7a, 0x2924f758, 0x1883702e, 0x6df6ca0e, 0x4e82e9bd, 0x037b0c08, 0xb4c0afdd, 0x5edf7c3f, 0xf70b8df5, 0xb494c0a7, 0x47319505, 0xac6b8125, 0x57d78aa9, 0x4a1f64fc, 0x944be247, 0xae158c49, 0x10b405ef,
0x0ca7f623, 0x6c00241a, 0x1c0bbaa3, 0xb846b675, 0x1b89d58d, 0x77e4d271, 0x7c96f2d5, 0x13f8280b, 0x5cf84d45, 0x16b51699, 0xacb454c9, 0x8b27dbd2, 0xd5c82535, 0xd51d9d1a, 0x54b628d2, 0xa132316a, 0xa55a3586, 0x6f948e0f, 0x4df05295, 0xbf64f4b9,
0x58689f93, 0x39a85648, 0xa3dedaec, 0x911158c4, 0x3c92bf0d, 0x9e348fe6, 0xd9cb180d, 0x97ebec50, 0x78b83de9, 0x73ec6c48, 0x6b7b4b63, 0x16aa6ba8, 0xa2039a01, 0x804b4464, 0x68942114, 0xa8dd6a02, 0x0d80029f, 0x374c2cbf, 0xfb353d8c, 0x3324161b,
0x0d2757de, 0x7f491efb, 0xb4456140, 0x3ffe2164, 0x9c967463, 0x27f7b5b8, 0xca0cd170, 0x044f53a4, 0x893563e7, 0xedbce4d8, 0x021b5414, 0x4a46b308, 0x5ab79e4a, 0x554f4a3f, 0xe4db258e, 0x7af947a4, 0x079e4882, 0x7ca6cf87, 0x8039be38, 0xdf33dc9a,
0xeea9b4e7, 0x0910e3b0, 0x579762f8, 0x7cafa12c, 0x2bafb910, 0x355e70bf, 0x95982c0e, 0xb5fa17b3, 0xcddbbfff, 0xbe459e56, 0xb10af261, 0x5c2a2af1, 0x4eee8c3a, 0x65d14d8a, 0x95897345, 0x7a4a7fd8, 0x6cc41bec, 0x7dec212c, 0xc0ea7b93, 0x6b57d950,
0xf16b98fd, 0xcdbc3491, 0x02d35ee0, 0xb1aba563, 0x2ad6fa58, 0x194b1e34, 0x89087286, 0x3d3ccca1, 0xb9fdf788, 0xf1e30c50, 0x5729a64b, 0x0a5befc6, 0x1f76c0b2, 0x3e77ea9d, 0x8a355c32, 0xdf45cd17, 0x985e31a6, 0xd29a51ca, 0x56f0b429, 0x042d4a8c,
0xdd765852, 0x63715628, 0x738cebe6, 0xfc2df4b1, 0x9b5ba9d6, 0x96f78f67, 0xcc2e210f, 0x9c6d611a, 0x366d6791, 0x9a43361c, 0x5fb81ec9, 0xb9fe6794, 0x7375d302, 0xc62fa818, 0xcc6e19ae, 0x2f4d38de, 0xd0010ed6, 0xae25aae8, 0xca30036e, 0xd615450a,
0xf244f954, 0x82959369, 0x7031c12f, 0x5933a6dd, 0x1169558f, 0x2b2ee00b, 0x6f89f242, 0x14149c3e, 0xee2fa1b1, 0xaa020888, 0x5285bcbe, 0x6b1a451d, 0x17c862ac, 0x677a7201, 0xaae07dfa, 0x61daeaa5, 0x0d4fb80e, 0x7d74345f, 0xc512b3bb, 0x566bb4d0,
0x06c69771, 0xe4955e92, 0xe2b5d04d, 0x5df4ee34, 0x3928168a, 0x48ed0c2a, 0xd7d95fe1, 0xf01c3264, 0x86cdc089, 0xa46b7cfb, 0x2fede941, 0x74ec18e2, 0x9d56e1cc, 0xcce5dc4c, 0xdbeabd6d, 0x9480971f, 0x786dcaa1, 0xad272375, 0x20003617, 0xe13ad0f6,
0xf0d86596, 0xf61b898b, 0x2aac895a, 0x241aad94, 0x3da6a9a8, 0x6d0bc797, 0xa92e8a90, 0x6e737090, 0x82c35afa, 0xdea3a945, 0x2fd754bd, 0x7a23f110, 0xf0f1a2f0, 0xf17280a2, 0x3f1eda1c, 0x0f03ed4e, 0x3e02d44a, 0x6073f404, 0x026302c6, 0xf9aa2e7d,
0x191eeded, 0x5857dfca, 0x4bd140d3, 0xe04e4969, 0xad0c6975, 0xdabc6526, 0x40db87f6, 0xd3fcb14d, 0x5f6733a7, 0xbdced7fd, 0x7fdf2471, 0x983bea50, 0x15ab4e1a, 0x408265c6, 0xb33b1df3, 0xfec4aae0, 0x299e846a, 0xd1a8f4aa, 0xbe72d405, 0x1ab0b5d1,
0x5fb1b52d, 0x9232ccdb, 0xbbfd8112, 0xf7198310, 0xeab71190, 0x135fe561, 0xbe0dcc84, 0x17601c74, 0x31ac3a98, 0xd67ca4b6, 0x99f14711, 0x5475ba0b, 0xbc011f67, 0x7f84e0d5, 0x506acdb6, 0x47bc32d1, 0x807de61d, 0x889f6834, 0x13be14a7, 0xe55f0f4c,
0x3289738b, 0xea35b862, 0x8a2cab9f, 0xf64f4dfc, 0x6e255608, 0x718a29c1, 0xe36ada40, 0x570d6a97, 0x661e616d, 0x68b69cd3, 0xd075c3f4, 0x71e9cfbd, 0x4ab3086b, 0xd8e2d945, 0x632a0c6e, 0xbe0e6145, 0xe60a45c9, 0x5467aa5a, 0x812dc36e, 0xc8ed5ed9,
0xc2aa5ac8, 0xb7bee330, 0x2a0b5456, 0xf4246482, 0x0ceaf7d9, 0xcba5b9b5, 0x1edb9f9b, 0xff7feefd, 0x36599e26, 0x350db259, 0x3d16422e, 0xff94c0a0, 0x15a7185c, 0x333b4cef, 0x91481df8, 0x882c24b8, 0x9ffc1ff5, 0xdc64b9ba, 0x0cb8510c, 0xfb68492d,
0x41d8f2a2, 0xeebe9afd, 0x7d1de998, 0xd0a8aaf0, 0x1df9329a, 0xaf2cc29a, 0x07635562, 0xae187f03, 0x858d40da, 0x996462dd, 0x196b8b75, 0xbed95cda, 0xabe4ab1c, 0xd9fc2e50, 0xd59f74c8, 0x174bf4bc, 0xf8a5bd8a, 0x9b1b0756, 0x2747c3b8, 0x3145d4e1,
0x3145d42a, 0xccfa3831, 0xc98fce4a
]

def do_xor(array: list, nblocks: int):
idx = 0
idx2 = 0
while idx < nblocks and idx < 0x200:
array[idx] ^= enc_tbl[idx2]
idx2 = (idx2 + 1) % len(enc_tbl)
idx += 1

while idx < nblocks and idx < nblocks - 0x200:
array[idx] ^= enc_tbl[idx2]
idx2 = (idx2 + 1) % len(enc_tbl)
idx += 4

while idx < nblocks:
array[idx] ^= enc_tbl[idx2]
idx2 = (idx2 + 1) % len(enc_tbl)
idx += 1

def decrypt_file(if_path: str, of_path: str):
with open(if_path, "rb") as f:
data = f.read()
assert data[:4] == file_magic

cipher = data[20:]
compressed_size = len(cipher)
nblocks = compressed_size // block_size
plain = [0] * nblocks
target_crc, _, origin_size = struct.unpack("3I", data[8:20])
assert nblocks > 0

for i in range(nblocks):
plain[i], = struct.unpack('I', cipher[i*4:(i+1)*4])

do_xor(plain, nblocks)

compressed = b''
for i in range(nblocks):
compressed += struct.pack('I', plain[i])

if compressed_size % block_size:
compressed += cipher[-(compressed_size % block_size):]

plain = zlib.decompress(compressed)
assert zlib.crc32(plain) & 0xFFFFFFFF == target_crc

with open(of_path, "wb") as g:
g.write(plain)

decrypt_file("526018661", "Util.luac64")

识别乱序 opcode

如果用现有反编译工具直接反编译得到的 luac64 文件,结果一定是不正确的,因为 LuaJIT 引擎的 opcode 顺序被修改了。这一板块主要介绍如何在 Native 层中识别并还原出引擎的 opcode 顺序。

lj_obj.h 文件中,我们可以找到 lua_State 结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Per-thread state object. */
struct lua_State {
GCHeader;
uint8_t dummy_ffid; /* Fake FF_C for curr_funcisL() on dummy frames. */
uint8_t status; /* Thread status. */
MRef glref; /* Link to global state. */
GCRef gclist; /* GC chain. */
TValue *base; /* Base of currently executing function. */
TValue *top; /* First free slot in the stack. */
MRef maxstack; /* Last free slot in the stack. */
MRef stack; /* Stack base. */
GCRef openupval; /* List of open upvalues in the stack. */
GCRef env; /* Thread environment (table of globals). */
void *cframe; /* End of C stack frame chain. */
MSize stacksize; /* True stack size (incl. LJ_STACK_EXTRA). */
};

其中 glref 字段指向了 global_State 结构体,顾名思义,其保存着 LuaJIT 的一些全局信息,由所有线程共享。但是我们无需去关心它的定义,因为我们感兴趣的字段不在这个结构体中,只是借助它来找到一个名为 GG_State 的结构体,后者保存着更为顶层的全局信息,其定义在 lj_dispatch.h 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Global state, main thread and extra fields are allocated together. */
typedef struct GG_State {
lua_State L; /* Main thread. */
global_State g; /* Global state. */
#if LJ_TARGET_MIPS
ASMFunction got[LJ_GOT__MAX]; /* Global offset table. */
#endif
#if LJ_HASJIT
jit_State J; /* JIT state. */
HotCount hotcount[HOTCOUNT_SIZE]; /* Hot counters. */
#endif
ASMFunction dispatch[GG_LEN_DISP]; /* Instruction dispatch tables. */
BCIns bcff[GG_NUM_ASMFF]; /* Bytecode for ASM fast functions. */
} GG_State;

这里可以发现一个非常有意思的的字段 dispatch,后方的注释也告诉我们该数组是 LuaJIT 内部维护的一张指令跳转表,每条 LuaJIT 虚拟机指令都能在这个数组中找到对应的处理例程。

现在我们来研究一下 LuaJIT 是怎么取指令和译码分发的,搞清楚这个流程才能找到跳转表的位置,进而才能找到各指令的具体实现及它们的先后顺序。

不妨在 LuaJIT 源码中跟一下 luaL_loadbuffer 函数的实现,看看它到底是如何加载运行 LuaJIT 字节码的,该函数定义在 lj_load.c 文件中,下面一并列出相关函数:

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
LUALIB_API int luaL_loadbuffer(lua_State *L, const char *buf, size_t size,
const char *name)
{
return luaL_loadbufferx(L, buf, size, name, NULL);
}

LUALIB_API int luaL_loadbufferx(lua_State *L, const char *buf, size_t size,
const char *name, const char *mode)
{
StringReaderCtx ctx;
ctx.str = buf;
ctx.size = size;
return lua_loadx(L, reader_string, &ctx, name, mode);
}

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

不难发现,luaL_loadbuffer 内部在完成一些结构体的初始化工作后,实际通过 lj_vm_cpcall 函数来启动 LuaJIT 虚拟机,并且当前脚本运行结束后,会将状态码存放到局部变量 status 中。因此我们应该着重分析 lj_vm_cpcall

这里插一句题外话,LuaJIT 为了追求虚拟机性能,特意使用汇编来书写 vm 的核心功能,其中包括取指令、译码执行等部分,并针对不同的架构定制了对应的 dasc 文件。本题的 so 是 arm64 架构,因此接下来的分析将基于 vm_arm64.dasc 文件。

vm_arm64.dasc 第 526 行可以找到 lj_vm_cpcall 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|->vm_cpcall:				// Setup protected C frame, call C.
| // (lua_State *L, lua_CFunction func, void *ud, lua_CPFunction cp)
| saveregs
| mov L, CARG1
| ldr RA, L:CARG1->stack
| str CARG1, SAVE_L
| ldr GL, L->glref // Setup pointer to global state.
| ldr RB, L->top
| str CARG1, SAVE_PC // Any value outside of bytecode is ok.
| ldr RC, L->cframe
| sub RA, RA, RB // Compute -savestack(L, L->top).
| str RAw, SAVE_NRES // Neg. delta means cframe w/o frame.
| str wzr, SAVE_ERRF // No error function.
| str RC, SAVE_CFRAME
| str fp, L->cframe // Add our C frame to cframe chain.
| str L, GL->cur_L
| blr CARG4 // (lua_State *L, lua_CFunction func, void *ud)
| mov BASE, CRET1
| mov PC, #FRAME_CP
| cbnz BASE, <3 // Else continue with the call.
| b ->vm_leave_cp // No base? Just remove C frame.

其中有一句 ldr GL, L->glrefglobal_State 结构体地址赋值给 GL,GL 的定义如下,其实就是 x22 寄存器

1
2
3
|.define GLREG,		x22	// Global state.
...
|.type GL, global_State, GLREG

接着程序正常执行,会通过 cbnz BASE, <3 跳转到前面的 标签 3 处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|3:  // Entry point for vm_cpcall/vm_resume (BASE = base, PC = ftype).
| str L, GL->cur_L
| ldp RB, CARG1, L->base // RB = old base (for vmeta_call).
| movz TISNUM, #(LJ_TISNUM>>1)&0xffff, lsl #48
| movz TISNUMhi, #(LJ_TISNUM>>1)&0xffff, lsl #16
| add PC, PC, BASE
| movn TISNIL, #0
| sub PC, PC, RB // PC = frame delta + frame type
| sub NARGS8:RC, CARG1, BASE
| st_vmstate ST_INTERP
|
|->vm_call_dispatch:
| // RB = old base, BASE = new base, RC = nargs*8, PC = caller PC
| ldr CARG3, [BASE, FRAME_FUNC]
| checkfunc CARG3, ->vmeta_call
|
|->vm_call_dispatch_f:
| ins_call

在设置好一些虚拟机将用到的寄存器初值后(PC 等),通过 ins_call 宏正式开始第一条指令的解释执行。该宏被声明在 214 行:

1
2
3
4
5
|.macro ins_call
| // BASE = new base, CARG3 = LFUNC/CFUNC, RC = nargs*8, PC = caller PC
| str PC, [BASE, FRAME_PC]
| ins_callt
|.endmacro

其中 ins_callt 也是一个宏定义,将其展开得到:

1
2
3
4
5
6
7
8
9
10
11
|.macro ins_call
| // BASE = new base, CARG3 = LFUNC/CFUNC, RC = nargs*8, PC = caller PC
| str PC, [BASE, FRAME_PC]
| ldr PC, LFUNC:CARG3->pc
| ldr INSw, [PC], #4
| add TMP1, GL, INS, uxtb #3
| decode_RA RA, INS
| ldr TMP0, [TMP1, #GG_G2DISP]
| add RA, BASE, RA, lsl #3
| br TMP0
|.endmacro

取指令部分主要通过 ldr INSw, [PC], #4 实现,PC 和 INSw 都定义在了文件头,分别是 x21 和 w16 寄存器。这句汇编的意思就是从 x21 指向的空间里取来 32 bits 存放到 x16 的低四字节,然后 x21 自增 4(指向下一条指令),由此也可以得知 LuaJIT 采用定长指令集,每条指令长度为 4 字节。

译码部分主要通过 decode_RA RA, INS 宏和 add RA, BASE, RA, lsl #3 来解析操作数(这个我们不关心),计算目标跳转地址的方式也终于在此处呈现:

1
2
3
add TMP1, GL, INS, uxtb #3
ldr TMP0, [TMP1, #GG_G2DISP]
br TMP0

这里的 TMP0 和 TMP1 分别是 x8 和 x9 寄存器,INS 就是 x16 寄存器。以上三条汇编可解释为:

  • add TMP1, GL, INS, uxtb #3:将 x16 的最低字节(opcode)无符号扩展到 32 位(uxtb)后,左移 3 位(乘 8),再加上 x22 (GL)赋值给 x9,即 x9 = x22 + (((unsigned int)(x16 & 0xFF)) << 3)
  • ldr TMP0, [TMP1, #GG_G2DISP]:从 x9 加上常数 #GG_G2DISP 后指向的地址空间里取出 8 字节放到 x8,此时的 x8 即为当前虚拟机指令的 opcode 所对应的处理例程地址
  • br TMP0 跳转到处理例程去执行

其中 GG_G2DISP 定义在 lj_dispatch.h 中:

1
2
3
4
5
6
7
8
9
10
typedef struct GG_State {
...
global_State g; /* Global state. */
...
ASMFunction dispatch[GG_LEN_DISP]; /* Instruction dispatch tables. */
...
} GG_State;

#define GG_OFS(field) ((int)offsetof(GG_State, field))
#define GG_G2DISP (GG_OFS(dispatch) - GG_OFS(g))

GG_State 结构体的 g 字段与 dispatch 字段的地址差值,该值可在 IDA 中查看,为 0xF30:

GG_G2DISP

因此,我们可以换一种顺序来理解上面的三条汇编。首先通过 GL 寄存器(x22)加上 0xF30 找到 dispatch 数组,该数组中每一项都是一个处理例程的指针(8 字节),元素的下标即为该处理例程对应的 opcode,在此基础上加上 opcode * 8 就能找到当前 opcode 的处理例程了。

另外,从 lj_bc.h 的 71 ~ 197 行我们可以得知 2.1.0-beta3 版的 LuaJIT 具有 97 种 opcode:

1
2
3
4
5
6
7
8
9
10
#define BCDEF(_) \
/* Comparison ops. ORDER OPR. */ \
_(ISLT, var, ___, var, lt) \
_(ISGE, var, ___, var, lt) \
_(ISLE, var, ___, var, le) \
_(ISGT, var, ___, var, le) \
\
_(ISEQV, var, ___, var, eq) \
_(ISNEV, var, ___, var, eq) \
... // 篇幅原因,此处省略

根据上述分析,我们可以编写 hook 脚本来读取这些处理例程在 so 中的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var output = false;
var lib_base = Module.findBaseAddress("libgame.so");

Interceptor.attach(lib_base.add(0xACFEF0), {
onEnter: function(args) {
if (!output) {
var GL = this.context.x22;
var dispatch = GL.add(0xF30);
for (var i = 0; i < 97; ++i) {
var prog_ptr = dispatch.add(i * 8).readPointer();
console.log(prog_ptr.sub(lib_base));
}
output = true;
}
},
onLeave: function(retval) {}
})

得到全部 97 种 opcode 的处理例程地址:

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
87
88
89
90
91
92
93
94
95
96
97
0xacdaf0
0xacdb70
0xacdbf0
0xacdc70
0xacdcf0
0xacdd74
0xacddf4
0xacde44
0xacde94
0xacdf20
0xacdfac
0xacdff0
0xace034
0xace060
0xace08c
0xace0b0
0xace0d0
0xace0f0
0xace120
0xace160
0xace1a0
0xace1d8
0xace210
0xace234
0xace258
0xace278
0xace2a8
0xace2ec
0xace334
0xace348
0xace3f0
0xace458
0xace4c8
0xace534
0xace5a0
0xace614
0xace65c
0xace6d0
0xace73c
0xace7a8
0xace81c
0xace864
0xace8d8
0xace944
0xace9b0
0xacea24
0xacea6c
0xaceae0
0xaceb28
0xaceb74
0xaceba8
0xacec18
0xacec80
0xacecb4
0xacece8
0xaced24
0xaced6c
0xacedcc
0xacee20
0xacee38
0xacee50
0xaceedc
0xacef70
0xacefdc
0xacf024
0xacf0d4
0xacf1d0
0xacf260
0xacf2f4
0xacf35c
0xacf36c
0xacf3b4
0xacf3c0
0xacf47c
0xacf4cc
0xacf570
0xacf62c
0xacf6a4
0xacf734
0xacf7d0
0xacf7ec
0xacf878
0xacf8fc
0xacf918
0xacf94c
0xacf97c
0xacf998
0xacf9b0
0xacf9d4
0xacf9f4
0xacfa10
0xacfa50
0xacfa80
0xacfa80
0xacfb04
0xacfb08
0xacfb50

接下来就是一个比较枯燥的过程了,我们需要对照源码 vm_arm64.dasc 在 IDA 中手动标识出上方 97 个地址所对应的 opcode(暂时没有想到自动化的标注方法,如果你有想法,欢迎交流)。具体的做法是从第一个地址开始,在源码的 build_ins 函数里找到汇编代码一致的 case,而后修改函数名为 opcode 名称。

这里以第二个地址为例,在 IDA 中可以看到一条 CSEL 汇编指令:

BC_ISGE_ida

对应到源码:

BC_ISGE_ida

所以该函数为 BC_ISGE 的处理例程,同时在 IDA 中修改函数名为 BC_ISGE。以此类推,手动修改其余 96 个函数的名称。此过程中应注意源码各个 case 的判断条件并主动展开部分宏定义,IDA 没有识别出来的例程应自行新建函数。最终的部分修改结果展示如下:

func_name_modified

这样我们就将 opcode 的顺序找到了,下一步就该对之前 dump 下来的 luac64 文件进行反编译了。

反编译 luac64 文件

在 Github 上可以找到很多用于反编译 LuaJIT 字节码的工具,但是它们中的大多数对于 64 位 LuaJIT 的支持不是很好,普遍缺乏对于 2-slot frame info 模式的适配,在解析函数调用语句时会发生参数错位等的情况。

经过一系列尝试之后,我发现 luajit-decompiler 项目的反编译效果较为优秀,我们只需要在其基础上修改部分代码使其适配新 opcode 顺序即可。

首先将项目代码 clone 下来,修改 ljd/rawdump/luajit/v2_1/luajit_opcode.py 中对于 _OPCODES 变量的赋值。借助上一个板块的分析结果和 IDAPython 脚本进行格式化输出:

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
# run in IDA
addrs = [
0xacdaf0, 0xacdb70, 0xacdbf0, 0xacdc70, 0xacdcf0,
0xacdd74, 0xacddf4, 0xacde44, 0xacde94, 0xacdf20,
0xacdfac, 0xacdff0, 0xace034, 0xace060, 0xace08c,
0xace0b0, 0xace0d0, 0xace0f0, 0xace120, 0xace160,
0xace1a0, 0xace1d8, 0xace210, 0xace234, 0xace258,
0xace278, 0xace2a8, 0xace2ec, 0xace334, 0xace348,
0xace3f0, 0xace458, 0xace4c8, 0xace534, 0xace5a0,
0xace614, 0xace65c, 0xace6d0, 0xace73c, 0xace7a8,
0xace81c, 0xace864, 0xace8d8, 0xace944, 0xace9b0,
0xacea24, 0xacea6c, 0xaceae0, 0xaceb28, 0xaceb74,
0xaceba8, 0xacec18, 0xacec80, 0xacecb4, 0xacece8,
0xaced24, 0xaced6c, 0xacedcc, 0xacee20, 0xacee38,
0xacee50, 0xaceedc, 0xacef70, 0xacefdc, 0xacf024,
0xacf0d4, 0xacf1d0, 0xacf260, 0xacf2f4, 0xacf35c,
0xacf36c, 0xacf3b4, 0xacf3c0, 0xacf47c, 0xacf4cc,
0xacf570, 0xacf62c, 0xacf6a4, 0xacf734, 0xacf7d0,
0xacf7ec, 0xacf878, 0xacf8fc, 0xacf918, 0xacf94c,
0xacf97c, 0xacf998, 0xacf9b0, 0xacf9d4, 0xacf9f4,
0xacfa10, 0xacfa50, 0xacfa80, 0xacfa80, 0xacfb04,
0xacfb08, 0xacfb50
]

for opcode in range(97):
if opcode == 0x5C:
bc_name = "FUNCV"
else:
bc_name = get_name(addrs[opcode])[3:]
print(f"\t({hex(opcode)}, instructions.{bc_name}){',' if opcode < 96 else ''}")

这里需要注意,从 addrs 数组中我们也可以发现,opcode 为 0x5C 和 0x5D 的处理例程地址相同。通过排除法,可以确定这两项只能是 BC_FUNCV 和 BC_IFUNCV,我们这里就暂时规定 0x5C 为 BC_FUNCV,0x5D 为 BC_IFUNCV。如果后续反编译过程出错,再调换二者的顺序。

运行后得到如下输出:

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
87
88
89
90
91
92
93
94
95
96
97
(0x0, instructions.ISLT),
(0x1, instructions.ISGE),
(0x2, instructions.ISLE),
(0x3, instructions.ISGT),
(0x4, instructions.ISEQV),
(0x5, instructions.ISNEV),
(0x6, instructions.ISEQS),
(0x7, instructions.ISNES),
(0x8, instructions.ISEQN),
(0x9, instructions.ISNEN),
(0xa, instructions.ISEQP),
(0xb, instructions.ISNEP),
(0xc, instructions.KSTR),
(0xd, instructions.KCDATA),
(0xe, instructions.KSHORT),
(0xf, instructions.KNUM),
(0x10, instructions.KPRI),
(0x11, instructions.KNIL),
(0x12, instructions.ISTC),
(0x13, instructions.ISFC),
(0x14, instructions.IST),
(0x15, instructions.ISF),
(0x16, instructions.ISTYPE),
(0x17, instructions.ISNUM),
(0x18, instructions.MOV),
(0x19, instructions.NOT),
(0x1a, instructions.UNM),
(0x1b, instructions.LEN),
(0x1c, instructions.RETM),
(0x1d, instructions.RET),
(0x1e, instructions.RET0),
(0x1f, instructions.RET1),
(0x20, instructions.ADDVN),
(0x21, instructions.SUBVN),
(0x22, instructions.MULVN),
(0x23, instructions.DIVVN),
(0x24, instructions.MODVN),
(0x25, instructions.ADDNV),
(0x26, instructions.SUBNV),
(0x27, instructions.MULNV),
(0x28, instructions.DIVNV),
(0x29, instructions.MODNV),
(0x2a, instructions.ADDVV),
(0x2b, instructions.SUBVV),
(0x2c, instructions.MULVV),
(0x2d, instructions.DIVVV),
(0x2e, instructions.MODVV),
(0x2f, instructions.POW),
(0x30, instructions.CAT),
(0x31, instructions.UGET),
(0x32, instructions.USETV),
(0x33, instructions.USETS),
(0x34, instructions.USETN),
(0x35, instructions.USETP),
(0x36, instructions.UCLO),
(0x37, instructions.FNEW),
(0x38, instructions.TNEW),
(0x39, instructions.TDUP),
(0x3a, instructions.GGET),
(0x3b, instructions.GSET),
(0x3c, instructions.TGETV),
(0x3d, instructions.TGETS),
(0x3e, instructions.TGETB),
(0x3f, instructions.TGETR),
(0x40, instructions.TSETV),
(0x41, instructions.TSETS),
(0x42, instructions.TSETB),
(0x43, instructions.TSETM),
(0x44, instructions.TSETR),
(0x45, instructions.CALLM),
(0x46, instructions.CALL),
(0x47, instructions.CALLMT),
(0x48, instructions.CALLT),
(0x49, instructions.ITERC),
(0x4a, instructions.ITERN),
(0x4b, instructions.VARG),
(0x4c, instructions.ISNEXT),
(0x4d, instructions.FORI),
(0x4e, instructions.JFORI),
(0x4f, instructions.FORL),
(0x50, instructions.IFORL),
(0x51, instructions.JFORL),
(0x52, instructions.ITERL),
(0x53, instructions.IITERL),
(0x54, instructions.JITERL),
(0x55, instructions.LOOP),
(0x56, instructions.ILOOP),
(0x57, instructions.JLOOP),
(0x58, instructions.JMP),
(0x59, instructions.FUNCF),
(0x5a, instructions.IFUNCF),
(0x5b, instructions.JFUNCF),
(0x5c, instructions.FUNCV),
(0x5d, instructions.IFUNCV),
(0x5e, instructions.JFUNCV),
(0x5f, instructions.FUNCC),
(0x60, instructions.FUNCCW)

将其覆盖到 _OPCODES 元组完成第一处修改。

第二处修改在 ljd/bytecode/instructions.py,我们需要修正从第 97 行开始的对于每条指令的定义顺序:

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
ISLT = _IDef("ISLT", 		T_VAR, 	None, 	T_VAR, 	"if {A} < {D}")
ISGE = _IDef("ISGE", T_VAR, None, T_VAR, "if {A} >= {D}")
ISLE = _IDef("ISLE", T_VAR, None, T_VAR, "if {A} <= {D}")
ISGT = _IDef("ISGT", T_VAR, None, T_VAR, "if {A} > {D}")

ISEQV = _IDef("ISEQV", T_VAR, None, T_VAR, "if {A} == {D}")
ISNEV = _IDef("ISNEV", T_VAR, None, T_VAR, "if {A} ~= {D}")

ISEQS = _IDef("ISEQS", T_VAR, None, T_STR, "if {A} == {D}")
ISNES = _IDef("ISNES", T_VAR, None, T_STR, "if {A} ~= {D}")

ISEQN = _IDef("ISEQN", T_VAR, None, T_NUM, "if {A} == {D}")
ISNEN = _IDef("ISNEN", T_VAR, None, T_NUM, "if {A} ~= {D}")

ISEQP = _IDef("ISEQP", T_VAR, None, T_PRI, "if {A} == {D}")
ISNEP = _IDef("ISNEP", T_VAR, None, T_PRI, "if {A} ~= {D}")

# Constant ops.

KSTR = _IDef("KSTR", T_DST, None, T_STR, "{A} = {D}")
KCDATA = _IDef("KCDATA", T_DST, None, T_CDT, "{A} = {D}")
KSHORT = _IDef("KSHORT", T_DST, None, T_SLIT, "{A} = {D}")
KNUM = _IDef("KNUM", T_DST, None, T_NUM, "{A} = {D}")
KPRI = _IDef("KPRI", T_DST, None, T_PRI, "{A} = {D}")

KNIL = _IDef("KNIL", T_BS, None, T_BS, "{from_A_to_D} = nil")

# Unary test and copy ops

ISTC = _IDef("ISTC", T_DST, None, T_VAR, "{A} = {D}; if {D}")
ISFC = _IDef("ISFC", T_DST, None, T_VAR, "{A} = {D}; if not {D}")

IST = _IDef("IST", None, None, T_VAR, "if {D}")
ISF = _IDef("ISF", None, None, T_VAR, "if not {D}")

ISTYPE = _IDef("ISTYPE", T_VAR, None, T_LIT, "ISTYPE unknow")
ISNUM = _IDef("ISNUM", T_VAR, None, T_LIT, "ISNUM unknow")
# Unary ops

MOV = _IDef("MOV", T_DST, None, T_VAR, "{A} = {D}")
NOT = _IDef("NOT", T_DST, None, T_VAR, "{A} = not {D}")
UNM = _IDef("UNM", T_DST, None, T_VAR, "{A} = -{D}")
LEN = _IDef("LEN", T_DST, None, T_VAR, "{A} = #{D}")

# Returns.

RETM = _IDef("RETM", T_BS, None, T_LIT,
"return {from_A_x_D_minus_one}, ...MULTRES")

RET = _IDef("RET", T_RBS, None, T_LIT,
"return {from_A_x_D_minus_two}")

RET0 = _IDef("RET0", T_RBS, None, T_LIT, "return")
RET1 = _IDef("RET1", T_RBS, None, T_LIT, "return {A}")

# Binary ops

ADDVN = _IDef("ADDVN", T_DST, T_VAR, T_NUM, "{A} = {B} + {C}")
SUBVN = _IDef("SUBVN", T_DST, T_VAR, T_NUM, "{A} = {B} - {C}")
MULVN = _IDef("MULVN", T_DST, T_VAR, T_NUM, "{A} = {B} * {C}")
DIVVN = _IDef("DIVVN", T_DST, T_VAR, T_NUM, "{A} = {B} / {C}")
MODVN = _IDef("MODVN", T_DST, T_VAR, T_NUM, "{A} = {B} % {C}")

ADDNV = _IDef("ADDNV", T_DST, T_VAR, T_NUM, "{A} = {C} + {B}")
SUBNV = _IDef("SUBNV", T_DST, T_VAR, T_NUM, "{A} = {C} - {B}")
MULNV = _IDef("MULNV", T_DST, T_VAR, T_NUM, "{A} = {C} * {B}")
DIVNV = _IDef("DIVNV", T_DST, T_VAR, T_NUM, "{A} = {C} / {B}")
MODNV = _IDef("MODNV", T_DST, T_VAR, T_NUM, "{A} = {C} % {B}")

ADDVV = _IDef("ADDVV", T_DST, T_VAR, T_VAR, "{A} = {B} + {C}")
SUBVV = _IDef("SUBVV", T_DST, T_VAR, T_VAR, "{A} = {B} - {C}")
MULVV = _IDef("MULVV", T_DST, T_VAR, T_VAR, "{A} = {B} * {C}")
DIVVV = _IDef("DIVVV", T_DST, T_VAR, T_VAR, "{A} = {B} / {C}")
MODVV = _IDef("MODVV", T_DST, T_VAR, T_VAR, "{A} = {B} % {C}")

POW = _IDef("POW", T_DST, T_VAR, T_VAR, "{A} = {B} ^ {C} (pow)")
CAT = _IDef("CAT", T_DST, T_RBS, T_RBS,
"{A} = {concat_from_B_to_C}")

# Upvalue and function ops.

UGET = _IDef("UGET", T_DST, None, T_UV, "{A} = {D}")

USETV = _IDef("USETV", T_UV, None, T_VAR, "{A} = {D}")
USETS = _IDef("USETS", T_UV, None, T_STR, "{A} = {D}")
USETN = _IDef("USETN", T_UV, None, T_NUM, "{A} = {D}")
USETP = _IDef("USETP", T_UV, None, T_PRI, "{A} = {D}")

UCLO = _IDef("UCLO", T_RBS, None, T_JMP,
"nil uvs >= {A}; goto {D}")

FNEW = _IDef("FNEW", T_DST, None, T_FUN, "{A} = function {D}")

# Table ops.

TNEW = _IDef("TNEW", T_DST, None, T_LIT, "{A} = new table("
" array: {D_array},"
" dict: {D_dict})")

TDUP = _IDef("TDUP", T_DST, None, T_TAB, "{A} = copy {D}")

GGET = _IDef("GGET", T_DST, None, T_STR, "{A} = _env[{D}]")
GSET = _IDef("GSET", T_VAR, None, T_STR, "_env[{D}] = {A}")

TGETV = _IDef("TGETV", T_DST, T_VAR, T_VAR, "{A} = {B}[{C}]")
TGETS = _IDef("TGETS", T_DST, T_VAR, T_STR, "{A} = {B}.{C}")
TGETB = _IDef("TGETB", T_DST, T_VAR, T_LIT, "{A} = {B}[{C}]")
TGETR = _IDef("TGETR", T_DST, T_VAR, T_VAR, "unkown TGETR")

TSETV = _IDef("TSETV", T_VAR, T_VAR, T_VAR, "{B}[{C}] = {A}")
TSETS = _IDef("TSETS", T_VAR, T_VAR, T_STR, "{B}.{C} = {A}")
TSETB = _IDef("TSETB", T_VAR, T_VAR, T_LIT, "{B}[{C}] = {A}")

TSETM = _IDef("TSETM", T_BS, None, T_NUM,
"for i = 0, MULTRES, 1 do"
" {A_minus_one}[{D_low} + i] = slot({A} + i)")

TSETR = _IDef("TSETR", T_VAR, T_VAR, T_VAR, "unkow TSETR")
# Calls and vararg handling. T = tail call.

CALLM = _IDef("CALLM", T_BS, T_LIT, T_LIT,
"{from_A_x_B_minus_two} = {A}({from_A_plus_one_x_C}, ...MULTRES)")

CALL = _IDef("CALL", T_BS, T_LIT, T_LIT,
"{from_A_x_B_minus_two} = {A}({from_A_plus_one_x_C_minus_one})")

CALLMT = _IDef("CALLMT", T_BS, None, T_LIT,
"return {A}({from_A_plus_one_x_D}, ...MULTRES)")

CALLT = _IDef("CALLT", T_BS, None, T_LIT,
"return {A}({from_A_plus_one_x_D_minus_one})")

ITERC = _IDef("ITERC", T_BS, T_LIT, T_LIT,
"{A}, {A_plus_one}, {A_plus_two} ="
" {A_minus_three}, {A_minus_two}, {A_minus_one};"
" {from_A_x_B_minus_two} ="
" {A_minus_three}({A_minus_two}, {A_minus_one})")

ITERN = _IDef("ITERN", T_BS, T_LIT, T_LIT,
"{A}, {A_plus_one}, {A_plus_two} ="
" {A_minus_three}, {A_minus_two}, {A_minus_one};"
" {from_A_x_B_minus_two} ="
" {A_minus_three}({A_minus_two}, {A_minus_one})")

VARG = _IDef("VARG", T_BS, T_LIT, T_LIT,
"{from_A_x_B_minus_two} = ...")

ISNEXT = _IDef("ISNEXT", T_BS, None, T_JMP,
"Verify ITERN at {D}; goto {D}")

# Loops and branches. I/J = interp/JIT, I/C/L = init/call/loop.

FORI = _IDef("FORI", T_BS, None, T_JMP,
"for {A_plus_three} = {A},{A_plus_one},{A_plus_two}"
" else goto {D}")

JFORI = _IDef("JFORI", T_BS, None, T_JMP,
"for {A_plus_three} = {A},{A_plus_one},{A_plus_two}"
" else goto {D}")

FORL = _IDef("FORL", T_BS, None, T_JMP,
"{A} = {A} + {A_plus_two};"
" if cmp({A}, sign {A_plus_two}, {A_plus_one}) goto {D}")

IFORL = _IDef("IFORL", T_BS, None, T_JMP,
"{A} = {A} + {A_plus_two};"
" if cmp({A}, sign {A_plus_two}, {A_plus_one}) goto {D}")

JFORL = _IDef("JFORL", T_BS, None, T_JMP,
"{A} = {A} + {A_plus_two};"
" if cmp({A}, sign {A_plus_two}, {A_plus_one}) goto {D}")

ITERL = _IDef("ITERL", T_BS, None, T_JMP,
"{A_minus_one} = {A}; if {A} != nil goto {D}")

IITERL = _IDef("IITERL", T_BS, None, T_JMP,
"{A_minus_one} = {A}; if {A} != nil goto {D}")

JITERL = _IDef("JITERL", T_BS, None, T_LIT,
"{A_minus_one} = {A}; if {A} != nil goto {D}")

LOOP = _IDef("LOOP", T_RBS, None, T_JMP, "Noop")
ILOOP = _IDef("ILOOP", T_RBS, None, T_JMP, "Noop")
JLOOP = _IDef("JLOOP", T_RBS, None, T_LIT, "Noop")

JMP = _IDef("JMP", T_RBS, None, T_JMP, " goto {D}")

# Function headers. I/J = interp/JIT, F/V/C = fixarg/vararg/C func.
# Shouldn't be ever seen - they are not stored in raw dump?

FUNCF = _IDef("FUNCF", T_RBS, None, None,
"Fixed-arg function with frame size {A}")

IFUNCF = _IDef("IFUNCF", T_RBS, None, None,
"Interpreted fixed-arg function with frame size {A}")

JFUNCF = _IDef("JFUNCF", T_RBS, None, T_LIT,
"JIT compiled fixed-arg function with frame size {A}")

FUNCV = _IDef("FUNCV", T_RBS, None, None,
"Var-arg function with frame size {A}")

IFUNCV = _IDef("IFUNCV", T_RBS, None, None,
"Interpreted var-arg function with frame size {A}")

JFUNCV = _IDef("JFUNCV", T_RBS, None, T_LIT,
"JIT compiled var-arg function with frame size {A}")

FUNCC = _IDef("FUNCC", T_RBS, None, None,
"C function with frame size {A}")
FUNCCW = _IDef("FUNCCW", T_RBS, None, None,
"Wrapped C function with frame size {A}")

UNKNW = _IDef("UNKNW", T_LIT, T_LIT, T_LIT, "Unknown instruction")

第三处修改在 ljd/ast/builder.py,按照如下方式修改,使得每种指令都落在正确的解析区域中:

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
diff -urNa ljd-old/ast/builder.py ljd-new/ast/builder.py
--- ljd-old/ast/builder.py 2020-05-09 03:43:27.000000000 -0700
+++ ljd-new/ast/builder.py 2022-09-20 02:04:53.955771000 -0700
@@ -276,7 +276,7 @@
last = instructions[-1]
opcode = 256 if len(instructions) == 1 else instructions[-2].opcode

- if opcode <= ins.ISF.opcode:
+ if opcode in (ins.ISLT.opcode, ins.ISGE.opcode, ins.ISLE.opcode, ins.ISGT.opcode, ins.ISEQV.opcode, ins.ISNEV.opcode, ins.ISEQS.opcode, ins.ISNES.opcode, ins.ISEQN.opcode, ins.ISNEN.opcode, ins.ISEQP.opcode, ins.ISNEP.opcode, ins.ISTC.opcode, ins.ISFC.opcode, ins.IST.opcode, ins.ISF.opcode):
assert last.opcode != ins.ISNEXT.opcode
return _build_conditional_warp(state, last_addr, instructions)
else:
@@ -507,7 +507,7 @@
expression = _build_unary_expression(state, addr, instruction)

# Binary assignment operators (A = B op C)
- elif opcode <= ins.POW.opcode:
+ elif ins.ADDVN.opcode <= opcode <= ins.POW.opcode:
expression = _build_binary_expression(state, addr, instruction)

# Concat assignment type (A = B .. B + 1 .. ... .. C - 1 .. C)
@@ -515,7 +515,7 @@
expression = _build_concat_expression(state, addr, instruction)

# Constant assignment operators except KNIL, which is weird anyway
- elif opcode <= ins.KPRI.opcode:
+ elif ins.KSTR.opcode <= opcode <= ins.KPRI.opcode:
expression = _build_const_expression(state, addr, instruction)

elif opcode == ins.UGET.opcode:
@@ -524,7 +524,7 @@
elif opcode == ins.USETV.opcode:
expression = _build_slot(state, addr, instruction.CD)

- elif opcode <= ins.USETP.opcode:
+ elif ins.USETS.opcode <= opcode <= ins.USETP.opcode:
expression = _build_const_expression(state, addr, instruction)

elif opcode == ins.FNEW.opcode:

在项目根目录运行 python main.py -f <script_name>.luac64 > <script_name>.lua 可以得到单个文件的反编译结果,也可以通过 -r 选项指定目录批量反编译。另外,-d 选项用于指定输出目录,-e 选项用于指定文件后缀。

代码分析

首先通过逆向思维来找到核心验证代码,在 GameScene.luacollisionH 函数中可以找到游戏胜利的判断语句:

winning_condition

即当碰撞到板栗仔时,主角的身体类型应该是 NORMAL 状态(初始状态为 SMALL)。在 collisionV 函数中可以找到修改主角 bodyType 属性的调用:

change_body_type

不过需要主角吃到蘑菇才能被调用。在 GameMap.lua 中,可以找到 isMarioEatMushroom 的定义,其紧挨着的上一个函数是 showNewMushroom,看名字应该是用于生成蘑菇的。查找一下 showNewMushroom 的引用,可以发现它在 breakBrick 中被调用:

check_trigger

上图中框起来的部分就是触发输入校验的逻辑,它将所有普通方块上的字符拼接成长度为 32 的字符串,存放在变量 slot5 中。这里反编译结果存在一些偏差,应该是下面这样才对:

1
2
3
4
slot5 = ""
for slot9 = 0, 31 do
slot5 = slot5 .. slot0.labelList["input" .. tostring(slot9)]:getString()
end

不过我们大致能猜到它的意思,拼接好后传递给 Util.lua 文件的 create 函数,得到一个 Util 模块实例 slot6。接着调用 OoO 函数和 oOo 函数,如果后者返回 true,则显示蘑菇。

所以问题简化成了如何令 Util.oOo 返回 true,这要求我们着重分析 Util.lua 文件。从现在开始,这道题就变成一道常规逆向题,还是给了源码的那种,稍微还原一下符号就能发现它是一个小型虚拟机。这里需要注意,除了 create 函数外,其他函数的第一个参数(slot0)都相当于 self,你可以简单理解为类静态函数和类成员函数的区别。

这里就不过多地去分析虚拟机的实现了,下面将 Util.lua 的源码奉上:

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
local Util=class("Util",function ()
return cc.Node:create()
end)

Util.slot = {{}, {}, nil, nil, nil, nil}

function Util:ctor()
end

function Util.create(input)
local util=Util.new()
local target={ 94, 106, 91, 110, 86, 100, 82, 20, 32, 20, 80, 21, 83, 107, 88, 98, 81, 19, 79, 10, 49, 117, 68, 120, 61, 13, 75, 115, 48, 8, 76, 123 }
util.slot[1]={ 65, 30, 37, 10, 50, 0, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 0, 50, 191, 37, 10, 50, 1, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 1, 50, 192, 37, 10, 50, 2, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 2, 50, 193, 37, 10, 50, 3, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 3, 50, 194, 37, 10, 50, 4, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 4, 50, 195, 37, 10, 50, 5, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 5, 50, 196, 37, 10, 50, 6, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 6, 50, 197, 37, 10, 50, 7, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 7, 50, 198, 37, 10, 50, 8, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 8, 50, 199, 37, 10, 50, 9, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 9, 50, 200, 37, 10, 50, 10, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 10, 50, 201, 37, 10, 50, 11, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 11, 50, 202, 37, 10, 50, 12, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 12, 50, 203, 37, 10, 50, 13, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 13, 50, 204, 37, 10, 50, 14, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 14, 50, 205, 37, 10, 50, 15, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 15, 50, 206, 37, 10, 50, 16, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 16, 50, 207, 37, 10, 50, 17, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 17, 50, 208, 37, 10, 50, 18, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 18, 50, 209, 37, 10, 50, 19, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 19, 50, 210, 37, 10, 50, 20, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 20, 50, 211, 37, 10, 50, 21, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 21, 50, 212, 37, 10, 50, 22, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 22, 50, 213, 37, 10, 50, 23, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 23, 50, 214, 37, 10, 50, 24, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 24, 50, 215, 37, 10, 50, 25, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 25, 50, 216, 37, 10, 50, 26, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 26, 50, 217, 37, 10, 50, 27, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 27, 50, 218, 37, 10, 50, 28, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 28, 50, 219, 37, 10, 50, 29, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 29, 50, 220, 37, 10, 50, 30, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 30, 50, 221, 37, 10, 50, 31, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 31, 144, 144, 144, 144 }
for i=1,256 do
util.slot[2][i]=0
end
for i=33,65 do
util.slot[2][i]=string.byte(input, i-32)
util.slot[2][i+96]=target[i-32]
end
util.slot[3] = 0
util.slot[4] = 0
util.slot[5] = 1
util.slot[6] = 0
return util
end

function Util:OoO()
while (true)
do
opcode=self.slot[1][self.slot[5]]
if opcode==0x11 then
local operand = self.slot[1][self.slot[5]+1]
local inputi = self.slot[1][33+operand]
self.slot[1][33+operand] = self:lil(inputi + 1, 0xFF)
self.slot[5] = self.slot[5] + 2

elseif opcode==0x21 then
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[operand-7]
self.slot[operand-7] = self:lil(regi + 1, 0xFF)
self.slot[5] = self.slot[5] + 2

elseif opcode==0x41 then
self.slot[6] = self.slot[6] + 1
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[6]
self.slot[2][regi] = operand
self.slot[5] = self.slot[5] + 2

elseif opcode==0x12 then
local operand = self.slot[1][self.slot[5]+1]
local inputi = self.slot[1][33+operand]
self.slot[1][33+operand] = self:lil(inputi - 1, 0xFF)
self.slot[5] = self.slot[5] + 2

elseif opcode==0x23 then
local operand1 = self.slot[1][self.slot[5]+1]
local operand2 = self.slot[1][self.slot[5]+2]
local regi = self.slot[operand1-7]

self.slot[operand1-7] = self:ili(regi,operand2)
self.slot[5] = self.slot[5] + 3

elseif opcode==0x32 then
self.slot[6] = self.slot[6] + 1
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[6]
self.slot[2][regi] = self.slot[2][33+operand]
self.slot[5] = self.slot[5] + 2

elseif opcode==0x24 then
local operand1 = self.slot[1][self.slot[5]+1]
local operand2 = self.slot[1][self.slot[5]+2]
local reg1 = self.slot[operand1-7]
local reg2 = self.slot[operand2-7]

self.slot[operand1-7] = self:ili(reg1,reg2)
self.slot[5] = self.slot[5] + 3

elseif opcode==0x31 then
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[6]
self.slot[2][224+operand] = self.slot[2][regi]
self.slot[6] = self.slot[6] - 1
self.slot[5] = self.slot[5] + 2

elseif opcode==0x22 then
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[operand-7]
self.slot[operand-7] = self:lil(regi - 1, 0xFF)
self.slot[5] = self.slot[5] + 2

elseif opcode==0x42 then
self.slot[6] = self.slot[6] + 1
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[6]
self.slot[2][regi] = self.slot[operand-7]
self.slot[5] = self.slot[5] + 2

elseif opcode==0x25 then
local operand = self.slot[1][self.slot[5]+1]
local regi = self.slot[6]
self.slot[operand-7] = self.slot[2][regi]
self.slot[6] = self.slot[6] - 1
self.slot[5] = self.slot[5] + 2

elseif opcode==0x90 then
return
else
break
end
end
end

function Util:oOo()
for i=1,32 do
if self.slot[2][i+128] ~= self.slot[2][223+i] then
return false
end
end
return true
end

function Util:ili(num1,num2)
local tmp1 = num1
local tmp2 = num2
local str = ""
repeat
local s1 = tmp1 % 2
local s2 = tmp2 % 2
if s1 == s2 then
str = "0"..str
else
str = "1"..str
end
tmp1 = math.modf(tmp1/2)
tmp2 = math.modf(tmp2/2)
until(tmp1 == 0 and tmp2 == 0)
return tonumber(str,2)
end

function Util:lil(num1,num2)
local tmp1 = num1
local tmp2 = num2
local str = ""
repeat
local s1 = tmp1 % 2
local s2 = tmp2 % 2
if s1 == s2 then
if s1 == 1 then
str = "1"..str
else
str = "0"..str
end
else
str = "0"..str
end
tmp1 = math.modf(tmp1/2)
tmp2 = math.modf(tmp2/2)
until(tmp1 == 0 and tmp2 == 0)
return tonumber(str,2)
end

return Util

其中 ili 是异或运算(^),lil 是按位与(&),OoORunVM 函数,oOoCheck 函数。

最终解题

分析好每种 opcode 的功能及模拟的内存和寄存器后,使用 python 模拟虚拟机执行并输出伪代码:

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
inst = [65, 30, 37, 10, 50, 0, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 0, 50, 191, 37, 10, 50, 1, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 1, 50, 192, 37, 10, 50, 2, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 2, 50, 193, 37, 10, 50, 3, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 3, 50, 194, 37, 10, 50, 4, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 4, 50, 195, 37, 10, 50, 5, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 5, 50, 196, 37, 10, 50, 6, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 6, 50, 197, 37, 10, 50, 7, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 7, 50, 198, 37, 10, 50, 8, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 8, 50, 199, 37, 10, 50, 9, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 9, 50, 200, 37, 10, 50, 10, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 10, 50, 201, 37, 10, 50, 11, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 11, 50, 202, 37, 10, 50, 12, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 12, 50, 203, 37, 10, 50, 13, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 13, 50, 204, 37, 10, 50, 14, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 14, 50, 205, 37, 10, 50, 15, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 15, 50, 206, 37, 10, 50, 16, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 16, 50, 207, 37, 10, 50, 17, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 17, 50, 208, 37, 10, 50, 18, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 18, 50, 209, 37, 10, 50, 19, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 19, 50, 210, 37, 10, 50, 20, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 20, 50, 211, 37, 10, 50, 21, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 21, 50, 212, 37, 10, 50, 22, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 22, 50, 213, 37, 10, 50, 23, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 23, 50, 214, 37, 10, 50, 24, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 24, 50, 215, 37, 10, 50, 25, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 25, 50, 216, 37, 10, 50, 26, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 26, 50, 217, 37, 10, 50, 27, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 27, 50, 218, 37, 10, 50, 28, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 28, 50, 219, 37, 10, 50, 29, 37, 11, 36, 10, 11, 33, 10, 66, 10, 49, 29, 50, 220, 37, 10, 50, 30, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 30, 50, 221, 37, 10, 50, 31, 37, 11, 36, 10, 11, 34, 10, 66, 10, 49, 31, 144, 144, 144, 144]
vm_ip = 0
temp_reg = 0

while vm_ip < len(inst):

opcode = inst[vm_ip]
operand1 = inst[vm_ip + 1]

if opcode == 0x11:
code = f"(byte) input[{operand1}]++"
vm_ip += 2

elif opcode == 0x12:
code = f"(byte) input[{operand1}]--"
vm_ip += 2

elif opcode == 0x21:
code = f"reg{operand1 - 8}++"
vm_ip += 2

elif opcode == 0x22:
code = f"reg{operand1 - 8}--"
vm_ip += 2

elif opcode == 0x23:
operand2 = inst[vm_ip + 2]
code = f"reg{operand1 - 8} ^= {operand2}"
vm_ip += 3

elif opcode == 0x24:
operand2 = inst[vm_ip + 2]
code = f"reg{operand1 - 8} ^= reg{operand2 - 8}"
vm_ip += 3

elif opcode == 0x25:
code = f"reg{operand1 - 8} = memory[{temp_reg}]"
temp_reg -= 1
vm_ip += 2

elif opcode == 0x31:
code = f"input[{operand1}] = (byte) memory[{temp_reg}]"
temp_reg -= 1
vm_ip += 2

elif opcode == 0x32:
temp_reg += 1
code = f"memory[{temp_reg}] = (byte) input[{operand1}]"
vm_ip += 2

elif opcode == 0x41:
temp_reg += 1
code = f"memory[{temp_reg}] = {operand1}"
vm_ip += 2

elif opcode == 0x42:
temp_reg += 1
code = f"memory[{temp_reg}] = reg{operand1 - 8}"
vm_ip += 2

else:
break

print(code)

将输出翻译为高级语言:

1
2
3
4
5
6
7
8
9
10
11
buf = list(map(ord, "?" * 32))

buf[0] = (buf[0] ^ 30) - 1

for i in range(1, 32):
buf[i] ^= buf[i - 1]
if i % 2 == 0 or i == 31:
buf[i] -= 1
else:
buf[i] += 1
buf[i] &= 0xFF

故有解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cipher = [ 94, 106, 91, 110, 86, 100, 82, 20, 32, 20, 80, 21, 83, 107, 88, 98, 81, 19, 79, 10, 49, 117, 68, 120, 61, 13, 75, 115, 48, 8, 76, 123 ]

for i in range(31, 0, -1):
if i % 2 == 0 or i == 31:
cipher[i] += 1
else:
cipher[i] -= 1
cipher[i] &= 0xFF
cipher[i] ^= cipher[i - 1]

cipher[0] = (cipher[0] + 1) ^ 30

flag = ''.join(map(chr, cipher))
print(flag)

在游戏里去顶砖块(上面一排是前 16 个字符,下面一排是后 16 个字符),最后再顶问号块,可以得到蘑菇:

show_mushroom

吃了蘑菇去碰板栗仔,墙就会消失,触碰旗帜通关成功:

win_game