Writeup by dagurb for Hvítur Kassi 1

crypto symmetric

May 7, 2025

Table of contents

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