choppers

[DRAFT] OverTheWire.org: vortex13 (Long Play)

2014-10-09 00:00:00

Intro

Releasing the solution to a wargame challenge seems like cheating, but because geohot released his "speedrun" solution on youtube here, the cat is already out of the bag. His solution is very much a hack, so I've descided write a more complete solution, or a "long play".

Description

Inconveniences

How big is your shellcode? This level has a non-executable stack. You must login to vortex.labs.overthewire.org to complete this level.

Analysis

vortex13@melinda:~$ uname -a
Linux melinda 3.15.4-x86_64-linode45 #1 SMP Mon Jul 7 08:42:36 EDT 2014 x86_64 x86_64 x86_64 GNU/Linux
vortex13@melinda:~$
vortex13@melinda:~$ md5sum /vortex/vortex13
585d3aab6cc29a38037fd8f866452fb5  /vortex/vortex13
vortex13@melinda:~$
vortex13@melinda:~$ file /vortex/vortex13
/vortex/vortex13: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xe95066e05e1a5ab5ae1046c2c8380facf1cf6e13, not stripped
vortex13@melinda:~$
vortex13@melinda:~$ execstack /vortex/vortex13
- /vortex/vortex13
vortex13@melinda:~$
vortex13@melinda:~$ ls -alF /vortex/vortex13
-r-sr-x--- 1 vortex14 vortex13 7503 Jun  6  2013 /vortex/vortex13*
vortex13@melinda:~$ md5sum /vortex/vortex13
585d3aab6cc29a38037fd8f866452fb5  /vortex/vortex13
vortex13@melinda:~$
vortex13@melinda:~$ ldd /vortex/vortex13
        linux-gate.so.1 =>  (0xf7ffd000)
        libc.so.6 => /lib32/libc.so.6 (0xf7e49000)
        /lib/ld-linux.so.2 (0x56555000)
vortex13@melinda:~$ md5sum /lib32/libc.so.6
4ff10721c60bfecbd079993d8bd3e994  /lib32/libc.so.6

vortex13 is a setguid executable. Exploiting it will give you permissions of user vortex14 and you can read the password file /etc/vortex_pass/vortex14.

When vortex13 starts, all command line arguments and environment variables are overwritten with zeros, then the vuln() function is called.

char *allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz"
        "0123456789%.$";

void __cdecl vuln()
{
        signed int i; // [sp+14h] [bp-14h]@3
        char *ptr; // [sp+1Ch] [bp-Ch]@1

        ptr = (char *)malloc(20u);
        if ( !fgets(ptr, 20, stdin) )
                exit(1);
        for ( i = 0; i <= 19; ++i )
        {
                if ( !strchr(allowed, ptr[i]) )
                        exit(1);
        }
        printf(ptr);
        free(ptr);
}

int __cdecl main(int argc, char **argv)
{
        int i; // [sp+18h] [bp-8h]@3
        int k; // [sp+18h] [bp-8h]@9
        int j; // [sp+1Ch] [bp-4h]@4
        int l; // [sp+1Ch] [bp-4h]@10

        if ( argc )
                exit(1);
        for ( i = 0; environ[i]; ++i )
        {
                for ( j = 0; environ[i][j]; ++j )
                        environ[i][j] = 0;
        }
        for ( k = 0; argv[k]; ++k )
        {
                for ( l = 0; argv[k][l]; ++l )
                        argv[k][l] = 0;
        }
        vuln();
        return 0;
}

Exploit Mitigations

The only mitigation in place is the non-executable stack. ASLR is disabled, and there are no stack canaries.

Vulnerablity

In the vuln() function, the string that is read in from stdin is sent as the format argument of printf. This is a obvious string format bug.

Exploit

By using the "%n" format specifier, an attacker can overwrite arbitrary memory, but for this technique to work you need a dword aligned pointer to the memory address you want to overwrite on the stack. Because the buffer is on the heap, we can't supply the address in our format string, as is done in normal stack-based format string exploits. Because the command line arguments and environment variables are wiped out before vuln() runs, the only way to get values on the stack is changing the filename of the binary. In the implementation of execve() in the Linux kernel, the filename argument is copied to the stack of the new process space.

execve() implementation in the Linux kernel (linux/fs/exec.c)

/*
 * sys_execve() executes a new program.
 */
static int do_execve_common(struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp)
{
    ...

    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval < 0)
        goto out;

    ...
}

Now that we can control some information on the stack, we need to know the distance in dwords from the first argument of printf to our addess. This value can be found by using a debugger, but because this paper is a long play, I need an automated method to measure this distance during run time. I want a solutions that will still work even if the system administrator updates libc or the dynamic linker that throws off the position the stack frames I'm going to be touching.

Because ASLR is disabled, we can at least count on the filename string ending at the same memory address for any specific kernel version. On a 32-bit Linux kernel, this address is found a few bytes before kernels space, 0xC0000000. On a 64-bit kernel running in compatibility mode, this address is around 0xFFFFE000.

To find the other end of the distance, we need to leak some information from the running process. By using "%10$08x" as the input string to vortex13, we can leak the value of EBP during main(). It is stored on the stack during the procedure prelude of vuln() (0x08048544). Now that we have both values, we can accuratly calculate the distance required for our format strings during runtime. The calculation can be found in the get_format_index_from_stack_addr() function of my exploit code.

Now that we can write values to arbitrary addresses in vortex13, the next problem is that we have a 19 character limit on our format strings. This isn't enough to make two passes to overwrite a full DWORD in vortex. We need a way to loop vuln() and we can only do it by overwriting a single WORD.

When a new process is loaded, the dynamic linker doesn't automatically bind all foreign symbols. By default, ld-linux delays the bindings until they are needed. This is called "Lazy Binding" (more this can be found on page 213 of Linkers And Loaders by John R. Levine). So, before free() is called in vortex13, the value of free() in the got is 0x08048426. We only need to overwrite the lower WORD, and we can jump somwhere vortex13's image in memory.

Now, we need to find an address to jump to inside of vuln(). Let's look at vuln() again. IDA Pro's F5 of vuln() is a little missleading. Here is a more accurate reconstruction of the original C code.

void __cdecl vuln()
{
        signed int i; // [sp+14h] [bp-14h]@3
    int n; // [bp - 10h]
        char *ptr; // [sp+1Ch] [bp-Ch]@1

    n = 20;
        ptr = (char *)malloc(n);
        if ( !fgets(ptr, n, stdin) )
                exit(1);
        for ( i = 0; i <= 19; ++i )
        {
                if ( !strchr(allowed, ptr[i]) )
                        exit(1);
        }
        printf(ptr);
        free(ptr);
}

The main difference is the extra local stack variable n. If we jump right after the initilization of n, we can get will be able to suply multiple format strings to vuln() and set us up nicely for our next move.

After patching free() in the got, vuln will look more like this:

void __cdecl vuln()
{
        signed int i; // [sp+14h] [bp-14h]@3
    int n; // [bp - 10h]
        char *ptr; // [sp+1Ch] [bp-Ch]@1

    n = 20;
loop:
        ptr = (char *)malloc(n);
        if ( !fgets(ptr, n, stdin) )
                exit(1);
        for ( i = 0; i <= 19; ++i )
        {
                if ( !strchr(allowed, ptr[i]) )
                        exit(1);
        }
        printf(ptr);
        asm("call loop");
}

Our next obstical is the 19 character limit. We have already leaked an address off of the stack. We can use that value to calculate the address of n on the stack and change it to a larger value.

void __cdecl vuln()
{
        signed int i; // [sp+14h] [bp-14h]@3
    int n; // [bp - 10h]
        char *ptr; // [sp+1Ch] [bp-Ch]@1

    n = 20;
loop:
        ptr = (char *)malloc(1024);
        if ( !fgets(ptr, 1024, stdin) )
                exit(1);
        for ( i = 0; i <= 19; ++i )
        {
                if ( !strchr(allowed, ptr[i]) )
                        exit(1);
        }
        printf(ptr);
        asm("call loop");
}
void __cdecl vuln()
{
        signed int i; // [sp+14h] [bp-14h]@3
    int n; // [bp - 10h]
        char *ptr; // [sp+1Ch] [bp-Ch]@1

    n = 20;
loop:
        ptr = (char *)malloc(1024);
        if ( !fgets(ptr, 1024, stdin) )
                exit(1);
        for ( i = 0; i <= 19; ++i )
        {
                if (!1) )
                        exit(1);
        }
        printf(ptr);
        asm("call loop");
}
void __cdecl vuln()
{
        signed int i; // [sp+14h] [bp-14h]@3
    int n; // [bp - 10h]
        char *ptr; // [sp+1Ch] [bp-Ch]@1

    n = 20;
loop:
        ptr = (char *)malloc(1024);
        if ( !fgets(ptr, 1024, stdin) )
                exit(1);
        for ( i = 0; i <= 19; ++i )
        {
                if (!1) )
                        exit(1);
        }
        system(ptr);
        asm("call loop");
}