diff --git a/2021/corctf/babypad/README.md b/2021/corctf/babypad/README.md new file mode 100644 index 0000000..c51ab7c --- /dev/null +++ b/2021/corctf/babypad/README.md @@ -0,0 +1,88 @@ +# 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](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') +``` diff --git a/2021/corctf/babypad/exploit.py b/2021/corctf/babypad/exploit.py new file mode 100644 index 0000000..cfbda55 --- /dev/null +++ b/2021/corctf/babypad/exploit.py @@ -0,0 +1,48 @@ +from pwn import * + +def run(): + #r = process(["python", "./server.py"]) + r = remote("babypad.be.ax", 1337) + enc = bytes.fromhex(r.readline().decode().strip()) + print(enc.hex()) + + def trial(s): + if isinstance(s, str): + s = s.encode() + r.readuntil("> ") + r.sendline(s.hex()) + result = r.readline().decode().strip() + return int(result) == 1 + + known_content = bytearray([0] * len(enc)) + + 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) + + # actual_pad = run_test(len(enc) - 1) + # print("found pad", actual_pad) + actual_pad = 4 + known_content[-4:] = b"\x04\x04\x04\x04" + + i = len(enc) - 5 + while i >= 0: + result = run_test(i) + print(known_content) + known_content[i] = result + i -= 1 + + return known_content + +if __name__ == "__main__": + run() diff --git a/2021/corctf/babypad/server.py b/2021/corctf/babypad/server.py new file mode 100644 index 0000000..20f1ec4 --- /dev/null +++ b/2021/corctf/babypad/server.py @@ -0,0 +1,37 @@ +from Crypto.Cipher import AES +from Crypto.Util import Counter +from Crypto.Util.Padding import pad, unpad +from Crypto.Util.number import bytes_to_long +import sys +import os + +flag = "fakeflag{this_is_super_fake_lol}".encode() +key = os.urandom(16) + +def encrypt(pt): + iv = os.urandom(16) + ctr = Counter.new(128, initial_value=bytes_to_long(iv)) + cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + return iv + cipher.encrypt(pad(pt, 16)) + +def decrypt(ct): + try: + iv = ct[:16] + ct = ct[16:] + ctr = Counter.new(128, initial_value=bytes_to_long(iv)) + cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + pt = cipher.decrypt(ct) + unpad(pt, 16) + return 1 + except Exception as e: + return 0 + +def main(): + print(encrypt(flag).hex()) + while True: + try: + print(decrypt(bytes.fromhex(input("> ")))) + except Exception as e: + pass + +main()