How To: Writing 64-Bit Shellcode - Part 1 (Beginner Assembly)

Writing 64-Bit Shellcode - Part 1 (Beginner Assembly)

In this simple tutorial you will be shown step-by-step how to write local shellcode for use on 64-Bit Linux systems. Shellcode is simple code, usually written in assembly that is used as payload in exploits such as buffer overflow attacks. Payloads are the arrow head of an exploit: though the rest of the arrow is important for the delivery of the attack, the arrow head deals the killing blow. In reality, payloads are slightly less exciting yet far more interesting and intelligent than medieval weaponry. What they actually do is execute code on a target computer. Though this code could do potentially anything that the size of your shellcode may allow, they generally throwback shells because this gives the attacker the ability to take control of a system and execute far many more commands than what a single payload may allow. For this tutorial, previous knowledge regarding assembly and C is very much recommended in ameliorating your understanding on the subject, however I will try to be rather verbose so that if you are new to this you can still take something from the tutorial. We will first write the code, then use it to exploit a program and throw back a shell on our 64-bit Kali Linux system.

In this tutorial I will be using the Kali Linux 64 bit operating system, GAS Syntax which is the standard assembly syntax for gcc, the gcc compiler for C programs and the as and ld commands for assembling and linking our programs.

Let's begin.

Our Program in a Higher Language

Before we move on, we need to know what exactly what we want our shellcode to do. If you are familiar with C, you may recognize this code. However, if you are not all you need to know is that this code simply throws back a shell by replacing the programs primary execution by whatever is specified as an argument.

Image via

As you can see above, it will replace program execution with /bin/sh and for our purpose of evil, execve is perfect, because if you can successfully exploit a root program, it will throw you back a shell with root privileges. Execve takes as argument 3 null terminated pointers to string literals, however you can just stick in string literals and nulls like shown above and it will work just fine.

Image via

As demonstrated above, running the program lol.c is the same thing as running ./bin/sh, which is your shell.

Making Our C Program in Assembly

Alright, let's move onto the real meat of this tutorial: the assembly section. Here is our shellcode written in assembly using GAS SYNTAX (seriously, take note of this):

Image via

I hope you aren't lost yet, as this still isn't useful to anything but our development process yet. We still have to take this code and turn it into machine code and then figure out how to make a program return execution to our injected payload. Let's take things one step at a time though, as we first work through our assembly code and understand it. Let's begin with the first two lines: .section .data and .section .text.

Data and Text Section

Without going into too great of depth here, for now let's just realize that this declares to your assembler that this is the data and text section of the code. The data section is where you declare initialized variables to the assembler. You can usually ignore this entirely because we don't use this section for our shellcode anyway, HOWEVER for our first rough draft program we will use it to store our string literal /bin/sh. However, the text section is very important to us. This is where we put all of our code, which is going to speak directly to the processor. This is our payload. Following is .globl _start and start. This tells our assembler that the start symbol is global and to start the program from _start. All of these formalities are important from a programming perspective, but don't be freaked out by them because you honestly don't need to know anything beyond the fact that they are there and you prepend your respective section with them. Let's move onto the real code now.

64 Bit Registers

After inspecting the code you may have noticed similarities between every three letter word beginning with a r. Very astute of you! If you are familiar with 32 bit assembly, just know that those are your 64 bit counterparts to your 32 bit registers. Instead of %eip, %esp and so on, you now have %rip, %rsp, etc. Just replace the e with the r. If you aren't familiar with assembly, an explanation is in order.

A register is kind of like a variable for the cpu. Instead of passing in long numbers and addresses for the computer to manipulate, you use registers to store these numbers. There are in fact still 16 bit registers that you can use, as well as 32 bit registers we saw earlier, like %eip and %esp (the e is for extended, signifying that they are an extension from 16 bit registers). These correspond with the size of the computer architecture you are using, and all have respective sizes. However, we won't use any of these smaller registers right now because our 64 bit system calls, which will be explained in a moment, need to use our 64 bit registers.


Instead of calling function directly like was shown in C by using execve, we need to use syscalls like the one at the very bottom of our code above. If you ever programmed in 32-bit assembly, you will know this as int $0x80. Just know for now that this calls the system to take over for a moment and passes certain registers as arguments to the system, which in this case is going to pass these arguments to the execve function of libc. Knowing this intimately is unnecessary, just know that you end the program with syscall, it calls some stuff with your registers (in this case, execve) as arguments, and then voila, shell.

Note: When calling syscall, %rax holds the syscall number which will determine what you want to happen at the call (1 is exit, 59 is execve, etc) and then the subsequent arguments go into %rdi, %rsi and %rdx.

Loading Our Arguments

Lets head back to the start though, and take a look at the lines pushq $0 and pushq name. Just know for now that the push command pushes items onto an abstract location of memory called the stack. The q at the end stands for quad word, which is standard in 64 bit assembly. Next we push the value $0 onto the stack (the $ sign assures that you are pushing the value of 0 and not whatever is at address 0) and name, which if we remember our data section is actually representing our string /bin/sh. The reason we put the 0 before name is because memory grows upwards and the stack down, so the 0 actually follows the /bin/sh string and not the other way around (this is bloody confusing if you are new to this, but honestly not very important at the moment because we are going to have to toss out the .data section later anyway). Now look one line ahead at movq %rsp, %rdi. This is simply moving whatever is in the %rsp register into %rdi. The %rsp pointer is called the stack pointer, and always points to the top (or bottom, whichever way you look at it) of the stack. And as we recall, it contains the string /bin/sh, which is the last thing we pushed on. So we are basically moving /bin/sh trailed by $0 into %rdi.

Next we are filling up our other arguments. As we recall, %rdi is the first argument, but we still need to move the other arguments into place. So we move $59 (remember the $ means literal value, not address) into %rax, the syscall register, and then our nullbytes (or zero's) into the last registers, %rsi and %rdx.

Next syscall executes. This will see that %rax holds 59, which means we want to execute execve. Then it takes our arguments from our other registers in the order of %rdi, %rsi and %rdx. This will look like (/bin/sh, 0, 0) or ("/bin/sh", NULL, NULL) like from our C program above.

Image via

Now here we assemble our program and run it, and as we see it gives us a shell, which is mighty fine.

Unfortunately, this is still useless to us because we want shellcode, not an executable assembly program. And on top of all this, our program still isn't ready because it will contain nullbytes (lame).

More Learning

Anyways, next time we will continue on and first perfect this shellcode and then exploit a program (don't worry, we'll get there eventually). For now, you can try to expand your reading if my explanation wasn't satisfactory. Of course teaching a short hand and sketchy lesson on assembly isn't the best way to learn, so here is some material if you really want to learn assembly intimately:

Programming from the Ground Up by Jonathan Bartlett

You can find PDF's or buy it if you really want. The book teaches basic 32 bit assembly, but once you understand 32 bit assembly you will find that 64 bit assembly is practically identical. I always like to further my reading of subjects, and since one can only write so much in a nullbyte article reading up is definitely helpful. On top all this I will continue writing these articles until we have successfully used our shellcode in exploitation, and if there is demand I will write a series on the basics of assembly and other programming and actually dedicate lots of time to explaining it in a satisfactory fashion instead of a quick overview like the above lesson presented, which focuses more on the shellcoding than the assembly, as this is not meant to teach assembly.


We put the system call number 59 into %rax, and when syscall happens, the kernel takes 59 and brings us the execve function. Then it takes three arguments from %rdi, %rsi and %rdx, in that order. The first argument is the string /bin/sh, and the next two arguments are nullbytes.

Next time the tutorial will focus entirely on the writing of shellcode and won't explain any assembly concepts, because I want it to be dedicated for the slightly more adept programmer who wants to write shellcode void of formalities and basic explanation. So again, I recommend reading up on these concepts if you want to follow along, because it is rewarding. Thanks for reading, I would very greatly appreciate criticism so that I may improve my writing.

Next part: Removing Null-Bytes

Just updated your iPhone? You'll find new features for Podcasts, News, Books, and TV, as well as important security improvements and fresh wallpapers. Find out what's new and changed on your iPhone with the iOS 17.5 update.


Look at that! Assembly is very much important for shellcode.

ye assembly has a 1 to 1 correspondence to machine code

This is awesome. I was in need of a simplified explanation of shellcode building to support the ones from Aleph1 and Erickson.

I suggest that you write the tools you used so that the reader might feel more comfortable (for example, I'm used to Intel syntax, even though all syntax are pretty similar, and I primarily use gdb). It's hard to provide a base of assembly for everyone, but you explained the topic in a way that it only needs you to read some good books (like the one you posted by Jonathan Barlett or "Hacking, The art of exploitation" by Jon Erickson), so kudos for you.

Hope you will continue this, looking forward to the nullbytes part.

Thanks for the write-up, I actually needed a tutorial like this as i was diving into the world of assembly ( I hate the language though ) ...... Very cool, looking forward for more ... +1

# Sergeant

Thanks for the tutorial, it was really well written and understandeble.
Sys call numbers were new to me ^^

What I find confusing is different syntaxes. There is at&t and intel syntax. There is also GAS and NASM. Somehow it doesn't make any sense which syntax belongs to who and what is compatible with what. I'm afraid It might mess with my computer if I use wrong syntax, especially when working with stack pointers.

Share Your Thoughts

  • Hot
  • Latest