Beispiel #1
0
    def __init__(self, config=None, api=None):
        self.api = api
        self.api.internal_engine = self
        if config is None:
            config = _generate_without_binary()
        if isinstance(config, str):
            config = generate_config(config)
        self.config = config

        # OS plugins place OS-specific, system-wide, functionality
        # in engine.zos

        class ZOS(object):
            def __init__(self):
                pass

        self.zos = ZOS()

        binary = config.filename

        # Set provided arguments
        self.original_binary = binary
        self.cmdline_args = ([] if config.cmdline_args is None else
                             config.cmdline_args)

        self.random_file_name = getattr(config, "random_file_name", False)

        self.log_level = getattr(logging, config.log.upper(), None)
        if not isinstance(self.log_level, int):
            raise ValueError("Invalid log level: %s" % config.log)

        # To get different logs based on the level, read this:
        # https://stackoverflow.com/questions/14844970
        #   /modifying-logging-message-format-based-on-message-
        #   logging-level-in-python3
        self._init_logging(self.log_level)

        # If verbose is true, print lots of info, including every
        # instruction
        self.original_file_name = ""
        self.main_module_name = ""
        self.main_module = None

        self.date = "2019-02-02"

        self.timer = util.Timer()

        self.hook_manager = HookManager(self, self.api)
        self.breakpoints = BreakpointManager(self.hook_manager)
        self.interrupt_handler = InterruptHooks(self.hook_manager, self)
        self.exception_handler = ExceptionHooks(self)
        self.files = FileSystem(self, self.hook_manager)
        self.processes = Processes(
            self.hook_manager,
            self.interrupt_handler,
            self.files,
            self.original_binary,
            self.STACK_SIZE,
            disableNX=self.config.disableNX,
        )

        self.os_plugins = OSPlugins(self)

        if binary is not None and binary != "":
            self.load_executable(binary, entrypoint_override=config.startat)
        else:
            self._initialize_zelos()  # For testing purposes.
            # If no binary is passed, default to UNIX-style paths.
            self.files.setup("/")

        head, tail = ntpath.split(config.filename)
        original_filename = tail or ntpath.basename(head)
        self.original_file_name = original_filename
        self.date = config.date

        if config.dns > 0:
            self.flags_dns = True

        if config.writetrace != "":
            target_addr = int(config.writetrace, 16)
            self.set_writetrace(target_addr)
        if config.memlimit > 0:
            self.set_mem_limit(config.memlimit)
        for m in config.mount:
            try:
                arch, dest, src = m.split(",")
                # TODO: Use arch to determine when to mount
                if os.path.isdir(src):
                    self.files.mount_folder(src, emulated_path=dest)
                else:
                    self.files.add_file(src, emulated_path=dest)
            except ValueError:
                self.logger.error(
                    f"Incorrectly formatted input to '--mount': {m}")
                continue
        if config.strace is not None:
            self.zos.syscall_manager.set_strace_file(config.strace)
