We have a Puzzlescript puzzle that we should solve, where the visible part just lets us input a flag and check if it is correct. Examining the source shows a large invisible board of symbols above the flag – the puzzle, after starting the check, looks something like the following:
.F...
.AAAR
.AAAR
CAAAR
fff
The f
’s are flag characters, A
’s and R
’s are random symbols from two different sets of 64 symbols (let’s label the sets A
and R
), and F
and C
are special symbols. We win if, after applying the rules, F
and C
are adjacent.
There are several types of rules:
[ A_47 | char15 ] -> [ L_46 | char15 ] again
: converts an A
-type symbol to an L
-type symbol (a third set of 64 different symbols) based on an adjacent flag character,right [ L_21 | R_25 ] -> [ R_17 | ] again
: converts a pair LR
to an R
and empty space,R
(let’s call it Rt
) that disappears when next to C
.For convenience, let’s label the function that maps (A, char) -> L
as T(a,c)
, and the function that maps (L, R) -> R
as F(l,r)
. We can see that, if we label the elements of a row as
a[n] ... a[2] a[1] a[0] r
the entire row will disappear if it is finally reduced to Rt
, i.e.
F(T(a[n], flag[n]), ... F(T(a[1], flag[1]), F(T(a[0], flag[0]), r))) = Rt
It is useful to take a second look at the structure of the functions F
and T
. If we do this, we will see that F(l, F(l, r)) = r
for all l
and r
, and that there exists an l0
such that F(l0, r) = r
for all r
. Therefore, if the sets L
and R
were equal, we could conclude that the group (L, T)
is isomorphic to bit-strings of length 6 with xor as the group operator.
Similarly, looking at T
shows that we can find a0
and c0
such that T(a0, c) = l0
and T(a, c0) = l0
for all a
and c
. For all a
other than a0
, T(a, c1) != T(a, c2)
, and similarly for c
’s. This all strongly hints that we are dealing with a finite field that is most likely GF(2^6)
, with T
as the multiplication operator and F
as the addition, with the added complication that each element can be written in four different ways, as an element of L
, R
, A
or a flag character.
This means that, if we can find mappings from all four of these sets to some set X
, we could define a field (X, +, *)
, where +
is F
and *
is T
, in which our condition for a row to disappear is:
a[n]*flag[n] + ... + a[1]*flag[1] + a[0]*flag[0] + r = Rt
Assume that we know the elements of R
, C
(the set of characters) and A
that map to the relevant neutral elements of the field: r0
, c1
and a1
. We can then build the entire mapping as:
R
, choose arbitrary different elements,L
: l -> l + r0
,A
: a -> a * c1
,C
: c -> c * a1
.For each guess of these three values, we build the field, check that it reasonable (for each set, all elements map to different field elements; it is consistent with the rules given; field axioms hold), and then solve the set of linear equations given by the rows to find the flag.
This returns several candidate flags (though this might be due to a missing check in my implementation), but it is easy to filter the candidates for the one matching the flag format: PCTF{CTFs4R3Ju5TPuzZLeHUnTSWi7hM0reCoMPut3R5}
.