I hate standing in line for gas….
Here’s the Python source:
1 | schedule = # ommitted because too long for writeup |
So… what exactly is going on here?
Let’s break it down step-by-step.
Step 1
1 | key = open("flag.txt","rb").read() |
flag.txt becomes the key, minus the flag prefix and suffix. The message being encrypted is “yellow submarine”.
Step 2
1 | def enc(p, k): |
The encryption itself is just a XOR between the message plaintext and some key generated by calling keysch(k)
.
Step 3
1 | def keysch(k): |
The keyschedule function first creates a bytearray of b'\x00*16'
. Then, it creates an array arr
filled with 128 0’s. arr
turns out to represent the binary of the ciphertext.
Step 3.1
We then enter a for loop with 128 iterations. For each iteration, we enter another for loop that loops through every bit_loc
in schedule[i]
. arr[i]
is XORed with the result of get_bit(k, bit_loc)
.
1 | def get_bit(b, n): |
By just testing custom inputs on this function, I figured out that get_bit
simply returns the nth bit of b == k == flag
.
Step 3.2
After the whole 128-iteration for loop completes, we enter another for loop. Here, we simply set out
equal to the result of set_bit(out, i, arr[i])
.
1 | def set_bit(b, n, v): |
Similar to the get_bit
function. set_bit
simply sets the nth bit of b == out == keysch return value
to v
.
Finally, we return out
.
So, essentially, each bit of the result of keysch, which is XORed with the plaintext “yellow submarine” to produce the ciphertext, is equivalent to the XOR of 64 (the length of each array in schedule
) bits of the flag.
z3????
no. we have 128 equations, 128 variables, and 64 variables per equation. A bit too much for z3 to handle, unfortunately.
xor == add
128 equations and 128 variables… hmmm… suspicious. But it’s not a linear system of equations… right?
Well, actually, it is! It’s all because XOR is a function with various interesting properties. For this challenge in particular, the important property is that XOR is essentially equivalent to binary addition with no carry. Just take a look at its inputs and outputs:
$$0 \oplus 0 ==> 0$$
$$1 \oplus 1 ==> 0$$
$$1 \oplus 0 ==> 1$$
$$0 \oplus 1 ==> 1$$
So how can that help us?
Well, since we’re working with only singular bits (we’re XORing the individual bits of the flag, so all numbers being XORed are 0 or 1), we can actually just make this a system of equations in an integer ring of mod 2, since it essentially makes addition become binary addition without carry!
So, now, it’s as simple as using schedule
to figure out what bits are being XORed together for each position, figure out the result of keyschedule (which is just equal to ct ^ m
, i.e. the ciphertext and message XORed), and put all that information into a matrix in SageMath under an IntegerModRing of 2. Once we have it all in a matrix, we can just solve with SageMath’s matrix magic.
Here’s the Sage implementation:
1 | schedule = # too large so not included in writeup |
And we get the flag!
tjctf{l1neeruwu8215413}