mirror of
https://github.com/avatartwo/avatar2.git
synced 2026-10-11 22:29:59 +00:00
Initial commit
This commit is contained in:
+102
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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/).
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
from .x86 import *
|
||||
from .arm import *
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
from .avatar_peripheral import *
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
@@ -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 *
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Author: Davide Balzarotti <[email protected]>
|
||||
# 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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -0,0 +1,7 @@
|
||||
avatar2
|
||||
=======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
avatar2
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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).
|
||||
@@ -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-')
|
||||
```
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
)
|
||||
Executable
+22
@@ -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
|
||||
|
||||
Executable
+22
@@ -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
|
||||
|
||||
Submodule
+1
Submodule targets/src/avatar-panda added at 7182a78658
Submodule
+1
Submodule targets/src/avatar-qemu added at 31244c807c
Reference in New Issue
Block a user