Bugtraq mailing list archives

Re: Format String Attacks


From: Dan Harkless <dan-bugtraq () DILVISH SPEED NET>
Date: Thu, 14 Sep 2000 20:23:10 -0700

Dan Harkless <dan-bugtraq () DILVISH SPEED NET> writes:
[...]
#!/usr/local/bin/perl
#
# wrap_setid_progs_with_envar_clearer
[...]

As I mentioned in my last post (assuming Aleph1 ends up approving it), I
discovered a small problem in the first version of my script.  If you had
multiple setid programs that were hard links to each other
(e.g. /usr/bin/{uptime,w} on Solaris 2.6), only one of them would get
wrapped (though all would get defanged due to the nature of hard links).

This new version of my script fixes this problem and adds two new
commandline options, -n and -u.  -n is like make -n or stow -n, and causes
the script to merely preview the actions it would take rather than taking
them.  -u causes the script to unwrap all the wrapped programs (restoring
them to their original state).

-u will be useful once Sun has patched this issue in the libraries.  Also,
if you ran the first version of my script on your system and are concerned
that some hard-linked setid executables might not have been wrapped, you can
undo what the first version did with -u and then re-run.

The new script appears below my .sig.  Again, please let me know if you spot
any security holes or other problems.

----------------------------------------------------------------------
Dan Harkless                   | To prevent SPAM contamination, please
dan-bugtraq () dilvish speed net  | do not mention this private email
SpeedGate Communications, Inc. | address in Usenet posts.  Thank you.




#!/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 [-n] (-u|<cc+opts> <env1> [<env2>...])
#
# EXAMPLES:
#   % wrap_setid_progs_with_envar_clearer 'gcc -O3 -Wall' NLSPATH
#   % wrap_setid_progs_with_envar_clearer -n -u | more
#
# 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).
#
#   The commands that are output after "<perl> " are just pseudo-commands -- for
#   instance, we don't actually execute "cat <<EOF | tee <wrapper_source.c>",
#   but that's a conveniently terse way to explain what we're doing.  The
#   commands that appear after "<system> " are really executed using system().
#   Note that for security reasons, we call system() in such a way as to not
#   have to go through /bin/sh.
#
#   One more thing to note is that if you run this script on a system more than
#   once, the second time through it'll be asking you if you want to wrap the
#   wrappers.  There are no special provisions to recognize and skip them.  Just
#   answer "n[o]" when asked if you want to wrap them.
#
#   If you want to unwrap all the wrapped programs (e.g. after your OS vendor
#   has released a patch that makes the wrappers unnecessary), you can use the
#   -u option.  Naturally, when using -u, there's no need to specify compiler or
#   environment variables.
#
#   If you want to see what actions this script would take without actually
#   taking them, you can use the -n option (like make -n or stow -n).  -n can be
#   used in conjunction with -u, but they each need their own '-' -- no '-nu's
#   or '-un's, please.
#
# DATE        MODIFICATION
# ==========  ==================================================================
# 2000-09-14  Added -u option -- unwraps wrapped programs.
# 2000-09-14  Added -n option (like make -n or stow -n) for previewing actions.
# 2000-09-14  Doing the find live means that if there are setid programs that
#             are hard links to each other (like /usr/bin/{uptime,w} on Solaris
#             2.6), only one of them will get wrapped (since the inode won't be
#             setid any more after the first link is processed).  Need to do the
#             find at the beginning and remember all the modes.
# 2000-09-14  Say "<system>" rather than "<perl>" where appropriate.
# 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() and fileparse()
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";


## Subroutine prototypes #######################################################
sub ARG_is_setid_program;
sub my_die;
sub output_source;
sub wrap_file;


