Joshua Campbell

Pwning Binaries with Surgeon: A Primer for Offensive Security Professionals

With the rise in popularity of security-oriented Linux distros like Parrot OS and Kali Linux, complete with their bundles of offensive security tools and no shortage of guides on YouTube and HackForums on how to use them, it seems like anyone can be a “hacker” nowadays. It doesn’t take any skill, or even knowledge, to fire up a tool like Wifite or Fern to try breaking into a poorly secured wifi network, but if you’re dependent upon a handful of tools written by people more knowledgeable than yourself, you won’t be successful on real-life red-team engagements with an actual IDS instead of a router left on default settings by a clueless, underpaid employee.
Hacking is an art, the practice of delving deeper, striving to understand things until you know them better than everyone else. The best aren’t reliant on tutorials and pre-built tools; the best tool is one you made yourself. These two principles lead me to build Surgeon, the Binary Pwning Tool (formerly CaveMan; download link at the end). It forced me to understand binary executables to the point that I could decode them and perform the calculations in the registers by hand, if need be, while allowing me to have full control over how to edit an executable file. If you’ve ever wondered how to back-door an executable, either in theory or are ready to get your hands dirty and spend god knows how much time debugging and making sure you’re undetectable, then this is for you.
This article is not entry-level material and requires an existing understanding of Assembly and Machine Code.

The ELF (Executable and Linkable File) Format

ELF is a standard for executable binaries, libraries, and core dumps, and is one of the most common file formats utilized by Linux systems. This guide will focus on ELF files, although Surgeon can also work with PE (Portable Executable) files. Every ELF file begins with what’s called the ELF header, whose first four bytes are always 0x7f 0x45 0x4c 0x46. This is the Magic Number, 0x7f followed by the ASCII values for the letters ELF. This magic number tells machines that can use ELF files that this is an ELF file. The ELF header is either 50 or 62 bytes, depending on whether it’s a 32-bit or 64-bit program; the first 24 bytes always indicate the same information, including the architecture. The byte following the magic number signifies the architecture, the byte following that indicates endianness, etc.
The ELF header contains all of the relevant information required to read and execute the file, including the layout of the executable. You can find a fantastic, in depth guide by Medium contributor James Fisher on all of the details here. The ELF header points to the offsets of the program header table and the section header table, tells us the size of the headers, number of entries in them, and most importantly, the entry point. Since everything, both data and instructions, is made of bytes, we can’t just begin executing bytes at the start of the file; the entry point points to the part of the program where actual, executable instructions are held, and instruction execution should begin.
Apart from the ELF Header, the other part of interest to us in this guide is the section header table. This table is basically the table of contents of an ELF file, pointing to the various sections, their lengths, their flags, etc. This tells us where to find the .data, .bss, .comment, and .text sections, as well as the PLT (Procedure Linkage Table) and GOT (Global Offset Table), and others.
The default behavior of Surgeon, when run without arguments or only with the -f <file> argument, is to parse the ELF Header and Section Header Table for us, and return the relevant information. (It also crawls for code caves in a predefined manner, that will be discussed below).
If we take a look above, we can see the parsed header and part of the section header table. Only the sections with an “X” marked under the flags are executable; attempting to execute code in a section marked non-executable will cause the program to SIGSEGV, and may trigger an IDS. We can also see the entry point is at 0x00000440, which corresponds to the offset of the start of the .text section, as we would expect because that holds the compiled instructions.

Code Caves and Back Doors

With the above information, we can begin to work on pwning a binary. In order to be sure that whatever our payload is will be executed, we need to do 2 things:
Find somewhere to store the code we wish to execute.Overwrite the Entry Point to point to our additions.
A common and easy, but sloppy method would be to add our own section, mark it executable, add an entry to the Section Header Table, and point the entry to there. This, however, presents a problem: we increase the file size of the executable. (Of course, if one wants to leave the entry point intact, pointing to the proper place, you can overwrite the first instructions with a JMP to your payload, execute it, then run the overwritten instructions before jumping back to the original execution flow.)
Let’s say, for example, throughout the course of an engagement, you decide to drop a modified version of a binary onto a target machine, that you intend to have executed. You can use social engineering, or backdoor a program that’s so commonly used, it’s almost impossible that the victim won’t wind up running it. Malware scanners can analyze the binary, and find that there are too many bytes inside of it for what is expected. It can analyze further, see there’s an extra section marked executable that isn’t expected, and flag it for quarantine.
Stealth is the name of the game here, there is a better way, and it’s called the code cave. Code caves are essentially a long, continuous string of Null Bytes. Sometimes compilers add code caves for alignment reasons between functions, or they can be used to allocate memory for run-time generated or self-modifying code. Whatever the reason, we can use them to store our code without overwriting existing code, or making the file size larger.
Ideally, one should find a section already marked executable with space for our code. Should we be unable to find one, we can use Surgeon to modify the flags set in the Section Header Table to allow execution of our code in the section we put it in.
In order to insert our code, we need to have code to insert. The easiest, fastest way is to usually write and compile the code you want, so you can pick out the compiled machine code and insert that where you want it. After executing your code, you have a few options:
  1. You can let the program continue, and most likely crash or segfault. This can trip anti-malware software and tip off the user and possibly an admin to your actions.
  2. Make a clean exit, and make the syscall to exit(). While a little better, this is only marginally so, because then our pwned program still won’t behave as it’s supposed to and run its own native code. This will also tip off the user to something being wrong.
  3. Make a jump in the execution flow back to the original entry point, and run as normal. This is ideal behavior that can leave our presence undetectable if done right.
