Adrian Citu's Blog

Introduction

This is very brief and basic list of steps to follow if you want to write a shellcode under Linux operating system.

1. Craft the shellcode

The first step and by far the most important one is to find a vulnerability and to write the shellcode that’s exploiting the vulnerability. In this tutorial we will write a dummy shellcode represented by the “Hello World” program. The easiest way to write a shellcode is first to write it in the C language and then in order to have a more compact version, to translate it or to rewrite the shellcode in assembler.

1.1 Craft the shellcode in C

The main goal of writing the shellode in C is to have first working version of the exploit without (yet) bothering about the constraints of the shellcode execution (see later the chapter about the validity of the shelcode). In our case, the C version of our dummy shellcode is the following one:

After the compilation (gcc -o hello hello.c) we can take a look at the generated assembly code (objdump -d ./hello -M intel) and we would see that for a very small C program the assembly version is quite long (this is mainly due to the C preprocessor); it’s 228 lines length ( objdump -d ./hello -M intel | wc -l).

Now, we would like to “translate” the C version of our shellcode in the assembly version and the most straightforward way is by finding the system calls that hat are made by the C version of the shellcode. In some cases the system calls are quite obvious (the case of the write function) but sometimes it’s not so easy to guess. The tool that will give to you the system calls is the strace. In our case the strace ./hello will have the following output (the parts that are interesting for us are in bold):

1.2 Craft the shellcode in assembler

Now that we have the system calls it is possible to get some infos like the parameters needed by the each system call (using man) and the system calls numbers (all the system calls names and number are in /usr/include/i386-linux-gnu/asm/unistd_32.h file).

So, the number of the write call is 4 (using cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep write) and the parameters are the following one (using man 2 write):

ssize_t write(int fd, const void *buf, size_t count);

For the exit system call the number is 1 and the call parameter are :

void _exit(int status);

Having all the needed information, we can write the assembler version of our dummy shellcode.

In order to call system calls in assembler, you must fill the tax register with the system call number and fill the register ebx, ecx, edx for every parameter that the system call need.

For example the write have 3 parameters so the tax register will be filled with 0x4 (the system call number), ebx register will contain the file descriptor (1 for sysout), ecx register will contains the address of the string to print, and edx register will contain the length of the string to print (if you don’t have any knowledge of linux assembler you can take a look to this very good introduction Assembly Language and Shellcoding on Linux ):

The above lines are simulating a vulnerable program by overwriting the return address of the main() function with the address of the shellcode, in order to execute the shellcode instructions upon exit from main().

The HEX version of the shellcode can be obtained from the binary file using the objdump utility and a much smarter version of the command can be found on commandlinefu.com

Lets compute the HEX version of our dummy shellcode and then test it with our test program.

The HEX version of our assembler version of the dummy shellcode is the following one: