Python: kompilierte gentoo Linux Pakete automatisiert in gechrootete Verzeichnisse übertragen
Aus Sicherheitsgründen werden bestimmte Dienste wie Web-, Mail- oder FTP-Services gechrooted, um den Diensten den Zugriff außerhalb des gechrooteten Basis-Verzeichnisses zu verwehren.
Sollte der Dienst einem erfolgreichen Angriff erlegen sein, befindet sich der Angreifer in einer eingeschränkten Umgebung, die nur für das erfolgreiche Laufen des Dienstes konfiguriert ist bzw. sein sollte.
Dadurch kann die Sicherheit des restlichen Systems bzw. der restlichen Dienste erhöht werden.
Unter gentoo Linux wird zur Packetverwaltung (der sog. Portage) das Tool `emerge‘ verwendet.
Logischerweise werden von den „eingesperrten“ Diensten bestimmte Bibliotheken und auch Binaries benötigt, um überhaupt laufen zu können.
Wenn mit `emerge‘ ein Software-Paket installiert wird, passiert das normalerweise abhängig vom Root-Verzeichnis. Für eine eingesperrte Umgebung ist das genau das Richtige, da ja das gechrootete Verzeichnis das neue „root“ bzw. Basis-Verzeichnis des zu laufenden Dienstes wird. (Das betrifft z.B. ld.so.conf, -prefix oder auch -rpath beim Kompilieren.)
Mit dem Befehl `equery‘ können Informationen zu installierten Programmpaketen abgerufen werden. Diese Informationen nutze ich, um die entsprechenden Dateien und Verzeichnisse mit dem python-Script in die gechrootete Umgebung zu übertragen.
Somit können über die Portage-Informationen die entsprechenden Dateien in die gechrootete Umgebung kopiert werden.
Das Script benötigt als hauptsächliche Information natürlich das entsprechende Paket, das in die chrooted-Umgebung übertragen werden soll, zum Beispiel dev-lang/php.
Ansonsten sollte unbedingt auf die gesetzten Default-Werte geachtet werden, wie zum Beispiel das Chroot-Verzeichnis oder die ignore-Basisverzeichnisse, die eventuell per Option angepasst werden müssen.
Die Hilfe des Scripts gibt daher die erforderlichen Informationen:
Hier also das Hauptscript (`copy2chroot.py‘):
#!/usr/bin/env python # -*- coding: utf-8 -*- ############################################ # import needed modules import sys import argparse import os import stat import copy2chroot_funcs as funcs from copy2chroot_funcs import pcolors ############################################ # default values of argument variables default_argument_values = { 'def_equery_files_bin': '/usr/bin/equery -C files', 'def_ldd_bin': '/usr/bin/ldd', 'def_chroot_bin': '/bin/chroot', 'def_chroot_dir': '/chroot/apache', 'def_dirs_to_ignore': '/etc/skel:/usr/share/man:/usr/share/doc:/usr/share/php/docs', 'def_logfile': '/var/log/copy2chroot.log' } ############################################ # parsing arguments parser = funcs.set_argparser(default_argument_values) args = funcs.get_args(parser) args.logfile = funcs.normalize_path(args.logfile) if (args.subcommand == 'copy'): args.target_dir = funcs.normalize_path(args.target_dir) args.package = funcs.normalize_path(args.package) args.equery_files_bin = funcs.normalize_path(args.equery_files_bin) args.ldd_bin = funcs.normalize_path(args.ldd_bin) args.chroot_bin = funcs.normalize_path(args.chroot_bin) args.ignore_base_objects = funcs.normalize_path(args.ignore_base_objects) args.dirs_to_ignore = args.ignore_base_objects.split(':'); if not os.path.isdir(args.target_dir): print 'Target chroot root directory `' + args.target_dir + '\' does not exist!' sys.exit(1) ############################################ # executing command print 'Using target chroot root directory `' + pcolors.pink + args.target_dir + pcolors.end + '\'' funcs.check_root_permissions() print 'Executing `' + args.equery_files_bin + ' ' + args.package + '\' - please wait...', equery_data = funcs.get_equery_data(args.equery_files_bin + ' ' + args.package) ############################################ # handling all returned data print pcolors.pink + 'got ' + str(len(equery_data)) + ' results' + pcolors.end + '.' + "\n" funcs.copy_equery_data(equery_data, args) funcs.log_equery(args) elif (args.subcommand == 'print'): args.only_chroot = funcs.normalize_path(args.only_chroot) funcs.print_equery_log(args)
Damit das Hauptscript übersichtlich bleibt, habe ich die eigentliche Arbeit der Datei `copy2chroot_funcs.py‘ überlassen (die dann auch in `copy2chroot.py‘ importiert wird, somit bei Änderungen anpassen):
# -*- coding: utf-8 -*- import sys import argparse import re import os import subprocess import shutil import logging import time ############################################ # command line colors class pcolors(): blue = '\033[94m' end = '\033[0m' green = '\033[92m' pink = '\033[95m' red = '\033[91m' yellow = '\033[93m' ############################################ # parsing arguments def set_argparser(def_vals): parser = argparse.ArgumentParser(description='Copies all data of an installed gentoo package to a given directory.') subparsers = parser.add_subparsers(title='action selection', description='valid subcommands are:', dest='subcommand') parser_copy = subparsers.add_parser('copy', help='copy a package to a chroot directory') parser_copy.add_argument('-b', '--create-backups', help='create a backup file for every existing file (not links)', action='store_true') parser_copy.add_argument('-c', '--check-libs', help='check all copied files against shared libraries', action='store_true') parser_copy.add_argument('-C', '--chroot-bin', help='absolute path of `chroot\' command (default: `' + def_vals['def_chroot_bin'] + '\')', default=def_vals['def_chroot_bin']) parser_copy.add_argument('-d', '--target-dir', help='absolute path to target base root directory (default: `' + def_vals['def_chroot_dir'] + '\')', default=def_vals['def_chroot_dir']) parser_copy.add_argument('-e', '--equery-files-bin', help='absolute path and argument of `equery files\' command (default: `' + def_vals['def_equery_files_bin'] + '\')', default=def_vals['def_equery_files_bin']) parser_copy.add_argument('-i', '--ignore-base-objects', help='`:\' separated list of baseobjects to ignore (default: `' + def_vals['def_dirs_to_ignore'] + '\')', default=def_vals['def_dirs_to_ignore']) parser_copy.add_argument('-l', '--logfile', help='log copied package name to LOGFILE (default: `' + def_vals['def_logfile'] + '\')', default=def_vals['def_logfile']) parser_copy.add_argument('-L', '--ldd-bin', help='absolute path of `ldd\' command (default: `' + def_vals['def_ldd_bin'] + '\')', default=def_vals['def_ldd_bin']) parser_copy.add_argument('-o', '--override', help='override existing files (maybe you want to set also `-b\' parameter)', action='store_true') parser_copy.add_argument('-p', '--package', help='full gentoo portage package atom to use', required=True) parser_print = subparsers.add_parser('print', help='print all copied packages') parser_print.add_argument('-l', '--logfile', help='read copied package informations from LOGFILE (default: `' + def_vals['def_logfile'] + '\')', default=def_vals['def_logfile']) parser_print.add_argument('-O', '--only-chroot', help='print only packages which are copied to ONLY_CHROOT', default='') parser_print.add_argument('-v', '--verbose', help='print complete logfile data lines', action='store_true') return parser def get_args(parser): if len(sys.argv) == 1: parser.print_help() sys.exit(1) return parser.parse_args() ############################################ # check root def check_root_permissions(): if os.geteuid() != 0: print 'You probably will need root permissions to execute all commands successfully.' + "\n" sys.exit(1) ############################################ # normalize def normalize_path(path): path = re.sub('\/+', '/', path) if (path[-1:] == '/'): path = path[:-1] return path ############################################ # get and work with data def get_equery_data(command): subproc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) (equery_data, equery_error) = subproc.communicate() equery_error = equery_error.split("\n") equery_data = equery_data.split("\n") equery_error.remove('') equery_data.remove('') if (len(equery_error) != 0): print "\n" + 'Error executing equery:' for line in equery_error: print line sys.exit(1) if (len(equery_data) < 1): print 'Error executing equery: no data returned!' sys.exit(1) return equery_data def create_backup_equery_chroot_file(equery_chroot_object): cur_time = time.time() shutil.move(equery_chroot_object, equery_chroot_object + '.' + str(cur_time) + '.bak') if (os.path.isfile(equery_chroot_object + '.' + str(cur_time) + '.bak')): print '[' + pcolors.green + 'moved to backup' + pcolors.end + ']', return True else: print '[' + pcolors.red + 'could not move backup' + pcolors.end + ']', return False def remove_equery_chroot_file(equery_chroot_object): os.remove(equery_chroot_object) if (os.path.isfile(equery_chroot_object)): print '[' + pcolors.red + 'could not remove file' + pcolors.end + ']', return False else: print '[' + pcolors.green + 'removed file' + pcolors.end + ']', return True def check_and_copy_equery_object(equery_object, equery_chroot_object, typ=0, args=False): created = False exists = False destroyed = False all_ok = False backup_created = False if (typ == 0): exists = os.path.isdir(equery_chroot_object) elif (typ == 1): exists = os.path.isfile(equery_chroot_object) elif (typ == 2): exists = os.path.islink(equery_chroot_object) if exists: print '[' + pcolors.yellow + 'already exists' + pcolors.end + ']', if (typ == 1): if (args.override == True): if (args.create_backups == True): backup_created = create_backup_equery_chroot_file(equery_chroot_object) else: destroyed = remove_equery_chroot_file(equery_chroot_object) if (backup_created == True) or (destroyed == True): shutil.copyfile(equery_object, equery_chroot_object) created = os.path.isfile(equery_chroot_object) if created: print '[' + pcolors.green + 'created new copy' + pcolors.end + ']', all_ok = set_equery_permissions(equery_object, equery_chroot_object) else: print '[' + pcolors.red + 'could not create new copy' + pcolors.end + ']', else: all_ok = set_equery_permissions(equery_object, equery_chroot_object) else: if (typ == 0): os.makedirs(equery_chroot_object) created = os.path.isdir(equery_chroot_object) elif (typ == 1): shutil.copyfile(equery_object, equery_chroot_object) created = os.path.isfile(equery_chroot_object) elif (typ == 2): linkto = os.readlink(equery_object) os.symlink(linkto, equery_chroot_object) created = os.path.islink(equery_chroot_object) if created: print '[' + pcolors.green + 'did not exist - created' + pcolors.end + ']', if (typ != 2): all_ok = set_equery_permissions(equery_object, equery_chroot_object) else: print '[' + pcolors.red + 'does not exist - and could not create it' + pcolors.end + ']', return all_ok def set_equery_permissions(orig, copy): all_ok = True orig_stat = os.stat(orig) orig_mode = (orig_stat.st_mode & 0777) orig_uid = orig_stat.st_uid orig_gid = orig_stat.st_gid ''' These two functions don't return a value ''' os.chmod(copy, orig_mode) os.chown(copy, orig_uid, orig_gid) copy_stat = os.stat(copy) copy_mode = (copy_stat.st_mode & 0777) copy_uid = copy_stat.st_uid copy_gid = copy_stat.st_gid if not copy_mode == orig_mode: print '[' + pcolors.red + 'chmod failed' + pcolors.end + ']', all_ok = False else: print '[' + pcolors.green + 'chmod done' + pcolors.end + ']', if not (copy_uid == orig_uid) or not (copy_gid == orig_gid): print '[' + pcolors.red + 'chown failed' + pcolors.end + ']', all_ok = False else: print '[' + pcolors.green + 'chown done' + pcolors.end + ']', return all_ok def check_equery_chroot_object(command): subproc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) (ldd_data, ldd_error) = subproc.communicate() ldd_error = ldd_error.split("\n") ldd_data = ldd_data.split("\n") ldd_error.remove('') ldd_data.remove('') print '[' + pcolors.pink + 'ldd' + pcolors.end + ':', err_counter = 0 if (len(ldd_error) != 0): for line in ldd_error: if not 'ldd: warning: you do not have execution permission for' in line: err_counter += 1 for line in ldd_data: if '=> not found' in line: err_counter += 1 if err_counter > 0: print pcolors.red + str(err_counter) + ' errors' + pcolors.end + ']', else: print pcolors.green + 'no errors found' + pcolors.end + ']', def copy_equery_data(equery_data, args): for equery_object in equery_data: equery_chroot_object = args.target_dir + '/' + equery_object equery_chroot_object = normalize_path(equery_chroot_object) ignored = False for ignore in args.dirs_to_ignore: len_ignore = len(ignore) if len(equery_object) >= len_ignore: substring = equery_object[:len_ignore] if (substring == ignore): ignored = True if ignored == True: print '`' + equery_object + '\' is in ignore list: [' + pcolors.blue + 'skipping it' + pcolors.end + ']', elif os.path.islink(equery_object): print '`' + equery_object + '\' is a ' + pcolors.pink + 'link' + pcolors.end + ' - checking target:', check_and_copy_equery_object(equery_object, equery_chroot_object, 2, args) elif os.path.isdir(equery_object): print '`' + equery_object + '\' is a ' + pcolors.pink + 'directory' + pcolors.end + ' - checking target:', check_and_copy_equery_object(equery_object, equery_chroot_object, args=args) elif os.path.isfile(equery_object): print '`' + equery_object + '\' is a ' + pcolors.pink + 'file' + pcolors.end + ' - checking target:', if check_and_copy_equery_object(equery_object, equery_chroot_object, 1, args): if args.check_libs == True: check_equery_chroot_object(args.chroot_bin + ' ' + args.target_dir + ' ' + args.ldd_bin + ' ' + equery_object) else: print '`' + equery_object + '\' not a recognized object: [' + pcolors.red + 'skipping it' + pcolors.end + ']', print '' def log_equery(args): logger = logging.getLogger('copy2chroot') log_handler = logging.FileHandler(args.logfile) log_format= logging.Formatter('%(asctime)s %(levelname)s %(message)s') log_handler.setFormatter(log_format) logger.addHandler(log_handler) logger.setLevel(logging.INFO) logger.info(args.target_dir + '|' + args.package + '|' + args.ignore_base_objects) def print_equery_log(args): lines = [line.strip() for line in open(args.logfile)] print_lines = [] for line in lines: print_line = True if not (args.only_chroot == ''): if not 'INFO ' + args.only_chroot + '|' in line: print_line = False if (print_line == True): if (args.verbose == True): print_lines.append(line) else: new_line = line.split(' ', 3)[3] new_prog = line.split('|')[1] if not new_prog in print_lines: print_lines.append(new_prog) for line in print_lines: print line
Hier ein kleines Beispiel, um die zlib nach /chroot/apache zu kopieren.
Dabei sind die enstprechenden Argumente zu beachten, um ein Backup zu erstellen oder auch die Binaries dahingehend zu prüfen, ob alle Abhängikeiten erfüllt sind. Für letzteres ist der bekannte Befehl `ldd‘ notwendig, um die angezogenen Bibliotheken zu prüfen. Da ist es aber wichtig, dass in der gechrooteten Umgebung `ldd‘ auch funktioniert (somit selbst übertragen wurde).
Ansonsten ist an für sich alles per Argument beeinflussbar.
Es gibt auch ein Logfile bzgl. der portierten Pakete.
Um die Informationen abzurufen, welche Pakete in welches gechrootete Verzeichnis kopiert wurden, gibt es bei diesem Script das Sub-Command `print':
Damit kann also festgestellt werden, in welcher Chroot-Umgebung welches installierte gentoo-Paket kopiert wurde.
Die angewandte Python-Version ist in diesem Fall 2.7.