Champion 🥇

Misbehave

The logic of program is simple. The flag xor with a number generated by gen_rand function.

It is worth noting that the memcmp in PLT is modified to sub_13A3 during the operation. The encrypted flag will xor with the state of random number generator before the comparison.

It can help you avoid many unnecessary mistakes using the c code generated by IDA directly.

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
#include <bits/stdc++.h>


unsigned __int64 state = 0xFEEDF00DDEADBEEF;


#define BYTE1(x) ((x >> 8) & 0xFF)


unsigned __int32 flag_enc[12] = {
2423218208, 4086265774, 1587874300, 1362717661, 1584332255, 2240307368,
1385551804, 4085442933, 2500108983, 1734086218, 3205726547, 3245367604};


__int64 gen_rand() {
int j; // [rsp+1Ch] [rbp-24h]
int i; // [rsp+20h] [rbp-20h]
unsigned int v3; // [rsp+24h] [rbp-1Ch]
unsigned __int64 v4; // [rsp+28h] [rbp-18h]
unsigned __int64 v5; // [rsp+30h] [rbp-10h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]


v6 = state & 0x1FF;
v5 = ((unsigned __int64)state >> 9) & 0x7FF;
v4 = ((unsigned __int64)state >> 20) & 0x1FFF;
for (i = 0; i <= 31; ++i) {
for (j = 0; j <= 30; ++j) {
v6 = ((v6 >> 4) ^ BYTE1(v6)) & 1 | (2 * (__int16)v6) & 0x1FF;
v5 = (BYTE1(v5) ^ (v5 >> 10)) & 1 | (2 * (__int16)v5) & 0x7FF;
v4 = ((v4 >> 11) ^ (v4 >> 10) ^ (v4 >> 7) ^ (v4 >> 12)) & 1 |
(2 * (__int16)v4) & 0x1FFF;
}
v3 = (v5 & (unsigned __int8)v6 | (unsigned __int8)(~(__int8)v6 & v4)) &
1 |
(2 * v3);
}
state = v6 | (v4 << 20) | (v5 << 9);
return v3;
}


void sub_13A3(unsigned int *a1) {
state = (unsigned int)*a1 ^ (unsigned __int64)state;
}


int main() {
for (int i = 0; i < 12; i++) {
int v1 = gen_rand();
sub_13A3(flag_enc + i);
flag_enc[i] ^= v1;
}
printf("%s\n", flag_enc);
return 0;
}
// TSGCTF{h1dd3n_func7i0n_4nd_s31f_g07_0verwr173}

Warmup SQLite

Firstly, we need to decompile the bytecode of SQLite. According to this site, we can get the meaning of each command.

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
| id  | opcode        | p1  | p2  | p3  | p4                            | p5  | Comment                    |
| --- | ------------- | --- | --- | --- | ----------------------------- | --- | -------------------------- |
| 0 | Init | 0 | 88 | 0 | | 0 | start = 88 |
| 1 | InitCoroutine | 1 | 68 | 2 | | 0 | |
| 2 | OpenPseudo | 1 | 2 | 3 | | 0 | c1 = PseudoLine(v2, 3) |
| 3 | OpenEphemeral | 4 | 3 | 0 | | 0 | c4 = TempTable(3) |
| 4 | InitCoroutine | 3 | 34 | 5 | | 0 | |
| 5 | OpenPseudo | 3 | 4 | 3 | | 0 | c3 = PseudoLine(v4, 3) |
| 6 | OpenEphemeral | 5 | 3 | 0 | | 0 | c5 = TempTable(3) |
| 7 | String8 | 0 | 5 | 0 | | 0 | v5 = "" |
| 8 | String8 | 0 | 6 | 0 | ~~Your input is filled here~~ | 0 | v6 = flag |
| 9 | Integer | -1 | 7 | 0 | | 0 | v7 = -1 |
| 10 | MakeRecord | 5 | 3 | 8 | | 0 | v8 = Record(v5, 3) |
| 11 | NewRowid | 5 | 9 | 0 | | 0 | v9 = NewRowID(c5) |
| 12 | Insert | 5 | 8 | 9 | | 8 | Insert(c5, v8, v9) |
| 13 | Rewind | 5 | 33 | 0 | | 0 | If c5 is empty jump to 33 |
| 14 | NullRow | 3 | 0 | 0 | | 0 | c3 = null |
| 15 | RowData | 5 | 4 | 0 | | 0 | v4 = Row(c5) |
| 16 | Delete | 5 | 0 | 0 | | 0 | Delete(c5) |
| 17 | Column | 3 | 0 | 10 | | 0 | v10 = c3[0] |
| 18 | Column | 3 | 1 | 11 | | 0 | v11 = c3[1] |
| 19 | Column | 3 | 2 | 12 | | 0 | v12 = c3[2] |
| 20 | Yield | 3 | 0 | 0 | | 0 | |
| 21 | Column | 3 | 1 | 8 | | 0 | v8 = c3[1] |
| 22 | Eq | 13 | 32 | 8 | BINARY-8 | 80 | jumpto 32 if v8 == v13 |
| 23 | Column | 3 | 1 | 14 | | 0 | v14 = c3[1] |
| 24 | Function | 6 | 14 | 5 | substr(3) | 0 | v5 = substr(v14, v15, v16) |
| 25 | Column | 3 | 1 | 17 | | 0 | v17 = c3[1] |
| 26 | Function | 2 | 17 | 6 | substr(2) | 0 | v6 = substr(v17, v18) |
| 27 | Column | 3 | 2 | 8 | | 0 | v8 = c3[2] |
| 28 | Add | 19 | 8 | 7 | | 0 | v7 = v8 + v19 |
| 29 | MakeRecord | 5 | 3 | 8 | | 0 | v8 = Record(v5, 3) |
| 30 | NewRowid | 5 | 9 | 0 | | 0 | v9 = NewRowID(c5) |
| 31 | Insert | 5 | 8 | 9 | | 8 | Insert(c5, v8, v9) |
| 32 | Goto | 0 | 13 | 0 | | 0 | |
| 33 | EndCoroutine | 3 | 0 | 0 | | 0 | |


