For the past many months I have been working on (what I think) is a novel design for PIC shellcode implants. While the platform is still firmly in development, I figured it might be a good idea to release some technical details as the implant framework portion wraps up development so I can share some of the issues I have run into and the solutions I’ve come up with to address them.

There isn’t an open source repository for this (yet), so this post will be largely architectural in nature.

The Problem

All C2 frameworks I have used thus far are not (in my humble opinion) truly frameworks. Rather they exist as self contained access tooling with configurable edges that allow you to modify, but not rewrite, their core functionality. Recently there have been updates to allow users to “plug in” their own tooling in order to modify some small aspects of implant functionality (such as modifying sleep masks or proxying API calls), but these fixes still do not allow users to really develop large projects on top of their platforms in meaningful ways without a lot of hack-job solutions and reverse engineering.

Seeing this, I set out to design something closer to a development platform for implants that moves away from modifying or extending the payload in favor of being able to implement each module of the payload and fully define its capabilities, tradecraft, and signatures at the lowest possible level. Instead of generating a payload and extending its capabilities with a BOF or sleep mask, what if you could write a custom allocator or listener and just link the thing into the implant at compilation time?

The Solution

Dynamic Dispatch & Shellcode Linking

A linker’s main job is building out the OS specific headers and sections such that the locations of all of the binary’s components can be located to allow functions to call each other and be properly routed to external APIs. It needs to satisfy the OS loader that is going to actually call the executable.

PIC implants work differently, they need to be position independent. Shellcode does not contain the same sort of metadata about the program being compiled as a standard executable application. It is impossible to know before execution what the memory address of all of your functions are going to be, so I needed to come up with a different way to resolve this that did not rely on updating a header stored in RW memory at runtime. The solution is dynamic dispatch based on offsets from the start address of the shellcode.

Capturing and Preserving the Start Address

The first instruction that the implant needs to run is this:

__asm__ volatile("leaq (%%rip), %q0" : "=r"(implant_base));      
implant_base = (uint8_t *)implant_base - 0xd;

That snippet of assembly is fairly simple at first glance, it just takes the current value of the instruction pointer, assigns it to a variable, and then removes the length of the first instruction. This properly captures the start address of the implant. However, the more astute reader may ask the question “How are you preserving the implant base variable”?

That brings us to the next issue I encountered. If everything needs to be based off of that memory address in order to resolve its actual location, it needs to be preserved through every function call. I could just pass it as an argument to every single function, but this requires authors of additional capabilities to add more boilerplate to their code and is just generally a pretty inelegant solution. Enter explicit register variables, a GCC extension that allows you to force the compiler to reserve a CPU register to store a piece of data. This piece of code in the header implements it:

void* implant_base;
register void* implant_base asm("r14");

As you can see it defines what is, effectively, a global variable for implant_base. It then maps that variable to register r14. r14 is a callee saved register, meaning that any calls out to external functions like the Windows API are required to restore this before returning. Since it’s a register, it’s also globally writeable if for any reason that becomes necessary (like maybe in the case of sleep obfuscation).

Additional Usecases

This same technique also allows us access to one more register (r15) to store a potentially mutable pointer, and in the instance of this framework it is the memory address created by the allocator. This post is already long enough and I won’t go over that here, but I’m sure you can come up with ways that this is a useful technique.

We now have properly captured and preserved the starting address of the implant.

Building the Dispatch Table

Now that we have the start address, we need to define the struct that we are going to use to calculate the offsets of each component.

Here is a trimmed down version of the struct just to illustrate the point:

struct _ImplantCTX {
 
    uint32_t implant_size;
 
    uint32_t get_module_handle;
    uint32_t get_function_address;
 
    uint32_t winapi_call;
    uint32_t syscall;
};
 

This is basically the output of the linker in regards to the implant. During link time it takes in whatever additional shellcode blobs are needed, calculates their position as an offset from 0, and then implants the struct itself at a known offset from 0 (like 0x0+0x32). It can then calculate the whole size of the implant and write it into the payload directly, since that is immutable.

Now, during runtime, we can resolve its location like this:

extern uint8_t CTX_OFFSET[] __asm__("CTX_OFFSET");

In the binary output for the implant harness, this creates a relocation entry which is basically asking for this value to be placed into a certain offset in the binary. The linker can just write the raw bytes of the completed struct during linking.

It is a little bit more complicated than that because the relocation table requires you to do some math to determine the exact position, but for the end user this is effectively just an external variable that can now be accessed to get the offset from 0 of the context and cast it to the struct from above, like this:

ImplantCTX *ctx = (ImplantCTX*)((uint8_t*)implant_base + *(uint32_t*)CTX_OFFSET);

This takes the implant_base stored in r14, adds it to a uint32_t created from that external CTX_OFFSET variable, and from that gets a valid pointer to the above struct that can be resolved without knowing the memory address at runtime (meaning it’s position independent).

Calling Functions from the Dispatch Table

The context struct contains within it uint32_ts which are themselves offsets from the implant_base to function definitions. If you have a function prototype, like this:

typedef HMODULE (* fn_get_module_handle)(ImplantCTX *ctx, uint64_t module);

You can now use this collection of offsets in the table to resolve and call a function pointer:

((fn_get_module_handle)(void*)((uint8_t*)implant_base) + (uint32_t)(uintptr_t)(ctx->get_module_handle))(ctx, arg1);

I know that this looks extremely ugly but it isn’t too hard to macro. In the actual codebase I have it working like this right now:

RESOLVE(ctx->anti_analysis, fn_anti_analysis)(ctx);

Conclusion

The implications of this design philosophy are fairly interesting in my view. First and foremost it allows you to hot-swap modules that are defined in the dispatcher provided that they satisfy the function prototype. With a system like this, whenever you go to generate a payload, you can simply drop the “default winapi_call” for “my custom winapi_call” and as long as it provides the same inputs and outputs, anything is able to call it provided that it has access to the context. You don’t need to lug around dead code or patch in hooks at compile time because the entire implant harness is basically just running a series of hooks.

Furthermore it removes the need to have large RWX stubs at the beginning of your PIC shellcode implants in order to patch data. In fact, in the framework I am building now the entire implant runs and works in RX memory after execution. It contains an allocator for anything that needs to be written to memory, but it’s entirely RW and only contains data, NOT code, and is NOT colocated with the shellcode.

This is useful from a tradecraft perspective because it avoids the issues of needing to keep a large portion of your implant in multiple suspicious memory states at the same time (unbacked AND rwx) or to be constantly switching back and forth between the RW and RX.

I am aware that this post is likely a little scatterbrained and short, but I wrote a lot of this many months ago and if I went into full depth on everything it would be very long. As I continue making progress on this tool and verify some of the existing patterns I will create more posts that go into more detail on specific topics. Thanks for reading!