## Subroutine definitions ######################################################
sub ARG_is_setid_program {
    if (not -l $ARG and -f $ARG and
        (-u $ARG or -g $ARG)) {
        if ($unwrap_mode and not $ARG =~ /$WRAPPER_EXTENSION$/) {
            return;
        }

        # A real file that's setid.  stat() it now and remember the info for
        # later so that we'll be able to properly handle multiple setid programs
        # that are symlinks to each other.
        my @setid_file_stat = stat(_);  # _ means to use the stat from filetests

        if (not @setid_file_stat) {
            my_die "Failed to stat($ARG)";
        }

        # Remember the mode, uid, and gid.
        $setid_files{$File::Find::name} = [$setid_file_stat[2] & 07777,
                                           $setid_file_stat[4],
                                           $setid_file_stat[5]];

        print ".";  # print status dots so it doesn't look like we're frozen

        # We could do the asking of users if given files should be considered
        # wrappable system programs here, but there are several advantages to
        # waiting until the second stage, including the fact that that way we
        # can go through the files in alphabetical order, where the grouping can
        # make it easier to answer the question quickly.
    }
}


sub my_die {
    # die() output is annoyingly verbose when diagnostics are turned on.
    print STDERR "\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 unwrap_file {
    my $wrapper_file_abs_path = shift;

    print "========================================";
    print "========================================\n";
    print "$wrapper_file_abs_path\n";
    print "========================================";
    print "========================================\n";
    print "\n";

    my $filename;
    my $wrapper_file_dir;
    my $wrapper_filename_extension;

    ($filename, $wrapper_file_dir, $wrapper_filename_extension)
      = fileparse($wrapper_file_abs_path, $WRAPPER_EXTENSION);

    my $wrapped_filename = "$filename$WRAPPED_EXTENSION";
    my $wrapper_filename = "$filename$wrapper_filename_extension";

    if (not chdir $wrapper_file_dir) {
        my_die "Failed to chdir to $wrapper_file_dir";
    }

    # 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 use full stat() info (either cached earlier or re-checked now) and
    # mimic ls output ourselves (no doubt there's a CPAN module that does that).
    print "<system> ls -lF $filename $wrapped_filename $wrapper_filename\n";
    system("ls", "-lF", $filename, $wrapped_filename, $wrapper_filename)
      == 0 or my_die "ls failed";
    print "\n";

    # Copy permissions from wrapper file to wrapped file.
    my $wrapper_file_mode = $setid_files{$wrapper_file_abs_path}[0];
    printf("<perl> chmod %o $wrapped_filename\n", $wrapper_file_mode);
    if (not $preview_mode) {
        chmod($wrapper_file_mode, $wrapped_filename)
          or my_die "Failed to chmod() $wrapped_filename";
    }
    print "\n";
        
    # Delete the symlink.
    print "<perl> rm $filename\n";
    if (not $preview_mode) {
        unlink "$filename"
          or my_die "Failed to delete $filename";
    }
    print "\n";
        
    # Delete the wrapper file.
    print "<perl> rm $wrapper_filename\n";
    if (not $preview_mode) {
        unlink "$wrapper_filename"
          or my_die "Failed to delete $wrapper_filename";
    }
    print "\n";
        
    # Rename the wrapped file to the original filename.
    print "<perl> mv $wrapped_filename $filename\n";
    if (not $preview_mode) {
        rename($wrapped_filename, $filename)
          or my_die "Failed to rename $wrapped_filename to $filename";
    }
    print "\n";
        
    if (not $preview_mode) {
        # Show what we're left with, to increase confidence that this script is
        # doing the right thing.  It'd be more meaningful to ls $filename* here,
        # but I'm not 100% positive the wildcarding is safe.
        print "<system> ls -lF $filename\n";
        system("ls", "-lF", $filename) == 0
          or my_die "ls failed";
        print "\n";
    }

    print "\n";
}


