# K3RN3LCTF 2021 - 1-800-758-6237

Cryptography – 437 pts (28 solves) – Chall author: Polymero (me)

“I NEED A PLUMBER ASAP, MY FLAG IS LEAKING ALL OVER THE PLACE!!!”

nc ctf.k3rn3l4rmy.com 2233

Files: 1-800-758-6237.py

This challenge was part of our very first CTF, K3RN3LCTF 2021.

## Exploration

When we connect to the netcat address we are send some hex encoded data every second.

e743f69e720a8c638aa1e80812933f4c27c525f4753a99d75115951da18d24f2684b60488b5c3928821192e1d5e38bff0b471091ef6d8aa3c57b2b80459038915b4eb8198c75b33dba01cb118fc4bfc99ec0d1078b4e8337c8334dcb60e8f71a5897980f0cff88bba68aa113689142fedd142c6cb02f6cfd68ee93f2e9064490bf42bbe1d2a8f0bcca7cac21b099851fb9bf04ad12c2dfc7137374491691379c0da9973a1b97dc4cd6b329d73f24118f5361e6ba0025eab7f32fe6731e5deffadf
ab4be5dd6652837fc1a1e80812933f27779d28f6360b87bc7453d609f9a515ec234e26159a37722dc45286b988b28baf534a12d2be33c1edd36032da07e253df1558a300d67eeb68b842df49c7c0fd8af0cac469ca4ded79de70528a60e2e22c19cea9411abccbaffe84fb5d6cc95cfee60b7643ab2640a43582fdf1ae5900c79246ffa0d2a8f0bcca25bc63a9b6e25886bc5e964acfdd843c686d220ef6589d69a6944f0b978441d4f069cd7d1b31c20c2ff0e20d27a9b3e33d9c351f5bbcf7ff
e743f6d36d4cd17294ecf74e4f82214c27ae00b27d2b87a23a45cd10a3ce3599234e26159a37276dd10c8af8bea6d3aa095e51d5a933c4b5de6271f51cd038915b4eb8198c75b33dbc1a850794dde593c1c99c3cc854ac79e56f119e38e5f5591be2c74b1bb98ff8d3c7ef05738818b9a81d6d5af16b2ba23582fdeb821e1ac4e74fb9a291ddb197d83b9322eaa2ba4282a671f834f4fcb96626527f35ef3786598bae6b0bc3c678f4fb63993f244b874422f2e21939eaa7bb3b9c731f5bbcf7ff


Let us take a closer look at the source code to see what the server is sending us. The data is generated in the leak() function.

def leak(drip):
rinds = sorted(random.sample(range(len(drip)+1), 16))

for i in range(len(rinds)):
ind  = rinds[i] + i*len(b'*drip*')
drip = drip[:ind] + b'*drip*' + drip[ind:]

aes = AES.new(key=server_key, mode=AES.MODE_CTR, nonce=b'NEEDaPLUMBER')
return aes.encrypt(drip).hex()


Within the flag plaintext, the server injects a total of 16 ‘*drip*’s at random positions and then even encrypts it with AES in CTR mode. For those unfamiliar with the various modes of AES, it is important to note that the CTR (Counter) mode of AES turns it into a stream cipher. This means that the key and nonce are used to create a pseudo-random key stream, which is then XORed with the plaintext to create the ciphertext.

$\mathrm{ciphertext} = \mathrm{keystream}(\mathrm{key}, \mathrm{nonce}) \oplus \mathrm{plaintext}$

A nonce (number only used once) should, as its name suggests, only be used once. The reason being that a set combination of key and nonce will always generate the same key stream. Which in the leak() function above turns out to be exactly the case. This means that all injected flags are XORed with the exact same key stream. Although we do not know this key stream, it does open up the possibility for some XOR shenanigans.

Our key observations are

• There are 16 *drip*s injected into the flag before encryption with AES-CTR.
• The AES-CTR re-uses the same key and nonce combination, such that all encryptions use the same key stream.

Any idea how we might use multiple outputs and some XOR tricks to recover the flag?

## Exploitation

In order to recover our flag we must first note two important things:

• All outputs are XORed with the same key stream.
• XORing a byte array with an equivalent byte array will result in a 0-byte array.

This means that if we would XOR two separate outputs all the parts that line up would result in 0-bytes, whereas the differing parts do not.

out1 = bytes.fromhex("e743f69e720a8c638aa1e80812933f4c27c525f4753a99d75115951da18d24f2684b60488b5c3928821192e1d5e38bff0b471091ef6d8aa3c57b2b80459038915b4eb8198c75b33dba01cb118fc4bfc99ec0d1078b4e8337c8334dcb60e8f71a5897980f0cff88bba68aa113689142fedd142c6cb02f6cfd68ee93f2e9064490bf42bbe1d2a8f0bcca7cac21b099851fb9bf04ad12c2dfc7137374491691379c0da9973a1b97dc4cd6b329d73f24118f5361e6ba0025eab7f32fe6731e5deffadf")

xor12 = bytes([out1[i] ^ out2[i] for i in range(len(out1))])
print(xor12.hex())

00004b4e161b195a4b0051504608446b4f1e50135d5a4e33284400254643145808111e754b25580d0243311e550b4e161b195a4b0b10580d0243441e09726b450b101a5a2f4e0000005a4e161b195a5a0000000000000000585504585a4e005806000a580d0243414400750941431458361d190000000000000000000000193b434314585a2f1b000000000000000000000075552636237e6f1b302d3a241a00007a34535314003422485a4e000600465f4e16580d02430410127a460106530d20


These discrepancies arises from the injection of the *drip*s, therefore the non-zero bytes are likely XORed with (a part of) this injection. So if we would XOR the non-zero bytes with the injected bytes we should be able to recover snippets of the flag!

Let us take a look at the first 6 non-zero bytes.

nonzero1 = bytes.fromhex("4b4e161b195a")
print(bytes([nonzero1[i] ^ b'*drip*'[i] for i in range(len(nonzero1))]))

b'a*drip'


Turns out we found a single flag character a and a large part of another injection… not too interesting. Let us try another part.

nonzero1 = bytes.fromhex("75552636237e")
print(bytes([nonzero1[i] ^ b'*drip*'[i] for i in range(len(nonzero1))]))

b'_1T_ST'


Now that looks more promising! We could continue searching for flag snippets by hand, but I tried my best to make the flag as long and annoying as possible to discourage this. So how can we automate this process? Introducing crib dragging!

Here’s the plan. We gather a bunch of outputs (about a hundred should be more than enough), then for every unique combination of two outputs we drag our known piece of plaintext, our ‘crib’, along the XORed outputs. For every 6-byte snippet we create through crib dragging, we check whether all 6 bytes are within our flag format alphabet. If so, we store the snippet in a Python set called the ‘droplet pool’ in the code below.

def xor(hex1, hex2):
return bytes([x^y for x,y in zip(*[list(bytes.fromhex(i)) for i in [hex1, hex2]])]).hex()

def dripxor(hex1, hex2, pos):
pos *= 2
return bytes.fromhex(xor(xor(hex1[pos:pos+12], hex2[pos:pos+12]), b'*drip*'.hex()))

# Farm leaks (100 should be enough most of the times)
cs = [leak(FLAG) for _ in range(100)]

# Create droplet pool
ALP  = list(b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{_}!?:')
POOL = set([p for q in [[j for j in [dripxor(cs[n], cs[n+1], i) for i in range(len(cs[0])//2)] if all([k in ALP for k in j])] for n in range(0, len(cs)-1)] for p in q] )

print(random.sample(POOL,5))

[b'T_ST00', b'4aA4!!', b'sdripM', b'wUrLCi', b'{o7q3_']


As you can see, not every snippet we find makes much sense… Luckily, we know where to start! We know that our flag should start with the 5 bytes flag{. So let’s see if there is one in our pool. I used a larger set of outputs than shown at the beginning.

[i for i in POOL if i[:5] == b'flag{']

[b'flag{4']


Found one! Let us continue this procedure, moving up a single byte at a time. So our next step would be to check for a pool member that starts with lag{4, etc etc. Time for some automation.

# Reconstruct flag
flag = b'flag{'

while flag[-1] != ord('}'):
flag += [i for i in POOL if flag[-5:] == i[:5]][0][5:]

print(flag)


Ta-da!

flag{44a4A4AA4aa44aA4!!th3_dr1pp1ng_1s_dr1v1ng_m3_1ns4n3_m4k3_1t_st0p_M4K3_1T_ST000PP!:droplet:}