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;
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;
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;
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:
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;
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;
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;
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;
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.
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!