Beispiel #2
0
class Engine:
    # Stack size
    # we need at least 0x80000 for some malware:
    # (ff07b93686aca11c9b3484f43b8b910306f30b52cc1c01638bfc16960038dd75)
    STACK_SIZE = 0x90000

    def __init__(self, config=None, api=None):
        self.api = api
        self.api.internal_engine = self
        if config is None:
            config = _generate_without_binary()
        if isinstance(config, str):
            config = generate_config(config)
        self.config = config

        # OS plugins place OS-specific, system-wide, functionality
        # in engine.zos

        class ZOS(object):
            def __init__(self):
                pass

        self.zos = ZOS()

        binary = config.filename

        # Set provided arguments
        self.original_binary = binary
        self.cmdline_args = ([] if config.cmdline_args is None else
                             config.cmdline_args)

        self.random_file_name = getattr(config, "random_file_name", False)

        self.log_level = getattr(logging, config.log.upper(), None)
        if not isinstance(self.log_level, int):
            raise ValueError("Invalid log level: %s" % config.log)

        # To get different logs based on the level, read this:
        # https://stackoverflow.com/questions/14844970
        #   /modifying-logging-message-format-based-on-message-
        #   logging-level-in-python3
        self._init_logging(self.log_level)

        # If verbose is true, print lots of info, including every
        # instruction
        self.original_file_name = ""
        self.main_module_name = ""
        self.main_module = None

        self.date = "2019-02-02"

        self.timer = util.Timer()

        self.hook_manager = HookManager(self, self.api)
        self.breakpoints = BreakpointManager(self.hook_manager)
        self.interrupt_handler = InterruptHooks(self.hook_manager, self)
        self.exception_handler = ExceptionHooks(self)
        self.files = FileSystem(self, self.hook_manager)
        self.processes = Processes(
            self.hook_manager,
            self.interrupt_handler,
            self.files,
            self.original_binary,
            self.STACK_SIZE,
            disableNX=self.config.disableNX,
        )

        self.os_plugins = OSPlugins(self)

        if binary is not None and binary != "":
            self.load_executable(binary, entrypoint_override=config.startat)
        else:
            self._initialize_zelos()  # For testing purposes.
            # If no binary is passed, default to UNIX-style paths.
            self.files.setup("/")

        head, tail = ntpath.split(config.filename)
        original_filename = tail or ntpath.basename(head)
        self.original_file_name = original_filename
        self.date = config.date

        if config.dns > 0:
            self.flags_dns = True

        if config.writetrace != "":
            target_addr = int(config.writetrace, 16)
            self.set_writetrace(target_addr)
        if config.memlimit > 0:
            self.set_mem_limit(config.memlimit)
        for m in config.mount:
            try:
                arch, dest, src = m.split(",")
                # TODO: Use arch to determine when to mount
                if os.path.isdir(src):
                    self.files.mount_folder(src, emulated_path=dest)
                else:
                    self.files.add_file(src, emulated_path=dest)
            except ValueError:
                self.logger.error(
                    f"Incorrectly formatted input to '--mount': {m}")
                continue
        if config.strace is not None:
            self.zos.syscall_manager.set_strace_file(config.strace)

    def __del__(self):
        try:
            self.processes.handles.close_all()
        except Exception as e:
            print("Engine: could not close handles:", e)

    def _init_logging(self, initial_log_level):
        if initial_log_level is None:
            initial_log_level = verboselogs.logging.INFO
        verboselogs.install()
        # This will be the parent to all loggers in this project.
        logger = verboselogs.logging.getLogger("zelos")
        fmt = "{asctime}:{module:_<10.10s}:{levelname:_<6.6s}:{message}"
        datefmt = "%H:%M:%S"
        try:
            import coloredlogs

            coloredlogs.install(
                logger=logger,
                level=initial_log_level,
                fmt=fmt,
                datefmt=datefmt,
                style="{",
            )
        except ModuleNotFoundError:
            logger.error("You do not have the required coloredlogs dependency")
            console = verboselogs.logging.StreamHandler()
            # set a format which is simpler for console use
            formatter = verboselogs.logging.Formatter(fmt, datefmt, style="{")
            console.setFormatter(formatter)
            logger.addHandler(console)

        self.logger = logger

    def set_log_level(self, log_level):
        fmt = "{asctime}:{module:_<10.10s}:{levelname:_<6.6s}:{message}"
        datefmt = "%H:%M:%S"
        try:
            import coloredlogs

            coloredlogs.install(
                logger=self.logger,
                reconfigure=True,
                level=log_level,
                fmt=fmt,
                datefmt=datefmt,
                style="{",
            )
        except ModuleNotFoundError:
            self.logger.setLevel(log_level)

    def hexdump(self, address: int, size: int) -> None:
        import hexdump

        try:
            data = self.memory.read(address, size)
            hexdump.hexdump(data)
        except Exception:
            self.logger.exception("Invalid address range.")

    @property
    def current_process(self):
        return self.processes.current_process

    @property
    def emu(self):
        return self.current_process.emu

    @property
    def memory(self):
        return self.current_process.memory

    @property
    def scheduler(self):
        return self.current_process.scheduler

    @property
    def thread_manager(self):
        return self.current_process.threads

    @property
    def current_thread(self):
        return self.current_process.current_thread

    @property
    def loader(self):
        return self.current_process.loader

    @loader.setter
    def loader(self, loader):
        self.current_process.loader = loader

    @property
    def modules(self):
        return self.current_process.modules

    @property
    def handles(self):
        return self.processes.handles

    def set_mem_limit(self, limit_in_mb: int) -> None:
        limit = limit_in_mb * 1024 * 1024
        """ Sets the memory limit for the python process"""
        try:
            import resource

            soft, hard = resource.getrlimit(resource.RLIMIT_AS)
            resource.setrlimit(resource.RLIMIT_AS, (limit, hard))
        except ModuleNotFoundError:
            self.logger.error("Unable to set memory limit in Windows")

    def set_writetrace(self, target):
        def hook(zelos, access, address, size, value):
            if address == target:
                self.logger.error(
                    "[WRITE 0x%x] EIP: 0x%x: value 0x%x" %
                    (address, self.current_thread.getIP(), value))

        self.hook_manager.register_mem_hook(HookType.MEMORY.WRITE,
                                            hook,
                                            name="write_trace")

    def _first_parse(self, module_path, random_file_name=False):
        """ Function to parse an executable """

        if random_file_name:
            self.original_file_name = module_path
            original_file_name = module_path
            # To ensure we don't get any issues with the size of the
            # file name, we copy the file and rename it 'target'
            fd, temp_path = mkstemp(dir=".", suffix=".xex")
            os.close(fd)
            temp_filename = os.path.basename(temp_path)
            copyfile(module_path, temp_filename)
            module_path = temp_filename
            self.hook_manager.register_close_hook(
                functools.partial(os.remove, temp_filename))
            self.logger.debug(f"Setting random file name for "
                              f"{original_file_name} : {module_path}")

        self.logger.verbose("Parse Main Module")

        with open(module_path, "rb") as f:
            file_data = bytearray(f.read())
        if file_data.startswith(b"ZENC"):
            file_data = util.in_mem_decrypt(file_data)

        return self._parse_file_data(module_path, file_data)

    def parse_file(self, filename):
        with open(filename, "rb") as f:
            file_data = bytearray(f.read())
        return self._parse_file_data(filename, file_data)

    def _parse_file_data(self, filename, filedata):
        parsed_file = self.os_plugins.parse(filename, filedata)

        if parsed_file is not None:
            assert len(parsed_file.Data) > 0, "File has no data"
            return parsed_file
        raise UnsupportedBinaryError(f"{filename} is unsupported file format")

    def load_executable(self, module_path, entrypoint_override=None):
        """
        This method simply loads the executable, without starting the
        emulation
        """

        original_file_name = os.path.basename(module_path)
        self.original_file_name = original_file_name

        file = self._first_parse(module_path,
                                 random_file_name=self.random_file_name)

        module_path = file.Filepath
        self.main_module = file
        self._initialize_zelos(file)

        self.os_plugins.load(file,
                             self.current_process,
                             entrypoint_override=entrypoint_override)

        # TODO: don't let this be in loader and zelos
        self.main_module_name = self.loader.main_module_name

        # We need to create this file in the file system, so that other
        # files can access it.
        self.files.create_file(self.files.zelos_file_prefix + module_path)

    def _initialize_zelos(self, binary=None):
        self.state = State(self, binary, self.date)

        cs_arch_mode_sm_dict = {
            "x86": (CS_ARCH_X86, CS_MODE_32),
            "x86_64": (CS_ARCH_X86, CS_MODE_64),
            "arm": (CS_ARCH_ARM, CS_MODE_ARM),
            "mips": (CS_ARCH_MIPS, CS_MODE_MIPS32),
        }

        arch = self.state.arch
        (cs_arch, cs_mode) = cs_arch_mode_sm_dict[arch]

        endianness = self.state.endianness
        if endianness == "little":
            cs_mode |= CS_MODE_LITTLE_ENDIAN
        elif endianness == "big":
            cs_mode |= CS_MODE_BIG_ENDIAN
        else:
            raise ZelosLoadException(f"Unsupported endianness {endianness}")
        self.cs = Cs(cs_arch, cs_mode)
        self.cs.detail = True

        self.logger.debug(
            f"Initialized {arch} {self.state.bits} emulator/disassembler")

        self.triggers = Triggers(self)
        self.processes.set_architecture(self.state)

        self.network = Network(self.helpers, self.files, None)

        self.processes._create_first_process(self.main_module_name)
        p = self.current_process
        p.cmdline_args = self.cmdline_args
        p.environment_variables = self.config.env_vars
        p.virtual_filename = self.config.virtual_filename
        p.virtual_path = self.config.virtual_path

        if hasattr(unicorn.unicorn, "WITH_ZEROPOINT_PATCH"):

            def process_switch_wrapper(*args, **kwargs):
                # Block count interrupt. Fires every 2^N blocks executed
                # Use this as an opportunity to swap threads.
                self.logger.info(">>> Tracing Thread Swap Opportunity")
                self.processes.schedule_next()

            self.interrupt_handler.register_interrupt_handler(
                0xF8F8F8F8, process_switch_wrapper)

        if self.config.filename is not None and self.config.filename != "":
            if (self.config.virtual_filename is not None
                    and self.config.virtual_filename != ""):
                self.files.add_file(self.config.filename,
                                    self.config.virtual_filename)
            else:
                self.files.add_file(self.config.filename)

        # TODO: SharedSection needs to be removed
        self.processes.handles.new("section", "\\Windows\\SharedSection")

    @property
    def helpers(self):
        """
        Helpers are the first layer in the components hierarchy, which
        mainly deal with providing help to developers.
        """
        helpers_class = namedtuple(
            "Helpers", ["handles", "triggers", "state", "processes"])
        return helpers_class(self.handles, self.triggers, self.state,
                             self.processes)

    def load_library(self, module_name):
        binary, _ = self.loader._load_module(module_name, depth=1)
        return binary

    def disas(self, address: int, size: int):
        """
        Disassemble code at the given address, for up to size bytes
        """
        code = self.memory.read(address, size)
        return [insn for insn in self.cs.disasm(bytes(code), address)]

    def step(self, count: int = 1) -> None:
        """ Steps one assembly level instruction """
        # You might be tempted to use unicorn's "count" argument to
        # step. However, printing instruction comments relies on an
        # ad-hoc "post instruction" method.
        #
        # Using unicorn's emu_start count argument
        #   run INST hook
        #   run instruction
        #   unicorn stops
        #
        # Current method:
        #   run INST hook (don't print)
        #   run instruction
        #   run INST hook (do print) then stop before next instruction
        #   unicorn stops
        #
        # Of course, we can simplify when we get a post instruction
        # hook working properly.

        inst_count = 0

        def step_n(zelos, addr, size):
            nonlocal inst_count
            inst_count += 1
            if inst_count > count:
                self.scheduler.stop("step")

        def quit_step_n():
            nonlocal inst_count
            return inst_count > count

        self.hook_manager.register_exec_hook(HookType.EXEC.INST,
                                             step_n,
                                             end_condition=quit_step_n)
        return self.start(swap_threads=False)

    def step_over(self, count: int = 1) -> None:
        """
        Steps on assembly level instruction up to count instructions
        """
        for i in range(count):
            if not self._step_over():
                return

    def _step_over(self):
        """Returns True if it successfully stepped."""
        max_inst_size = 15
        insts = self.disas(self.emu.getIP(), max_inst_size)
        if len(insts) == 0:
            self.logger.notice(f"Unable to disassemble 0x{self.emu.getIP():x}")
            return False
        i = insts[0]
        if insts[0].group(CS_GRP_CALL):
            self.plugins.runner.run_to_addr(i.address + i.size)
        else:
            self.step()
        return True

    def start(self, timeout=0, swap_threads=True) -> Optional[BreakState]:
        """
        Starts execution of the program at the given offset or entry
        point.
        """
        if timeout > 0:
            self.timer.begin(timeout)

            def timeout_hook(zelos, addr, size):
                self._check_timeout()

            # TODO: Delete timeout hook after timeout is triggered.
            self.hook_manager.register_exec_hook(HookType.EXEC.BLOCK,
                                                 timeout_hook,
                                                 name="timeout_hook")

        if self.processes.num_active_processes() == 0:
            self.processes.logger.info(
                "No more processes or threads to execute.")
            return None

        self.ehCount = 0

        # Main emulated execution loop
        while self._should_continue():
            if self.current_thread is None:
                self.processes.swap_with_next_thread()

            self.plugins.trace.should_print_last_instruction = False
            self.plugins.trace.last_instruction = self.emu.getIP()
            self.plugins.trace.last_instruction_size = 1

            try:
                if self.processes.num_active_processes() == 0:
                    self.processes.logger.info(
                        "No more processes or threads to execute.")
                else:
                    # Execute until emulator exception
                    self._run(self.current_process)
            except UcError as e:
                # TODO: This is a special case for forcing a stop.
                # Sometimes setting a stop reason doesn't stop
                # execution (especially when changingEIP).
                # This is a hack. Fix me plz
                if self.current_thread is not None and not (
                        self.emu.getIP() == 0x30
                        and "kill thread" in self.scheduler.end_reasons):
                    self.exception_handler.handle_exception(e)

            # If we get here and there are no end_reasons this is
            # because emu ended early. If we have swap thread set, this
            # is because this is a signal to zelos to swap threads.
            # Otherwise, this is a signal that execution is over
            # (for example, stepping)
            if not self.scheduler._has_end_reasons():
                if not swap_threads:
                    break
                self.processes.swap_with_next_thread()

        return self.zos.syscall_manager.generate_break_state()

    def _run(self, p):
        t = p.current_thread
        assert (t is not None
                ), "Current thread is None. Something has gone horribly wrong."

        self.breakpoints._disable_breakpoints_on_start(t.getIP())
        if t.emu.is_running:
            self.logger.critical(
                "Trying to run unicorn while unicorn is already running. "
                "You are entering untested waters")

        try:
            t.emu.emu_start(t.getIP(), 0)
        finally:
            stop_addr = p.threads.scheduler._pop_stop_addr(t.id)
            self.hook_manager._clear_deleted_hooks()

        # Only set the stop addr if you stopped benignly
        if stop_addr is not None:
            t.setIP(stop_addr)

    def _should_continue(self):
        """
        Takes the reasons for ending unicorn execution, and decides
        whether to continue or end execution
        """

        if self.current_thread is None:
            self.processes.swap_with_next_thread()

        if self.scheduler._resolve_end_reasons() is False:
            return False

        if self.processes.num_active_processes() == 0:
            return False

        # Keep running unless told otherwise.
        return True

    def close(self) -> None:
        """ Handles the end of the run command """
        for closure in self.hook_manager._get_hooks(HookType._OTHER.CLOSE):
            try:
                closure()
            except Exception:
                self.logger.exception("Exception while trying to close Zelos")

    def _dbgprint(self, address):
        service = self.emu.get_reg("eax")
        if service == 1:  # DbgPrint functionality
            length = self.emu.get_reg("edx")
            buffer = self.emu.get_reg("ecx")
            buffer_s = self.memory.read_string(buffer, length)
            print("[DBGPRINT SYSCALL] {0}".format(buffer_s))
        else:
            self.logger.info(">>> Tracing DebugService at 0x%x Routine 0x%x" %
                             (address, service))

    def _check_timeout(self):
        if self.timer.is_timed_out():
            self.scheduler.stop("timeout")

    # Estimates the number of function arguments with the assumption
    # that the callee is responsible for cleaning up the stack.
    # Disassembles insts linearly until a RETN instruction is
    # encountered. The RETN operand indicates the number of stack bytes
    # the caller had pushed as arguments.

    def _estimate_function_stack_adjustment(self, function_start_address):
        address = function_start_address
        while True:
            code = self.emu.mem_read(address, 1000)
            for insn in self.cs.disasm(str(code), address):
                if insn.mnemonic != "ret":
                    address += insn.size
                    continue
                if len(insn.operands) == 0:
                    return 0  # no stack adjustment
                # imm bytes popped by this function
                return insn.operands[0].imm