Skip to content

Commit

Permalink
Merge PR #97: Add UNIX socket support for GDBProtocol and protocol users
Browse files Browse the repository at this point in the history
This PR adds support for using UNIX sockets for GDB connections instead
of TCP.

One aspect I didn't consider was where the sockets should be created by
default since I use is not None to detect if UNIX should be used. Maybe
sockets should be automatically placed in the avatar2 output directory
(which should be unique per instance), but this would require more
invasive changes to the KWArgs for transport selection.

Co-authored-by: Grant Hernandez <[email protected]>
Co-authored-by: sampl <[email protected]>
  • Loading branch information
rawsample and grant-h committed Jan 26, 2022
2 parents 37fa74d + 4f98afb commit 3f26761
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 68 deletions.
93 changes: 41 additions & 52 deletions avatar2/protocols/gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,7 @@ def set_abi(self, abi):

return ret

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
"""

def _remote_connect_common(self, remote_string, transport_setup=None):
req = ['-gdb-set', 'target-async', 'on']
ret, resp = self._sync_request(req, GDB_PROT_DONE)
if not ret:
Expand All @@ -356,8 +348,6 @@ def remote_connect(self, ip='127.0.0.1', port=3333):

req = ['-gdb-set', 'architecture', self._arch.gdb_name]
ret, resp = self._sync_request(req, GDB_PROT_DONE)


if not ret:
self.log.critical(
"Unable to set architecture, received response: %s" %
Expand All @@ -379,7 +369,11 @@ def remote_connect(self, ip='127.0.0.1', port=3333):
resp)
raise Exception("GDBProtocol was unable to set endianness")

req = ['-target-select', 'remote', '%s:%d' % (ip, int(port))]
# transport unique setup, if applicable
if transport_setup:
transport_setup()

req = ['-target-select', 'remote', remote_string]
ret, resp = self._sync_request(req, GDB_PROT_CONN)

self.log.debug(
Expand All @@ -393,6 +387,26 @@ def remote_connect(self, ip='127.0.0.1', port=3333):

return ret

def remote_connect(self, ip='127.0.0.1', port=3333):
"""
connect to a remote gdb server via TCP
: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
"""

return self._remote_connect_common('%s:%d' % (ip, int(port)))

def remote_connect_unix(self, unix_path):
"""
connect to a remote gdb server via a UNIX domain socket path
:param unix_path: path of the UNIX domain socket
:returns: True on successful connection
"""
return self._remote_connect_common(unix_path)

def remote_connect_serial(self, device='/dev/ttyACM0', baud_rate=38400,
parity='none'):
"""
Expand All @@ -404,49 +418,24 @@ def remote_connect_serial(self, device='/dev/ttyACM0', baud_rate=38400,
:returns: True on successful connection
"""

req = ['-gdb-set', 'architecture', self._arch.gdb_name]
ret, resp = self._sync_request(req, GDB_PROT_DONE)
if not ret:
self.log.critical(
"Unable to set architecture, received response: %s" %
resp)
raise Exception(("GDBProtocol was unable to set the architecture\n"
"Did you select the right gdb_executable?"))

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")
def serial_setup():
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', '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)
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")

self.update_target_regs()
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")

return ret
return self._remote_connect_common(device, transport_setup=serial_setup)

def remote_disconnect(self):
"""
Expand Down
9 changes: 8 additions & 1 deletion avatar2/targets/gdb_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class GDBTarget(Target):
def __init__(self, avatar,
gdb_executable=None, gdb_additional_args=None,
gdb_ip='127.0.0.1', gdb_port=3333,
gdb_unix_socket_path=None,
gdb_serial_device='/dev/ttyACM0',
gdb_serial_baud_rate=38400,
gdb_serial_parity='none',
Expand All @@ -26,6 +27,7 @@ def __init__(self, avatar,
self.gdb_additional_args = gdb_additional_args if gdb_additional_args else []
self.gdb_ip = gdb_ip
self.gdb_port = gdb_port
self.gdb_unix_socket_path = gdb_unix_socket_path
self.gdb_serial_device = gdb_serial_device
self.gdb_serial_baud_rate = gdb_serial_baud_rate
self.gdb_serial_parity = gdb_serial_parity
Expand All @@ -49,7 +51,12 @@ def init(self):
# If we are debugging a program locally,
# we do not need to establish any connections
if not self._local_binary:
if not self._serial:
if self.gdb_unix_socket_path is not None:
if gdb.remote_connect_unix(self.gdb_unix_socket_path):
self.log.info("Connected to Target")
else:
self.log.warning("Connecting failed")
elif not self._serial:
if gdb.remote_connect(ip=self.gdb_ip, port=self.gdb_port):
self.log.info("Connected to Target")
else:
Expand Down
19 changes: 17 additions & 2 deletions avatar2/targets/qemu_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
firmware=None,
gdb_executable=None,
gdb_port=3333,
gdb_unix_socket_path=None,
additional_args=None,
gdb_additional_args=None,
gdb_verbose=False,
Expand Down Expand Up @@ -58,6 +59,7 @@ def __init__(
)

self.gdb_port = gdb_port
self.gdb_unix_socket_path = gdb_unix_socket_path
self.gdb_additional_args = gdb_additional_args if gdb_additional_args else []
self.gdb_verbose = gdb_verbose

Expand Down Expand Up @@ -90,7 +92,10 @@ def assemble_cmd_line(self):

machine = ["-machine", "configurable"]
kernel = ["-kernel", self.qemu_config_file]
gdb_option = ["-gdb", "tcp::" + str(self.gdb_port)]
if self.gdb_unix_socket_path:
gdb_option = ["-gdb", "unix:%s,server,nowait" % str(self.gdb_unix_socket_path)]
else:
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]
Expand Down Expand Up @@ -301,10 +306,20 @@ def _connect_protocols(self):
self.protocols.monitor = qmp
self.protocols.remote_memory = rmp

if gdb.remote_connect(port=self.gdb_port) and qmp.connect():
connect_success = True

if self.gdb_unix_socket_path:
connect_success = connect_success and gdb.remote_connect_unix(self.gdb_unix_socket_path)
else:
connect_success = connect_success and gdb.remote_connect(port=self.gdb_port)

connect_success = connect_success and qmp.connect()

if connect_success:
self.log.info("Connected to remote target")
else:
self.log.warning("Connection to remote target failed")

if rmp:
rmp.connect()
self.wait()
38 changes: 26 additions & 12 deletions tests/test_gdbprotocol.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import unittest

from avatar2.protocols.gdb import GDBProtocol
import avatar2

import subprocess
import os
import time
import re

from os.path import dirname, realpath

import avatar2
from avatar2.protocols.gdb import GDBProtocol

from utils import unix2tcp

SLEEP_TIME = .1

PORT = 4444

SLEEP_TIME = .1
PORT = 4444
X86_REGS = [u'rax', u'rbx', u'rcx', u'rdx', u'rsi', u'rdi', u'rbp', u'rsp',
u'r8', u'r9', u'r10', u'r11', u'r12', u'r13', u'r14', u'r15',
u'rip', u'eflags', u'cs', u'ss', u'ds', u'es', u'fs', u'gs']
Expand All @@ -26,7 +27,7 @@ class GdbProtocolTestCase(unittest.TestCase):
def setUp(self):
pass

def setup_env(self, binary):
def setup_env(self, binary, unix_socket=False):

self.process = subprocess.Popen(['gdbserver', '--once', '127.0.0.1:%d' % PORT, binary],
stderr=subprocess.PIPE)
Expand All @@ -37,12 +38,18 @@ def setup_env(self, binary):
self.assertEqual(str(PORT) in out, True, out)

self.gdb = GDBProtocol(arch=avatar2.archs.X86_64)
self.gdb.remote_connect(port=PORT)

# let's resolve the base address of the binary
ret, out = self.gdb.console_command("p &main")
main_addr = int(re.search("0x[0-9a-f]+", out).group(0), 16)
self.base_address = main_addr - main_addr % 0x1000
if unix_socket is True:
socket_path ='/tmp/test_socket'
unix2tcp(socket_path, "127.0.0.1", PORT)
self.gdb.remote_connect_unix(socket_path)

else:
self.gdb.remote_connect(port=PORT)

# Base addresses can change across kernel versions due to PIE binaries
self.base_address = self.gdb.get_symbol("main")[1] & ~0xfff


def wait_stopped(self):
# As we do not have access to avatar synchronizing target states
Expand Down Expand Up @@ -113,9 +120,16 @@ def test_watchpoint(self):
self.assertEqual(ret, 0x464c457f, ret)


class GDBProtocolTestCaseOnInfiniteLoop(GdbProtocolTestCase):
class GDBProtocolWithUnixSocketTestCase(GDBProtocolTestCaseOnHelloWorld):

def setUp(self):
dir_path = dirname(realpath(__file__))
binary = '%s/binaries/hello_world' % dir_path
self.setup_env(binary, unix_socket=True)


class GDBProtocolTestCaseOnInfiniteLoop(GdbProtocolTestCase):

def setUp(self):
dir_path = dirname(realpath(__file__))
binary = '%s/binaries/infinite_loop' % dir_path
Expand Down
16 changes: 15 additions & 1 deletion tests/test_qemutarget.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,18 @@ class QemuTargetTestCase(unittest.TestCase):


def setUp(self):
self.setup_env()

def setup_env(self, gdb_unix_socket_path=None):
self.rom_addr = None
self.arch = None
self.setup_arch()

self.avatar = Avatar(arch=self.arch, output_directory=TEST_DIR)
self.avatar = Avatar(arch=self.arch, output_directory=TEST_DIR, configure_logging=False)
self.qemu = QemuTarget(self.avatar, name='qemu_test',
#firmware="./tests/binaries/qemu_arm_test",
firmware='%s/firmware' % TEST_DIR,
gdb_unix_socket_path=gdb_unix_socket_path,
)
self.fake_target = FakeTarget()

Expand Down Expand Up @@ -182,6 +185,17 @@ def test_remote_memory_read(self):
self.assertEqual(remote_memory_read, 0xdeadbeef, remote_memory_read)


class QemuTargetWithUnixSocketTestCase(QemuTargetTestCase):

def setUp(self):
self.setup_env(gdb_unix_socket_path="/tmp/test_sock")

#def test_initialization():
# self.qemu.init()
# self.qemu.wait()
# self.assertEqual(qemu.state, TargetStates.STOPPED, self.qemu.state)



if __name__ == '__main__':
unittest.main()
50 changes: 50 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import socket
import threading
import os
import time

# oneshot UNIX to TCP socket proxy. Stops after first connection
def unix2tcp(unix_socket_path, tcp_host, tcp_port):
try:
os.unlink(unix_socket_path)
except OSError:
pass

usock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
tsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

usock.bind(unix_socket_path)
usock.listen(1)

def proxy_loop():
uconn, addr = usock.accept()
tsock.connect((tcp_host, tcp_port))

uconn.setblocking(False)
tsock.setblocking(False)

while True:
data = None

try:
data = uconn.recv(1000)
if len(data) == 0:
break
tsock.sendall(data)
except BlockingIOError:
pass

try:
data = tsock.recv(1000)
if len(data) == 0:
break
uconn.sendall(data)
except BlockingIOError:
pass

usock.close()
uconn.close()
tsock.close()

threading.Thread(target=proxy_loop, daemon=True).start()

0 comments on commit 3f26761

Please sign in to comment.