Finding and Exploiting Code Caves

A code cave is a section of unused, empty bytes in a PE file. This empty section allows a developer or attacker to add shellcode to it, and redirect the execution flow to the code cave. The new instructions are run, and then the program returns to the original instructions. They do have legitimate purposes in some cases, but they are often used for game hacking and malware, as they provide a stealthy way to backdoor legitimate binaries.

There are many articles out there about how to use a debugger to find code caves and add write shellcode to them. But in this article, we will explore how to programatically find a code cave, write shellcode to it, patch the binary’s entry point to jump to the shellcode, and then jump back to the original entry point to resume execution.

In part two of the blog, we will create our own shellcode by writing position-independent Zig code that will download and execute a payload from the internet.

To keep things simple, we will only target x86 binaries and look for caves in the .text section of the PE file. More on PE sections later.

The first thing we’ll do is create a new C++ console app project in Visual Studio.

The program will take two arguments: the first is the PE file we want to backdoor, and the second is the raw shellcode binary file. I will skip going over the checks to ensure the correct command-line arguments are passed and that the target binary is 32-bit, but that will be in the full code on GitHub.

The next first thing we’ll do is read the shellcode payload into memory.

After that, we’ll open the target binary for reading and writing. I opted to use MapViewOfFile, instead of just allocating memory on the heap and reading into that. The reason for this is that with MapViewOfFile, we can write the shellcode directly to the file. That said, it’s probably a good idea to make a copy of the original file before modifying it.

Now pBuffer is the address of the file in memory. To search the file and find a code cave, we need to be in the right section. You can think of a PE file like a book with different chapters. PE-bear is a great tool for visualizing the makeup of a PE file. We will use putty.exe as the target PE file we attempt to backdoor. When we open it in PE-Bear, we can see the different sections of the PE file.

We will cast pBuffer to PIMAGE_DOS_HEADER so we can access its fields. MSDN is a great resource for documentation on the makeup of various structs. I also recommend looking at winnt.h and occasionally winternl.h on your local machine, as they are great for finding data type definitions that are not on MSDN. Your path may very, but you can typically find these files in:

C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\winnt.h

C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\winternl.h

We will access the field e_lfanew which is an offset to NT Headers.

From NT Headers we can access OptionalHeader from which we will get the AddressOfEntryPoint RVA. We can also use the IMAGE_FIRST_SECTION macro (defined in winnt.h) to get a pointer to the first section header.

All of these fields and values are also viewable in PE-Bear.

Now that we have a pointer to the first section header, we will traverse all the headers and look for the .text section. But first, I think it’s worth taking a look at the section headers and the different sections that make up a PE file.

The section headers are arrays of IMAGE_SECTION_HEADER structs that contain info about the various sections, such as:

  • SizeOfRawData: Size of a section in the file.

  • VirtualSize: Size of a section in memory.

  • VirtualAddress: RVA of the section in memory.

  • Characteristics: Memory access rights for the section. (R, RW, RX, RWX)

The sections that make up a PE file are as follows:

  • .text - contains executable code. (RX)

  • .rdata - read-only initialized data. (R)

  • .data - initialized global and static variables that the program may modify during execution. (RW)

  • .reloc - contains information about the address of the relocation table. (R)

  • .rsrc - resource section containing things like images.

My boss at my very first dev job once gave me great advice. He said, “Write the code you wish you had”. At first, I had no idea what he meant by that. But then it clicked. When you are writing code, write things you need and will use, even if they don’t exist yet, and create them later. That’s exactly what we will implement here. We will create a struct called CAVE_INFO to store information about the discovered code caves. We will loop through the sections and print their information. When we get to the .text section, we will pass a pointer to the section, the PE file buffer, and a pointer to the CODE_CAVE object to a function that will enumerate the section and perform the actual search.

Thinking about the CODE_CAVE struct, what are the important bits of information we will need? We will need to know:

  • Start address of the cave

  • End address of the cave

  • Size of the cave

  • File offset

  • RVA

So we will create the struct as follows:

The first half of the enumerateSection function will look like this:

The variable definitions should be pretty self-explanatory. We will iterate over the section's length and examine each byte. If the byte is 0×00, 0×90, or 0xcc we will set the previousAddress variable to the current address.

It should be noted that 0×90 and 0xcc are not super relevant for us since we are searching in the .text section, but it’s worth including for the sake of completeness.

Next, we check if pStartAddress is NULL. If it is, we will also set that to the current address. It will always be NULL on the first check, and if it is NULL, that means we are at the start of a potential code cave. If pStartAddress is not NULL, it means we are currently in a potential code cave, and we’ll increment the size and set the pEndAddress to the current address, just in case the current byte is the last byte of the cave.

We land in the else block if the current byte is not one of the bytes we are searching for. For example, reaching the end of a discovered cave. We will add a minimum length and bounds check (I chose a minimum cave size of 50 bytes), and create a CAVE_INFO struct and add it to an array of discovered code caves. Lastly, we clear pStartAddress, pEndAddress, and caveSize for the next loop iteration.

The second half of the function will look like this:

We will loop through all of the discovered code caves and select the biggest one. From there, we need to calculate the RVA and file offset.

A quick note on the differences between these two, because it can sometimes get confusing:

An offset, or file offset, is the distance to a location in the PE file beginning from the start of the file.

The RVA is the distance to a location in memory relative to the image base (where the PE is loaded by the OS).

To get the file offset, first, we need to find the cave section offset. This is the offset of the beginning of the code cave relative to the start of the .text section. To do this, we take the address of the start of the cave and subtract the address of the start of the .text section.

DWORD caveSectionOffset = (DWORD)(caveInfoArray[largestCaveIndex].pStartAddress - pSectionStart);

Then, to get the file offset, we add the cave section offset to the PointerToRawData field on the section header.

DWORD caveFileOffset = pSectionHeader->PointerToRawData + caveSectionOffset;

Finally, to get the RVA, we need to take the cave’s file offset and subtract PointerToRawData field value on the section header. We then take that value and VirtualAddress field value of the section header.

DWORD rva = pSectionHeader->VirtualAddress + (caveFileOffset - pSectionHeader->PointerToRawData);

Now that we have found a suitable code cave, we need to write our shellcode payload into the code cave, update the PE file's entry point to the start of the code cave, and add a jmp back to the original entry point at the end of our shellcode.

Back in the main function, we’ll create another function generatePayload that takes in the original entry point, pointer to the caveInfo struct, the address of the cave, a pointer to our payload buffer, and the size of the payload.

Our generatePayload function looks like this:

First, our code cave must be 5 bytes longer than the shellcode payload. This is because we need to add instructions to jump back to the original entry point. The entry point RVA is 4 bytes, plus one byte for the jmp instruction.

After checking the cave size, we proceed to write the shellcode into the cave with memcpy. I also decided to print the shellcode bytes here for extra verbosity, and since it’s a small payload.

Next, we have to jump back to the PE’s original entry point. But how do we do that? How do we know what address we need to jump to? We can’t just jump to the original entry point because once the PE file is loaded into memory, we won’t know the base address it’s been loaded into.

We will have to calculate the Relative 32-bit displacement (rel32), which is the offset distance to a target location (the original entry point in our case).

The formula for this is as follows:

rel32 = target_RVA - (jmp_RVA + 5)

The target RVA is the original entry point, and the jmp RVA is cave RVA + the size of the payload. We can then add the jmp instruction, 0xE9, and the rel32 address to the end of the shellcode.

Finally, we just need to update the PE’s entry point RVA with the RVA of the code cave and write the changes to the file with FlushViewOfFile.

As a simple PoC, we’ll test it with calc shellcode from msfvenom. It should be noted that the shellcode throws an access violation after popping the calc through no fault of our own. It’s fine for testing purposes; we just need to confirm the successful jump and execution of the shellcode. In part two, we will write our own shellcode using Zig, which will not have this issue and will allow the PE file to resume normal execution.

Thanks for reading. Full source code is available below, and stay tuned for part two.