| id | opcode | p1 | p2 | p3 | p4 | p5 | Comment |
| --- | ------------- | --- | --- | --- | ---------- | --- | ------------------------- |
| 34 | InitCoroutine | 3 | 0 | 5 | | 0 | |
| 35 | Yield | 3 | 46 | 0 | | 0 | |
| 36 | Copy | 10 | 20 | 0 | | 2 | v20 = v10 |
| 37 | Eq | 13 | 45 | 20 | BINARY-8 | 80 | jump to 45 if v20 == v13 |
| 38 | Copy | 10 | 20 | 0 | | 2 | |
| 39 | Function | 0 | 20 | 21 | unicode(1) | 0 | v21 = unicode(v20) |
| 40 | Copy | 12 | 22 | 0 | | 2 | v22 = v12 |
| 41 | Integer | 0 | 23 | 0 | | 0 | v23 = 0 |
| 42 | MakeRecord | 21 | 3 | 20 | | 0 | v20 = Record(v21, 3) |
| 43 | NewRowid | 4 | 24 | 0 | | 0 | v24 = NewRowID(c4) |
| 44 | Insert | 4 | 20 | 24 | | 8 | Insert(c4, v20, v24) |
| 45 | Goto | 0 | 35 | 0 | | 0 | |
| 46 | Rewind | 4 | 67 | 0 | | 0 | If c4 is empty jump to 67 |
| 47 | NullRow | 1 | 0 | 0 | | 0 | c1 = Null |
| 48 | RowData | 4 | 2 | 0 | | 0 | v2 = Row(c4) |
| 49 | Delete | 4 | 0 | 0 | | 0 | |
| 50 | Column | 1 | 0 | 25 | | 0 | v25 = c1[0] |
| 51 | Column | 1 | 1 | 26 | | 0 | v26 = c1[1] |
| 52 | Column | 1 | 2 | 27 | | 0 | v27 = c1[2] |
| 53 | Yield | 1 | 0 | 0 | | 0 | |
| 54 | Column | 1 | 2 | 20 | | 0 | v20 = c1[2] |
| 55 | Ge | 28 | 66 | 20 | BINARY-8 | 80 | jump to 66 if v20 >= v28 |
| 56 | Column | 1 | 0 | 29 | | 0 | v29 = c1[0] |
| 57 | Multiply | 30 | 29 | 24 | | 0 | v24 = v29 * v30 |
| 58 | Add | 31 | 24 | 20 | | 0 | v20 = v24 + v31 |
| 59 | Remainder | 32 | 20 | 21 | | 0 | v21 = v20 % v32 |
| 60 | Column | 1 | 1 | 22 | | 0 | v22 = c1[1] |
| 61 | Column | 1 | 2 | 20 | | 0 | v20 = c1[2] |
| 62 | Add | 19 | 20 | 23 | | 0 | v23 = v20 + v19 |
| 63 | MakeRecord | 21 | 3 | 20 | | 0 | |
| 64 | NewRowid | 4 | 24 | 0 | | 0 | |
| 65 | Insert | 4 | 20 | 24 | | 8 | |
| 66 | Goto | 0 | 46 | 0 | | 0 | |
| 67 | EndCoroutine | 1 | 0 | 0 | | 0 | |


