0%

xctf-final-SuperFlagio

SuperFlagio

在做xctf决赛的题时,明显感觉跟平常的题不是一个等级,当时选了lambda来做,发现并不是很好做,那个题真就做了一天,赛后复现一下SuperFlagio这个题。

所需知识

Lua

lua是小巧脚本语言。其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。

lua,luac,luajit三种文件区别:

lua是明文代码,可看见代码

luac是lua编译后的字节码,头文件为0x1B 0X4C 0X75 0X61 版本号构成

lua虚拟机能够直接解析lua和luac脚本文件,而luajit是另一个文件lua的实现版本,luajit比lua和luac更高效,文件头是0X18 0X4C 0X4A

lua符号恢复

首先搜索到lua版本,然后编译对应版本

1
2
3
4
5
6
7
8
9
curl -R -O http://www.lua.org/ftp/lua-5.3.4.tar.gz

tar zxf lua-5.3.4.tar.gz

cd lua-5.3.4

sudo make linux test

sudo make install

编译完成在bin文件中找到lua文件,然后ida载入lua,关闭并保存得到i64,然后打开要恢复的lua文件,ctrl+6打开bindiff,选择diff database,将confidence和similarity全部改成0.5,点ok,然后再点ctrl+6,恢复成功。

luac反编译

unluac

unluac1.2.2.155是比较稳定的一个版本,命令:

1
java -jar unluac.jar main.luac

luadec

安装luadec方法:

1
2
3
4
5
6
7
git clone https://github.com/viruscamp/luadec
cd luadec
git submodule update --init lua-5.1
cd lua-5.1
make linux
cd ../luadec
make LUAVER=5.1

版本切换:

1
2
3
4
5
6
git submodule update --init lua-5.3
cd lua-5.3
make linux
cd ../luadec
make clean
make LUAVER=5.3

luajit反编译

luajit-decomp

推荐使用ljd,还有一种是luajit-decomp不推荐使用(比较难看)

首先确定luajit版本,通过字符串搜索可知。

下载luajit对应版本并编译,直接在文件夹根目录下:

1
mingw32-make

然后复制编译后的LuaJIT-2.1.0-beta2\src路径下的lua51.dll、luajit.exe文件和jit文件夹覆盖到luajit-decomp目录中。将原脚本默认是只反编译当前目录下名为test.lua的文件,所以我们要重命名我们要反编译的文件为test.lua,接着运行decoder_new.exe,最终生成test.asm out.lua out2.lua,out2.lua就是我们要的文件

ljd

在根目录下使用

1
python main.py -f <script_name>.luac64 > <script_name>.lua

即可编译。

luajit如何编译成arm文件?

1
2
3
4
git clone https://github.com/LuaJIT/LuaJIT.git
cd LuaJIT
git checkout v2.1.0-beta3
make -j `nproc`

即可

目前遇到了三种类型的lua题目,第一种是lua加载时魔改了lua chunk,第二种是lua加载魔改opcode,第三种是luajit加载魔改opcode,显然还有一种是luajit加载魔改lua chunk的情况没有出现。

首先给了一个apk文件,用jadx打开发现OnCreate中有很多检测,比如:

image-20230404170743487

下面是java层,Cocos2dxUtil.lowEngineVersion 对是否root进行检测:

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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
package org.cocos2dx.lib;

import android.os.Build;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;

