Skip to content

Commit

Permalink
Fix support for rr (#1047)
Browse files Browse the repository at this point in the history
Fix support for `rr` for the `gef.session.remote` api changed last year.

Co-authored-by: Grazfather <grazfather@gmail.com>
  • Loading branch information
hugsy and Grazfather committed Feb 22, 2024
1 parent db5b7b8 commit 0fca698
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 63 deletions.
47 changes: 47 additions & 0 deletions docs/commands/gef-remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,50 @@ To test locally, you can use the mini image linux x64 vm
2. Use `--qemu-user` and `--qemu-binary vmlinuz` when starting `gef-remote`

![qemu-system](https://user-images.githubusercontent.com/590234/175071351-8e06aa27-dc61-4fd7-9215-c345dcebcd67.png)

### `rr` support

GEF can be used with the time-travel tool [`rr`](https://rr-project.org/) as it will act as a
remote session. Most of the commands will work as long as the debugged binary is present on the
target.

GEF can be loaded from `rr` as such in a very similar way it is loaded gdb. The `-x` command line
toggle can be passed load it as it would be for any gdbinit script

```text
$ cat ~/load-with-gef-extras
source ~/code/gef/gef.py
gef config gef.extra_plugins_dir ~/code/gef-extras/scripts
gef config pcustom.struct_path ~/code/gef-extras/structs
$ rr record /usr/bin/date
[...]
$ rr replay -x ~/load-with-gef-extras
[...]
(remote) gef➤ pi gef.binary
ELF('/usr/bin/date', ELF_64_BITS, X86_64)
(remote) gef➤ pi gef.session
Session(Remote, pid=3068, os='linux')
(remote) gef➤ pi gef.session.remote
RemoteSession(target=':0', local='/', pid=3068, mode=RR)
(remote) gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x0000000068000000 0x0000000068200000 0x0000000000200000 rwx
0x000000006fffd000 0x0000000070001000 0x0000000000004000 r-x /usr/lib/rr/librrpage.so
0x0000000070001000 0x0000000070002000 0x0000000000001000 rw- /tmp/rr-shared-preload_thread_locals-801763-0
0x00005580b30a3000 0x00005580b30a6000 0x0000000000003000 r-- /usr/bin/date
0x00005580b30a6000 0x00005580b30b6000 0x0000000000010000 r-x /usr/bin/date
0x00005580b30b6000 0x00005580b30bb000 0x0000000000005000 r-- /usr/bin/date
0x00005580b30bc000 0x00005580b30be000 0x0000000000002000 rw- /usr/bin/date
0x00007f21107c7000 0x00007f21107c9000 0x0000000000002000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007f21107c9000 0x00007f21107f3000 0x000000000002a000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007f21107f3000 0x00007f21107fe000 0x000000000000b000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007f21107ff000 0x00007f2110803000 0x0000000000004000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffcc951a000 0x00007ffcc953c000 0x0000000000022000 rw- [stack]
0x00007ffcc95ab000 0x00007ffcc95ad000 0x0000000000002000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 --x [vsyscall]
(remote) gef➤ pi len(gef.memory.maps)
14
```
167 changes: 125 additions & 42 deletions gef.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,12 @@ def __eq__(self, other: "Section") -> bool:
self.permission == other.permission and \
self.path == other.path

def overlaps(self, other: "Section") -> bool:
return max(self.page_start, other.page_start) <= min(self.page_end, other.page_end)

def contains(self, addr: int) -> bool:
return addr in range(self.page_start, self.page_end)


Zone = collections.namedtuple("Zone", ["name", "zone_start", "zone_end", "filename"])

Expand Down Expand Up @@ -10432,9 +10438,7 @@ def __gef_prompt__(current_prompt: Callable[[Callable], str]) -> str:
"""GEF custom prompt function."""
if gef.config["gef.readline_compat"] is True: return GEF_PROMPT
if gef.config["gef.disable_color"] is True: return GEF_PROMPT
prompt = ""
if gef.session.remote:
prompt += Color.boldify("(remote) ")
prompt = gef.session.remote.mode.prompt_string() if gef.session.remote else ""
prompt += GEF_PROMPT_ON if is_alive() else GEF_PROMPT_OFF
return prompt

Expand All @@ -10461,7 +10465,7 @@ def __init__(self) -> None:

def reset_caches(self) -> None:
super().reset_caches()
self.__maps = None
self.__maps: Optional[List[Section]] = None
return

def write(self, address: int, buffer: ByteString, length: Optional[int] = None) -> None:
Expand Down Expand Up @@ -10518,13 +10522,10 @@ def read_ascii_string(self, address: int) -> Optional[str]:
@property
def maps(self) -> List[Section]:
if not self.__maps:
self.__maps = self._parse_maps()
if not self.__maps:
raise RuntimeError("Failed to get memory layout")
self.__maps = self.__parse_maps()
return self.__maps

@classmethod
def _parse_maps(cls) -> Optional[List[Section]]:
def __parse_maps(self) -> Optional[List[Section]]:
"""Return the mapped memory sections. If the current arch has its maps
method defined, then defer to that to generated maps, otherwise, try to
figure it out from procfs, then info sections, then monitor info
Expand All @@ -10533,24 +10534,24 @@ def _parse_maps(cls) -> Optional[List[Section]]:
return list(gef.arch.maps())

try:
return list(cls.parse_gdb_info_proc_maps())
return list(self.parse_gdb_info_proc_maps())
except:
pass

try:
return list(cls.parse_procfs_maps())
return list(self.parse_procfs_maps())
except:
pass

try:
return list(cls.parse_monitor_info_mem())
return list(self.parse_monitor_info_mem())
except:
pass

return None
raise RuntimeError("Failed to get memory layout")

@staticmethod
def parse_procfs_maps() -> Generator[Section, None, None]:
@classmethod
def parse_procfs_maps(cls) -> Generator[Section, None, None]:
"""Get the memory mapping from procfs."""
procfs_mapfile = gef.session.maps
if not procfs_mapfile:
Expand Down Expand Up @@ -10581,38 +10582,56 @@ def parse_procfs_maps() -> Generator[Section, None, None]:
path=pathname)
return

@staticmethod
def parse_gdb_info_proc_maps() -> Generator[Section, None, None]:
@classmethod
def parse_gdb_info_proc_maps(cls) -> Generator[Section, None, None]:
"""Get the memory mapping from GDB's command `maintenance info sections` (limited info)."""

if GDB_VERSION < (11, 0):
raise AttributeError("Disregarding old format")

lines = (gdb.execute("info proc mappings", to_string=True) or "").splitlines()
output = (gdb.execute("info proc mappings", to_string=True) or "")
if not output:
raise AttributeError

start_idx = output.find("Start Addr")
if start_idx == -1:
raise AttributeError

# The function assumes the following output format (as of GDB 11+) for `info proc mappings`
output = output[start_idx:]
lines = output.splitlines()
if len(lines) < 2:
raise AttributeError

# The function assumes the following output format (as of GDB 11+) for `info proc mappings`:
# - live process (incl. remote)
# ```
# process 61789
# Mapped address spaces:
#
# Start Addr End Addr Size Offset Perms objfile
# 0x555555554000 0x555555558000 0x4000 0x0 r--p /usr/bin/ls
# 0x555555558000 0x55555556c000 0x14000 0x4000 r-xp /usr/bin/ls
# [...]
# ```
# or
# - coredump & rr
# ```
# Start Addr End Addr Size Offset objfile
# 0x555555554000 0x555555558000 0x4000 0x0 /usr/bin/ls
# 0x555555558000 0x55555556c000 0x14000 0x4000 /usr/bin/ls
# ```
# In the latter case the 'Perms' header is missing, so mock the Permission to `rwx` so
# `dereference` will still work.

if len(lines) < 5:
raise AttributeError

# Format seems valid, iterate to generate sections
for line in lines[4:]:
mock_permission = all(map(lambda x: x.strip() != "Perms", lines[0].split()))
for line in lines[1:]:
if not line:
break

parts = [x.strip() for x in line.split()]
addr_start, addr_end, offset = [int(x, 16) for x in parts[0:3]]
perm = Permission.from_process_maps(parts[4])
path = " ".join(parts[5:]) if len(parts) >= 5 else ""
if mock_permission:
perm = Permission(7)
path = " ".join(parts[4:]) if len(parts) >= 4 else ""
else:
perm = Permission.from_process_maps(parts[4])
path = " ".join(parts[5:]) if len(parts) >= 5 else ""
yield Section(
page_start=addr_start,
page_end=addr_end,
Expand All @@ -10622,8 +10641,8 @@ def parse_gdb_info_proc_maps() -> Generator[Section, None, None]:
)
return

@staticmethod
def parse_monitor_info_mem() -> Generator[Section, None, None]:
@classmethod
def parse_monitor_info_mem(cls) -> Generator[Section, None, None]:
"""Get the memory mapping from GDB's command `monitor info mem`
This can raise an exception, which the memory manager takes to mean
that this method does not work to get a map.
Expand Down Expand Up @@ -10665,6 +10684,22 @@ def parse_info_mem():
page_end=int(end, 0),
permission=perm)

def append(self, section: Section):
if not self.maps:
raise AttributeError("No mapping defined")
if not isinstance(section, Section):
raise TypeError("section has an invalid type")

assert self.__maps
for s in self.__maps:
if section.overlaps(s):
raise RuntimeError(f"{section} overlaps {s}")
self.__maps.append(section)
return self

def __iadd__(self, section: Section):
return self.append(section)


class GefHeapManager(GefManager):
"""Class managing session heap."""
Expand Down Expand Up @@ -10955,7 +10990,11 @@ def reset_caches(self) -> None:
return

def __str__(self) -> str:
return f"Session({'Local' if self.remote is None else 'Remote'}, pid={self.pid or 'Not running'}, os='{self.os}')"
_type = "Local" if self.remote is None else f"Remote/{self.remote.mode}"
return f"Session(type={_type}, pid={self.pid or 'Not running'}, os='{self.os}')"

def __repr__(self) -> str:
return str(self)

@property
def auxiliary_vector(self) -> Optional[Dict[str, int]]:
Expand Down Expand Up @@ -11032,7 +11071,7 @@ def canary(self) -> Optional[Tuple[int, int]]:
try:
canary_location = gef.arch.canary_address()
canary = gef.memory.read_integer(canary_location)
except NotImplementedError:
except (NotImplementedError, gdb.error):
# Fall back to `AT_RANDOM`, which is the original source
# of the canary value but not the canonical location
return self.original_canary
Expand Down Expand Up @@ -11075,6 +11114,27 @@ def root(self) -> Optional[pathlib.Path]:
class GefRemoteSessionManager(GefSessionManager):
"""Class for managing remote sessions with GEF. It will create a temporary environment
designed to clone the remote one."""

class RemoteMode(enum.IntEnum):
GDBSERVER = 0
QEMU = 1
RR = 2

def __str__(self):
return self.name

def __repr__(self):
return f"RemoteMode = {str(self)} ({int(self)})"

def prompt_string(self) -> str:
if self == GefRemoteSessionManager.RemoteMode.QEMU:
return Color.boldify("(qemu) ")
if self == GefRemoteSessionManager.RemoteMode.RR:
return Color.boldify("(rr) ")
if self == GefRemoteSessionManager.RemoteMode.GDBSERVER:
return Color.boldify("(remote) ")
raise AttributeError("Unknown value")

def __init__(self, host: str, port: int, pid: int =-1, qemu: Optional[pathlib.Path] = None) -> None:
super().__init__()
self.__host = host
Expand All @@ -11083,6 +11143,13 @@ def __init__(self, host: str, port: int, pid: int =-1, qemu: Optional[pathlib.Pa
self.__local_root_path = pathlib.Path(self.__local_root_fd.name)
self.__qemu = qemu

if self.__qemu is not None:
self._mode = GefRemoteSessionManager.RemoteMode.QEMU
elif os.environ.get("GDB_UNDER_RR", None) == "1":
self._mode = GefRemoteSessionManager.RemoteMode.RR
else:
self._mode = GefRemoteSessionManager.RemoteMode.GDBSERVER

def close(self) -> None:
self.__local_root_fd.cleanup()
try:
Expand All @@ -11092,11 +11159,11 @@ def close(self) -> None:
warn(f"Exception while restoring local context: {str(e)}")
return

def in_qemu_user(self) -> bool:
return self.__qemu is not None

def __str__(self) -> str:
return f"RemoteSession(target='{self.target}', local='{self.root}', pid={self.pid}, qemu_user={bool(self.in_qemu_user())})"
return f"RemoteSession(target='{self.target}', local='{self.root}', pid={self.pid}, mode={self.mode})"

def __repr__(self) -> str:
return str(self)

@property
def target(self) -> str:
Expand All @@ -11117,7 +11184,7 @@ def file(self) -> pathlib.Path:
if not filename:
raise RuntimeError("No session started")
start_idx = len("target:") if filename.startswith("target:") else 0
self._file = pathlib.Path(filename[start_idx:])
self._file = pathlib.Path(progspace.filename[start_idx:])
return self._file

@property
Expand All @@ -11131,6 +11198,10 @@ def maps(self) -> pathlib.Path:
self._maps = self.root / f"proc/{self.pid}/maps"
return self._maps

@property
def mode(self) -> RemoteMode:
return self._mode

def sync(self, src: str, dst: Optional[str] = None) -> bool:
"""Copy the `src` into the temporary chroot. If `dst` is provided, that path will be
used instead of `src`."""
Expand Down Expand Up @@ -11175,13 +11246,17 @@ def connect(self, pid: int) -> bool:

def setup(self) -> bool:
# setup remote adequately depending on remote or qemu mode
if self.in_qemu_user():
if self.mode == GefRemoteSessionManager.RemoteMode.QEMU:
dbg(f"Setting up as qemu session, target={self.__qemu}")
self.__setup_qemu()
else:
elif self.mode == GefRemoteSessionManager.RemoteMode.RR:
dbg(f"Setting up as rr session")
self.__setup_rr()
elif self.mode == GefRemoteSessionManager.RemoteMode.GDBSERVER:
dbg(f"Setting up as remote session")
self.__setup_remote()

else:
raise Exception
# refresh gef to consider the binary
reset_all_caches()
gef.binary = Elf(self.lfile)
Expand Down Expand Up @@ -11238,6 +11313,14 @@ def __setup_remote(self) -> bool:

return True

def __setup_rr(self) -> bool:
#
# Simply override the local root path, the binary must exist
# on the host.
#
self.__local_root_path = pathlib.Path("/")
return True

def remote_objfile_event_handler(self, evt: "gdb.events.NewObjFileEvent") -> None:
dbg(f"[remote] in remote_objfile_handler({evt.new_objfile.filename if evt else 'None'}))")
if not evt or not evt.new_objfile.filename:
Expand Down

0 comments on commit 0fca698

Please sign in to comment.