Skip to content

Commit

Permalink
Add support for BlackMagicProbe (#99)
Browse files Browse the repository at this point in the history
This adds three things:

`gef-bmp-remote` command, to connect to a black magic probe. This is its
own command because it connects to a tty instead of a host/port.

`GefBMPRemoteSessionManager`, which handles connecting and such.

`ARMBlackMagicProbe`, a new arch, which handles its own memory
management and has different register names that e.g. openocd.
  • Loading branch information
Grazfather authored Jan 30, 2024
1 parent 3a63c22 commit b6b8ad1
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 3 deletions.
3 changes: 0 additions & 3 deletions archs/README.md

This file was deleted.

203 changes: 203 additions & 0 deletions archs/arm-blackmagicprobe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""
ARM through the Black Magic Probe support for GEF
To use, source this file *after* gef
Author: Grazfather
"""

from typing import Optional

import gdb


class ARMBlackMagicProbe(ARM):
arch = "ARMBlackMagicProbe"
aliases = ("ARMBlackMagicProbe",)
all_registers = ("$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6",
"$r7", "$r8", "$r9", "$r10", "$r11", "$r12", "$sp",
"$lr", "$pc", "$xpsr")
flag_register = "$xpsr"
@staticmethod
def supports_gdb_arch(arch: str) -> Optional[bool]:
if "arm" in arch and arch.endswith("-m"):
return True
return None

@staticmethod
def maps():
yield from GefMemoryManager.parse_info_mem()


@register
class BMPRemoteCommand(GenericCommand):
"""GDB `target remote` command on steroids. This command will use the remote procfs to create
a local copy of the execution environment, including the target binary and its libraries
in the local temporary directory (the value by default is in `gef.config.tempdir`). Additionally, it
will fetch all the /proc/PID/maps and loads all its information. If procfs is not available remotely, the command
will likely fail. You can however still use the limited command provided by GDB `target remote`."""

_cmdline_ = "gef-bmp-remote"
_syntax_ = f"{_cmdline_} [OPTIONS] TTY"
_example_ = [
f"{_cmdline_} --scan /dev/ttyUSB1"
f"{_cmdline_} --scan /dev/ttyUSB1 --power"
f"{_cmdline_} --scan /dev/ttyUSB1 --power --keep-power"
f"{_cmdline_} --file /path/to/binary.elf --attach 1 /dev/ttyUSB1",
f"{_cmdline_} --file /path/to/binary.elf --attach 1 --power /dev/ttyUSB1",
]

def __init__(self) -> None:
super().__init__(prefix=False)
return

@parse_arguments({"tty": ""}, {"--file": "", "--attach": "", "--power": False,
"--keep-power": False, "--scan": False})
def do_invoke(self, _: List[str], **kwargs: Any) -> None:
if gef.session.remote is not None:
err("You're already in a remote session. Close it first before opening a new one...")
return

# argument check
args: argparse.Namespace = kwargs["arguments"]
if not args.tty:
err("Missing parameters")
return

if not args.scan and not args.attach:
err("Must provide target to attach to if not scanning")
return

# Try to establish the remote session, throw on error
# Set `.remote_initializing` to True here - `GefRemoteSessionManager` invokes code which
# calls `is_remote_debug` which checks if `remote_initializing` is True or `.remote` is None
# This prevents some spurious errors being thrown during startup
gef.session.remote_initializing = True
session = GefBMPRemoteSessionManager(args.tty, args.file, args.attach, args.scan, args.power)

dbg(f"[remote] initializing remote session with {session.target} under {session.root}")

# Connect can return false if it wants us to disconnect
if not session.connect():
gef.session.remote = None
gef.session.remote_initializing = False
return
if not session.setup():
gef.session.remote = None
gef.session.remote_initializing = False
raise EnvironmentError("Failed to setup remote target")

gef.session.remote_initializing = False
gef.session.remote = session
reset_all_caches()
gdb.execute("context")
return


class GefBMPRemoteSessionManager(GefRemoteSessionManager):
"""Class for managing remote sessions with GEF. It will create a temporary environment
designed to clone the remote one."""
def __init__(self, tty: str="", file: str="", attach: int=1,
scan: bool=False, power: bool=False, keep_power: bool=False) -> None:
self.__tty = tty
self.__file = file
self.__attach = attach
self.__scan = scan
self.__power = power
self.__keep_power = keep_power
self.__local_root_fd = tempfile.TemporaryDirectory()
self.__local_root_path = pathlib.Path(self.__local_root_fd.name)

def __str__(self) -> str:
return f"BMPRemoteSessionManager(tty='{self.__tty}', file='{self.__file}', attach={self.__attach})"

def close(self) -> None:
self.__local_root_fd.cleanup()
try:
gef_on_new_unhook(self.remote_objfile_event_handler)
gef_on_new_hook(new_objfile_handler)
if self.__power and not self.__keep_power:
self._power_off()
except Exception as e:
warn(f"Exception while restoring local context: {str(e)}")
return

@property
def root(self) -> pathlib.Path:
return self.__local_root_path.absolute()

@property
def target(self) -> str:
return f"{self.__tty} (attach {self.__attach})"

def sync(self, src: str, dst: Optional[str] = None) -> bool:
# We cannot sync from this target
return None

@property
def file(self) -> Optional[pathlib.Path]:
if self.__file:
return pathlib.Path(self.__file).expanduser()
return None

def connect(self) -> bool:
"""Connect to remote target. If in extended mode, also attach to the given PID."""
# before anything, register our new hook to download files from the remote target
dbg(f"[remote] Installing new objfile handlers")
try:
gef_on_new_unhook(new_objfile_handler)
except SystemError:
# the default objfile handler might already have been removed, ignore failure
pass

gef_on_new_hook(self.remote_objfile_event_handler)

# Connect
with DisableContextOutputContext():
self._gdb_execute(f"target extended-remote {self.__tty}")

# Optionally enable target-powering
if self.__power:
self._power_on()

# We must always scan, but with --scan we are done here
self._gdb_execute("monitor swdp_scan")
if self.__scan:
self._gdb_execute("disconnect")

# Returning false cleans up the session
return False

try:
with DisableContextOutputContext():
if self.file:
self._gdb_execute(f"file {self.file}")
self._gdb_execute(f"attach {self.__attach or 1}")
except Exception as e:
err(f"Failed to connect to {self.target}: {e}")
# a failure will trigger the cleanup, deleting our hook
return False

return True

def setup(self) -> bool:
dbg(f"Setting up as remote session")

# refresh gef to consider the binary
reset_all_caches()
if self.file:
gef.binary = Elf(self.file)
# We'd like to set this earlier, but we can't because of this bug
# https://sourceware.org/bugzilla/show_bug.cgi?id=31303
reset_architecture("ARMBlackMagicProbe")
return True

def _power_off(self):
self._gdb_execute("monitor tpwr disable")

def _power_on(self):
self._gdb_execute("monitor tpwr enable")

def _gdb_execute(self, cmd):
dbg(f"[remote] Executing '{cmd}'")
gdb.execute(cmd)
4 changes: 4 additions & 0 deletions docs/archs/arm-blackmagicprobe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## ARMBlackMagicProbe

The ARM BlackMagicProbe architecture is a special arcthtecture used with the `gef-bmp-remote`
command. Please read the [documentation](../commands/gef-bmp-remote.md) for the command.
38 changes: 38 additions & 0 deletions docs/commands/gef-bmp-remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Command gef-bmp-remote

The `gef-bmp-command` is used with the [`ARMBlackMagicProbe`](../../archs/arm-blackmagicprobe.py]
architecture.

The [Black Magic Probe](https://black-magic.org/) is a JTAG/SWD debugger that handles communicating
with your device and exposes a _gdbserver_ for GDB to connect to. This allows you to connect to it
via GDB with the `target extended-remote` command. However, because this is exposed via a tty, GEF
cannot handle it with its `gef-remote` command (which assumes a _host:port_ connection). The
[arm-blackmagicprobe.py](../../archs/arm-blackmagicprobe.py) script offers a way around this. It
creates a custom ARM-derived `Architecture`, as well as the `gef-bmp-remote` command, which lets you
scan for devices, power the target, and ultimately connect to the target device.

### Scan for devices

```bash
gef➤ gef-bmp-remote --scan /dev/ttyUSB1"
[=] [remote] Executing 'monitor swdp_scan'
Target voltage: 3.3V
Available Targets:
No. Att Driver
1 Raspberry RP2040 M0+
2 Raspberry RP2040 M0+
3 Raspberry RP2040 Rescue (Attach to reset!)
```
This will connect to the BMP and use its scan feature to find valid targets connected. They will be
numbered. Use the appropriate number to later `--attach`.
If you are powering the device through the BMP, then make sure to add the `--power` arguments,
otherwise the target may not be powered up when you attempt the scan.
If you want to keep power between scanning and attaching, then use `--keep-power`.
```bash
gef➤ gef-bmp-remote --file /path/to/binary.elf --attach 1 /dev/ttyUSB1",
gef➤ gef-bmp-remote --file /path/to/binary.elf --attach 1 --power /dev/ttyUSB1",
```

0 comments on commit b6b8ad1

Please sign in to comment.