public class Cocos2dxUtil {
private static String TAG = "org.cocos2dx.lib.Cocos2dxUtil";

public static boolean a() {
String str = Build.TAGS;
return str != null && str.contains("test-keys");
}

public static boolean b() {
try {
return new File("/system/app/SuperSU/SuperSU.apk").exists();
} catch (Exception unused) {
return false;
}
}

public static boolean c() {
String[] strArr = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/"};
for (int i = 0; i < strArr.length; i++) {
try {
File file = new File(strArr[i] + "su");
if (file != null && file.exists()) {
return true;
}
} catch (Exception unused) {
}
}
return false;
}

public static boolean d() {
return j(new String[]{"/system/xbin/which", "su"}) != null;
}

/* JADX WARNING: Code restructure failed: missing block: B:15:0x001e, code lost:
return false;
*/
/* JADX WARNING: Exception block dominator not found, dom blocks: [] */
/* Code decompiled incorrectly, please refer to instructions dump. */
public static synchronized boolean e() {
/*
java.lang.Class<org.cocos2dx.lib.Cocos2dxUtil> r0 = org.cocos2dx.lib.Cocos2dxUtil.class
monitor-enter(r0)
r1 = 2
r2 = 0
java.lang.String[] r1 = new java.lang.String[r1] // Catch:{ Exception -> 0x001d, all -> 0x001a }
java.lang.String r3 = "busybox"
r1[r2] = r3 // Catch:{ Exception -> 0x001d, all -> 0x001a }
java.lang.String r3 = "df"
r4 = 1
r1[r4] = r3 // Catch:{ Exception -> 0x001d, all -> 0x001a }
java.util.ArrayList r1 = j(r1) // Catch:{ Exception -> 0x001d, all -> 0x001a }
if (r1 == 0) goto L_0x0018
monitor-exit(r0)
return r4
L_0x0018:
monitor-exit(r0)
return r2
L_0x001a:
r1 = move-exception
monitor-exit(r0)
throw r1
L_0x001d:
monitor-exit(r0)
return r2
*/
throw new UnsupportedOperationException("Method not decompiled: org.cocos2dx.lib.Cocos2dxUtil.e():boolean");
}

/* JADX WARNING: Code restructure failed: missing block: B:16:0x0020, code lost:
return false;
*/
/* JADX WARNING: Exception block dominator not found, dom blocks: [] */
/* Code decompiled incorrectly, please refer to instructions dump. */
public static synchronized boolean f() {
/*
java.lang.Class<org.cocos2dx.lib.Cocos2dxUtil> r0 = org.cocos2dx.lib.Cocos2dxUtil.class
monitor-enter(r0)
r1 = 0
java.lang.String r2 = "test_ok"
java.lang.String r3 = "/data/su_test"
h(r3, r2) // Catch:{ Exception -> 0x001f, all -> 0x001c }
java.lang.String r3 = "/data/su_test"
java.lang.String r3 = i(r3) // Catch:{ Exception -> 0x001f, all -> 0x001c }
boolean r2 = r2.equals(r3) // Catch:{ Exception -> 0x001f, all -> 0x001c }
if (r2 == 0) goto L_0x001a
r1 = 1
monitor-exit(r0)
return r1
L_0x001a:
monitor-exit(r0)
return r1
L_0x001c:
r1 = move-exception
monitor-exit(r0)
throw r1
L_0x001f:
monitor-exit(r0)
return r1
*/
throw new UnsupportedOperationException("Method not decompiled: org.cocos2dx.lib.Cocos2dxUtil.f():boolean");
}

/* JADX DEBUG: Failed to insert an additional move for type inference into block B:51:? */
/* JADX DEBUG: Failed to insert an additional move for type inference into block B:43:0x0050 */
/* JADX DEBUG: Multi-variable search result rejected for r1v0, resolved type: boolean */
/* JADX DEBUG: Multi-variable search result rejected for r1v1, resolved type: boolean */
/* JADX DEBUG: Multi-variable search result rejected for r1v2, resolved type: boolean */
/* JADX WARN: Multi-variable type inference failed */
/* JADX WARN: Type inference failed for: r1v3, types: [java.lang.Throwable] */
/* JADX WARN: Type inference failed for: r1v8 */
/* JADX WARN: Type inference failed for: r1v9 */
/* JADX WARN: Type inference failed for: r1v10 */
/* JADX WARN: Type inference failed for: r1v11 */
/* JADX WARNING: Can't wrap try/catch for region: R(5:30|(0)|36|37|38) */
/* JADX WARNING: Code restructure failed: missing block: B:45:0x0054, code lost:
r1 = move-exception;
*/
/* JADX WARNING: Code restructure failed: missing block: B:49:0x005b, code lost:
throw r1;
*/
/* JADX WARNING: Failed to process nested try/catch */
/* JADX WARNING: Missing exception handler attribute for start block: B:37:0x004c */
/* JADX WARNING: Removed duplicated region for block: B:34:0x0046 A[SYNTHETIC, Splitter:B:34:0x0046] */
/* JADX WARNING: Removed duplicated region for block: B:43:0x0050 A[SYNTHETIC, Splitter:B:43:0x0050] */
/* Code decompiled incorrectly, please refer to instructions dump. */
public static synchronized boolean g() {
/*
java.lang.Class<org.cocos2dx.lib.Cocos2dxUtil> r0 = org.cocos2dx.lib.Cocos2dxUtil.class
monitor-enter(r0)
r1 = 0
r2 = 0
java.lang.Runtime r3 = java.lang.Runtime.getRuntime() // Catch:{ Exception -> 0x004d, all -> 0x0042 }
java.lang.String r4 = "su"
java.lang.Process r3 = r3.exec(r4) // Catch:{ Exception -> 0x004d, all -> 0x0042 }
java.io.DataOutputStream r4 = new java.io.DataOutputStream // Catch:{ Exception -> 0x004e, all -> 0x0040 }
java.io.OutputStream r5 = r3.getOutputStream() // Catch:{ Exception -> 0x004e, all -> 0x0040 }
r4.<init>(r5) // Catch:{ Exception -> 0x004e, all -> 0x0040 }
java.lang.String r2 = "exit\n"
r4.writeBytes(r2) // Catch:{ Exception -> 0x003e, all -> 0x003b }
r4.flush() // Catch:{ Exception -> 0x003e, all -> 0x003b }
int r2 = r3.waitFor() // Catch:{ Exception -> 0x003e, all -> 0x003b }
if (r2 != 0) goto L_0x0031
r1 = 1
if (r4 == 0) goto L_0x002c
r4.close() // Catch:{ Exception -> 0x002f }
L_0x002c:
r3.destroy() // Catch:{ Exception -> 0x002f }
L_0x002f:
monitor-exit(r0)
return r1
L_0x0031:
if (r4 == 0) goto L_0x0036
r4.close() // Catch:{ Exception -> 0x0039 }
L_0x0036:
r3.destroy() // Catch:{ Exception -> 0x0039 }
L_0x0039:
monitor-exit(r0)
return r1
L_0x003b:
r1 = move-exception
r2 = r4
goto L_0x0044
L_0x003e:
r2 = r4
goto L_0x004e
L_0x0040:
r1 = move-exception
goto L_0x0044
L_0x0042:
r1 = move-exception
r3 = r2
L_0x0044:
if (r2 == 0) goto L_0x0049
r2.close() // Catch:{ Exception -> 0x004c }
L_0x0049:
r3.destroy() // Catch:{ Exception -> 0x004c }
L_0x004c:
throw r1 // Catch:{ all -> 0x0054 }
L_0x004d:
r3 = r2
L_0x004e:
if (r2 == 0) goto L_0x0056
r2.close() // Catch:{ Exception -> 0x005c }
goto L_0x0056
L_0x0054:
r1 = move-exception
goto L_0x005a
L_0x0056:
r3.destroy() // Catch:{ Exception -> 0x005c }
goto L_0x005c
L_0x005a:
monitor-exit(r0)
throw r1
L_0x005c:
monitor-exit(r0)
return r1
*/
throw new UnsupportedOperationException("Method not decompiled: org.cocos2dx.lib.Cocos2dxUtil.g():boolean");
}

public static Boolean h(String str, String str2) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(str);
fileOutputStream.write(str2.getBytes());
fileOutputStream.close();
return true;
} catch (Exception unused) {
return false;
}
}