| id | opcode | p1 | p2 | p3 | p4 | p5 | Comment |
| --- | ------------- | --- | --- | --- | -------- | --- | ------------------------ |
| 68 | SorterOpen | 6 | 5 | 0 | k(1,B) | 0 | c6 = table(5) |
| 69 | InitCoroutine | 1 | 0 | 2 | | 0 | |
| 70 | Yield | 1 | 79 | 0 | | 0 | |
| 71 | Copy | 27 | 33 | 0 | | 2 | v33 = v27 |
| 72 | Ne | 28 | 78 | 33 | BINARY-8 | 80 | jump to 78 if v28 != v33 |
| 73 | Copy | 25 | 35 | 0 | | 2 | v35 = v25 |
| 74 | Copy | 27 | 36 | 0 | | 2 | v36 = v27 |
| 75 | Copy | 26 | 34 | 0 | | 2 | v34 = v26 |
| 76 | MakeRecord | 34 | 3 | 38 | | 0 | v38 = Record(v34, 3) |
| 77 | SorterInsert | 6 | 38 | 34 | 3 | 0 | Insert(c6, v38, v34, 3) |
| 78 | Goto | 0 | 70 | 0 | | 0 | |
| 79 | OpenPseudo | 7 | 39 | 5 | | 0 | |
| 80 | SorterSort | 6 | 87 | 0 | | 0 | |
| 81 | SorterData | 6 | 39 | 7 | | 0 | |
| 82 | Column | 7 | 2 | 37 | | 0 | v37 = c7[2] |
| 83 | Column | 7 | 0 | 36 | | 0 | v36 = c7[0] |
| 84 | Column | 7 | 1 | 35 | | 0 | v35 = c7[1] |
| 85 | ResultRow | 35 | 3 | 0 | | 0 | |
| 86 | SorterNext | 6 | 81 | 0 | | 0 | |
| 87 | Halt | 0 | 0 | 0 | | 0 | |
| 88 | String8 | 0 | 13 | 0 | | 0 | v13 = "" |
| 89 | Integer | 1 | 15 | 0 | | 0 | v15 = 1 |
| 90 | Integer | 1 | 16 | 0 | | 0 | v16 = 1 |
| 91 | Integer | 2 | 18 | 0 | | 0 | v18 = 2 |
| 92 | Integer | 1 | 19 | 0 | | 0 | v19 = 1 |
| 93 | Integer | 10 | 28 | 0 | | 0 | v28 = 10 |
| 94 | Integer | 7 | 30 | 0 | | 0 | v30 = 7 |
| 95 | Integer | 2 | 31 | 0 | | 0 | v31 = 2 |
| 96 | Integer | 256 | 32 | 0 | | 0 | v32 = 256 |
| 97 | Goto | 0 | 1 | 0 | | 0 | |

By analyzing the bytecode, it just splits the flag into characters, transforms them into unicode, and modifies each value $x$ to $(7x + 2) \bmod 256$.

1
2
3
4
5
6
7
8
9
10
11
12
flag_enc = [100, 115, 39, 99, 100, 54, 27, 115, 69, 220, 69, 99, 100, 191, 56, 161, 131, 11, 101, 162, 191, 54, 130, 175, 205, 191, 222, 101, 162, 116, 147, 191, 55, 24, 69, 130, 69, 191, 252, 101, 102, 101, 252, 189, 82, 116, 41, 147, 161, 147, 132, 101, 162, 82, 191, 220, 9, 205, 9, 100, 191, 38, 68, 253]

from sympy import mod_inverse


for i in range(len(flag_enc)):
for j in range(10):
flag_enc[i] = ((flag_enc[i] - 2) % 256) * mod_inverse(7, 256) % 256


