. .

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.


Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>