Position independent code (PIC). What is it? And why is modern malware dependent on it?

Wherever you look, they seem to be the core of every modern command and control framework, such are Cobalt Strike, Havoc, Sliver, etc...

A lifecycle of a shellcode implant looks somewhat like this; First you need to inject your shellcode into a locally running process via one of many "process injection" techniques. This is done usually when you already have a first stage malware present within the target's environment. Or at the other hand inject it remotely via vulnerabilities such are buffer overflow or "user after free" bugs.

What is position independent code?

Well, let's start from the beginning. Position independent code is a blob of machine code that can be executed regardless of the memory layout. In software development, it is commonly used for things such are shared libraries. Best example of such is ntdll and kernel32 DLLs on Windows, and libc on Linux.

But what does that mean in maldev? In practice, a blob of PIC machine code is dependent on the CPU architecture such is x86 or ARM. It is not dependent on the operating system. This means that you can have one file, a single shellcode that targets multiple operating systems. However, it was never done before due to engineering costs, that is... until now.

This means that standard library of the programming language you're using to write your implant in is unavailable, because it relies on specific stack and heap structures to be in place. Then you might be wondering "how do you even do anything?", well... malware has a few cards in its hand.

Direct syscalls vs system libraries

On both Windows and Linux it can execute direct system calls using CPU instruction named "syscall". Look at system calls as functions. Kernel would look at a special CPU register to understand what system call aka. function you're trying to invoke, and would take values from some other general purpose registers and pass them to the system call as arguments. Nothing in the user-land happens without system calls. When you use standard library of C or C++, it would invoke system libraries which in turn would invoke system calls. By doing direct syscalls we get rid of the middleman.

However, middleman is fine, moreover it is convenient to use system libraries. Therefore, malware can do a couple of tricks.

PEB walk on Windows

On Windows every user-space process has a structure named Process Environment Block (PEB). It is unique and tailored to each individual process because the kernel allocates it inside of the process' own memory space and can be located by reading its 8-byte pointer located at the offset 0x60. Process enviornment block contains a list of memory addresses of loaded dynamically-shared libraries aka. DLLs, request it or not, Windows provides DLLs to your process. Getting the addresses of those DLLs is called "PEB walk". Once you find addresses of DLLs such are ntdll or kernel32, you can pretty much do anything that a normal application can. The doors to the castle are open!

Link map walk on Linux

But wait, on Linux we do not have DLLs, then how do we interact with the system without making direct system calls? Well, that's why we perform a technique named "link map walk". On Linux system there is the dynamic linker, it maintains a list of every shared object mapped to a process. Shared objects are Linux equivalents of DLLs, sometimes refered to as SO libraries. Instead of reading 8-byte pointer on offset 0x60 like on Windows, we would instead scan upward from the current stack pointer to find the base address of dynamic linker. Meaning that we are looking for ELF auxiliary vector with assigned value of 7 (AT_BASE tag) that kernel specified. Once we locate the dynamic linker we can walk the loaded libraries exactly as PEB walk walks the list of DLLs on Windows. When we find the entry for libc we parse its ELF headers, locate the symbol table and resolve the functions we need. Note that there are multiple ways to locate the dynamic linker.

You see, it's all about using the system libraries without ever officially asking the operating system for them. From that point on, our shellcode aka. PIC malware gains capabilities to do everything that a normal application can. Another advantage of using this "middleman" is that system calls change from version to version, and in the case of closed-source operating systems such is Windows, there might be little to no documentation on some of them. Tho this is certainly not the only advantage, PEB and link map walks help us bypass user-mode API hooks and many EDR telemetary points.

That being said; it's important to note that most real-world implants are almost always OS-dependent, because they simply do not have the code for both PEB and link map walk. And even if they do it requires a lot of effort and care to create utility functions for your implant that produce equivalent behavior across operating systems. Theoretically speaking, it is possible to create shellcode based implant that functions on Windows, Linux, Mac and Android... Tho for sure as shit it would be difficult.

