Fleeting
3 minutes to read
We are given this short Python code to encrypt the flag, and also the ciphertext as a comment:
print((f:=lambda s:s and[s[0]^s[-1],*f(s[::-1][1:])])([*open('flag','rb')][0]))
#[20, 37, 47, 47, 56, 52, 38, 46, 51, 56, 23, 58, 57, 50, 86, 95, 95, 103, 0]
Source code analysis
If we analyze the code, we will see that it is defining a lambda function named f and calling it with the contents of the flag in bytes:
$ echo -n 'ictf{test_flag}' > flag
$ python3 -q
>>> [*open('flag','rb')][0]
b'ictf{test_flag}'
So, f is a function that outputs s and [...]. In Python, this expression is equivalent to returning the last object, since s is a truthy value (s is the flag in bytes):
>>> s = b'ictf{test_flag}'
>>> s and [1, 2, 3]
[1, 2, 3]
>>> s and []
[]
The list [...] is formed by s[0] ^ s[-1] (which is the XOR operation between the first and last bytes of s). Then, we have *f(s[::-1][1:]), which is f called on s reversed and from the second character:
>>> s
b'ictf{test_flag}'
>>> s[::-1][1:]
b'galf_tset{ftci'
Actually, the * in this situation is used to put the elemtents of the list output by f into [s[0] ^ s[-1], ...].
Therefore, f is a function that takes a bytes object s and outputs [s[0] ^ s[-1], ...], where ... is the output of f(s[::-1][1:]). In brief, a recursive function until s is empty.
Reverse engineering
This is the output list:
[20, 37, 47, 47, 56, 52, 38, 46, 51, 56, 23, 58, 57, 50, 86, 95, 95, 103, 0]
We know that 20 is the result of s[0] ^ s[-1]. Since ImaginaryCTF flags are like ictf{...}, we can find s[0] because of XOR properties:
>>> chr(ord('}') ^ 20)
'i'
As expected, the first character is i.
Then, 37 is again the XOR between the first and last bytes, but from s[::-1][1:], so we have s[-1] = b'i' and we can find s[0]:
>>> chr(ord('i') ^ 37)
'L'
Then 47 is the XOR between L and s[0] (which should be a c):
>>> chr(ord('L') ^ 47)
'c'
Another 47, but this time is the XOR between s[0] and c:
>>> chr(ord('c') ^ 47)
'L'
Let’s do another one, which should be t:
>>> chr(ord('L') ^ 56)
't'
Flag
In the end, we can come up with an algorithm to recover the flag using the above procedure:
$ python3 -q
>>> arr = [20, 37, 47, 47, 56, 52, 38, 46, 51, 56, 23, 58, 57, 50, 86, 95, 95, 103, 0]
>>> last_char = '}'
>>> flag = [0] * len(arr)
>>> for i, a in enumerate(arr):
... flag[i // 2 if i % 2 else -(i + 1) // 2] = last_char
... last_char = chr(a ^ ord(last_char))
...
>>> ''.join(flag)
'ictf{TW33TlenCH@LL}'