print("".join([chr(x) for x in flag_enc]))
# TSGCTF{SELECT_hacker_FROM_nerds_WHERE_level="disaster"_LIMIT_64}

TSGDBinary

First of all, we need to deobfuscate the tsgdbinary.py to check what it does during the operation.

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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import gdb
import sys
import os
import types


def run_gdb_cmd(cmd):
global gdb_history
gdb.execute(cmd, to_string=True)
gdb_history = gdb.parameter("history filename")
hs = gdb.parameter("history save")
if hs and gdb_history:
with open(gdb_history, "a") as file:
file.write(cmd + "\n")


def set_reg_byte(i):
gdb.parse_and_eval("*(unsigned char *)$" + i + " = $al")
return


def set_reg_int64(i):
gdb.parse_and_eval("*(unsigned long long *)$" + i + " = $rax")
return


def set_ing64(i, m):
gdb.parse_and_eval("$" + i + " = *(unsigned long long *)$" + m)
return


def set_byte(i, m):
gdb.parse_and_eval("$" + i + " = *(unsigned char *)$" + m)
return


def set_reg(i, m):
gdb.parse_and_eval("$" + i + " = " + hex(m))
return


def event_breakpoint_connect(u):
gdb.events.breakpoint_created.connect(u)
return


def event_breakpoint_disconnect(u):
gdb.events.breakpoint_created.disconnect(u)
return


def event_stop_connect(u):
gdb.events.stop.connect(u)
return


def event_stop_disconnect(u):
gdb.events.stop.disconnect(u)
return


def output_file(pi, v):
with open(tmp_file, "wb") as f:
for b in pi:
f.write(bytes([ord(b) ^ v]))
return


def read_memory(s, l):
return gdb.inferiors()[0].read_memory(s, l)


def get_pc():
return gdb.selected_frame().pc()


def read_reg(o):
return gdb.selected_frame().read_register(o)


def set_breakpoint(a):
try:
gdb.Breakpoint(f"*{a}").silent = True
return
except RuntimeError as e:
return


def disable_output():
sys.stdout = open(os.devnull, "w")
sys.stderr = open(os.devnull, "w")
return


def enable_output():
sys.stderr.close()
sys.stderr = sys.__stderr__
sys.stdout.close()
sys.stdout = sys.__stdout__
return


def change_last_breakpoint1(e):
global bp_lock
if bp_lock:
return
bp_lock = True
bps = gdb.breakpoints()
if not bps:
return
bp = bps[-1]
if bp.location.startswith("*"):
o = int(bp.location[1:])
n = o - 0x7200
bp.delete()
set_breakpoint(n)
bp_lock = False
return


def change_last_breakpoint2(e):
global bp_lock
if bp_lock:
return
bp_lock = True
bps = gdb.breakpoints()
if not bps:
return
bp = bps[-1]
if bp.location.startswith("*"):
o = int(bp.location[1:])
n = o + 0x30
bp.delete()
set_breakpoint(n)
bp_lock = False
return


def remove_first_breakpoint(e):
bps = gdb.breakpoints()
if bps:
bps[0].delete()
return


def gdb_exec(p):
run_gdb_cmd("source " + p)
return


def gdb_continue(n):
gdb.execute("c")


def print_breakpoints():
bps = gdb.breakpoints()
print([hex(int(bp.location[1:])) for bp in bps])


gdb.execute("shell rm ./kk")
gdb.execute("set history filename ./kk")
gdb.execute("set history save on")


gdb_history = "./kk"
tmp_file = "./pkcool"

bp_lock = False
pos = [0, 34, 31, 32, 47, 42]
lweknt = 0x89FC76AE
base = 0x401000
mnt = 0xF8D6A8C3
base1 = 0x6547EA867000
base2 = 0x72433A3C000
kqwee = base1 + 0xD12
pwsu = base1 + 0xFA0
xend = pwsu + 0x30

rdx = "rdx"
rcx = "rcx"
rax = "rax"
rdi = "rdi"
rip = "rip"
rsp = "rsp"
rsi = "rsi"

set_breakpoint(base + 0x7F6)
set_breakpoint(base + 0x8BC)
gdb.execute("run")

run_gdb_cmd("set pagination off")
run_gdb_cmd("info b")
run_gdb_cmd("show")
run_gdb_cmd("info b")
event_stop_connect(remove_first_breakpoint)
bps = gdb.breakpoints()
print([hex(int(bp.location[1:])) for bp in bps])
run_gdb_cmd("c")
event_breakpoint_connect(change_last_breakpoint2)
set_breakpoint(base + 0x3E4)

