From a36ffbec735237f80f5ed00505f4e42ac092b571 Mon Sep 17 00:00:00 2001 From: clubby789 Date: Fri, 21 Oct 2022 18:09:48 +0200 Subject: [PATCH] Fix filesystem paths for debugging process in containers (#897) * Only replace `target:` *prefix*, fix string literals " If the path *begins* with `target:`, replace only the first occurrence with the real root. * Add note on vDSO * Add note on containers to FAQ * Add `session.root` tests * Loosen test requirements for Qemu --- docs/faq.md | 8 ++++++++ gef.py | 22 ++++++++++++++++++++-- tests/api/gef_session.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 1c1256a8f..e7f623854 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -172,3 +172,11 @@ Discord is your answer: join and talk to us by clicking here If you cannot find the answer to your problem here or on the Discord, then go to the project [Issues page](https://github.com/hugsy/gef/issues) and fill up the forms with as much information as you can! +## How can I use GEF to debug a process in a container? + +GEF can attach to a process running in a container using `gdb --pid=$PID`, where `$PID` is the ID of the running process *on the host*. To find this, you can use `docker top -o pid | awk '!/PID/' | xargs -I'{}' pstree -psa {}` to view the process tree for the container. + +`sudo` may be required to attach to the process, which will depend on your system's security settings. + +Please note that cross-container debugging may have unexpected issues. Installing gdb and GEF inside the container, or using [the official GEF docker image](https://hub.docker.com/r/crazyhugsy/gef) may improve results. + diff --git a/gef.py b/gef.py index 22a5698f3..2159038a4 100644 --- a/gef.py +++ b/gef.py @@ -3490,8 +3490,13 @@ def hook_stop_handler(_: "gdb.StopEvent") -> None: def new_objfile_handler(evt: Optional["gdb.NewObjFileEvent"]) -> None: """GDB event handler for new object file cases.""" reset_all_caches() + path = evt.new_objfile.filename if evt else gdb.current_progspace().filename try: - target = pathlib.Path( evt.new_objfile.filename if evt else gdb.current_progspace().filename) + if gef.session.root and path.startswith("target:"): + # If the process is in a container, replace the "target:" prefix + # with the actual root directory of the process. + path = path.replace("target:", str(gef.session.root), 1) + target = pathlib.Path(path) FileFormatClasses = list(filter(lambda fmtcls: fmtcls.is_valid(target), __registered_file_formats__)) GuessedFileFormatClass : Type[FileFormat] = FileFormatClasses.pop() if len(FileFormatClasses) else Elf binary = GuessedFileFormatClass(target) @@ -3501,7 +3506,11 @@ def new_objfile_handler(evt: Optional["gdb.NewObjFileEvent"]) -> None: else: gef.session.modules.append(binary) except FileNotFoundError as fne: - warn(f"Failed to find objfile or not a valid file format: {str(fne)}") + # Linux automatically maps the vDSO into our process, and GDB + # will give us the string 'system-supplied DSO' as a path. + # This is normal, so we shouldn't warn the user about it + if "system-supplied DSO" not in path: + warn(f"Failed to find objfile or not a valid file format: {str(fne)}") except RuntimeError as re: warn(f"Not a valid file format: {str(re)}") return @@ -10433,6 +10442,7 @@ def reset_caches(self) -> None: self._file = None self._canary = None self._maps: Optional[pathlib.Path] = None + self._root: Optional[pathlib.Path] = None return def __str__(self) -> str: @@ -10526,6 +10536,14 @@ def maps(self) -> Optional[pathlib.Path]: self._maps = pathlib.Path(f"/proc/{self.pid}/maps") return self._maps + @property + def root(self) -> Optional[pathlib.Path]: + """Returns the path to the process's root directory.""" + if not is_alive(): + return None + if not self._root: + self._root = pathlib.Path(f"/proc/{self.pid}/root") + return self._root class GefRemoteSessionManager(GefSessionManager): """Class for managing remote sessions with GEF. It will create a temporary environment diff --git a/tests/api/gef_session.py b/tests/api/gef_session.py index 6fdec2c85..b97d759d7 100644 --- a/tests/api/gef_session.py +++ b/tests/api/gef_session.py @@ -3,14 +3,22 @@ """ +from logging import root import subprocess +import os from tests.utils import ( TMPDIR, gdb_test_python_method, _target, GefUnitTestGeneric, + gdbserver_session, + gdb_run_cmd, + qemuuser_session ) +import re +GDBSERVER_PREFERED_HOST = "localhost" +GDBSERVER_PREFERED_PORT = 1234 class GefSessionApi(GefUnitTestGeneric): """`gef.session` test module.""" @@ -40,3 +48,30 @@ def test_func_auxiliary_vector(self): self.assertTrue("'AT_PLATFORM'" in res) self.assertTrue("'AT_EXECFN':" in res) self.assertFalse("'AT_WHATEVER':" in res) + + def test_root_dir(self): + func = "(s.st_dev, s.st_ino)" + res = gdb_test_python_method(func, target=_target("default"), before="s=os.stat(gef.session.root)") + self.assertNoException(res) + st_dev, st_ino = eval(res.split("\n")[-1]) + stat_root = os.stat("/") + # Check that the `/` directory and the `session.root` directory are the same + assert (stat_root.st_dev == st_dev) and (stat_root.st_ino == st_ino) + + port = GDBSERVER_PREFERED_PORT + 1 + before = [f"gef-remote {GDBSERVER_PREFERED_HOST} {port}", + "pi s = os.stat(gef.session.root)"] + with gdbserver_session(port=port) as _: + res = gdb_run_cmd(f"pi {func}", target=_target("default"), before=before) + self.assertNoException(res) + st_dev, st_ino = eval(res.split("\n")[-1]) + assert (stat_root.st_dev == st_dev) and (stat_root.st_ino == st_ino) + + port = GDBSERVER_PREFERED_PORT + 2 + with qemuuser_session(port=port) as _: + target = _target("default") + before = [ + f"gef-remote --qemu-user --qemu-binary {target} {GDBSERVER_PREFERED_HOST} {port}"] + res = gdb_run_cmd(f"pi gef.session.root", target=_target("default"), before=before) + self.assertNoException(res) + assert re.search(r"\/proc\/[0-9]+/root", res)