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