public static String i(String str) {
try {
FileInputStream fileInputStream = new FileInputStream(new File(str));
byte[] bArr = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while (true) {
int read = fileInputStream.read(bArr);
if (read <= 0) {
return new String(byteArrayOutputStream.toByteArray());
}
byteArrayOutputStream.write(bArr, 0, read);
}
} catch (Exception unused) {
return null;
}
}

public static ArrayList<String> j(String[] strArr) {
ArrayList<String> arrayList = new ArrayList<>();
try {
Process exec = Runtime.getRuntime().exec(strArr);
new BufferedWriter(new OutputStreamWriter(exec.getOutputStream()));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
while (true) {
try {
String readLine = bufferedReader.readLine();
if (readLine == null) {
break;
}
arrayList.add(readLine);
} catch (Exception unused) {
}
}
return arrayList;
} catch (Exception unused2) {
return null;
}
}

public static boolean lowEngineVersion() {
return a() || b() || c() || d() || e() || f() || g();
}
}

如何绕过root检测,如果不绕过是没法用root手机打开的,只需要面具里装shamiko插件就行:https://zhuanlan.zhihu.com/p/541299125

在native层中存在pthread_create检查是否有idaserver和frida server。

这个题显然是用cocos2d-x引擎和lua开发的,这个游戏是将flag输对才能通关。传统的cocos2d-x luac加密是xxtea,密钥很好找,而这个题修改了源码,加密不一样了。一般cocos-x游戏脚本被存放在asserts/src目录下,但是该目录下文件名称全部被加密了。

如何将luajit编译成arm架构的libluajit.so呢?

1
2
3
4
git clone https://github.com/LuaJIT/LuaJIT.git
cd LuaJIT
git checkout v2.1.0-beta3
make -j `nproc`

然后生成的lib.so文件在src目录中,用bindiff还原函数名称,发现lua加载函数luaL_loadbuffer 的地址为 0xAC4E9C,该函数的原型为 LUALIB_API int luaL_loadbuffer(lua_State *L, const char *buf, size_t size, const char *name),第二个参数指向当前加载字节码文件的二进制内容,第三个参数指明 buf 的大小,第四个参数为模块路径名。

采用hook该函数,打印第四个参数,可以获得加载的lua模块路径名

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) {}
})
1
2
3
4
Load: scene/GameScene.pyc
Load: core/GameMap.pyc
Load: entity/Enemy.pyc
Load: entity/Mario.pyc

中间遇到了很多问题,还没解决。

把加载的文件dump出来:

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

然后发现这五个lua文件全部是64位luadjit文件,前面已经了解了如何反编译,但是这个题不能直接反编译,因为luajit加载函数改变了,该题是将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字段,这个字段指向了globle_State结构体,保存着luajit全局信息,通过这个可以找到GG_State结构体,这个结构体保存着dispatch字段,这个字段是一个数组,这个数组维护luajit内部指令跳转表,每条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;

lua_loadbuffer函数是加载运行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
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;
}

首先在调用lua_loadx前会保存当前变量和长度,然后通过调用lj_vm_cpcall函数来启动虚拟机。但接下来是比较难分析的,因为该函数是汇编写法,总之,该汇编基本原理如下:

取指令通过ldr INSw, [PC], #4 实现,PC和INSw定义在文件头,对应x21和x16寄存器。这句汇编的意思就是从 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          x9 = x22 + (((unsigned int)(x16 & 0xFF)) << 3)
ldr TMP0, [TMP1, #GG_G2DISP] x9 加上常数 #GG_G2DISP 后指向的地址空间里取出 8 字节放到 x8
br TMP0 跳转到处理例程去执行

这里的 TMP0 和 TMP1 分别是 x8 和 x9 寄存器,INS 就是 x16 寄存器。

其中 GG_G2DISP 定义在 lj_dispatch.h 中:

1
2
3
4
5
6
7
8
9
10
11
C
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:

image-20230410215934466

因此可以进一步理解三条汇编:首先通过GL寄存器加上0xf30找到dispatch数组,该数组每一项(8字节)都是处理例程的指针,元素下标位该处理例程的opcode,在此基础上加上opcode*8得到当前ocpode处理例程。

从lj_bc.h可以知道该版本下的luajit有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
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

#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) \
_(ISEQS, var, ___, str, eq) \
_(ISNES, var, ___, str, eq) \
_(ISEQN, var, ___, num, eq) \
_(ISNEN, var, ___, num, eq) \
_(ISEQP, var, ___, pri, eq) \
_(ISNEP, var, ___, pri, eq) \
\
/* Unary test and copy ops. */ \
_(ISTC, dst, ___, var, ___) \
_(ISFC, dst, ___, var, ___) \
_(IST, ___, ___, var, ___) \
_(ISF, ___, ___, var, ___) \
_(ISTYPE, var, ___, lit, ___) \
_(ISNUM, var, ___, lit, ___) \
\
/* Unary ops. */ \
_(MOV, dst, ___, var, ___) \
_(NOT, dst, ___, var, ___) \
_(UNM, dst, ___, var, unm) \
_(LEN, dst, ___, var, len) \
\
/* Binary ops. ORDER OPR. VV last, POW must be next. */ \
_(ADDVN, dst, var, num, add) \
_(SUBVN, dst, var, num, sub) \
_(MULVN, dst, var, num, mul) \
_(DIVVN, dst, var, num, div) \
_(MODVN, dst, var, num, mod) \
\
_(ADDNV, dst, var, num, add) \
_(SUBNV, dst, var, num, sub) \
_(MULNV, dst, var, num, mul) \
_(DIVNV, dst, var, num, div) \
_(MODNV, dst, var, num, mod) \
\
_(ADDVV, dst, var, var, add) \
_(SUBVV, dst, var, var, sub) \
_(MULVV, dst, var, var, mul) \
_(DIVVV, dst, var, var, div) \
_(MODVV, dst, var, var, mod) \
\
_(POW, dst, var, var, pow) \
_(CAT, dst, rbase, rbase, concat) \
\
/* Constant ops. */ \
_(KSTR, dst, ___, str, ___) \
_(KCDATA, dst, ___, cdata, ___) \
_(KSHORT, dst, ___, lits, ___) \
_(KNUM, dst, ___, num, ___) \
_(KPRI, dst, ___, pri, ___) \
_(KNIL, base, ___, base, ___) \
\
/* Upvalue and function ops. */ \
_(UGET, dst, ___, uv, ___) \
_(USETV, uv, ___, var, ___) \
_(USETS, uv, ___, str, ___) \
_(USETN, uv, ___, num, ___) \
_(USETP, uv, ___, pri, ___) \
_(UCLO, rbase, ___, jump, ___) \
_(FNEW, dst, ___, func, gc) \
\
/* Table ops. */ \
_(TNEW, dst, ___, lit, gc) \
_(TDUP, dst, ___, tab, gc) \
_(GGET, dst, ___, str, index) \
_(GSET, var, ___, str, newindex) \
_(TGETV, dst, var, var, index) \
_(TGETS, dst, var, str, index) \
_(TGETB, dst, var, lit, index) \
_(TGETR, dst, var, var, index) \
_(TSETV, var, var, var, newindex) \
_(TSETS, var, var, str, newindex) \
_(TSETB, var, var, lit, newindex) \
_(TSETM, base, ___, num, newindex) \
_(TSETR, var, var, var, newindex) \
\
/* Calls and vararg handling. T = tail call. */ \
_(CALLM, base, lit, lit, call) \
_(CALL, base, lit, lit, call) \
_(CALLMT, base, ___, lit, call) \
_(CALLT, base, ___, lit, call) \
_(ITERC, base, lit, lit, call) \
_(ITERN, base, lit, lit, call) \
_(VARG, base, lit, lit, ___) \
_(ISNEXT, base, ___, jump, ___) \
\
/* Returns. */ \
_(RETM, base, ___, lit, ___) \
_(RET, rbase, ___, lit, ___) \
_(RET0, rbase, ___, lit, ___) \
_(RET1, rbase, ___, lit, ___) \
\
/* Loops and branches. I/J = interp/JIT, I/C/L = init/call/loop. */ \
_(FORI, base, ___, jump, ___) \
_(JFORI, base, ___, jump, ___) \
\
_(FORL, base, ___, jump, ___) \
_(IFORL, base, ___, jump, ___) \
_(JFORL, base, ___, lit, ___) \
\
_(ITERL, base, ___, jump, ___) \
_(IITERL, base, ___, jump, ___) \
_(JITERL, base, ___, lit, ___) \
\
_(LOOP, rbase, ___, jump, ___) \
_(ILOOP, rbase, ___, jump, ___) \
_(JLOOP, rbase, ___, lit, ___) \
\
_(JMP, rbase, ___, jump, ___) \
\
/* Function headers. I/J = interp/JIT, F/V/C = fixarg/vararg/C func. */ \
_(FUNCF, rbase, ___, ___, ___) \
_(IFUNCF, rbase, ___, ___, ___) \
_(JFUNCF, rbase, ___, lit, ___) \
_(FUNCV, rbase, ___, ___, ___) \
_(IFUNCV, rbase, ___, ___, ___) \
_(JFUNCV, rbase, ___, lit, ___) \
_(FUNCC, rbase, ___, ___, ___) \
_(FUNCCW, rbase, ___, ___, ___)

根据上述规则,我们可以用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) {}
})

得到所有地址:

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名称,这样我们就将 opcode 的顺序找到了,下一步就该对之前 dump 下来的 luac64 文件进行反编译了。

综上所述,总结一下:该apk是通过cocos2d引擎lua开发的游戏,这个题的lua通过luajit加载器加载到了so文件中,而且这个luajit加载器的源码被改过。所以想反编译就必须找到正确的opcode顺序,可以通过hook,找到每个opcode处理地址,然后手动对比dump出的地址处的汇编和源码汇编并修改函数名称,再用idapython dump出对应顺序,最后修改 ljd/rawdump/luajit/v2_1/luajit_opcode.py_OPCODES元组,ljd/bytecode/instructions.py处的指令和ljd/ast/builder.py指令解析。

该指令方法如下:

1、打开ljd/rawdump/luajit/v2_1/luajit_opcode.py,修改_OPCODES如下:

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)

2、修改ljd/bytecode/instructions.py的第97行开始,按照1的函数顺序

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

3、修改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
40
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:

然后在luajit-decompiler-master根目录运行python main.py -f <script_name>.luac64 > <script_name>.lua 可以得到单个文件的反编译结果

image-20230411213234681

最后就可以看到lua代码,而不是汇编(这点还是很友善地)

代码分析

在GameScene.lua中有判断胜利的条件:

1
2
3
4
5
6
7
8
9
10
11
if (slot0.mario.face == MarioState.RIGHT or slot3 == MarioState.JUMP_RIGHT) and slot0.mainMap.enemyList[1] and slot4.position.x <= slot2.x + slot0.marioSize.width and slot2.y <= slot4.position.y + 10 then
if slot0.mario.bodyType == MarioBodyState.NORMAL then
slot0.win = true

slot0.mainMap:onWin()
else
slot0:doMarioDie()

return
end
end

首先是要求马里奥的bodyType大小是normal,正常初始是small,发现在collisionV 函数中

1
2
3
4
5
for slot6 = 6, slot0.marioSize.width - 7 do
if slot0.mainMap:isMarioEatMushroom(slot0.mainMap:pointToTileCoord(cc.p(slot1.x - slot0.marioSize.width / 2 + slot6, slot1.y + slot0.marioSize.height))) then
slot0.mario:changeForGotMushroom()
cc.SimpleAudioEngine:getInstance():playEffect(music_eatmushroomOrFlower)
end

存在changeForGotMushroom(),猜测是吃蘑菇变大,在GameMap.lua中有isMarioEatMushroom来判断是否吃蘑菇,在这个函数上方有showNewMushroom函数,是显示蘑菇,这个就比较重要了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function slot0.showNewMushroom(slot0, slot1, slot2)
cc.SimpleAudioEngine:getInstance():playEffect(music_dingchumoguhuohua)

slot0.mushTileCoord = cc.p(slot1.x, slot1.y - 1)
slot3 = cc.pAdd(slot0:tileCoordToPoint(slot0.mushTileCoord), cc.p(slot0.tileSize.width / 2, slot0.tileSize.height / 2))

if slot2 == MarioBodyState.SMALL then
slot0.mushSprite = cc.Sprite:create(pic_rewardMushroom, cc.rect(0, 0, 16, 16))
elseif MarioBodyState.NORMAL == slot2 then
slot0.mushSprite = cc.Sprite:create(pic_toolMushroom, cc.rect(0, 0, 18, 18))
end

slot3.y = slot3.y - slot0.tileSize.height

slot0.mushSprite:setPosition(slot3)
slot0:addChild(slot0.mushSprite)
slot0.mushSprite:runAction(cc.MoveBy:create(0.5, cc.p(0, slot0.tileSize.height)))
end

这个函数是在breakBrick函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function slot0.breakBrick(slot0, slot1, slot2)
if slot0:getPropertiesForGID(slot0.brickLayer:getTileGIDAt(slot1)) == 0 then
slot0:chageBlockState(slot1)
elseif slot4 == 601 and slot0:itemInList(slot0.mushroomCoordList, slot1) then
for slot9 = 0, 31 do
slot5 = "" .. slot0.labelList["input" .. tostring(slot9)]:getString()
end

