Post

netmsg-2

Besides the netmsg application itself, you also managed to get your hands on a packet capture of one of the users of the service. Don't ask. Try your hand at cracking the strong encryption used to secure the traffic (see netmsg-1 for the client application).

This is the same binary as netmsg-1, but now we also have a .pcap file. We need to understand the interaction between the server and the client and try to decrypt the traffic in the capture file.

As the traffic uses encryption, the most important function to understand is main_connect. Data transfer is done with 2 groups of functions: SendMsg/RecvMsg (unencrypted), SendWrapped/RecvWrapped (encrypted).

After initiating the connection with Dial, the client sends a message containing the same string each time. We can see this by creating a netcat listener and running the client with the listener as a server.

This is most probably a magic value, or a version, or just some packet so that we can receive data from the server.

Then we have a call to RecvMsg and the value received seems to change with every connection. This might be some kind of identifier from the server.

We have a call to genRSA, which, at the end, seems to return n, e and d:

It sends this data (or some of it) to the server and this is the first time encrypted connection is used. But we don’t have a key yet. Let’s take a look at the implementation for SendWrapped, more specifically at EncryptPayload:

The code is pretty obfuscated, but we see a call to md5 and an AES encryption in CTR mode, using the crypto library. Let’s place a breakpoint before NewCipher, which has the key as a parameter:

Pwndbg neatly shows us the argument address. We can dump 16 bytes from there:

Now, let’s run the binary again, and dump bytes from the argument address once more. We notice that it’s exactly the same! So, while a key has not been agreed upon, the server and the client use a hardcoded key for encryption and decryption. We also figure out that the key is updated later, because:

EncryptPayload is called with c->CryptKey, and later on, after sending the RSA components, we notice that:

the CryptKey is updated with something received from the server, and decrypted using the d and n components.

Here’s what’s happening until now:

  1. The client sends a hello message
  2. The server returns a client ID
  3. The client generates an RSA key pair and sends the public key to the server, encrypting it with AES CTR with a hardcoded key
  4. The server generates a new AES key, encrypts it using the RSA public key and sends it back to the client (still encrypting it with the hardcoded key)
  5. The client decrypts the new AES key using its RSA private key, and then updates the AES key that will be used in the future

Now we have understood the key part, we are still missing the IV, though. Let’s see EncryptPayload once more:

It puts m->ID in a buffer (this field holds the value received from the server in step 2 above), then does some other stuff and calls md5. What are the chances that the IV is the md5 sum of the client ID? (100%)

Let’s place a breakpoint after receiving the client ID, more specifically in RecvMsg right after the call to UnmarshalBinary:

Pwndbg emulates the next instructions, which are a series of mov instructions, most probably involving a structure copy.

Now let’s place a breakpoint in EncryptPayload and see the arguments passed to md5...Write, which takes as a parameter the data to be hashed:

Notice that arg[1] is the same as the value to be moved in ECX from the previous screenshot. Now let’s place a breakpoint before the call to NewCTR, which has the IV as the second parameter (well, third, because in assembly the first argument is the struct itself, since it’s a member function):

And let’s dump 16 bytes from arg[2]:

Now let’s test our theory that the IV is the md5 sum of the client ID by calculating the hash in CyberChef:

Our theory was right! We can now decrypt the data in the PCAP file. But first, notice that at the end of the connect function there is another pair of send-receive. This is just to finish the “handshake” and confirm that both parties have successfully set up the encryption.

Here is how it looks in Wireshark:

We can still use dynamic analysis to make our life easier. Since we did not analyze the way the client decrypts the AES key (we just think it’s RSA but we did not see any calls to any actual RSA functions), we can write a fake server that mimics the actual server in the PCAP by replaying the packets.

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
import socket
from pwn import log

mappings = {
    "010000000033c4": "01c8bf0000b7da",
    22: "02c8bf270094736d1aaa024d1d86931d34f24db7f10b75eedd00e4c4b311dba0c1583ee6e66288809c3fcb0a042f",
}

io = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
io.bind(("0.0.0.0", 1337))
io.listen()

client, client_address = io.accept()
log.info(f"New connection from {client_address}")

while True:
    data = client.recv(1024)
    log.info(f"Received {data.hex()}")

    if data.hex() in mappings:
        to_send = mappings[data.hex()]
    elif len(data) in mappings:
        to_send = mappings[len(data)]
    else:
        log.info("Nothing to do")
        break

    log.info(f"Sending {to_send}")

    to_send = bytes.fromhex(to_send)
    client.sendall(to_send)

We now need to find out the values for n and d to insert them at runtime, since we have to match the connection exactly as in the PCAP file. Let’s place 2 breakpoints at the first call of SendWrapped, before the calls to NewCipher and NewCTR, to extract the initial hardcoded key and the IV (the IV will not change after receiving the key from the server):

Initial Key: 114a5bc70cacd58ca54d70c4797aed13
IV: d02a346d68ded459529a2480c7d0e8de

We can decode the packet containing the values for the RSA key using CyberChef. We have to get rid of the first 5 bytes, since they are just packet metadata.

Notice the 10001 bytes? That’s the value of e (65537 in decimal). This means that our decryption is correct!

Looking at the SendWrapped call when sending the RSA components, we notice that the type being sent is PayloadRsaPubkey, which IDA tells us it looks like this:

Be careful: the numbers are in little endian! So the numbers in the packet look like this:

n: 0xcb1dcc15

We do not have the value of d, however. But the primes are really small, we can find them on FactorDB:

We can calculate the value of d using a simple Python script:

1
2
3
4
5
6
7
from Crypto.Util.number import inverse
p = 52883
q = 64439
e = 65537
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
print(hex(d))
d: 0x95b208a9

Now, in order to decrypt the AES key received from the server, we first need to break at the end of the genRSA function and replace the values of d and n.

Once we do that, we can break into EncryptPayload before NewCipher and dump the key. But be careful to skip the calls before the key is set:

Final AES Key: 30e5188aa7f4262ed635078a325f333d

We can now plug the values into CyberChef and decode subsequent packets. Let’s take the first packet after the handshake, which contains the username and password:

We have successfully decrypted it and we have a username and a password! Let’s try logging in.

The mailbox contains the flag, and we have solved the challenge!

This post is licensed under CC BY 4.0 by the author.