HackTheBox CA 2022 - Android in the Middle

Within this post I’ll be doing a write up of the Android in the Middle challenge from the HackTheBox Cyber Apocalypse 2022 CTF competition (14/05/2022). This write up will be written according to my throught process whilst I was trying to complete the challenge.

Reconnaissance

First look

After downloading the challenge from HackTheBox’s CTF platform, I decided to open the zip file and take a look at the contents. Within the zip is a single file, ‘source.py’. I went through this file and trimmed out the non-essential parts in order to make the source easier to read. The result is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes
import hashlib
import random
import socketserver
import signal


FLAG = "HTB{--REDACTED--}"
DEBUG_MSG = "DEBUG MSG - "
p = 0x509efab16c5e2772fa00fc180766b6e62c09bdbd65637793c70b6094f6a7bb8189172685d2bddf87564fe2a6bc596ce28867fd7bbc300fd241b8e3348df6a0b076a0b438824517e0a87c38946fa69511f4201505fca11bc08f257e7a4bb009b4f16b34b3c15ec63c55a9dac306f4daa6f4e8b31ae700eba47766d0d907e2b9633a957f19398151111a879563cbe719ddb4a4078dd4ba42ebbf15203d75a4ed3dcd126cb86937222d2ee8bddc973df44435f3f9335f062b7b68c3da300e88bf1013847af1203402a3147b6f7ddab422d29d56fc7dcb8ad7297b04ccc52f7bc5fdd90bf9e36d01902e0e16aa4c387294c1605c6859b40dad12ae28fdfd3250a2e9
g = 2

def decrypt(encrypted, shared_secret):
    key = hashlib.md5(long_to_bytes(shared_secret)).digest()
    cipher = AES.new(key, AES.MODE_ECB)
    message = cipher.decrypt(encrypted)
    return message

def main(s):
    c = random.randrange(2, p - 1)

    M = recieveMessage(s, "Enter The Public Key of The Memory: ")

    try:
        M = int(M)
    except:
        sendMessage(s, DEBUG_MSG + "Unexpected Error Occured\n")
        exit()

    shared_secret = pow(M, c, p)

    encrypted_sequence = recieveMessage(
        s, "Enter The Encrypted Initialization Sequence: ")

    try:
        encrypted_sequence = bytes.fromhex(encrypted_sequence)
        assert len(encrypted_sequence) % 16 == 0
    except:
        sendMessage(s, DEBUG_MSG + "Unexpected Error Occured\n")
        exit()

    sequence = decrypt(encrypted_sequence, shared_secret)

    if sequence == b"Initialization Sequence - Code 0":
        sendMessage(s, "\n" + DEBUG_MSG +
                    "Reseting The Protocol With The New Shared Key\n")
        sendMessage(s, DEBUG_MSG + f"{FLAG}")
    else:
        exit()

So it appears that the file provided is the source code for the challenge, which will be operating on a container on the CTF platform and which I have to exploit in order to get it to spit out the flag.

Analysis

Looking through the source, it seems that the program is using my input (variable M) to calculate a shared secret. The calculation can be described as shared_secret = M ^ c % p, where ‘c’ is a random number between 2 and p - 1, with the value of ‘p’ being defined at the top of the source as a huge number. This immediately stuck out to me. There must have been a reason I was able to control the value of ‘M’, but I couldn’t quite figure out why.

So I decided to create the following script called ’example.py’.

1
2
3
4
5
6
7
8
9
import random

p = 0x509efab16c5e2772fa00fc180766b6e62c09bdbd65637793c70b6094f6a7bb8189172685d2bddf87564fe2a6bc596ce28867fd7bbc300fd241b8e3348df6a0b076a0b438824517e0a87c38946fa69511f4201505fca11bc08f257e7a4bb009b4f16b34b3c15ec63c55a9dac306f4daa6f4e8b31ae700eba47766d0d907e2b9633a957f19398151111a879563cbe719ddb4a4078dd4ba42ebbf15203d75a4ed3dcd126cb86937222d2ee8bddc973df44435f3f9335f062b7b68c3da300e88bf1013847af1203402a3147b6f7ddab422d29d56fc7dcb8ad7297b04ccc52f7bc5fdd90bf9e36d01902e0e16aa4c387294c1605c6859b40dad12ae28fdfd3250a2e9