Any instructions you provide will most likely affect various registers and states of the processor. In order to do it all correctly, you should take a look at how the registers look when the program begins execution at the entry point. Any registers that have changed by your code will need to be reset to their original values when you are ready to jump back to the original entry point, to ensure proper function.
Editing anything in a compiled program is dangerous, and the smallest of changes can have cascading effects that drastically change how the program behaves. It’s easy to shoot yourself in the foot here.
Ready? Let’s try it out.

A Simple Example

For the following example, I’ve decided to use a simple program I wrote that can act as a CrackMe and as a simple Proof-of-Concept to demonstrate how to trigger a Buffer Overflow and hijack execution flow. Seeing as it’s a compiled ELF executable file, it’ll also work perfectly to demonstrate the proof-of-concept outlined above. Our mission is simple: find a suitable place to insert some code, change the entry point of the program to that spot, and insert some machine code that we can execute. The machine code we will insert will only do one thing: jump to the instructions at the original entry point, so the program can continue as normal. The code is below if you’d like to follow:
#include<stdio.h>
#include<string.h>
void benderdontcare(char* input){
 char name[181];
 strcpy(name, input);
 printf(“%s? Yeah, I don’t care, meatbag.\n”, name);
}
int main(int argc, char** argv){
 if(argc != 2){
 printf(“That’s now how you interact with me, Bender. You need to say something! And if you put spaces in it, put the whole thing in quotes! What am I, an AI language parser? (I’m only 40 percent language parser!)\n”);}
 else{
 int bender = 1;
 printf(“I’m Bender, the Magnificent body-stealing robot!\n”);
 if(bender==2){
 printf(“But Inspector 7 said I was perfect!\n”);
 }
 benderdontcare(argv[1]); 
 printf(“Anything less than immortality is a complete waste of time.\n”);
 return 0;}
}
The default actions of Surgeon search for any code caves in all sections (the -A argument) of null bytes (0x00) of at least 64 bytes in length, in all sections rather than only executable ones. A snippet of the default output was shown above, but Surgeon was unable to find any suitable code caves. Since we’ll only need a few bytes, we can specify a lower number of consecutive nulls to find a good cave.
To narrow the workable results down, we’ll search for only the executable sections, and need only 8 bytes to find a cave. Lo and behold, one reveals itself, in the Procedural Linkage Table:
Okay, we have a place to put our instructions! Since we are only going to perform a JMP, we can simply look up the syntax here. We’re lucky because the offset of the code cave is at 0x3e9 and the original entry point is at 0x440, meaning they’re only 0x57 (decimal 87) bytes away from our target! This puts us in range to perform a short jump, requiring the smallest number of bytes for our opcodes, 2 bytes.
According to the above guide on x86 instructions, the opcode for a short jump is 0xEB, followed by the signed byte value (-128/+127) for the position of the jump to make. As a rule, whenever I insert my own code in a code cave, I make sure to leave the first byte as a null byte. Null bytes are often used as terminators, especially when you must use a section that was previously data, not instructions, and not executable. While not always necessary, best practices should be applied consistently.
As a result, we will start inserting our code at 0x3ea, instead of 0x3e9. The first byte is 0xEB, telling the processor to perform a short jump. The byte after is located at offset 0x3eb. The next byte would ordinarily be located at offset 0x3ec, which is 0x54 (decimal 84) bytes from our target, the original entry point, 0x440. So, we need to jump 84 bytes forward. The opcodes we need to insert are then 0xeb 0x54, at the offset 0x3ea, like so:
Did it work? We can still run the executable with no discernible difference in performance from where we see it, so let’s fire up GDB and step through the instructions! Using the “info file” command, we can see the entry point is located at 0x3ea. However, when the file is actually run, it’ll be loaded at a different spot. Telling GDB to place a breakpoint at 0x3ea will result in it not setting a breakpoint at the start of execution. To determine where we need to set our breakpoint, we need to use the “starti” command. GDB usefully has starti to let us step into the first instruction, before even _start or main, and stop at a breakpoint. Mine stops at “0xf7fd6c70 in ?? () from /lib/ld-linux.so.2". Using the “info file” command here shows us our true entry point that can have a breakpoint set at it: “Entry point: 0x565553ea.”
So, we will set a breakpoint at 0x565553ea, and run the program. This will halt execution right at our injected shellcode, allowing us to monitor opcodes as we step through it. The command “disas/r 0x565553ea, 0x565553ec” will force GDB to return to us the opcodes and Assembly code we’re looking for, instead of grumbling about having no function at the specified address . GDB reflects the exact changes we’re looking for: a JMP instruction to 0x56555440, <_start>, our original entry point.
Here, we can see that we’ve successfully added our own shellcode to a completed, compiled binary, inside of an available code cave. This shellcode executes exactly as expected, before returning control to the original entry point, allowing our program to work as originally intended while not tipping off an end user to the presence of our code. We did not have to modify the binary by adding our code in a way that changed the file size, nor did we have to modify existing sections that were not marked as executable. This is a perfect Proof of Concept for our pwned file.
Surgeon is a work in progress with support for PE Windows files being added. You can download it for free on github here. You and you alone are responsible for what you do with it. In no way should this article be interpreted as an endorsement for any illegal activity; this is meant to empower Red Team members who may not have had knowledge on how to do this, and Blue Team members who need to understand how this works to defend against it. With that said, happy hacking!

Tags

More by Joshua Campbell

Topics of interest