Writeups for challenges I wrote for CTFSG 2022, and how it's made. Contains a lot of cool trinkets.
So I was invited to write challenges for CTFSG 2022
and had a lot of fun designing the challenges and spectating the competition. I hope yall had fun!
78
122
CTFSG{CFG_4n1m4t10n}
So @prokarius made the music, and I made the cursed IDA video player, so am gonna talk abt the Cursed IDA Video Player. The binary IDA runs is a massive binary that contains a huge function which IDA plots the control flow of in a way that displays a frame of the video. The function is essentially a huge jump table padded with nop
s to control the brightness of each pixel.
So I actually did smth similar before where I played bad apple on IDA but decided to rewrite it to make it not self modifying. This is so that I can play longer videos at higher resolution.
And you know how powerful C++ templates are? I decided to generate the video frames in C++ templates:
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
#pragma once
#include "gen/image.data"
#pragma inline_recursion(on)
#pragma inline_depth(255)
template <size_t N> __forceinline void
nops()
{
if constexpr (N != 0) {
__asm __emit 0x90;
nops<N - 1>();
}
}
static char _pixel_dummy_mem[0x64];
template <size_t N, bool wide = true> __forceinline void
pixel()
{
if constexpr (wide) {
__asm vfmaddsub132ps xmm0, xmm1, xmmword ptr cs : [edi + esi * 4 + _pixel_dummy_mem] ;
}
nops<N>();
}
template <class T, size_t N>
using Arr_t = const T[N];
template <size_t H, Arr_t<size_t, H>& PVALS, bool wide = true, size_t _PTR = 0> __forceinline void
pixel_column()
{
#define JMP_0 __asm __emit 0xe9 __asm __emit 0x00 __asm __emit 0x00 __asm __emit 0x00 __asm __emit 0x00
if constexpr (_PTR < H) {
pixel<PVALS[_PTR], wide>();
JMP_0;
pixel_column<H, PVALS, wide, _PTR + 1>();
}
#undef JMP_0
}
template<int N>
struct _pixel_padding_arr {
constexpr _pixel_padding_arr() : arr() {
for (auto i = 0; i != N; ++i)
const_cast<size_t*>(arr)[i] = 25;
}
Arr_t<size_t, N> arr;
};
template <size_t W, size_t H, Arr_t<Arr_t<size_t, H>, W>& PVALS> void
pixel_image()
{
unsigned int select = 0;
__asm xor edi, edi
__asm xor esi, esi
__asm int 3
static constexpr size_t padding[H] = _pixel_padding_arr<H>().arr;
switch (select) {
#define ITER_GADGET(N) case N: pixel_column<H, PVALS[N]>(); __asm jmp end;
#define ITER_END (IMAGE_WIDTH-1)
#include "iter.x"
case W: pixel_column<H, padding, false>(); __asm jmp end; // TODO: Make padding not so wide
default: __asm jmp end;
}
end:
return;
}
template <size_t NFRAMES, size_t W, size_t H, Arr_t<Arr_t<Arr_t<size_t, H>, W>, NFRAMES>& PVALS, size_t _PTR=0> void
movie()
{
if constexpr (_PTR < NFRAMES) {
pixel_image<W, H, PVALS[_PTR]>();
movie<NFRAMES, W, H, PVALS, _PTR + 1>();
}
}
Here's a summary of what I encountered in the process of writing (and compiling) those cursed templates
Internal Compiler Error
which just means the compiler crashed for some unhandled reason.Fatal Error C1128: Number of sections exceeded object file format limit
Fatal Error C1202: recursive type or function dependency context too complex
Fatal Error C1055: compiler limit: out of keys
Needless to say, those 70 lines of C++ took forever to write (3hrs), and it wasn't even scalable. Turns out C++ templates are super inefficient to compile. I then rewrote everything in C99 macros.
47
500
CTFSG{Th3_Perf3cT_Pl4YeR_0j2nlhe}
1
2
3
4
5
6
Hey remember that hand game thing called chopsticks?
Pat's pretty good at a modified version of it.
Can you win?
connect to the service at:
chals.ctf.sg:20101
Players are tasked to play a simplified game of Chopsticks with a player, Pat
, with the perfect losing strategy (to be defined). The game is just like regular chopsticks but you can't revive dead hands, and the overflow happens at 7
instead of 5
. If the player wins they'll get the flag.
Notice that according to the rules, the total count of feathers of both players will never decrease. Since you started first, the strategy is to stall (To split such that your feather count never increases) strategically such that the total feather count of both of you increases before you attack your opponent.
It also turns out that in this game, you have the winning strategy since you started first, i.e. if both of you play perfectly (Pat does), you would win. One could programmatically figure out the winning strategy and play it against Pat.
So I mentioned that Pat
plays perfectly. However, Pat
, having have to start 2nd, will lose against another perfect player. If you think abt it, it's not so clear how to define the “perfect losing strategy”.
I'm going to define it as such:
The perfect losing strategy is a strategy that maximises the odds of winning a game against a random player, a player who will randomly pick any available move with equal probability. The perfect losing strategy will hence 'guide' a player towards moves at which the player is most likely to fuck up.
Now I'm not going to sit and manually figure out what this strategy is when I can write code to do that.
Let $s$ be the game state. This game state contains the number of feathers in both player's hands, whose turn it is and the visited states. This state fully describes the game and what states can come after it. Let $f(s)$ be the forward
function. $f$ returns a set of all possible game states that can come after state $s$. Let $T(s)$ return whose turn it is at state $s$, $T(s) \in [0,1]$, where $0$ is the player who started first.
Finally, let $P(s)$ be the probability that player $1$ wins, following the perfect losing strategy $S_{\text{lose}}(s)$ when the game is at state $s$, given that player $0$ is a random player. Let $q(s)$ be the probability that player $0$, a random player, takes a move that brings the game state to $s$. Needless today, $P(s)$ and $S_{\text{lose}}(s)$ only make sense when $T(s) = 1$, i.e. it's player $1$'s turn.
The $P(s)$ and $S_{\text{lose}}(s)$ can then be computed recursively through the game tree starting with $s_0$, if $T(s) = 1$, as:
\[\begin{aligned} S_{\text{lose}}(s), \; P(s) &= \operatorname*{argmax, \; max}_{x \in f(s)} \sum_{x' \in f(x)} q(x') P(x') \\ &= \operatorname*{argmax, \; max}_{x \in f(s)} \frac{1}{|f(x)|} \sum_{x' \in f(x)} P(x') \end{aligned}\]This basically formalises the statement “$S_{\text{lose}}(s)$ is about picking the move that maximises player $1$'s chances of winning”.
This can be computed (with some optimisations to not traverse the game tree again) as below. Turns out the expected win-rate is $1$ every $15120$ games with a random player.
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
def get_best_losing_strategy(player, dbg=False, _state=None, _visited=None, _cache=None, _d=0):
if _state == None:
_state = init_state
if _visited == None:
_visited = set([_state])
if _cache == None:
_cache = {}
turn = _state[-1]
if dbg: print(" "*_d + str(turn) + ": " + str(_state))
w,p = is_win(_state)
if w:
w = p==player
if dbg: print(" "*_d + [">_<", "^-^"][w])
if w:
return 1, _state, set([_state])
return 0, None, None
ns = [*(set(gen_possible(_state)) - _visited)]
if len(ns) == 0:
return turn != player, _state, set([_state])
if _state in _cache:
ret, v, d = _cache[_state]
if _visited.isdisjoint(d) and \
v.issubset(_visited):
return 1, ret, d
if turn == player: # Player can pick!
scores = []
strats = []
for s in ns:
nv = _visited.copy(); nv.add(s)
w, strat, d = get_best_losing_strategy(player, dbg, s, nv, _cache, _d+1)
scores.append(w); strats.append(strat)
if w == 1:
dep = d.copy(); dep.add(s)
_cache[_state] = (s,strat), _visited, dep
return 1, (s,strat), dep
bs = max(scores)
idx = scores.index(bs)
return bs, (ns[idx], strats[idx]), None
# It's not player's turn...
strategy = {}
score = 0
nwin = 0
dep = set()
for s in ns:
nv = _visited.copy(); nv.add(s)
w, strat, d = get_best_losing_strategy(player, dbg, s, nv, _cache, _d+1)
score += w
strategy[s] = strat
if w == 1:
nwin += 1
dep = dep | d
if nwin==len(ns):
_cache[_state] = strategy, _visited, dep
return 1, strategy, dep
return score/len(ns), strategy, dep
The full script to generate the losing (and winning strategy) can be found here.
25
912
CTFSG{Ch0pst!ck5_m4STeR!11!_aim48djam3}
1
2
3
4
5
6
Oh have you beat Pat in the previous game? Nice!
Well here's another that's closer to the classic chopsticks game:
You can now revive your hand!
connect to the service at:
chals.ctf.sg:20201
Players are tasked to play a harder version of a game of Chopsticks with a player, Pat
, with a decent enough strategy. If the player wins they'll get the flag. The game is just like regular chopsticks with reviving but now it overflows at 7
instead of 5
.
The strategy is just like regular chopsticks: Force the other player to have only 1
feather. You could try to get your hand number count to be high, and one of Pat
's hand be 1
, so you can kill to other hand. The difficulty is that Pat
is actively trying to do the same to you. Based on the games I've been automatically playing with Pat
, it's humanely possible to win Pat
.
Look at The Making if you wanna see how to automatically derive a strategy. The first player (which is you) is advantaged as they only have to look ahead 9
moves, while Pat
has to look ahead 10
moves.
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
Pat: (1, 1, 1, 1, 0)
Jul: (1, 2, 1, 1, 1) ('1', ('A', 'B'))
Jule's Confidence: 0.07000000000000002
Pat: (1, 2, 1, 2, 0)
Jul: (2, 3, 1, 2, 1) ('1', ('B', 'A'))
Jule's Confidence: 0.08235942050080036
Pat: (2, 3, 2, 3, 0)
Jul: (2, 3, 2, 3, 1) ('2', ('2', '3'))
Jule's Confidence: 0.07000000000000002
Pat: (2, 3, 3, 5, 0)
Jul: (2, 3, 0, 3, 1) ('1', ('A', 'D'))
Jule's Confidence: 0.1114579285714286
Pat: (2, 3, 1, 2, 0)
Jul: (1, 4, 1, 2, 1) ('2', ('1', '4'))
Jule's Confidence: 0.07422300636093018
Pat: (1, 6, 1, 2, 0)
Jul: (3, 4, 1, 2, 1) ('2', ('3', '4'))
Jule's Confidence: 0.05571428571428572
Pat: (3, 4, 1, 2, 0)
Jul: (3, 4, 1, 6, 1) ('1', ('B', 'D'))
Jule's Confidence: 0.15000000000000002
Pat: (3, 4, 2, 5, 0)
Jul: (3, 4, 0, 2, 1) ('1', ('A', 'D'))
Jule's Confidence: 0.26881364534550006
Pat: (3, 4, 1, 1, 0)
Jul: (3, 4, 1, 1, 1) ('2', ('3', '4'))
Jule's Confidence: 0.7
Pat: (3, 5, 1, 1, 0)
Jul: (3, 5, 1, 4, 1) ('1', ('A', 'D'))
Jule's Confidence: 0.746709285714286
Pat: (0, 3, 1, 4, 0)
Jul: (0, 3, 0, 1, 1) ('1', ('A', 'D'))
Jule's Confidence: 1.0
Pat: (0, 4, 0, 1, 0)
Jul: (1, 3, 0, 1, 1) ('2', ('1', '3'))
Jule's Confidence: 1.0
Pat: (1, 4, 0, 1, 0)
Jul: (1, 4, 0, 1, 1) ('2', ('1', '4'))
Jule's Confidence: 1.0
Pat: (1, 5, 0, 1, 0)
Jul: (3, 3, 0, 1, 1) ('2', ('3', '3'))
Jule's Confidence: 1.0
Pat: (3, 4, 0, 1, 0)
Jul: (2, 5, 0, 1, 1) ('2', ('2', '5'))
Jule's Confidence: 1.0
Pat: (3, 5, 0, 1, 0)
Jul: (0, 5, 0, 1, 1) ('1', ('B', 'A'))
Jule's Confidence: 1.0
Pat: (0, 6, 0, 1, 0)
Jul: (0, 6, 0, 0, 1) ('1', ('B', 'C'))
Jule's Confidence: 1.0
We win!!!
...
+-------+-----------------------------------------+
| /\ /\ | You: ^-^ |
|((-v-))| |
|():::()| { You win! } |
+--VVV--+-----------------------------------------+
+...............+ : +...............+
: : : : / / /:
: : : : ,=/\ ,=/\ ,=/\:
: : A:0 : B:6 :,=/\/,=/\/,=/\/:
: : : :(,\/ (,\/ (,\/ :
: : : :,=/\/,=/\/,=/\/:
: : : :(,\/ (,\/ (,\/ :
+...............+ : +...............+
.................................................
+...............+ : +...............+
: : : : :
: : : : :
: : : : :
: : C:0 : D:0 : :
: : : : :
: : : : :
+...............+ : +...............+
+-------+-----------------------------------------+
| /\_/\ | Pat: Nice! CTFSG{Ch0pst!ck5_m4STeR!11!_ |
|( o.o )| aim48djam3} |
| > ^ < | { They lost! } |
+-------+-----------------------------------------+
End.
Another solution is to simply split 1-1
as your first move, effectively making Pat
the first player, and then play Pat
against themselves on another instance.
Since now players can revive through splitting, the game tree is significantly bigger. The “perfect losing strategy” can't be computed on this version of the game anymore. So it's time to write a good enough strategy.
A common way to write such a strategy is via the Minimax Algorithm. You're trying to optimize smth, but the opponent will do whatever to minimise your gain. We start at the current game state, and evaluate each possible move at a certain depth, how much you can hope to gain from said move.
However, this is inefficient, and we can perform Alpha Beta Pruning to stop exploring states that you already know have a worse outcome.
Notice that Alpha Beta Pruning's is more efficient if we try the better moves first. So we can first compute some heuristic of each state and try the states with a higher heuristic. This further improves performance by ~10x.
This allows Pat
to pretty much respond immediately despite searching a depth of 10
, though I still made the server sleep to prevent players from overwhelming the server.
We still need to form some kind of heuristic for each state that says how good a state is. For that I considered a modified game where visiting previous states are allowed. This allows the game state to not be dependent on previous visited states which makes it a lot easier to analyse, which also allows aggressive pruning.
I then computed the probability of a player winning at the current state, assuming the opponent will pick the best move ~70%
of the time (and the other moves uniformly randomly), and used that as the heuristic. I then traversed the game tree once to compute the heuristics of all reachable states, and save it into a lookup table. This takes… a negligible amount of time.
All these is implemented in around 150
lines of 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
import sys
sys.setrecursionlimit(10000)
# state = (a,b,c,d,turn)
init_state = (1,1,1,1,0)
NHANDS = 7
def is_win(state):
a,b,c,d,turn = state
if a+b == 0: return True, 1
if c+d == 0: return True, 0
return False, None
def add_move(a,b):
x = a+b
if x >= NHANDS: return 0
return x
def gen_possible(state, _cache={}):
win, player = is_win(state)
if win: return set()
if state in _cache: return _cache[state].copy()
a,b,c,d,turn = state
ret = []
if turn == 0:
n = a+b
ret += [(i,n-i,c,d,1) for i in range(1,NHANDS) if 0<n-i<NHANDS and 0<i<NHANDS and i<=n-i]
if c!=0: ret += [(a,b,*sorted((add_move(c,j),d)),1) for j in (a,b) if j!=0]
if d!=0: ret += [(a,b,*sorted((c,add_move(d,j))),1) for j in (a,b) if j!=0]
if a!=0: ret += [(*sorted((j,add_move(b,a))),c,d,1) for j in (a,b)]
else:
n = c+d
ret += [(a,b,i,n-i,0) for i in range(1,NHANDS) if 0<n-i<NHANDS and 0<i<NHANDS and i<=n-i]
if a!=0: ret += [(*sorted((add_move(a,j),b)),c,d,0) for j in (c,d) if j != 0]
if b!=0: ret += [(*sorted((a,add_move(b,j))),c,d,0) for j in (c,d) if j != 0]
if c!=0: ret += [(a,b,*sorted((j,add_move(c,d))),0) for j in (c,d)]
ret = set(ret)
_cache[state] = ret
return ret.copy()
def gen_win_states(player):
if player == 0:
return [
(i,j,0,0,1) for i in range(NHANDS) for j in range(i,NHANDS)
][1:]
return [
(0,0,i,j,0) for i in range(NHANDS) for j in range(i,NHANDS)
][1:]
INF = 1.1
def gen_lookup_scores(player, _state=None, _cache=None, _visited=None):
if _state == None: _state = init_state
if _cache == None: _cache = {}
if _visited == None: _visited = set()
s = _state; t = s[-1]
w,p = is_win(s)
if w:
sc = [-1,1][p == player] * INF
_cache[s] = sc
return sc, _cache
if s in _cache:
return _cache[s], _cache
if s in _visited:
return 0, _cache # Idk
_visited = _visited.copy()
_visited.add(s)
ns = gen_possible(s)
if t==player:
ms = 0
for n in ns:
sc, _cache = gen_lookup_scores(player, n, _cache, _visited)
ms = max(ms, sc)
_cache[s] = ms
return ms, _cache
ms = []
for n in ns:
sc, _cache = gen_lookup_scores(player, n, _cache, _visited)
ms += [sc]
midx = ms.index(min(ms))
w = .7
sc = (1-w)*(sum(ms) - ms[midx]) + w*ms[midx]
_cache[s] = sc/len(ms)
return sc, _cache
p_lookup = [
gen_lookup_scores(0)[1],
gen_lookup_scores(1)[1]
]
def alphabeta(player, state, depth, visited, _a=-INF, _b=INF):
# `visited` doesn't contains `state`
s = state; t=s[-1]
visited = visited.copy(); visited.add(s)
w,p = is_win(s)
if w:
return [INF,-INF][p!=player]
if depth == 0:
return p_lookup[t][s] * [1,-1][t!=player]
ns = sorted([*(gen_possible(s) - visited)],
key=lambda x: p_lookup[1^t][x])
if player==t: # max tiem
val = -INF
for n in ns:
val = max(val, alphabeta(player, n, depth-1, visited, _a, _b))
if val >= _b:
break
_a = max(_a, val)
return val
val = INF
for n in ns:
val = min(val, alphabeta(player, n, depth-1, visited, _a, _b))
if val <= _a:
break
_b = min(_b, val)
return val
def evaluate_possible(state, depth, visited):
# `visited` doesn't contains `state`
visited = visited.copy(); visited.add(state)
t = state[-1]
ns = sorted([*(gen_possible(state) - visited)],
key=lambda x: p_lookup[1^t][x])
alpha = -INF
vs = []
for n in ns:
v = alphabeta(t, n, depth, visited, alpha)
vs.append(v)
alpha = max(v, alpha)
if alpha == INF:
return alpha, n
idx = vs.index(max(vs))
return vs[idx], ns[idx]
2
1000
CTFSG{I_respect_Ur_Cunning_BUT_no}
1
I found this script somewhere in Kebun Baru, it seems to require a password.
Players are given a singular python script that asks the user for the flag, and checks the input with a function verify
whether it's the correct flag.
1
2
3
4
5
6
flag = input("Input flag: ")
match = re.match(r'^CTFSG\{[a-zA-Z_]+\}$', flag)
if match and verify(flag.encode('utf-8')):
print("Flag is: %s"%flag)
else:
print("Wrong flag")
The verify
function is probably the most visually aesthetic piece of code I've ever written:
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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
verify = (
lambda h,e,w,r,o:h(e(h(h(w)(e(1)))(e(0))))(h(e(h(o(lambda f:lambda a:lambda b:(lambda c:lambda d:lambda i:lambda g:
c(a)(c(b)(e)(e(w)))(g(d(a))(d(b))(lambda
x
:f(i(a))(
i(b))(x))(e(w
))))(h(w)(e(e)))
(h(e(h(w)(e(e))
))(h(w)(e(e(w))))
)(h(e(h(w)(e(e(w))
)))(h (w)(e(e(w)))))(h(e
(h(h(e(h(e(h(h(h)(e(h(
e(h (h)(e(e))))(h(e(e))(h(e
(h))(h(e(h(w)))(e)))))))(e(h(e
(h(e (h(e(h(h)(e(e))))(h(e(e))(h
(h) (e(e(w)))))))))(h(e(h(h)(e(h(
e(h(e(h(e(h(w)))(e)))))(h(e(h(w))
)(e))))))(e))))))(e))))(e(e(w)))))(h
(e(h(h(e(h))(h(e(h(h)(e(h(e(h(h)
(e(h(e(e))(h(e(h))(e))))))(h(e(e))(
h(e(h(h)))(e)))))))(h(e(h(e(h))))(
h(e(h(h)(e(h(e(h(h)(e(h(e(e))(h(e(h))( e))))))(h(e(e)
)(h(e(h(h)))(h(e(e))(h(e(h(h)(e(e)))) (h(e(e))(h))))))))))(h
(e(e))(h(e(h))(h(e(e))(h(e(h(e(h(e(h(h)(e(h(h(h))(e)(h)))))(
e)))))(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h))(h(e(e))(
h(e(h(e(w))))(h(e(h))(h(e(e))(h(e(h(h)))(h(e(e))(e))
)))))))))))))))))))(h(e(h(e(e))))(h(e(h(h)(e(h(e(h
(w)))(h(e(e))(e))))))(h(e(e))(h(e(h(h)))(h(e(e)
)(e)))))))(e))))(e(h(e(h(h(h)(e(e(h(h(e(h(h(
w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))
(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h(h(w)(e(
h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(h
(e(h))(e))(h(h(e(h))(e))(w))))(w))))))(e(h
(e(h(h(w)(e(h(h(e(h))(e)))))))(e)(h(e(h(h(
h(e(h))(e)))))(e)(h(
h(e(h))(e))(w))
(h(h(e(h))
(e
))( w
) ))(h( e(h(
h(h (e(h ))( e
)))))( e)(h (h(e (h)
)(e))(h(h(e(h))(e))(h(h(e(
h))(e))(w))))(h(h(e(h))(e))
(w)))))))(h(e(h(h)(e(h(e(h(h)(
e(h(e(h(e(h(e(h))(h(e(e))(h(e(h
))(h(e(e))(h(e(h(w)))))))))))(h(
e(h(h)(e(h(e(e))(h(e(h(h)))(h(e(e)
)(e)))))))(h(e(e))(h(e(h(h)))(h(e(e
))(h(e(h(e(h(w)))))(h(h(e(h))(h(e(e
))(h(e(h(h)))(h(e(e))(e)))))(h(e(h(
e(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h(h(w
)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e
)))(h (h(e(h))(e))(h(e(h(h(h(e(h))(e)))))(
e)(h(h(e(h)) (e))(h(h(e(h))(e))(h(h(e(h))(e))(h
(h(e(h))(e))(h( h(e(h))(e))(w))))))(w))))))))(h(e
(h(w)))(h(e(h(h)( e(e(h(h(e(h))(e))(h(h(e(h(h(w)(e(
h(h(e(h))(e)))))))(e) )(h(h(e(h))(e)))(h(h(e(h))(e))(h(
h(e(h))(e))(h(h(e(h))(e))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h)
)(e))(w)))(h(h(e(h))(e))(w))))))))))))(h(w)))))))))(h(e(h(h)(e(h(e(h(w)))(h
(e(h(h)(e(e(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e)(h(h(e(h(h(h(e(h))(e))
)))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(w)))(h(e(h(h(h(e(h))(
e)))))(e)(h(h(e(h))(e))(w))(h(h(e(h))(e))(h(h(e
(h))(e))(h(h(e(h))(e))(w))))))))))(h(e(h(w)
))(h(e(h(h)(e(e(h(h(e(h))(e))(h(e(h(h(h(e
(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))
(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h
(e(h))(
e))(w))
))))(w)
))))))(
h(
w))))))
)))(h(e(e)
)(h(e(h(h)
))(h(e(e))(e
))))))))))))
))))(h(e(e))(h
(e(h(h)))(h(e(e
))(h(e(h(e(h(e(h(h(e
(h))(h(e(e))(h(e(h (h)
))(h(e(e))(e)))))))(
h(e(h(h)(e(h(e(h(h)(e(h
(w)))))(h(e(e))(h(e(h(h))
)(h(e(e))(e))))))))(h(e(
e))(h(e(h))(e))))))))(h(
e(h(h)(e(h( e(h(h)(e(h(w)))))(h(e(e))(
h(e(h(h)))(h(e(e))(e))))))))(h(e(e))(h(e(h
))(h(e(e))(h(e(h(e(h(w)))))(h(e(h(h)(e(h
(w)))))(h(e(e))(h(e(h(h)))(h(e(e))(e)
)))))))))))))))))(h(e(h(e(h(e(h))(
h(e(e))(h(e(h(h)(e(e(h(h(e(h))(e ) )(
h(h(e(h))(e))(h(h(e(h(h(w)(e(h (h (e (h
))(e)))))))(e))(h(h(e(h))(e)) )(h(h (e (h(
h(w)(e(h(h(e(h))(e)))))))(e)) (h(h(e(h))(e))
)(h(e(h(h(h(e(h))(e)))))(e)(h (h(e(h))(e))(h(h
(e(h))(e))(w)))(h(h(e(h))(e) )(w)))))))))))(h(
e(h))(h(e(e)) (h(e(h(e(h(h(e(h(
e(h(h(e(h( e(h(h(h)(e(e(h(e(h
(e(h( e(h(h)(e(e))))(h(e(
e))(h(e(h))(h(e(h(w
)))(e))))(e(w))))) )(h(e
(h(h)(e(h(e(e))(h( e(h))(h(
e(h(w)))(e))))))) (h(e(e))(h(
e(h(h(h)(e(e)))))(e))))))))(e(h(e(h(h(h(
e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(
h))(e))(h(e(h(h(h(e(h))(e)
))))(e)(h(h(e(h))(e))(h
(h(e(h))(e))(w)))(w))
))(w)))))(h(e(h(h)
(e(
e)))
) (h(e(h(e(h))(h(
w)))))))))(e(h(h(
e(h(h)(e(e(e(w))))))(
h(e(h(w)))(h(e(e))(h
(e(h(h)(e(e))))(h(e(e) )(h(e(h(
h(h(w)(e(h(h(e(h))(e))))))))(e))))))
)(h(h(e(h))(e)))(h(h(e(h))(e))(h
(e(h(h(h(e(h))(e)))))(e)(h(h(
e(h))(e))(h(h(e(h))(e))(w
) ))(w)))))))(h(h(e(h))(h(e
(e )) (h (e(h(h)))(h(e(e))(e))))))
)))(e(h(h(e(h(h( w)(e(h(h(
e(h))(e)))))))(e))
(h(h(e(h))(e)))(h(e
(h(h(h(e(h))(e)))))(e
)(h(h(e(h))(e))(h(h(e
(h))(e))(h(h(e(h))(e))(
h(h(e(h ))(e))(h(h(e(h))(e))
(w))))))(w ))))))))(h(e(h(h)(e(
h(e(h(e(h(e(h))(h (e(e))(h(e(h))(h(e(e))(h(e
(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h(h(w)(e(h
(h(e(h))(e)))))))(e))(h(h(e(
h))(e)))(h(h(e(h))(e))(h
(h(e(h))(e))(w))))))))
(h(e
(h
(w)))
(h(e(h(
h)(e(e(h (
h(e(h(h(w)(e
(h(h(e(h))(e))
)))))(e))(h(h(e(
h))(e)))(h(h(e(h)
)(e))(h(e(h(h(h(e(
h))(e)))))(e)(h(h(e(h
))(e))(h(h(e(h))(e))(w)))(w)))
)))))(h(e(h(w)))(h(e(h(h)(e(e(h(e(h(h(w
)(e(h(h(e(h))(e)))))))(e)(h(e(h(h(h
(e(h))(e)))))(e)(h(h
(e(h))(e))(w
))(h(h(e(h))(e))
(h(h(e(h))(e))(h(h
(e(h))(e))(w)))))(h
(e(h(h(h(e(h))(e)))))(
e)(h(h(e(h))(e))(h(h(e(h)
)(e))(h(h(e(h))(e))(w))))(h(
h(e(h))(e))(w))))))))(h(e(h(w))
)(h(e(h(h)(e(e(h(e (h(h(h(e(h))(e
)))))(e)(h(h(e(h) )(e))
(h(h(e(h))(e))
(h(h(e(h) )
(e))(h(h (
e(h))(e ) )(w)
)))) ( w)))))
)(h(e( h(w)))(
h(e(h( h)(e(e(h(h
(e(h)) (e))(h(h(e( h)
)(e ))(h(h(e(h))(e
))(h(h(e(h))(e
))(h( h(e(h(h(h(e(h))
(e)))))(e))(h(h(e(h))(e)
))(h(h(e(h))(e))(w)))
))))))))(h(w))))))
)))))))))))))))(h
(h(e(h))(h(e(e))(
h(e(h(h
)
))(h(e(e)
)(e))))))))))(
h(e(e))(h(e(h(h)
))(e)))))))))))))(
h(h(e(h))(h(e(e))(h(
e(h))(h(e(e))(h(e(h))(h
(e(e))(h(e(h(e(h(e(h(h)(e(
e))))(h(e(e))(h(e
(h))(h(e(h(w)))(h
(e(h(h )(e(e(h(h(e(h))(
e))(h(e( h(h(h(e(h))(e
)))))(e)(h (h(e(h))(
e))(h(h(e(h) ) (e))(h(
h(e(h))(e))(h(h ( e(h))(e
))(w)))))(h(h(e( h ))(e)
)(w))))))))(h(w))))))))))(h ( h(e(
h))(h(e(e))(h(e(h(h)))( h(e(e)
)(e)))))(h(e(h(e(h(w )))))
(h(h(e(h))(h(e(e))( h(e
(h (h )) )(h(e(e))(e)))))(h
(e(h(e(h(e(h (w)))
(h(e(h(h)(e(e(
h(h(e(h))(e))(h
(h(e(h(h(w)(e(h(
h(e(h ))(e)))))))(e))
(h(h(e(h)) (e)))(h(e(h(h(h
(e(h))(e)))))(e)(h(h(e(h))(e))(
h(h(e(h))(e))(h(h(e(h
))(e))(h(h(e(h))(
e))(
h(h(e(
h))(e))(w
))))))(w)))
)))))(h(w))))
)))(h(e(h(h)(
e(h(e(h(w)))(h(e(h(
h)(e(e(h(h(e(h(h(w)(e(h(h(e(h
))(e)))))))(e))(h(h(e(h))
(e)))(h(h( e(h))(e)
)(h(h(e(h))
(e))(h(e(h(h(
h(e(h))(e)))))(e
)(h(h(e(h))(e))(h(h
(e(h))(e))(w )))(w))))
)))))(h(w) )
)))))( h(e(e
))(h (e(h(
h)) )(h(e(e)
)(e) ))))))))))
))) ))(h(e(h(e
( h(h(e(h))(h(e(e)
)(h(e(h(h)))(h
(e(e))(e))))
)))))(h(e(h(
h)(
e
(h(e(e))(
h(e(h))(h(e(
e))(h(e(h(e(
h (e(h(w)))(h(e
(h(h)( e(e(h(e(h(h(
h(e(h))(e)))))(e)(h(h(e(
h))(e))(w))(h(h
(
e (h))
( e) )
(h(h( e(h))(e))(h(h(e(h
))(e))( w)))))))))(h(w))))))
)(h(e(h(h )(e(h( w)))))(h(e
(e))(h(e(h (h)))(h(e (e ))(e))
))))) ) )) ) ) ( h(e (e)) (h(e
(h(h ) ) ) ( h ( e ( e))(h (e(
h(e(h(e( h ( h ) ( e ( e)
)))(h(e ( e ) ) ( h ( e (
h))(h(w ) ) ) ) ) ) ) ) ( h (e(h
(h)(e(e) ) ) ) ( h ( e (e))(
h(e(h)) ( h ( e ( h (w)))(h
( e ( h ( h ( w ) ( h(h(w)(h(
h ( w ) ( e(h(e( h ( h )(e(e))))
( h ( e(e))(h(e ( h ) )(h(e(h(w)))(e)))
) ( e ) ( e))))(e(h ( h ( e (h(h(w)(e(h(h(
e ( h ) ) (e)))))))( e ) ) (h(h(e(h(h(w
) ( e (h(h(e(h))(e) ) ) ) )))(e))(h(h(
e ( h ) )(e))))(h(e(h(h ( h ( e (h))
( e ) ) )))(e)(h(h(e(h ) ) (
e ) )(h(h(e(h))(e)) (
h ( h (e(h))(e))(h(h ( e (
h ) ) (e))( w)))) ) ( w )
) ) )) )(e(h (e( h(h ( h (
e(h))(e)) )) )( e)(h( h(e( h) )( e) )(h(e(h(h
( h ( e(h) ) ( e ) ) )
) ) (e) ( h ( h (
e ( h) ) ( e )
) ( h ( h ( e
( h ) ) ( e
) ) ( w ) )
) ( w ) ) )
( w ) ) ) )
) ) ) ( e )
) ) ) ) ) )
) ) ) ) ) ) )
) ( h ( h ( e
( h ) ) ( e )
) ( h (h ( e (h )) (e )
) (h (e (h (h (h (e (h )) (e )
))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(w)))(w))))))))(o(lambda y:
lambda z:h(e(h(e(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(e))
))(e(w))))))(h(e(h(h)(e(h(e(e))(h(e(h))(h(e(h(w)))(e)))))))(h(e(e))(h(e(h(h(h)(e(e)))))(e))))(lambda x:y(z[1:])(x))(o(lambda y:
lambda m: h(h(e(h))(e))(y(r(m))) if r(m) else w)(z[0])) if z else h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(e))))(e)(e)))))(
lambda x:lambda y:lambda z:x(z)(y(z)),lambda x:lambda y:x,lambda x:x,lambda n:(n+2022)%127,lambda f:(lambda g:f(g(g)))(lambda g
:f(lambda y: g(g)(y))))
The intended solution is a side-channel attack. The verify
function is intended to look super freaking scary to deter any attempts to reverse it (it's still possible!). There are many ways to do the side channel, but the basic idea is that, there is some statistical difference in having n
characters correct, and n+1
characters correct.
This means that you can guess the 1st char and use the statistical difference to know if the 1st char is correct, and move on to the next char, and so on.
I chose to use sys.settrace
to log debugging events. But you can totally modify the verify
function like counting the number of times one of the lambda
s got called in verify
, it all works.
For my sys.settrace
method, experimentation shows if a character is guessed wrong, regardless of the next character, the difference in number of events is less than 1000
. If the character is guessed right, the number of events shoot up really obviously. This'll be the statistical difference to exploit.
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
import chal # Challenge code
import sys, time
t = time.time()
ALLOWED = b"_qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM{}"
N_EVT = 0
def tracer(frame, event, arg=None):
global N_EVT
N_EVT += 1
sys.settrace(tracer)
def side_channel(b:bytes)->int:
global N_EVT
N_EVT = 0
chal.verify(b)
return N_EVT
flag = []
while True:
for c in ALLOWED:
d = abs(side_channel(flag+[c,0]) - side_channel(flag+[c,1]))
print(f"{chr(c)} {d} ", end="\r")
if d > 1000: break
flag += [c]
flagpt = bytes(flag).decode('utf-8')
print(f"{flagpt}" + " "*15)
if chal.verify(flag):
break
print(f"DONE {time.time()-t}s")
Using pypy
bcuz it's faster, you'll get the flag in abt 250 seconds.
Time to actually reverse the challenge! I guess this counts as an unintended solution but if someone actually manages to do this, they totally deserve the flag.
So go ahead and use python formatter to prettify the challenge code, and you'd see at the bottom some interesting functions:
1
2
3
4
5
h = lambda x: lambda y: lambda z: x(z)(y(z)), # S
e = lambda x: lambda y: x, # K
w = lambda x: x, # I
r = lambda n: (n + 2022) % 127, # Something
o = lambda f: (lambda g: f(g(g)))(lambda g: f(lambda y: g(g)(y))), # Z Combinator
If you're familiar with lambda calculus, you might recognise some of the functions used:
h
: S Combinator in SKI Calculuse
: K Combinator in SKI Calculusw
: I Combinator in SKI Calculusr
: Meh just keep this in mindo
: Z Combinator
You might also notice a huge chunk of stuff made up of only h
, e
and w
.
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
e(h(e(h(h(h)(e(e(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(e(h(h(h(
e(h))(e)))))(e)(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(h(e(h))(e
))(h(h(e(h))(e))(w))))(w))))))(e(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e)(h(e(h(h(h(e(
h))(e)))))(e)(h(h(e(h))(e))(w))(h(h(e(h))(e))(w)))(h(e(h(h(h(e(h))(e)))))(e)(h(h
(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(w))))(h(h(e(h))(e))(w)))))))(h(e(h(h)(e(h
(e(h(h)(e(h(e(h(e(h(e(h))(h(e(e))(h(e(h))(h(e(e))(h(e(h(w)))))))))))(h(e(h(h)(e(
h(e(e))(h(e(h(h)))(h(e(e))(e)))))))(h(e(e))(h(e(h(h)))(h(e(e))(h(e(h(e(h(w)))))(
h(h(e(h))(h(e(e))(h(e(h(h)))(h(e(e))(e)))))(h(e(h(e(h(e(h(w)))(h(e(h(h)(e(e(h(h(
e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(h(e(h(h(h(e(h))(e
)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(w
))))))(w))))))))(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h))(e))(h(h(e(h(h(w)(e(h(h(e(h))(
e)))))))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(e(h(h(h(
e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(w)))(h(h(e(h))(e))(w))))))))))))(h(w
)))))))))(h(e(h(h)(e(h(e(h(w)))(h(e(h(h)(e(e(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e)(
h(h(e(h(h(h(e(h))(e)))))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(w)))(h(e(h(h(h(e(h))(e
)))))(e)(h(h(e(h))(e))(w))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(w))))))))))
(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h))(e))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h
(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(w))))))(w)))))))(h(w)))))
))))(h(e(e))(h(e(h(h)))(h(e(e))(e))))))))))))))))(h(e(e))(h(e(h(h)))(h(e(e))(h(e
(h(e(h(e(h(h(e(h))(h(e(e))(h(e(h(h)))(h(e(e))(e)))))))(h(e(h(h)(e(h(e(h(h)(e(h(w
)))))(h(e(e))(h(e(h(h)))(h(e(e))(e))))))))(h(e(e))(h(e(h))(e))))))))(h(e(h(h)(e(
h(e(h(h)(e(h(w)))))(h(e(e))(h(e(h(h)))(h(e(e))(e))))))))(h(e(e))(h(e(h))(h(e(e))
(h(e(h(e(h(w)))))(h(e(h(h)(e(h(w)))))(h(e(e))(h(e(h(h)))(h(e(e))(e))))))))))))))
))))(h(e(h(e(h(e(h))(h(e(e))(h(e(h(h)(e(e(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h(h(
w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))
(h(h(e(h))(e)))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(w)))(h(h(e
(h))(e))(w)))))))))))(h(e(h))(h(e(e))(h(e(h(e(h(h(e(h(e(h(h(e(h(e(h(h(h)(e(e(h(e
(h(e(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(e))))(e(w))))))(h(e(h(h)(e(h(e(
e))(h(e(h))(h(e(h(w)))(e)))))))(h(e(e))(h(e(h(h(h)(e(e)))))(e))))))))(e(h(e(h(h(
h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h)
)(e))(h(h(e(h))(e))(w)))(w))))(w)))))(h(e(h(h)(e(e))))(h(e(h(e(h))(h(w)))))))))(
e(h(h(e(h(h)(e(e(e(w))))))(h(e(h(w)))(h(e(e))(h(e(h(h)(e(e))))(h(e(e))(h(e(h(h(h
(w)(e(h(h(e(h))(e))))))))(e)))))))(h(h(e(h))(e)))(h(h(e(h))(e))(h(e(h(h(h(e(h))(
e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(w)))(w)))))))(h(h(e(h))(h(e(e))(h(e(h(h)))
(h(e(e))(e)))))))))(e(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(e(h
(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h
(e(h))(e))(w))))))(w))))))))(h(e(h(h)(e(h(e(h(e(h(e(h))(h(e(e))(h(e(h))(h(e(e))(
h(e(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))
(h(h(e(h))(e))(h(h(e(h))(e))(w))))))))(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h(h(w)(e(h(
h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(h(e(h(h(h(e(h))(e)))))(e)(h(h(
e(h))(e))(h(h(e(h))(e))(w)))(w))))))))(h(e(h(w)))(h(e(h(h)(e(e(h(e(h(h(w)(e(h(h(
e(h))(e)))))))(e)(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(w))(h(h(e(h))(e))(h(h(
e(h))(e))(h(h(e(h))(e))(w)))))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))
(e))(h(h(e(h))(e))(w))))(h(h(e(h))(e))(w))))))))(h(e(h(w)))(h(e(h(h)(e(e(h(e(h(h
(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(w))))
)(w))))))(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(
e(h))(e))(h(h(e(h(h(h(e(h))(e)))))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(w)))))))))))
(h(w)))))))))))))))))))))(h(h(e(h))(h(e(e))(h(e(h(h)))(h(e(e))(e))))))))))(h(e(e
))(h(e(h(h)))(e)))))))))))))(h(h(e(h))(h(e(e))(h(e(h))(h(e(e))(h(e(h))(h(e(e))(h
(e(h(e(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(h(e(h(h)(e(e(h(h(e(h))(e))(h(
e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(
w)))))(h(h(e(h))(e))(w))))))))(h(w))))))))))(h(h(e(h))(h(e(e))(h(e(h(h)))(h(e(e)
)(e)))))(h(e(h(e(h(w)))))(h(h(e(h))(h(e(e))(h(e(h(h)))(h(e(e))(e)))))(h(e(h(e(h(
e(h(w)))(h(e(h(h)(e(e(h(h(e(h))(e))(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(
h))(e)))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(h(h
(e(h))(e))(h(h(e(h))(e))(w))))))(w))))))))(h(w)))))))(h(e(h(h)(e(h(e(h(w)))(h(e(
h(h)(e(e(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h))(e)))(h(h(e(h))(e))(h(h(
e(h))(e))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(w)))(w)))))))))(
h(w)))))))(h(e(e))(h(e(h(h)))(h(e(e))(e))))))))))))))))(h(e(h(e(h(h(e(h))(h(e(e)
)(h(e(h(h)))(h(e(e))(e)))))))))(h(e(h(h)(e(h(e(e))(h(e(h))(h(e(e))(h(e(h(e(h(e(h
(w)))(h(e(h(h)(e(e(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(w))(h(h(e(h))(e))(h(h
(e(h))(e))(h(h(e(h))(e))(w)))))))))(h(w)))))))(h(e(h(h)(e(h(w)))))(h(e(e))(h(e(h
(h)))(h(e(e))(e))))))))))))(h(e(e))(h(e(h(h)))(h(e(e))(h(e(h(e(h(e(h(h)(e(e))))(
h(e(e))(h(e(h))(h(w))))))))(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(h(e(h(h(
w)(h(h(w)(h(h(w)(e(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(e))))(e)(e))))(e(
h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(e(h(h(w)(e(h(h(e(h))(e)))))))(e))(h(h(
e(h))(e))))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(h(h(e(h))(e))(
h(h(e(h))(e))(w)))))(w))))))(e(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(e(h(h(h
(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e))(w)))(w)))(w)))))))(e)))))))))))))))
(h(h(e(h))(e))(h(h(e(h))(e))(h(e(h(h(h(e(h))(e)))))(e)(h(h(e(h))(e))(h(h(e(h))(e
))(w)))(w)))))
Since said “huge chunk” is made up of only SKI combinators, we could beta-reduce it into regular lamba expressions and hope that it converges.
We don't need to write our own beta reduction algo (tho I did write it, see The Making
), we could just use one online.
This one online however has some annoying limits: Reduction error: length exceeded 1000
, so I just modified the js
limit to be some ungodly huge number, and the site outputs the following (huge) lambda expression:
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
(x0->(x1->x1((x2->(x3->x3)))((x4->x4((x5->(x6->x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5
(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5
(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5
(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x5(x6))))))))))))))))))))))
))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))(
(x7->x7((x8->(x9->x9)))((x10->x10((x11->(x12->x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11
(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11
(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11
(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11
(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x11(x12)))))))))))))))))))))))))))))))))))))))))))))))))))))
))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x13->x13((x14->(x15->x15)))((x16->x16((x17->
(x18->x17(x17(x17(x17(x17(x17(x17(x18))))))))))((x19->x19((x20->(x21->x21)))((x22->x22((x23->(x24->x23(x23
(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x23(x24))))))))))))))))))))))))(
(x25->x25((x26->(x27->x27)))((x28->x28((x29->(x30->x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29
(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29
(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29
(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29(x29
(x29(x30)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))(
(x31->x31((x32->(x33->x33)))((x34->x34((x35->(x36->x35(x35(x35(x35(x35(x35(x35(x35(x35(x35(x35(x35(x35(x35
(x35(x35(x35(x35(x35(x35(x35(x35(x35(x35(x35(x36))))))))))))))))))))))))))))((x37->x37((x38->(x39->x39)))(
(x40->x40((x41->(x42->x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x41(x42))))
)))))))))))))))))))((x43->x43((x44->(x45->x45)))((x46->x46((x47->(x48->x47(x47(x47(x47(x47(x47(x47(x47(x47
(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47
(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47
(x47(x47(x47(x47(x47(x47(x47(x47(x47(x47(x48)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
)))))))))))((x49->x49((x50->(x51->x51)))((x52->x52((x53->(x54->x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53
(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53
(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53
(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53
(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x53(x54)))))))))))))))))))))
)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x55->x55(
(x56->(x57->x57)))((x58->x58((x59->(x60->x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59
(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59
(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59
(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59(x59
(x59(x60)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
)((x61->x61((x62->(x63->x63)))((x64->x64((x65->(x66->x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65
(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65
(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65(x65
(x65(x65(x65(x65(x65(x65(x65(x66)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
)((x67->x67((x68->(x69->x69)))((x70->x70((x71->(x72->x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71
(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71
(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x71(x72)))))))))))))))))))))
))))))))))))))))))))))))))))))))))))))))))))((x73->x73((x74->(x75->x75)))((x76->x76((x77->(x78->x77(x77(x77
(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77
(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77
(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77
(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x77(x78)))))))))))))))))))))))))))))))))))))))))))))
)))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x79->x79((x80->(x81->x81)))((x82->x82((x83->
(x84->x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83
(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x83(x84)))))))))))))))))
))))))))))))))))))))))))))))))))))((x85->x85((x86->(x87->x87)))((x88->x88((x89->(x90->x89(x89(x89(x89(x89(x89
(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89(x89
(x89(x89(x89(x89(x90))))))))))))))))))))))))))))))))))))))))((x91->x91((x92->(x93->x93)))((x94->x94((x95->
(x96->x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95
(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95
(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x95(x96)))))))))))))))))))))))))
)))))))))))))))))))))))))))))))))))))))))))))))))))((x97->x97((x98->(x99->x99)))((x100->x100((x101->
(x102->x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101
(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101
(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101
(x101(x101(x101(x101(x101(x101(x101(x101(x101(x101(x102))))))))))))))))))))))))))))))))))))))))))))))))))))))
)))))))))))))))))))))((x103->x103((x104->(x105->x105)))((x106->x106((x107->(x108->x107(x107(x107(x107(x107
(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107
(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107
(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107
(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107
(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107(x107
(x107(x107(x107(x108)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
)))))))))))))))))))))))))))((x109->x109((x110->(x111->x111)))((x112->x112((x113->(x114->x113(x113(x113(x113
(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113
(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113
(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113(x113
(x113(x113(x113(x113(x113(x113(x114))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
))((x115->x115((x116->(x117->x117)))((x118->x118((x119->(x120->x119(x119(x119(x119(x119(x119(x119(x119(x119
(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119
(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119
(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119
(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119
(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119(x119
(x119(x119(x119(x119(x119(x119(x119(x120)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
)))))))))))))))))))))))))))))))))))))))))))))))))))))))((x121->x121((x122->(x123->x123)))((x124->x124((x125->
(x126->x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125
(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125
(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125
(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125
(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125
(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125(x125
(x125(x126)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
))))))))))))))))))))))))))))))((x127->x127((x128->(x129->x129)))((x130->x130((x131->(x132->x131(x131(x131
(x131(x131(x131(x131(x131(x131(x131(x131(x132))))))))))))))((x133->x133((x134->(x135->x135)))((x136->x136(
(x137->(x138->x137(x137(x137(x137(x137(x137(x137(x137(x137(x137(x137(x138))))))))))))))((x139->x139((x140->
(x141->x141)))((x142->x142((x143->(x144->x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143
(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143
(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143
(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x143(x144)))))))))
))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x145->x145((x146->(x147->x147)))(
(x148->x148((x149->(x150->x149(x149(x149(x149(x149(x149(x149(x149(x149(x149(x149(x150))))))))))))))(
(x151->x151((x152->(x153->x153)))((x154->x154((x155->(x156->x155(x155(x155(x155(x155(x155(x155(x155(x155(x155
(x155(x155(x155(x155(x155(x155(x155(x155(x155(x155(x155(x155(x155(x156))))))))))))))))))))))))))((x157->x157(
(x158->(x159->x159)))((x160->x160((x161->(x162->x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161
(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161
(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161
(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x161(x162)))))))))
)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x163->x163((x164->(x165->x165)))(
(x166->x166((x167->(x168->x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167
(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x167(x168))))))))))))))))))))))))
)))))))))))((x169->x169((x170->(x171->x171)))((x172->x172((x173->(x174->x173(x173(x173(x173(x173(x173(x173
(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173
(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173
(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173(x173
(x173(x173(x174)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x175->x175(
(x176->(x177->x177)))((x178->x178((x179->(x180->x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179
(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179
(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179
(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179
(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179
(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x179(x180))))))))))))))))))))))))))))))))))
)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x181->x181((x182->
(x183->x183)))((x184->x184((x185->(x186->x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185
(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185
(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185
(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x185(x186))))))))))))))
))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))((x187->x187((x188->(x189->x189)))((x190->x190(
(x191->(x192->x191(x191(x191(x191(x191(x191(x191(x191(x191(x191(x191(x192))))))))))))))((x193->x193((x194->
(x195->x195)))((x196->x196((x197->(x198->x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197
(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197
(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197
(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197
(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197(x197
(x197(x197(x197(x198)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
))))))))))))))((x199->x199((x200->(x201->x201)))((x202->x202((x203->(x204->x203(x203(x203(x203(x203(x203(x203
(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203
(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203
(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203(x203
(x203(x203(x203(x203(x203(x203(x204))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
)))))((x205->x205((x206->(x207->x206)))((x208->(x209->x208)))))))))))))))))))))))))))))))))))))))))))))))))))
))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
Finally! Patterns! It doesn't take a seer to guess that this probably represents the string that verify
is checking user input against, which is the flag!
The lambda expression consists of a lot of repeated applications of a function, like f(f(f(...)))
. One can guess that the string is encoded with Church Encoding, though it is also feasible to reverse that out without guessing, since it's relatively readable now.
Reversing further will find that I represented the flag as a linked list of integers. You can read the article I linked above about Church Encoding on how exactly I did that.
Getting out the array of integers in that lambda expression is relatively striaght-forward:
1
2
3
4
5
6
7
8
lambda_expression = "(x0->(x1->x1((x2->(x3-> ... )))))))))))))))"
nums = [lambda_expression.count(f"x{i}(") for i in range(208)]
nums = [i for i in nums if i>2] # Whatever, crude approximation
print(nums)
print(bytes(nums))
# > [121, 110, 7, 21, 96, 25, 20, 73, 113, 99, 75, 62, 99, 48, 37, 73, 72, 113, 73, 121, 126, 11, 11, 74, 11, 23, 73, 32, 72, 110, 73, 11, 100, 76]
# > b'yn\x07\x15`\x19\x14IqcK>c0%IHqIy~\x0b\x0bJ\x0b\x17I HnI\x0bdL'
Oh but the numbers are gibberish! However, recall that while a Church Encoding for an integer is n_church = f -> x -> f^n(x)
, we still need a way to convert from the Church Encoding to an actual integer! The common way to do it is to simply do n_church(x -> x+1)(0)
. This would simply apply x -> x+1
, n
times to 0
and you'll output the integer n
. That's not what is happening in the challenge! (I didn't do that to slightly harden the challenge. Tho if you've made it this far, it shouldn't hinder you too much either.)
So, verify
would have to Church Encode the user input in order to compare the user input with the string within verify
, or the other way around. Reversing a little more on the verify
function, specifically this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
o( # char by char loop
lambda y: lambda z: h(
e(h(e(h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(e))))(e(w)))))
)(
h(e(h(h)(e(h(e(e))(h(e(h))(h(e(h(w)))(e)))))))(
h(e(e))(h(e(h(h(h)(e(e)))))(e))
)
)(
lambda x: y(z[1:])(x)
)(
o( # char -> church loop
lambda y: lambda m: h(h(e(h))(e))(y(r(m))) if r(m) else w
)(z[0])
)
if z
else h(e(h(h)(e(e))))(h(e(e))(h(e(h))(h(e(h(w)))(e))))(e)(e)
)
Would show that it's the former. Also notice how r = lambda n: (n + 2022) % 127
is being used? It turns out, verify
converts the integer n
into the Church Encoding f -> x -> f^m(x)
, where r^m(n) = (n + 2022*m) % 127 = 0
. This means, to convert a Church Encoded n_church
integer back into an integer in verify
, you'd do
n_church(x -> (x - 2022)%127)(0)
, or, since it's easier for us, (n_church(x -> x+1)(0)*-2022) % 127
.
Fun fact, if I could write the above in SKI combinators, I would, but Python doesn't do lazy evaulation properly so that's impossible.
Let's modify the code above to do the latter to get the flag!
1
2
3
4
5
6
7
lambda_expression = "(x0->(x1->x1((x2->(x3-> ... )))))))))))))))"
nums = [lambda_expression.count(f"x{i}(") for i in range(208)]
nums = [i for i in nums if i>2] # Whatever, crude approximation
nums = [(-i*2022)%127 for i in nums]
print(bytes(nums))
# > b'CTFSG{I_respect_Ur_Cunning_BUT_no}'
This is The Fun Part, it'll take too long to write abt all the detail I wanna write so maybe I'll write a follow up post just on the SKI compiler itself. If you're interested, here's the source
If you've read the 2nd solution for this challenge you'll know that the challenge is written in SKI Combinators, and implements Church Encodings.
But SKI Combinators is an insanely low level language to write in so I actually wrote my lambda expressions in Floof syntax, an esolang I wrote a while back, and used a compiler I wrote last year, but never published, to compile into SKI Combinators.
A little on floof syntax: The function $\lambda x. y$ is represented as
[x:y]
. Imo its a way more elegant thing to write.
Once I've got the challenge code, I then made an image of a bird flying out of a cage and formatted the code look like that.
If you look at the wikipedia page for Combinatory Logic, you'd see a compiler $T$ described in 6 lines:
\[\begin{aligned} T[x] &= x \\ T[AB] &= T[A] \: T[B] \\ T[\lambda x. E] &= K \:T[E] & \text{if x does not occur free in E} \\ T[\lambda x.x] &= I \\ T[\lambda x. \lambda y. E] &= T[\lambda x. T[\lambda y.E]] & \text{if x occurs free in E} \\ T[\lambda x. AB] &= S \: T[\lambda x.A] \: T[\lambda x.B] & \text{if x occurs free in A and B} \end{aligned}\]This is what it looks like compiling the numbers 0
to 3
from church encoding to SKI:
1
2
3
4
5
6
7
8
9
10
>>> str2str_compile("[f:[x:x]]")
'K(I)'
>>> str2str_compile("[f:[x:f(x)]]")
'S(S(K(S))(S(K(K))(I)))(K(I))'
>>> str2str_compile("[f:[x:f(f(x))]]")
'S(S(K(S))(S(K(K))(I)))(S(S(K(S))(S(K(K))(I)))(K(I)))'
>>> str2str_compile("[f:[x:f(f(f(x)))]]")
'S(S(K(S))(S(K(K))(I)))(S(S(K(S))(S(K(K))(I)))(S(S(K(S))(S(K(K))(I)))(K(I))))'
>>> len(str2str_compile(church(128)))
2308
As you can see, the length of the compiled expression grows pretty quickly. Compiling the number 128
results in an expression of over 2k chars! In fact, simply trying to evaluate the number 33
results in a parsing stack overflow in Python 3.8, let alone evaluating a whole string!
1
2
3
4
5
>>> eval(str2str_compile(church(33)))
s_push: parser stack overflow
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError
Clearly the naive compiler method isn't scalable enough.
If you were to take a look at my compiler, which is in need of reorganising, you'd see that a lot of it has to do with optimization, in particular, the compiler optimises based on a set of Rules.
1
2
3
4
5
6
7
8
9
10
11
12
def compile_SKI(expr:SKInode_T, optimisation:bool=True) -> SKInode_T:
opt_rules = [
Rule("M(L)(N(L))", "S(M)(N)(L)"),
Rule("S(K(S(K(A))))(S(K(S(K(B))))(C))", "S(K(S(K(S(K(A))(B)))))(C)"),
Rule("S(K(S(K(A))))(S(K(B)))", "S(K(S(K(A))(B)))"),
Rule("S(K(A))(K(B))", "K(A(B))"),
Rule("S(K(A))(I)", "A"),
Rule("S(S(K(A))(B))(K(C))", "S(K(S(A)(K(C))))(B)"),
Rule("S(S(I(A)))(I)", "S(S(S)(K))(A)"),
] if optimisation else []
expr = _compile_SKI(expr, opt_rules)
return expr
Every time the compiler does a step in the naive compiler, it traverses the entire AST to see if it can apply any of the above rules to simplify the AST. Needless to say this is really slow so I implemented aggressive caching to greatly speed up the optimization.
Here's the results:
1
2
3
4
5
6
7
8
9
10
>>> str2str_compile("[f:[x:x]]")
'K(I)'
>>> str2str_compile("[f:[x:f(x)]]")
'I'
>>> str2str_compile("[f:[x:f(f(x))]]")
'S(S(K(S))(K))(I)'
>>> str2str_compile("[f:[x:f(f(f(x)))]]")
'S(S(K(S))(K))(S(S(K(S))(K))(I))'
>>> len(str2str_compile(church(128)))
1906
Unfortunately, It's Not Enough as now our limit is only 99
. An improvement from 33
, but not exactly ideal.
1
2
3
4
5
>>> eval(str2str_compile(church(99)))
s_push: parser stack overflow
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError
Remember how a church encoded integer $n$ is simply [f:[x:f^n(x)]]
? So far we've simply written $n$ as [f:[x:f(f(f(...(x)...)))]]
and asked the compiler to compile it. That's not ideal, as the starting expression is alr pretty long. Take for example, this expression that represents $8^9 = 134217728$: [p:p(p([f:[x:f(f(x))]]))]([a:[x:a(a(a)(x))]])
. Clearly this expression is many times more efficient than our naive scheme.
1
2
3
4
5
>>> expr = "[p:p(p([f:[x:f(f(x))]]))]([a:[x:a(a(a)(x))]])"
>>> eval(str2str_compile(expr) + "(lambda n:n+1)(0)")
134217728
>>> str2str_compile(expr)
'S(I)(S(I)(K(S(S(K(S))(K))(I))))(S(S(K(S))(K))(S(I)(I)))'
To find more efficient representations, am gonna introduce operations on church integers. Take for instance [n:[f:[x:f(n(f)(x))]]]
. This increments a church numeral. There are also lambda expressions that do addition, multiplication, subtraction, power, etc.
The idea is to find a more efficient representation in the form of these operations. E.g. $8^9$ can be represented as $8^9 = p(p(2)), p(x) = x^{x+1}$, and results in the above concise expression.
I hence basically bruteforced the operations to find the most concise one. I start with a list of church numerals encoded with the naive method, and perform operations on them, saving the shorter one until the list converges.
See here to see the final results. In particular, 128
went down from needing 1906
characters to represent to only 121
characters. The source code for this bruteforce can be found here.
I then used this representation to craft the church encoded flag string and used it in the challenge.
11
980
CTFSG{81aee17d64bd59cd167ffce34523060f7e2bcac0}
1
XorXorXorXorXorXorXorXorXorXorXorXorXorXorXorXor
Players are given a script xor.py
and a file ct
. xor.py
generates a random key and plaintext, and xors the key with the plaintext, and saves the 'encrypted' text into the file ct
. The key length is random, and the plaintext is made up of a characters in string.ascii_lowercase + '_- .!?'
. Players are required to decrypt ct
without the key and find the sha1
hash of the plaintext.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import string
import random
from itertools import cycle
from hashlib import sha1
allowed_chars = string.ascii_lowercase + '_- .!?'
allowed_chars = allowed_chars.encode('utf-8')
xor_enc = lambda pt,key: [x^y for x,y in zip(pt, cycle(key))]
pt = [allowed_chars[i&31] for i in os.urandom(0x100000)]
key = os.urandom(random.randint(10,0x1000))
ct = xor_enc(pt, key)
open('flag', 'w').write(f"CTFSG{sha1(bytes(pt)).hexdigest()}")
open('ct', 'wb').write(bytes(ct))
Since the plaintext is made up of only printable characters, the MSB
of each char is always 0
. Hence, upon xor-encrypt with the key, the MSB
of each 'block', which is the same length as the key, should always be the same. We can use this property to find the key length.
For each character of the key, we can guess among the 256
possibilities. We'd know that a guess is correct if when we decrypt all the ct
bytes that got xored by said key character, they are all within allowed_chars
.
Upon recovering the key, we can simply compute the sha1
hash and get 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
31
32
33
34
35
36
37
38
39
40
41
42
43
from itertools import cycle
import string
from hashlib import sha1
ct = open("../dist/ct", 'rb').read()
allowed_chars = string.ascii_lowercase + '_- .!?'
allowed_chars = allowed_chars.encode()
def is_key_len(ct_msb, klen):
for x,y in zip(ct_msb[:klen], ct_msb[klen:2*klen]):
if x!=y: return False
return True
ct_msb = [x >> 7 for x in ct[:0x20000]]
klen = 10
while not is_key_len(ct_msb, klen):
klen += 1
print("Key length:", klen)
# p^k = c
# Find for possible p, the possible k given c
c_to_k = {}
for p in allowed_chars:
for k in range(0x100):
c = p^k
if c not in c_to_k:
c_to_k[c] = set()
c_to_k[c].add(k)
# Recover the key
key = []
for kidx in range(klen):
data = ct[kidx::klen]
k = c_to_k[data[0]].copy()
for c in data[1:]:
k &= c_to_k[c]
assert len(k) == 1, "Not enough info!"
key.append(list(k)[0])
# Recover secret
pt = [c^k for c,k in zip(ct, cycle(key))]
flag = f"CTFSG{sha1(bytes(pt)).hexdigest()}"
print(flag)
2
1000
CTFSG{d0_n0t_r0ll_y0ur_0wn_ae_ev4r}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Okay so you've heard of AE? It's so in right now.
Even Signal uses AES-CBC HMAC-SHA256.
I too, wanna use AES-CBC HMAC-SHA256,
but python doesn't really have one built in.
EZ, I made mah own.
With some goodies too!
1. Random IVs!
2. Salted keys!
3. Unknown padding! (No padding oracles here!)
This is obviously so secure right now.
http://chals.ctf.sg:10201
This challenge is based on the Cryptographic Doom Principle.
Players are given a http server to interact with and the source code for the server. The server assumes the player's name is Ash, and prompts the player to change their name. The player's name is stored as a cookie, which is an encrypted string containing the username
and the flag
. The player's goal is to exploit the decryption routine to leak the flag
.
This is my attempt at writing a “realistic” challenge that isn't too unwieldy. There's a common problem in the industry where developers combine otherwise secure cryptographic primitives together in insecure ways, and am tryna emulate that.
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
from Crypto.Cipher import AES
from urllib import parse
import base64
import hmac
import re
import os
import secret # Only the server has this >-<
from flask import Flask
from flask import request, make_response, render_template
app = Flask(__name__, template_folder='templates/')
class CryptError(Exception):
pass
class CookieError(Exception):
pass
class Crypt:
'''
Encrypt/Decrypt object that totally securely
implements AES-CBC HMAC-SHA256 yall:
Encrypted message format:
final_encrypt = [salt:16][iv:16][encrypted_data]
encrypted_data = AESCBC(PAD([len:2][data][hmac:32]))
hmac = HMACSHA256([len:2][data])
Awesome features of this implementation:
1. Random IVs!
2. Salted keys!
3. Unknown padding! (No padding oracles here!)
'''
def __init__(self, key:bytes):
if len(key) != 16:
raise CryptError("Invalid key size. `key` has to be 16 bytes.")
self.key = key
@staticmethod
def _pad(data:bytes)->bytes:
'''
Pads `data` to blocksize 16 with random
bytes. No padding oracle attack here >-<
'''
i = -len(data)%16
return data+os.urandom(i)
@staticmethod
def _unpad(data:bytes, length:int)->bytes:
'''
Unpads `data`. `length` must be given as
self._pad pads with random bytes.
'''
return data[:length]
def _AESCBC(self, data:bytes, salt:bytes, decrypt=False)->bytes:
'''
Encrypts/decrypts `data` with self.key
If `decrypt` == False (default),
`data` is encrypted with AES-CBC and
the iv appended to the start.
If `decrypt` == True,
`data` is decrypted, assuming the iv is
the first 16 bytes of `data`
'''
iv = data[:16] if decrypt else os.urandom(16)
cipher = AES.new(self.key+salt, AES.MODE_CBC, iv=iv)
if decrypt:
return cipher.decrypt(data[16:])
return iv + cipher.encrypt(data)
def _HMAC(self, data:bytes, salt:bytes)->bytes:
'''
Returns HMAC-SHA256 of `data`
'''
auth = hmac.new(self.key+salt, digestmod='sha256')
auth.update(data)
return auth.digest()
def encrypt(self, data:bytes)->bytes:
'''Encrypts `data`'''
length = len(data)+34
if not (length < 1<<16):
raise CryptError("Encryption of `data` length %d>%d not supported"%(length, 1<<16))
# Generate salt
salt = os.urandom(16)
# Add length field
data = length.to_bytes(2, 'little') + data
# Generate signature
hmac = self._HMAC(data, salt)
data += hmac
# Pad and AES encrypt
data = self._pad(data)
data = self._AESCBC(data, salt)
# Add salt to data and return
return salt+data
def decrypt(self, data:bytes)->bytes:
'''Decrypts data and verifies signature'''
if len(data)%16 != 0 and len(data)>=80:
raise CryptError("`data` has to be multiple of 16 and at least length 80")
# Get salt
salt, data = data[:16], data[16:]
# Verify length field before unpadding
data = self._AESCBC(data, salt, decrypt=True)
length = int.from_bytes(data[:2], 'little')
if not (len(data) < 1<<16 and len(data)-16 < length <= len(data)):
raise CryptError("`data` has invalid length")
# Unpad data
data = self._unpad(data, length)
# Verify hmac
data, hmac = data[:-32], data[-32:]
if not hmac == self._HMAC(data, salt):
raise CryptError("`data` does not have a valid signature")
# Remove length field
data = data[2:]
return data
class Cookie:
def __init__(self):
self.crypt = Crypt(secret.key)
self.data = None
pass
def parse(self, cookie:str)->bool:
'''
Parses `cookie`.
Inits self.cookie, self.data.
@Returns bool
True if success
False if fail. Error can be accessed at self.error
'''
self.cookie = cookie
try:
cookie = base64.b64decode(cookie+'='*4) # fix padding
except Exception as e:
self.error = e
return False
try:
serialized = self.crypt.decrypt(cookie).decode('utf-8', errors='ignore')
except CryptError as e:
self.error = e
return False
self.data = dict(parse.parse_qsl(serialized, strict_parsing=True))
return True
def gen(self, username:str)->bool:
'''
Generates cookie based on `username`.
Inits self.cookie, self.data.
@Returns bool
True if success
False if fail. Error can be accessed at self.error
'''
naked_flag = re.findall(r"^CTFSG\{([a-z0-9_]+)\}$", secret.flag)[0]
self.data = {
"username": username,
"flag": naked_flag,
"placeholder": "yo!~~"
}
serialized = parse.urlencode(self.data).encode('utf-8', errors='ignore')
try:
cookie = self.crypt.encrypt(serialized)
except CryptError as e:
self.error = e
return False
self.cookie = base64.b64encode(cookie)
return True
def get(self, param:str):
'''
Gets `param` from cookie
@Returns bool
True if success
False if fail. Error can be accessed at self.error
'''
try:
if type(self.data) != dict:
raise CookieError("Cookie not initialised! Call self.gen or self.parse")
if param not in self.data:
raise CookieError("Param `%s` not in cookie!"%param)
except CookieError as e:
self.error = e
return False, None
return True, self.data[param]
@app.route('/', methods=['POST', 'GET'])
def index():
cookie = Cookie()
status = True
username = None
if request.method=='POST' and 'username' in request.form:
username = request.form['username']
status &= cookie.gen(username)
else:
user = request.cookies.get('user')
status &= cookie.parse(user) if user else cookie.gen("Ash")
if status:
status, username = cookie.get("username")
error = None if status else str(cookie.error)
if not status:
cookie.gen('Ash')
_,username = cookie.get("username")
resp = make_response(
render_template(
"index.html",
username=username,
error=error
)
)
resp.set_cookie("user", cookie.cookie)
return resp
The data that is being encrypted has this format: b'username={username}&flag={flag}&placeholder=yo%21~~'
, and obviously we wanna leak the flag
parameter.
The server code also documents the encrypted message format:
1
2
3
4
Encrypted message format:
final_encrypt = [salt:16][iv:16][encrypted_data]
encrypted_data = AESCBC(PAD([len:2][data][hmac:32]))
hmac = HMACSHA256([len:2][data])
And the decryption routine:
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
def decrypt(self, data:bytes)->bytes:
'''Decrypts data and verifies signature'''
if len(data)%16 != 0 and len(data)>=80:
raise CryptError("`data` has to be multiple of 16 and at least length 80")
# Get salt
salt, data = data[:16], data[16:]
# Verify length field before unpadding
data = self._AESCBC(data, salt, decrypt=True)
length = int.from_bytes(data[:2], 'little')
if not (len(data) < 1<<16 and len(data)-16 < length <= len(data)):
raise CryptError("`data` has invalid length")
# Unpad data
data = self._unpad(data, length)
# Verify hmac
data, hmac = data[:-32], data[-32:]
if not hmac == self._HMAC(data, salt):
raise CryptError("`data` does not have a valid signature")
# Remove length field
data = data[2:]
return data
As you can see the encrypted message is “verified” by the hmac
parameter, so if you were to change the ciphertext, the server will error out at data does not have a valid signature
as the hmac won't match. Furthermore, the padding consists of random bytes, and so in order for the decryption routine to unpad the message, the length of the data
has to be stored within the encrypted message format.
However, note that the hmac
is computed on the decrypted data. This means that, in order for the server to verify the hmac
, it has to first decrypt the data, and then compute the hmac
. Notice anything dangerous in this design choice? Can you leak any information about the decrypted data before the hmac
is computed?
Turns out the length
field is one part of the decrypted data that is being leaked! If the server returns no errors or a data does not have a valid signature
error, we know that the length field has to satisfy this check: if not (len(data) < 1<<16 and len(data)-16 < length <= len(data))
, otherwise the server would have returned a data has invalid length
error!
So what if we can trick the server into thinking the length
field is something else, like the flag? Then we can leak information about the flag!
Because the encryption algorithm uses the CBC Block Ciphering Mode, we could easily have the server decrypt from a random block in the encryption instead of from the start. We do this simply by feeding the previous block as the IV.
Here's what we can do:
username
to be a length such that the server intepretes one of the chars of the flag to be the length field if you start decrypting from another block.data has invalid length
, the guess is wrong: Guess again. Otherwise repeat for the next chars.It's a little finicky to get all the offsets right but once done, the flag can be recovered in ~700 requests to the server.
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
import requests
import base64
from threading import Thread
URL = "http://192.168.1.112:7777"
n_requests = 0
def get_cookie(username: bytes):
global n_requests
n_requests += 1
r = requests.post(URL, data={b'username': username})
return base64.b64decode(r.cookies['user'])
def test_cookie(cookie):
global n_requests
n_requests += 1
cookie = base64.b64encode(cookie).decode('utf-8')
r = requests.get(URL, cookies={'user': cookie})
content = r.content.decode('utf-8')
if "An error occurred!" not in content:
return 0 # No error
if "`data` has invalid length" in content:
return 1 # Length invalid
if "`data` does not have a valid signature" in content:
return 2 # Length valid
return 3 # Should not be encountering this case
def pad(data: bytes):
i = -len(data) % 16
return data + b'\0'*i
def job(c: int, send: bytes):
global job_ret
if job_ret:
return
case = test_cookie(send)
if case == 2:
job_ret = c # Valid length! value of `c` has been leaked!
allowed = b'0123456789_qwertyuiopasdfghjklzxcvbnm?'
known = ord(b'=')
k = 6
flag = ""
idx = 0
while True:
enc = get_cookie(b'A'*(64-idx))
salt = enc[:16]
curr = enc[16*k:16*k+32]
threads = []
job_ret = None
for c in allowed:
i = c*0x100 + known - 16
send = salt + curr + b'\0'*i
send = pad(send)
threads.append(Thread(target=job, args=(c, send)))
for t in threads:
t.start()
for t in threads:
t.join()
known = job_ret
# None of `allowed` matches. End of flag.
if not known:
break
flag += chr(known)
idx += 1
print(flag, end="\r")
print(f"Flag: CTFSG{flag}")
print("Number of requests made:", n_requests)
2
1000
CTFSG{https://arxiv.org/abs/1802.03367?salt=290nlk01nx}
1
2
3
4
Two times the RSA mean two times the security.
Connect to the service at:
chals.ctf.sg:10301
The player is given a server to connect to and the source code to said server. The server pads the flag
with a half-assed OAEP so it's reasonably secure.
The server then encrypts the flag with a keys pub1, priv1
, and then encrypts priv1
with keys pub2, priv2
.
The encrypted flag, encrypted priv1
, and both public keys are made known to the player.
The server then repeatedly asks for enc_priv1
and tries to decrypt flag
with the player's input for enc_priv1
.
If the decrypted flag matches the actual flag
, the server exits.
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
from typing import Tuple
from Crypto.Util.number import bytes_to_long, getPrime
from os import urandom
from hashlib import shake_128
from flag import flag
e = 1+(1 << 16)
def xor_bytes(a, b): return type(a)([x ^ y for x, y in zip(a, b)])
def gen(strength: int) -> Tuple[int, int]:
p = getPrime(strength)
q = getPrime(strength)
n = p*q
phi = n - p - q + 1
d = pow(e, -1, phi)
return n, d
def pad(msg: bytes) -> bytes:
# Yea I made the padding really hard to attack,
# so don't attack it
assert len(msg) < 60
l = 76-len(msg)-8
msg = bytearray(
msg + urandom(8) + bytes([l]*l)
)
msg[:38] = xor_bytes(msg[:38], shake_128(msg[38:]).digest(38))
msg[38:] = xor_bytes(msg[38:], shake_128(msg[:38]).digest(38))
return bytes(msg)
def unpad(msg: bytes) -> bytes:
msg = bytearray(msg)
msg[38:] = xor_bytes(msg[38:], shake_128(msg[:38]).digest(38))
msg[:38] = xor_bytes(msg[:38], shake_128(msg[38:]).digest(38))
msg = msg[:-msg[-1]-8]
return bytes(msg)
def rsa_enc(pub: int, msg: int) -> int:
assert msg < pub, "Error encrypting!"
return pow(msg, e, pub)
def rsa_dec(pub: int, priv: int, msg: int) -> int:
assert msg < pub and priv < pub, "Error decrypting"
return pow(msg, priv, pub)
def main():
pflag = bytes_to_long(pad(flag))
# 306>305 so it doesn't error out on
# rsa_dec(pub2, priv2, enc_priv1)
pub1, priv1 = gen(305)
pub2, priv2 = gen(306)
enc_flag = rsa_enc(pub1, pflag)
enc_priv1 = rsa_enc(pub2, priv1)
print("enc_flag =", hex(enc_flag))
print("enc_priv1 =", hex(enc_priv1))
print("pub1 =", hex(pub1))
print("pub2 =", hex(pub2))
while True:
while not (user_in := input("enc_priv1: ")).isdigit():
...
user_in = int(user_in)
dflag = 0
try:
dec_priv1 = rsa_dec(pub2, priv2, user_in)
dflag = rsa_dec(pub1, dec_priv1, enc_flag)
except AssertionError as e:
print(f"[x] Error! {e}")
continue
if dflag == pflag:
break
print("[*] Wrong!")
print("[*] Byeeee ^-^")
print("[*] Remember to get the flag on your way out!")
if __name__ == "__main__":
main()
So clearly the server never actually prints the flag so the player has to somehow leak the flag in the main loop. I wanna bring our attention to this few lines:
1
2
3
4
5
6
7
8
9
10
11
12
13
def rsa_dec(pub: int, priv: int, msg: int) -> int:
assert msg < pub and priv < pub, "Error decrypting"
return pow(msg, priv, pub)
...
# 306>305 so it doesn't error out on
# rsa_dec(pub2, priv2, enc_priv1)
pub1, priv1 = gen(305)
pub2, priv2 = gen(306)
enc_flag = rsa_enc(pub1, pflag)
enc_priv1 = rsa_enc(pub2, priv1)
Note that when encrypting priv1
, priv1
isn't padded? We're gonna exploit this. Furthermore, notice that priv1 < pub2
. This is so that priv1
is always small enough to be encrypted by pub2
(and not loop around). However, this also implies that it is possible to feed the server a enc_priv1
value that results in priv1 > pub1
. Refering to the rsa_dec
function, this would result in the server raising Error decrypting
. We can use whether or not the error appears to perform the Bleichenbacher's Attack.
The idea behind Bleichenbacher's Attack is very simple. If we can get reduce the possible values of the plaintext by mutating the ciphertext and querying the server, we could repeat that until the number of possible values is small enough to bruteforce.
So how can we reduce the search space on priv1
from the server?
RSA has this unfortunate property whereby mutating the ciphertext can have very predictable outcomes on the plaintext. This is one of many reason why I hate RSA. (I wanted to write an article about why we should stop teaching RSA as The Encryption to students but Trail Of Bits already beat me to that).
From now on, I'll be refering to pub1
and pub2
as $n_1$ and $n_2$, and priv1
and priv2
as $d_1$ and $d_2$, and enc_priv1
as $c$.
If you were to send the server $c' = c x^e \text{ mod } n_2$, the server will decrypt $d_1' = d_1 c \text{ mod } n_2$. If $d_1' > n_1$, the server will throw Error decrypting
.
Now say for a given $x_l \le x < x_h$, the server doesn't throw Error decrypting
but will for $x_h$. Then we can infer that:
Similarly,
\[\begin{aligned} x_l d_1 &\le n_1 \quad \text{ mod } n_2 \\ x_l d_1 - n_2 k &\le n_1 \quad \text{ for one of integer } 0 \le k \le \frac{x_l d_1 - n_1}{n_2} < x_l - \frac{n_1}{n_2} \\ d_1 &\le \frac{n_1 + n_2k}{x_l} \end{aligned}\]Since for all $x_l \le x < x_h$ the server doesn't throw Error decrypting
, the $k$ value would be the same for both the upper and lower bound. For simplicity of implementation, we shall say that
which isn't as tight as is possible, but it's sufficient. Now we just repeat this for progressively bigger and bigger $x$ until our search space reduces to a bruteforcable amount. I chose to increase $x$ by a factor of $\lfloor n2/n1 \rfloor$ each time. However, sometimes this is too fast, as it would take too long to compute the intersection of the sets $(1)$. If that happens, I simply rerun the solution script.
priv1
and hence the flag can be recovered from the server in ~1000 to ~3000 requests.
1
2
3
> python sol.py
[*] Queried server 1073 times!
[*] Flag: CTFSG{https://arxiv.org/abs/1802.03367?salt=290nlk01nx}
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
from hashlib import shake_128
from Crypto.Util.number import long_to_bytes
from typing import Tuple
from nclib import Netcat
from libnum.ranges import Ranges
NC = ("192.168.139.128", 7777)
# Let p be priv1,
# c be enc_priv1,
# d be priv2,
# n2 be pub2,
# n1 be pub1
# c^d = p
# (x^e c)^d = x^ed c^d = xp
# If it does not error from xl to xh-1, but does so at xh,
# then xl*p - n2*k < n1 <= xh*p - n2*k:
#
# Upper:
# xh*p - n2*k >= n1, k <= (xh*p - n1) / n2, p >= (n1 + n2*k) / xh
# Since 0 < p < n2, 0 <= k <= (xh*p - n1) / n2 < xh - n1/n2
#
# Lower:
# xl&p - n2*k < n1, _, p < (n1 + n2*k) / xl
#
# For 0 < k < x - n1/n2:
# (n1 + n2*k) / xh <= p < (n1 + n2*k) / xl
def ru(nc: Netcat, b: bytes) -> bytes:
"""recieve until"""
s = b""
while b not in s:
s += nc.recv(1)
return s[:-len(b)]
def init_nc() -> Netcat:
return Netcat(NC)
def get_constants_from_server(nc: Netcat) -> Tuple[int, int, int, int]:
ctn = ru(nc, b"enc_priv1: ").decode()
ctn = ctn.split("\n")
enc_flag = int(ctn[0].split("=")[1], 16)
enc_priv1 = int(ctn[1].split("=")[1], 16)
pub1 = int(ctn[2].split("=")[1], 16)
pub2 = int(ctn[3].split("=")[1], 16)
return enc_flag, enc_priv1, pub1, pub2
nquery = 0
def test_waters(nc: Netcat, x: int) -> int:
global nquery
nquery += 1
tosend = (pow(x, 0x10001, n2)*c) % n2
nc.send(str(tosend).encode() + b"\n")
res = ru(nc, b"enc_priv1: ").decode().strip()
if res == "[x] Error! Error decrypting":
return 0
if res == "[*] Wrong!":
return 1
return 2
def get_x(nc: Netcat, start: int) -> Tuple[int, int]:
x = 0
while True:
res = test_waters(nc, x+start)
if x == 0 and res == 0:
start += 1
continue
if res == 0:
break
x += 1
return start, x+start
def improve_range(nc: Netcat, start: int, r: Ranges) -> Tuple[int, Ranges]:
xl, xh = get_x(nc, start)
nr = Ranges()
for rl, rh in r._segments:
# (n1 + n2*kl)/xl + 1 > rl
# kl > ((rl-1)*xl - n1) / n2
kl = ((rl-1)*xl - n1) // n2 + 1
# ((n1 + n2*kh)//xh + 1 < rh
# kh < ((rh-1)*xh - n1) / n2
kh = ((rh-1)*xh - n1) // n2
for k in range(kl, kh+1):
al, ah = (n1 + n2*k)//xh + 1, (n1 + n2*k)//xl
al, ah = max(rl, al), min(rh, ah)
nr = nr | Ranges((al, ah))
return xh, nr
def xor_bytes(a, b): return type(a)([x ^ y for x, y in zip(a, b)])
def unpad(msg: bytes) -> bytes:
msg = bytearray(msg)
msg[38:] = xor_bytes(msg[38:], shake_128(msg[:38]).digest(38))
msg[:38] = xor_bytes(msg[:38], shake_128(msg[38:]).digest(38))
msg = msg[:-msg[-1]-8]
return bytes(msg)
def rsa_dec(pub: int, priv: int, msg: int) -> int:
assert msg < pub and priv < pub, "Error decrypting"
return pow(msg, priv, pub)
nc = init_nc()
f, c, n1, n2 = get_constants_from_server(nc)
growth = int(n2//n1)
while True:
assert growth >= 2, "[x] Ratio between n1 and n2 not big enough!"
r = Ranges((0, n1))
xh = 2
i = 0
succ = True
while r.len > 1 << 10:
lr = len(r._segments)
bl = xh.bit_length()
print(f"{i} {lr} {bl} ~{int(bl/602*100+.5)}% {nquery} queries \r", end="")
i += 1
if lr >= 20: # Growth rate too fast!
print(f"[*] Growth rate `{growth}` too big!")
print(f"[*] Potential unlucky run, consider rerunning if it takes too long.")
growth -= 1
succ = False
break
xh, r = improve_range(nc, xh, r)
xh = xh*growth
if succ:
break
print(f"[*] Queried server {nquery} times!")
for p in r:
flag = rsa_dec(n1, p, f)
flag = unpad(long_to_bytes(flag))
if b"CTFSG" in flag:
break
print("[*] Flag:", flag.decode())
1
1000
CTFSG{Oh I gUesS I hAVe alwAys likEd pOurIng ThingS inTo oTher ThiNgs}
1
2
3
4
5
6
7
8
9
Luz attempted to implement SHA-CBC. It's like AES-CBC but with SHAKE-128 instead.
Luz got as far as implementing the encryption before Luz realised:
SHAKE-128 isn't symmetric like AES is.
Luz can't write the decryption at all.
Even if Luz knew the key.
Luz, being overzealous, Luz encrypted the flag and threw it away.
Have fun recovering it.
Players are given a file flag.enc
which contains the encryption of the flag (without the CTFSG{...}
prefix and suffix) repeated 100000
times. The encryption algo uses the CBC
block ciphering mode but with SHAKE-128
as the primitive instead of AES
. The block length is 4
bytes.
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
import hashlib
import re
class SHACBC:
def __init__(self, key:bytes, block_size:int):
self.bz = block_size
self.key = key
def _gethash(self, block:bytes):
return hashlib.shake_128(self.key+block).digest(self.bz)
def _pad(self, pt:bytes):
padlen = (-len(pt))%self.bz
return pt + b'\0'*padlen
def encrypt(self, pt:bytes):
ct = b""
prev_ct_blk = b"\0"*self.bz
pt = self._pad(pt)
pt = [pt[i*self.bz : (i+1)*self.bz] for i in range(len(pt)//self.bz)]
for blk in pt:
xored = bytes([x^y for x,y in zip(prev_ct_blk, blk)])
prev_ct_blk = self._gethash(xored)
ct += prev_ct_blk
return ct
def decrypt(self, ct:bytes):
"Oh no"
flag = open('flag').read()
match = re.match(r"^CTFSG\{([a-zA-Z ]+)\}$", flag)
assert match, "`flag` doesn't follow flag format!"
naked_flag = match.group(1)
naked_flag = naked_flag.encode('utf-8')
key = open('key', 'rb').read()
s = SHACBC(key, 4)
pt = naked_flag*100000
ct = s.encrypt(pt)
open('flag.enc', 'wb').write(ct)
So since SHAKE-128
is a hashing function, there is no hope in writing a decryption routine. Furthermore, because of the usage of an unknown key, rainbow tables aren't possible either. However, the block length is suspiciously small, meaning there's a huge chance for block collisions.
Refering to the image above, there are three reasons for ciphertext block collision:
Only collisions resulting from reason $(2)$ leaks information about the flag. Say ciphertext blocks $c_i$ and $c_j$ collide via reason $(2)$. Then we know that $c_{i-1} \oplus p_i = c_{j-1} \oplus p_j, \; p_i \oplus p_j = c_{i-1} \oplus c_{j-1}$. Since we know the $rhs$, we've obtain a constraint on the plaintext, i.e. the flag.
However, we still need to differentiate collisions resulting from reason $(2)$ from $(1)$ and $(3)$. Due to the repetitive nature of the plaintext (it's just the flag times 100000
), we expect to see a lot of collisions from reason $(3)$. Luckily, we can differentiate reason $(3)$ by checking if $c_{i-1} = c_{j-1}$.
Unfortunately, there doesn't seem to be a way to distinguish reason $(1)$. After removing all collisions from reason $(3)$, there are about $130$ collisions, of which half of which we expect to be from reason $(2)$ since $(1)$ and $(2)$ occur roughly with the same probability.
If we were to treat all $130$ collisions as being from reason $(2)$, we find that there are no solutions for the flag. We hence need to find a way to seperate collisions from reason $(1)$ and $(2)$. Let's assume there is a unique solution for the flag. Then there exists a locally maximal subset of collisions which, if treated as all from reason $(2)$, contains a unique solution which is the flag.
We can achieve this via depth first search. Start with a random collision and assume it is from reason $(2)$. We progressively add another one until there are no solutions. We can then remove the latest one we added, and try adding another one. If we can't add any more collisions, we've reached a locally maximal subset, and we print the solutions. After that, we simply try another branch.
Doing so, we find that $63$ out of the $130$ collisions resulted from reason $(2)$.
1
2
3
4
5
> python solve.py
Found 130 potential collisions
New depth reached, 67 constraints left
Possible flag: CTF{Oh I gUesS I hAVe alwAys likEd pOurIng ThingS inTo oTher ThiNgs}
Done
Author's note: During the CTF I realised, since all the constraints are linear, you can just gaussian eliminate to find the largest set of constaints that's satisfiable, no need for all that fancy depth first search.
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
import z3
# Init parameters
ct = open('../dist/flag.enc','rb').read()
blksize = 4
n = 100000
iv = b'\0'*4
allowed = b"qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM "
# Get cipher text blocks
cblks = [ct[i*blksize:(i+1)*blksize] for i in range(len(ct) // blksize)]
# Don't consider collisions from blocks with too high frequency
# as that would probably be from same-input collisions.
# Init symbolic flag
flag_len = len(ct)//n
flag_sym = [z3.BitVec("f%02d"%i, 8) for i in range(flag_len)]
# Get all collisions
# This cannot distinguish identical cipher text blocks due to
# hash collisions or input collisions (input = xor(prev_plaintext_blk, curr_ciphertext_blk))
# We only want input collisions as that leaks plaintext info.
collisions = {}
for idx,blk in enumerate(cblks):
if blk not in collisions:
collisions[blk] = []
# Remove all collisions that results from reason 3.
if cblks[idx-1] in [cblks[i-1] for i in collisions[blk]]:
continue
collisions[blk].append(idx)
collisions = [idxlist for _, idxlist in collisions.items() if len(idxlist)!=1]
# Flatten collisions into pairs
collisions = [(L[i], L[j]) for L in collisions for i in range(len(L)) for j in range(i+1,len(L))]
print("Found %d potential collisions"%len(collisions))
def init_solver():
'''
Init z3 solver and
constraint flag to be composed of `allowed` characters
'''
s = z3.Solver()
for c_sym in flag_sym:
s.add(z3.Or([c_sym == c for c in allowed]))
return s
def build_constraints(collisions):
'''
Returns constraints on `flag_sym` assuming collisions
are caused by input collisions and not hash collisions
'''
constraints = []
for idxlist in collisions:
tmp = ()
prev_blks = [cblks[idx-1] if idx != 0 else iv for idx in idxlist]
ci,cj = prev_blks[0], prev_blks[1]
ii,ij = idxlist[0], idxlist[1]
cxor = [x^y for x,y in zip(ci,cj)]
for k in range(blksize):
pi = flag_sym[(ii*blksize + k)%flag_len]
pj = flag_sym[(ij*blksize + k)%flag_len]
tmp += (pi^pj == cxor[k],)
constraints.append(tmp)
return constraints
def print_sols(solver, n=3):
'''
Given a solver, print n solutions for `flag_sym` (default 3)
'''
while solver.check()!=z3.unsat and n>0:
m = solver.model()
print(f"Possible flag: CTF{
bytes([m[c].as_long() for c in flag_sym]).decode('utf-8')
}")
solver.add(z3.Not(z3.And([m[c]==c for c in flag_sym])))
n -= 1
if solver.check()!=z3.unsat:
print("...")
def recurse(solver, constraints):
'''
Recursively tries all maximal subsets of `constraints` that yield a satisfiable model (>0 solutions)
A maximal subset A of `constraints` means it is impossible to increase the size of subset A
by adding more constraints from `constraints` such that A still yields a satisfiable model.
This allows finding a sufficient subset of `constraints` caused by input collisions and not
hash collisions, recovering the flag.
Prints solutions reached by these maximal subsets
Returns True if solver is satisfiable
'''
# Stores the minimum number of constraints left ever reached.
global depth_left
# Stores constraints tried
global tried
# Init default globals
if 'depth_left' not in globals():
depth_left = len(constraints)
if 'tried' not in globals():
tried = []
# Check if has tried
s = set(constraints)
for c in tried:
if c.issubset(s):
return True
# Update tried
tried.append(s)
# Stop recursing if solver is already unsatisfiable
if solver.check()==z3.unsat:
return True
# Stop recursing if no constraints are left.
if len(constraints)==0:
print_sols(solver)
return True
print("Constraints left: %d"%len(constraints), end=" \r")
valid = False
# Try to append one more constraint from those left
for c in constraints:
# Duplicate current solver
new_solver = solver.translate(z3.main_ctx())
new_solver.add(c)
valid != recurse(new_solver, [con for con in constraints if con!=c])
# None of the new models are satisfiable
# set of constraints in `solver` is maximal
if not valid:
# depth reached before, ignore
if len(constraints) >= depth_left:
return True
# Depth not reached before, print solutions
depth_left = len(constraints)
print("New depth reached, %d constraints left"%depth_left)
print_sols(solver)
return True
solver = init_solver()
constraints = build_constraints(collisions)
recurse(solver, constraints)
print("Done")