ou = read_memory(base + 0x3080, 71)
print(b"".join(ou[:0x40]))
output_file(ou, 0xD7)
gdb_exec(tmp_file)
# call mmap((void *)0x6547ea867000, 0x1000, 7, 0x32, -1, 0)

# get_script()
ou1 = read_memory(base + 0x30E0, 305)
output_file(ou1, 0x3C)
print(open(tmp_file, "r").read())
gdb_exec(tmp_file)
# call mmap((void *)0x72433a3c000, 0x400, 6, 0x32, -1, 0)
# set $src = 0x7fffffffe460
# set $dst = 0x72433a3c000
# set $size = 4
# set $count = 0x30 / $size
# set $i = 0
# while ($i < $count)
# set $value = *(int *)($src + $i * $size)
# set *(int *)($dst + $i * $size) = $value
# set $i = $i + 1
# end

set_breakpoint(base + 0x3FE)
# disable_output()
# etg()
ou1 = read_memory(base + 0x3220, 270)
print(b"".join(ou1[:0x40]))
for i in range(0, 0x30):
set_reg(rcx, base2 + i)
set_byte(rdi, rcx)
set_reg(rip, 0x401414)
set_reg(rsi, int.from_bytes(ou[i], byteorder="little"))
if i == 0:
gdb_continue(4)
gdb_continue(4)
set_reg_byte(rcx)
set_breakpoint(0x40142E - 0x30)

print(b"".join(ou[:0x40]))
for i in range(0, 0x30):
set_reg(rcx, base2 + i)
set_byte(rdi, rcx)
set_reg(rip, 0x401414)
set_reg(rsi, int.from_bytes(ou[i + 0x10], byteorder="little"))
gdb_continue(4)
set_reg_byte(rcx)
set_breakpoint(0x40142E - 0x30)

event_breakpoint_disconnect(change_last_breakpoint2)
event_breakpoint_connect(change_last_breakpoint1)
event_stop_disconnect(remove_first_breakpoint)

k = 0
for i in range(0, 6):
set_reg(rdx, 0x1000)
set_reg(rsi, base + 0x3340 + 0x1000 * i)
set_reg(rdi, base1)
set_reg(rip, base + 0x4D6)
set_breakpoint(base + 0x7741)
gdb_continue(4)
set_reg(rax, base + 0x5A5)
set_breakpoint(base + 0x77A5)
set_reg(rip, base1 + pos[i])
set_reg(rcx, base2 + k)
set_ing64(rsi, rcx)
set_reg_int64(rsp)
gdb_continue(4)
bps = gdb.breakpoints()
for bp in bps:
bp.enabled = False
set_breakpoint(base + 0x77C1)
set_reg(rsi, read_reg(rax))
set_reg(rdi, 0x89FC76AEF8D6A8C3)
set_reg(rcx, xend + k)
gdb_continue(4)
set_reg_int64(rcx)
k += 8

enable_output()
set_reg(rdx, 0x30)
set_reg(rsi, pwsu)
set_reg(rdi, xend)
set_reg(rip, base + 0x8CF)
gdb_continue(4)
# gdb.execute("shell rm ./kk")
# gdb.execute("set history save off")

The program will break before calling the memcmp. The flag input will be modified by the debugger and then the program will jump to 0x404340. The code at 0x404340 in binary is encrypted, we need to decrypt it first.

1
2
3
4
5
6
7
8
9
10
11
12
code = idc.get_bytes(0x404340, 0x6000)

tmp = [0] * 0x1000
file = open("tmp", "wb")
for i in range(0, 6):
tmp = [tmp[j] ^ code[i * 0x1000 + j] for j in range(0x1000)]
file.write(bytes(tmp))


pos = [0, 34, 31, 32, 47, 42]
print([hex(i) for i in pos])
# ['0x0', '0x22', '0x1f', '0x20', '0x2f', '0x2a']

The code at 0x404340 is sevaral mapping functions. We can find the original value manually and then decrypt the flag.

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
import struct


