Analysis
We get a function check_flag
with an uncomfortably large pickled and compressed blob. Printing the unpickled object we see that it is just a very large array.
The important bits of check_flag
(annotated)
for r in range(36):
s = [ WB[256*r+16*pos+s[pos]] for pos in range(16) ]
tmp = [
# segment 0
WB[11520+3072*r+16*s[0]+s[10]],
WB[12288+3072*r+16*s[1]+s[11]],
WB[13056+3072*r+16*s[2]+s[ 8]],
WB[13824+3072*r+16*s[3]+s[ 9]],
]
s = [
# segment 1
WB[11776+3072*r+16*tmp[0]+s[13]],
WB[12544+3072*r+16*tmp[1]+s[14]],
WB[13312+3072*r+16*tmp[2]+s[15]],
WB[14080+3072*r+16*tmp[3]+s[12]],
# segment 2
WB[9216+64*r+s[0]],
WB[9232+64*r+s[1]],
WB[9248+64*r+s[2]],
WB[9264+64*r+s[3]],
# segment 3
WB[12032+3072*r+16*s[7]+s[10]],
WB[12800+3072*r+16*s[4]+s[11]],
WB[13568+3072*r+16*s[5]+s[ 8]],
WB[14336+3072*r+16*s[6]+s[ 9]],
# segment 4
tmp[0],
tmp[1],
tmp[2],
tmp[3],
]
Our goal is to find some initial s
which turns into b’ECSC2019’ after 36 rounds of transformations.
Reversing this is somewhat straight forward.
- Segment 2 only depends on s[0] to s[3], so they can be individually bruteforced.
- Segment 0 depends on segment 4 (which is given), s[0] to s[3] (which is now known), and s[8] to s[11], which can be individually bruteforced.
- Segment 1 depends on segment 4 and s[12] to s[15], which can be individually bruteforced.
- Segment 3 depends on s[8] to s[11] (which is now known) and s[4] to s[7], which can be individually bruteforced.
One would expect that when bruteforcing any s[i], there would be multiple solutions, but this challenge was carefully designed to avoid that.
Finally there is the code snippet at the top:
s = [ WB[256*r+16*pos+s[pos]] for pos in range(16) ]
Since each s[i] only depends on the previous s[i], we can individually bruteforce them. Again, one would expect that when bruteforcing any s[i], there would be multiple solutions. But there aren’t.
Repeating all of this 36 times, we get s = [9, 7, 6, 11, 4, 5, 9, 0, 6, 8, 10, 6, 10, 14, 6, 12]
, which is 976b459068a6ae6c
in hex.
Running python wb.py 976b459068a6ae6c
(NOTE: not python3
), we get
Nice! You can enter this flag: ECSC{976b459068a6ae6c}.
P.S. Hvítur Kassi means “White Box” in Icelandic.
Love,
Team Iceland.
Solution script
from dump import WB
target = b'ECSC2019'.hex()
s = [int(_, 16) for _ in target]
def undoFirstPart(r: int, s: list[int]):
newS = [0] * 16
for pos in range(16):
# enumerate all possibilities to make sure I didn't bork it lol
poss = [val for val in range(16) if WB[256*r + 16*pos + val] == s[pos]]
if len(poss) != 1:
raise Exception('Hmm something went wrong here mvm')
newS[pos] = poss[0]
return newS
def undoRound(r: int, s: list[int]):
tmp = s[12:16]
prevS: list[int] = [0] * 16
for val in range(16):
if WB[9216 + 64 * r + val] == s[4]:
prevS[0] = val
if WB[9232 + 64 * r + val] == s[5]:
prevS[1] = val
if WB[9248 + 64 * r + val] == s[6]:
prevS[2] = val
if WB[9264 + 64 * r + val] == s[7]:
prevS[3] = val
for val in range(16):
if WB[11520 + 3072 * r + 16 * prevS[0] + val] == tmp[0]:
prevS[10] = val
if WB[12288 + 3072 * r + 16 * prevS[1] + val] == tmp[1]:
prevS[11] = val
if WB[13056 + 3072 * r + 16 * prevS[2] + val] == tmp[2]:
prevS[8] = val
if WB[13824 + 3072 * r + 16 * prevS[3] + val] == tmp[3]:
prevS[9] = val
for val in range(16):
if WB[11776 + 3072 * r + 16 * tmp[0] + val] == s[0]:
prevS[13] = val
if WB[12544 + 3072 * r + 16 * tmp[1] + val] == s[1]:
prevS[14] = val
if WB[13312 + 3072 * r + 16 * tmp[2] + val] == s[2]:
prevS[15] = val
if WB[14080 + 3072 * r + 16 * tmp[3] + val] == s[3]:
prevS[12] = val
for val in range(16):
if WB[12032 + 3072*r + 16*val + prevS[10]] == s[8]:
prevS[7] = val
if WB[12800 + 3072*r + 16*val + prevS[11]] == s[9]:
prevS[4] = val
if WB[13568 + 3072*r + 16*val + prevS[8]] == s[10]:
prevS[5] = val
if WB[14336 + 3072*r + 16*val + prevS[9]] == s[11]:
prevS[6] = val
print(prevS)
print('=' * 16)
return undoFirstPart(r, prevS)
for r in reversed(range(36)):
s = undoRound(r, s)
print(s)
print(''.join(map(lambda x: hex(x)[2:].zfill(1), s)))