#!/usr/bin/perl # # wrap_setid_progs_with_envar_clearer # # AUTHOR: # Dan Harkless # # COPYRIGHT: # This file is Copyright (C) 2008 by Dan Harkless, and is released under the # GNU General Public License . # # USAGE: # % wrap_setid_progs_with_envar_clearer [-d dir] [-n] compiler env1[ env2...] # % wrap_setid_progs_with_envar_clearer [-d dir] [-n] -u # # EXAMPLES: # % wrap_setid_progs_with_envar_clearer "gcc -O3 -Wall" NLSPATH # % wrap_setid_progs_with_envar_clearer -d /usr/local/stow/lsof-4.45 -n -u # # 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]., 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 # specified by -d (or '/' by default) 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 to . # then compile the wrapper to be . Instead, the wrapper is compiled to # . and is renamed to # ., then a symbolic link is made from to # .. This is so that if 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 -- see below # for more discussion of system patches). # # The commands that are output after " " are just pseudo-commands -- for # instance, we don't actually execute "cat <", # but that's a conveniently terse way to explain what we're doing. The # commands that appear after " " 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. Alternatively, use -d on # successive runs to only look in specific directories where you know new # unwrapped system setid programs to lie. # # 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. # # It's a good idea to unwrap all programs just prior to applying any vendor # patches. Otherwise, the patching process might balk when the file it's # expecting to see is actually just a symlink to some other file. Also, if # your OS's patching routine backs up the previous version, it may be backing # up a useless symlink. If your OS is one like Solaris, the symlink # will simply be replaced with a new executable, which will be sitting next to # .. This is fine if you catch this soon after the # patching (e.g. with a system integrity management tool like Tripwire), but # you may get into trouble if you don't fix this soon, because next time you # run this script in unwrap mode, you'll overwrite the newly-patched version # with the old wrapped version. Best to lock out logins by all users (due to # the security hole you'll be temporarily re-opening), then unwrap all # programs, then apply patches, then rewrap all system executables. # # 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). # # DATE MODIFICATION # ========== ================================================================== # 2008-09-02 "use English qw(-no_match_vars)": avoid regex performance penalty. # 2006-04-17 Variables that may contain regexp metacharacters need to be # surrounded by \Q and \E when appearing in regexps. # 2001-01-24 Print `file' command output on files so in case there's a data # file that's setid for some reason, the user will know to not wrap. # 2001-01-22 Added advice above to unwrap all programs before applying patches. # 2000-09-29 Added -d so we can start at a directory other than '/'. # 2000-09-15 Need to ask user whether it's okay to unwrap each file in -u mode, # or a user could trick the script into clobbering another user's # file in a +w +t directory like /tmp by creating a fake wrapper. # 2000-09-15 For paranoia's sake, print errno as an int rather than calling # strerror() and clear $NLSPATH by hand rather than using putenv(). # To do the latter, we use the third parameter to main(), though # POSIX says to use global variable 'environ' instead. Solaris 2.6 # doesn't appear to declare environ in any of the system headers... # 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 "" rather than "" where appropriate. # 2000-09-13 Original. ## Modules used ################################################################ use diagnostics; # turn on -w and output verbose warnings use English qw(-no_match_vars); # allow use of names like @ARG rather than @_ use File::Basename; # for basename() and fileparse() use File::Find; # for find() use FileHandle; # for FileHandle::new, etc. use Getopt::Std; # for getopts() ## 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 (defined $opt_u and not $ARG =~ /\Q$WRAPPER_EXTENSION\E$/) { 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 beginning of the C source file. print $output_filehandle < /* for errno */ #include /* for fprintf(), etc. */ #include /* for EXIT_FAILURE, etc. */ #include /* for execve() */ #define ABSOLUTE_PATH_OF_WRAPPED_PROGRAM \\ "$absolute_path_of_wrapped_program$WRAPPED_EXTENSION" int main(int argc, char** argv, char** envp) { char** envp_ptr = envp; while (envp_ptr != NULL) { char* c; if (*envp_ptr == NULL) break; HERE_DOCUMENT_EOF_1 # Output the middle of the C source file -- clearing code for each # environment variable specified on our commandline. Do the clearing # manually rather than calling putenv(), and do the string comparing # manually rather than calling strncmp(), just to be on the paranoid side. foreach $envar (@envars_to_clear) { my $i; print $output_filehandle "\n"; print $output_filehandle " c = *envp_ptr;\n"; print $output_filehandle "\n"; print $output_filehandle " if (\n"; for ($i = 0; $i < length($envar); $i++) { print $output_filehandle " *c++ == '" . substr($envar, $i, 1) . "' &&\n"; } print $output_filehandle " *c++ == '=')\n"; $i++; print $output_filehandle " *c = '\\0';\n"; } # Output the end of the C source file. print $output_filehandle < ls -lF $filename $wrapped_filename $wrapper_filename\n"; system("ls", "-lF", $filename, $wrapped_filename, $wrapper_filename) == 0 or my_die "ls failed"; print "\n"; print "Is this a system program which should be unwrapped (n|y)? "; my $answer = ; print "\n"; if ($answer =~ /^y/i) { # Copy permissions from wrapper file to wrapped file. my $wrapper_file_mode = $setid_files{$wrapper_file_abs_path}[0]; printf(" chmod %o $wrapped_filename\n", $wrapper_file_mode); if (not defined $opt_n) { chmod($wrapper_file_mode, $wrapped_filename) or my_die "Failed to chmod() $wrapped_filename"; } print "\n"; # Delete the symlink. print " rm $filename\n"; if (not defined $opt_n) { unlink "$filename" or my_die "Failed to delete $filename"; } print "\n"; # Delete the wrapper file. print " rm $wrapper_filename\n"; if (not defined $opt_n) { unlink "$wrapper_filename" or my_die "Failed to delete $wrapper_filename"; } print "\n"; # Rename the wrapped file to the original filename. print " mv $wrapped_filename $filename\n"; if (not defined $opt_n) { rename($wrapped_filename, $filename) or my_die "Failed to rename $wrapped_filename to $filename"; } print "\n"; if (not defined $opt_n) { # 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 " ls -lF $filename\n"; system("ls", "-lF", $filename) == 0 or my_die "ls failed"; print "\n"; } } else { print "Okay, not unwrapping '$filename'.\n\n"; } print "\n"; } sub wrap_file { my $file_abs_path = shift; print "========================================"; print "========================================\n"; system("file", $file_abs_path) == 0 or my_die "file failed"; 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 " 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 = ; print "\n"; if ($answer =~ /^y/i) { # Write the wrapper source file. my $wrapper_source_filename = "$filename$WRAPPER_EXTENSION.c"; print " cat < $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 " $compiler $wrapper_source_filename -o" . " $wrapper_filename\n"; my @compiler_and_options = split / /, $compiler; if (not defined $opt_n) { 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 " chown $wrapped_file_uid:$wrapped_file_gid" . " $wrapper_filename\n"; if (not defined $opt_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(" chmod %o $wrapper_filename\n", $wrapped_file_mode); if (not defined $opt_n) { 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(" chmod %o $filename\n", $wrapper_file_mode); if (not defined $opt_n) { 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 $WRAPPED_EXTENSION and then put a # symlink pointing from to that. my $wrapped_filename = "$filename$WRAPPED_EXTENSION"; print " mv $filename $wrapped_filename\n"; if (not defined $opt_n) { rename($filename, $wrapped_filename) or my_die "Failed to rename $filename to $wrapped_filename"; } print "\n"; print " ln -s $wrapper_filename $filename\n"; if (not defined $opt_n) { 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 " rm $wrapper_source_filename\n"; if (not defined $opt_n) { unlink "$wrapper_source_filename" or my_die "Failed to delete $wrapper_source_filename"; } print "\n"; if (not defined $opt_n) { # Show what we're left with, to increase confidence that this script # is doing the right thing. print " 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 [-d dir] [-n] \"cc[ opts]\" env1[ env2...]\n" . "$our_program_name [-d dir] [-n] -u\n"; if (not getopts("d:nu")) { print STDERR $usage_error; exit 1; } if (defined $opt_u) { $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 files. Please wait.\n\n"; $OUTPUT_AUTOFLUSH = 1; # so the progress dots will come out right away if (not defined $opt_d) { $opt_d = "/"; } find(\&ARG_is_setid_program, $opt_d); print "\n\n"; foreach $file (sort keys %setid_files) { if (defined $opt_u) { unwrap_file $file; } else { wrap_file $file; } } print "$our_program_name: Finished successfully.\n";