From 0fca69886b67ec5d8ea694fd4096eb13bb96a325 Mon Sep 17 00:00:00 2001 From: crazy hugsy Date: Thu, 22 Feb 2024 08:14:38 -0800 Subject: [PATCH] Fix support for `rr` (#1047) Fix support for `rr` for the `gef.session.remote` api changed last year. Co-authored-by: Grazfather --- docs/commands/gef-remote.md | 47 ++++++++++ gef.py | 167 ++++++++++++++++++++++++++--------- tests/api/gef_memory.py | 9 +- tests/commands/gef_remote.py | 40 +++++---- 4 files changed, 200 insertions(+), 63 deletions(-) diff --git a/docs/commands/gef-remote.md b/docs/commands/gef-remote.md index fc9ab0a32..917601dd9 100644 --- a/docs/commands/gef-remote.md +++ b/docs/commands/gef-remote.md @@ -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 +``` diff --git a/gef.py b/gef.py index f4344bab3..e89fcffda 100644 --- a/gef.py +++ b/gef.py @@ -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"]) @@ -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 @@ -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: @@ -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 @@ -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: @@ -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, @@ -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. @@ -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.""" @@ -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]]: @@ -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 @@ -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 @@ -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: @@ -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: @@ -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 @@ -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`.""" @@ -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) @@ -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: diff --git a/tests/api/gef_memory.py b/tests/api/gef_memory.py index be74ea44e..d771b1038 100644 --- a/tests/api/gef_memory.py +++ b/tests/api/gef_memory.py @@ -51,6 +51,7 @@ def test_api_gef_memory_parse_info_proc_maps_expected_format(self): int(parts[3], 16) assert end_addr == start_addr + size assert len(parts[4]) == 4, f"Expected permission string, got {parts[4]}" + Permission = root.eval("Permission") Permission.from_process_maps(parts[4]) @@ -71,10 +72,10 @@ def test_api_gef_memory_parse_info_proc_maps(self): if self.gdb_version < (11, 0): # expect an exception with pytest.raises(AttributeError): - next(gef.memory.parse_gdb_info_proc_maps()) + next(root.eval("gef.memory.parse_gdb_info_proc_maps()") ) else: - for section in gef.memory.parse_gdb_info_proc_maps(): + for section in root.eval("gef.memory.parse_gdb_info_proc_maps()"): assert isinstance(section, Section) def test_func_parse_permissions(self): @@ -95,11 +96,11 @@ def test_func_parse_maps_local_procfs(self): root, gdb, gef = self._conn.root, self._gdb, self._gef with pytest.raises(FileNotFoundError): - root.eval("list(GefMemoryManager.parse_procfs_maps())") + root.eval("list(gef.memory.parse_procfs_maps())") gdb.execute("start") - sections = root.eval("list(GefMemoryManager.parse_procfs_maps())") + sections = root.eval("list(gef.memory.parse_procfs_maps())") for section in sections: assert section.page_start & ~0xFFF assert section.page_end & ~0xFFF diff --git a/tests/commands/gef_remote.py b/tests/commands/gef_remote.py index d2ca7d026..b5c980667 100644 --- a/tests/commands/gef_remote.py +++ b/tests/commands/gef_remote.py @@ -14,6 +14,7 @@ GDBSERVER_DEFAULT_HOST, ) + class GefRemoteCommand(RemoteGefUnitTestGeneric): """`gef_remote` command test module""" @@ -21,47 +22,52 @@ def setUp(self) -> None: self._target = debug_target("default") return super().setUp() - def test_cmd_gef_remote(self): + def test_cmd_gef_remote_gdbserver(self): gdb = self._gdb + gef = self._gef root = self._conn.root + gdbserver_mode = "GDBSERVER" while True: port = random.randint(1025, 65535) - if port != self._port: break + if port != self._port: + break with gdbserver_session(port=port): gdb.execute(f"gef-remote {GDBSERVER_DEFAULT_HOST} {port}") - res = root.eval("str(gef.session.remote)") - self.assertIn( - f"RemoteSession(target='{GDBSERVER_DEFAULT_HOST}:{port}', local='/tmp/", res) - self.assertIn(", qemu_user=False)", res) - + res: str = root.eval("str(gef.session.remote)") + assert res.startswith(f"RemoteSession(target='{GDBSERVER_DEFAULT_HOST}:{port}', local='/tmp/") + assert res.endswith(f"pid={gef.session.pid}, mode={gdbserver_mode})") @pytest.mark.slow def test_cmd_gef_remote_qemu_user(self): gdb = self._gdb + gef = self._gef root = self._conn.root + qemu_mode = "QEMU" while True: port = random.randint(1025, 65535) - if port != self._port: break - + if port != self._port: + break with qemuuser_session(port=port): - cmd = f"gef-remote --qemu-user --qemu-binary {self._target} {GDBSERVER_DEFAULT_HOST} {port}" + cmd = f"gef-remote --qemu-user --qemu-binary {self._target} {GDBSERVER_DEFAULT_HOST} {port}" gdb.execute(cmd) res = root.eval("str(gef.session.remote)") - assert f"RemoteSession(target='{GDBSERVER_DEFAULT_HOST}:{port}', local='/tmp/" in res - assert ", qemu_user=True)" in res - + assert res.startswith(f"RemoteSession(target='{GDBSERVER_DEFAULT_HOST}:{port}', local='/tmp/") + assert res.endswith(f"pid={gef.session.pid}, mode={qemu_mode})") def test_cmd_target_remote(self): gdb = self._gdb + gef = self._gef root = self._conn.root + gdbserver_mode = "GDBSERVER" while True: port = random.randint(1025, 65535) - if port != self._port: break + if port != self._port: + break with gdbserver_session(port=port) as _: gdb.execute(f"target remote {GDBSERVER_DEFAULT_HOST}:{port}") - res = root.eval("str(gef.session.remote)") - self.assertIn(f"RemoteSession(target=':0', local='/tmp/", res) - self.assertIn(", qemu_user=False)", res) + res: str = root.eval("str(gef.session.remote)") + assert res.startswith(f"RemoteSession(target=':0', local='/tmp/") + assert res.endswith(f"pid={gef.session.pid}, mode={gdbserver_mode})")