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
e743bdd064119539c1a1b958549b7b2768db75e72860d7e479519538e7ce30aa605a7e3dc07961258052a3ff80e8c5e9105e4adae47dd2aec7386f9e4ce253d4505ea243a33bb33dba5b850794dde5939ec0d1078b4e8337906649933aa6f7425e97925701fdcbfae28ad41a29d256a6eb09356cb02f6cfd68ee93f2e9065dabfc01afb98887ebbcca7cac21b099851fb9bf71f834f4fcb97c6844642cb52d9c0dd3a3694883dc78f4fb73993f2211c90c2ff0e20d27a9b3e33d9c351f5bbcf7ff
e743f6d33c1c977ad0ece80812933f4c27c525f4753a99d75115de53b7963da8234b60488b37773e9908c88ad5e380ef111d3fdfe47dd2aec738298049d10e855b1bba00d63bb365ba019e0d81eba4caf084d22ad34df637c8285f8823fcaf425e8c984237e0cbaffe84fb5d29a718b0f0106f19b02f6cfd68eee6fbaf041ede9246ffa0d2a8f0bcca7cac21b099851fb9a64abb09db85de13730a7237ac778669a6943145819f588e8e69d7293908931c7bb2983974faa7bb3d9c351f5bbcf7ff
ab4be5dd6652837fc1a1e80812933f27779d28f6360b87bc7453d609f9a515ec234e26159a37722dc45286b988b28baf534a12d2be33c1edd36032da07e253df1558a300d67eeb68b842df49c7c0fd8af0cac469ca4ded79de70528a60e2e22c19cea9411abccbaffe84fb5d6cc95cfee60b7643ab2640a43582fdf1ae5900c79246ffa0d2a8f0bcca25bc63a9b6e25886bc5e964acfdd843c686d220ef6589d69a6944f0b978441d4f069cd7d1b31c20c2ff0e20d27a9b3e33d9c351f5bbcf7ff
e705f3c67f08cf7fc1a1e80812933f4c27ae00b27d2b87a271608b58f2ce30aa605a7e08c079612580528ae185b0c8eb531d3fdfbc28d0b79d7629c35d89569b1675fb03a33bb33dbc1ac104b9d9bfddddc7c6728962ed3dc93655d02efeec03028ea9485ce6d2e1e89fe20730a703a0dd0a3557f3286fe372f7a8edb41d44c4bd7486ffcb93b3a1e5259322eaa2ba4282a671f834f485893e2b503c1691379c0da9ed7f539a8602f4fb63993f24118f1c2ff0a1197fa5e9ad208529505bbcf7ff
e743f6d36d4cd17294ecf74e4f82214c27ae00b27d2b87a23a45cd10a3ce3599234e26159a37276dd10c8af8bea6d3aa095e51d5a933c4b5de6271f51cd038915b4eb8198c75b33dbc1a850794dde593c1c99c3cc854ac79e56f119e38e5f5591be2c74b1bb98ff8d3c7ef05738818b9a81d6d5af16b2ba23582fdeb821e1ac4e74fb9a291ddb197d83b9322eaa2ba4282a671f834f4fcb96626527f35ef3786598bae6b0bc3c678f4fb63993f244b874422f2e21939eaa7bb3b9c731f5bbcf7ff
e743f69e720a8c638aa1e80812933f4c27c525f4753a99d7510bdb0bba947eec684b6048c07961258052838ad5e380ef111d3fdfbc288aa3c57b2b805dd3569b1600ae02952feb53fb58f04982c6fcc985cac469cc0cb234e5285f8823fcaf1e02d9844c18a7d294a689f91e6ad203baa81d6d5af16b6cfd68ee93f2e9065dabe74fb9a291ddf0e2f17db161eab2975886bc5e962d9fe4de13730a7237ac778669a6944f11c3c678f4fb63993f24118f5361e6ba0025eab7e3369e300a05bcf7ff
e743f6d33c1c977ad0ece8080c96796463c575e7280b99f26248cf53e7a515ec684b60488b37773e9908c88ad5e380ef534a12d2be33c8f4e87671ce5fca17df0300ae02952feb3dba01cb118fc4bfc99ec0d1078b4ef637c86b4bd015e8f74202d9844c18a797fae28ad41a309c40bdf2532c6cb02f6cfd68f7a8edb41d44c79246ffa0d2a8f0bcca7cf275b2afb80186bc5e964acfdd843c686d220ef6589d69a6944f11c3dc4cd6b329d77d1b31c20c2ff0a71939a5b7f32fe6731e5deffadf
ab4be5dd6652837fc1a1e80812933f4c27ae00b27d2b87a2710bdb0bba947e993d0b7e1898746366d10c8ae185b0c8eb531d3fdfbc28d0b7867c3cf507c7159c0100fb03a33beb68b842df499485e388c184d22ad34df634e5285f8823fcaf1e02d9844c18a7d294bd83f84374cb6db9b6122c6cb03540ba72edbcc090470a86a45be1ffaac49ef9c150f275b2afb801a1d804ad12c2dfc77c72104615e43dc8179db57251d984478ebe2b943d6111c95239ebb84339a5b7f378882b135facf7ff
e705f3c67f08cf7f8aa2e15556d86a6a68db6be26e23c3bc24408b38e7a515ec684b6048c07961258052838ad5e38bff0b471091ef6dc8f4e876299b07c7159c0100ba00cd31eb68b842df4981eba4ca85cac431ca178379de70528a60e8f7425e8c984237e0d294bd83f85d7e8a5ba4a84d7100ab256da076f793f2e9064490bf42bbe1d2a8ebace766a83bf1b2975886bc5e962d9fe4de137374491691379c0da9973a1b978447d4b673993f2211c95361e6ba0025eab7f32fe6731e5deffadf

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")
out2 = bytes.fromhex("e743bdd064119539c1a1b958549b7b2768db75e72860d7e479519538e7ce30aa605a7e3dc07961258052a3ff80e8c5e9105e4adae47dd2aec7386f9e4ce253d4505ea243a33bb33dba5b850794dde5939ec0d1078b4e8337906649933aa6f7425e97925701fdcbfae28ad41a29d256a6eb09356cb02f6cfd68ee93f2e9065dabfc01afb98887ebbcca7cac21b099851fb9bf71f834f4fcb97c6844642cb52d9c0dd3a3694883dc78f4fb73993f2211c90c2ff0e20d27a9b3e33d9c351f5bbcf7ff")

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

Thanks for reading! <3

~ Polymero