The challenge

For this challenge we were given a file (namely TAKE_IT_EASY.zip) that contains two other files inside. One of them is a password-protected zip file and the other, getkey.txt, contains three numbers (p, ct and e) related to the RSA-encrypted password we need to decompress the first file.

Part I

To proceed into solving this challenge, we first need to break the encryption and recover the password. The three numbers were:

    p = 147310848610710067833452759772211595299756697892124273309283511558003008852730467644332450478086759935097628336530735607168904129699752266056721879451840506481443745340509935333411835837548485362030793140972434873394072578851922470507387225635362369992377666988296887264210876834248525673247346510754984183551
    ct = 43472086389850415096247084780348896011812363316852707174406536413629129
    e = 3

There numbers are the public exponent e, the ciphertext ct and p, one of the primes used to generate the modulus. The problem is that without n, recovering the plain text would be very hard as we would need q as well to calculate the private exponent d. But soon I noticed that the public exponent e was equal to 3, which is generally considered too small for encrypting anything because there is always the chance that \(m^e < n\) what means that the resulting ct would not be really encrypted as it did not wrap up the modulus and as such, m could be recovered by calculating \(ct^{1/e}\).

Seeing as p is a very large number (1024 bits), there is a big chance that this would happen, specially if no padding was used. To test this, I used the Perl module Math::BigInt to calculate the cube root of ct and convert it to bytes:

perl -MMath::BigInt -e 'print Math::BigInt->new("43472086389850415096247084780348896011812363316852707174406536413629129")->broot(3)->to_bytes(), "\n"'

And I got the key: Ju5t_@_K3Y

Using it, I extracted the zip file for the second part of the challenge.

Part II

After decompressing the file, we would get two other files (namely chall.py and cipher.txt). The contents of cipher.txt were as follows:

    B0 : b'\nQ&4'
    B1 : b"\x17'\x0e\x0f"
    B2 : b'1X5\r'
    B3 : b'072E'
    B4 : b'\x18\x00\x15/'

Each line (from B0 to B4) seems to be a python3 bytes object that was encrypted somehow. Looking into chall.py, we can get a clue of how to decrypt them:

#!/usr/bin/env python3

from struct import pack, unpack
flag = b'darkCON{XXXXXXXXXXXXXXXXXXX}'

def Tup_Int(chunk):
	return unpack("I",chunk)[0]

chunks = [flag[i*4:(i+1)*4] for i in range(len(flag)//4)]
ciphertext = ""

f = open('cipher.txt','w')
for i in range(len(chunks) - 2):
	block = pack("I", Tup_Int(chunks[i]) ^ Tup_Int(chunks[i+2]))
	ciphertext = 'B' + str(i) + ' : ' + str(block) + '\n'
	f.write(ciphertext)

As we can see, the flag was divided into several chunks of 4 bytes each. These chunks are then converted to 32 bit integers using the unpack() function from python and combined using a bit-wise xor operation, where each element i is combined with the element i+2. Seeing as we have 5 lines on the cipher.txt file and there were two more elements on the original flag, the original size of it was of 28 bytes ((5 + 2) * 4).

To revert this operation, let’s start with how theses chunks were combined with others:

   F  0    1    2    3    4    5    6
    |____|____|____|____|____|____|____|

B0 = F0 ^ F2
B1 = F1 ^ F3
B2 = F2 ^ F4
B3 = F3 ^ F5
B4 = F4 ^ F6

Now to undo this, we would need to know at least one of the chunks of the original flag in order to decrypt the others. Luckily, we already know that the flag must start with “darkCON{“, so the first 4 bytes (F0) would be “dark” and the next four bytes (F1) would be “CON{“.

Using this information, we can recover F2 and F3, which in turn allows us to recover F4 and F5, and then F6 can be recovered from F4. To do this, each 4-byte chunk must be converted to a 32-bit integer using the unpack() function and then xor’d against the respective element as follows:

    from struct import pack, unpack
    
    # lines from cipher.txt
    B0 = unpack("I", b'\nQ&4')
    B1 = unpack("I", b"\x17'\x0e\x0f")
    B2 = unpack("I", b'1X5\r')
    B3 = unpack("I", b'072E')
    B4 = unpack("I", b'\x18\x00\x15/')
    
    #known chunks
    F0 = unpack("I", b'dark')
    F1 = unpack("I", b'CON{')

    #recovering the rest of the flag
    F2 = B0 ^ F0
    F3 = B1 ^ F1
    F4 = B2 ^ F2
    F5 = B3 ^ F3
    F6 = B4 ^ F4

    #join everything

    print("".join([pack("I", f).decode() for f in [F0, F1, F2, F3, F4, F5, F6]]))

Because xor has the interesting property of being the inverse of itself (i.e, if n=a^b, n^a=b and n^b=a), the operations are reversed and we got our flag: darkCON{n0T_Th@t_haRd_r1Ght}

And that’s all, folks.