flag_enc = list(
struct.unpack(
"<" + "Q" * 6,
b"B\xd3\x1f1d\xfe\xae\xa2\x02\xad\x05H\x1c\xac\x96\xd5\xe6bK#\xb5\xd0\xf7\xa7\xcaV\x19Y\x08`:\xacu}\xc4\x05\n\x8e\xb8\x07Oy=\xef\xadsy8",
)
)
for i in range(6):
flag_enc[i] = (flag_enc[i] - 0x89FC76AEF8D6A8C3) & 0xFFFFFFFFFFFFFFFF
print([hex(i)[2:].upper() for i in flag_enc])
# ['18B287B538492A7F', '4B9A356D4F2F043F', '1DFB5A062A74BA23', '223DE9596042AE07', '7DBC175B0CEDD4B2', 'AE7CFCFEF666D08C']

flag = [
0x66221E42571B1B1D,
0x2063383B75652F77,
0x6B655A6961635674,
0x641733226F50717C,
0x624F786E344F6E7A,
0x1C566D342C774434,
]
ou = b"\xb4\xb6\xbb\xbb\xf7\xba\xba\xb6\xa7\xff\xff\xa1\xb8\xbe\xb3\xf7\xfd\xfe\xe7\xaf\xe1\xe2\xe3\xe0\xb2\xb6\xef\xe1\xe0\xe7\xe7\xe7\xfb\xf7\xe7\xaf\xe6\xe7\xe7\xe7\xfb\xf7\xe0\xfb\xf7\xe7\xaf\xe4\xe5\xfb\xf7\xfa\xe6\xfb\xf7\xe7\xfe\xdd\xf6\xa5\xba\xf7\xf9\xf8"
flag = struct.pack("<" + "Q" * 6, *flag)


flag = [flag[i] ^ ou[i + 0x10] for i in range(0x30)]
flag = [flag[i] ^ ou[i] for i in range(0x30)]
print(bytes(flag))
# TSGCTF{0bfu5ca70r_can_al50_u53_gdb_and_b1nary}

serverless

We extract all the regex rules from the compose.yaml and divide them into two parts: ones are expanding the length, and the others are reducing.

Focus on the reducing part first.

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
[
(r"^(.*)M\(m\).*", r"^(.*)M\(m\)", r"\1"),
(r"^(.*)H\(h\).*", r"^(.*)H\(h\)", r"\1"),
(r"^(.*)Y\(y\).*", r"^(.*)Y\(y\)", r"\1"),
(r"^(.*)C\(c\).*", r"^(.*)C\(c\)", r"\1"),
(r"^(.*)A\(a\).*", r"^(.*)A\(a\)", r"\1"),
(r"^(.*)J\(j\).*", r"^(.*)J\(j\)", r"\1"),
(r"^(.*)D\(d\).*", r"^(.*)D\(d\)", r"\1"),
(r"^(.*)F\(f\).*", r"^(.*)F\(f\)", r"\1"),
(r"^(.*)G\(g\).*", r"^(.*)G\(g\)", r"\1"),
(r"^(.*)K\(k\).*", r"^(.*)K\(k\)", r"\1"),
(r"^(.*)T\(t\).*", r"^(.*)T\(t\)", r"\1"),
(r"^(.*)X\(x\).*", r"^(.*)X\(x\)", r"\1"),
(r"^(.*)R\(r\).*", r"^(.*)R\(r\)", r"\1"),
(r"^(.*)P\(p\).*", r"^(.*)P\(p\)", r"\1"),
(r"^(.*)U\(u\).*", r"^(.*)U\(u\)", r"\1"),
(r"^(.*)L\(l\).*", r"^(.*)L\(l\)", r"\1"),
(r"^(.*)V\(v\).*", r"^(.*)V\(v\)", r"\1"),
(r"^(.*)O\(o\).*", r"^(.*)O\(o\)", r"\1"),
(r"^(.*)B\(b\).*", r"^(.*)B\(b\)", r"\1"),
(r"^(.*)Q\(q\).*", r"^(.*)Q\(q\)", r"\1"),
(r"^(.*)N\(n\).*", r"^(.*)N\(n\)", r"\1"),
(r"^(.*)E\(e\).*", r"^(.*)E\(e\)", r"\1"),
(r"^(.*)W\(w\).*", r"^(.*)W\(w\)", r"\1"),
(r"^(.*)I\(i\).*", r"^(.*)I\(i\)", r"\1"),
(r"^(.*)Z\(z\).*", r"^(.*)Z\(z\)", r"\1"),
(r"^(.*)S\(s\).*", r"^(.*)S\(s\)", r"\1"),
]

It is easy to find that the rules are removing an uppercase letter with a corresponding lowercase letter. We know the flag starts with TSGCTF. If we want to remove all the characters, we need to construct (f)(t)(c)(g)(s)(t) within {}.

Pay attention to the expanding part next.

1
2
3
4
5
6
7
8
9
10
11
12
13
[
(r"^(.*)/eq.*", r"^(.*)/eq", r"\1(w)(s)(p)/"),
(r"^(.*)/6i.*", r"^(.*)/6i", r"\1(s)(y)(n)RZK/"),
(r"^(.*)/g7.*", r"^(.*)/g7", r"\1(q)(k)(s)MFF/"),
(r"^(.*)/9g.*", r"^(.*)/9g", r"\1(e)(f)(z)EFT/"),
...,
(r"^(.*)/n.*", r"^(.*)/n", r"\1(v)(g)(h)PRZ/"),
(r"^(.*)%7D%7B.*", r"^(.*)%7D%7B", r"\1+"),
(r"^(.*)%7D.*", r"^(.*)%7D", r"\1)"),
(r"^(.*)%7B.*", r"^(.*)%7B", r"\1(/"),
(r"^(.*)_.*", r"^(.*)_", r"\1)(/"),
(r"^(.*)/\).*", r"^(.*)/\)", r"\1)"),
]

It can be noticed that an underscore _ will be replaced by )(, which creates a new group. If we want to create 6 groups, we need to contain 5 underscores with in {}. The regex rules will generate a / right after the {, using it to replate the pattern one by one from left to right.

The pattern rules can be divided into three categories:

  1. Replace a pattern to (a)(b)(c)DEF.
  2. Replace a pattern to (a)(b)(c), which is the end of a group.
  3. Replace a pattern to aDEF, which is the start of a group.

We can build a graph to represent the rules and use BFS to find the path.

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
rules = [
(r"^(.*)/eq.*", r"^(.*)/eq", r"\1(w)(s)(p)/"),
(r"^(.*)/6i.*", r"^(.*)/6i", r"\1(s)(y)(n)RZK/"),
(r"^(.*)/g7.*", r"^(.*)/g7", r"\1(q)(k)(s)MFF/"),
(r"^(.*)/9g.*", r"^(.*)/9g", r"\1(e)(f)(z)EFT/"),
...,
(r"^(.*)/n.*", r"^(.*)/n", r"\1(v)(g)(h)PRZ/"),
]


import re


edges = []
for i in range(26 * 26 * 26 + 26):
edges.append([])




def get_num(node):
if len(node) == 9:
return ((ord(node[7]) - 97) * 26 + (ord(node[4]) - 97)) * 26 + (
ord(node[1]) - 97
)
if len(node) == 3:
return ((ord(node[0]) - 65) * 26 + (ord(node[1]) - 65)) * 26 + (
ord(node[2]) - 65
)
if len(node) == 1:
return ord(node) - 97 + 26 * 26 * 26




for rule in rules:
if re.match(r"\\1(\([a-z]\)){3}[A-Z]{3}", rule[2]):
m = re.match(r"\\1((\([a-z]\)){3})([A-Z]{3})", rule[2])
fr = m.group(1)
to = m.group(3)
edges[get_num(fr)].append((get_num(to), rule[0][6:-2]))
elif re.match(r"\\1[a-z]([A-Z]{3})", rule[2]):
m = re.match(r"\\1([a-z])([A-Z]{3})", rule[2])
fr = m.group(1)
to = m.group(2)
edges[get_num(fr)].append((get_num(to), rule[0][6:-2]))
elif re.match(r"\\1(\([a-z]\)){3}", rule[2]):
m = re.match(r"\\1((\([a-z]\)){3})", rule[2])
fr = m.group(1)
edges[get_num(fr)].append((-1, rule[0][6:-2]))


flag = ""
for st in "ftcgst":
qu = []
vis = [False] * (26 * 26 * 26 + 26)
qu.append((get_num(st), ""))
vis[get_num(st)] = True
while len(qu) > 0:
u, path = qu.pop(0)
if u == -1:
flag += path + "_"
break
for v, p in edges[u]:
if not vis[v]:
vis[v] = True
qu.append((v, path + p))
print("TSGCTF{" + flag[:-1].replace("\\", "") + "}")
# TSGCTF{http-red1rect5-can_funct10n_as-tur1ng-c0mp1ete_m4rk0v-a1g0rithm_proce55ing_funct10n}