2021: corctf: babypad
This commit is contained in:
parent
2e31754f85
commit
01b330f7d1
|
@ -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')
|
||||||
|
```
|
|
@ -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()
|
|
@ -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()
|
Loading…
Reference in New Issue