diff --git a/README.md b/README.md index 4214c5aa..2a333eb5 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ Currently, custom projects can only be used programmatically as follows: import os from s2e_env.commands.new_project import Command as NewProjectCommand -from s2e_env.commands.project_creation.abstract_project import AbstractProject +from s2e_env.commands.project_creation import AbstractProject from s2e_env.manage import call_command diff --git a/s2e_env/command.py b/s2e_env/command.py index 6a422727..9928bed5 100644 --- a/s2e_env/command.py +++ b/s2e_env/command.py @@ -30,6 +30,7 @@ """ +from abc import ABCMeta, abstractmethod from argparse import ArgumentParser import json import logging @@ -38,8 +39,6 @@ import yaml -from s2e_env.utils import log - class CommandError(Exception): """ @@ -104,16 +103,15 @@ class BaseCommand(object): ``help`` class attribute should be specified. """ + # Abstract class + __metaclass__ = ABCMeta + # Metadata about this command help = '' # Configuration shortcuts that alter various logic. called_from_command_line = False - def __init__(self): - # Initialize the default logger - log.configure_logging() - def create_parser(self, prog_name, subcommand): """ Create and return the ``CommandParser`` which will be used to parse @@ -189,6 +187,7 @@ def execute(self, *args, **options): def name(self): return self.__module__.split('.')[-1] + @abstractmethod def handle(self, *args, **options): """ The actual logic of the command. Subclasses must implement this method. @@ -211,17 +210,6 @@ def __init__(self): self._env_dir = None self._config = None - def _init_logging(self): - config_lvl = self.config.get('logging', {}).get('level', 'info') - color = self.config.get('logging', {}).get('color', True) - - level = logging.getLevelName(config_lvl.upper()) - if not isinstance(level, int): - raise CommandError('Invalid logging level \'%s\' in s2e.yaml' % - config_lvl) - - log.configure_logging(level, color) - def handle_common_args(self, **options): """ Adds the environment directory as a class member. @@ -242,10 +230,9 @@ def handle_common_args(self, **options): self._config = yaml.load(f) except IOError: raise CommandError('This does not look like an S2E environment - ' - 'it does not contain an s2e.yaml configuration file (%s does not exist)' % path) - - # Reinitialize logging with settings from the environment's config - self._init_logging() + 'it does not contain an s2e.yaml configuration ' + 'file (%s does not exist). Source %s in your ' + 'environment' % (path, self.env_path('s2e_activate'))) def add_arguments(self, parser): super(EnvCommand, self).add_arguments(parser) diff --git a/s2e_env/commands/code_coverage/basic_block.py b/s2e_env/commands/code_coverage/basic_block.py index 43ca3ce4..15b2238f 100644 --- a/s2e_env/commands/code_coverage/basic_block.py +++ b/s2e_env/commands/code_coverage/basic_block.py @@ -23,6 +23,7 @@ from __future__ import division +from abc import abstractmethod from collections import defaultdict import json import itertools @@ -276,6 +277,7 @@ def _initialize_disassembler(self): """ pass + @abstractmethod def _get_disassembly_info(self, module_path): """ Disassemble the give module using on the of the supported backends (IDA diff --git a/s2e_env/commands/coverage.py b/s2e_env/commands/coverage.py index 88da1b5c..86b88705 100644 --- a/s2e_env/commands/coverage.py +++ b/s2e_env/commands/coverage.py @@ -22,7 +22,6 @@ from s2e_env.command import ProjectCommand, CommandError -from s2e_env.commands.code_coverage.basic_block import BasicBlockCoverage from s2e_env.commands.code_coverage.lcov import LineCoverage from s2e_env.manage import call_command @@ -62,7 +61,7 @@ def add_arguments(self, parser): help='Where to store the LCOV report (by default into s2e-last)', required=False) - bb_parser = subparsers.add_parser('basic_block', cmd=BasicBlockCoverage(), + bb_parser = subparsers.add_parser('basic_block', cmd=IDABasicBlockCoverage(), help='Generate a basic block report') bb_parser.add_argument('-d', '--disassembler', choices=('ida', 'r2', 'binaryninja'), default='ida', help='Disassembler backend to use') diff --git a/s2e_env/commands/new_project.py b/s2e_env/commands/new_project.py index 7c835fbf..4abfda1f 100644 --- a/s2e_env/commands/new_project.py +++ b/s2e_env/commands/new_project.py @@ -23,40 +23,24 @@ import argparse import logging -import os -import re - -from magic import Magic from s2e_env.command import EnvCommand, CommandError -from s2e_env.commands.project_creation.abstract_project import AbstractProject -from s2e_env.commands.project_creation.cgc_project import CGCProject -from s2e_env.commands.project_creation.linux_project import LinuxProject -from s2e_env.commands.project_creation.windows_project import WindowsProject, WindowsDLLProject, WindowsDriverProject -from s2e_env.infparser.driver import Driver +from s2e_env.commands.project_creation import CGCProject +from s2e_env.commands.project_creation import LinuxProject +from s2e_env.commands.project_creation import WindowsProject, \ + WindowsDLLProject, WindowsDriverProject +from s2e_env.commands.project_creation import Target from s2e_env.manage import call_command logger = logging.getLogger('new_project') -# Paths -FILE_DIR = os.path.dirname(__file__) -CGC_MAGIC = os.path.join(FILE_DIR, '..', 'dat', 'cgc.magic') - -# Magic regexs -CGC_REGEX = re.compile(r'^CGC 32-bit') -ELF32_REGEX = re.compile(r'^ELF 32-bit') -ELF64_REGEX = re.compile(r'^ELF 64-bit') -PE32_REGEX = re.compile(r'^PE32 executable') -PE64_REGEX = re.compile(r'^PE32\+ executable') -MSDOS_REGEX = re.compile(r'^MS-DOS executable') -DLL32_REGEX = re.compile(r'^PE32 executable \(DLL\)') -DLL64_REGEX = re.compile(r'^PE32\+ executable \(DLL\)') - -PROJECT_CLASSES = { +PROJECT_TYPES = { 'cgc': CGCProject, 'linux': LinuxProject, 'windows': WindowsProject, + 'windows_dll': WindowsDLLProject, + 'windows_driver': WindowsDriverProject, } @@ -83,135 +67,14 @@ def _parse_sym_args(sym_args_str): return sym_args -def _get_arch(target_path): - """ - Check that the given target is supported by S2E. - - The target's magic is checked to see if it is a supported file type (e.g. - ELF, PE, etc.). The architecture that the target was compiled for (e.g. - i386, x64, etc.) is also checked. - - Returns: - A tuple containing the target's architecture and a project class is - returned. - """ - default_magic = Magic() - magic_checks = [ - (Magic(magic_file=CGC_MAGIC), CGC_REGEX, CGCProject, 'i386'), - (default_magic, ELF32_REGEX, LinuxProject, 'i386'), - (default_magic, ELF64_REGEX, LinuxProject, 'x86_64'), - (default_magic, DLL32_REGEX, WindowsDLLProject, 'i386'), - (default_magic, DLL64_REGEX, WindowsDLLProject, 'x86_64'), - (default_magic, PE32_REGEX, WindowsProject, 'i386'), - (default_magic, PE64_REGEX, WindowsProject, 'x86_64'), - (default_magic, MSDOS_REGEX, WindowsProject, 'i386') - ] - - # Check the target program against the valid file types - for magic_check, regex, proj_class, arch in magic_checks: - magic = magic_check.from_file(target_path) - matches = regex.match(magic) - - # If we find a match, create that project - if matches: - return arch, proj_class - - return None, None - - -def _handle_win_driver_project(target_path, driver_files, *args, **options): - first_sys_file = None - for f in driver_files: - if f.endswith('.sys'): - first_sys_file = f - - # TODO: prompt the user to select the right driver - if not first_sys_file: - raise CommandError('Could not find any *.sys file in the INF file. ' - 'Make sure the INF file is valid and belongs to a ' - 'Windows driver') - - # Determine the architecture of the first sys file - first_sys_file = os.path.realpath(first_sys_file) - arch, _ = _get_arch(first_sys_file) - if not arch: - raise CommandError('Could not determine architecture for %s' % - first_sys_file) - - options['target_files'] = [target_path] + driver_files - options['target_arch'] = arch - - # TODO: support multiple kernel drivers - options['modules'] = [(os.path.basename(first_sys_file), True)] - - call_command(WindowsDriverProject(), *args, **options) - - -def _extract_inf_files(target_path): - driver = Driver(target_path) - driver.analyze() - driver_files = driver.get_files() - if not driver_files: - raise CommandError('Driver has no files') +def _handle_with_file(target_path, proj_class, *args, **options): + target = Target.from_file(target_path, proj_class) + options['target'] = target - base_dir = os.path.dirname(target_path) + return call_command(target.initialize_project(), *args, **options) - logger.info(' Driver files:') - file_paths = [] - for f in driver_files: - full_path = os.path.join(base_dir, f) - if not os.path.exists(full_path): - if full_path.endswith('.cat'): - logger.warn('Catalog file %s is missing', full_path) - continue - else: - raise CommandError('%s does not exist' % full_path) - logger.info(' %s', full_path) - file_paths.append(full_path) - - return list(set(file_paths)) - - -def _handle_generic_project(target_path, *args, **options): - arch, proj_class = _get_arch(target_path) - if not arch: - raise CommandError('%s is not a valid target for S2E analysis' % target_path) - - options['target_files'] = [target_path] - options['target_arch'] = arch - - # The module list is a list of tuples where the first element is the module - # name and the second element is True if the module is a kernel module - options['modules'] = [(os.path.basename(target_path), False)] - - call_command(proj_class(), *args, **options) - - -def _handle_with_file(target_path, *args, **options): - # Check that the target is a valid file - if not os.path.isfile(target_path): - raise CommandError('Target %s is not valid' % target_path) - - if target_path.endswith('.inf'): - # Don't call realpath on an inf file. Doing so will force - # lookup of binary files in the same directory as the actual inf file. - logger.info('Detected Windows INF file, attempting to create a driver project...') - driver_files = _extract_inf_files(target_path) - - _handle_win_driver_project(target_path, driver_files, *args, **options) - elif target_path.endswith('.sys'): - logger.info('Detected Windows SYS file, attempting to create a driver project...') - target_path = os.path.realpath(target_path) - - _handle_win_driver_project(target_path, [], *args, **options) - else: - target_path = os.path.realpath(target_path) - - _handle_generic_project(target_path, *args, **options) - - -def _handle_empty_project(*args, **options): +def _handle_empty_project(proj_class, *args, **options): if not options['no_target']: raise CommandError('No target binary specified. Use the -m option to ' 'create an empty project') @@ -224,21 +87,19 @@ def _handle_empty_project(*args, **options): raise CommandError('An empty project requires a name. Use the -n ' 'option to specify one') - project_types = PROJECT_CLASSES.keys() - if options['type'] not in project_types: - raise CommandError('An empty project requires a type. Use the -t ' - 'option and specify one from %s' % project_types) - - options['target_files'] = [] - options['target_arch'] = None + # If the project class wasn't explicitly overridden programmatically, get + # one of the default project classes from the command line + if not proj_class: + project_types = PROJECT_TYPES.keys() + if options['type'] not in project_types: + raise CommandError('An empty project requires a type. Use the -t ' + 'option and specify one from %s' % project_types) + proj_class = PROJECT_TYPES[options['type']] - # The module list is a list of tuples where the first element is - # the module name and the second element is True if the module is - # a kernel module - options['modules'] = [] + target = Target.empty(proj_class) + options['target'] = target - project = PROJECT_CLASSES[options['type']] - call_command(project(), *args, **options) + return call_command(target.initialize_project(), *args, **options) class Command(EnvCommand): @@ -274,11 +135,12 @@ def add_arguments(self, parser): parser.add_argument('-m', '--no-target', required=False, default=False, action='store_true', - help='Create an empty, target-less project. Used when no binary is needed') + help='Create an empty, target-less project. Used ' + 'when no binary is needed') parser.add_argument('-t', '--type', required=False, default=None, help='Project type (%s), valid only when creating empty projects' % - ','.join(PROJECT_CLASSES.keys())) + ','.join(PROJECT_TYPES.keys())) parser.add_argument('-s', '--use-seeds', action='store_true', help='Use this option to use seeds for creating ' @@ -302,13 +164,8 @@ def handle(self, *args, **options): # it is typically used when creating a custom project programatically. # It provides a class that is instantiated with the current # command-line arguments and options - proj_class = options.get('project_class') - if proj_class: - if not issubclass(proj_class, AbstractProject): - raise CommandError('Custom projects must be a subclass of ' - 'AbstractProject') - call_command(proj_class(), *args, **options) - elif options['target']: - _handle_with_file(options.pop('target'), *args, **options) + proj_class = options.pop('project_class', None) + if options['target']: + _handle_with_file(options.pop('target'), proj_class, *args, **options) else: - _handle_empty_project(*args, **options) + _handle_empty_project(proj_class, *args, **options) diff --git a/s2e_env/commands/project_creation/__init__.py b/s2e_env/commands/project_creation/__init__.py index 6774cf4c..f153ab57 100644 --- a/s2e_env/commands/project_creation/__init__.py +++ b/s2e_env/commands/project_creation/__init__.py @@ -22,384 +22,9 @@ """ -import datetime -import json -import logging -import os -import re -import shutil - -from s2e_env.command import CommandError -from s2e_env.commands.recipe import Command as RecipeCommand -from s2e_env.manage import call_command -from s2e_env.utils.templates import render_template from .abstract_project import AbstractProject - - -logger = logging.getLogger('new_project') - - -def _check_project_dir(project_dir, force=False): - """ - Check if a project directory with the given name already exists. - - If such a project exists, only continue if the ``force`` flag has been - specified. - """ - if not os.path.isdir(project_dir): - return - - if force: - logger.info('\'%s\' already exists - removing', - os.path.basename(project_dir)) - shutil.rmtree(project_dir) - else: - raise CommandError('\'%s\' already exists. Either remove this ' - 'project or use the force option' % - os.path.basename(project_dir)) - - -def is_valid_arch(target_arch, os_desc): - """ - Check that the image's architecture is consistent with the target binary. - """ - return not (target_arch == 'x86_64' and os_desc['arch'] != 'x86_64') - - -def _save_json_description(project_dir, config): - """ - Create a JSON description of the project. - - This information can be used by other commands. - """ - logger.info('Creating JSON description') - - project_desc_path = os.path.join(project_dir, 'project.json') - with open(project_desc_path, 'w') as f: - s = json.dumps(config, sort_keys=True, indent=4) - f.write(s) - - -def _symlink_target_files(project_dir, files): - """ - Create symlinks to the files that compose the program. - """ - for f in files: - logger.info('Creating a symlink to %s', f) - target_file = os.path.basename(f) - os.symlink(f, os.path.join(project_dir, target_file)) - - -def _symlink_guestfs(project_dir, guestfs_path): - """ - Create a symlink to the guestfs directory. - - Return ``True`` if the guestfs directory exists, or ``False`` otherwise. - """ - logger.info('Creating a symlink to %s', guestfs_path) - os.symlink(guestfs_path, os.path.join(project_dir, 'guestfs')) - - return True - - -class Project(AbstractProject): - """ - Base class used by the ``new_project`` command to create a specific - project. The ``make_project`` method builds up a configuration dictionary - that is then used to generate the required files for the project. CGC, - Linux and Windows projects extend this class. These projects implement - methods to validate the configuration dictionary and do basic static - analysis on the target. - """ - - def __init__(self, project_type, bootstrap_template, lua_template): - super(Project, self).__init__() - - self._project_type = project_type - self._bootstrap_template = bootstrap_template - self._lua_template = lua_template - - def _make_config(self, *args, **options): - # Check that the target files are valid - target_files = options['target_files'] - if target_files: - for tf in target_files: - if not os.path.isfile(tf): - raise CommandError('Target file %s is not valid' % tf) - else: - logger.warn('Creating a project without a target file. You must ' - 'manually edit bootstrap.sh') - - # The target program that will be executed is the first target file - if target_files: - target_path = target_files[0] - else: - target_path = None - - target_arch = options['target_arch'] - - # Decide on the image to be used - img_desc = self._select_image(target_path, target_arch, - options.get('image'), - options.get('download_image', False)) - - # Check architecture consistency (if the target has been specified) - if target_path and not is_valid_arch(target_arch, img_desc['os']): - raise CommandError('Binary is %s while VM image is %s. Please ' - 'choose another image' % (target_arch, - img_desc['os']['arch'])) - - # Determine if guestfs is available for this image - guestfs_path = self._select_guestfs(img_desc) - if not guestfs_path: - logger.warn('No guestfs available. The VMI plugin may not run optimally') - - # Generate the name of the project directory. The default project name - # is the target program name without any file extension - project_name = options.get('name') - if not project_name: - project_name, _ = os.path.splitext(os.path.basename(target_path)) - project_dir = self.env_path('projects', project_name) - - # Prepare the project configuration - config = { - 'creation_time': str(datetime.datetime.now()), - 'project_dir': project_dir, - 'project_type': self._project_type, - 'image': img_desc, - 'target_path': target_path, - 'target_arch': target_arch, - 'target_args': options.get('target_args', []), - - # This contains paths to all the files that must be downloaded into - # the guest - 'target_files': target_files, - - # List of module names that go into ModuleExecutionDetector - 'modules': options.get('modules', []), - - # List of binaries that go into ProcessExecutionDetector. These are - # normally executable files - 'processes': [os.path.basename(target_path)] if target_path else [], - - # Target arguments to be made symbolic - 'sym_args': options.get('sym_args', []), - - # See _create_bootstrap for an explanation of the @@ marker - 'use_symb_input_file': '@@' in options.get('target_args', []), - - # The use of seeds is specified on the command line - 'use_seeds': options.get('use_seeds', False), - 'seeds_dir': os.path.join(project_dir, 'seeds'), - - # The use of recipes is set by the specific project - 'use_recipes': False, - 'recipes_dir': os.path.join(project_dir, 'recipes'), - - # The use of guestfs is dependent on the specific image - 'has_guestfs': guestfs_path is not None, - 'guestfs_path': guestfs_path, - - # These options are determined by a static analysis of the target - 'dynamically_linked': False, - 'modelled_functions': False, - - # Specific projects can silence warnings in case they have specific - # hard-coded options - 'warn_seeds': True, - 'warn_input_file': True, - - # Searcher options - 'use_cupa': True, - - 'use_test_case_generator': True, - 'use_fault_injection': False, - - # This will add analysis overhead, so disable unless requested by - # the user. Also enabled by default for Decree targets. - 'enable_pov_generation': options.get('enable_pov_generation', False), - } - - # Do some basic analysis on the target (if it exists) - if target_path: - self._analyze_target(target_path, config) - - if config['enable_pov_generation']: - config['use_recipes'] = True - - # The config dictionary may be modified here. After this point the - # config dictionary should NOT be modified - self._validate_config(config) - - return config - - def _create(self, config, force=False): - project_dir = config['project_dir'] - - # Check if the project directory already exists - _check_project_dir(project_dir, force) - - # Create the project directory - os.mkdir(project_dir) - - if config['use_seeds'] and not os.path.isdir(config['seeds_dir']): - os.mkdir(config['seeds_dir']) - - # Create symlinks to the target files (if they exist) - if config['target_files']: - _symlink_target_files(project_dir, config['target_files']) - - # Create a symlink to the guest tools directory - self._symlink_guest_tools(project_dir, config['image']) - - # Create a symlink to guestfs (if it exists) - if config['guestfs_path']: - _symlink_guestfs(project_dir, config['guestfs_path']) - - # Render the templates - self._create_launch_script(project_dir, config) - self._create_lua_config(project_dir, config) - self._create_bootstrap(project_dir, config) - - # Save the project configuration as JSON - _save_json_description(project_dir, config) - - # Generate recipes for PoV generation - if config['use_recipes']: - os.makedirs(config['recipes_dir']) - call_command(RecipeCommand(), [], project=os.path.basename(project_dir)) - - # Display messages/instructions to the user - display_marker_warning = config['target_path'] and \ - config['warn_input_file'] and \ - not (config['use_symb_input_file'] or config['sym_args']) - - if display_marker_warning: - logger.warning('You did not specify the input file marker @@. ' - 'This marker is automatically substituted by a ' - 'file with symbolic content. You will have to ' - 'manually edit the bootstrap file in order to run ' - 'the program on multiple paths.\n\n' - 'Example: %s @@\n\n' - 'You can also make arguments symbolic using the ' - '``S2E_SYM_ARGS`` environment variable in the ' - 'bootstrap file', config['target_path']) - - if config['use_seeds'] and not config['use_symb_input_file'] and config['warn_seeds']: - logger.warning('Seed files have been enabled, however you did not ' - 'specify an input file marker (i.e. \'@@\') to be ' - 'substituted with a seed file. This means that ' - 'seed files will be fetched but never used. Is ' - 'this intentional?') - - def _create_instructions(self, config): - instructions = render_template(config, 'instructions.txt') - - # Due to how templates work, there may be many useless new lines, - # remove them here - return re.sub(r'([\r\n][\r\n])+', r'\n\n', instructions) - - def _validate_config(self, config): - """ - Validate a project's configuration options. - - This method may modify values in the ``config`` dictionary. If an - invalid configuration is found, a ``CommandError` should be thrown. - """ - pass - - def _analyze_target(self, target_path, config): - """ - Perform static analysis on the target binary. - - The results of this analysis can be used to add and/or modify values in - the ``config`` dictionary. - """ - pass - - def _create_launch_script(self, project_dir, config): - """ - Create the S2E launch script. - """ - logger.info('Creating launch script') - - context = { - 'creation_time': config['creation_time'], - 'env_dir': self.env_path(), - 'rel_image_path': os.path.relpath(config['image']['path'], self.env_path()), - 'qemu_arch': config['image']['qemu_build'], - 'qemu_memory': config['image']['memory'], - 'qemu_snapshot': config['image']['snapshot'], - 'qemu_extra_flags': config['image']['qemu_extra_flags'], - } - - template = 'launch-s2e.sh' - script_path = os.path.join(project_dir, template) - render_template(context, template, script_path, executable=True) - - def _create_lua_config(self, project_dir, config): - """ - Create the S2E Lua config. - """ - logger.info('Creating S2E configuration') - - target_path = config['target_path'] - context = { - 'creation_time': config['creation_time'], - 'target': os.path.basename(target_path) if target_path else None, - 'target_lua_template': self._lua_template, - 'project_dir': config['project_dir'], - 'use_seeds': config['use_seeds'], - 'use_cupa': config['use_cupa'], - 'use_test_case_generator': config['use_test_case_generator'], - 'enable_pov_generation': config['enable_pov_generation'], - 'seeds_dir': config['seeds_dir'], - 'has_guestfs': config['has_guestfs'], - 'guestfs_path': config['guestfs_path'], - 'recipes_dir': config['recipes_dir'], - 'target_files': [os.path.basename(tf) for tf in config['target_files']], - 'modules': config['modules'], - 'processes': config['processes'], - } - - for f in ('s2e-config.lua', 'models.lua', 'library.lua'): - output_path = os.path.join(project_dir, f) - render_template(context, f, output_path) - - def _create_bootstrap(self, project_dir, config): - """ - Create the S2E bootstrap script. - """ - logger.info('Creating S2E bootstrap script') - - # The target arguments are specified using a format similar to the - # American Fuzzy Lop fuzzer. Options are specified as normal, however - # for programs that take input from a file, '@@' is used to mark the - # location in the target's command line where the input file should be - # placed. This will automatically be substituted with a symbolic file - # in the S2E bootstrap script. - parsed_args = ['${SYMB_FILE}' if arg == '@@' else arg - for arg in config['target_args']] - - target_path = config['target_path'] - context = { - 'creation_time': config['creation_time'], - 'target': os.path.basename(target_path) if target_path else None, - 'target_args': parsed_args, - 'sym_args': config['sym_args'], - 'target_bootstrap_template': self._bootstrap_template, - 'image_arch': config['image']['os']['arch'], - 'use_symb_input_file': config['use_symb_input_file'], - 'use_seeds': config['use_seeds'], - 'use_fault_injection': config['use_fault_injection'], - 'enable_pov_generation': config['enable_pov_generation'], - 'dynamically_linked': config['dynamically_linked'], - 'project_type': config['project_type'], - 'target_files': [os.path.basename(tf) for tf in config['target_files']], - 'modules': config['modules'], - 'processes': config['processes'], - } - - template = 'bootstrap.sh' - script_path = os.path.join(project_dir, template) - render_template(context, template, script_path) +from .cgc_project import CGCProject +from .linux_project import LinuxProject +from .target import Target +from .windows_project import WindowsProject, WindowsDLLProject, \ + WindowsDriverProject diff --git a/s2e_env/commands/project_creation/abstract_project.py b/s2e_env/commands/project_creation/abstract_project.py index 39823665..dd6b7a8f 100644 --- a/s2e_env/commands/project_creation/abstract_project.py +++ b/s2e_env/commands/project_creation/abstract_project.py @@ -23,16 +23,23 @@ """ +from abc import abstractmethod import logging import os +import shutil from s2e_env import CONSTANTS from s2e_env.command import EnvCommand, CommandError -from s2e_env.utils.images import ImageDownloader, get_image_templates, get_image_descriptor +from s2e_env.utils.images import ImageDownloader, get_image_templates, \ + get_image_descriptor logger = logging.getLogger('new_project') +# Paths +FILE_DIR = os.path.dirname(__file__) +LIBRARY_LUA_PATH = os.path.join(FILE_DIR, '..', '..', 'dat', 'library.lua') + class AbstractProject(EnvCommand): """ @@ -41,12 +48,12 @@ class AbstractProject(EnvCommand): This class must be overridden and the following methods **must** be implemented: - - ``_make_config`` + - ``_configure`` - ``_create`` The following methods may be optionally implemented: - - ``_create_instructions`` + - ``_get_instructions`` - ``_is_valid_image`` ``AbstractProject`` provides helper methods for deciding on the virtual @@ -54,46 +61,63 @@ class AbstractProject(EnvCommand): """ def handle(self, *args, **options): - config = self._make_config(*args, **options) - + target = options.pop('target') + config = self._configure(target, *args, **options) self._create(config, options['force']) - logger.success(self._create_instructions(config)) + instructions = self._get_instructions(config) + if instructions: + logger.success(instructions) + + # + # Abstract methods to overwrite + # - def _make_config(self, *args, **kwargs): + @abstractmethod + def _configure(self, target, *args, **kwargs): """ - Create the configuration dictionary that describes this project. + Generate the configuration dictionary that describes this project. - Should return a ``dict``. + Args: + target: A ``Target`` object that represents the program under + analysis. + + Returns: + A configuration ``dict``. """ raise NotImplementedError('Subclasses of AbstractProject must provide ' - 'a _make_config method') + 'a _configure method') + @abstractmethod def _create(self, config, force=False): """ - Create the actual project on disk, based on the given project - configuration dictionary. + Create the actual project based on the given project configuration + dictionary. """ raise NotImplementedError('Subclasses of AbstractProject must provide ' 'a _create method') - def _create_instructions(self, config): + def _get_instructions(self, config): """ - Create instructions for the user on how to use their newly-created + Generate instructions for the user on how to use their newly-created project. These instructions should be returned as a string. """ pass - def _is_valid_image(self, target_arch, target_path, os_desc): + def _is_valid_image(self, target, os_desc): """ - Validate a binary against a particular image description. + Validate a target against a particular image description. - This validation may vary depending on the binary and image type. + This validation may vary depending on the target and image type. Returns ``True`` if the binary is valid and ``False`` otherwise. """ pass - def _select_image(self, target_path, target_arch, image=None, download_image=True): + # + # Image helper methods + # + + def _select_image(self, target, image=None, download_image=True): """ Select an image to use for this project. @@ -110,27 +134,27 @@ def _select_image(self, target_path, target_arch, image=None, download_image=Tru img_templates = get_image_templates(img_build_dir) if not image: - image = self._guess_image(target_path, img_templates, target_arch) + image = self._guess_image(target, img_templates) return self._get_or_download_image(img_templates, image, download_image) - def _guess_image(self, target_path, templates, target_arch): + def _guess_image(self, target, templates): """ At this stage, images may not exist, so we get the list of images from images.json (in the guest-images repo) rather than from the images folder. """ logger.info('No image was specified (-i option). Attempting to guess ' - 'a suitable image for a %s binary...', target_arch) + 'a suitable image for a %s binary...', target.arch) for k, v in templates.iteritems(): - if self._is_valid_image(target_arch, target_path, v['os']): + if self._is_valid_image(target, v['os']): logger.warning('Found %s, which looks suitable for this ' 'binary. Please use -i if you want to use ' 'another image', k) return k - raise CommandError('No suitable image available for this binary') + raise CommandError('No suitable image available for this target') def _get_or_download_image(self, templates, image, do_download=True): img_path = self.image_path(image) @@ -147,22 +171,29 @@ def _get_or_download_image(self, templates, image, do_download=True): return get_image_descriptor(img_path) - def _select_guestfs(self, img_desc): - """ - Select the guestfs to use, based on the chosen virtual machine image. + # + # Misc. helper methods + # - Args: - img_desc: An image descriptor read from the image's JSON - description. + # pylint: disable=no-self-use + def _copy_lua_library(self, project_dir): + """ + Copy library.lua into the project directory. - Returns: - The path to the guestfs directory, or `None` if a suitable guestfs - was not found. + library.lua contains a number of helper methods that can be used by the + S2E Lua configuration file. """ - image_dir = os.path.dirname(img_desc['path']) - guestfs_path = self.image_path(image_dir, 'guestfs') + shutil.copy(LIBRARY_LUA_PATH, project_dir) - return guestfs_path if os.path.exists(guestfs_path) else None + # pylint: disable=no-self-use + def _symlink_project_files(self, project_dir, *files): + """ + Create symlinks to the files that compose the project. + """ + for f in files: + logger.info('Creating a symlink to %s', f) + target_file = os.path.basename(f) + os.symlink(f, os.path.join(project_dir, target_file)) def _symlink_guest_tools(self, project_dir, img_desc): """ @@ -180,3 +211,28 @@ def _symlink_guest_tools(self, project_dir, img_desc): logger.info('Creating a symlink to %s', guest_tools_path) os.symlink(guest_tools_path, os.path.join(project_dir, 'guest-tools')) + + def _select_guestfs(self, img_desc): + """ + Select the guestfs to use, based on the chosen virtual machine image. + + Args: + img_desc: An image descriptor read from the image's JSON + description. + + Returns: + The path to the guestfs directory, or `None` if a suitable guestfs + was not found. + """ + image_dir = os.path.dirname(img_desc['path']) + guestfs_path = self.image_path(image_dir, 'guestfs') + + return guestfs_path if os.path.exists(guestfs_path) else None + + # pylint: disable=no-self-use + def _symlink_guestfs(self, project_dir, guestfs_path): + """ + Create a symlink to the guestfs directory. + """ + logger.info('Creating a symlink to %s', guestfs_path) + os.symlink(guestfs_path, os.path.join(project_dir, 'guestfs')) diff --git a/s2e_env/commands/project_creation/base_project.py b/s2e_env/commands/project_creation/base_project.py new file mode 100644 index 00000000..603109d3 --- /dev/null +++ b/s2e_env/commands/project_creation/base_project.py @@ -0,0 +1,371 @@ +""" +Copyright (c) 2017 Cyberhaven +Copyright (c) 2017 Dependable Systems Laboratory, EPFL + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +import datetime +import json +import logging +import os +import re +import shutil + +from s2e_env.command import CommandError +from s2e_env.commands.recipe import Command as RecipeCommand +from s2e_env.manage import call_command +from s2e_env.utils.templates import render_template +from .abstract_project import AbstractProject + + +logger = logging.getLogger('new_project') + + +def _check_project_dir(project_dir, force=False): + """ + Check if a project directory with the given name already exists. + + If such a project exists, only continue if the ``force`` flag has been + specified. + """ + if not os.path.isdir(project_dir): + return + + if force: + logger.info('\'%s\' already exists - removing', + os.path.basename(project_dir)) + shutil.rmtree(project_dir) + else: + raise CommandError('\'%s\' already exists. Either remove this ' + 'project or use the force option' % + os.path.basename(project_dir)) + + +def is_valid_arch(target_arch, os_desc): + """ + Check that the image's architecture is consistent with the target binary. + """ + return not (target_arch == 'x86_64' and os_desc['arch'] != 'x86_64') + + +def _save_json_description(project_dir, config): + """ + Create a JSON description of the project. + + This information can be used by other commands. + """ + logger.info('Creating JSON description') + + project_desc_path = os.path.join(project_dir, 'project.json') + with open(project_desc_path, 'w') as f: + s = json.dumps(config, sort_keys=True, indent=4) + f.write(s) + + +class BaseProject(AbstractProject): + """ + Base class used by the ``new_project`` command to create a specific + project. The ``make_project`` method builds up a configuration dictionary + that is then used to generate the required files for the project. CGC, + Linux and Windows projects extend this class. These projects implement + methods to validate the configuration dictionary and do basic static + analysis on the target. + """ + + def __init__(self, bootstrap_template, lua_template): + super(BaseProject, self).__init__() + + self._bootstrap_template = bootstrap_template + self._lua_template = lua_template + + def _configure(self, target, *args, **options): + target_path = target.path + target_arch = target.arch + + if target.is_empty(): + logger.warn('Creating a project without a target file. You must ' + 'manually edit bootstrap.sh') + + # Decide on the image to be used + img_desc = self._select_image(target, options.get('image'), + options.get('download_image', False)) + + # Check architecture consistency (if the target has been specified) + if target_path and not is_valid_arch(target_arch, img_desc['os']): + raise CommandError('Binary is %s while VM image is %s. Please ' + 'choose another image' % (target_arch, + img_desc['os']['arch'])) + + # Determine if guestfs is available for this image + guestfs_path = self._select_guestfs(img_desc) + if not guestfs_path: + logger.warn('No guestfs available. The VMI plugin may not run optimally') + + # Generate the name of the project directory. The default project name + # is the target program name without any file extension + project_name = options.get('name') + if not project_name: + project_name, _ = os.path.splitext(os.path.basename(target_path)) + project_dir = self.env_path('projects', project_name) + + # Prepare the project configuration + config = { + 'creation_time': str(datetime.datetime.now()), + 'project_dir': project_dir, + 'image': img_desc, + 'target_path': target_path, + 'target_arch': target_arch, + 'target_args': options.get('target_args', []), + + # This contains paths to all the files that must be downloaded into + # the guest + 'target_files': ([target_path] if target_path else []) + target.aux_files, + + # List of module names that go into ModuleExecutionDetector + 'modules': [(os.path.basename(target_path), False)] if target_path else [], + + # List of binaries that go into ProcessExecutionDetector. These are + # normally executable files + 'processes': [os.path.basename(target_path)] if target_path else [], + + # Target arguments to be made symbolic + 'sym_args': options.get('sym_args', []), + + # See _create_bootstrap for an explanation of the @@ marker + 'use_symb_input_file': '@@' in options.get('target_args', []), + + # The use of seeds is specified on the command line + 'use_seeds': options.get('use_seeds', False), + 'seeds_dir': os.path.join(project_dir, 'seeds'), + + # The use of recipes is set by the specific project + 'use_recipes': False, + 'recipes_dir': os.path.join(project_dir, 'recipes'), + + # The use of guestfs is dependent on the specific image + 'has_guestfs': guestfs_path is not None, + 'guestfs_path': guestfs_path, + + # These options are determined by a static analysis of the target + 'dynamically_linked': False, + 'modelled_functions': False, + + # Specific projects can silence warnings in case they have specific + # hard-coded options + 'warn_seeds': True, + 'warn_input_file': True, + + # Searcher options + 'use_cupa': True, + + 'use_test_case_generator': True, + 'use_fault_injection': False, + + # This will add analysis overhead, so disable unless requested by + # the user. Also enabled by default for Decree targets. + 'enable_pov_generation': options.get('enable_pov_generation', False), + } + + # Do some basic analysis on the target (if it exists) + if target_path: + self._analyze_target(target, config) + + if config['enable_pov_generation']: + config['use_recipes'] = True + + # The config dictionary may be modified here. After this point the + # config dictionary should NOT be modified + self._finalize_config(config) + + return config + + def _create(self, config, force=False): + project_dir = config['project_dir'] + + # Check if the project directory already exists + _check_project_dir(project_dir, force) + + # Create the project directory + os.mkdir(project_dir) + + if config['use_seeds'] and not os.path.isdir(config['seeds_dir']): + os.mkdir(config['seeds_dir']) + + # Create symlinks to the target files (if they exist) + if config['target_files']: + self._symlink_project_files(project_dir, *config['target_files']) + + # Create a symlink to the guest tools directory + self._symlink_guest_tools(project_dir, config['image']) + + # Create a symlink to guestfs (if it exists) + if config['guestfs_path']: + self._symlink_guestfs(project_dir, config['guestfs_path']) + + # Render the templates + self._create_launch_script(project_dir, config) + self._create_lua_config(project_dir, config) + self._create_bootstrap(project_dir, config) + + # Save the project configuration as JSON + _save_json_description(project_dir, config) + + # Generate recipes for PoV generation + if config['use_recipes']: + os.makedirs(config['recipes_dir']) + call_command(RecipeCommand(), [], project=os.path.basename(project_dir)) + + # Display relevant messages to the user + display_marker_warning = config['target_path'] and \ + config['warn_input_file'] and \ + not (config['use_symb_input_file'] or config['sym_args']) + + if display_marker_warning: + logger.warning('You did not specify the input file marker @@. ' + 'This marker is automatically substituted by a ' + 'file with symbolic content. You will have to ' + 'manually edit the bootstrap file in order to run ' + 'the program on multiple paths.\n\n' + 'Example: %s @@\n\n' + 'You can also make arguments symbolic using the ' + '``S2E_SYM_ARGS`` environment variable in the ' + 'bootstrap file', config['target_path']) + + if config['use_seeds'] and not config['use_symb_input_file'] and config['warn_seeds']: + logger.warning('Seed files have been enabled, however you did not ' + 'specify an input file marker (i.e. \'@@\') to be ' + 'substituted with a seed file. This means that ' + 'seed files will be fetched but never used. Is ' + 'this intentional?') + + def _get_instructions(self, config): + instructions = render_template(config, 'instructions.txt') + + # Due to how templates work, there may be many useless new lines, + # remove them here + return re.sub(r'([\r\n][\r\n])+', r'\n\n', instructions) + + def _finalize_config(self, config): + """ + Validate and finalize a project's configuration options. + + This method may modify values in the ``config`` dictionary. If an + invalid configuration is found, a ``CommandError` should be thrown. + """ + pass + + def _analyze_target(self, target, config): + """ + Perform static analysis on the target binary. + + The results of this analysis can be used to add and/or modify values in + the ``config`` dictionary. + """ + pass + + def _create_launch_script(self, project_dir, config): + """ + Create the S2E launch script. + """ + logger.info('Creating launch script') + + context = { + 'creation_time': config['creation_time'], + 'env_dir': self.env_path(), + 'rel_image_path': os.path.relpath(config['image']['path'], self.env_path()), + 'qemu_arch': config['image']['qemu_build'], + 'qemu_memory': config['image']['memory'], + 'qemu_snapshot': config['image']['snapshot'], + 'qemu_extra_flags': config['image']['qemu_extra_flags'], + } + + template = 'launch-s2e.sh' + output_path = os.path.join(project_dir, template) + render_template(context, template, output_path, executable=True) + + def _create_lua_config(self, project_dir, config): + """ + Create the S2E Lua config. + """ + logger.info('Creating S2E configuration') + + self._copy_lua_library(project_dir) + + target_path = config['target_path'] + context = { + 'creation_time': config['creation_time'], + 'target': os.path.basename(target_path) if target_path else None, + 'target_lua_template': self._lua_template, + 'project_dir': project_dir, + 'use_seeds': config['use_seeds'], + 'use_cupa': config['use_cupa'], + 'use_test_case_generator': config['use_test_case_generator'], + 'enable_pov_generation': config['enable_pov_generation'], + 'seeds_dir': config['seeds_dir'], + 'has_guestfs': config['has_guestfs'], + 'guestfs_path': config['guestfs_path'], + 'recipes_dir': config['recipes_dir'], + 'target_files': [os.path.basename(tf) for tf in config['target_files']], + 'modules': config['modules'], + 'processes': config['processes'], + } + + for template in ('s2e-config.lua', 'models.lua'): + output_path = os.path.join(project_dir, template) + render_template(context, template, output_path) + + def _create_bootstrap(self, project_dir, config): + """ + Create the S2E bootstrap script. + """ + logger.info('Creating S2E bootstrap script') + + # The target arguments are specified using a format similar to the + # American Fuzzy Lop fuzzer. Options are specified as normal, however + # for programs that take input from a file, '@@' is used to mark the + # location in the target's command line where the input file should be + # placed. This will automatically be substituted with a symbolic file + # in the S2E bootstrap script. + parsed_args = ['${SYMB_FILE}' if arg == '@@' else arg + for arg in config['target_args']] + + target_path = config['target_path'] + context = { + 'creation_time': config['creation_time'], + 'target': os.path.basename(target_path) if target_path else None, + 'target_args': parsed_args, + 'sym_args': config['sym_args'], + 'target_bootstrap_template': self._bootstrap_template, + 'image_arch': config['image']['os']['arch'], + 'use_symb_input_file': config['use_symb_input_file'], + 'use_seeds': config['use_seeds'], + 'use_fault_injection': config['use_fault_injection'], + 'enable_pov_generation': config['enable_pov_generation'], + 'dynamically_linked': config['dynamically_linked'], + 'project_type': config['project_type'], + 'target_files': [os.path.basename(tf) for tf in config['target_files']], + 'modules': config['modules'], + 'processes': config['processes'], + } + + template = 'bootstrap.sh' + output_path = os.path.join(project_dir, template) + render_template(context, template, output_path) diff --git a/s2e_env/commands/project_creation/cgc_project.py b/s2e_env/commands/project_creation/cgc_project.py index 8812e2b1..d2c6e078 100644 --- a/s2e_env/commands/project_creation/cgc_project.py +++ b/s2e_env/commands/project_creation/cgc_project.py @@ -24,21 +24,24 @@ import logging from s2e_env.command import CommandError -from . import is_valid_arch, Project + +from .base_project import is_valid_arch, BaseProject logger = logging.getLogger('new_project') -class CGCProject(Project): +class CGCProject(BaseProject): def __init__(self): - super(CGCProject, self).__init__('cgc', 'bootstrap.cgc.sh', + super(CGCProject, self).__init__('bootstrap.cgc.sh', 's2e-config.cgc.lua') - def _is_valid_image(self, target_arch, target_path, os_desc): - return is_valid_arch(target_arch, os_desc) and 'decree' in os_desc['binary_formats'] + def _is_valid_image(self, target, os_desc): + return is_valid_arch(target.arch, os_desc) and 'decree' in os_desc['binary_formats'] + + def _finalize_config(self, config): + config['project_type'] = 'cgc' - def _validate_config(self, config): args = config.get('target_args', []) if args: raise CommandError('Command line arguments for Decree binaries ' diff --git a/s2e_env/commands/project_creation/linux_project.py b/s2e_env/commands/project_creation/linux_project.py index a6ff46d2..9871e74c 100644 --- a/s2e_env/commands/project_creation/linux_project.py +++ b/s2e_env/commands/project_creation/linux_project.py @@ -22,19 +22,22 @@ from s2e_env.analysis.elf import ELFAnalysis -from . import is_valid_arch, Project +from .base_project import is_valid_arch, BaseProject -class LinuxProject(Project): + +class LinuxProject(BaseProject): def __init__(self): - super(LinuxProject, self).__init__('linux', - 'bootstrap.linux.sh', + super(LinuxProject, self).__init__('bootstrap.linux.sh', 's2e-config.linux.lua') - def _is_valid_image(self, target_arch, target_path, os_desc): - return is_valid_arch(target_arch, os_desc) and 'elf' in os_desc['binary_formats'] + def _is_valid_image(self, target, os_desc): + return is_valid_arch(target.arch, os_desc) and 'elf' in os_desc['binary_formats'] - def _analyze_target(self, target_path, config): - with ELFAnalysis(target_path) as elf: + def _analyze_target(self, target, config): + with ELFAnalysis(target.path) as elf: config['dynamically_linked'] = elf.is_dynamically_linked() config['modelled_functions'] = elf.get_modelled_functions() + + def _finalize_config(self, config): + config['project_type'] = 'linux' diff --git a/s2e_env/commands/project_creation/target.py b/s2e_env/commands/project_creation/target.py new file mode 100644 index 00000000..50d5f867 --- /dev/null +++ b/s2e_env/commands/project_creation/target.py @@ -0,0 +1,237 @@ +""" +Copyright (c) 2017 Dependable Systems Laboratory, EPFL +Copyright (c) 2018 Adrian Herrera + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +import logging +import os +import re + +from magic import Magic + +from s2e_env.infparser.driver import Driver + +from .abstract_project import AbstractProject +from .cgc_project import CGCProject +from .linux_project import LinuxProject +from .windows_project import WindowsProject, WindowsDLLProject, WindowsDriverProject + + +logger = logging.getLogger('new_project') + +# Paths +FILE_DIR = os.path.dirname(__file__) +CGC_MAGIC = os.path.join(FILE_DIR, '..', '..', 'dat', 'cgc.magic') + +# Magic regexs +CGC_REGEX = re.compile(r'^CGC 32-bit') +ELF32_REGEX = re.compile(r'^ELF 32-bit') +ELF64_REGEX = re.compile(r'^ELF 64-bit') +DLL32_REGEX = re.compile(r'^PE32 executable \(DLL\)') +DLL64_REGEX = re.compile(r'^PE32\+ executable \(DLL\)') +WIN32_DRIVER_REGEX = re.compile(r'^PE32 executable \(native\)') +WIN64_DRIVER_REGEX = re.compile(r'^PE32\+ executable \(native\)') +PE32_REGEX = re.compile(r'^PE32 executable') +PE64_REGEX = re.compile(r'^PE32\+ executable') +MSDOS_REGEX = re.compile(r'^MS-DOS executable') + + +def _determine_arch_and_proj(target_path): + """ + Check that the given target is supported by S2E. + + The target's magic is checked to see if it is a supported file type (e.g. + ELF, PE, etc.). The architecture and operating system that the target was + compiled for (e.g., i386 Windows, x64 Linux, etc.) is also checked. + + Returns: + A tuple containing the target's architecture, operating system and a + project class. A tuple containing three ``None``s is returned on + failure. + """ + default_magic = Magic() + magic_checks = ( + (Magic(magic_file=CGC_MAGIC), CGC_REGEX, CGCProject, 'i386', 'decree'), + (default_magic, ELF32_REGEX, LinuxProject, 'i386', 'linux'), + (default_magic, ELF64_REGEX, LinuxProject, 'x86_64', 'linux'), + (default_magic, DLL32_REGEX, WindowsDLLProject, 'i386', 'windows'), + (default_magic, DLL64_REGEX, WindowsDLLProject, 'x86_64', 'windows'), + (default_magic, WIN32_DRIVER_REGEX, WindowsDriverProject, 'i386', 'windows'), + (default_magic, WIN64_DRIVER_REGEX, WindowsDriverProject, 'x86_64', 'windows'), + (default_magic, PE32_REGEX, WindowsProject, 'i386', 'windows'), + (default_magic, PE64_REGEX, WindowsProject, 'x86_64', 'windows'), + (default_magic, MSDOS_REGEX, WindowsProject, 'i386', 'windows'), + ) + + # Need to resolve symbolic links, otherwise magic will report the file type + # as being a symbolic link + target_path = os.path.realpath(target_path) + + # Check the target program against the valid file types + for magic_check, regex, proj_class, arch, operating_sys in magic_checks: + magic = magic_check.from_file(target_path) + + # If we find a match, create that project + if regex.match(magic): + return arch, operating_sys, proj_class + + return None, None, None + + +def _extract_inf_files(target_path): + """Extract Windows driver files from an INF file.""" + driver = Driver(target_path) + driver.analyze() + driver_files = driver.get_files() + if not driver_files: + raise TargetError('Driver has no files') + + base_dir = os.path.dirname(target_path) + + logger.info(' Driver files:') + file_paths = [] + for f in driver_files: + full_path = os.path.join(base_dir, f) + if not os.path.exists(full_path): + if full_path.endswith('.cat'): + logger.warn('Catalog file %s is missing', full_path) + continue + else: + raise TargetError('%s does not exist' % full_path) + + logger.info(' %s', full_path) + file_paths.append(full_path) + + return list(set(file_paths)) + + +class TargetError(Exception): + """An error occurred when creating a new S2E analysis target.""" + pass + + +class Target(object): + """ + Encapsulates a program (e.g., executable, driver, DLL, etc.) to be analyzed + by S2E. + """ + + @staticmethod + def from_file(path, project_class=None): + # Check that the target is a valid file + if not os.path.isfile(path): + raise TargetError('Target %s does not exist' % path) + + if path.endswith('.inf'): + logger.info('Detected Windows INF file, attempting to create a ' + 'driver project...') + driver_files = _extract_inf_files(path) + + first_sys_file = None + for f in driver_files: + if f.endswith('.sys'): + first_sys_file = f + + # TODO: prompt the user to select the right driver + if not first_sys_file: + raise TargetError('Could not find a *.sys file in the INF ' + 'file. Make sure that the INF file is valid ' + 'and belongs to a Windows driver') + + path_to_analyze = first_sys_file + aux_files = driver_files + else: + path_to_analyze = path + aux_files = [] + + arch, operating_sys, proj_class = _determine_arch_and_proj(path_to_analyze) + if not arch: + raise TargetError('Could not determine architecture for %s' % + path_to_analyze) + + # Overwrite the automatically-derived project class if one is provided + if project_class: + if not issubclass(project_class, AbstractProject): + raise TargetError('Custom projects must be a subclass of ' + '`AbstractProject`') + proj_class = project_class + + return Target(path, arch, operating_sys, proj_class, aux_files) + + @staticmethod + def empty(project_class): + """Create an empty target.""" + return Target(None, None, None, project_class) + + # pylint: disable=too-many-arguments + def __init__(self, path, arch, operating_sys, project_class, aux_files=None): + """ + This constructor should not be called directly. Rather, the + ``from_file`` or ``empty`` static methods should be used to create a + ``Target``. + """ + self._path = path + self._arch = arch + self._os = operating_sys + self._proj_class = project_class + + if not aux_files: + aux_files = [] + + self._aux_files = aux_files + + @property + def path(self): + """The path of the program under analysis.""" + return self._path + + @property + def arch(self): + """ + The architecture (e.g., i386, x86-64, etc.) of the program under + analysis. + """ + return self._arch + + @property + def operating_system(self): + """The operating system that the target executes on.""" + return self._os + + @property + def aux_files(self): + """ + A list of any auxillary files required by S2E to analysis the target + program. + """ + return self._aux_files + + def initialize_project(self): + """Initialize an s2e-env analysis project for this target.""" + return self._proj_class() + + def is_empty(self): + """Returns ``True`` if the target is an empty one.""" + return not self._path + + def __str__(self): + return 'Target(path=%s,arch=%s)' % (self._path, self._arch) diff --git a/s2e_env/commands/project_creation/windows_project.py b/s2e_env/commands/project_creation/windows_project.py index 4ac5d2cf..468b72fb 100644 --- a/s2e_env/commands/project_creation/windows_project.py +++ b/s2e_env/commands/project_creation/windows_project.py @@ -21,25 +21,29 @@ """ +import os import logging from s2e_env.analysis.pe import PEAnalysis from s2e_env.command import CommandError -from . import is_valid_arch, Project + +from .base_project import is_valid_arch, BaseProject logger = logging.getLogger('new_project') -class WindowsProject(Project): +class WindowsProject(BaseProject): def __init__(self, bootstrap_template='bootstrap.windows.sh'): - super(WindowsProject, self).__init__('windows', bootstrap_template, + super(WindowsProject, self).__init__(bootstrap_template, 's2e-config.windows.lua') - def _is_valid_image(self, target_arch, target_path, os_desc): - return is_valid_arch(target_arch, os_desc) and 'pe' in os_desc['binary_formats'] + def _is_valid_image(self, target, os_desc): + return is_valid_arch(target.arch, os_desc) and 'pe' in os_desc['binary_formats'] + + def _finalize_config(self, config): + config['project_type'] = 'windows' - def _validate_config(self, config): # Make all module names lower-case (in line with the WindowsMonitor plugin) config['modules'] = [(mod.lower(), kernel_mode) for mod, kernel_mode in config.get('modules', [])] @@ -48,14 +52,14 @@ class WindowsDLLProject(WindowsProject): def __init__(self): super(WindowsDLLProject, self).__init__('bootstrap.windows_dll.sh') - def _is_valid_image(self, target_arch, target_path, os_desc): - if not target_path.endswith('.dll'): + def _is_valid_image(self, target, os_desc): + if not target.path.endswith('.dll'): raise CommandError('Invalid DLL name - requires .dll extension') - return super(WindowsDLLProject, self)._is_valid_image(target_arch, target_path, os_desc) + return super(WindowsDLLProject, self)._is_valid_image(target, os_desc) - def _validate_config(self, config): - super(WindowsDLLProject, self)._validate_config(config) + def _finalize_config(self, config): + super(WindowsDLLProject, self)._finalize_config(config) # Not supported for DLLs config['processes'] = [] @@ -68,8 +72,8 @@ def _validate_config(self, config): logger.warn('No DLL entry point provided - defaulting to ``DllEntryPoint``') config['target_args'] = ['DllEntryPoint'] - def _analyze_target(self, target_path, config): - with PEAnalysis(target_path) as pe: + def _analyze_target(self, target, config): + with PEAnalysis(target.path) as pe: config['dll_exports'] = pe.get_exports() @@ -77,12 +81,21 @@ class WindowsDriverProject(WindowsProject): def __init__(self): super(WindowsDriverProject, self).__init__('bootstrap.windows_driver.sh') - def _is_valid_image(self, target_arch, target_path, os_desc): + def _is_valid_image(self, target, os_desc): # Windows drivers must match the OS's bit-ness - return os_desc['name'] == 'windows' and os_desc['arch'] == target_arch - - def _validate_config(self, config): - super(WindowsDriverProject, self)._validate_config(config) + return os_desc['name'] == 'windows' and os_desc['arch'] == target.arch + + def _finalize_config(self, config): + super(WindowsDriverProject, self)._finalize_config(config) + + # By default, the list of modules will only include the target program. + # However, for a Windows driver this may be an INF file, which is not a + # valid module. + # + # Instead, find all the *.sys files and add them to the module list. + sys_files = [os.path.basename(tf) for tf in config['target_files'] + if tf.endswith('.sys')] + config['modules'] = [(sys_file, True) for sys_file in sys_files] # Not supported for drivers config['processes'] = [] diff --git a/s2e_env/dat/config.yaml b/s2e_env/dat/config.yaml index 9ab46080..3f143982 100644 --- a/s2e_env/dat/config.yaml +++ b/s2e_env/dat/config.yaml @@ -111,13 +111,13 @@ required_versions: - 16 - 18 # images.json must have this version - guest_images: 2 + guest_images: 3 # Repo. Used for managing the S2E git repositories repo: url: https://storage.googleapis.com/git-repo-downloads/repo -# Directories that will automatically be built when running ``s2e init`` +# Directories that will automatically be created when running ``s2e init`` dirs: - build - images diff --git a/s2e_env/templates/library.lua b/s2e_env/dat/library.lua similarity index 100% rename from s2e_env/templates/library.lua rename to s2e_env/dat/library.lua diff --git a/s2e_env/execution_trace/trace_entries.py b/s2e_env/execution_trace/trace_entries.py index 1540047d..92520ee9 100644 --- a/s2e_env/execution_trace/trace_entries.py +++ b/s2e_env/execution_trace/trace_entries.py @@ -20,6 +20,8 @@ SOFTWARE. """ + +from abc import ABCMeta, abstractmethod import binascii import logging import re @@ -104,6 +106,9 @@ class TraceEntry(object): run-time size of the item **must** be provided. """ + # Abstract method + __metaclass__ = ABCMeta + FORMAT = None def __init__(self, fmt=''): @@ -158,6 +163,7 @@ def deserialize(cls, data, size=None): # pylint: disable=unused-argument except struct.error: raise TraceEntryError('Cannot deserialize %s data' % cls.__name__) + @abstractmethod def serialize(self): """ Serializes the object using the given ``_struct`` property. The user diff --git a/s2e_env/manage.py b/s2e_env/manage.py index 3004e042..dfab0eba 100644 --- a/s2e_env/manage.py +++ b/s2e_env/manage.py @@ -36,6 +36,7 @@ import sys from s2e_env.command import BaseCommand, CommandError, CommandParser +from s2e_env.utils import log COMMANDS_DIR = os.path.join(os.path.dirname(__file__), 'commands') @@ -184,6 +185,7 @@ def main(): Use the command manager to execute a command. """ + log.configure_logging() manager = CommandManager(sys.argv) manager.execute() diff --git a/s2e_env/symbols/__init__.py b/s2e_env/symbols/__init__.py index f39d74c1..6ccdd86e 100644 --- a/s2e_env/symbols/__init__.py +++ b/s2e_env/symbols/__init__.py @@ -20,6 +20,8 @@ SOFTWARE. """ + +from abc import ABCMeta, abstractmethod import json import logging import os @@ -43,6 +45,9 @@ class DebugInfo(object): It must be subclassed to handle specific formats (ELF, PE, etc.). """ + # Abstract class + __metaclass__ = ABCMeta + def __init__(self, path, search_paths=None): self._path = path self._search_paths = search_paths @@ -94,6 +99,7 @@ def get_coverage(self, addr_counts, include_covered_files_only=False): return file_line_info + @abstractmethod def parse(self): """ To be implemented by clients diff --git a/s2e_env/templates/bootstrap.sh b/s2e_env/templates/bootstrap.sh index a260f0ad..5e8a90c8 100644 --- a/s2e_env/templates/bootstrap.sh +++ b/s2e_env/templates/bootstrap.sh @@ -2,12 +2,12 @@ # # This file was automatically generated by s2e-env at {{ creation_time }} # -# This bootstrap file is used to control the execution of the Linux target -# program in S2E. +# This bootstrap script is used to control the execution of the target program +# in an S2E guest VM. # -# When you run launch-s2e.sh, the guest VM calls s2eget to fetch and execute this -# bootstrap script. This bootstrap script and the S2E config file determine how -# the target program is analyzed. +# When you run launch-s2e.sh, the guest VM calls s2eget to fetch and execute +# this bootstrap script. This bootstrap script and the S2E config file +# determine how the target program is analyzed. # set -x diff --git a/s2e_env/templates/instructions.txt b/s2e_env/templates/instructions.txt index e3761697..819b092b 100644 --- a/s2e_env/templates/instructions.txt +++ b/s2e_env/templates/instructions.txt @@ -18,7 +18,7 @@ include: {% endif %} {#############################################################################} -{% if dynamically_linked and image.group_name == 'linux' %} +{% if dynamically_linked and image.image_group == 'linux' %} s2e.so ====== @@ -113,4 +113,4 @@ If something does not run as expected, you can troubleshoot like this: * Run S2E in GDB using ./launch-s2e.sh debug ------- -Project {{ project_name }} created. +Project {{ project_dir }} created. diff --git a/s2e_env/templates/launch-s2e.sh b/s2e_env/templates/launch-s2e.sh index 0c46edf1..edf4f631 100644 --- a/s2e_env/templates/launch-s2e.sh +++ b/s2e_env/templates/launch-s2e.sh @@ -2,8 +2,8 @@ # # This file was automatically generated by s2e-env at {{ creation_time }} # -# This script can be used to run the S2E analysis. Additional QEMU command line -# arguments can be passed to this script at run time +# This script is used to run the S2E analysis. Additional QEMU command line +# arguments can be passed to this script at run time. # ENV_DIR="{{ env_dir }}" diff --git a/s2e_env/templates/s2e-config.lua b/s2e_env/templates/s2e-config.lua index 6296faf2..6c1ad11a 100644 --- a/s2e_env/templates/s2e-config.lua +++ b/s2e_env/templates/s2e-config.lua @@ -2,7 +2,8 @@ This is the main S2E configuration file ======================================= -This file was automatically generated by s2e-env at {{ creation_time }}. +This file was automatically generated by s2e-env at {{ creation_time }} + Changes can be made by the user where appropriate. ]]-- @@ -10,7 +11,7 @@ Changes can be made by the user where appropriate. -- This section configures the S2E engine. s2e = { logging = { - -- Possible values include "info", "warn", "debug", "none". + -- Possible values include "all", "debug", "info", "warn" and "none". -- See Logging.h in libs2ecore. console = "debug", logLevel = "debug", @@ -236,7 +237,7 @@ pluginsConfig.CUPASearcher = { -- Otherwise too frequent state switching may decrease performance. "batch", - {% if use_pov_generation %} + {% if enable_pov_generation %} -- This class is used with the Recipe plugin in order to prioritize -- states that have a high chance of containing a vulnerability. "group", diff --git a/s2e_env/templates/s2e.yaml b/s2e_env/templates/s2e.yaml index 7c91b34b..a563d1a3 100644 --- a/s2e_env/templates/s2e.yaml +++ b/s2e_env/templates/s2e.yaml @@ -6,12 +6,6 @@ # The s2e-env version version: {{ version }} -logging: - # One of the Python logging levels - debug, info, warning, error, critical - level: info - # Use colored output for logs - color: true - ida: # Path to the IDA Pro installation directory dir: /opt/ida-6.8 diff --git a/s2e_env/utils/images.py b/s2e_env/utils/images.py index e9b77432..9afb7c1a 100644 --- a/s2e_env/utils/images.py +++ b/s2e_env/utils/images.py @@ -105,9 +105,10 @@ def _validate_version(descriptor, filename): version = descriptor.get('version') required_version = CONSTANTS['required_versions']['guest_images'] if version != required_version: - raise CommandError('Need version %s for %s. Make sure that you have ' - 'the correct revision of the guest-images ' - 'repository' % (required_version, filename)) + raise CommandError('%s versions do not match (s2e-env: %.2f, image: ' + '%.2f). Make sure that you have the correct ' + 'revision of the guest-images repository' % + (filename, required_version, version)) def get_image_templates(img_build_dir): @@ -144,6 +145,8 @@ def get_image_descriptor(image_dir): ret['path'] = os.path.join(image_dir, 'image.raw.s2e') return ret - except Exception: - raise CommandError('Unable to open image description %s. Check that ' - 'the image exists, was built, or downloaded' % img_json_path) + except CommandError: + raise + except Exception, e: + raise CommandError('Unable to open image description %s: %s' % + (img_json_path, e)) diff --git a/s2e_env/utils/templates.py b/s2e_env/utils/templates.py index 9568f579..11bd5779 100644 --- a/s2e_env/utils/templates.py +++ b/s2e_env/utils/templates.py @@ -24,12 +24,13 @@ import os import stat -from jinja2 import Environment, FileSystemLoader +from jinja2 import Environment, FileSystemLoader, StrictUndefined from .memoize import memoize -TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), '..', 'templates') +DEFAULT_TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), '..', + 'templates') def _datetimefilter(value, format_='%H:%M %d-%m-%Y'): @@ -40,41 +41,45 @@ def _datetimefilter(value, format_='%H:%M %d-%m-%Y'): @memoize -def _init_template_env(templates_dir=TEMPLATES_DIR): +def _init_template_env(templates_dir=None): """ Initialize the jinja2 templating environment using the templates in the given directory. """ + if not templates_dir: + templates_dir = DEFAULT_TEMPLATES_DIR + env = Environment(loader=FileSystemLoader(templates_dir), - autoescape=False) + autoescape=False, undefined=StrictUndefined) env.filters['datetimefilter'] = _datetimefilter return env -def render_template(context, template, path=None, executable=False): +def render_template(context, template, output_path=None, templates_dir=None, + executable=False): """ Renders the ``template`` template with the given ``context``. The result is - written to ``path``. If ``path`` is not specified, the result is - returned as a string + returned as a string and written to ``output_path`` (if specified). + + A directory containing the Jinja templates can optionally be specified. """ - env = _init_template_env() - data = env.get_template(template).render(context) + env = _init_template_env(templates_dir) + + rendered_data = env.get_template(template).render(context) # Remove trailing spaces cleaned_lines = [] - for line in data.splitlines(): + for line in rendered_data.splitlines(): cleaned_lines.append(line.rstrip()) - data = '\n'.join(cleaned_lines) - - if not path: - return data + rendered_data = '\n'.join(cleaned_lines) - with open(path, 'w') as f: - f.write(data) + if output_path: + with open(output_path, 'w') as f: + f.write(rendered_data) - if executable: - st = os.stat(path) - os.chmod(path, st.st_mode | stat.S_IEXEC) + if executable: + st = os.stat(output_path) + os.chmod(output_path, st.st_mode | stat.S_IEXEC) - return True + return rendered_data diff --git a/setup.py b/setup.py index 6a30976a..c6a2f9dd 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ 'pytrie', 'pwntools==3.12.0' ], - test_requires=[ + tests_require=[ 'mock', ], packages=find_packages(), diff --git a/tests/commands/project_creation/__init__.py b/tests/commands/project_creation/__init__.py index 33c8ee1f..b0b98b38 100644 --- a/tests/commands/project_creation/__init__.py +++ b/tests/commands/project_creation/__init__.py @@ -22,6 +22,17 @@ import os +from tempfile import gettempdir + +from mock import MagicMock DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'dat') + + +def monkey_patch_project(project, img_desc): + """Monkey patch the given project to mock the appropriate values.""" + project._select_image = MagicMock(return_value=img_desc) + project._env_dir = MagicMock(return_value=gettempdir()) + + return project diff --git a/tests/commands/project_creation/test_cgc_project.py b/tests/commands/project_creation/test_cgc_project.py index 11710d63..ac6e17b1 100644 --- a/tests/commands/project_creation/test_cgc_project.py +++ b/tests/commands/project_creation/test_cgc_project.py @@ -25,10 +25,10 @@ from tempfile import gettempdir from unittest import TestCase -from mock import MagicMock +from s2e_env.commands.project_creation import CGCProject +from s2e_env.commands.project_creation import Target -from s2e_env.commands.project_creation.cgc_project import CGCProject -from . import DATA_DIR +from . import DATA_DIR, monkey_patch_project CGC_IMAGE_DESC = { @@ -58,21 +58,17 @@ class CGCProjectTestCase(TestCase): - def setUp(self): - self._cgc_project = CGCProject() - self._cgc_project._select_image = MagicMock(return_value=CGC_IMAGE_DESC) - self._cgc_project._env_dir = MagicMock(return_value=gettempdir()) - def test_empty_project_config(self): """Test empty CGC project creation.""" - args = { + target = Target.empty(CGCProject) + project = monkey_patch_project(target.initialize_project(), + CGC_IMAGE_DESC) + options = { 'image': 'cgc_debian-9.2.1-i386', 'name': 'test', - 'target_files': [], - 'target_arch': None, } - config = self._cgc_project._make_config(**args) + config = project._configure(target, **options) # Assert that we have actually created a CGC project self.assertEqual(config['project_type'], 'cgc') @@ -84,6 +80,7 @@ def test_empty_project_config(self): # Should be empty when no target is specified self.assertFalse(config['processes']) + self.assertFalse(config['modules']) # CGC binaries have no input files self.assertFalse(config['target_args']) @@ -106,12 +103,11 @@ def test_cadet0001_project_config(self): Test CGC project creation given a CGC binary and nothing else. No image, project name, etc. is provided. """ - args = { - 'target_files': [CADET_00001_PATH], - 'target_arch': 'i386', - } + target = Target.from_file(CADET_00001_PATH) + project = monkey_patch_project(target.initialize_project(), + CGC_IMAGE_DESC) - config = self._cgc_project._make_config(**args) + config = project._configure(target) # Assert that we have actually created a CGC project self.assertEqual(config['project_type'], 'cgc') @@ -121,6 +117,7 @@ def test_cadet0001_project_config(self): self.assertEqual(config['target_arch'], 'i386') self.assertListEqual(config['target_files'], [CADET_00001_PATH]) self.assertListEqual(config['processes'], [CADET_00001]) + self.assertListEqual(config['modules'], [(CADET_00001, False)]) # Assert that the CGC image has been selected self.assertDictEqual(config['image'], CGC_IMAGE_DESC) diff --git a/tests/commands/project_creation/test_linux_project.py b/tests/commands/project_creation/test_linux_project.py index 1ec36528..ed8edee5 100644 --- a/tests/commands/project_creation/test_linux_project.py +++ b/tests/commands/project_creation/test_linux_project.py @@ -25,10 +25,10 @@ from tempfile import gettempdir from unittest import TestCase -from mock import MagicMock +from s2e_env.commands.project_creation import LinuxProject +from s2e_env.commands.project_creation import Target -from s2e_env.commands.project_creation.linux_project import LinuxProject -from . import DATA_DIR +from . import DATA_DIR, monkey_patch_project LINUX_IMAGE_DESC = { @@ -58,21 +58,17 @@ class LinuxProjectTestCase(TestCase): - def setUp(self): - self._linux_project = LinuxProject() - self._linux_project._select_image = MagicMock(return_value=LINUX_IMAGE_DESC) - self._linux_project._env_dir = MagicMock(return_value=gettempdir()) - def test_empty_x86_project_config(self): """Test empty Linux x86 project creation.""" - args = { + target = Target.empty(LinuxProject) + project = monkey_patch_project(target.initialize_project(), + LINUX_IMAGE_DESC) + options = { 'image': 'debian-9.2.1-i386', 'name': 'test', - 'target_files': [], - 'target_arch': None, } - config = self._linux_project._make_config(**args) + config = project._configure(target, **options) # Assert that we have actually created a Linux project self.assertEqual(config['project_type'], 'linux') @@ -84,6 +80,7 @@ def test_empty_x86_project_config(self): # Should be empty when no target is specified self.assertFalse(config['processes']) + self.assertFalse(config['modules']) # An empty project with no target will have no arguments self.assertFalse(config['target_args']) @@ -101,12 +98,11 @@ def test_cat_x86_concrete_project_config(self): Test Linux project creation given a x86 binary (``cat``) and nothing else. No image, project name, symbolic arguments, etc. are provided. """ - args = { - 'target_files': [CAT_X86_PATH], - 'target_arch': 'i386', - } + target = Target.from_file(CAT_X86_PATH) + project = monkey_patch_project(target.initialize_project(), + LINUX_IMAGE_DESC) - config = self._linux_project._make_config(**args) + config = project._configure(target) self._assert_cat_x86_common(config) @@ -126,13 +122,14 @@ def test_cat_x86_symbolic_project_config(self): Test Linux project creation given a x86 binary (``cat``) and a symbolic file argument. """ - args = { - 'target_files': [CAT_X86_PATH], + target = Target.from_file(CAT_X86_PATH) + project = monkey_patch_project(target.initialize_project(), + LINUX_IMAGE_DESC) + options = { 'target_args': ['-T', '@@'], - 'target_arch': 'i386', } - config = self._linux_project._make_config(**args) + config = project._configure(target, **options) self._assert_cat_x86_common(config) @@ -157,6 +154,7 @@ def _assert_cat_x86_common(self, config): self.assertEqual(config['target_arch'], 'i386') self.assertListEqual(config['target_files'], [CAT_X86_PATH]) self.assertListEqual(config['processes'], [CAT_X86]) + self.assertListEqual(config['modules'], [(CAT_X86, False)]) # Assert that the x86 Linux image was selected self.assertDictEqual(config['image'], LINUX_IMAGE_DESC) diff --git a/tests/commands/project_creation/test_target.py b/tests/commands/project_creation/test_target.py new file mode 100644 index 00000000..7f143caa --- /dev/null +++ b/tests/commands/project_creation/test_target.py @@ -0,0 +1,120 @@ +""" +Copyright (c) 2018 Adrian Herrera + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +import os +from unittest import TestCase + +from s2e_env.commands.project_creation import CGCProject +from s2e_env.commands.project_creation import LinuxProject +from s2e_env.commands.project_creation import WindowsProject, \ + WindowsDLLProject, WindowsDriverProject +from s2e_env.commands.project_creation import Target + +from . import DATA_DIR + + +class TargetTestCase(TestCase): + def test_cgc_target(self): + """Test CGC executable target.""" + target_path = os.path.join(DATA_DIR, 'CADET_00001') + target = Target.from_file(target_path) + + self.assertEqual(target.path, target_path) + self.assertEqual(target.arch, 'i386') + self.assertEqual(target.operating_system, 'decree') + self.assertFalse(target.aux_files) + self.assertIsInstance(target.initialize_project(), CGCProject) + self.assertFalse(target.is_empty()) + + def test_empty_cgc_target(self): + """Test empty CGC target.""" + target = Target.empty(CGCProject) + + self.assertFalse(target.path) + self.assertFalse(target.arch) + self.assertFalse(target.operating_system) + self.assertFalse(target.aux_files) + self.assertIsInstance(target.initialize_project(), CGCProject) + self.assertTrue(target.is_empty()) + + def test_linux_i386_target(self): + """Test Linux i386 executable target.""" + target_path = os.path.join(DATA_DIR, 'cat') + target = Target.from_file(target_path) + + self.assertEqual(target.path, target_path) + self.assertEqual(target.arch, 'i386') + self.assertEqual(target.operating_system, 'linux') + self.assertFalse(target.aux_files) + self.assertIsInstance(target.initialize_project(), LinuxProject) + self.assertFalse(target.is_empty()) + + def test_windows_x86_64_target(self): + """Test Windows x86_64 executable target.""" + target_path = os.path.join(DATA_DIR, 'scanuser.exe') + target = Target.from_file(target_path) + + self.assertEqual(target.path, target_path) + self.assertEqual(target.arch, 'x86_64') + self.assertEqual(target.operating_system, 'windows') + self.assertFalse(target.aux_files) + self.assertIsInstance(target.initialize_project(), WindowsProject) + self.assertFalse(target.is_empty()) + + def test_windows_x86_64_dll_target(self): + """Test Windows x86_64 DLL target.""" + target_path = os.path.join(DATA_DIR, 'myputs.dll') + target = Target.from_file(target_path) + + self.assertEqual(target.path, target_path) + self.assertEqual(target.arch, 'x86_64') + self.assertEqual(target.operating_system, 'windows') + self.assertFalse(target.aux_files) + self.assertIsInstance(target.initialize_project(), WindowsDLLProject) + self.assertFalse(target.is_empty()) + + def test_windows_x86_64_sys_target(self): + """Test Windows x86_64 SYS driver target.""" + target_path = os.path.join(DATA_DIR, 'scanner.sys') + target = Target.from_file(target_path) + + self.assertEqual(target.path, target_path) + self.assertEqual(target.arch, 'x86_64') + self.assertEqual(target.operating_system, 'windows') + self.assertFalse(target.aux_files) + self.assertIsInstance(target.initialize_project(), WindowsDriverProject) + self.assertFalse(target.is_empty()) + + def test_windows_x86_64_inf_target(self): + """Test Windows x86_64 INF driver target.""" + target_path = os.path.join(DATA_DIR, 'scanner.inf') + target = Target.from_file(target_path) + + self.assertEqual(target.path, target_path) + self.assertEqual(target.arch, 'x86_64') + self.assertEqual(target.operating_system, 'windows') + self.assertItemsEqual(target.aux_files, + [os.path.join(DATA_DIR, 'scanner.sys'), + os.path.join(DATA_DIR, 'scanuser.exe')]) + self.assertIsInstance(target.initialize_project(), WindowsDriverProject) + self.assertFalse(target.is_empty()) diff --git a/tests/commands/project_creation/test_windows_project.py b/tests/commands/project_creation/test_windows_project.py index b52dea9b..49b9b455 100644 --- a/tests/commands/project_creation/test_windows_project.py +++ b/tests/commands/project_creation/test_windows_project.py @@ -25,11 +25,10 @@ from tempfile import gettempdir from unittest import TestCase -from mock import MagicMock +from s2e_env.commands.project_creation import Target +from s2e_env.commands.project_creation import WindowsProject -from s2e_env.commands.new_project import _extract_inf_files -from s2e_env.commands.project_creation.windows_project import WindowsProject, WindowsDLLProject, WindowsDriverProject -from . import DATA_DIR +from . import DATA_DIR, monkey_patch_project WINDOWS_XPSP3_X86_IMAGE_DESC = { @@ -94,29 +93,17 @@ class WindowsProjectTestCase(TestCase): - def setUp(self): - self._windows_project = WindowsProject() - self._windows_project._select_image = MagicMock(return_value=WINDOWS_XPSP3_X86_IMAGE_DESC) - self._windows_project._env_dir = MagicMock(return_value=gettempdir()) - - self._windows_driver_project = WindowsDriverProject() - self._windows_driver_project._select_image = MagicMock(return_value=WINDOWS_7SP1_X64_IMAGE_DESC) - self._windows_driver_project._env_dir = MagicMock(return_value=gettempdir()) - - self._windows_dll_project = WindowsDLLProject() - self._windows_dll_project._select_image = MagicMock(return_value=WINDOWS_7SP1_X64_IMAGE_DESC) - self._windows_dll_project._env_dir = MagicMock(return_value=gettempdir()) - def test_empty_xpsp3pro_project_config(self): """Test empty Windows XP SP3 project creation.""" - args = { + target = Target.empty(WindowsProject) + project = monkey_patch_project(target.initialize_project(), + WINDOWS_XPSP3_X86_IMAGE_DESC) + options = { 'image': 'windows-xpsp3pro-i386', 'name': 'test', - 'target_files': [], - 'target_arch': None, } - config = self._windows_project._make_config(**args) + config = project._configure(target, **options) # Assert that we have actually created a Windows project self.assertEqual(config['project_type'], 'windows') @@ -128,6 +115,7 @@ def test_empty_xpsp3pro_project_config(self): # Should be empty when no target is specified self.assertFalse(config['processes']) + self.assertFalse(config['modules']) # An empty project with no target will have no arguments self.assertFalse(config['target_args']) @@ -142,13 +130,11 @@ def test_empty_xpsp3pro_project_config(self): def test_scanner_driver_7sp1ent_x64_project_config(self): """Test x64 driver project creation.""" - driver_files = _extract_inf_files(SCANNER_INF_PATH) - args = { - 'target_files': [SCANNER_INF_PATH] + driver_files, - 'target_arch': 'x86_64', - } + target = Target.from_file(SCANNER_INF_PATH) + project = monkey_patch_project(target.initialize_project(), + WINDOWS_7SP1_X64_IMAGE_DESC) - config = self._windows_driver_project._make_config(**args) + config = project._configure(target) # Assert that we've actually created a Windows project self.assertEqual(config['project_type'], 'windows') @@ -157,7 +143,9 @@ def test_scanner_driver_7sp1ent_x64_project_config(self): self.assertEqual(config['target_path'], SCANNER_INF_PATH) self.assertEqual(config['target_arch'], 'x86_64') self.assertItemsEqual(config['target_files'], - [SCANNER_INF_PATH, SCANNER_SYS_PATH, SCANNER_USER_EXE_PATH]) + [SCANNER_INF_PATH, SCANNER_SYS_PATH, + SCANNER_USER_EXE_PATH]) + self.assertItemsEqual(config['modules'], [(SCANNER_SYS, True)]) # Assert that the x86_64 Windows 7 image was selected self.assertDictEqual(config['image'], WINDOWS_7SP1_X64_IMAGE_DESC) @@ -173,13 +161,14 @@ def test_scanner_driver_7sp1ent_x64_project_config(self): def test_myputs_dll_7sp1ent_x64_project_config(self): """Test x64 DLL project creation.""" - args = { - 'target_files': [MYPUTS_DLL_PATH], + target = Target.from_file(MYPUTS_DLL_PATH) + project = monkey_patch_project(target.initialize_project(), + WINDOWS_7SP1_X64_IMAGE_DESC) + options = { 'target_args': ['MyPuts'], - 'target_arch': 'x86_64', } - config = self._windows_dll_project._make_config(**args) + config = project._configure(target, **options) # Assert that we have actually created a Windows project self.assertEqual(config['project_type'], 'windows') @@ -188,6 +177,7 @@ def test_myputs_dll_7sp1ent_x64_project_config(self): self.assertEqual(config['target_path'], MYPUTS_DLL_PATH) self.assertEqual(config['target_arch'], 'x86_64') self.assertItemsEqual(config['target_files'], [MYPUTS_DLL_PATH]) + self.assertListEqual(config['modules'], [(MYPUTS_DLL, False)]) # Assert that the x86_64 Windows 7 image was selected self.assertDictEqual(config['image'], WINDOWS_7SP1_X64_IMAGE_DESC)