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}'