c = random.randrange(2, p - 1)

M = 1

print (pow(M, c, p))

This script contained the variables ‘p’ and ‘c’ as seen within the source, but the power calculation was instead done with M being the hardcoded value of 1. The output from running the script is as follows:

The result of running the example.py script

I ran it a couple more times just to be sure that the output was consistent and this wasn’t a fluke.

The result of running the example.py script several more times

So it seemed that when the variable ‘M’ is set to 1, ‘shared_secret’ would also be 1, but why? Well, if you broke the equation down, it made an awful lot of sense. If you subsituted M = 1 into shared_secret = M ^ c % p, you got shared_secret = 1 ^ c % p. As a rule, 1 times itself any number of times will always be 1, no matter what, so the ^ c can be removed from the equation. So I had 1 % p, in which case % p can also be ignored, as 1 is far less than the value of p, so having % p in this equation will not change the outcome. So effectively, I had ‘shared_secret’ = 1, when ‘M’ = 1.

Exploitation

Understanding the key generation

Brilliant. So I was able to control the shared_secret, but what was it actually used for? I took at look back at the source to find out, with these particular sections of the source becoming of interest to me.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def decrypt(encrypted, shared_secret):
    key = hashlib.md5(long_to_bytes(shared_secret)).digest()
    cipher = AES.new(key, AES.MODE_ECB)
    message = cipher.decrypt(encrypted)
    return message

    encrypted_sequence = recieveMessage(
        s, "Enter The Encrypted Initialization Sequence: ")

    try:
        encrypted_sequence = bytes.fromhex(encrypted_sequence)
        assert len(encrypted_sequence) % 16 == 0
    except:
        sendMessage(s, DEBUG_MSG + "Unexpected Error Occured\n")
        exit()

    sequence = decrypt(encrypted_sequence, shared_secret)

    if sequence == b"Initialization Sequence - Code 0":
        sendMessage(s, "\n" + DEBUG_MSG +
                    "Reseting The Protocol With The New Shared Key\n")
        sendMessage(s, DEBUG_MSG + f"{FLAG}")
    else:
        exit()

So it appeared that the ‘shared_secret’ was used to calculate an MD5 value, which was then used as the key to decrypt a message encrypted with AES submitted to the program. Then, if the content of the decrypted message is ‘Initialization Sequence - Code 0’, then the program spits out the flag. So all I needed to do was write a script which, firstly, implemented an encryption function using AES and then used that function to encrypt the string ‘Initialization Sequence - Code 0’. Then I would be able to send this to the program and get the flag.

Implementing the exploit

I ended up creating the following script called ‘genmessage.py’:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes
import hashlib

shared_secret = 1

def encrypt(shared_secret, message):
    key = hashlib.md5(long_to_bytes(shared_secret)).digest()
    cipher = AES.new(key, AES.MODE_ECB)
    encrypted = cipher.encrypt(message)
    return encrypted

print(encrypt(shared_secret, b"Initialization Sequence - Code 0"))

So all I needed to do was test it! After running the script it gave me the following output:

The output of the genmessage.py script with the encrypted message in the wrong format

Hmm, I wasn’t entirely sure if it would work with the hex in that format, but I tried regardless. I connected to the container running on the CTF platform and entered in the values.

A failed attempt to exploit the program

Unfortunately it didn’t work. I decided to do some research on how to get python to output the hex in a nicer way. Eventually, I discovered that I could use the .hex() function on the bytes object to convert it to a hex representation. Therefore, line 13 instead was:

1
 print(encrypt(shared_secret, b"Initialization Sequence - Code 0").hex())

Retry

I tried running the script again.

The output of the genmessage.py script with the encrypted message in hex format

Great, that looks better! Now to exploit the program.

A successful attempt in exploiting the program

Brilliant, it worked! This was an interesting challenge for sure.

Mitigations

Key generation

Values less than 2 should not be provided to the python ‘pow’ function in order to ensure acceptable entropy and keep the key unpredictable.

Monero

Monero

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy