Now that BOFs are commonplace for Windows agents, some people have talked about wanting a non-Windows only version. In this blog post, we’ve got something for you: the same thing but for Linux/Mac. The process of building in memory loaders are the same, no matter the file format type. In this case, we’ll just cover the differences between the process for COFF objects and ELF objects. Building in memory loaders for ELF files isn’t anything new. The only new thing here is the addition of Cobalt Strikes BOF internal functions.
Understanding the ELF format
Unlike what we did with the COFFLoader, the header structures for ELF files are available. It starts with the Elf64_Ehdr structure, which holds the type, machine, entry point, and all needed info about sections. The second structure is the Elf64_Phdr (program headers) structure that is implemented as an array of entries. In this case, it isn’t used since nothing is being linked, so this could be skipped. The last two Elf64_Shdr (section headers) or Elf64_Sym (symbol) structures are going to be used for much of the loading. The section headers will identify the size, offset, and permissions which will be used for allocating, loading, and setting permissions.
For this we can do one of two things: We can either re-define the structures so that they will be more portable, or the other solution is to copy over a pre-defined header with minor modification. This wouldn’t be needed if you were only going to build on Linux or BSD based systems that use elf files, but for Mac you will need it since the header isn’t available. In the case of this project, the path I’m doing is making a ‘minimal_elf’ header based off FreeBSD’s headers. The main reason for using FreeBSD’s instead of Linux’s LGPL’ed headers is simply the licensing. Below are screenshots of all the header structures we’ll need to re-define.
Parsing the Format
Once you have all the structures defined, you can start parsing the object file. The process is pretty much the same as the COFFLoader writeup. The main differences are that you set the Ehdr structure as the start of the bytes read in, get the offsets for the program header, section headers, and symbol headers, then iterate over the section headers to load the sections into memory and repeat the process to handle all the relocations and symbols.
When iterating over the section headers, we can allocate memory with ‘mmap’, copy over the memory with ‘memcpy’, handle relocations in a separate loop, and then ‘mprotect’ all the sections with valid permissions. With COFFLoader, we used the same function lookup method as Cobalt Strike does for resolving symbols, whereas for this one we’re going to be resolving using the OS’s default libraries. In the case of Linux, we’re using libc, and Mac is libSystemB, both using ‘dlsym’ with re-defined RTLD_DEFAULT of the appropriate values.
Building Object Files
This is where things are different from the COFFLoader project. To build the object files, you need to build with a few flags. For x86 you need to use ‘-fno-stack-protector’, and ‘-fno-pie’. For x86_64 you just need to use ‘-fPIC’ and the rest is about the same. When building Mac-specific functions, you’ll want to double check OS with ‘uname’, then use ‘dlopen’/’dlsym’ to resolve the symbols and define all the functions you are going to use. No examples of that are in the project as of now, but will work on a few examples.
Once all the sections are copied over, relocations are handled, and permissions are set, the only thing left is to run the code. To that, we can just save the pointer of the entry point symbol, in this case “go”, cast it to the pointer, and then run the pointer.
Now that we have the ELF version built, we can use it across operating systems (Linux and Mac), without having to compile for each OS. Pairing this version with the COFFLoader version, we can use a standard process to share capabilities across agents and frameworks for both *nix and Windows systems.