Skip to content

Commit

Permalink
Fix filesystem paths for debugging process in containers (#897)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
clubby789 committed Oct 21, 2022
1 parent 0b17993 commit a36ffbe
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 2 deletions.
8 changes: 8 additions & 0 deletions docs/faq.md
Expand Up @@ -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 <container ID> -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.

22 changes: 20 additions & 2 deletions gef.py
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions tests/api/gef_session.py
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)

0 comments on commit a36ffbe

Please sign in to comment.