Developing custom shellcode in x64 using pure assembly

Abdul wajed Nawazish
7 min readFeb 25, 2020

By now we know that most of the shellcodes generated by metasploit can be detected by any antivirus.

So, lets learn, how to develop our own shellcode with whatever we wish the shellcode to do.

Before we start developing our shellcode we need to learn coding in basic assembly as we will be using assembly instructions to create our shellcode.

Basics of Assembly?

A computer CPU only understand one language, which is binary (0 and 1). Since it is really hard for us to code in binary. We can use one of the higher human understandable languages. Below is a list of programming languages from low to high:

  1. Machine Language
  2. Assembly
  3. C
  4. Python, Java and etc.

In this article, we will code in pure assembly and then use each instructions’s Op-Code in our shellcode.

Assembly consist of registers, below are the list of registers in a x64 bit architecture:

These instructions have been designed to perform specific tasks, but you can use it for different purposes as well. For example, if you wish to print something on the screen, you store system call in RAX, you then store the data in RSI, store the length of your data in RDX and call the kernel to execute your instruction. But you can use RAX for moving data around the memory as well. For more information about each instruction, you can read this article: https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture

Print “Hello” in assembly (NASM), print.asm:

SECTION .data
SECTION .text
global main
main:
mov rax, 1
mov rsi, 0x6f6c6c6548 ; "Hello" is stored in reverse order "olleH"
push rsi
mov rsi, rsp
mov rdx, 5
syscall
mov rax, 60
syscall

Explanation:

In above code SECTION .data is where you store the data or define static variables. But since we are in need of op-codes, we cannot define a variable because variables do not have op-codes. So, we need to push the data directly to register. The SECTION .text is the section where we put our code at. We also have other sections such as SECTION .bss which is used for variables that are not initialized and are used to store user’s input data and etc.

Moving value 1 to RAX or mov rax, 1 tells the system that we want to write something. Note: these values are changeable by anyone. You can find all the system calls for x64 bit at unistd_64.h or /usr/include/x86_64-linux-gnu/asm/unistd_64.h.

We then store our string we want to write to screen at RSI. The architectures that are using little-endian, the data are stored in reverse order e.g 1234 is stored as 4321, hence in our code above 0x6f6c6c6548 which is the hex representation for “olleH” in ASCII table is reversed:

We then push our string to stack and move the location of top-of-stack which is RSP to RSI. Precisely Said, RSI hold the location of our string “Hello” in the stack as shown in below figure.

and since our string is 5 byte in length, we move 5 to RDX and call the system to execute our code by usingsyscall. Lastly mov rax, 60 is system call for exit.

Now that we have our code ready, we can use nasm and gcc to compile our code. We first use nasm which shall generate us an object file as following nasm -f elf64 print.asm and then use gcc to get our binary file as follows: gcc -g print.o -o print. If you execute your executable now, you should get a Hello in your screen.

root@kali:/home# nasm -f elf64 print.asm
root@kali:/home# gcc -g print.o -o print
root@kali:/home# ./print
Hello

Now that we know our assembly code works, we need to gather each instruction’s op-code and test it using a c program. There are multiple ways and tools to obtain an executable’s op-code but in this article, we will be using objdump. To get the op-codes of a program using objdump, you can do below:

root@kali:/home# objdump -d ./print0000000000001130 <main>:
1130: b8 01 00 00 00 mov $0x1,%eax
1135: 48 be 48 65 6c 6c 6f movabs $0x6f6c6c6548,%rsi
113c: 00 00 00
113f: 56 push %rsi
1140: 48 89 e6 mov %rsp,%rsi
1143: ba 05 00 00 00 mov $0x5,%edx
1148: 0f 05 syscall
114a: b8 3c 00 00 00 mov $0x3c,%eax
114f: 0f 05 syscall
1151: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
1158: 00 00 00
115b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

Although, you shall see a lot more than above, but you need to find the section where it says <main>. The two digit values in the middle are the op-codes of each instructions in our program. All we need now is to gather these values and test it in a c program, testshellcode.c:

#include <stdio.h>
const char shellcode[]="\xb8\x01\x00\x00\x00\x48\xbe\x48\x65\x6c\x6c\x6f\x00\x00\x00\x56\x48\x89\xe6\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05\x66\x2e\x0f\x1f\x84\x00\x00\x00\x00\x00\x0f\x1f\x44\x00\x00";
int main(){
(*(void (*)()) shellcode)();
return 0;
}

Note: \x before each op-code is to tell the compiler that these are hex values.

To compile our c code, we can do below:

root@kali:/home# gcc -g -z execstack -fno-stack-protector -o testshellcode testshellcode.c

In above -z execstack makes our stack executable since the new architectures store code and data in two separate locations and -fno-stack-protector disables stack protections for our program.

root@kali:/home# ./testshellcode 
Hello

Awesome! no? our shellcode works perfectly and prints us “Hello”.

Example 2:

Now that we know the basics of assembly and shellcode, we can take it a step further and try to execute a system command.

The function for executing system command is execve which have the system call number 59. This function takes three arguments as following execve(filename,arguments,NULL).

  1. Filename or the binary that you wish to execute e.g /bin/ls
  2. Arguments e.g /bin/ls ./
  3. Null

In memory, each argument is separated by 8 null bytes, for example, if you want to execute ping google.com -c 100. In memory, it shall be stored as null_bytes + ping + null_bytes + google.com + null_bytes + -c + null_bytes + 100. Coming back to assembly, filename will be stored at RDI and ping + google.com + null_byte + -c + null_byte + 100 will be stored at RSI.

Execute.asm:

SECTION .data
SECTION .text
global main
main:
xor rax, rax
xor rdx, rdx

push rdx
mov rcx, 0x736c2f2f6e69622f ; push "/bin//ls" to stack
push rcx
mov rdi, rsp
mov rcx, 0x2f2e ; push "./" to stack
push rcx
mov rsi, rsp

push rax ; push 8 null bytes
push rsi ; push "./" location to stack
push rdi ; push "/bin//ls" location to stack
mov rsi, rsp
mov rax, 59
syscall
mov rax, 60
syscall

In above example, we are basically executing /bin//ls with argument ./ to list us our current working directory.

Explanation:

Xoring any value by itself results to 0 or NULL. So, xor rax, rax and xor rdx, rdx cleans up our RAX and RDX registers. We then start aligning our arguments as discussed above and push each one-by-one to stack. We first push a null byte to our stack with push rdx then mov rcx, 0x736c2f2f6e69622f pushes filename or “sl//nib/” which is reverse for “/bin//ls” to stack. Now, we move address location of executing filename to RDI. Part one completed!

Part two is to push our second argument which is ./ to stack. That is is being achieved by mov rcx, 0x2f2e , push rcx and then mov rsi, rsp moves the address location of filename from stack to RSI.

mov rax, 59 tells the system that we want to call execve function.

Okay, our code is ready to be cooked:

root@kali:/home# nasm -f elf64 -g system.asm
root@kali:/home# gcc -g system.o -o system
root@kali:/home# ./system
null print.asm system system.o testshellcode.c
print print.o system.asm testshellcode

Great, it worked :)

We once again can use objdump to get the op-codes for our instructions as below:

root@kali:/home# objdump -d ./system
0000000000001130 <main>:
1130: 48 31 c0 xor %rax,%rax
1133: 48 31 d2 xor %rdx,%rdx
1136: 52 push %rdx
1137: 48 b9 2f 62 69 6e 2f movabs $0x736c2f2f6e69622f,%rcx
113e: 2f 6c 73
1141: 51 push %rcx
1142: 48 89 e7 mov %rsp,%rdi
1145: b9 2e 2f 00 00 mov $0x2f2e,%ecx
114a: 51 push %rcx
114b: 48 89 e6 mov %rsp,%rsi
114e: 50 push %rax
114f: 56 push %rsi
1150: 57 push %rdi
1151: 48 89 e6 mov %rsp,%rsi
1154: b8 3b 00 00 00 mov $0x3b,%eax
1159: 0f 05 syscall
115b: b8 3c 00 00 00 mov $0x3c,%eax
1160: 0f 05 syscall
1162: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
1169: 00 00 00
116c: 0f 1f 40 00 nopl 0x0(%rax)

Please note: the unnecessary parts of the output is not being shown.

We start by collecting our op-codes from left-top to right-bottom and put it into a c program buffer for testing:

testshellcode.c:

#include <stdio.h>const char shellcode[]="\x48\x31\xc0\x48\x31\xd2\x52\x48\xb9\x2f\x62\x69\x6e\x2f\x2f\x6c\x73\x51\x48\x89\xe7\xb9\x2e\x2f\x00\x00\x51\x48\x89\xe6\x50\x56\x57\x48\x89\xe6\xb8\x3b\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05\x66\x2e\x0f\x1f\x84\x00\x00\x00\x00\x00\x0f\x1f\x40\x00";int main(){
(*(void (*)()) shellcode)();
return 0;
}

And then cook our shellcode as below:

root@kali:/home# gcc -g -z execstack -fno-stack-protector -o testshellcode testshellcode.croot@kali:/home# ./testshellcode
null null1 print print.asm print.o system system.asm system.o testshellcode testshellcode.c

--

--