GrabCON CTF 2021 - Old Monk Pass

"Old Monk Pass" is a crypto challenge in GrabCON CTF 2021. You can download the challenge file here.

Details

The challenge file describes how a password is 'encrypted'. Our task is to find out what the password is and enclose it as the flag format GrabCON{<password>}. Thus, it is imperative that we understand how is the password 'encrypted' by understanding the python script.

First, we notice that a member function encode of the class pass_w is used to encode the password. The script also provided 3 different encodings enc, enc1 and enc2. Thus, we will have to understand what encode does.

Let's reverse this! We first note that the function takes in 2 arguments, text and i. Then, an if-else conditioning on i. The conditioning checks if i is negative or greater than the length of x by 1, and will randomize i by uniformly selecting a value between 0 and length of x + 1. So we can think of i as the 'seed' of the encoding.

if i < 0 or i > len(self.x) + 1:
    i = random.randint(0, len(self.x) + 1)

After 'seeding' the encoder, the seed is used to XOR with each character of text. The seed is incremented modulo 79, meaning the seed value recycles every 79 characters. We also notice before the XOR loop begins, the seed is actually the first character in the encoded output!

out = chr(i)
for c in text:
    out += chr(ord(c) ^ ord(self.x[i]))
    i = (i + 1)%79

return codecs.encode(out)

That's a fatal (intentional) mistake! If the seed is included in the encoding, we could use it to reverse the encoded message and get our flag. That's because the seed determines how the bytes in the original text is XOR'ed. Combining all of this information, we can build our decoder.

def decode(enc):
    seed = enc[0]

    out = ''
    for c in enc[1:]:
        out += chr(c ^ ord(gx[seed]))
        seed = (seed + 1)%79
    return out

The decoder is simple, in the spirit of this blog. First, we extract the seed from the encoded string, then we repeat the encoding process (because you undo a XOR by doing XOR again) to reverse the encoding. To test that this works, we could decode the encoded dummy text 'REDACTED' and see if we get the same one.

ct = y.encode("REDACTED")
print(ct)
pt = decode(ct)
print(pt)

By running the script, we get the following output. It confirms our decoding function works.

b"'\x1f\x08\x06\x0c\x19\x17\x0b-"
REDACTED

Now, what's left to do is to decode the 3 encoded strings found in the challenge script.

pt = decode(enc)
print(pt)
pt = decode(enc1)
print(pt)
pt = decode(enc2)
print(pt)

We obtain 3 similar strings, which when enclosed in the flag format GrabCON{<password>}, gives us the flag to the challenge.

817letmein40986728ilikeapples
817letmein40986728ilikeapples
817letmein40986728ilikeapples