Writeup DG’hAck: Involucrypt 1
This writeup uses a naive approach, see my writeup of involucrypt 2 for a better solution.
The challenge starts off with two files:
crypt.py
involucrypt1
The first one is a script that can encode a message, the second one contains crypted data.
Here is the (slightly redacted) script:
import itertools
import operator
import sys
BLOCK = 150
class mersenne_rng(object):
...
def keystream(seeds, length, base=None):
key = base if base else []
for seed in seeds:
random = mersenne_rng(seed)
for _ in range(BLOCK):
if len(key) == length:
break
key.append(random.get_rand_int(0, 255))
random.shuffle(key)
if len(key) == length:
break
return key
def encrypt(string, key):
key = keystream(map(ord, key), len(string))
stream = itertools.starmap(operator.xor, zip(key, map(ord, string)))
return bytearray(stream)
if __name__ == "__main__":
if sys.version_info >= (3, 0):
sys.stdout.buffer.write(encrypt(sys.stdin.read(), sys.argv[1]))
else:
sys.stdout.write(encrypt(sys.stdin.read(), sys.argv[1]))
I omitted the mersenne_rng
class: you don’t need it to solve the challenge. It’s a particular type of RNG, the one used by python2. Since the script is cross-version, I think it’s just there to ensure coherent results.
The encryption consists in XORing the input data with an array of integers, which is generated by the keystream
function. This stream is composed of different blocks of 150 integers. Each block corresponds to a letter of the key, whose ASCII code is used to seed the RNG that generates the block integers.
The tricky part is that after adding an integer to the stream (called key
here), it is shuffled:
key.append(random.get_rand_int(0, 255))
random.shuffle(key)
This makes brute-forcing the blocks independently pretty hard. Fortunately, we don’t have to!
$ wc -c involucrypt1
370 involucrypt1
The file is 370 bytes long, so the key must be 3 chars long (370 divided by 150 is 2.47). This is really short, so let’s brute-force it!
import math
import string
import itertools
# Note: I renamed the script `crypt.py` to `ccrypt.py`, to avoid naming issues.
from ccrypt import keystream
ALPHA = string.ascii_lowercase
with open("involucrypt1", "rb") as f:
data = f.read()
block_count = math.ceil(len(data) / 150)
iterations = len(ALPHA) ** block_count
for index, key in enumerate(itertools.product(ALPHA, repeat=block_count)):
percent = index / iterations * 100
print(
f"{percent:.2f}% ({index} / {iterations}) : " + "".join(key),
end="\r",
flush=True,
)
key = keystream(map(ord, key), len(data))
decoded = ""
valid = True
for char_key, char in zip(key, data):
decoded_char = chr(char_key ^ char)
if decoded_char not in string.printable:
valid = False
break
decoded += decoded_char
if valid:
print("\n")
print(decoded)
break
After a second and a half (thanks to pypy3) short seconds, we see :
$ pypy3 brute_1.py involucrypt1
1.95% (342 / 17576) : ane
The flag is: TheKeyIsTooDamnWeak!
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
I assumed the key was only made up of lowercase letters, but if the bruteforce didn’t work, I would have started it again with ALPHA = string.printable
.
The key is ane
, and the flag is TheKeyIsTooDamnWeak!
. See my writeup of involucrypt 2 for a better solution.