Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix support for rr #1047

Merged
merged 27 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
789f114
unused import
hugsy Jan 13, 2024
cf779a5
update actions version in validate
hugsy Jan 13, 2024
f07035f
use `info proc mappings` as preferred way to collect memory layout
hugsy Jan 13, 2024
206f9f5
[tests] moved mem layout parsing tests to gef_memory.py
hugsy Jan 13, 2024
8b9464d
test fix - all passes
hugsy Jan 13, 2024
62ad2fd
fixed rr support
hugsy Jan 13, 2024
2139a4b
fixed tests
hugsy Jan 13, 2024
0dbc80b
Merge branch 'main' into fix_rr_support
hugsy Jan 21, 2024
5e09e68
pr feedback
hugsy Jan 21, 2024
e825b41
added `rr` documentation
hugsy Jan 21, 2024
d869a15
allow session canary to fallback on using auxval
hugsy Jan 21, 2024
1550aaa
[ci] adjusted tests
hugsy Jan 21, 2024
bb0dbb5
Merge branch 'main' into fix_rr_support
hugsy Jan 27, 2024
9b9e30a
update default prompt to see which mode gef runs under
hugsy Feb 4, 2024
9242ddd
show session type in SessionManager::str
hugsy Feb 4, 2024
052b9db
Merge branch 'main' of github.com:hugsy/gef into fix_rr_support
hugsy Feb 11, 2024
d789fa2
restored coredump parsing
hugsy Feb 11, 2024
e5607f7
parse memory function have no reason to be static nor public
hugsy Feb 11, 2024
17e1b1d
removed `parse_info_mem`, obsoleted by the use of `gef.arch.maps()` f…
hugsy Feb 11, 2024
0e965a2
allow to manually insert memory section - restore coredump support
hugsy Feb 11, 2024
f4bfd27
fixed test
hugsy Feb 12, 2024
646b70f
pr feedback
hugsy Feb 17, 2024
e5a996a
Unhide parse methods
Grazfather Feb 22, 2024
8d9e07e
Unhide mode attribute
Grazfather Feb 22, 2024
38bfcca
Raise exception earlier
Grazfather Feb 22, 2024
6df4263
Fix tests
Grazfather Feb 22, 2024
6455ae9
explain
Grazfather Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading
Loading