NULL-Free Position-Independent Shellcode

Position independent shellcode refers to executable code that can be loaded and executed from any memory location, independent of its absolute address, making it highly flexible and resilient.

If you have written 32-bit shellcode for use in an exploit then you will be very familiar with position-independent code (PIC).

PIC is designed to be relocatable, meaning it does not contain any absolute memory addresses or offsets that would tie it to a specific location. Instead, it utilises relative addressing and runtime calculations to access data and functions within the code.

Various innovative techniques can be employed to circumvent the use of NULL bytes while preserving position independence in shellcode. This section will present some, but this is not an exhaustive study.

Jump Instructions

The jmp (jump) instruction is used to transfer control of the program execution to a different location in code. It allows for unconditional branching to a specified destination address, for example after a cmp instruction. The jmp instruction is commonly used for implementing loops, conditional statements, function calls, but its can be a useful instruction for handling general program flow, and avoiding NULL bytes.

In 64-bit shellcode the jmp instruction can make relative jumps to alter instruction flow. If we look at this meaningless 64-bit assembly below we can see that shorter jumps do not cause us any issue:

Short jump
main:
   xor rax, rax         ;   
   push rax             ;
   jmp sub1             ; this is the short jump - eb 04 (NO NULL BYTES)
   xor rax, rax         ;
   push rax             ;
sub1:
   xor rax, rax         ;
   push rax             ;

The short jump is compiled into eb 04.

However, if we want to make a longer jump this introduces NULL bytes, for example a jump of 204 is represented by the instruction: e9 cc 00 00 00.

In some circumstances it might be ok to have NULL bytes, for example in some local privilege escalation scenarios. When developing remote exploits more often than not NULL bytes will terminate our buffer and they should be avoided along with any other 'bad characters' (these are discussed in a later section).

If we jump backwards then we also avoid NULL bytes because we are making a negative relative jump.

We can sometimes make a series of short jumps to control the flow of our assembly instructions and avoid NULL bytes. The graphic below depicts different types of jumps (the red numbers indicate NULL bytes).

Whilst the jmp instruction is good for unconditional branching and non-structured control flow, it is not appropriate for function calls.

Call Instructions

The call instruction in x86 assembly language is used to call a subroutine or function. It transfers control to the specified subroutine and saves the return address on the stack, allowing for a subsequent ret (return) instruction to return execution back to the original calling location. It enables the reuse of code and helps manage program flow by encapsulating specific functionality into self-contained units.

Interestingly, we can use the call instruction to get a pointer to a function by not executing the ret instruction and popping the saved return address on the stack to a general purpose register instead:

Function pointer snippet
my_func_pis:
    jmp my_func_get_addr        ; make a short jump to get the function address
my_func_ret:
    pop r15                     ; here we can save the address of my_function in
                                ; r15 for use later (or we can save on the stack)
    jmp somewhere_else          ; continue code execution
my_func_get_addr:
    call my_func_ret            ; when this instruction is called, the address of
                                ; my_function is pushed on the stack
my_function:
    xor rax, rax                ; function starts here
                                ; ...

Now whenever we want to call this function we can simply use a call r15 instruction, this avoids NULL bytes in our calls because it uses a pointer to the function address instead of a relative call.

A flow diagram is shown below to demonstrate this:

An alternative approach to obtain the function address and avoid NULL bytes involves leveraging the following creative technique:

Function pointer snippet 2
get_function_address:
    lea r15, [rel get_function + 0x41414141] 
                                    ; load the function address in to R15
                                    ; with a large offset to avoid NULL bytes
    sub r15, 0x41414141             ; SUB the offset, this leaves the function
                                    ; address in r15
    jmp somewhere_else              ;
get_function:
    xor rax, rax                    ; function starts here
                                    ; ...

There are many techniques in which to generate NULL-free position-independent shellcode but these do not help us when we need to call Win32 functions. When our shellcode is injected in to memory we do not know the address of the functions we need to call, we don't even know the base addresses of the modules in which the function calls are made.

The next three sections will discuss this.

Last updated