LossyORacle - CrossCTF Quals 2018 (crypto)
No one believes I can recover the message from this crappy ORacle.
nc ctf.pwn.sg 1401
Creator - prokarius (@prokarius)
Challenge
We are given a service that runs the python script lossyoracle.py. We can connect to the service using nc
and it will spit out the encrypted flag.
The script reads the flag file, then encrypts it with a key using the following function.
def encrypt(data, key, func):
length = len(key)
output = []
for i in range(len(data)):
output.append(func(data[i],key[i%length]))
return bytes(output)
From the following code, we notice that func
over here is just a bitwise OR (|) operation applied to each byte in the flag with the key.
function = [lambda x,y:x&y, lambda x,y:x|y]
print (base64.b64encode(encrypt(data, key, function[1])).decode("utf-8"))
One problematic thing is, the key is always random. Not only that the bytes in the key are random every time, but the length of the key is also random every time!
key = []
for i in range(random.randrange(64,128)):
key.append(random.randrange(0,255))
key = bytes(key)
Vulnerability
Or maybe it is not problematic for us, but a helpful vulnerability instead.
For the following, we will refer to ON as a bit being equal to 1, while OFF as a bit being equal to 0.
Bitwise OR
The property of a bitwise OR operation is that bits that are ON (equal to 1) will always stay ON, no matter what the bit is ORed with.
This means, for each byte in the flag, for every single bit in them, as long as they are ON, no matter what key is used, they will always be ON after encryption. On the other hand, for bits in the flag that are OFF, sometimes, since the key is randomly generated, they may end up still being OFF after encryption.
This means, we can take a lot different ciphertexts, and in at least one of them, the bits that are supposed to be OFF in the original flag must be OFF, since they definitely can't be ON forever, considering we are using a randomly generated key.
Bitwise AND
Now, the property of a bitwise AND operation is that bits that are OFF (equal to 0) will always stay OFF, no matter what the bit is ANDed with. So, we can just apply the bitwise AND operation on many different ciphertexts together.
How this works is that bits that are ON in the flag are ON forever regardless of the key, so even after applying bitwise AND on so many different ciphertexts they will still be ON.
On the other hand, if we have many different ciphertexts, there must be at least one occurence where a bit that is originally OFF stays OFF, as the corresponding part of the key encrypting it may be OFF as well. By applying bitwise AND on all the ciphertexts together, this bit will be turned OFF.
So, the final result would be a set of bytes, which has the same bits as the flag that are ON, and the same bits that are OFF, which is the flag!
Exploit
Get many ciphertexts
First thing we need is to automate the process of getting ciphertexts since we need A LOT of them.
We can easily do it using pwntools.
from pwn import *
import base64
HOST = 'ctf.pwn.sg'
PORT = 1401
r = remote(HOST, PORT)
m = base64.b64decode(r.readall().strip().encode('utf-8'))
assert len(m) == 14160
Along the way, we also convert the message to the correct encoding, and add a safety check to make sure the number of bytes received is always the same.
AND them all!
We can write a simple python function to do the bitwise AND between two strings for us. We initialize message to be a bunch of \xff
bytes, which means all bits in it are ON at the start.
message = '\xff' * 14160
def and_strings(s1, s2):
return ''.join([chr(ord(s1[i]) & ord(s2[i])) for i in range(len(s1))])
Combining all together
Our final solution is
from pwn import *
import base64
HOST = 'ctf.pwn.sg'
PORT = 1401
message = '\xff' * 14160
def and_strings(s1, s2):
return ''.join([chr(ord(s1[i]) & ord(s2[i])) for i in range(len(s1))])
for i in range(500):
print i
r = remote(HOST, PORT)
m = base64.b64decode(r.readall().strip().encode('utf-8'))
assert len(m) == 14160
message = and_strings(message, m)
open('flag', 'w').write(message)
At first, I tried to read the flag on the terminal, but I get a bunch of unreadable bytes. I was confused and doubted my solution. But then, I remembered it was 14kb of data, sounds more likely to be a file.
So, open('flag', 'w').write(message)
was added to save the decrypted flag in a file.
Reading the flag
While doing file flag
, I get
flag: MPEG ADTS, layer III, v2, 16 kbps, 24 kHz, Monaural
A MPEG file! Play it using mpg123 flag
and we get the flag!