sub wrap_file {
    my $file_abs_path = shift;

    print "========================================";
    print "========================================\n";
    print "$file_abs_path\n";
    print "========================================";
    print "========================================\n";
    print "\n";

    my $filename;
    my $file_dir;

    ($filename, $file_dir) = fileparse($file_abs_path);

    if (not chdir $file_dir) {
        my_die "Failed to chdir to $file_dir";
    }

    # 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 use full stat() info (either cached earlier or re-checked now) and
    # mimic ls output ourselves (no doubt there's a CPAN module that does that).
    print "<system> ls -lF $file_abs_path\n";
    system("ls", "-lF", $file_abs_path) == 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 = "$filename$WRAPPER_EXTENSION.c";
        print "<perl> cat <<EOF | tee $wrapper_source_filename\n";
        output_source($file_abs_path, \*STDOUT);
        print "EOF\n";
        if (not $preview_mode) {
            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_abs_path, \*$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 = "$filename$WRAPPER_EXTENSION";
        print "<system> $compiler $wrapper_source_filename -o"
          . " $wrapper_filename\n";
        my @compiler_and_options = split / /, $compiler;
        if (not $preview_mode) {
            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_mode = $setid_files{$file_abs_path}[0];
        my $wrapped_file_uid = $setid_files{$file_abs_path}[1];
        my $wrapped_file_gid = $setid_files{$file_abs_path}[2];
        print "<perl> chown $wrapped_file_uid:$wrapped_file_gid"
          . " $wrapper_filename\n";
        if (not $preview_mode) {
            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);
        if (not $preview_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 $filename\n",
               $wrapper_file_mode);
        if (not $preview_mode) {
            chmod($wrapper_file_mode, $filename)
              or my_die "Failed to chmod() $filename";
        }
        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 = "$filename$WRAPPED_EXTENSION";
        print "<perl> mv $filename $wrapped_filename\n";
        if (not $preview_mode) {
            rename($filename, $wrapped_filename)
              or my_die "Failed to rename $filename to $wrapped_filename";
        }
        print "\n";
        print "<perl> ln -s $wrapper_filename $filename\n";
        if (not $preview_mode) {
            symlink($wrapper_filename, $filename)
              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";
        if (not $preview_mode) {
            unlink "$wrapper_source_filename"
              or my_die "Failed to delete $wrapper_source_filename";
        }
        print "\n";
        
        if (not $preview_mode) {
            # Show what we're left with, to increase confidence that this script
            # is doing the right thing.
            print
              "<system> ls -lF $filename $wrapped_filename $wrapper_filename\n";
            system("ls", "-lF", $filename, $wrapped_filename, $wrapper_filename)
              == 0 or my_die "ls failed";
            print "\n";
        }
    }
    else {
        print "Okay, not wrapping '$filename'.\n\n";
    }
    print "\n";
}


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

$usage_error = "Usage:\n"
  . "$our_program_name [-n] (-u | \"compiler[ opts]\" env1 [env2...])\n";

$preview_mode = 0;
$unwrap_mode = 0;

# Could enable -nu / -un by using Getopt::Std, but error checking is messier
# with it.
while (defined $ARGV[0] and $ARGV[0] =~ /^-/) {
    $option = shift;

    if ($option eq "-n") {
        $preview_mode = 1;
    }
    elsif ($option eq "-u") {
        $unwrap_mode = 1;
    }
    else {
        print STDERR $usage_error;
        exit 1;
    }
}

if ($unwrap_mode) {
    $progtype = "wrapper";
}
else {
    # If -u not specified, compiler and environment variable(s) must be.
    if (scalar @ARGV < 2) {
        print STDERR $usage_error;
        exit 1;
    }

    $compiler = shift;

    @envars_to_clear = @ARGV;

    $progtype = "setid";
}

print "$our_program_name: Finding all $progtype programs.  Please wait.\n\n";

$OUTPUT_AUTOFLUSH = 1;  # so the progress dots will come out right away

find(\&ARG_is_setid_program, "/");

print "\n\n";

foreach $file (sort keys %setid_files) {
    if ($unwrap_mode) {
        unwrap_file $file;
    }
    else {
        wrap_file $file;
    }
}

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


Current thread: