Initial commit

This commit is contained in:
mariusmue
2017-06-13 23:37:03 +02:00
commit cedc4a8ae9
50 changed files with 4238 additions and 0 deletions
+102
View File
@@ -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
+6
View File
@@ -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
+190
View File
@@ -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.
+27
View File
@@ -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/).
+14
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
from .x86 import *
from .arm import *
+15
View File
@@ -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
+55
View File
@@ -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 = []
+306
View File
@@ -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
+24
View File
@@ -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)
+45
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
from .avatar_peripheral import *
+44
View File
@@ -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)
+136
View File
@@ -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
View File
+40
View File
@@ -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)
+165
View File
@@ -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)
View File
+600
View File
@@ -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
+109
View File
@@ -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
+148
View File
@@ -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])
+182
View File
@@ -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
+7
View File
@@ -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 *
+140
View File
@@ -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
+52
View File
@@ -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()
+71
View File
@@ -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()
+108
View File
@@ -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')
+175
View File
@@ -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()
+253
View File
@@ -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
+109
View File
@@ -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
+20
View File
@@ -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)
+30
View File
@@ -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:
+30
View File
@@ -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:
+30
View File
@@ -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:
+46
View File
@@ -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:
+57
View File
@@ -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:
+62
View File
@@ -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:
+158
View File
@@ -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'),
]
+21
View File
@@ -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`
+7
View File
@@ -0,0 +1,7 @@
avatar2
=======
.. toctree::
:maxdepth: 4
avatar2
+133
View File
@@ -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()
```
+136
View File
@@ -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).
+122
View File
@@ -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-')
```
+152
View File
@@ -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()
```
+39
View File
@@ -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.
+22
View File
@@ -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'
)
+22
View File
@@ -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
+22
View File
@@ -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