slot6 = require("core.Util").create(slot5)

slot6:OoO()

if slot6:oOo() then
slot0:showNewMushroom(slot1, slot2)
slot0:removeItem(slot0.mushroomCoordList, slot1)
end
end
end

这个东西是最重要的:

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

也就是通过顶砖块,进行32个字符的输入,输入正确就能得到蘑菇,击败板栗就能成功。之后将输入的拼接成slot5传递给util.lua文件的create函数,返回slot6,这个值再调用OoO(),然后调用slot6:oOo(),返回1就行,所以如何使slot6:oOo()函数返回true。

看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
function slot0.OoO(slot0)
while true do
opcode = slot0.slot[1][slot0.slot[5]]

if opcode == 17 then
slot1 = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[1][33 + slot1] = slot0:lil(slot0.slot[1][33 + slot1] + 1, 255)
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 33 then
slot1 = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[slot1 - 7] = slot0:lil(slot0.slot[slot1 - 7] + 1, 255)
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 65 then
slot0.slot[6] = slot0.slot[6] + 1
slot0.slot[2][slot0.slot[6]] = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 18 then
slot1 = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[1][33 + slot1] = slot0:lil(slot0.slot[1][33 + slot1] - 1, 255)
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 35 then
slot1 = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[slot1 - 7] = slot0:ili(slot0.slot[slot1 - 7], slot0.slot[1][slot0.slot[5] + 2])
slot0.slot[5] = slot0.slot[5] + 3
elseif opcode == 50 then
slot0.slot[6] = slot0.slot[6] + 1
slot0.slot[2][slot0.slot[6]] = slot0.slot[2][33 + slot0.slot[1][slot0.slot[5] + 1]]
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 36 then
slot1 = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[slot1 - 7] = slot0:ili(slot0.slot[slot1 - 7], slot0.slot[slot0.slot[1][slot0.slot[5] + 2] - 7])
slot0.slot[5] = slot0.slot[5] + 3
elseif opcode == 49 then
slot0.slot[2][224 + slot0.slot[1][slot0.slot[5] + 1]] = slot0.slot[2][slot0.slot[6]]
slot0.slot[6] = slot0.slot[6] - 1
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 34 then
slot1 = slot0.slot[1][slot0.slot[5] + 1]
slot0.slot[slot1 - 7] = slot0:lil(slot0.slot[slot1 - 7] - 1, 255)
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 66 then
slot0.slot[6] = slot0.slot[6] + 1
slot0.slot[2][slot0.slot[6]] = slot0.slot[slot0.slot[1][slot0.slot[5] + 1] - 7]
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 37 then
slot0.slot[slot0.slot[1][slot0.slot[5] + 1] - 7] = slot0.slot[2][slot0.slot[6]]
slot0.slot[6] = slot0.slot[6] - 1
slot0.slot[5] = slot0.slot[5] + 2
elseif opcode == 144 then
return
else
break
end
end
end

slot0.slot[5] + 1是计数的,slot0.slot[1]是数组,slot0:lil函数代表第一个操作数的运算,比如:

1
2
3
4
5
6
slot0.slot[1][33 + slot1] = slot0:lil(slot0.slot[1][33 + slot1] + 1, 255)
也就是实现了slot0.slot[1][33 + slot1] ++
slot0.slot[1][33 + slot1] = slot0:lil(slot0.slot[1][33 + slot1] - 1, 255)
实现了slot0.slot[1][33 + slot1] --
slot0.slot[slot1 - 7] = slot0:ili(slot0.slot[slot1 - 7], slot0.slot[1][slot0.slot[5] + 2])
实现了异或
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
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
memory[1] = 30
reg2 = memory[1]
memory[1] = (byte) input[0]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[0] = (byte) memory[1]
memory[1] = (byte) input[191]
reg2 = memory[1]
memory[1] = (byte) input[1]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[1] = (byte) memory[1]
memory[1] = (byte) input[192]
reg2 = memory[1]
memory[1] = (byte) input[2]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[2] = (byte) memory[1]
memory[1] = (byte) input[193]
reg2 = memory[1]
memory[1] = (byte) input[3]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[3] = (byte) memory[1]
memory[1] = (byte) input[194]
reg2 = memory[1]
memory[1] = (byte) input[4]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[4] = (byte) memory[1]
memory[1] = (byte) input[195]
reg2 = memory[1]
memory[1] = (byte) input[5]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[5] = (byte) memory[1]
memory[1] = (byte) input[196]
reg2 = memory[1]
memory[1] = (byte) input[6]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[6] = (byte) memory[1]
memory[1] = (byte) input[197]
reg2 = memory[1]
memory[1] = (byte) input[7]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[7] = (byte) memory[1]
memory[1] = (byte) input[198]
reg2 = memory[1]
memory[1] = (byte) input[8]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[8] = (byte) memory[1]
memory[1] = (byte) input[199]
reg2 = memory[1]
memory[1] = (byte) input[9]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[9] = (byte) memory[1]
memory[1] = (byte) input[200]
reg2 = memory[1]
memory[1] = (byte) input[10]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[10] = (byte) memory[1]
memory[1] = (byte) input[201]
reg2 = memory[1]
memory[1] = (byte) input[11]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[11] = (byte) memory[1]
memory[1] = (byte) input[202]
reg2 = memory[1]
memory[1] = (byte) input[12]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[12] = (byte) memory[1]
memory[1] = (byte) input[203]
reg2 = memory[1]
memory[1] = (byte) input[13]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[13] = (byte) memory[1]
memory[1] = (byte) input[204]
reg2 = memory[1]
memory[1] = (byte) input[14]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[14] = (byte) memory[1]
memory[1] = (byte) input[205]
reg2 = memory[1]
memory[1] = (byte) input[15]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[15] = (byte) memory[1]
memory[1] = (byte) input[206]
reg2 = memory[1]
memory[1] = (byte) input[16]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[16] = (byte) memory[1]
memory[1] = (byte) input[207]
reg2 = memory[1]
memory[1] = (byte) input[17]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[17] = (byte) memory[1]
memory[1] = (byte) input[208]
reg2 = memory[1]
memory[1] = (byte) input[18]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[18] = (byte) memory[1]
memory[1] = (byte) input[209]
reg2 = memory[1]
memory[1] = (byte) input[19]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[19] = (byte) memory[1]
memory[1] = (byte) input[210]
reg2 = memory[1]
memory[1] = (byte) input[20]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[20] = (byte) memory[1]
memory[1] = (byte) input[211]
reg2 = memory[1]
memory[1] = (byte) input[21]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[21] = (byte) memory[1]
memory[1] = (byte) input[212]
reg2 = memory[1]
memory[1] = (byte) input[22]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[22] = (byte) memory[1]
memory[1] = (byte) input[213]
reg2 = memory[1]
memory[1] = (byte) input[23]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[23] = (byte) memory[1]
memory[1] = (byte) input[214]
reg2 = memory[1]
memory[1] = (byte) input[24]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[24] = (byte) memory[1]
memory[1] = (byte) input[215]
reg2 = memory[1]
memory[1] = (byte) input[25]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[25] = (byte) memory[1]
memory[1] = (byte) input[216]
reg2 = memory[1]
memory[1] = (byte) input[26]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[26] = (byte) memory[1]
memory[1] = (byte) input[217]
reg2 = memory[1]
memory[1] = (byte) input[27]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[27] = (byte) memory[1]
memory[1] = (byte) input[218]
reg2 = memory[1]
memory[1] = (byte) input[28]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[28] = (byte) memory[1]
memory[1] = (byte) input[219]
reg2 = memory[1]
memory[1] = (byte) input[29]
reg3 = memory[1]
reg2 ^= reg3
reg2++
memory[1] = reg2
input[29] = (byte) memory[1]
memory[1] = (byte) input[220]
reg2 = memory[1]
memory[1] = (byte) input[30]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[30] = (byte) memory[1]
memory[1] = (byte) input[221]
reg2 = memory[1]
memory[1] = (byte) input[31]
reg3 = memory[1]
reg2 ^= reg3
reg2--
memory[1] = reg2
input[31] = (byte) memory[1]

其实就能看出来运算了,给出脚本:

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)

得到flag:A766957A53EDA9290CCF8E03F1A9B7E0