Bugtraq mailing list archives

Re: Format String Attacks


From: Dan Harkless <dan-bugtraq () DILVISH SPEED NET>
Date: Wed, 13 Sep 2000 23:48:38 -0700

Doug Hughes <Doug.Hughes () ENG AUBURN EDU> writes:
Since I don't recall anybody else posting one, here is a simple, generic,
setuid wrapper that people could use around, for instance, /usr/bin/eject
or other setuid programs.

Yes, too simple!  It's got a massive, gaping security hole!

/*
 * This program provided AS IS with no warranty
 * Copyright 2000, doug () eng auburn edu
 * Use freely.
 * The environment from the original program is completely obliviated
 */
#include <stdio.h>
#include <stdlib.h>


main (int argc, char *argv[]) {

      char *origfile;
      char *envp[1] = { (char *) NULL };

      if ((origfile = (char *) malloc(strlen(argv[0])+6)) == NULL) {
              perror("allocating memory");
              exit(1);
      }
      strcpy(origfile, argv[0]);
      strcat(origfile, ".orig");

      execve(origfile, argv, envp);
}

This would work fine if users were forced to always run programs using a
leading path.  However, that's not the case.  A user can simply say, for
example, "eject".  They can tweak their $PATH so that they have a program
called eject.orig (perhaps a copy of /bin/sh) earlier in their $PATH than
/usr/bin/eject.orig, and now you'll execute their arbitrary program with
root privileges.

I started to write a wrapper similar to yours (where you could just compile
it once and then make one copy of it per wrapped program), but it requires
you to re-implement $PATH checking as done by the shell.  If you are called
without a leading path, you need to go through the $PATH and figure out
which directory the shell plucked you out of.  It's tricky make this
cross-platform, because different UNIXes have different rules as to how many
groups one can be in simultaneously, and which system call you make to get
those groups.  If you make one mistake in emulating the shell's $PATH
checking you can be back to square one, so I decided to take a different
approach.

Instead of making a single, complex wrapper program, I decided to go with a
separate, simple one for each program to be wrapped.  Each wrapper has the
path of the wrapped program hardcoded into it, so there's no way users can
abuse the wrapper (assuming you don't wrap programs in user-writable
directories).

I put all the smarts in a Perl script (appearing below) that trundles
through the system looking for setid programs and replacing them with
compiled-on-the-fly wrappers.  For each one it finds, it asks you if you
want to wrap it.  You should only say yes in directories where users don't
have write-access.  If you have setuid root executables in /tmp, for
instance, do *not* have the script wrap those, or the users may abuse the
wrappers and get root.

I have run this script on our Solaris 2.6 systems without any apparent ill
effect.  If anyone sees any security problems with this script, please let
me know.  Pretty sure it's kosher, though.


#!/usr/local/bin/perl
#
# wrap_setid_progs_with_envar_clearer
#
# AUTHOR:
#   Dan Harkless <software () harkless org>       http://harkless.org/dan/software/
#
# COPYRIGHT:
#   This file is Copyright (C) 2000 by Dan Harkless, and is released under the
#   GNU General Public License <http://www.gnu.org/copyleft/gpl.html>.
#
# USAGE:
#   % wrap_setid_progs_with_envar_clearer <compiler+opts> <envar1> [<envar2>...]
#
# EXAMPLE:
#   % wrap_setid_progs_with_envar_clearer 'gcc -O3 -Wall' NLSPATH
#
# DESCRIPTION:
#   A security hole was discovered on most UNIXes that allows getting root
#   access by using a custom $NLSPATH and message files in conjunction with a
#   setuid executable.  The hole is another of the now-legendary "format string
#   bugs".  According to Sun, the POSIX standard itself is broken in this
#   respect.  Damn, why did the designers of printf() have to get so fancy?
#
#   In any case, while the Linux vendors had patches out right away (in fact
#   there was some controversy about them releasing patches despite a request
#   not to until all vendors had it fixed), Sun has been dragging their feet
#   saying they don't want to release a patch that's going to break POSIX
#   compliance (even though they themselves say the standard is broken).
#
#   In the meantime, all Solaris machines are extremely vulnerable.  A
#   workaround is to replace all system set[gu]id executables with wrapper
#   programs that clear the $NLSPATH environment variable then execute the real
#   program.
#
#   My first stab at that workaround was to write a single wrapper program that
#   would decide what the name of the wrapped program was by looking at its
#   argv[0].  However, this gets tricky because you can't just execute
#   argv[0].<extension>, because the user can trick you into running a file with
#   that name out of one of their $PATH directories, again giving them root.
#   Therefore, if this wrapper program is called without a leading path being
#   specified, it needs to emulate the shell and figure out what $PATH directory
#   it was found in, and only run the wrapped program in *that* directory.  This
#   is tricky to make cross-platform, since different UNIXes have different
#   rules about how many groups you can be in at a time and what system calls
#   you use to get those groups.
#
#   As my all-purpose wrapper got more and more complex, I came to the
#   conclusion that it was better to make the wrapper simple (with the path of
#   the wrapped program hardcoded into the executable), and put the smarts in
#   the script that goes out and does all the wrapping.  That is this script.
#
#   When you run it (you must specify the compiler to use and the environment
#   variable(s) to clear, as shown above), it will start in the '/' directory
#   and recurse down looking for setid programs.  When it finds one, it'll ask
#   you if it's a system program that should be wrapped.  Please look at the
#   path of the file before answering 'y'.  You should *not* wrap setid programs
#   in user directories (including /tmp).  If for some reason you have, say, a
#   setuid root executable in a user directory, the user will be able to delete
#   that executable afterwards and replace it with an arbitrary program that
#   will be given root privileges by the installed wrapper.  Setid programs in
#   user directories that are just setuid user (or setgid user group) are a
#   little more arguable -- technically, they should be wrapped, as otherwise,
#   users can become each other with the exploit.  However, what if you wrap a
#   user program that depends on $NLSPATH being settable?  Also, even if you
#   wrap a user program, they may unwrap it.  Another potential danger of
#   running this script on a program in a user-writable directory is that the
#   user might make a symlink in that directory to a system file, and this
#   script (since it must be run with root privileges) could traverse the
#   symlink and wipe out the important file.
#
#   Note that this script does not simply rename <file> to <file>.<extension>
#   then compile the wrapper to be <file>.  Instead, the wrapper is compiled to
#   <file>.<wrapper_extension> and <file> is renamed to
#   <file>.<wrapped_extension>, then a symbolic link is made from <file> to
#   <file>.<wrapped_extension>.  This is so that if <file> is wiped out by a
#   system patch, it'll be easy to recover (if that patching traverses the link
#   and writes the new version in-place, though, we're out of luck).
#
# DATE        MODIFICATION
# ==========  ==================================================================
# 2000-09-13  Original.


## Modules used ################################################################
use diagnostics;     # turn on -w and output verbose versions of warnings
use English;         # allow long English names like $PROGRAM_NAME instead of $0
use File::Basename;  # for basename()
use File::Find;      # for find()
use FileHandle;      # for FileHandle::new, etc.


## Constants ###################################################################
$WRAPPED_EXTENSION = ".wrapped_due_to_envar_security_hole";
$WRAPPER_EXTENSION = ".wrapper_due_to_envar_security_hole";


## Subroutines #################################################################
sub my_die {
    # die() output is annoyingly verbose when diagnostics are turned on.
    print "\n$our_program_name: @ARG.  Aborting.\n";
    exit 1;
}


sub output_source {
    my $absolute_path_of_wrapped_program = shift;
    my $output_filehandle = shift;

    # Output the header of the C source file.
    print $output_filehandle <<HERE_DOCUMENT_EOF_1;
#include <errno.h>   /* for errno */
#include <stdio.h>   /* for fprintf(), etc. */
#include <stdlib.h>  /* for putenv(), etc. */
#include <string.h>  /* for strerror() */
#include <unistd.h>  /* for execv() */


#define ABSOLUTE_PATH_OF_WRAPPED_PROGRAM \\
        "$absolute_path_of_wrapped_program$WRAPPED_EXTENSION"


char*  our_program_name_global;


void  die(char*  failed_syscall) {
  fprintf(stderr, "%s (wrapping " ABSOLUTE_PATH_OF_WRAPPED_PROGRAM
          "): %s() failed: %s.  Aborting.\\n",
          our_program_name_global, failed_syscall, strerror(errno));

  exit(EXIT_FAILURE);
}


int  main(int  argc, char**  argv) {
  our_program_name_global = argv[0];

HERE_DOCUMENT_EOF_1


    # Output the middle of the C source file -- clearing code for each
    # environment variable specified on our commandline.
    foreach $envar (@envars_to_clear) {
        print $output_filehandle <<HERE_DOCUMENT_EOF_2;
  if (putenv("$envar=") != 0)
    die("putenv");

HERE_DOCUMENT_EOF_2
    }


    # Output the trailer of the C source file.
    print $output_filehandle <<HERE_DOCUMENT_EOF_3
  if (execv(ABSOLUTE_PATH_OF_WRAPPED_PROGRAM, argv) != 0)
    die("execv");

  return EXIT_FAILURE;  /* just here to quiet compiler warning */
}
HERE_DOCUMENT_EOF_3
}


sub wanted {
    if (not -l $ARG and -f $ARG and
        (-u $ARG or -g $ARG)) {
        print "========================================";
        print "========================================\n";
        print "$File::Find::name\n";
        print "========================================";
        print "========================================\n";
        print "\n";

        # This ls call (and the one down below) won't output the group on
        # systems (BSDish ones?)  that require a -g to do that.  Best thing to
        # do would be to move the stat() call up here and then mimic ls output
        # ourselves (no doubt there's a CPAN module that does that).
        print "<perl> ls -lF $File::Find::name\n";
        system("ls", "-lF", $File::Find::name) == 0
          or my_die "ls failed";
        print "\n";

        print "Is this a system program which should be wrapped (n|y)? ";
        my $answer = <STDIN>;
        print "\n";
        if ($answer =~ /y/i) {
            # Write the wrapper source file.
            my $wrapper_source_filename = "$ARG$WRAPPER_EXTENSION.c";
            print "<perl> cat <<EOF | tee $wrapper_source_filename\n";
            output_source($File::Find::name, \*STDOUT);
            print "EOF\n";
            my $wrapper_source_file
              = new FileHandle "> $wrapper_source_filename";
            if (not defined $wrapper_source_file) {
                my_die "Failed to open $wrapper_source_filename for writing";
            }
            output_source($File::Find::name, \*$wrapper_source_file);
            close $wrapper_source_file;
            print "\n";

            # Compile the wrapper source file, with executable taking the place
            # of the wrapped program.
            my $wrapper_filename = "$ARG$WRAPPER_EXTENSION";
            print "<perl> $compiler $wrapper_source_filename -o"
              . " $wrapper_filename\n";
            my @compiler_and_options = split / /, $compiler;
            system(@compiler_and_options, $wrapper_source_filename, "-o",
                   $wrapper_filename)
              == 0 or my_die "Failed to compile $wrapper_source_filename";
            print "\n";
        
            # Copy ownership from wrapped file to wrapper file.
            my @wrapped_file_stat = stat $ARG;
            if (not @wrapped_file_stat) {
                my_die "Failed to stat($ARG)";
            }
            my $wrapped_file_mode = $wrapped_file_stat[2] & 07777;
            my $wrapped_file_uid = $wrapped_file_stat[4];
            my $wrapped_file_gid = $wrapped_file_stat[5];
            print "<perl> chown $wrapped_file_uid:$wrapped_file_gid"
              . " $wrapper_filename\n";
            chown($wrapped_file_uid, $wrapped_file_gid, $wrapper_filename)
              or my_die "Failed to chown() $wrapper_filename";
            print "\n";

            # Copy permissions from wrapped file to wrapper file.
            printf("<perl> chmod %o $wrapper_filename\n", $wrapped_file_mode);
            chmod($wrapped_file_mode, $wrapper_filename)
              or my_die "Failed to chmod() $wrapper_filename";
            print "\n";

            # Remove setid permissions from wrapped file.
            my $wrapper_file_mode = $wrapped_file_mode & 071777;
            printf("<perl> chmod %o $ARG\n",
                   $wrapper_file_mode);
            chmod($wrapper_file_mode, $ARG)
              or my_die "Failed to chmod() $ARG";
            print "\n";

            # When the system is patched, our special wrapper program is likely
            # to get wiped out.  To make it easy to recover from this, rename
            # the wrapped file to <file>$WRAPPED_EXTENSION and then put a
            # symlink pointing from <file> to that.
            my $wrapped_filename = "$ARG$WRAPPED_EXTENSION";
            print "<perl> mv $ARG $wrapped_filename\n";
            rename($ARG, $wrapped_filename)
              or my_die "Failed to rename $ARG to $wrapped_filename";
            print "\n";
            print "<perl> ln -s $wrapper_filename $ARG\n";
            symlink($wrapper_filename, $ARG)
              or my_die "Failed to make a symbolic link to $wrapper_filename";
            print "\n";

            # Delete the wrapper source file (would be nice to also do this if
            # we exit abnormally).
            print "<perl> rm $wrapper_source_filename\n";
            unlink "$wrapper_source_filename"
              or my_die "Failed to delete $wrapper_source_filename";
            print "\n";

            # Show what we're left with, to increase confidence that this script
            # is doing the right thing.
            print "<perl> ls -lF $ARG $wrapped_filename $wrapper_filename\n";
            system("ls", "-lF", $ARG, $wrapped_filename, $wrapper_filename) == 0
              or my_die "ls failed";
            print "\n";
        }
        else {
            print "Okay, not wrapping '$ARG'.\n\n";
        }
        print "\n";
    }
}


## Main ########################################################################
$our_program_name = basename($PROGRAM_NAME);

if (scalar @ARGV < 2) {
    print STDERR "Usage: $our_program_name \"compiler[ opts]\" envar1"
      . " [envar2...]\n";
    exit 1;
}

$compiler = shift;

@envars_to_clear = @ARGV;

find(\&wanted, "/");

print "$our_program_name: Finished successfully.\n";


Current thread: