Rating:
# <https://git.lain.faith/BLAHAJ/writeups/src/branch/writeups/2021/corctf/babypad>
# babypad
by [haskal](https://awoo.systems)
crypto / 484 pts / 35 solves
>padding makes everything secure right
>
>`nc babypad.be.ax 1337`
>
>note: if you are finding that your exploit script is very slow, it is highly recommended to use a
>shell from `meta/shell`
provided files: [server.py](https://git.lain.faith/BLAHAJ/writeups/src/branch/writeups/2021/corctf/babypad/server.py)
## solution
this server runs AES in [counter
mode](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)), which essentially
turns AES into a stream cipher, where the output of AES on counter blocks initialized with a random
nonce are XOR'd with the plaintext to produce the ciphertext. Traditionally, XOR-based stream
ciphers should have a MAC to make sure the message was not tampered with but we notice this server
produces and accepts messages with no MAC. this means we can change the content of the ciphertext to
produce different plaintext if we know what some of the plaintext is. the general method is
`encrypted_data ^ wanted_plaintext ^ known_plaintext`
there's a problem with this... we don't know any of the plaintext because that's what we're trying
to find out. luckily there is _also_ a `pkcs#7` padding operation done. this is totally useless for
a stream cipher, and as you'll find out it actually makes it less secure because it turns into an
oracle for telling us whether our plaintext guess was right or not
the exploit method is start with a pad size of 1, and guess every possible value of the last
character. send off that guess with the last char of the ciphertext XOR'd with `1 XOR guess`. if
it's correct, the server will say so and you've guessed the last character. if it's incorrect, the
`unpad` function will fail because the decrypted message will contain invalid padding. now on the
first step, this gets us the value of the real padding amount (which turns out to be 4), so we skip
back 4 chars and start guessing actual flag values. basically, if you know the last `n` chars of a
block, you XOR with those chars and then XOR with an `n+1` size pad to try to guess the `n+1`th
char. once you reach 16 bytes, which is the block size, cut off the last block and start from pad
`1` on the next last block
```python
known_content = bytearray([0] * len(enc))
# runs a test for a byte position by adding trial padding of the size (16 - (position % 16)) and
# guessing every possible byte value until it works
def run_test(position):
npads = 16 - (position % 16)
for x in range(256):
if x == npads:
continue
test = bytearray(xor(enc, known_content))
test[position] ^= x ^ npads
for z in range(position + 1, position + npads):
test[z] ^= npads
if trial(test[:position + npads]):
return x
raise Exception("none found for", position)
# first, find the actual pad size
# if this turns out to be 1 then you'll get an error here (but then you know it's 1)
actual_pad = run_test(len(enc) - 1)
print("found pad", actual_pad)
# turns out it's 4
actual_pad = 4
known_content[-4:] = b"\x04\x04\x04\x04"
# start back 4 chars and guess the flag
i = len(enc) - 5
while i >= 0:
result = run_test(i)
known_content[i] = result
print(known_content)
i -= 1
```
this is pretty slow, as the challenge text says, on your own computer
but running it will eventually produce
```
...
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00rctf{CTR_p4dd1ng?n0_n33d!}\x04\x04\x04\x04')
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00orctf{CTR_p4dd1ng?n0_n33d!}\x04\x04\x04\x04')
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00corctf{CTR_p4dd1ng?n0_n33d!}\x04\x04\x04\x04')
```