From 983425241ae8a47ae3e03bb8d7c7df7cbf4e24f6 Mon Sep 17 00:00:00 2001 From: hugsy Date: Sat, 25 Jun 2022 14:08:13 -0700 Subject: [PATCH] Better `gef-remote` - part 2: Remote Qemu (#846) * - rewrite of `gef-remote` to properly manager remote session - removed unused functions (& tests) * fixes gef's got function fails in remote debug sessions #806 * fully restore `qemu-user` + `test` * added more `__str__` for clarity * better __str__ * better __str__ * better __str__ * last changes to `__str__` * add `qemu_user` support * stupid typo * stupid typo * qemu-system works too * [gef-remote] Updated the docs * Apply suggestions from code review * Update docs/commands/gef-remote.md --- docs/commands/gef-remote.md | 142 ++++++++++++++++------------------- gef.py | 132 +++++++++++++++++++++++--------- tests/commands/gef_remote.py | 24 +++++- tests/utils.py | 35 +++++++-- 4 files changed, 214 insertions(+), 119 deletions(-) diff --git a/docs/commands/gef-remote.md b/docs/commands/gef-remote.md index 64d5c2354..8c79f005c 100644 --- a/docs/commands/gef-remote.md +++ b/docs/commands/gef-remote.md @@ -1,108 +1,98 @@ -## Command gef-remote ## +## Command `gef-remote` -It is possible to use `gef` in a remote debugging environment. Required files -will be automatically downloaded and cached in a temporary directory (`/tmp/gef` -on most Unix systems). Remember to manually delete the cache if you change the -target file or `gef` will use the outdated version. +[`target remote`](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Debugging.html#Remote-Debugging) is the traditional GDB way of debugging process or system remotely. However this command by itself does a limited job (80's bandwith FTW) to collect more information about the target, making the process of debugging more cumbersome. GEF greatly improves that state with the `gef-remote` command. -### With a local copy ### +📝 **Note**: If using GEF, `gef-remote` **should** be your way or debugging remote processes. Maintainers will not provide support or help if you decide to use the traditional `target remote` command. -If you want to remotely debug a binary that you already have, you simply need to -tell to `gdb` where to find the debug information. +`gef-remote` can function in 2 ways: + - `remote` which is meant to enrich use of GDB `target remote` command, when connecting to a "real" gdbserver instance + - `qemu-mode` when connecting to GDB stab of either `qemu-user` or `qemu-system`. + +The reason for this difference being that Qemu provides *a lot* less information that GEF can extract to enrich debugging. Whereas GDBServer allows to download remote file (therefore allowing to create a small identical environment), GDB stub in Qemu does not support file transfer. As a consequence, in order to use GEF in qemu mode, it is required to provide the binary being debugged. GEF will create a mock (limited) environment so that all its most useful features are available. + +### Remote mode + +#### `remote` + +If you want to remotely debug a binary that you already have, you simply need to tell to `gdb` where to find the debug information. For example, if we want to debug `uname`, we do on the server: ``` -$ gdbserver 0.0.0.0:1234 /bin/uname -Process /bin/uname created; pid = 32280 +$ gdbserver :1234 /tmp/default.out +Process /tmp/default.out created; pid = 258932 Listening on port 1234 ``` -![](https://i.imgur.com/Zc4vnBd.png) +![gef-remote-1](https://i.imgur.com/Zc4vnBd.png) -And on the client, simply run `gdb`: +On the client, when the original `gdb` would use `target remote`, GEF's syntax is roughly similar (shown running in debug mode for more verbose output, but you don't have to): ``` -$ gdb /bin/uname -gef➤ target remote 192.168.56.1:1234 -Process /bin/uname created; pid = 10851 -Listening on port 1234 +$ gdb -ex 'gef config gef.debug 1' +GEF for linux ready, type `gef' to start, `gef config' to configure +90 commands loaded and 5 functions added for GDB 10.2 using Python engine 3.8 +gef➤ gef-remote localhost 1234 +[=] [remote] initializing remote session with localhost:1234 under /tmp/tmp8qd0r7iw +[=] [remote] Installing new objfile handlers +[=] [remote] Enabling extended remote: False +[=] [remote] Executing 'target remote localhost:1234' +Reading /tmp/default.out from remote target... +warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead. +Reading /tmp/default.out from remote target... +Reading symbols from target:/tmp/default.out... +[=] [remote] in remote_objfile_handler(target:/tmp/default.out)) +[=] [remote] downloading '/tmp/default.out' -> '/tmp/tmp8qd0r7iw/tmp/default.out' +Reading /lib64/ld-linux-x86-64.so.2 from remote target... +Reading /lib64/ld-linux-x86-64.so.2 from remote target... +[=] [remote] in remote_objfile_handler(/usr/lib/debug/.build-id/45/87364908de169dec62ffa538170118c1c3a078.debug)) +[=] [remote] in remote_objfile_handler(target:/lib64/ld-linux-x86-64.so.2)) +[=] [remote] downloading '/lib64/ld-linux-x86-64.so.2' -> '/tmp/tmp8qd0r7iw/lib64/ld-linux-x86-64.so.2' +[=] [remote] in remote_objfile_handler(system-supplied DSO at 0x7ffff7fcd000)) +[*] [remote] skipping 'system-supplied DSO at 0x7ffff7fcd000' +0x00007ffff7fd0100 in _start () from target:/lib64/ld-linux-x86-64.so.2 +[=] Setting up as remote session +[=] [remote] downloading '/proc/258932/maps' -> '/tmp/tmp8qd0r7iw/proc/258932/maps' +[=] [remote] downloading '/proc/258932/environ' -> '/tmp/tmp8qd0r7iw/proc/258932/environ' +[=] [remote] downloading '/proc/258932/cmdline' -> '/tmp/tmp8qd0r7iw/proc/258932/cmdline' +[...] ``` -Or +And finally breaking into the program, showing the current context: -``` -$ gdb -gef➤ file /bin/uname -gef➤ target remote 192.168.56.1:1234 -``` +![gef-remote](https://i.imgur.com/IfsRDvK.png) -### Without a local copy ### +You will also notice the prompt has changed to indicate the debugging mode is now "remote". Besides that, all of GEF features are available: -It is possible to use `gdb` internal functions to copy our targeted binary. +![gef-remote-command](https://i.imgur.com/05epyX6.png) -Following our previous example, if we want to debug `uname`, run `gdb` and -connect to our `gdbserver`. To be able to locate the right process in the -`/proc` structure, the command `gef-remote` requires 1 argument, the target -host and port. The option `--pid` must be provided and indicate the process -PID on the remote host, only if the extended mode (`--is-extended-remote`) -is being used. -``` -$ gdb -gef➤ gef-remote 192.168.56.1:1234 -[+] Connected to '192.168.56.1:1234' -[+] Downloading remote information -[+] Remote information loaded, remember to clean '/tmp/gef/10851' when your session is over -``` +#### `remote-extended` -As you can observe, if it cannot find the debug information, `gef` will try to -automatically download the target file and store in the local temporary -directory (on most Unix `/tmp`). If successful, it will then automatically load -the debug information to `gdb` and proceed with the debugging. +Extended mode works the same as `remote`. Being an extended session, gdbserver has not spawned or attached to any process. Therefore, all that's required is to add the `--pid` flag when calling `gef-remote`, along with the process ID of the process to debug. -![gef-remote-autodownload](https://i.imgur.com/nLtvCxP.png) -You can then reuse the downloaded file for your future debugging sessions, use -it under IDA and such. This makes the entire remote debugging process -(particularly for Android applications) a child's game. +### Qemu mode -### Handling remote libraries ### +Qemu mode of `gef-remote` allows to connect to the [Qemu GDB stub](https://qemu-project.gitlab.io/qemu/system/gdb.html) which allows to live debug into either a binary (`qemu-user`) or even the kernel (`qemu-system`), of any architecture supported by GEF, which makes now even more sense 😉 And using it is very straight forward. -Often times you are missing a specific library the remote process is using. -To remedy this `gef-remote` can download remote libraries (and other files) if -the remote target supports it (and the remote gdbserver has sufficient -permissions). The `--download-lib LIBRARY` option can download a remote file -specified by its filepath. Furthermore `--download-everything` downloads all -remote libs found in the process's virtual memory map (`vmmap`). +#### `qemu-user` -Another issue with libraries is that even if you have the same libraries that -are used remotely they might have different filepaths and GDB can't -automatically find them and thereby can't resolve their symbols. The option -`--update-solib` adds the previously (with `--dowload-everything`) downloaded -libraries to the solib path so GDB can take full advantage of their symbols. + 1. Run `qemu-x86_64 :1234 /bin/ls` + 2. Use `--qemu-user` and `--qemu-binary /bin/ls` when starting `gef-remote` -### QEMU-user mode ### +![qemu-user](https://user-images.githubusercontent.com/590234/175072835-e276ab6c-4f75-4313-9e66-9fe5a3fd220e.png) -Although GDB through QEMU-user works, QEMU only supports a limited subset of all -commands existing in the `gdbremote` protocol. For example, commands such as -`remote get` or `remote put` (to download and upload a file from remote target, -respectively) are not supported. As a consequence, the default `remote` mode -for `gef` will not work either, as `gef` won't be able to fetch the content of -the remote procfs. -To circumvent this and still enjoy `gef` features with QEMU-user, a simple stub -can be artificially added, with the option `--qemu-mode` option of `gef-remote`. -Note that you need to set the architecture to match the target binary first: -``` -$ qemu-arm -g 1234 ./my/arm/binary -$ gdb-multiarch ./my/arm/binary -gef➤ set architecture arm -gef➤ gef-remote --qemu-mode localhost:1234 -``` +#### `qemu-system` + +To test locally, you can use the mini image linux x64 vm [here](https://mega.nz/file/ldQCDQiR#yJWJ8RXAHTxREKVmR7Hnfr70tIAQDFeWSYj96SvPO1k). + 1. Run `./run.sh` + 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) + -![gef-qemu-user](https://i.imgur.com/A0xgEdR.png) -When debugging a process in QEMU both the memory map of QEMU and of the process -are being shown alongside. diff --git a/gef.py b/gef.py index b3080199e..566988921 100644 --- a/gef.py +++ b/gef.py @@ -67,7 +67,7 @@ import pathlib import platform import re -import site +import shutil import socket import string import struct @@ -76,13 +76,13 @@ import tempfile import time import traceback - import warnings from functools import lru_cache from io import StringIO, TextIOWrapper from types import ModuleType -from typing import (Any, ByteString, Callable, Dict, Generator, IO, Iterable, Iterator, List, - NoReturn, Optional, Sequence, Tuple, Type, Union) +from typing import (IO, Any, ByteString, Callable, Dict, Generator, Iterable, + Iterator, List, NoReturn, Optional, Sequence, Tuple, Type, + Union) from urllib.request import urlopen @@ -113,7 +113,7 @@ def update_gef(argv: List[str]) -> int: try: - import gdb # type:ignore + import gdb # type:ignore except ImportError: # if out of gdb, the only action allowed is to update gef.py if len(sys.argv) >= 2 and sys.argv[1].lower() in ("--update", "--upgrade"): @@ -448,6 +448,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Callable: argtype = int_wrapper argname_is_list = not isinstance(argname, str) + assert not argname_is_list and isinstance(argname, str) if not argname_is_list and argname.startswith("-"): # optional args if argtype is bool: @@ -3481,7 +3482,7 @@ def get_terminal_size() -> Tuple[int, int]: return 600, 100 if platform.system() == "Windows": - from ctypes import windll, create_string_buffer + from ctypes import create_string_buffer, windll hStdErr = -12 herr = windll.kernel32.GetStdHandle(hStdErr) csbi = create_string_buffer(22) @@ -5793,14 +5794,15 @@ class RemoteCommand(GenericCommand): _cmdline_ = "gef-remote" _syntax_ = f"{_cmdline_} [OPTIONS] TARGET" - _example_ = [f"{_cmdline_} --pid 6789 localhost 1234", - f"{_cmdline_} --qemu-mode localhost4444 "] + _example_ = [f"{_cmdline_} localhost 1234", + f"{_cmdline_} --pid 6789 localhost 1234", + f"{_cmdline_} --qemu-user --qemu-binary /bin/debugme localhost 4444 "] def __init__(self) -> None: super().__init__(prefix=False) return - @parse_arguments({"host": "", "port": 0}, {"--pid": -1,}) + @parse_arguments({"host": "", "port": 0}, {"--pid": -1, "--qemu-user": True, "--qemu-binary": ""}) def do_invoke(self, _: List[str], **kwargs: Any) -> None: if gef.session.remote is not None: err("You already are in remote session. Close it first before opening a new one...") @@ -5812,8 +5814,21 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None: err("Missing parameters") return + # qemu-user support + qemu_binary: Optional[pathlib.Path] = None + try: + if args.qemu_user: + qemu_binary = pathlib.Path(args.qemu_binary).expanduser().absolute() if args.qemu_binary else gef.session.file + if not qemu_binary or not qemu_binary.exists(): + raise FileNotFoundError(f"{qemu_binary} does not exist") + except Exception as e: + err(f"Failed to initialize qemu-user mode, reason: {str(e)}") + return + # try to establish the remote session, throw on error - gef.session.remote = GefRemoteSessionManager(args.host, args.port, args.pid) + gef.session.remote = GefRemoteSessionManager(args.host, args.port, args.pid, qemu_binary) + reset_all_caches() + gdb.execute("context") return @@ -7904,12 +7919,12 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None: addr = align_address(parse_address(args.location)) size, fcode = self.SUPPORTED_SIZES[self.format] - values = args.values + values = args.values - if size == 1: + if size == 1: if values[0].startswith("$_gef"): var_name = values[0] - try: + try: values = str(gdb.parse_and_eval(var_name)).lstrip("{").rstrip("}").replace(",","").split(" ") except: gef_print(f"Bad variable specified, check value with command: p {var_name}") @@ -10135,10 +10150,11 @@ def __parse_maps(self) -> List[Section]: def __parse_procfs_maps(self) -> Generator[Section, None, None]: """Get the memory mapping from procfs.""" - __process_map_file = gef.session.remote.maps if gef.session.remote else gef.session.maps - if not __process_map_file: - return - with __process_map_file.open("r") as fd: + procfs_mapfile = gef.session.maps + if not procfs_mapfile: + is_remote = gef.session.remote is not None + raise FileNotFoundError(f"Missing {'remote ' if is_remote else ''}procfs map file") + with procfs_mapfile.open("r") as fd: for line in fd: line = line.strip() addr, perm, off, _, rest = line.split(" ", 4) @@ -10340,10 +10356,10 @@ def invoke_hooks(self, is_read: bool, setting: GefSetting) -> None: if not setting.hooks: return idx = 0 if is_read else 1 - if not setting.hooks[idx]: - return - for callback in setting.hooks[idx]: - callback() + if setting.hooks[idx]: + for callback in setting.hooks[idx]: + callback() + return class GefSessionManager(GefManager): @@ -10377,7 +10393,7 @@ 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}')" + return f"Session({'Local' if self.remote is None else 'Remote'}, pid={self.pid or 'Not running'}, os='{self.os}')" @property def auxiliary_vector(self) -> Optional[Dict[str, int]]: @@ -10457,30 +10473,32 @@ def canary(self) -> Optional[Tuple[int, int]]: @property def maps(self) -> Optional[pathlib.Path]: + """Returns the Path to the procfs entry for the memory mapping.""" if not is_alive(): return None - if gef.session.remote is not None: - return gef.session.remote.maps if not self._maps: - self._maps = pathlib.Path(f"/proc/{self.pid}/maps") + if gef.session.remote is not None: + self._maps = gef.session.remote.maps + else: + self._maps = pathlib.Path(f"/proc/{self.pid}/maps") return self._maps class GefRemoteSessionManager(GefSessionManager): """Class for managing remote sessions with GEF. It will create a temporary environment designed to clone the remote one.""" - def __init__(self, host: str, port: int, pid: int =-1) -> None: + def __init__(self, host: str, port: int, pid: int =-1, qemu: Optional[pathlib.Path] = None) -> None: super().__init__() self.__host = host self.__port = port self.__local_root_fd = tempfile.TemporaryDirectory() self.__local_root_path = pathlib.Path(self.__local_root_fd.name) + self.__qemu = qemu dbg(f"[remote] initializing remote session with {self.target} under {self.root}") if not self.connect(pid): raise EnvironmentError(f"Cannot connect to remote target {self.target}") if not self.setup(): raise EnvironmentError(f"Failed to create a proper environment for {self.target}") - gdb.execute("context") return def __del__(self) -> None: @@ -10492,8 +10510,11 @@ def __del__(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})" + return f"RemoteSession(target='{self.target}', local='{self.root}', pid={self.pid}, qemu_user={bool(self.in_qemu_user())})" @property def target(self) -> str: @@ -10563,9 +10584,57 @@ def connect(self, pid: int) -> bool: return False def setup(self) -> bool: + # setup remote adequately depending on remote or qemu mode + if self.in_qemu_user(): + dbg(f"Setting up as qemu session, target={self.__qemu}") + self.__setup_qemu() + else: + dbg(f"Setting up as remote session") + self.__setup_remote() + + # refresh gef to consider the binary + reset_all_caches() + gef.binary = Elf(self.lfile) + reset_architecture() + return True + + def __setup_qemu(self) -> bool: + # setup emulated file in the chroot + assert self.__qemu + target = self.root / str(self.__qemu.parent).lstrip("/") + target.mkdir(parents=True, exist_ok=False) + shutil.copy2(self.__qemu, target) + self._file = self.__qemu + assert self.lfile.exists() + + # create a procfs + procfs = self.root / f"proc/{self.pid}/" + procfs.mkdir(parents=True, exist_ok=True) + + ## /proc/pid/cmdline + cmdline = procfs / "cmdline" + if not cmdline.exists(): + with cmdline.open("w") as fd: + fd.write("") + + ## /proc/pid/environ + environ = procfs / "environ" + if not environ.exists(): + with environ.open("wb") as fd: + fd.write(b"PATH=/bin\x00HOME=/tmp\x00") + + ## /proc/pid/maps + maps = procfs / "maps" + if not maps.exists(): + with maps.open("w") as fd: + fname = self.file.absolute() + mem_range = "00000000-ffffffff" if is_32bit() else "0000000000000000-ffffffffffffffff" + fd.write(f"{mem_range} rwxp 00000000 00:00 0 {fname}\n") + return True + + def __setup_remote(self) -> bool: # get the file fpath = f"/proc/{self.pid}/exe" - print(self.file, fpath) if not self.sync(fpath, str(self.file)): err(f"'{fpath}' could not be fetched on the remote system.") return False @@ -10584,11 +10653,6 @@ def setup(self) -> bool: fname = self.file.absolute() mem_range = "00000000-ffffffff" if is_32bit() else "0000000000000000-ffffffffffffffff" fd.write(f"{mem_range} rwxp 00000000 00:00 0 {fname}\n") - - # refresh gef to consider the binary - reset_all_caches() - gef.binary = Elf(self.lfile) - reset_architecture() return True def remote_objfile_event_handler(self, evt: "gdb.NewObjFileEvent") -> None: diff --git a/tests/commands/gef_remote.py b/tests/commands/gef_remote.py index afee52da5..eceb2ecdc 100644 --- a/tests/commands/gef_remote.py +++ b/tests/commands/gef_remote.py @@ -3,7 +3,8 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdbserver_session +from tests.utils import (GefUnitTestGeneric, _target, gdb_run_cmd, + gdbserver_session, qemuuser_session) GDBSERVER_PREFERED_HOST = "localhost" GDBSERVER_PREFERED_PORT = 1234 @@ -13,12 +14,27 @@ class GefRemoteCommand(GefUnitTestGeneric): def test_cmd_gef_remote(self): - before = [f"gef-remote {GDBSERVER_PREFERED_HOST} {GDBSERVER_PREFERED_PORT}"] - with gdbserver_session(port=GDBSERVER_PREFERED_PORT) as _: + port = GDBSERVER_PREFERED_PORT + 1 + before = [f"gef-remote {GDBSERVER_PREFERED_HOST} {port}"] + with gdbserver_session(port=port) as _: res = gdb_run_cmd( "pi print(gef.session.remote)", before=before) self.assertNoException(res) self.assertIn( - f"RemoteSession(target='{GDBSERVER_PREFERED_HOST}:{GDBSERVER_PREFERED_PORT}', local='/tmp/", res) + f"RemoteSession(target='{GDBSERVER_PREFERED_HOST}:{port}', local='/tmp/", res) + self.assertIn(", qemu_user=False)", res) + def test_cmd_gef_remote_qemu_user(self): + port = GDBSERVER_PREFERED_PORT + 2 + target = _target("default") + before = [ + f"gef-remote --qemu-user --qemu-binary {target} {GDBSERVER_PREFERED_HOST} {port}"] + with qemuuser_session(port=port) as _: + res = gdb_run_cmd( + "pi print(gef.session.remote)", before=before) + self.assertNoException(res) + self.assertIn( + f"RemoteSession(target='{GDBSERVER_PREFERED_HOST}:{port}', local='/tmp/", res) + self.assertIn(", qemu_user=True)", res) + diff --git a/tests/utils.py b/tests/utils.py index 0d4e8bc8b..cb8cd088b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,6 +3,7 @@ """ import contextlib +import enum import os import pathlib import platform @@ -10,11 +11,9 @@ import subprocess import tempfile import unittest -from urllib.request import urlopen import warnings -import enum - -from typing import Dict, Iterable, Union, List, Optional +from typing import Dict, Iterable, List, Optional, Union +from urllib.request import urlopen TMPDIR = pathlib.Path(tempfile.gettempdir()) ARCH = (os.getenv("GEF_CI_ARCH") or platform.machine()).lower() @@ -229,7 +228,7 @@ def _target(name: str, extension: str = ".out") -> pathlib.Path: def start_gdbserver(exe: Union[str, pathlib.Path] = _target("default"), - port: int = 1234) -> subprocess.Popen: + port: int = GDBSERVER_DEFAULT_PORT) -> subprocess.Popen: """Start a gdbserver on the target binary. Args: @@ -265,6 +264,32 @@ def gdbserver_session(*args, **kwargs): finally: stop_gdbserver(sess) + +def start_qemuuser(exe: Union[str, pathlib.Path] = _target("default"), + port: int = GDBSERVER_DEFAULT_PORT) -> subprocess.Popen: + return subprocess.Popen(["qemu-x86_64", "-g", str(port), exe], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def stop_qemuuser(process: subprocess.Popen) -> None: + if process.poll() is None: + process.kill() + process.wait() + + +@contextlib.contextmanager +def qemuuser_session(*args, **kwargs): + exe = kwargs.get("exe", "") or _target("default") + port = kwargs.get("port", 0) or GDBSERVER_DEFAULT_PORT + sess = start_gdbserver(exe, port) + try: + yield sess + finally: + stop_gdbserver(sess) + + + + def findlines(substring: str, buffer: str) -> List[str]: """Extract the lines from the buffer which contains the pattern `substring`