From cedc4a8ae96514580ad2d82b24219e7d7d0e575f Mon Sep 17 00:00:00 2001 From: mariusmue Date: Tue, 13 Jun 2017 13:10:03 +0200 Subject: [PATCH] Initial commit --- .gitignore | 102 ++++ .gitmodules | 6 + LICENSE | 190 +++++++ README.md | 27 + avatar2/__init__.py | 14 + avatar2/archs/__init__.py | 2 + avatar2/archs/arm.py | 15 + avatar2/archs/x86.py | 55 +++ avatar2/avatar2.py | 306 ++++++++++++ avatar2/memory_range.py | 24 + avatar2/message.py | 45 ++ avatar2/peripherals/__init__.py | 2 + avatar2/peripherals/avatar_peripheral.py | 44 ++ avatar2/peripherals/nucleo_usart.py | 136 +++++ avatar2/plugins/__init__.py | 0 avatar2/plugins/instruction_forwarder.py | 40 ++ avatar2/plugins/orchestrator.py | 165 +++++++ avatar2/protocols/__init__.py | 0 avatar2/protocols/gdb.py | 600 +++++++++++++++++++++++ avatar2/protocols/openocd.py | 109 ++++ avatar2/protocols/qmp.py | 148 ++++++ avatar2/protocols/remote_memory.py | 182 +++++++ avatar2/targets/__init__.py | 7 + avatar2/targets/dummy_target.py | 140 ++++++ avatar2/targets/gdb_target.py | 52 ++ avatar2/targets/openocd_target.py | 71 +++ avatar2/targets/panda_target.py | 108 ++++ avatar2/targets/qemu_target.py | 175 +++++++ avatar2/targets/target.py | 253 ++++++++++ avatar2/watchmen.py | 109 ++++ docs/Makefile | 20 + docs/source/avatar2.archs.rst | 30 ++ docs/source/avatar2.peripherals.rst | 30 ++ docs/source/avatar2.plugins.rst | 30 ++ docs/source/avatar2.protocols.rst | 46 ++ docs/source/avatar2.rst | 57 +++ docs/source/avatar2.targets.rst | 62 +++ docs/source/conf.py | 158 ++++++ docs/source/index.rst | 21 + docs/source/modules.rst | 7 + handbook/0x01_intro.md | 133 +++++ handbook/0x02_targets.md | 136 +++++ handbook/0x03_memory.md | 122 +++++ handbook/0x04_execution.md | 152 ++++++ handbook/0x05_watchmen.md | 39 ++ setup.py | 22 + targets/build_panda.sh | 22 + targets/build_qemu.sh | 22 + targets/src/avatar-panda | 1 + targets/src/avatar-qemu | 1 + 50 files changed, 4238 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 README.md create mode 100644 avatar2/__init__.py create mode 100644 avatar2/archs/__init__.py create mode 100644 avatar2/archs/arm.py create mode 100644 avatar2/archs/x86.py create mode 100644 avatar2/avatar2.py create mode 100644 avatar2/memory_range.py create mode 100644 avatar2/message.py create mode 100644 avatar2/peripherals/__init__.py create mode 100644 avatar2/peripherals/avatar_peripheral.py create mode 100644 avatar2/peripherals/nucleo_usart.py create mode 100644 avatar2/plugins/__init__.py create mode 100644 avatar2/plugins/instruction_forwarder.py create mode 100644 avatar2/plugins/orchestrator.py create mode 100644 avatar2/protocols/__init__.py create mode 100644 avatar2/protocols/gdb.py create mode 100644 avatar2/protocols/openocd.py create mode 100644 avatar2/protocols/qmp.py create mode 100644 avatar2/protocols/remote_memory.py create mode 100644 avatar2/targets/__init__.py create mode 100644 avatar2/targets/dummy_target.py create mode 100644 avatar2/targets/gdb_target.py create mode 100644 avatar2/targets/openocd_target.py create mode 100644 avatar2/targets/panda_target.py create mode 100644 avatar2/targets/qemu_target.py create mode 100644 avatar2/targets/target.py create mode 100644 avatar2/watchmen.py create mode 100644 docs/Makefile create mode 100644 docs/source/avatar2.archs.rst create mode 100644 docs/source/avatar2.peripherals.rst create mode 100644 docs/source/avatar2.plugins.rst create mode 100644 docs/source/avatar2.protocols.rst create mode 100644 docs/source/avatar2.rst create mode 100644 docs/source/avatar2.targets.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/modules.rst create mode 100644 handbook/0x01_intro.md create mode 100644 handbook/0x02_targets.md create mode 100644 handbook/0x03_memory.md create mode 100644 handbook/0x04_execution.md create mode 100644 handbook/0x05_watchmen.md create mode 100644 setup.py create mode 100755 targets/build_panda.sh create mode 100755 targets/build_qemu.sh create mode 160000 targets/src/avatar-panda create mode 160000 targets/src/avatar-qemu diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..6e122442263 --- /dev/null +++ b/.gitignore @@ -0,0 +1,102 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Emacs backup files +*~ +\#*\# +.\#* + +# Vim swap files +*.swp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..e045dfa7833 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "targets/src/avatar-qemu"] + path = targets/src/avatar-qemu + url = ../avatar-qemu.git +[submodule "targets/src/avatar-panda"] + path = targets/src/avatar-panda + url = ../avatar-panda.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..1597a8f0537 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2017 Marius Muench & Dario Nisi, EURECOM + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000000..0bd6625a7ec --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +Welcome to avatar², the target orchestration framework with focus on dynamic + analysis of embedded devices' firmware! + +Avatar² is developed and maintained by [Eurecom's S3 Group](http://s3.eurecom.fr/). + +# Building + +Building avatar² is easy! +The following three commands are enough to install the core. +``` +$ git clone https://github.com/avatartwo/avatar2.git +$ cd avatar2 +$ sudo pip install . +``` +Afterwards, the different target endpoints can be built, such as QEmu or PANDA. +``` +$ cd targets +$ ./build_*.sh +``` + +# Getting started +For discovering the power of avatar² and getting a feeling of its usage, +we recommend checking out the +[handbook](https://github.com/avatartwo/avatar2/tree/master/handbook) here on +github. +Additionally, a documentation of the API is provided +[here](https://avatartwo.github.io/avatar2-docs/). diff --git a/avatar2/__init__.py b/avatar2/__init__.py new file mode 100644 index 00000000000..ef65b57df05 --- /dev/null +++ b/avatar2/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import + +#from . import archs + +from .memory_range import MemoryRange +from .message import * +from .archs import * +from .protocols import * +from .targets import * +from .avatar2 import Avatar +#from .analyses import Analyses + +#GDB_TARGET = targets.gdb.GDBTarget +#QEMU_TARGET = targets.qemu.QemuTarget diff --git a/avatar2/archs/__init__.py b/avatar2/archs/__init__.py new file mode 100644 index 00000000000..b8192fa1ab4 --- /dev/null +++ b/avatar2/archs/__init__.py @@ -0,0 +1,2 @@ +from .x86 import * +from .arm import * diff --git a/avatar2/archs/arm.py b/avatar2/archs/arm.py new file mode 100644 index 00000000000..ef164ecb54b --- /dev/null +++ b/avatar2/archs/arm.py @@ -0,0 +1,15 @@ +from capstone import CS_ARCH_ARM, CS_MODE_LITTLE_ENDIAN, CS_MODE_BIG_ENDIAN + +class ARM(object): + qemu_name = 'arm' + registers = {'r0': 0, 'r1': 1, 'r2': 2, 'r3': 3, 'r4': 4, 'r5': 5, 'r6': 6, + 'r7': 7, 'r8': 8, 'r9': 9, 'r10': 10, 'r11': 11, 'r12': 12, + 'sp': 13, 'lr': 14, 'pc': 15, 'cpsr': 25, + } + unemulated_instructions = ['mcr', 'mrc'] + capstone_arch = CS_ARCH_ARM + capstone_mode = CS_MODE_LITTLE_ENDIAN + +class ARMBE(ARM): + qemu_name = 'armeb' + capstone_mode = CS_MODE_BIG_ENDIAN diff --git a/avatar2/archs/x86.py b/avatar2/archs/x86.py new file mode 100644 index 00000000000..a53bc790202 --- /dev/null +++ b/avatar2/archs/x86.py @@ -0,0 +1,55 @@ +from capstone import CS_ARCH_X86, CS_MODE_32 + +class X86(object): + qemu_name = 'i386' + registers = {'eax': 0, + 'ecx': 1, + 'edx': 2, + 'ebx': 3, + 'esp': 4, + 'ebp': 5, + 'esi': 6, + 'edi': 7, + 'eip': 8, + 'eflags': 9, + 'cs': 10, + 'ss': 11, + 'ds': 12, + 'es': 13, + 'fs': 14, + 'gs': 15, } + unemulated_instructions = [] + capstone_arch = CS_ARCH_X86 + capstone_mode = CS_MODE_32 + + + +class X86_64(object): + qemu_name = 'x86_64' + registers = {'rax': 0, + 'rbx': 1, + 'rcx': 2, + 'rdx': 3, + 'rsi': 4, + 'rdi': 5, + 'rbp': 6, + 'rsp': 7, + 'r8 ': 8, + 'r9': 9, + 'r10': 10, + 'r11': 11, + 'r12': 12, + 'r13': 13, + 'r14': 14, + 'r15': 15, + 'rip': 16, + 'pc': 16, + 'eflags': 17, + 'cs' : 18, + 'ss' : 19, + 'ds' : 20, + 'es' : 21, + 'fs' : 22, + 'gs' : 23, + } + unemulated_instructions = [] diff --git a/avatar2/avatar2.py b/avatar2/avatar2.py new file mode 100644 index 00000000000..5fd0671ec10 --- /dev/null +++ b/avatar2/avatar2.py @@ -0,0 +1,306 @@ +import sys +if sys.version_info < (3, 0): + import Queue as queue +else: + import queue + +import tempfile +import intervaltree +import logging +import signal + + +from os import path, makedirs +from threading import Thread, Event + + +from .archs.arm import ARM +from .memory_range import MemoryRange +from .message import * +from .peripherals import AvatarPeripheral +from .targets.target import TargetStates +from .watchmen import Watchmen, BEFORE, AFTER, watch + + +class Avatar(Thread): + """The Avatar-object is the main interface of avatar. + Here we can set the different targets, and more + + :ivar arch: architecture of all targets + :ivar endness: used endianness + + + """ + + def __init__(self, arch=ARM, endness='little', output_directory=None): + super(Avatar, self).__init__() + + self.arch = arch + self.endness = endness + self.watchmen = Watchmen(self) + self.targets = {} + self.transitions = {} + self.status = {} + self.memoryRanges = intervaltree.IntervalTree() + + self.output_directory = (tempfile.mkdtemp(suffix="_avatar") + if output_directory is None + else output_directory) + if not path.exists(self.output_directory): + makedirs(self.output_directory) + + self._close = Event() + self.queue = queue.Queue() + self.start() + + self.log = logging.getLogger('avatar') + format = '%(asctime)s | %(name)s.%(levelname)s | %(message)s' + logging.basicConfig(filename='%s/avatar.log' % self.output_directory, + level=logging.INFO, format=format) + self.log.info("Initialized Avatar. Output directory is %s" % + self.output_directory) + + signal.signal(signal.SIGINT, self.sigint_wrapper) + self.sigint_handler = self.shutdown + self.loaded_plugins = [] + + + def shutdown(self): + """ + Shuts down all targets and Avatar. Should be called at end of script + in order to cleanly exit all spawned processes and threads + """ + for t in self.targets.values(): + if t.state == TargetStates.RUNNING: + t.stop() + for t in self.targets.values(): + t.shutdown() + for range in self.memoryRanges: + if isinstance(range.data.forwarded_to, AvatarPeripheral): + range.data.forwarded_to.shutdown() + + self.stop() + + def sigint_wrapper(self, signal, frame): + self.log.info("Avatar Received SIGINT") + self.sigint_handler() + + + def load_plugin(self, name): + plugin = __import__("avatar2.plugins.%s" % name, + fromlist=['avatar2.plugins']) + plugin.load_plugin(self) + self.loaded_plugins += [name] + + def add_target(self, name, backend, *args, **kwargs): + """ + Adds a new target to the analyses + + :ivar name: name of the target + :ivar backend: the desired backend. Implemented for now: qemu, gdb + :kwargs: those argument will be passed to the target-object itself + :return: The created TargetObject + """ + + self.targets[name] = backend(name, self, *args, **kwargs) + + return self.targets[name] + + def get_target(self, name): + """ + Retrieves a target of the analyses by it's name + + :param name: The name of the desired target + :return: The Target! + """ + + return self.targets.get(name, None) + + def get_targets(self): + """ + A generator for all targets. + """ + for target in self.targets.items(): + yield target + + def init_targets(self): + """ + Inits all created targets + """ + for t in self.get_targets(): + t[1].init() + + + + def add_memory_range(self, address, size, name='', permissions='rwx', + file=None, forwarded=False, forwarded_to=None, + emulate=None, **kwargs): + """ + Adds a memory range to avatar + + :param address: Base-Address of the Range + :param size: Size of the range + :param file: A file backing this range, if applicable + :param forwarded: Whether this range should be forwarded + :param forwarded_to: If forwarded is true, specify the forwarding target + """ + if emulate: + python_peripheral = emulate(name, address, size, **kwargs) + forwarded = True + forwarded_to = python_peripheral + kwargs.update({'python_peripheral': python_peripheral}) + + if forwarded == True: + kwargs.update({'qemu_name': 'avatar-rmemory'}) + m = MemoryRange(address, size, name=name, permissions=permissions, + file=file, forwarded=forwarded, + forwarded_to=forwarded_to, **kwargs) + self.memoryRanges[address:address+size] = m + return m + + + def get_memory_range(self, address): + """ + Get a memory range from an address + Note: for now just get's one range. If there are multiple ranges + at the same address, this method won't work (for now) + + :param address: the address of the range + :returns: the memory range + """ + ranges = self.memoryRanges[address] + if len(ranges) > 1: + raise Exception("More than one memory range specified at 0x%x, \ + not supported yet!" % address) + elif len(ranges) == 0: + raise Exception("No Memory range specified at 0x%x" % + address) + return ranges.pop().data + + + + @watch('StateTransfer') + def transfer_state(self, from_target, to_target, + synch_regs=True, synched_ranges=[]): + """ + Transfers the state from one target to another one + + :param from_target: the source target + :param to_target: the destination target + :param synch_regs: Whether registers should be synched + :param synched_ranges: The memory ranges whose contents should be + transfered + :type from_target: Target() + :type to_target: Target() + :type synch_regs: bool + :type synched_ranges: list + """ + + + if from_target.state != TargetStates.STOPPED or \ + to_target.state != TargetStates.STOPPED: + raise Exception("Targets must be stopped for State Transfer, \ + but target_states are (%s, %s)" % + (from_target.state, to_target.state)) + + if synch_regs: + for r in self.arch.registers: + to_target.write_register(r, from_target.read_register(r)) + self.log.info('Synchronized Registers') + + for range in synched_ranges: + m = from_target.read_memory(range.address, 1, range.size, raw=True) + to_target.write_memory(range.address, 1, m, raw=True) + self.log.info('Synchronized Memory Range: %s' % range.name) + + + @watch('UpdateState') + def _handle_updateStateMessage(self, message): + self.log.info("Received state update of target %s to %s" % + (message.origin.name, message.state)) + message.origin.update_state(message.state) + + @watch('BreakpointHit') + def _handle_breakpointHitMessage(self, message): + self.log.info("Breakpoint hit for Target: %s" % message.origin.name) + message.origin.update_state(TargetStates.STOPPED) + + @watch('RemoteMemoryRead') + def _handle_remote_memory_read_message(self, message): + range = self.get_memory_range(message.address) + + if range.forwarded != True: + raise Exception("Forward request for non forwarded range received!") + if range.forwarded_to == None: + raise Exception("Forward request for non existing target received.\ + (Address = 0x%x)" % message.address) + + + try: + mem = range.forwarded_to.read_memory(message.address, message.size) + success = True + except: + mem = -1 + success = False + message.origin._remote_memory_protocol.sendResponse(message.id, mem, + success) + return (message.id, mem, success) + + @watch('RemoteMemoryWrite') + def _handle_remote_memory_write_msg(self, message): + range = self.get_memory_range(message.address) + if range.forwarded != True: + raise Exception("Forward request for non forwarded range received!") + if range.forwarded_to == None: + raise Exception("Forward request for non existing target received!") + + success = range.forwarded_to.write_memory(message.address, message.size, + message.value) + + message.origin._remote_memory_protocol.sendResponse(message.id, 0, + success) + return (message.id, 0, success) + + def run(self): + """ + The code of the Thread managing the asynchronous messages received. + Default behavior: wait for the priority queue to hold a message and call + the _async_handler method to process it. + """ + self._close.clear() + while True: + if self._close.is_set(): + break + + message = None + try: + message = self.queue.get(timeout=0.5) + except: + continue + self.log.debug("Avatar received %s" % message) + + if isinstance(message, UpdateStateMessage): + self._handle_updateStateMessage(message) + elif isinstance(message, BreakpointHitMessage): + self._handle_breakpointHitMessage(message) + elif isinstance(message, RemoteMemoryReadMessage): + self._handle_remote_memory_read_message(message) + elif isinstance(message, RemoteMemoryWriteMessage): + self._handle_remote_memory_write_msg(message) + else: + raise Exception("Unknown Avatar Message received") + + + + def stop(self): + """ + Stop the thread which manages the asynchronous messages. + """ + self._close.set() + self.join() + + @watch('AvatarGetStatus') + def get_status(self): + return self.status + + diff --git a/avatar2/memory_range.py b/avatar2/memory_range.py new file mode 100644 index 00000000000..6c2a51f2c49 --- /dev/null +++ b/avatar2/memory_range.py @@ -0,0 +1,24 @@ + + +class MemoryRange(object): + """ + This class represents a MemoryRange which can be mapped in one of Avatar-Targets. + :ivar address: The load-address of the memory range + :ivar size: The size of the memory range + :ivar name: User-defined name for the memory range + :ivar permissions: The permisions of the range, represented as textual unix file permission (rwx) + :ivar file: A file used for backing the memory range + :ivar forwarded: Enable or disable forwarding for this range + :ivar forwarded_to: List of targets this range should be forwarded to + """ + + def __init__(self, address, size, name='', permissions='rwx', + file=None, forwarded=False, forwarded_to=None, **kwargs): + self.address = address + self.size = size + self.name = name + self.permissions = permissions + self.file = file + self.forwarded = forwarded + self.forwarded_to = forwarded_to + self.__dict__.update(kwargs) diff --git a/avatar2/message.py b/avatar2/message.py new file mode 100644 index 00000000000..5ef3dd43b27 --- /dev/null +++ b/avatar2/message.py @@ -0,0 +1,45 @@ +from enum import Enum + +class AvatarMessage(object): + """This class provides constants to create and parse the avatar message-dict""" + + def __init__(self, origin): + self.origin = origin + + def __str__(self): + if self.origin: + return "%s from %s" % (self.__class__.__name__, self.origin.name) + else: + return "%s from unkown origin" % self.__class__.__name__ + + + +class UpdateStateMessage(AvatarMessage): + + def __init__(self, origin, new_state): + super(self.__class__, self).__init__(origin) + self.state = new_state + + +class BreakpointHitMessage(AvatarMessage): + + def __init__(self, origin, breakpoint_number, address): + super(self.__class__, self).__init__(origin) + self.breakpoint_number = breakpoint_number + self.address = address + + +class RemoteMemoryReadMessage(AvatarMessage): + def __init__(self, origin, id, address, size): + super(self.__class__, self).__init__(origin) + self.id = id + self.address = address + self.size = size + +class RemoteMemoryWriteMessage(AvatarMessage): + def __init__(self, origin, id, address, value, size): + super(self.__class__, self).__init__(origin) + self.id = id + self.address = address + self.value = value + self.size = size diff --git a/avatar2/peripherals/__init__.py b/avatar2/peripherals/__init__.py new file mode 100644 index 00000000000..55ab03f47e6 --- /dev/null +++ b/avatar2/peripherals/__init__.py @@ -0,0 +1,2 @@ +from .avatar_peripheral import * + diff --git a/avatar2/peripherals/avatar_peripheral.py b/avatar2/peripherals/avatar_peripheral.py new file mode 100644 index 00000000000..336d8ce07ac --- /dev/null +++ b/avatar2/peripherals/avatar_peripheral.py @@ -0,0 +1,44 @@ +from intervaltree import IntervalTree + +class AvatarPeripheral(object): + def __init__(self, name, address, size, **kwargs): + self.name = name + self.address = address + self.size = size + self.read_handler = IntervalTree() + self.write_handler = IntervalTree() + + def shutdown(self): + """ + Some peripherals will require to be shutdowned when avatar exits. + In those cases, this method should be overwritten. + """ + pass + + + def write_memory(self, address, size, value): + offset = address-self.address + intervals = self.write_handler[offset:offset+size-1] + if intervals == set(): + raise Exception("No write handler for peripheral %s at offset %d \ + (0x%x)" % (self.name, address-self.address, + address)) + if len(intervals) > 1: + raise Exception("Multiple write handler for peripheral %s\ + at offset %d" % (self.name, self.address-address)) + return intervals.pop().data(size, value) + + + def read_memory(self, address, size): + offset = address-self.address + intervals = self.read_handler[offset:offset+size-1] + if intervals == set(): + raise Exception("No read handler for peripheral %s at offset %d \ + (0x%x)" % (self.name, address-self.address, + address)) + if len(intervals) > 1: + raise Exception("Multiple read handler for peripheral %s\ + at offset %d" % (self.name, self.address-address)) + return intervals.pop().data(size) + + diff --git a/avatar2/peripherals/nucleo_usart.py b/avatar2/peripherals/nucleo_usart.py new file mode 100644 index 00000000000..951cc51af96 --- /dev/null +++ b/avatar2/peripherals/nucleo_usart.py @@ -0,0 +1,136 @@ +import socket + +from threading import Thread, Lock, Event +from .avatar_peripheral import AvatarPeripheral + + +SR_RXNE = 0x20 +SR_TXE = 0x80 +SR_TC = 0x40 + + +class NucleoRTC(AvatarPeripheral): + + def nop_read(self, size): + return 0x00 + + def __init__(self, name, address, size, **kwargs): + AvatarPeripheral.__init__(self, name, address, size) + self.read_handler[0:size] = self.nop_read + +class NucleoTIM(AvatarPeripheral): + + def nop_read(self, size): + return 0x00 + + def nop_write(self, size, value): + return True + + def __init__(self, name, address, size, **kwargs): + AvatarPeripheral.__init__(self, name, address, size) + self.read_handler[0:size] = self.nop_read + self.write_handler[0:size] = self.nop_write + + + +class NucleoUSART(AvatarPeripheral, Thread): + + def read_status_register(self, size): + self.lock.acquire(True) + ret = self.status_register + self.lock.release() + return ret + + def read_data_register(self, size): + self.lock.acquire(True) + ret = self.data_buf[0] + self.data_buf = self.data_buf[1:] + if len(self.data_buf) == 0: + self.status_register &= ~SR_RXNE + self.lock.release() + return ord(ret) + + + def write_data_register(self, size, value): + if self.connected: + self.conn.send(bytes((chr(value)))) + return True + + def nop_read(self, size): + return 0x00 + + def nop_write(self, size, value): + return True + + def __init__(self, name, address, size, nucleo_usart_port=5656, **kwargs): + Thread.__init__(self) + AvatarPeripheral.__init__(self, name, address, size) + self.port = nucleo_usart_port + + self.data_buf = '' + self.status_register = SR_TXE | SR_TC + + self.read_handler[0:3] = self.read_status_register + self.read_handler[4:7] = self.read_data_register + self.write_handler[4:7] = self.write_data_register + + self.read_handler[8:size] = self.nop_read + self.write_handler[8:size] = self.nop_write + + self.connected = False + + self.lock = Lock() + self._close = Event() + self.start() + self.sock = None + self.conn = None + + def shutdown(self): + self._close.set() + + if self.conn: + self.conn.close() + + if self.sock: + self.sock.close() + + + def run(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.setblocking(0) + self.sock.bind(('127.0.0.1', self.port)) + self.sock.settimeout(0.1) + + while not self._close.is_set(): + self.sock.listen(1) + + try: + self.conn, addr = self.sock.accept() + self.conn.settimeout(0.1) + self.connected = True + except socket.timeout: + continue + except OSError as e: + if e.errno == 9: + # Bad file descriptor error. Only happens when we called + # closed on the socket, the continuing the loop will + # terminate, which is the desired behaviour + continue + else: + # Something terrible happened + raise(e) + + while not self._close.is_set(): + try: + chr = self.conn.recv(1) + except socket.timeout: + continue + if not chr: + break + self.lock.acquire(True) + self.data_buf += chr + self.status_register |= SR_RXNE + self.lock.release() + self.connected = False + diff --git a/avatar2/plugins/__init__.py b/avatar2/plugins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/avatar2/plugins/instruction_forwarder.py b/avatar2/plugins/instruction_forwarder.py new file mode 100644 index 00000000000..f1b9d9f08e5 --- /dev/null +++ b/avatar2/plugins/instruction_forwarder.py @@ -0,0 +1,40 @@ +from types import MethodType + +from capstone import * + + +def forward_instructions(self, from_target, to_target, + memory_region, instructions=None, + read_from_file=True): + if instructions == None: + instructions = self.arch.unemulated_instructions + + if memory_region.forwarded == True: + raise Exception("Cannot forward instructions from forwarded" + + " memory region") + + if read_from_file == True and memory_region.file == None: + raise Exception("No file specified for this memory region") + + content = bytes() + if read_from_file == True: + with open(memory_region.file, 'rb') as f: + content = f.read() + else: + content = from_target.rm(memory_region.address, memory_region.size) + + md = Cs(self.arch.capstone_arch, self.arch.capstone_mode) + for (addr, size, op, _) in md.disasm_lite(content, memory_region.address): + if op in instructions: + self.log.debug("%s instruction found at %x. " + + "Adding transition.") + self.add_transition(addr, from_target, to_target, + synch_regs=True) + self.add_transition(addr + size, to_target, from_target, + synch_regs=True) + + +def load_plugin(avatar): + if 'orchestrator' not in avatar.loaded_plugins: + avatar.load_plugin('orchestrator') + avatar.forward_instructions = MethodType(forward_instructions, avatar) diff --git a/avatar2/plugins/orchestrator.py b/avatar2/plugins/orchestrator.py new file mode 100644 index 00000000000..7d7e44587d8 --- /dev/null +++ b/avatar2/plugins/orchestrator.py @@ -0,0 +1,165 @@ +from types import MethodType +from threading import Event +from enum import Enum + +from avatar2.watchmen import AFTER, BEFORE, watch +from avatar2 import TargetStates + +watched_events = { + 'OrchestrationTransitionAdd', + 'OrchestrationTransition', + 'OrchestrationStart', + 'OrchestrationResumed', + 'OrchestrationStop', + 'OrchestrationTransitionsDisabled', + 'OrchestrationTransitionsEnabled' +} + +class OrchestrationStopReason(Enum): + STOPPING_TRANSITION_HIT = 0 + UNKNOWN_BREAKPOINT_HIT = 1 + TARGET_EXITED = 2 + USER_REQUESTED = 3 + + +class Transition(object): + + def __init__(self, address, from_target, to_target, + synch_regs, synched_ranges, enabled=True, + max_hits=0, stop=False, hw_bkpt=False): + self.address = address + self.from_target = from_target + self.to_target = to_target + self.synch_regs = synch_regs + self.synched_ranges = synched_ranges + self.enabled = enabled + self.max_hits = max_hits + self.num_hits = 0 + self.stop = stop + self.hw_bkpt = hw_bkpt + +def update_state_callback(avatar, message, **kwargs): + if message.state == TargetStates.EXITED and \ + message.origin == avatar.last_target: + avatar.stop_orchestration( + OrchestrationStopReason.TARGET_EXITED) + + +def transition_callback(avatar, message, **kwargs): + from_target = message.origin + address = message.address + + if avatar.transitions.get((address, from_target), None) is not None: + trans = avatar.transitions[(address, from_target)] + if trans.enabled: + avatar.watchmen.trigger('OrchestrationTransition', BEFORE, trans) + avatar.transfer_state(from_target, trans.to_target, + trans.synch_regs, trans.synched_ranges) + trans.num_hits += 1 + avatar.last_target = trans.to_target + if trans.stop == True: + avatar.stop_orchestration( + OrchestrationStopReason.STOPPING_TRANSITION_HIT) + else: + trans.to_target.cont() + avatar.watchmen.trigger('OrchestrationTransition', AFTER, trans) + elif avatar.orchestration_stopped.is_set() == False: + avatar.stop_orchestration( + OrchestrationStopReason.UNKNOWN_BREAKPOINT_HIT) + +@watch('OrchestrationTransitionAdd') +def add_transition(self, address, from_target, to_target, + synch_regs=True, synched_ranges=[], stop=False, + hw_breakpoint=False): + + trans = Transition(address, from_target, to_target, + synch_regs=synch_regs, + synched_ranges = synched_ranges, + stop=stop, hw_bkpt=hw_breakpoint) + + self.transitions[(address, from_target)] = trans + + +@watch('OrchestrationTransitionsEnabled') +def enable_transitions(self): + for t in self.targets.values(): + if t.state != TargetStates.STOPPED: + raise Exception("%s has to be stopped to enable transitions" % t) + for t in self.transitions.values(): + t.bkptno = t.from_target.set_breakpoint(t.address, hardware=t.hw_bkpt) + + +@watch('OrchestrationTransitionsDisabled') +def disable_transitions(self): + for t in self.transitions.values(): + t.from_target.remove_breakpoint(t.bkptno) + +def _orchestrate(self, target, blocking=True): + self.enable_transitions() + self.orchestration_stopped_reason = None + self.orchestration_stopped.clear() + self.last_target = target + + target.cont() + if blocking == True: + saved_handler = self.sigint_handler + self.sigint_handler = self.stop_orchestration + self.orchestration_stopped.wait() + self.sigint_handler = saved_handler + + +@watch('OrchestrationStart') +def start_orchestration(self, force_init=False, blocking=True): + if self.start_target == None: + raise Exception("No starting target specified!") + for t in self.targets.values(): + if t.state == TargetStates.CREATED or force_init == True: + t.init() + + self._orchestrate(self.start_target, blocking) + +@watch('OrchestrationResumed') +def resume_orchestration(self, blocking=True): + if self.last_target == None: + raise Exception("No Orchestration was running before!") + self._orchestrate(self.last_target, blocking) + +@watch('OrchestrationStop') +def stop_orchestration(self, reason=OrchestrationStopReason.USER_REQUESTED): + for t in self.targets.values(): + if t.state == TargetStates.RUNNING: + t.stop() + self.disable_transitions() + self.orchestration_stopped.set() + self.orchestration_stopped_reason = reason + + + +def load_plugin(avatar): + avatar.transitions = {} + avatar.orchestration_stopped = Event() + avatar.orchestration_stopped.set() + avatar.orchestration_stopped_reason = None + + avatar.start_target = None + avatar.last_target = None + + avatar.watchmen.add_watch_types(watched_events) + avatar.watchmen.add_watchman('BreakpointHit', when=AFTER, + callback=transition_callback) + avatar.watchmen.add_watchman('UpdateState', when=AFTER, + callback=update_state_callback) + + + avatar.add_transition = MethodType(add_transition, avatar) + avatar.enable_transitions = MethodType(enable_transitions, avatar) + avatar.disable_transitions = MethodType(disable_transitions, avatar) + avatar.start_orchestration = MethodType(start_orchestration, avatar) + avatar.resume_orchestration = MethodType(resume_orchestration, avatar) + avatar.stop_orchestration = MethodType(stop_orchestration, avatar) + avatar._orchestrate = MethodType(_orchestrate, avatar) + + + + + diff --git a/avatar2/protocols/__init__.py b/avatar2/protocols/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/avatar2/protocols/gdb.py b/avatar2/protocols/gdb.py new file mode 100644 index 00000000000..521e8d3182a --- /dev/null +++ b/avatar2/protocols/gdb.py @@ -0,0 +1,600 @@ +import sys +if sys.version_info < (3, 0): + import Queue as queue + #__class__ = instance.__class__ +else: + import queue + +from threading import Thread, Event, Condition +from struct import pack, unpack +from codecs import encode +from string import hexdigits +import logging +import pygdbmi.gdbcontroller + +from avatar2.archs.arm import ARM +from avatar2.targets import TargetStates +from avatar2.message import AvatarMessage, UpdateStateMessage, BreakpointHitMessage + + +GDB_PROT_DONE = 'done' +GDB_PROT_CONN = 'connected' +GDB_PROT_RUN = 'running' + + +class GDBResponseListener(Thread): + """ + This class creates objects waiting for responses from the gdb-process + Depending whether a synchronous or asynchronous message is received, + it is either put in a synchronous dictionary or parsed/lifted + to an AvatarMessage and added to the Queue of the according target + """ + + def __init__(self, gdb_protocol, gdb_controller, avatar_queue, origin=None): + super(GDBResponseListener, self).__init__() + self._protocol = gdb_protocol + self._token = -1 + self._async_responses = queue.Queue() if avatar_queue is None\ + else avatar_queue + self._sync_responses = {} + self._gdb_controller = gdb_controller + self._gdb = gdb_protocol + self._close = Event() + self._closed = Event() + self._close.clear() + self._closed.clear() + self._sync_responses_cv = Condition() + self._last_exec_token = 0 + self._origin = origin + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + + def get_token(self): + """Gets a token for a synchronous request + :returns: An (integer) token + """ + self._token += 1 + return self._token + + def parse_async_notify(self, response): + """ + This functions converts gdb notify messages to an avatar message + + :param response: A pygdbmi response dictonary + :returns: An avatar message + """ + + # Make sure this is a notify-response + if response['type'] != 'notify': + raise RunTimeError() + + msg = response['message'] + payload = response['payload'] + avatar_msg = None + + self.log.debug("Received Message: %s", msg) + + if msg.startswith('thread'): + pass # Thread group handling is not implemented yet + elif msg.startswith('tsv'): + pass # likewise tracing + elif msg.startswith('library'): + pass # library loading not supported yet + elif msg == 'breakpoint-modified': + pass # ignore breakpoint modified for now + elif msg == 'memory-changed': + pass # ignore changed memory for now + elif msg == 'stopped': + if payload.get('reason') == 'breakpoint-hit': + avatar_msg = BreakpointHitMessage(self._origin, payload['bkptno'], + int(payload['frame']['addr'], 16)) + elif payload.get('reason') == 'exited-normally': + avatar_msg = UpdateStateMessage( + self._origin, TargetStates.EXITED) + elif payload.get('reason') == 'end-stepping-range': + avatar_msg = UpdateStateMessage( + self._origin, TargetStates.STOPPED) + elif payload.get('reason') == 'signal-received': + avatar_msg = UpdateStateMessage( + self._origin, TargetStates.STOPPED) + elif payload.get('reason') == 'watchpoint-trigger': + avatar_msg = UpdateStateMessage( + self._origin, TargetStates.STOPPED) + elif payload.get('reason') is not None: + self.log.critical("Target stopped with unknown reason: %s" % + payload['reason']) + #raise RuntimeError + else: + avatar_msg = UpdateStateMessage( + self._origin, TargetStates.STOPPED) + elif msg == 'running': + avatar_msg = UpdateStateMessage(self._origin, TargetStates.RUNNING) + + else: + self.log.critical('Catched unknown async message: %s' % response) + + return avatar_msg + + def parse_async_response(self, response): + """ + This functions converts a async gdb/mi message to an avatar message + + :param response: A pygdbmi response dictonary + """ + + if response['type'] == 'console': + pass # TODO: implement handler for console messages + elif response['type'] == 'log': + pass # TODO: implement handler for log messages + elif response['type'] == 'target': + pass # TODO: implement handler for target messages + elif response['type'] == 'output': + pass # TODO: implement handler for output messages + elif response['type'] == 'notify': + return self.parse_async_notify(response) + + + else: + raise Exception("GDBProtocol got unexpected response of type %s" % + response['type']) + + def get_async_response(self, timeout=0): + return self._async_responses.get(timeout=timeout) + + def get_sync_response(self, token, timeout=5): + for x in range(timeout * 2): + self._sync_responses_cv.acquire() + ret = self._sync_responses.pop(token, None) + if ret is None: + self._sync_responses_cv.wait(timeout=0.5) + + self._sync_responses_cv.release() + if ret is not None: + return ret + raise TimeoutError() + + def run(self): + while(1): + if self._close.is_set(): + break + + responses = None + + try: + responses = self._gdb_controller.get_gdb_response( + timeout_sec=0.5 + ) + except: + continue + + for response in responses: + if response.get('token', None) is not None: + self._sync_responses_cv.acquire() + self._sync_responses[response['token']] = response + self._sync_responses_cv.notifyAll() + self._sync_responses_cv.release() + else: + avatar_msg = self.parse_async_response(response) + self.log.debug("Parsed an avatar_msg %s", avatar_msg) + if avatar_msg is not None: + if self._gdb._async_message_handler is not None: + self._gdb._async_message_handler(avatar_msg) + else: + self._async_responses.put(avatar_msg) + self._closed.set() + + def stop(self): + """Stops the listening thread. Useful for teardown of the target""" + self._close.set() + self._closed.wait() + + +class GDBProtocol(object): + """Main class for the gdb communication protocol + :ivar gdb_executable: the path to the gdb which should be executed + :ivar arch: the architecture + :ivar additional_args: additional arguments for gdb + :ivar avatar_queue : The queue serving as message sink for async messages + """ + + def __init__( + self, + gdb_executable="gdb", + arch=ARM, + additional_args=[], + async_message_handler=None, + avatar_queue=None, + origin=None): + self._async_message_handler = async_message_handler + self._arch = arch + self._gdbmi = pygdbmi.gdbcontroller.GdbController( + gdb_path=gdb_executable, + gdb_args=[ + '--nx', + '--quiet', + '--interpreter=mi2'] + + additional_args, + verbose=False) # set to True for debugging + self._communicator = GDBResponseListener( + self, self._gdbmi, avatar_queue, origin) + self._communicator.start() + self._avatar_queue = avatar_queue + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + + def __del__(self): + self.shutdown() + + def shutdown(self): + if self._communicator is not None: + self._communicator.stop() + self._communicator = None + if self._gdbmi is not None: + self._gdbmi.exit() + self._gdbmi = None + + + def _sync_request(self, request, rexpect): + """ Generic method to send a synchronized request + + :param request: the request as list + :param rexpect: the expected response type + :returns: Whether a response of type rexpect was received + :returns: The response + """ + + token = self._communicator.get_token() + request = [request] if isinstance(request, str) else request + + req = str(token) + ' '.join(request) + self.log.debug("Sending request: %s" % req) + + self._gdbmi.write(req, read_response=False) + try: + response = self._communicator.get_sync_response(token) + ret = True if response['message'] == rexpect else False + except: + response = None + ret = None + return ret, response + + def remote_connect(self, ip='127.0.0.1', port=3333): + """ + connect to a remote gdb server + + :param ip: ip of the remote gdb-server (default: localhost) + :param port: port of the remote gdb-server (default: port) + :returns: True on successful connection + """ + + req = ['-gdb-set', 'target-async', 'on'] + ret, resp = self._sync_request(req, GDB_PROT_DONE) + if not ret: + self.log.critical( + "Unable to set GDB/MI to async, received response: %s" % + resp) + raise Exception("GDBProtocol was unable to switch to asynch") + + req = ['-target-select', 'remote', '%s:%d' % (ip, int(port))] + ret, resp = self._sync_request(req, GDB_PROT_CONN) + + self.log.debug( + "Attempted to connect to target. Received response: %s" % + resp) + if not ret: + self.log.critical("GDBProtocol was unable to connect to remote target") + raise Exception("GDBProtocol was unable to connect") + + return ret + + def remote_connect_serial(self, device='/dev/ttyACM0', baud_rate=38400, + parity='none'): + """ + connect to a remote gdb server through a serial device + + :param device: file representing the device (default: /dev/ttyACM0) + :param baud_rate: baud_rate of the serial device (default: 38400) + :param parity: parity of the serial link (default no parity) + :returns: True on successful connection + """ + + if parity not in ['none', 'even', 'odd']: + self.log.critical("Parity must be none, even or odd") + raise Exception("Cannot set parity to %s" % parity) + + req = ['-gdb-set', 'mi-async', 'on'] + ret, resp = self._sync_request(req, GDB_PROT_DONE) + if not ret: + self.log.critical( + "Unable to set GDB/MI to async, received response: %s" % + resp) + raise Exception("GDBProtocol was unable to connect") + + req = ['-gdb-set', 'serial', 'parity', '%s' % parity] + ret, resp = self._sync_request(req, GDB_PROT_DONE) + if not ret: + self.log.critical("Unable to set parity") + raise Exception("GDBProtocol was unable to set parity") + + req = ['-gdb-set', 'serial', 'baud', '%i' % baud_rate] + ret, resp = self._sync_request(req, GDB_PROT_DONE) + if not ret: + self.log.critical("Unable to set baud rate") + raise Exception("GDBProtocol was unable to set Baudrate") + + req = ['-target-select', 'remote', '%s' % (device)] + ret, resp = self._sync_request(req, GDB_PROT_CONN) + + self.log.debug( + "Attempted to connect to target. Received response: %s" % + resp) + return ret + + def remote_disconnect(self): + """ + disconnects from remote target + """ + + ret, resp = self._sync_request('-target-disconnect', GDB_PROT_DONE) + + self.log.debug( + "Attempted to disconnect from target. Received response: %s" % + resp) + return ret + + def get_register_names(self): + """fetch all register names + :returns: a list with all registers names, in order as known to gdb + """ + + ret, resp = self._sync_request( + '-data-list-register-names', GDB_PROT_DONE) + + self.log.debug( + "Attempted to obtain register names. Received response: %s" % resp) + return resp['payload']['register-names'] if ret else None + + def set_breakpoint(self, line, + hardware=False, + temporary=False, + regex=False, + condition=None, + ignore_count=0, + thread=0): + """Inserts a breakpoint + + :param bool hardware: Hardware breakpoint + :param bool tempory: Tempory breakpoint + :param str regex: If set, inserts breakpoints matching the regex + :param str condition: If set, inserts a breakpoint with specified condition + :param int ignore_count: Amount of times the bp should be ignored + :param int thread: Threadno in which this breakpoints should be added + :returns: The number of the breakpoint + """ + cmd = ["-break-insert"] + if temporary: + cmd.append("-t") + if hardware: + cmd.append("-h") + if regex: + assert((not temporary) and (not condition) and (not ignore_count)) + cmd.append("-r") + cmd.append(str(regex)) + if condition: + cmd.append("-c") + cmd.append(str(condition)) + if ignore_count: + cmd.append("-i") + cmd.append("%d" % ignore_count) + if thread: + cmd.append("-p") + cmd.append("%d" % thread) + + if isinstance(line, int): + cmd.append("*0x%x" % line) + else: + cmd.append(str(line)) + + ret, resp = self._sync_request(cmd, GDB_PROT_DONE) + self.log.debug("Attempted to set breakpoint. Received response: %s" % resp) + if ret == True: + return int(resp['payload']['bkpt']['number']) + else: + return -1 + + def set_watchpoint(self, variable, write=True, read=False): + cmd = ["-break-watch"] + if read == False and write == False: + raise ValueError("At least one read and write must be True") + elif read == True and write == False: + cmd.append("-r") + elif read == True and write == True: + cmd.append("-a") + + if isinstance(variable, int): + cmd.append("*0x%x" % variable) + else: + cmd.append(str(variable)) + + ret, resp = self._sync_request(cmd, GDB_PROT_DONE) + self.log.debug("Attempted to set watchpoint. Received response: %s" % resp) + + if ret == True: + # The payload contains different keys according to the + # type of the watchpoint which has been set. + # The possible keys are: [(hw-)][ar]wpt + for k in resp['payload'].keys(): + if k.endswith('wpt'): + break + else: + return -1 + return int(resp['payload'][k]['number']) + else: + return -1 + + def remove_breakpoint(self, bkpt): + """Deletes a breakpoint""" + ret, resp = self._sync_request( + ['-break-delete', str(bkpt)], GDB_PROT_DONE) + + self.log.debug( + "Attempted to delete breakpoint. Received response: %s" % + resp) + return ret + + def write_memory(self, address, wordsize, val, num_words=1, raw=False): + """Writes memory + + :param address: Address to write to + :param wordsize: the size of the write (1, 2, 4 or 8) + :param val: the written value + :type val: int if num_words == 1 and raw == False + list if num_words > 1 and raw == False + str or byte if raw == True + :param num_words: The amount of words to read + :param raw: Specifies whether to write in raw or word mode + :returns: True on success else False + """ + num2fmt = {1: 'B', 2: 'H', 4: 'I', 8:'Q'} + + max_write_size = 0x100 + + if raw == True: + for i in range(0, len(val), max_write_size): + write_val = encode(val[i:max_write_size+i], 'hex_codec').decode('ascii') + ret, resp = self._sync_request( + ["-data-write-memory-bytes", str(address+i), write_val], + GDB_PROT_DONE) + + else: + fmt = '<%d%s' % (num_words, num2fmt[wordsize]) + if num_words == 1: + contents = pack(fmt, val) + else: + contents = pack(fmt, *val) + + hex_contents = encode(contents, 'hex_codec').decode('ascii') + ret, resp = self._sync_request( + ["-data-write-memory-bytes", str(address), hex_contents], + GDB_PROT_DONE) + + self.log.debug("Attempted to write memory. Received response: %s" % resp) + return ret + + + + + def read_memory(self, address, wordsize=4, num_words=1, raw=False): + """reads memory + + :param address: Address to write to + :param wordsize: the size of a read word (1, 2, 4 or 8) + :param num_words: the amount of read words + :param raw: Whether the read memory should be returned unprocessed + :return: The read memory + """ + + num2fmt = {1: 'B', 2: 'H', 4: 'I', 8:'Q'} + + max_read_size = 0x100 + raw_mem = b'' + for i in range(0, wordsize*num_words, max_read_size): + to_read = max_read_size if wordsize*num_words > i+max_read_size-1 else \ + wordsize*num_words % max_read_size + res, resp = self._sync_request(["-data-read-memory-bytes", str(address+i), + str(to_read)], + GDB_PROT_DONE) + + self.log.debug("Attempted to read memory. Received response: %s" % resp) + + if not res: + raise Exception("Failed to read memory!") + + # the indirection over the bytearray is needed for legacy python support + read_mem = bytearray.fromhex(resp['payload']['memory'][0]['contents']) + raw_mem += bytes(read_mem) + + if raw == True: + return raw_mem + else: + # Todo: Endianness support + fmt = '<%d%s' % (num_words, num2fmt[wordsize]) + mem = list(unpack(fmt, raw_mem)) + + if num_words == 1: + return mem[0] + else: + return mem + + def read_register(self, reg): + return self.read_register_from_nr(self._arch.registers[reg]) + + def read_register_from_nr(self, reg_num): + """Gets the value of a single register + + :param reg_num: number of the register + :returns: the value as integer on success, else None + :todo: Implement function for multiple registers + """ + ret, resp = self._sync_request( + ["-data-list-register-values", "x", "%d" % reg_num], GDB_PROT_DONE) + + self.log.debug( + "Attempted to get register value. Received response: %s" % + resp) + return int(resp['payload']['register-values'] + [0]['value'], 16) if ret else None + + def write_register(self, reg, value): + """Set one register on the target + :returns: True on success""" + ret, resp = self._sync_request( + ["-gdb-set", "$%s=0x%x" % (reg, value)], GDB_PROT_DONE) + + self.log.debug("Attempted to set register. Received response: %s" % resp) + return ret + + def step(self): + """Step one instruction on the target + :returns: True on success""" + ret, resp = self._sync_request( + ["-exec-step-instruction"], GDB_PROT_RUN) + + self.log.debug( + "Attempted to step on the target. Received response: %s" % + resp) + return ret + + def cont(self): + """Continues the execution of the target + :returns: True on success""" + ret, resp = self._sync_request(["-exec-continue"], GDB_PROT_RUN) + + self.log.debug( + "Attempted to continue execution on the target. Received response: %s" % + resp) + return ret + + def stop(self): + """Stops execution of the target + :returns: True on success""" + + ret, resp = self._sync_request( + ["-exec-interrupt", "--all"], GDB_PROT_DONE) + + self.log.debug( + "Attempted to stop execution of the target. Received response: %s" % + resp) + return ret + + def set_endianness(self, endianness='little'): + req = ['-gdb-set', 'endian', '%s' % endianness] + ret, resp = self._sync_request(req, GDB_PROT_DONE) + + self.log.debug("Attempt to set endianness of the target. Received: %s" % + resp) + return ret diff --git a/avatar2/protocols/openocd.py b/avatar2/protocols/openocd.py new file mode 100644 index 00000000000..556e9a2984e --- /dev/null +++ b/avatar2/protocols/openocd.py @@ -0,0 +1,109 @@ +import sys +if sys.version_info < (3, 0): + import Queue as queue +else: + import queue + +import subprocess +import telnetlib +import logging + + + + +END_OF_MSG = b'\r\n\r>' + +class OpenOCDProtocol(object): + """ + This class implements the openocd protocol. + Although OpenOCD itself is very powerful, it is only used as monitor + protocol, since all other functionalities are also exposed via the + gdb-interface, which is easier to parse in an automatic manner. + + :param openocd_script: The openocd scripts to be executed. + :type openocd_script: str or list + :param openocd_executable: The executable + :param additional_args: Additional arguments delivered to openocd. + :type additional_args: list + :param telnet_port: the port used for the telnet connection + :param gdb_port: the port used for openocds gdb-server + """ + + def __init__(self, openocd_script, openocd_executable="openocd", + additional_args=[], telnet_port=4444, gdb_port=3333, + origin=None, output_directory='/tmp'): + if isinstance(openocd_script, str): + self.openocd_files = [openocd_script] + elif isinstance(openocd_script, list): + self.openocd_files = openocd_script + else: + raise TypeError("Wrong type for OpenOCD configuration files") + + self._telnet = None + self._telnet_port = telnet_port + self._cmd_line = openocd_executable \ + + ''.join([' -f %s '% f for f in self.openocd_files]) \ + + '--command "telnet_port %d" ' % telnet_port \ + + '--command "gdb_port %d" ' % gdb_port \ + + ''.join(additional_args) + + self._openocd = None + + with open("%s/openocd_out.txt" % output_directory,"wb") as out, \ + open("%s/openocd_err.txt" % output_directory,"wb") as err: + self._openocd = subprocess.Popen(self._cmd_line, + stdout=out, stderr=err, shell=True) + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + + def __del__(self): + self.shutdown() + + def connect(self): + """ + Connects to OpenOCDs telnet-server for all subsequent communication + returns: True on success, else False + """ + + self._telnet = telnetlib.Telnet('127.0.0.1', self._telnet_port) + resp = self._telnet.read_until(END_OF_MSG) + if 'Open On-Chip Debugger' in str(resp): + return True + else: + self.log.error('Failed to connect to OpenOCD') + return False + + def reset(self): + """ + Resets the target + returns: True on success, else False + """ + self._telnet.write('reset halt\n'.encode('ascii')) + resp = self._telnet.read_until(END_OF_MSG) + if 'target state: halted' in str(resp): + return True + else: + self.log.error('Failed to reset the target with OpenOCD') + return False + + def shutdown(self): + """ + Shuts down OpenOCD + returns: True on success, else False + """ + if self._telnet: + self._telnet.write('shutdown\n'.encode('ascii')) + resp = self._telnet.read_all() + if 'shutdown command invoked' in str(resp): + return True + else: + self.log.error('Failed to shutdown the target with OpenOCD') + return False + if self._openocd is not None: + self._openocd.kill() + self._openocd = None + + + diff --git a/avatar2/protocols/qmp.py b/avatar2/protocols/qmp.py new file mode 100644 index 00000000000..08e9599784c --- /dev/null +++ b/avatar2/protocols/qmp.py @@ -0,0 +1,148 @@ +import sys +if sys.version_info < (3, 0): + import Queue as queue +else: + import queue + +from threading import Thread, Event, Condition +import logging +import json +import telnetlib +import re + + +class QMPResponseListener(Thread): + + def __init__(self, gdb_protocol, gdb_controller, avatar_queue, origin=None): + super(QMPResponseListener, self).__init__() + self._protocol = gdb_protocol + self._token = -1 + self._async_responses = queue.Queue() if avatar_queue is None\ + else avatar_queue + self._sync_responses = {} + self._gdb_controller = gdb_controller + self._gdb = gdb_protocol + self._close = Event() + self._closed = Event() + self._close.clear() + self._closed.clear() + self._sync_responses_cv = Condition() + self._last_exec_token = 0 + self._origin = origin + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + + def run(self): + while(1): + if self._close.is_set(): + break + + responses = None + + try: + responses = self._gdb_controller.get_gdb_response( + timeout_sec=0.5 + ) + except: + continue + + for response in responses: + if response.get('token', None) is not None: + self._sync_responses_cv.acquire() + self._sync_responses[response['token']] = response + self._sync_responses_cv.notifyAll() + self._sync_responses_cv.release() + else: + avatar_msg = self.parse_async_response(response) + self.log.debug("Parsed an avatar_msg %s", avatar_msg) + if avatar_msg is not None: + if self._gdb._async_message_handler is not None: + self._gdb._async_message_handler(avatar_msg) + else: + self._async_responses.put(avatar_msg) + self._closed.set() + + def stop(self): + """Stops the listening thread. Useful for teardown of the target""" + self._close.set() + self._closed.wait() + + +class QMPProtocol(object): + + def __init__(self, port, origin=None): + + self.port = port + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + self.id = 0 + + #self._communicator = QMPResponseListener(self, origin.avatar.queue, + #origin) + #self._communicator.start() + + def __del__(self): + self.shutdown() + + def connect(self): + self._telnet = telnetlib.Telnet('127.0.0.1', self.port) + self._telnet.read_until('\r\n'.encode('ascii')) + self.execute_command('qmp_capabilities') + return True + + + def execute_command(self, cmd, args=None): + command = {} + command['execute'] = cmd + if args: + command['arguments'] = args + command['id'] = self.id + self._telnet.write(('%s\r\n' % json.dumps(command)).encode('ascii')) + + while True: + resp = self._telnet.read_until('\r\n'.encode('ascii')) + resp = json.loads(resp.decode('ascii')) + if 'event' in resp: + continue + if 'id' in resp: + break + if resp['id'] != self.id: + raise Exception('Mismatching id for qmp response') + self.id += 1 + if 'error' in resp: + return resp['error'] + if 'return' in resp: + return resp['return'] + raise Exception("Response contained neither an error nor an return") + + + def reset(self): + """ + Resets the target + returns: True on success, else False + """ + pass + + def shutdown(self): + """ + returns: True on success, else False + """ + #self._communicator.stop() + pass + + def get_registers(self): + """ + Gets the current register state based on the hmp info registers + command. In comparison to register-access with the register protocol, + this function can also be called while the target is executing. + returns: A dictionary with the registers + """ + regs_s = self.execute_command("human-monitor-command", + {"command-line":"info registers"}) + regs_r = re.findall('(...)=([0-9a-f]{8})', regs_s) + return dict([(r.lower(), int(v,16)) for r,v in regs_r]) + diff --git a/avatar2/protocols/remote_memory.py b/avatar2/protocols/remote_memory.py new file mode 100644 index 00000000000..f9c28c94161 --- /dev/null +++ b/avatar2/protocols/remote_memory.py @@ -0,0 +1,182 @@ +import logging + +from enum import Enum +from os import O_CREAT, O_WRONLY, O_RDONLY +from threading import Thread, Event +from ctypes import Structure, c_uint32, c_uint64 + +from posix_ipc import MessageQueue, ExistentialError + +from avatar2.message import RemoteMemoryReadMessage, RemoteMemoryWriteMessage + + + + + +class operation(Enum): + READ = 0 + WRITE = 1 + + +class RemoteMemoryReq(Structure): + _fields_ = [ + ('id', c_uint64), + ('address', c_uint64), + ('value', c_uint64), + ('size', c_uint32), + ('operation', c_uint32) + ] + + +class RemoteMemoryResp(Structure): + _fields_ = [ + ('id', c_uint64), + ('value', c_uint64), + ('success', c_uint32) + ] + + +class RemoteMemoryRequestListener(Thread): + def __init__(self, rx_queue, avatar_queue, origin): + super(RemoteMemoryRequestListener, self).__init__() + self._rx_queue = rx_queue + self._avatar_queue = avatar_queue + self._origin = origin + self._close = Event() + self._closed = Event() + self._close.clear() + self._closed.clear() + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + + def run(self): + while True: + if self._close.is_set(): + break + + request = None + try: + request = self._rx_queue.receive(0.5) + except: + continue + + req_struct = RemoteMemoryReq.from_buffer_copy(request[0]) + + if operation(req_struct.operation) == operation.READ: + self.log.debug("Received RemoteMemoryRequest. Read from %x" % + req_struct.address) + MemoryForwardMsg = RemoteMemoryReadMessage(self._origin, + req_struct.id, + req_struct.address, + req_struct.size) + elif operation(req_struct.operation) == operation.WRITE: + self.log.debug("Received RemoteMemoryRequest. Write to %x" % + req_struct.address) + MemoryForwardMsg = RemoteMemoryWriteMessage(self._origin, + req_struct.id, + req_struct.address, + req_struct.value, + req_struct.size) + else: + raise ValueError("Received Message with unkown operation %d" % + req_struct.operation) + + self._avatar_queue.put(MemoryForwardMsg) + + self._closed.set() + + def stop(self): + self._close.set() + self._closed.wait() + + +class RemoteMemoryProtocol(object): + """ + This class listens to memoryforward requests and lifts them to avatar + messages. Likewise it can be directed to emit memoryforward-response + messages + + :param rx_queue_name: Name of the queue for receiving + :param tx_queue_name: Name of the queue for sending + :param avatar_queue: Queue to dispatch received requests to + :param origin: Reference to the Target utilizing this protocol + + """ + + def __init__(self, rx_queue_name, tx_queue_name, avatar_queue, origin=None): + self._rx_queue = None + self._tx_queue = None + self._rx_listener = None + + self.rx_queue_name = rx_queue_name + self.tx_queue_name = tx_queue_name + self._avatar_queue = avatar_queue + self._origin = origin + + self.log = logging.getLogger('%s.%s' % + (origin.log.name, self.__class__.__name__) + ) if origin else \ + logging.getLogger(self.__class__.__name__) + + def connect(self): + """ + Connect to the message queues for remote memory + + :return True on success, else False + """ + try: + self._rx_queue = MessageQueue(self.rx_queue_name, flags=O_RDONLY, + read = True, write = False) + except Exception as e: + self.log.error("Unable to create rx_queue: %s" % e) + return False + + try: + self._tx_queue = MessageQueue(self.tx_queue_name, flags=O_WRONLY, + read = False, write = True) + except Exception as e: + self.log.error("Unable to create tx_queue: %s" % e) + self._rx_queue.close() + return False + self._rx_listener = RemoteMemoryRequestListener(self._rx_queue, + self._avatar_queue, + self._origin) + self._rx_listener.start() + self.log.info("Successfully connected rmp") + return True + + + def __del__(self): + self.shutdown() + + def shutdown(self): + if self._rx_listener: + self._rx_listener.stop() + self._rx_listener = None + if self._rx_queue: + try: + self._rx_queue.unlink() + self._rx_queue.close() + self._rx_queue = None + except ExistentialError: + self.log.warning("Tried to close/unlink non existent rx_queue") + if self._tx_queue: + try: + self._tx_queue.unlink() + self._tx_queue.close() + self._tx_queue = None + except ExistentialError: + self.log.warning("Tried to close/unlink non existent tx_queue") + + + def sendResponse(self, id, value, success): + response = RemoteMemoryResp(id, value, success) + try: + self._tx_queue.send(response) + self.log.debug("Send RemoteMemoryResponse with id %d, %x" % (id, value)) + return True + except Exception as e: + self.log.error("Unable to send response: %s" % e) + return False diff --git a/avatar2/targets/__init__.py b/avatar2/targets/__init__.py new file mode 100644 index 00000000000..b56da65d758 --- /dev/null +++ b/avatar2/targets/__init__.py @@ -0,0 +1,7 @@ +from .target import Target, TargetStates, action_valid_decorator_factory +from .dummy_target import DummyTarget +from .openocd_target import * +from .gdb_target import * +from .qemu_target import * +from .panda_target import * + diff --git a/avatar2/targets/dummy_target.py b/avatar2/targets/dummy_target.py new file mode 100644 index 00000000000..ebc2a9a6e45 --- /dev/null +++ b/avatar2/targets/dummy_target.py @@ -0,0 +1,140 @@ +# Author: Davide Balzarotti +# Creation Date: 04-04-2017 + +import time, threading, random + +from avatar2.targets import Target, TargetStates +from avatar2.message import RemoteMemoryReadMessage, BreakpointHitMessage, UpdateStateMessage + +class _TargetThread(threading.Thread): + """Thread that mimics a running target""" + + def __init__(self, target): + threading.Thread.__init__(self) + self.target = target + self.please_stop = False + self.steps = 0 + + def run(self): + self.please_stop = False + # Loops until someone (the Dummy Target) tells me to stop by + # externally setting the "please_stop" variable to True + while not self.please_stop: + self.target.log.info("Dummy target doing Nothing..") + time.sleep(1) + self.steps += 1 + # 10% chances of triggering a breakpoint + if random.randint(0,100) < 10: + # If there are not breakpoints set, continue + if len(self.target.bp)==0: + continue + # Randomly pick one of the breakpoints + addr = random.choice(self.target.bp) + self.target.log.info("Taking a break..") + # Add a message to the Avatar queue to trigger the + # breakpoint + self.target.avatar.queue.put(BreakpointHitMessage(self.target, 1, addr)) + break + # 90% chances of reading from a forwarded memory address + else: + # Randomly pick one forwarded range + mem_range = random.choice(self.target.mranges) + # Randomly pick an address in the range + addr = random.randint(mem_range[0], mem_range[1]-4) + # Add a message in the Avatar queue to read the value at + # that address + self.target.avatar.queue.put(RemoteMemoryReadMessage(self.target, 55, addr, 4)) + self.target.log.info("Avatar told me stop..") + +class DummyTarget(Target): + """ + This is a Dummy target that can be used for testing purposes. + It simulates a device that randomly reads from forwarded memory ranges + and triggers breakpoints. + """ + + def __init__(self, name, avatar): + super(DummyTarget, self).__init__(name, avatar) + # List of breakpoints + self.bp = [] + # List of forwarded memory ranges + self.mranges = [] + self.thread = None + + # Avatar will try to answer to our messages (e.g., with the value + # the Dummy Target tries to read from memory). To handle that + # we need a memory protocol. However, here we set the protocol to + # ourself (its a dirty trick) and later implement the sendResponse + # method + self._remote_memory_protocol=self + + # This is called by Avatar to initialize the target + def init(self): + self.log.info("Dummy Target Initialized and ready to rock") + # Ack. It should actually go to INITIALIZED but then the protocol + # should change that to STOPPED + self.avatar.queue.put(UpdateStateMessage(self, TargetStates.STOPPED)) + # We fetch from Avatar the list of memory ranges that are + # configured to be forwarded + for range in self.avatar.memoryRanges: + range = range.data + if range.forwarded == True: + self.mranges.append((range.address, range.address+range.size)) + self.wait() + + # If someone ones to read memory from this target, we always return + # the same value, no matter what address it is requested + def read_memory(*args, **kwargs): + return 0xdeadbeef + + # This allow Avatar to answer to our memory read requests. + # However, we do not care about it + def sendResponse(self, id, value, success): + if success != True: + self.log.warning("RemoteMemoryRequest with id %d failed" % id) + if success == True: + self.log.debug("RemoteMemoryRequest with id %d returned 0x%x" % + (id, value)) + + # We let Avatar writes to our memory.. well.. at least we let it + # believe so + def write_memory(addr, size, val): + return True + + # We keep tracks of breakpoints + def set_breakpoint(self, line, hardware=False, temporary=False, regex=False, condition=None, ignore_count=0, thread=0): + self.bp.append(line) + + def remove_breakpoint(self, breakpoint): + # FIXME.. how do you remove a breakpoint? + # sle.bp.remove(breakpoint) does not work + pass + + #def wait(self): + #self.thread.join() + + def cont(self): + if self.state != TargetStates.RUNNING: + self.avatar.queue.put(UpdateStateMessage(self, TargetStates.RUNNING)) + self.thread = _TargetThread(self) + self.thread.start() + + def get_status(self): + if self.thread: + self.status.update({"state": self.state, "steps": self.thread.steps}) + else: + self.status.update({"state": self.state, "steps": '-'}) + return self.status + + # Since we set the memory protocol to ourself, this is important to avoid + # an infinite recursion (otherwise by default a target would call + # shutdown to all its protocols) + def shutdown(self): + pass + + def stop(self): + if self.state == TargetStates.RUNNING: + self.thread.please_stop = True + self.avatar.queue.put(UpdateStateMessage(self, TargetStates.STOPPED)) + return True + diff --git a/avatar2/targets/gdb_target.py b/avatar2/targets/gdb_target.py new file mode 100644 index 00000000000..e1da9e1c580 --- /dev/null +++ b/avatar2/targets/gdb_target.py @@ -0,0 +1,52 @@ +from avatar2.targets import Target +from avatar2.protocols.gdb import GDBProtocol + + +class GDBTarget(Target): + + def __init__(self, name, avatar, + gdb_executable='gdb', gdb_additional_args=[], gdb_port=3333, + gdb_serial_device='/dev/ttyACM0', + gdb_serial_baud_rate=38400, + gdb_serial_parity='none', + serial=False + ): + + super(GDBTarget, self).__init__(name, avatar) + + self.gdb_executable = gdb_executable + self.gdb_additional_args = gdb_additional_args + self.gdb_port = gdb_port + self.gdb_serial_device = gdb_serial_device + self.gdb_serial_baud_rate = gdb_serial_baud_rate + self.gdb_serial_parity = gdb_serial_parity + self._serial = serial + + def init(self): + + gdb = GDBProtocol(gdb_executable=self.gdb_executable, + arch=self._arch, + additional_args=self.gdb_additional_args, + avatar_queue=self.avatar.queue, origin=self) + + if not self._serial: + if gdb.remote_connect(port=self.gdb_port): + self.log.info("Connected to Target") + else: + self.log.warning("Connecting failed") + else: + if gdb.remote_connect_serial(device=self.gdb_serial_device, + baud_rate=self.gdb_serial_baud_rate, + parity=self.gdb_serial_parity): + self.log.info("Connected to Target") + else: + self.log.warning("Connecting failed") + + + self._exec_protocol = gdb + self._memory_protocol = gdb + self._register_protocol = gdb + self._signal_protocol = gdb + self._monitor_protocol = None + + self.wait() diff --git a/avatar2/targets/openocd_target.py b/avatar2/targets/openocd_target.py new file mode 100644 index 00000000000..96217aa6282 --- /dev/null +++ b/avatar2/targets/openocd_target.py @@ -0,0 +1,71 @@ +import sys +if sys.version_info < (3, 0): + from Queue import PriorityQueue +else: + from queue import PriorityQueue +import time + + + + +from avatar2.targets import Target +from avatar2.protocols.gdb import GDBProtocol +from avatar2.protocols.openocd import OpenOCDProtocol + + +class OpenOCDTarget(Target): + + def __init__(self, name, avatar, executable="openocd", + openocd_script=None, additional_args=[], + telnet_port=4444, + gdb_executable='gdb', gdb_additional_args=[], gdb_port=3333, + + ): + + super(OpenOCDTarget, self).__init__(name, avatar) + + self.executable = executable + self.openocd_script = openocd_script + self.additional_args = additional_args + self.telnet_port = telnet_port + self.gdb_executable = gdb_executable + self.gdb_additional_args = gdb_additional_args + self.gdb_port = gdb_port + + + + + def init(self): + openocd = OpenOCDProtocol(self.openocd_script, + openocd_executable=self.executable, + additional_args=self.additional_args, + telnet_port=self.telnet_port, + gdb_port=self.gdb_port, + origin=self, + output_directory=self.avatar.output_directory) + + gdb = GDBProtocol(gdb_executable=self.gdb_executable, + arch=self._arch, + additional_args=self.gdb_additional_args, + avatar_queue=self.avatar.queue, origin=self) + + time.sleep(.1) # give openocd time to start. Find a better solution? + + if openocd.connect() and gdb.remote_connect(port=self.gdb_port): + openocd.reset() + self.log.info("Connected to Target") + else: + self.log.warning("Connecting failed") + + + self._exec_protocol = gdb + self._memory_protocol = gdb + self._register_protocol = gdb + self._signal_protocol = gdb + self._monitor_protocol = openocd + + self.wait() + + + + diff --git a/avatar2/targets/panda_target.py b/avatar2/targets/panda_target.py new file mode 100644 index 00000000000..07762b8fc40 --- /dev/null +++ b/avatar2/targets/panda_target.py @@ -0,0 +1,108 @@ +from subprocess import Popen, PIPE +import json +import intervaltree + +from avatar2.targets import Target,action_valid_decorator_factory +from avatar2.targets import TargetStates +from avatar2.targets import QemuTarget +from avatar2.protocols.gdb import GDBProtocol +from avatar2.protocols.remote_memory import RemoteMemoryProtocol + +import logging + + +class PandaTarget(QemuTarget): + + def init(self, *args, **kwargs): + super(self.__class__, self).init(*args, **kwargs) + #self._monitor_protocol = self._exec_protocol + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def begin_record(self, record_name): + """ + Starts recording the execution in PANDA + + :param record_name: The name of the record file + """ + filename = "%s/%s" % (self.avatar.output_directory, record_name) + return self._monitor_protocol.execute_command('begin_record', + {'file_name': filename}) + #self._monitor_protocol._sync_request('monitor begin_record "%s"' + # % filename, 'done') + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def end_record(self): + """ + Stops recording the execution in PANDA + """ + return self._monitor_protocol.execute_command('end_record') + #self._monitor_protocol._sync_request('monitor end_record', 'done') + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def begin_replay(self, replay_name): + """ + Starts replaying a captured replay + + :param replay_name: The name of the file to be replayed + """ + self._monitor_protocol.execute_command('begin_replay', + {'file_name': replay_name}) + self.cont() + + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def end_replay(self): + """ + Stops a current ongoing replay + """ + return self._monitor_protocol.execute_command('end_replay') + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def load_plugin(self, plugin_name, plugin_args=None, file_name=None ): + """ + Loads a PANDA plugin + + :param plugin_name: The name of the plugin to be loaded + :param plugin_args: Arguments to be passed to the plugin, + aseperated by commas + :param file_name: Absolute path to the plugin shared object file, + in case that the default one should not be used + """ + + args_dict = {'plugin_name': plugin_name} + if plugin_args: + args_dict['plugin_args'] = plugin_args + if file_name: + args_dict['file_name'] = file_name + + return self._monitor_protocol.execute_command('load_plugin', args_dict) + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def unload_plugin(self, plugin_name): + """ + Unloads a PANDA plugin + + :param plugin_name: The name of the plugin to be unloaded + :return: True if the requested plugin was present + """ + full_plugin_name = 'panda_%s.so' % plugin_name + for plugin_dict in self.list_plugins(): + if plugin_dict['name'] == full_plugin_name: + self._monitor_protocol.execute_command('unload_plugin', + {'index' : plugin_dict['index']}) + return True + return False + + + + + @action_valid_decorator_factory(TargetStates.STOPPED, '_monitor_protocol') + def list_plugins(self): + """ + Lists the laoded PANDA plugins + + :return: a list with the loaded panda_plugins + """ + return self._monitor_protocol.execute_command('list_plugins') + + diff --git a/avatar2/targets/qemu_target.py b/avatar2/targets/qemu_target.py new file mode 100644 index 00000000000..2e0786af81c --- /dev/null +++ b/avatar2/targets/qemu_target.py @@ -0,0 +1,175 @@ +from subprocess import Popen, PIPE +import json +import intervaltree + +from avatar2.targets import Target +from avatar2.protocols.gdb import GDBProtocol +from avatar2.protocols.qmp import QMPProtocol +from avatar2.protocols.remote_memory import RemoteMemoryProtocol + +import logging + + +class QemuTarget(Target): + """ + """ + + QEMU_CONFIG_FILE = "conf.json" + + def __init__(self, name, avatar, executable="qemu-system-", + cpu_model=None, firmware=None, + gdb_executable='gdb', gdb_port=3333, + additional_args=[], gdb_additional_args=[], + qmp_port=3334, + entry_address=0x00): + super(QemuTarget, self).__init__(name, avatar) + + # Qemu parameters + self.executable = executable + self.fw = firmware + self.cpu_model = cpu_model + self.entry_address = entry_address + self.additional_args = additional_args + + # gdb parameters + self.gdb_executable = gdb_executable + self.gdb_port = gdb_port + self.gdb_additional_args = gdb_additional_args + + self.qmp_port = qmp_port + + self._process = None + self._entry_address = entry_address + self._memory_mapping = avatar.memoryRanges + + self.rmem_rx_queue_name = '/%s_rx_queue' % name + self.rmem_tx_queue_name = '/%s_tx_queue' % name + + def assemble_cmd_line(self): + executable_name = [self.executable + self._arch.qemu_name] + machine = ["-machine", "configurable"] + kernel = ["-kernel", "%s/%s" % + (self.avatar.output_directory, self.QEMU_CONFIG_FILE)] + gdb_option = ["-gdb", "tcp::" + str(self.gdb_port)] + stop_on_startup = ["-S"] + nographic = ["-nographic"] #, "-monitor", "/dev/null"] + qmp = ['-qmp','tcp:127.0.0.1:%d,server,nowait' % self.qmp_port] + + return executable_name + machine + kernel + gdb_option \ + + stop_on_startup + self.additional_args + nographic + qmp + + def shutdown(self): + if self._process is not None: + self._process.kill() + self._process = None + super(QemuTarget, self).shutdown() + + + + def _serialize_memory_mapping(self): + ret = [] + for (start, end, mr) in self._memory_mapping: + mr_dict = {} + mr_dict['name'] = mr.name + mr_dict['address'] = mr.address + mr_dict['size'] = mr.size + mr_dict['permissions'] = mr.permissions + if hasattr(mr, 'qemu_name'): + mr_dict['qemu_name'] = mr.qemu_name + mr_dict['properties'] = [] + mr_dict['bus'] = 'sysbus' + if mr.qemu_name == 'avatar-rmemory': + size_properties = {'type': 'uint32', + 'value': mr.size, + 'name': 'size'} + mr_dict['properties'].append(size_properties) + address_properties = {'type': 'uint64', + 'value': mr.address, + 'name': 'address'} + mr_dict['properties'].append(address_properties) + rx_queue_properties = {'type': 'string', + 'value': self.rmem_rx_queue_name, + 'name': 'rx_queue_name'} + mr_dict['properties'].append(rx_queue_properties) + tx_queue_properties = {'type': 'string', + 'value': self.rmem_tx_queue_name, + 'name': 'tx_queue_name'} + mr_dict['properties'].append(tx_queue_properties) + + + elif hasattr(mr, 'qemu_properties'): + mr_dict['properties'].append(mr.qemu_properties) + elif hasattr(mr, 'file') and mr.file is not None: + mr_dict['file'] = mr.file + ret.append(mr_dict) + return ret + + def generate_configuration(self): + """ + Generates the configuration passed to avatar-qemus configurable machine + """ + conf_dict = {} + if self.cpu_model is not None: + conf_dict['cpu_model'] = self.cpu_model + if self.fw is not None: + conf_dict['kernel'] = self.fw + conf_dict['entry_address'] = self.entry_address + if not self._memory_mapping.is_empty(): + conf_dict['memory_mapping'] = self._serialize_memory_mapping() + else: + self.log.warning("The memory mapping of QEMU is empty.") + return conf_dict + + #def add_memory_range(self, mr, **kwargs): + #self._memory_mapping[mr.address: mr.address + mr.size] = mr + # TODO: add qemu specific properties to the memory region object + + def init(self): + """ + Spawns a Qemu process and connects to it + """ + cmd_line = self.assemble_cmd_line() + + with open("%s/%s" % (self.avatar.output_directory, + self.QEMU_CONFIG_FILE), "w") as conf_file: + conf_dict = self.generate_configuration() + json.dump(conf_dict, conf_file) + + with open("%s/%s_out.txt" % (self.avatar.output_directory, self.name) + ,"wb") as out,\ + open("%s/%s_err.txt" % (self.avatar.output_directory, self.name) + ,"wb") as err: + self._process = Popen(cmd_line, stdout=out, stderr=err) + self.log.debug("QEMU command line: %s" % ' '.join(cmd_line)) + self.log.info("QEMU process running") + + gdb = GDBProtocol(gdb_executable=self.gdb_executable, + arch=self.avatar.arch, + additional_args=self.gdb_additional_args, + avatar_queue=self.avatar.queue, origin=self) + qmp = QMPProtocol(self.qmp_port, origin=self) # TODO: Implement QMP + + + if 'avatar-rmemory' in [i[2].qemu_name for i in + self._memory_mapping.iter() if + hasattr(i[2],'qemu_name')]: + rmp = RemoteMemoryProtocol(self.rmem_tx_queue_name, + self.rmem_rx_queue_name, + self.avatar.queue, self) + else: + rmp = None + + self._exec_protocol = gdb + self._memory_protocol = gdb + self._register_protocol = gdb + self._signal_protocol = gdb + self._monitor_protocol = qmp + self._remote_memory_protocol = rmp + + if gdb.remote_connect(port=self.gdb_port) and qmp.connect(): + self.log.info("Connected to remote target") + else: + self.log.warning("Connection to remote target failed") + if rmp: + rmp.connect() + self.wait() diff --git a/avatar2/targets/target.py b/avatar2/targets/target.py new file mode 100644 index 00000000000..c9446e97c96 --- /dev/null +++ b/avatar2/targets/target.py @@ -0,0 +1,253 @@ +from threading import Thread, Event +from enum import Enum +import logging +from functools import wraps + + +def action_valid_decorator_factory(state, protocol): + """ + This decorator factory is used to generate decorators which verify that + requested actions on a target, such as step(), stop(), read_register(), + write_register() and so on are actually executable. + + :param state: The required state of the Target + :type state: An entry of the Enum TargetStates + :param protocol: The protocol required to execute the action. + :type protocol: str + """ + def decorator(func): + @wraps(func) + def check(self, *args, **kwargs): + if getattr(self, protocol) == None: + raise Exception( + "%s() requested but %s is undefined." % + (func.__name__, protocol)) + if self.state != state: + raise Exception("%s() requested but Target is %s" % + (func.__name__, TargetStates(self.state).name)) + return func(self, *args, **kwargs) + return check + return decorator + + + + + +class TargetStates(Enum): + """ + A simple Enum for the different states a target can be in. + """ + CREATED = 0x1 + INITIALIZED = 0x2 + STOPPED = 0x4 + RUNNING = 0x8 + SYNCHING = 0x10 + EXITED = 0x20 + +class Target(object): + """The Target object is one of Avatars core concept, as Avatar orchestrate + different targets. + While the generic target has no implementation, it provides an insight over + all the functions a Target MUST implement + """ + + + def __init__(self, name, avatar): + super(Target, self).__init__() + self.state = TargetStates.CREATED + + self.name = name + self.avatar = avatar + self.status = {} + self._arch = avatar.arch + self._exec_protocol = None + self._memory_protocol = None + self._register_protocol = None + self._signal_protocol = None + self._monitor_protocol = None + self._remote_memory_protocol = None + + self.state = TargetStates.CREATED + self._no_state_update_pending = Event() + + self.log = logging.getLogger('%s.targets.%s' % (avatar.log.name, name)) + log_file = logging.FileHandler('%s/%s.log' % (avatar.output_directory, name)) + formatter = logging.Formatter('%(asctime)s | %(name)s.%(levelname)s | %(message)s') + log_file.setFormatter(formatter) + self.log.addHandler(log_file) + + + def init(self): + """ + Initializes the target to start the analyses + """ + pass + + def shutdown(self): + """ + Shutdowns the target + """ + if self._exec_protocol: + self._exec_protocol.shutdown() + self._exec_protocol = None + if self._memory_protocol: + self._memory_protocol.shutdown() + self._memory_protocol = None + if self._register_protocol: + self._register_protocol.shutdown() + self._register_protocol = None + if self._signal_protocol: + self._signal_protocol.shutdown() + self._signal_protocol = None + if self._monitor_protocol: + self._monitor_protocol.shutdown() + self._monitor_protocol = None + if self._remote_memory_protocol: + self._remote_memory_protocol.shutdown() + self._remote_memory_protocol = None + + @action_valid_decorator_factory(TargetStates.STOPPED, '_exec_protocol') + def cont(self): + """ + Continues the execution of the target + :returns: True on success + """ + self._no_state_update_pending.clear() + return self._exec_protocol.cont() + + + @action_valid_decorator_factory(TargetStates.RUNNING, '_exec_protocol') + def stop(self): + """ + Stops the execution of the target + """ + self._no_state_update_pending.clear() + return self._exec_protocol.stop() + + + @action_valid_decorator_factory(TargetStates.STOPPED, '_exec_protocol') + def step(self): + """ + Steps one instruction + """ + self._no_state_update_pending.clear() + return self._exec_protocol.step() + + + @action_valid_decorator_factory(TargetStates.STOPPED, '_memory_protocol') + def write_memory(self, address, size, value, num_words=1, raw=False): + """ + Writing to memory of the target + + :param address: The address from where the memory-write should + start + :param size: The size of the memory write + :param value: The actual value written to memory + :type val: int if num_words == 1 and raw == False + list if num_words > 1 and raw == False + str or byte if raw == True + :param num_words: The amount of words to read + :param raw: Specifies whether to write in raw or word mode + :returns: True on success else False + """ + return self._memory_protocol.write_memory(address, size, value, + num_words, raw) + + + @action_valid_decorator_factory(TargetStates.STOPPED, '_memory_protocol') + def read_memory(self, address, size, words=1, raw=False): + """ + Reading from memory of the target + + :param address: The address to read from + :param size: The size of a read word + :param words: The amount of words to read (default: 1) + :param raw: Whether the read memory is returned unprocessed + :return: The read memory + """ + return self._memory_protocol.read_memory(address, size, words, raw) + + + @action_valid_decorator_factory(TargetStates.STOPPED, '_register_protocol') + def write_register(self, register, value): + """ + Writing a register to the target + + :param register: The name of the register + :param value: The actual value written to the register + """ + return self._register_protocol.write_register(register, value) + + @action_valid_decorator_factory(TargetStates.STOPPED, '_register_protocol') + def read_register(self, register): + """ + Reading a register from the target + + :param register: The name of the register + :return: The actual value read from the register + """ + return self._register_protocol.read_register(register) + + @action_valid_decorator_factory(TargetStates.STOPPED, '_exec_protocol') + def set_breakpoint(self, line, hardware=False, temporary=False, regex=False, + condition=None, ignore_count=0, thread=0): + """Inserts a breakpoint + + :param bool hardware: Hardware breakpoint + :param bool tempory: Tempory breakpoint + :param str regex: If set, inserts breakpoints matching the regex + :param str condition: If set, inserts a breakpoint with the condition + :param int ignore_count: Amount of times the bp should be ignored + :param int thread: Threadno in which this breakpoints should be added + """ + return self._exec_protocol.set_breakpoint(line, hardware=hardware, + temporary=temporary, + regex=regex, + condition=condition, + ignore_count=ignore_count, + thread=thread) + + @action_valid_decorator_factory(TargetStates.STOPPED, '_exec_protocol') + def set_watchpoint(self, variable, write=True, read=False): + """Inserts a watchpoint + + :param variable: The name of a variable or an address to watch + :param bool write: Write watchpoint + :param bool read: Read watchpoint + """ + return self._exec_protocol.set_watchpoint(variable, + write=write, + read=read) + + @action_valid_decorator_factory(TargetStates.STOPPED, '_exec_protocol') + def remove_breakpoint(self, bkptno): + """Deletes a breakpoint""" + return self._exec_protocol.remove_breakpoint(bkptno) + + + def update_state(self, state): + self.log.info("State changed to to %s" % TargetStates(state)) + self.state = state + self._no_state_update_pending.set() + + + def wait(self): + while True: + self._no_state_update_pending.wait(.1) + if self.state == TargetStates.STOPPED and \ + self._no_state_update_pending.is_set(): + break + + def get_status(self): + """ + Returns useful information about the target as a dict. + """ + self.status['state'] = self.state + return self.status + + + ###generic aliases### + wr = write_register + rr = read_register + rm = read_memory + wm = write_memory diff --git a/avatar2/watchmen.py b/avatar2/watchmen.py new file mode 100644 index 00000000000..610c36e98db --- /dev/null +++ b/avatar2/watchmen.py @@ -0,0 +1,109 @@ +from threading import Thread +from functools import wraps + +watched_types = { + 'StateTransfer', + 'BreakpointHit', + 'UpdateState', + 'RemoteMemoryRead', + 'RemoteMemoryWrite', + 'AvatarGetStatus', +} + +BEFORE = 'before' +AFTER = 'after' + +def watch(watched_type): + """ + Decorator for the watchmen system + """ + def decorator(func): + @wraps(func) + def watchtrigger(self, *args, **kwargs): + self.watchmen.t(watched_type, BEFORE, *args, **kwargs) + ret = func(self, *args, **kwargs) + kwargs.update({'watched_return': ret}) + self.watchmen.t(watched_type, AFTER, *args, **kwargs) + return ret + return watchtrigger + return decorator + + +class asyncReaction(Thread): + def __init__(self, avatar, callback, *args, **kwargs): + super(asyncReaction, self).__init__() + self.avatar = avatar + self.callback = callback + self.args = args + self.kwargs = kwargs + def run(self): + self.callback(self.avatar, *self.args, **self.kwargs) + + +class WatchedEvent(object): + + def __init__(self, type, when, callback, async, *args, **kwargs): + self._callback = callback + self.type = type + self.when = when + self.async = async + + def react(self, avatar, *args, **kwargs): + if self._callback == None: + raise Exception("No callback defined for watchmen of type %s" % + self.type) + else: + if self.async: + thread = asyncReaction(avatar, self._callback, *args, **kwargs) + thread.start() + else: + self._callback(avatar, *args, **kwargs) + + + +class Watchmen(object): + """ + """ + + def __init__(self, avatar): + self._watched_events = {} + self._avatar = avatar + self.watched_types = watched_types + + for e in self.watched_types: + self._watched_events[e] = [] + + def add_watch_types(self, watched_types): + self.watched_types.update(watched_types) + for e in watched_types: + self._watched_events[e] = [] + + def add_watchman(self, type, when=BEFORE, callback=None, async=False, *args, **kwargs): + + if type not in self.watched_types: + raise Exception("Requested event_type does not exist") + if when not in (BEFORE, AFTER): + raise Exception("Watchman has to be invoked \'before\' or \'after\'!") + + w = WatchedEvent(type, when, callback, async, *args, **kwargs) + self._watched_events[type].append(w) + return w + + add = add_watchman + + def remove_watchman(self, type, watchman): + if type not in self.watched_types: + raise Exception("Requested event_type does not exist") + self._watched_events[type].remove(watchman) + + + def trigger(self, type, when, *args, **kwargs): + for watchman in self._watched_events[type]: + if watchman.when == when: + watchman.react(self._avatar, *args, **kwargs) + + t = trigger + + + + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000000..ff4e8cc2597 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = avatar +SOURCEDIR = source +BUILDDIR = ../../avatar2-docs + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/avatar2.archs.rst b/docs/source/avatar2.archs.rst new file mode 100644 index 00000000000..54a2b63cce5 --- /dev/null +++ b/docs/source/avatar2.archs.rst @@ -0,0 +1,30 @@ +avatar2\.archs package +====================== + +Submodules +---------- + +avatar2\.archs\.arm module +-------------------------- + +.. automodule:: avatar2.archs.arm + :members: + :undoc-members: + :show-inheritance: + +avatar2\.archs\.x86 module +-------------------------- + +.. automodule:: avatar2.archs.x86 + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: avatar2.archs + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/avatar2.peripherals.rst b/docs/source/avatar2.peripherals.rst new file mode 100644 index 00000000000..738218148f0 --- /dev/null +++ b/docs/source/avatar2.peripherals.rst @@ -0,0 +1,30 @@ +avatar2\.peripherals package +============================ + +Submodules +---------- + +avatar2\.peripherals\.avatar\_peripheral module +----------------------------------------------- + +.. automodule:: avatar2.peripherals.avatar_peripheral + :members: + :undoc-members: + :show-inheritance: + +avatar2\.peripherals\.nucleo\_usart module +------------------------------------------ + +.. automodule:: avatar2.peripherals.nucleo_usart + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: avatar2.peripherals + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/avatar2.plugins.rst b/docs/source/avatar2.plugins.rst new file mode 100644 index 00000000000..0f0a34748fe --- /dev/null +++ b/docs/source/avatar2.plugins.rst @@ -0,0 +1,30 @@ +avatar2\.plugins package +======================== + +Submodules +---------- + +avatar2\.plugins\.instruction\_forwarder module +----------------------------------------------- + +.. automodule:: avatar2.plugins.instruction_forwarder + :members: + :undoc-members: + :show-inheritance: + +avatar2\.plugins\.orchestrator module +------------------------------------- + +.. automodule:: avatar2.plugins.orchestrator + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: avatar2.plugins + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/avatar2.protocols.rst b/docs/source/avatar2.protocols.rst new file mode 100644 index 00000000000..f4d90823619 --- /dev/null +++ b/docs/source/avatar2.protocols.rst @@ -0,0 +1,46 @@ +avatar2\.protocols package +========================== + +Submodules +---------- + +avatar2\.protocols\.gdb module +------------------------------ + +.. automodule:: avatar2.protocols.gdb + :members: + :undoc-members: + :show-inheritance: + +avatar2\.protocols\.openocd module +---------------------------------- + +.. automodule:: avatar2.protocols.openocd + :members: + :undoc-members: + :show-inheritance: + +avatar2\.protocols\.qmp module +------------------------------ + +.. automodule:: avatar2.protocols.qmp + :members: + :undoc-members: + :show-inheritance: + +avatar2\.protocols\.remote\_memory module +----------------------------------------- + +.. automodule:: avatar2.protocols.remote_memory + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: avatar2.protocols + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/avatar2.rst b/docs/source/avatar2.rst new file mode 100644 index 00000000000..7c51a111d36 --- /dev/null +++ b/docs/source/avatar2.rst @@ -0,0 +1,57 @@ +avatar2 package +=============== + +Subpackages +----------- + +.. toctree:: + + avatar2.archs + avatar2.peripherals + avatar2.plugins + avatar2.protocols + avatar2.targets + +Submodules +---------- + +avatar2\.avatar2 module +----------------------- + +.. automodule:: avatar2.avatar2 + :members: + :undoc-members: + :show-inheritance: + +avatar2\.memory\_range module +----------------------------- + +.. automodule:: avatar2.memory_range + :members: + :undoc-members: + :show-inheritance: + +avatar2\.message module +----------------------- + +.. automodule:: avatar2.message + :members: + :undoc-members: + :show-inheritance: + +avatar2\.watchmen module +------------------------ + +.. automodule:: avatar2.watchmen + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: avatar2 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/avatar2.targets.rst b/docs/source/avatar2.targets.rst new file mode 100644 index 00000000000..dd488dfc359 --- /dev/null +++ b/docs/source/avatar2.targets.rst @@ -0,0 +1,62 @@ +avatar2\.targets package +======================== + +Submodules +---------- + +avatar2\.targets\.dummy\_target module +-------------------------------------- + +.. automodule:: avatar2.targets.dummy_target + :members: + :undoc-members: + :show-inheritance: + +avatar2\.targets\.gdb\_target module +------------------------------------ + +.. automodule:: avatar2.targets.gdb_target + :members: + :undoc-members: + :show-inheritance: + +avatar2\.targets\.openocd\_target module +---------------------------------------- + +.. automodule:: avatar2.targets.openocd_target + :members: + :undoc-members: + :show-inheritance: + +avatar2\.targets\.panda\_target module +-------------------------------------- + +.. automodule:: avatar2.targets.panda_target + :members: + :undoc-members: + :show-inheritance: + +avatar2\.targets\.qemu\_target module +------------------------------------- + +.. automodule:: avatar2.targets.qemu_target + :members: + :undoc-members: + :show-inheritance: + +avatar2\.targets\.target module +------------------------------- + +.. automodule:: avatar2.targets.target + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: avatar2.targets + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000000..f19edd960a7 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# avatar² documentation build configuration file, created by +# sphinx-quickstart on Tue Jun 13 10:59:37 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../../')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.githubpages'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] +#source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'avatar²' +copyright = '2017, eurecom-s3' +author = 'eurecom-s3' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'avatardoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'avatar.tex', 'avatar\\(\\sp{\\text{2}}\\) Documentation', + 'eurecom-s3', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'avatar', 'avatar² Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'avatar', 'avatar² Documentation', + author, 'avatar', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000000..9f406db155f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,21 @@ +.. avatar² documentation master file, created by + sphinx-quickstart on Tue Jun 13 10:59:37 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to avatar²'s API-documentation! +======================================= +This place contains the automatic generated API-documentation of avatar². + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 00000000000..2c247316283 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +avatar2 +======= + +.. toctree:: + :maxdepth: 4 + + avatar2 diff --git a/handbook/0x01_intro.md b/handbook/0x01_intro.md new file mode 100644 index 00000000000..2cc649826e8 --- /dev/null +++ b/handbook/0x01_intro.md @@ -0,0 +1,133 @@ +# What Is Avatar²? + +Avatar is an orchestration framework designed to support dynamic analysis +of embedded devices. Avatar² is the second generation of the framework, +which has been completely re-designed and re-implemented from scratch to +improve performance, usability, and support for advanced features. + +An Avatar² setup consists of three parts: + + - A set of targets + - A memory layout + - An execution plan + +**Targets** are responsible for the execution and the analysis of the firmware +code. While it is possible to run Avatar² with a single target, most +configurations will have at least two (typically an emulator and a physical +device). The **memory layout** describes the different regions of memory and +their role in the system (e.g., the fact that may be mapped to an external +peripheral or connected to a file) as well as the _memory access rules_, i.e., how +memory read and write operations needs to be forwarded between targets. +Finally, the **execution plan** tells Avatar² how the actual execution of the +firmware needs to be divided among the targets in order to achieve the analyst goal. + +If this sounds complex, it is because Avatar² is an extremely powerful and +flexible framework designed to adapt to different scenarios and support +complex configurations. However, a simple Avatar² example is quite +straightforward to write and understand. + + +# Avatar² Architecture + +The architecture of Avatar² consists of four different types of +components: the Avatar object itself, and a set of **targets**, **protocols**, and +**endpoints**. Avatar is the root-object that is responsible for orchestrating a +non-empty set of targets, which in turn communicate to their corresponding +endpoints using a number of protocols. Endpoints hereby can be anything - such as an +emulator, an analysis framework, or a physical device. Targets are instead +the python abstractions that are made available to Avatar to perform a +given analysis task. + +For clarity, the figure below gives a schematic overview over the avatar +architecture. + +``` ++------------------------------------------------------------------------------+ +| AVATAR | ++----------------+--------------------------------------------+----------------+ + | | + | | + +------+------+ +------+------+ + | Target_1 | ... | Target_n | + +------+------+ +------+------+ + | | + +-----------------------+ +-----------------------+ + | | | | | | ++----+----+ +----+----+ +----+----+ +----+----+ +----+----+ +----+----+ +|Execution| | Memory | |Register | ... |Execution| | Memory | |Register | +| Protocol| | Protocol| | Protocol| | Protocol| | Protocol| | Protocol| ++----+----+ +----+----+ +-----+---+ +----+----+ +----+----+ +-----+---+ + | | | | | | + | | | | | | + | +------+------+ | | +------+------+ | + +----+ Endpoint_1 +-----+ ... +----+ Endpoint_n +-----+ + +-------------+ +-------------+ +``` + +# "Hello World" from Avatar² + +Every respectable documentation needs to start by showing how to say hello +to the world. + +So here it is, an Avatar² script that modifies and executes a binary running +inside a gdb-server to print "Hello World!". + +```python +import os +import subprocess + +from avatar2 import * + + +filename = 'a.out' +GDB_PORT = 1234 + +# This is a bare minimum elf-file, gracefully compiled from +# https://github.com/abraithwaite/teensy +tiny_elf = (b'\x7f\x45\x4c\x46\x02\x01\x01\x00\xb3\x2a\x31\xc0\xff\xc0\xcd\x80' + b'\x02\x00\x3e\x00\x01\x00\x00\x00\x08\x00\x40\x00\x00\x00\x00\x00' + b'\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x40\x00\x38\x00\x01\x00\x00\x00\x00\x00\x00\x00' + b'\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00' + b'\x78\x00\x00\x00\x00\x00\x00\x00\x78\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x20\x00\x00\x00\x00\x00') + + + +# Hello world shellcode +shellcode = (b'\x68\x72\x6c\x64\x21\x48\xb8\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x50' + b'\x48\x89\xef\x48\x89\xe6\x6a\x0c\x5a\x6a\x01\x58\x0f\x05') + + +# Save our executable to disk +with open(filename, 'wb') as f: + f.write(tiny_elf) +os.chmod(filename, 0o744) + +# Create the avatar instance and specify the architecture for this analysis +avatar = Avatar(arch=archs.x86.X86_64) + +# Create the endpoint: a gdbserver connected to our tiny ELF file +gdbserver = subprocess.Popen('gdbserver --once 127.0.0.1:%d a.out' % GDB_PORT, shell=True) + +# Create the corresponding target, using the GDBTarget backend +target = avatar.add_target("gdb", GDBTarget, gdb_port=GDB_PORT) + +# Initialize the target. +# This usually connects the target to the endpoint +target.init() + +# Now it is possible to interact with the target. +# For example, we can insert our shellcode at the current point of execution +target.write_memory(target.read_register('pc'), len(shellcode), + shellcode, raw=True) + +# We can now resume the execution in our target +# You should see hello world printed on your screen! :) +target.cont() + +# Clean up! +os.remove(filename) +avatar.shutdown() +``` diff --git a/handbook/0x02_targets.md b/handbook/0x02_targets.md new file mode 100644 index 00000000000..c8d006cba3a --- /dev/null +++ b/handbook/0x02_targets.md @@ -0,0 +1,136 @@ +# Targets + +The first information that Avatar² requires is a set of targets required +for the analyst job. Let's have a look how to add a simple target: + +```python +from avatar2 import * + +avatar = Avatar() + +qemu = avatar.add_target('qemu1', QemuTarget) +``` + +This instantiates a QemuTarget object and assign it a mnemonic name +("qemu1"). From now on we can interact with the target using the +_qemu_ variable, or we can ask avatar to work on it +by specifying its name. + +```python +>>> from avatar2 import * +>>> avatar = Avatar() +>>> qemu = avatar.add_target('qemu1', QemuTarget) +>>> avatar.targets['qemu1'] == qemu +True +``` + +It is important to note that while a target can be configured after it has been created, +this should **only** be performed before the _init()_ procedure of the target is called. + +```python +from avatar2 import * + +avatar = Avatar() + +# add qemu and specify that its gdbserver should listen at address 1234 +qemu = avatar.add_target('qemu1', QemuTarget, gdb_port=1234) + +# do other things, initialize memory ranges and so on +[...] + +# valid: change the gdbserver listening port to 2234 +qemu.gdb_port = 2234 + +# This will initialize the qemu target +qemu.init() + +# invalid: qemu and its gdbserver are already spawned, changing the gdb port +# here will not have any effect. +qemu.gdb_port = 3234 +``` + +To sum up, defining a target is seemingly simple at first, but the devil is +in the details: as a variety of different targets are supported and each +target accepts its own set of _target specific arguments_. So, let's look +at the different targets that are currently supported by Avatar², alongside +with the arguments they accept. + +## GDBTarget +This is probably one of the most intuitive targets: It will simply connect to +an existing gdbserver instance, either via a TCP or a serial connection, +based on the value of it's _serial_ argment. Therefore, it is not important +whether the gdbserver runs on a remote physical target or is just locally +spawned on the Avatar² host system. +For a flexible configuration, he following keywords can be passed when adding a +GDBTarget: + +| name | type | default | purpose | +|---------------------|-------|----------------|----------------------------------------------------------------------------------------------------------| +| gdb_executable | str | 'gdb' | Path to the gdb-executable which shall be used | +| gdb_additional_args | [str] | [] | List with additional arguments which shall be passed to gdb | +| gdb_port | int | 3333 | Port on which the gdbserver being connected to listens | +| serial | bool | False | Whether to connect to a gdbserver via serial, than via tcp. Enables the gdb_serial parameters to be used | +| gdb_serial_device | str | '/dev/ttyACM0' | The serial device we want to connect to | +| gdb_serial_baudrate | int | 38400 | The serial baud rate | +| gdb_serial_parity | str | 'none' | The serial parity settings to be used | + +## OpenOCDTarget +The purpose of the OpenOCDTarget is the possibility to connect to physical +targets over JTAG access by using [openocd](http://openocd.org/). + +As an Avatar² host can control openocd, and, subsequently, its target using either +a connection to a gdbserver or a telnetinterface to openocd, the following +parameters can be specified on a OpenOCDTarget: + +| name | type | default | purpose | +|---------------------|-------|---------|-------------------------------------------------------------------------------------------------| +| openocd_script | str | None | *mandatory* path to an openocd script. This script normally controls the actual JTAG-connection | +| additional_args | [str] | [] | List with additional arguments which shall be passed to openocd | +| telnet_port | int | 4444 | Port for the openocd telnet server | +| gdb_executable | str | 'gdb' | Path to the gdb-executable which shall be used | +| gdb_additional_args | [str] | [] | List with additional arguments which shall be passed to gdb | +| gdb_port | int | 3333 | Port on which the gdbserver being connected to listens | + +## QemuTarget +Qemu is a full-system emulator which has beed modified for avatar² in order to +allow a complete free (hardware-) configuration of the system to be emulated and +in order to allow performant forwarding of memory from within qemu to other +targets. As these are quite impactful changes, several different configuration +options are available for QemuTargets: + + +| name | type | default | purpose | +|---------------------|-------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| executable | str | 'qemu-system-' | Path to the qemu executable which will be used, without architecture suffix. The architecture suffix gets automatically detected by avatar² | +| additional_args | [str] | [] | List with additional arguments which shall be passed to qemu | +| cpu_model | str | None | A specific cpu-model to be used by qemu | +| firmware | str | None | (optional) path to a kernel or kernel-like firmware to be used by qemu | +| qmp_port | int | 3334 | Port for the qemu monitor protocol | +| entry_address | int | 0 | Address of the first instruction to be executed | +| gdb_executable | str | 'gdb' | Path to the gdb-executable which shall be used | +| gdb_additional_args | [str] | [] | List with additional arguments which shall be passed to gdb | +| gdb_port | int | 3333 | Port on which the gdbserver being connected to listens + + +## PandaTarget +[PANDA](https://github.com/panda-re/panda) is a dynamic binary analysis platform +with a lot of useful features, among others the record and replay of an +execution and a plugin-system for varios analysis tasks during execution and +replay. +As PANDA itself is based on qemu, the avatar² PandaTarget directly inherits +from the QemuTarget and accepts all of its arguments. +However, in comparison to the above described targets, it has a variety of +_target specific methods_ for driving PANDA in the execution-phase: + +| method-name | arguments | purpose | +|---------------|-------------------------------------|-------------------------------------------------| +| begin_record | record_name | Advice PANDA to begin a record of the execution | +| end_record | - | Advice PANDA to end and ongoing record | +| begin_replay | replay_name | Replay a recorded execution | +| end_replay | - | End the ongoing replay | +| load_plugin | plugin_name, plugin_args, file_name | Load a PANDA plugin with specified arguments | +| unload_plugin | plugin_name | Unload plugin with the specified name | +| list_plugins | - | List the already loaded PANDA plugins | + +For more information about these function, we suggest to have a look at our +[autodoc](https://avatartwo.github.io/avatar2-docs). diff --git a/handbook/0x03_memory.md b/handbook/0x03_memory.md new file mode 100644 index 00000000000..1eae17bad60 --- /dev/null +++ b/handbook/0x03_memory.md @@ -0,0 +1,122 @@ +# Memory Configuration + +The second piece of information required to create a Avatar² script is the specification of +a memory layout. Avatar keeps track of all memory ranges and pushes the resulting +memory mapping that combines all ranges down to the individual targets. + +## Memory Range Definition + +Adding a memory range is straightforward. Assuming the presence of an Avatar² +object named 'avatar', it's enough to add the following line to create a basic +memory area of size 0x1000 at address 0x40000000: + +```python +dummy_range = avatar.add_memory_range(0x40000000, 0x1000) +``` + +Memory ranges are highly flexible and allow for a variety of additional +keywords during their creation, some of which may be used only by +specific classes of targets. Below is a list of all target-independent +keyword arguments which can be used during the creation of a memory range. + +| Keyword | Description | +|--------------|---------------------------------------------------------------------------| +| name | An optional name for the memory range | +| permissions | The permissions in textual representation. Default: 'rwx' | +| file | Path to a file which holds the initial contents for the memory | +| forwarded | Whether memory accesses to the range needs to be forwarded to a specific target | +| forwarded_to | If forwarding is enabled, reference to the target that will handle the memory accesses | +| emulate | Enable avatars peripheral emulation for the given memory range | + +## Memory Forwarding + +One of the core features of Avatar² is the separation between execution and +memory accesses. This yields the capability to forward memory accesses +among different targets. For instance, a firmware image can be executed +inside an emulator, while all memory access can be forwarded to the real +physical device. + +The forwarding rules themselves are set up during the configuration of the +memory ranges, by using the forwarded and forwarded_to arguments. +Let's assume we are analyzing in QEMU a physical device that contains memory-mapped peripherals. +An exemplary memory range configuration could look like the following: + +```python +mmio = avatar.add_memory_range(0x4000000, 0x10000, name='mmio', + permissions='rw-' + forwarded=True, forwarded_to=phys_device) +ram = avatar.add_memory_range(0x2000000, 0x1000000, name='ram', + permissions='rw-') +rom = avatar.add_memory_range(0x0800000, 0x1000000, name='rom', + file='./firmware.bin', + permissions='r-x') +``` + + +## Qemu-Target Peripheral Emulation Ranges + +As QEmu is a full system emulator, it also capable of emulating a large set of peripherals. +Logically, Avatar² can take advantage of this feature +by specifying the _target specific keywords_ +'qemu\_name' and 'qemu\_properties' parameters on a memory range. + +For instance, a very common device which can be emulated in QEmu +instead of forwarding its I/O accesses to the physical device is a serial +interface, as shown in the example below: + +```python + +# Properties as required by qemu +serial_qproperties = {'type' : 'serial', 'value': 0, 'name':'chardev'} + +serial = avatar.add_memory_range(0x40004c00, 0x100, name='usart', + qemu_name='stm32l1xx-usart', + qemu_properties=serial_qproperties, + permissions='rw-') + +# Provide serial I/O via tcp +qemu.additional_args = ["-serial", "tcp::1234,server,nowait"] +``` + +## Avatar² Peripheral Emulation Ranges + +Unfortunately, QEmu does not support all existing peripherals nor will every +Avatar² set-up utilize a Qemu target. +As a result Avatar² allows to specify user-defined peripheral implementations +using the AvatarPeripheral class. + +To do so, two steps are required: + +1. Create a child class from AvatarPeripheral which defines custom read and +write handler in its \_\_init\_\_ function. +2. Pass a reference of this class to the emulate keyword of a memory range. + +The example below provides an implementation of a HelloWorldPeripheral, which +returns another part of the string 'Hello World' upon every read. + +```python +from avatar2 import * + +class HelloWorldPeripheral(AvatarPeripheral): + + def hw_read(self, size): + ret = self.hello_world[:size] + self.hello_world = self.hello_world[size:] + self.hello_world[:size] + return ret + + def nop_write(self, size, value): + return True + + def __init__(self, name, address, size, **kwargs): + AvatarPeripheral.__init__(self, name, address, size) + + self.hello_world='Hello World' + + self.read_handler[0:size] = self.hw_read + self.write_handler[0:size] = self.nop_write + +[...] + +hw = avatar.add_memory_range(0x40004c00, 0x100, name='hello_world', + emulate=HelloWorldPeripheral, permissions='rw-') +``` diff --git a/handbook/0x04_execution.md b/handbook/0x04_execution.md new file mode 100644 index 00000000000..efdd307fba2 --- /dev/null +++ b/handbook/0x04_execution.md @@ -0,0 +1,152 @@ +# Execution + +After the set of targets and the memory layout have been defined, the actual +analysis part of Avatar² can take place, which we denote as the _execution-phase_. + +To tell Avatar² that the setup phase is completed and the actual execution can begin, +the targets have first to be initialized. + +```python +from avatar2 import * + +avatar = Avatar() + +# Target setup +[...] + +# Memory setup +[...] + +# Initialize all targets and prepare for execution +avatar.init_targets() +``` + +During the execution phase, Avatar² can interact with each target +to control its execution or to manipulate its memory or register values. + + +## Controlling the Target Execution + +Avatar² can control the execution of a target by using a set of functionalities +very similar to those provided by a traditional debugger. In particular, all targets +support basic functionalities for +continuing, stepping, and stopping the execution. Additionally, breakpoints and +watchpoints can also be set as long as the underlying target supports these +features. + +However, in comparison to traditional debuggers, Avatar² is not suspended while +a target is executing, as the analyst may want to setup complex +orchestration scheme involving parallel executions. Hence, +targets provide a _wait()_ method, which will force the avatar script to +wait until the target stops its execution. + +Let's see how target execution can look like in an Avatar² script: + +```python +# Get a target which we initialized before +qemu = avatar.targets['qemu1'] + +# Set a breakpoint +bkpt = qemu.set_breakpoint(0x800e34) + +# Continue execution +qemu.cont() + +# Before doing anything else, wait for the breakpoint to be hit +qemu.wait() + +# Remove the breakpoint +qemu.remove_breakpoint(bkpt) + +# Step one instruction +qemu.step() +``` + +## Controlling the Target Registers + +Avatar can inspect and modify the register state of a target in a very easy manner: + +```python + +# Get the content of a register +r0 = qemu.read_register("r0") + +# Set the content of a register +qemu.write_register("r0", 0x41414141) + +# Shorter aliases to the exact same functions above +r0 = qemu.rr("r0") +qemu.wr("r0", 0x41414141) +``` + +## Controlling the Target Memory + +Similar to the register state of a target, it is often desirable to obtain or +modify the memory content of a target, which is as simple as reading or writing +to a register: + +```python +# read 4 bytes from addres 0x20000000 +qemu.read_memory(0x20000000, 4) + +# write 4 bytes to address 0x20000000 +qemu.write_memory(0x20000000, 4, 0xdeadbeef) + +# aliases +qemu.rm(0x20000000, 4) +qemu.wm(0x20000000, 4, 0xdeadbeef) +``` + +## Transferring the Execution State between Targets + +One of the more interesting features of Avatar² is the possibility to transfer +the state between different targets during their execution, in order to allow +a successfull orchestration. +Take a look at the following example, which includes the target setup, the memory +layout specification, and the transfer of execution (and state) +from one target to another: + +```python +from avatar2 import * + +sample = 'firmware.bin' +openocd_conf = 'nucleo-l152re.cfg' + +# Create avatar instance with custom output directory +avatar = Avatar(output_directory='/tmp/myavatar') + +# Add first target +qemu = avatar.add_target("qemu", QemuTarget, + gdb_executable="arm-none-eabi-gdb", + firmware=sample, cpu_model="cortex-m3" + executable="targets/qemu/arm-softmmu/qemu-system-") + +# Add the second target +nucleo = avatar.add_target("nucleo", OpenOCDTarget, + gdb_executable="arm-none-eabi-gdb", + openocd_script=openocd_conf) + +# Set up custom gdb ports to avoid collisions +qemu.gdb_port = 1234 +nucleo.gdb_port = 1235 + +# Specify first memory range +rom = avatar.add_memory_range(0x08000000, 0x1000000, 'rom', + file=sample) +# Specify second memory range +ram = avatar.add_memory_range(0x20000000, 0x14000, 'ram') + +# Initialize Targets +avatar.init_targets() + +# Execute on the nucleo up to a specific address +nucleo.set_breakpoint(0x800B570) +nucleo.cont() +nucleo.wait() + +# Transfer the state over to qemu +avatar.transfer_state(nucleo, qemu, synch_regs=True, synched_ranges=[ram]) + +# Continue execution on qemu +qemu.cont() +``` diff --git a/handbook/0x05_watchmen.md b/handbook/0x05_watchmen.md new file mode 100644 index 00000000000..64dc8c65521 --- /dev/null +++ b/handbook/0x05_watchmen.md @@ -0,0 +1,39 @@ +# Watchmen + +Avatar² allows the user to hook various events during the +orchestration. These hooks are user defined callbacks and allow to change or +inspect the analyses state before or after the according event occured. + +The interface for adding such a hook looks as follows: +```python +from avatar2 import * + +def my_callback(avatar, *args, **kwargs): + print("StateTransfer occured!") + +avatar = Avatar() +avatar.watchmen.add_watchmen('StateTransfer', 'after', my_callback) +``` + +The first argument is the event to be hooked, the second argument +specifies whether the callback shall be executed before or after the event is +handled, and the third argument is a reference to the callback function to be + executed. + +For now, avatar² supports to hook the following events: + +| event-name | trigger | additional vars | +|-------------------|--------------------------------------------------|----------------------------------------------------| +| StateTransfer | Transfering the state from one target to another | from_target, to_target, synch_regs, synched_ranges | +| BreakpointHit | A breakpoint is reached | BreakpointMessage | +| UpdateState | A target changes its state | UpdateStateMessage | +| RemoteMemoryRead | A forwarded memory read is happening | RemoteMemoryReadMessage | +| RemoteMemoryWrite | A forwarded memory write is happening | RemoteMemoryWriteMessage | +| AvatarGetStatus | The current status of avatar is requested | - | + +While all of the callbacks will get the avatar-instance passed as the first +argument, additional arguments may be passed as well, as shown in the table. +Quite notable, a lot of events will allow to inspect _AvatarMessages_. These +are envelops containing all important information about a specific event. +These messages are normally generated by the targets' protocols and are passed to +the avatar instance which dispatches them. diff --git a/setup.py b/setup.py new file mode 100644 index 00000000000..0d456f7b43f --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from distutils.core import setup + +setup( + name='avatar2', + version='1.0', + packages=['avatar2', + 'avatar2/archs', + 'avatar2/targets', + 'avatar2/protocols', + 'avatar2/peripherals', + 'avatar2/plugins' + ], + install_requires=[ + 'pygdbmi>=0.7.3.1', + 'intervaltree', + 'ipython==5.3', + 'posix_ipc>=1.0.0', + 'capstone>=3.0.4' + ], + url='http://www.s3.eurecom.fr/tools/avatar/', + description='Dynamic firmware analysis' +) diff --git a/targets/build_panda.sh b/targets/build_panda.sh new file mode 100755 index 00000000000..3b70c31567a --- /dev/null +++ b/targets/build_panda.sh @@ -0,0 +1,22 @@ +#!/bin/bash +distr=`cat /etc/issue` +ci_distr="Ubuntu 16.04.2 LTS \n \l" + +if [[ "$distr" == "$ci_distr" ]] +then + echo "deb-src http://archive.ubuntu.com/ubuntu/ xenial-security main restricted" >> /etc/apt/sources.list + apt-get update + apt-get build-dep -y qemu +fi + +cd `dirname "$BASH_SOURCE"`/src/ +git submodule update --init avatar-panda + +cd avatar-panda +git submodule update --init dtc + +mkdir -p ../../build/panda/panda +cd ../../build/panda/panda +../../../src/avatar-panda/configure --disable-sdl --target-list=arm-softmmu +make -j4 + diff --git a/targets/build_qemu.sh b/targets/build_qemu.sh new file mode 100755 index 00000000000..b43f400660b --- /dev/null +++ b/targets/build_qemu.sh @@ -0,0 +1,22 @@ +#!/bin/bash +distr=`cat /etc/issue` +ci_distr="Ubuntu 16.04.2 LTS \n \l" + +if [[ "$distr" == "$ci_distr" ]] +then + echo "deb-src http://archive.ubuntu.com/ubuntu/ xenial-security main restricted" >> /etc/apt/sources.list + apt-get update + apt-get build-dep -y qemu +fi + +cd `dirname "$BASH_SOURCE"`/src/ +git submodule update --init avatar-qemu + +cd avatar-qemu +git submodule update --init dtc + +mkdir -p ../../build/qemu/ +cd ../../build/qemu +../../src/avatar-qemu/configure --disable-sdl --target-list=arm-softmmu +make -j4 + diff --git a/targets/src/avatar-panda b/targets/src/avatar-panda new file mode 160000 index 00000000000..7182a786581 --- /dev/null +++ b/targets/src/avatar-panda @@ -0,0 +1 @@ +Subproject commit 7182a7865818bc5f6a0638398baccb5698a162eb diff --git a/targets/src/avatar-qemu b/targets/src/avatar-qemu new file mode 160000 index 00000000000..31244c807c8 --- /dev/null +++ b/targets/src/avatar-qemu @@ -0,0 +1 @@ +Subproject commit 31244c807c8da404b8727b52d873a72bb085020e