Piclang

Throughout my research I have been writing PIC malware in C++ and a little bit of Zig. I have to admit, I wrote Zig because I hate C++ and it did not used to be that way. I have started my programming journey at 10 years old learning C++, and I fell in love with it, however, C++ has grown into that toxic ex you hate-love. Too many baggage... overlapping features, complex syntax, and in my opinion there is no clear direction for where that language is going.

On another hand there is the C language, however, by reading a lot of PIC malware code written mainly in C++, and because of statements made by people I respect I thought that C language is simply not good enough for one reason or another. Therefore, I have decided to make my own programming language.

I had a clear vision in mind; make a programming language specifically tailored for writing position independent code malware. And thus "Piclang" was born. (https://github.com/zarkones/Piclang)

It is statically-typed programming language that compiles to raw position independent x86-64 shellcode. It provides many quality of life improvements over writing PIC malware in C++. It allows you easily to interact with strings and obfuscates them, tho it also obfuscates section and function names, all this hinders static analysis. It would take a lot of effort to do in C++, however, in Piclang it's done by default.

And most of all, the biggest quality of life improvement comes from its standard library which is powered by a lightweight runtime that performs PEB or link map walk depending on the operating system in order to find ntdll & kernel32 or libc. Then it adds another middleman in a form of a layer of abstraction through the standard library itself.

In Piclang you can import "std/file" package and write to file using "file.WriteString" function. No need to write operating system specific code. An example of fully working implant can be found here: https://github.com/zarkones/Piclang/tree/production/examples/implant

However, all this is not enough to win the hearts and minds of people. They won't switch from an industry standard such is the C mono-culture and bet their implants on immature programming language. Nonetheless, it was not in vain... Piclang was a testament to my research. Payload in a form of a single shellcode that targets multiple operating systems, no one has ever done that...

Porting the idea to C

Tho it got me thinking... Piclang is really similar to the C programming language, and by accident I have picked up a book on modern C in the same week that I finished the initial version of Piclang. Therefore, I started porting the spirit of Piclang's standard library to C. So far it resolves libc or ntdll & kernel32 depending on what operating system it detects. And it follows a convention of modules. A module can be something as simple as privilage escalation function to something complex such is a standalone fully featured implant.

The way you'd use is the following:

Libc libc = {
    .base = nullptr,
};


Win win = {
    .ntdll = nullptr,
    .k32 = nullptr,
};


ModuleContext ctx = {
    .libc = &libc,
    .win = &win,
    .ready = false,
    .os = RUNTIME_OS_UNKNOWN,
};


// Figure out our environment, like OS, resolve libc/ntdll/kernel32, etc...
if (!init_module(&ctx, stack_hint)) {
    return 1;
}

Then we can write our logic utilizing platform specific code:

INLINE_STR(file_path, "./output.txt");
INLINE_STR(payload, "Hello from a cross-platform position-independent memory block!");


int fd = ctx.libc->open(file_path, 577, 0644);
if (fd < 0) {
    return 1;
}


size_t len = 0;
while (payload[len]) {
    len++;
}


long written = ctx.libc->write(fd, payload, len);
ctx.libc->close(fd);


if (written != (long)len) {
    return 1;
}

This PIC C framework is not yet available, however, it will be in the future. I'd recommend you to stay tuned, I constantly do research and will publish more interesting and innovative stuff in the future.

Closing thoughts

I would like to close my remarks with the following statement; Even in the face of AI, never give up on learning and refining skills. Think about it, the alternative makes no sense. Double down on being technical. I want you to really think about it because the argument makes no sense. We have an imposter syndrom as a species.

Caesar once stated "Do you think I have not just cause to weep, when I consider that Alexander at my age had conquered so many nations, and I have all this time done nothing that is memorable?", he said that and went on to become the ruler of the known world.

We have built AI, and we will go on to build greater things. Moreover, we cannot give agency to AI. All this I'll explore in my next blog...