diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 291490f7e..b1237fae7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -80,10 +80,10 @@ jobs: * ${(forbidden_words.length === 0 || forbidden_words[0] === '') ? "**does not** include forbidden words" : "includes the forbidden words:" + forbidden_words.join(", ")} `; - const { owner, repo, number } = context.issue; - await github.rest.issues.createComment({ owner, repo, issue_number: number, body: comment }); - try { + const { owner, repo, number } = context.issue; + await github.rest.issues.createComment({ owner, repo, issue_number: number, body: comment }); + if(docs_changes > 0) { await github.rest.issues.addLabels({ owner: owner, @@ -92,9 +92,7 @@ jobs: labels: ['documentation'] }); } - } catch (err) { console.log(err); } - try { if(tests_changes > 0) { await github.rest.issues.addLabels({ owner: owner, diff --git a/.pylintrc b/.pylintrc index 8d6c69538..1a9979b4e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -409,7 +409,25 @@ confidence= ; anomalous-backslash-in-string, ; bad-open-mode -enable = F,E,unreachable,duplicate-key,unnecessary-semicolon,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode,dangerous-default-value,trailing-whitespace,unneeded-not,singleton-comparison,unused-import,line-too-long,multiple-statements,consider-using-f-string,global-variable-not-assigned +enable = + F, + E, + anomalous-backslash-in-string, + bad-format-string, + bad-open-mode, + consider-using-f-string, + dangerous-default-value, + duplicate-key, + global-variable-not-assigned + line-too-long, + singleton-comparison, + trailing-whitespace, + unnecessary-semicolon, + unneeded-not, + unreachable, + unused-import, + unused-variable, + binary-op-exception disable = all [REPORTS] diff --git a/docs/testing.md b/docs/testing.md index 6dc3f5951..c6d1584eb 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -49,8 +49,11 @@ You can then use `pytest` directly to help you fix each error specifically. GEF entirely relies on [`pytest`](https://pytest.org) for its testing. Refer to the project documentation for details. -Adding a new command __requires__ for extensive testing in a new dedicated test module that should -be located in `/root/of/gef/tests/commands/my_new_command.py` +Adding new code __requires__ extensive testing. Tests can be added in their own module in the +`tests/` folder. For example, if adding a new command to `gef`, a new test module should be created +and located in `/root/of/gef/tests/commands/my_new_command.py`. The test class __must__ inherit +`tests.base.RemoteGefUnitTestGeneric`. This class allows one to manipulate gdb and gef through rpyc +under their respective `self._gdb` and `self._gef` attributes. A skeleton of a test module would look something like: @@ -60,30 +63,74 @@ A skeleton of a test module would look something like: """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.utils import RemoteGefUnitTestGeneric -class MyCommandCommand(GefUnitTestGeneric): +class MyCommandCommand(RemoteGefUnitTestGeneric): """`my-command` command test module""" + def setUp(self) -> None: + # By default, tests will be executed against the default.out binary + # You can change this behavior in the `setUp` function + self._target = debug_target("my-custom-binary-for-tests") + return super().setUp() + def test_cmd_my_command(self): - # `my-command` is expected to fail if the session is not active - self.assertFailIfInactiveSession(gdb_run_cmd("my-command")) + # some convenience variables + root, gdb, gef = self._conn.root, self._gdb, self._gef + + # You can then interact with any command from gdb or any class/function/variable from gef + # For instance: - # `my-command` should never throw an exception in GDB when running - res = gdb_start_silent_cmd("my-command") - self.assertNoException(res) + # * tests that `my-command` is expected to fail if the session is not active + output = gdb.execute("my-command", to_string=True) + assert output == ERROR_INACTIVE_SESSION_MESSAGE - # it also must print out a "Hello World" message - self.assertIn("Hello World", res) + # * `my-command` must print "Hello World" message when executed in running context + gdb.execute("start") + output = gdb.execute("my-command", to_string=True) + assert "Hello World" == output ``` +You might want to refer to the following documentations: + +* [`pytest`](https://docs.pytest.org/en/) +* [`gdb Python API`](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-API.html) +* (maybe) [`rpyc`](https://rpyc.readthedocs.io/en/latest/) + When running your test, you can summon `pytest` with the `--pdb` flag to enter the python testing environment to help you get more information about the reason of failure. One of the most convenient ways to test `gef` properly is using the `pytest` integration of modern editors such as VisualStudio Code or PyCharm. Without proper tests, new code will not be integrated. +Also note that GEF can be remotely controlled using the script `scripts/remote_debug.py` as such: + +```text +$ gdb -q -nx +(gdb) source /path/to/gef/gef.py +[...] +gef➤ source /path/to/gef/scripts/remote_debug.py +gef➤ pi start_rpyc_service(4444) +``` + +Here RPyC will be started on the local host, and bound to the TCP port 4444. We can now connect +using a regular Python REPL: + +```text +>>> import rpyc +>>> c = rpyc.connect("localhost", 4444) +>>> gdb = c.root._gdb +>>> gef = c.root._gef +# We can now fully control the remote GDB +>>> gdb.execute("file /bin/ls") +>>> gdb.execute("start") +>>> print(hex(gef.arch.pc)) +0x55555555aab0 +>>> print(hex(gef.arch.sp)) +0x7fffffffdcf0 +``` + ### Linting GEF You can use the Makefile at the root of the project to get the proper linting settings. For most diff --git a/gef.py b/gef.py index 5c2dbfd24..3300b1c85 100644 --- a/gef.py +++ b/gef.py @@ -84,10 +84,13 @@ from io import StringIO, TextIOWrapper from types import ModuleType from typing import (Any, ByteString, Callable, Dict, Generator, Iterable, - Iterator, List, NoReturn, Optional, Sequence, Set, Tuple, Type, - Union) + Iterator, List, Literal, NoReturn, Optional, Sequence, Set, Tuple, Type, + Union, TYPE_CHECKING) from urllib.request import urlopen +if TYPE_CHECKING: + import gdb + GEF_DEFAULT_BRANCH = "main" GEF_EXTRAS_DEFAULT_BRANCH = "main" @@ -102,29 +105,17 @@ def http_get(url: str) -> Optional[bytes]: def update_gef(argv: List[str]) -> int: - """Try to update `gef` to the latest version pushed on GitHub main branch. - Return 0 on success, 1 on failure. """ - ver = GEF_DEFAULT_BRANCH - latest_gef_data = http_get(f"https://raw.githubusercontent.com/hugsy/gef/{ver}/scripts/gef.sh") - if not latest_gef_data: - print("[-] Failed to get remote gef") - return 1 - with tempfile.NamedTemporaryFile(suffix=".sh") as fd: - fd.write(latest_gef_data) - fd.flush() - fpath = pathlib.Path(fd.name) - return subprocess.run(["bash", fpath, ver], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL).returncode + """Obsolete. Use `gef.sh`.""" + return -1 try: 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"): - sys.exit(update_gef(sys.argv[2:])) + print("[-] `update_gef` is obsolete. Use the `gef.sh` script to update gef from the command line.") print("[-] gef cannot run as standalone") - sys.exit(0) + sys.exit(1) GDB_MIN_VERSION = (8, 0) @@ -293,6 +284,10 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: # Helpers # +class ObsoleteException(Exception): pass + +class AlreadyRegisteredException(Exception): pass + def p8(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: """Pack one byte respecting the current architecture endianness.""" endian = e or gef.arch.endianness @@ -699,6 +694,15 @@ def __str__(self) -> str: return (f"Section(page_start={self.page_start:#x}, page_end={self.page_end:#x}, " f"permissions={self.permission!s})") + def __eq__(self, other: "Section") -> bool: + return other and \ + self.page_start == other.page_start and \ + self.page_end == other.page_end and \ + self.offset == other.offset and \ + self.permission == other.permission and \ + self.inode == other.inode and \ + self.path == other.path + Zone = collections.namedtuple("Zone", ["name", "zone_start", "zone_end", "filename"]) @@ -729,7 +733,7 @@ class FileFormat: sections: List[FileFormatSection] def __init__(self, path: Union[str, pathlib.Path]) -> None: - raise NotImplemented + raise NotImplementedError def __init_subclass__(cls: Type["FileFormat"], **kwargs): global __registered_file_formats__ @@ -742,8 +746,8 @@ def __init_subclass__(cls: Type["FileFormat"], **kwargs): return @classmethod - def is_valid(cls, path: pathlib.Path) -> bool: - raise NotImplemented + def is_valid(cls, _: pathlib.Path) -> bool: + raise NotImplementedError def __str__(self) -> str: return f"{self.name}('{self.path.absolute()}', entry @ {self.entry_point:#x})" @@ -1956,7 +1960,7 @@ def __exit__(self, *exc: Any) -> None: class RedirectOutputContext: def __init__(self, to_file: str = "/dev/null") -> None: - if " " in to_file: raise ValueEror("Target filepath cannot contain spaces") + if " " in to_file: raise ValueError("Target filepath cannot contain spaces") self.redirection_target_file = to_file return @@ -1977,7 +1981,7 @@ def __exit__(self, *exc: Any) -> None: def enable_redirect_output(to_file: str = "/dev/null") -> None: """Redirect all GDB output to `to_file` parameter. By default, `to_file` redirects to `/dev/null`.""" - if " " in to_file: raise ValueEror("Target filepath cannot contain spaces") + if " " in to_file: raise ValueError("Target filepath cannot contain spaces") gdb.execute("set logging overwrite") gdb.execute(f"set logging file {to_file}") gdb.execute("set logging redirect on") @@ -2292,6 +2296,9 @@ def __init_subclass__(cls, **kwargs): def __str__(self) -> str: return f"Architecture({self.arch}, {self.mode or 'None'}, {repr(self.endianness)})" + def __repr__(self) -> str: + return self.__str__() + @staticmethod def supports_gdb_arch(gdb_arch: str) -> Optional[bool]: """If implemented by a child `Architecture`, this function dictates if the current class @@ -2834,7 +2841,7 @@ class X86(Architecture): def flag_register_to_human(self, val: Optional[int] = None) -> str: reg = self.flag_register - if not val: + if val is None: val = gef.arch.register(reg) return flags_to_human(val, self.flags_table) @@ -3017,7 +3024,7 @@ class PowerPC(Architecture): def flag_register_to_human(self, val: Optional[int] = None) -> str: # https://www.cebix.net/downloads/bebox/pem32b.pdf (% 2.1.3) - if not val: + if val is None: reg = self.flag_register val = gef.arch.register(reg) return flags_to_human(val, self.flags_table) @@ -3120,7 +3127,7 @@ class SPARC(Architecture): def flag_register_to_human(self, val: Optional[int] = None) -> str: # https://www.gaisler.com/doc/sparcv8.pdf reg = self.flag_register - if not val: + if val is None: val = gef.arch.register(reg) return flags_to_human(val, self.flags_table) @@ -3395,7 +3402,7 @@ def get_os() -> str: def is_qemu() -> bool: if not is_remote_debug(): return False - response = gdb.execute("maintenance packet Qqemu.sstepbits", to_string=True, from_tty=False) + response = gdb.execute("maintenance packet Qqemu.sstepbits", to_string=True, from_tty=False) or "" return "ENABLE=" in response @@ -3403,7 +3410,7 @@ def is_qemu() -> bool: def is_qemu_usermode() -> bool: if not is_qemu(): return False - response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) + response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) or "" return "Text=" in response @@ -3411,7 +3418,7 @@ def is_qemu_usermode() -> bool: def is_qemu_system() -> bool: if not is_qemu(): return False - response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) + response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) or "" return "received: \"\"" in response @@ -3602,7 +3609,7 @@ def exit_handler(_: "gdb.ExitedEvent") -> None: warn(f"{bkp_fpath} exists, content will be overwritten") with bkp_fpath.open("w") as fd: - for bp in gdb.breakpoints(): + for bp in list(gdb.breakpoints()): if not bp.enabled or not bp.is_valid: continue fd.write(f"{'t' if bp.temporary else ''}break {bp.location}\n") @@ -4076,14 +4083,16 @@ def instantiate(self, base: int) -> None: self.destroy() try: - res = gdb.execute(self.set_func(base), to_string=True) + res = gdb.execute(self.set_func(base), to_string=True) or "" + if not res: return except gdb.error as e: - err(e) + err(str(e)) return if "Breakpoint" not in res: err(res) return + res_list = res.split() self.bp_num = res_list[1] self.bp_addr = res_list[3] @@ -4499,16 +4508,18 @@ def register_priority_command(cls: Type["GenericCommand"]) -> Type["GenericComma def register(cls: Union[Type["GenericCommand"], Type["GenericFunction"]]) -> Union[Type["GenericCommand"], Type["GenericFunction"]]: global __registered_commands__, __registered_functions__ if issubclass(cls, GenericCommand): - assert( hasattr(cls, "_cmdline_")) - assert( hasattr(cls, "do_invoke")) - assert( all(map(lambda x: x._cmdline_ != cls._cmdline_, __registered_commands__))) + assert hasattr(cls, "_cmdline_") + assert hasattr(cls, "do_invoke") + if any(map(lambda x: x._cmdline_ == cls._cmdline_, __registered_commands__)): + raise AlreadyRegisteredException(cls._cmdline_) __registered_commands__.add(cls) return cls if issubclass(cls, GenericFunction): - assert( hasattr(cls, "_function_")) - assert( hasattr(cls, "invoke")) - assert( all(map(lambda x: x._function_ != cls._function_, __registered_functions__))) + assert hasattr(cls, "_function_") + assert hasattr(cls, "invoke") + if any(map(lambda x: x._function_ == cls._function_, __registered_functions__)): + raise AlreadyRegisteredException(cls._function_) __registered_functions__.add(cls) return cls @@ -4557,6 +4568,8 @@ def invoke(self, args: str, from_tty: bool) -> None: # catching generic Exception, but rather specific ones. This is allows a much cleaner use. if is_debug(): show_last_exception() + if gef.config["gef.propagate_debug_exception"] is True: + raise else: err(f"Command '{self._cmdline_}' failed to execute properly, reason: {e}") return @@ -4636,7 +4649,7 @@ def __set_repeat_count(self, argv: List[str], from_tty: bool) -> None: self.repeat_count = 0 return - command = gdb.execute("show commands", to_string=True).strip().split("\n")[-1] + command = (gdb.execute("show commands", to_string=True) or "").strip().split("\n")[-1] self.repeat = self.__last_command == command self.repeat_count = self.repeat_count + 1 if self.repeat else 0 self.__last_command = command @@ -4750,9 +4763,9 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None: out = f"var buf = [{sdata}]" elif args.lang == "asm": asm_type = self.format_matrix[args.bitlen][2] - out = "buf {0} {1}".format(asm_type, sdata) + out = f"buf {asm_type} {sdata}" elif args.lang == "hex": - out = binascii.hexlify(gef.memory.read(start_addr, end_addr-start_addr)).decode() + out = gef.memory.read(start_addr, end_addr-start_addr).hex() else: raise ValueError(f"Invalid format: {args.lang}") @@ -4929,7 +4942,7 @@ def do_invoke(self, argv: List[str]) -> None: try: gdb.execute(f"attach {' '.join(argv)}", to_string=True) except gdb.error as e: - err(e) + err(str(e)) return # after attach, we are stopped so that we can # get base address to modify our breakpoint @@ -5545,7 +5558,7 @@ def do_invoke(self, _: List) -> None: struct_color = gef.config["pcustom.structure_type"] filename_color = gef.config["pcustom.structure_name"] for module in manager.modules.values(): - __modules = ", ".join([Color.colorify(structure_name, struct_color) for structure_name in module.values()]) + __modules = ", ".join([Color.colorify(str(structure), struct_color) for structure in module.values()]) __filename = Color.colorify(str(module.path), filename_color) gef_print(f"{RIGHT_ARROW} {__filename} ({__modules})") return @@ -5811,7 +5824,7 @@ def search_pattern_by_address(self, pattern: str, start_address: int, end_addres try: mem = gef.memory.read(chunk_addr, chunk_size) - except gdb.MemoryError as e: + except gdb.MemoryError: return [] for match in re.finditer(_pattern, mem): @@ -6840,41 +6853,6 @@ def do_invoke(self, *_: Any, **kwargs: Any) -> None: return -@register -class SolveKernelSymbolCommand(GenericCommand): - """Solve kernel symbols from kallsyms table.""" - - _cmdline_ = "ksymaddr" - _syntax_ = f"{_cmdline_} SymbolToSearch" - _example_ = f"{_cmdline_} prepare_creds" - - @parse_arguments({"symbol": ""}, {}) - def do_invoke(self, _: List[str], **kwargs: Any) -> None: - def hex_to_int(num): - try: - return int(num, 16) - except ValueError: - return 0 - args : argparse.Namespace = kwargs["arguments"] - if not args.symbol: - self.usage() - return - sym = args.symbol - with open("/proc/kallsyms", "r") as f: - syms = [line.strip().split(" ", 2) for line in f] - matches = [(hex_to_int(addr), sym_t, " ".join(name.split())) for addr, sym_t, name in syms if sym in name] - for addr, sym_t, name in matches: - if sym == name.split()[0]: - ok(f"Found matching symbol for '{name}' at {addr:#x} (type={sym_t})") - else: - warn(f"Found partial match for '{sym}' at {addr:#x} (type={sym_t}): {name}") - if not matches: - err(f"No match for '{sym}'") - elif matches[0][0] == 0: - err("Check that you have the correct permissions to view kernel symbol addresses") - return - - @register class DetailRegistersCommand(GenericCommand): """Display full details on one, many or all registers value from current architecture.""" @@ -7167,7 +7145,7 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None: try: elf = Elf(filename) - except ValueError as ve: + except ValueError: err(f"`{filename}` is an invalid value for ELF file") return @@ -7786,7 +7764,6 @@ def context_source(self) -> None: show_full_path_max = self["show_full_source_file_name_max_len"] show_basename_path_max = self["show_basename_source_file_name_max_len"] - show_prefix_path_len = self["show_prefix_source_path_name_len"] nb_line = self["nb_lines_code"] fn = symtab.filename @@ -7875,6 +7852,7 @@ def context_trace(self) -> None: frames = [current_frame] while current_frame != orig_frame: current_frame = current_frame.older() + if not current_frame: break frames.append(current_frame) nb_backtrace_before = self["nb_lines_backtrace_before"] @@ -9623,7 +9601,7 @@ def __init__(self) -> None: gef.config["gef.autosave_breakpoints_file"] = GefSetting("", str, "Automatically save and restore breakpoints") gef.config["gef.disable_target_remote_overwrite"] = GefSetting(False, bool, "Disable the overwrite of `target remote`") plugins_dir = GefSetting("", str, "Autoload additional GEF commands from external directory", hooks={"on_write": GefSetting.no_spaces}) - plugins_dir.add_hook("on_write", lambda _: self.load_extra_plugins()) + plugins_dir.add_hook("on_write", lambda _: self.reload_extra_plugins()) gef.config["gef.extra_plugins_dir"] = plugins_dir gef.config["gef.disable_color"] = GefSetting(False, bool, "Disable all colors in GEF") gef.config["gef.tempdir"] = GefSetting(GEF_TEMP_DIR, str, "Directory to use for temporary/cache content", hooks={"on_write": GefSetting.no_spaces}) @@ -9632,6 +9610,7 @@ def __init__(self) -> None: gef.config["gef.bruteforce_main_arena"] = GefSetting(False, bool, "Allow bruteforcing main_arena symbol if everything else fails") gef.config["gef.libc_version"] = GefSetting("", str, "Specify libc version when auto-detection fails") gef.config["gef.main_arena_offset"] = GefSetting("", str, "Offset from libc base address to main_arena symbol (int or hex). Set to empty string to disable.") + gef.config["gef.propagate_debug_exception"] = GefSetting(False, bool, "If true, when debug mode is enabled, Python exceptions will be propagated all the way.") self.commands : Dict[str, GenericCommand] = collections.OrderedDict() self.functions : Dict[str, GenericFunction] = collections.OrderedDict() @@ -9639,19 +9618,19 @@ def __init__(self) -> None: return @property + @deprecated() def loaded_commands(self) -> List[Tuple[str, Type[GenericCommand], Any]]: - print("Obsolete loaded_commands") - raise + raise ObsoleteException("Obsolete loaded_commands") @property + @deprecated() def loaded_functions(self) -> List[Type[GenericFunction]]: - print("Obsolete loaded_functions") - raise + raise ObsoleteException("Obsolete loaded_functions") @property + @deprecated() def missing_commands(self) -> Dict[str, Exception]: - print("Obsolete missing_commands") - raise + raise ObsoleteException("Obsolete missing_commands") def setup(self) -> None: self.load() @@ -9673,46 +9652,44 @@ def load_plugin(fpath: pathlib.Path) -> bool: try: dbg(f"Loading '{fpath}'") gdb.execute(f"source {fpath}") + except AlreadyRegisteredException: + pass except Exception as e: warn(f"Exception while loading {fpath}: {str(e)}") return False return True - nb_added = -1 - start_time = time.perf_counter() - try: + def load_plugins_from_directory(plugin_directory: pathlib.Path): + nb_added = -1 nb_inital = len(__registered_commands__) - directories: List[str] = gef.config["gef.extra_plugins_dir"].split(";") or [] - for d in directories: - d = d.strip() - if not d: continue - directory = pathlib.Path(d).expanduser() - if not directory.is_dir(): continue - sys.path.append(str(directory.absolute())) - for entry in directory.iterdir(): - if entry.is_dir(): - if entry.name in ('gdb', 'gef', '__pycache__'): continue - load_plugin(entry / "__init__.py") - else: - if entry.suffix != ".py": continue - if entry.name == "__init__.py": continue - load_plugin(entry) - - nb_added = len(__registered_commands__) - nb_inital - if nb_added > 0: - self.load() - nb_failed = len(__registered_commands__) - len(self.commands) - end_time = time.perf_counter() - load_time = end_time - start_time - ok(f"{Color.colorify(str(nb_added), 'bold green')} extra commands added from " - f"'{Color.colorify(', '.join(directories), 'bold blue')}' in {load_time:.2f} seconds") - if nb_failed != 0: - warn(f"{Color.colorify(str(nb_failed), 'bold light_gray')} extra commands/functions failed to be added. " - "Check `gef missing` to know why") + start_time = time.perf_counter() + for entry in plugin_directory.glob("**/*.py"): + load_plugin(entry) - except gdb.error as e: - err(f"failed: {e}") - return nb_added + try: + nb_added = len(__registered_commands__) - nb_inital + if nb_added > 0: + self.load() + nb_failed = len(__registered_commands__) - len(self.commands) + load_time = time.perf_counter() - start_time + ok(f"{Color.colorify(str(nb_added), 'bold green')} extra commands added " \ + f"in {load_time:.2f} seconds") + if nb_failed != 0: + warn(f"{Color.colorify(str(nb_failed), 'bold light_gray')} extra commands/functions failed to be added. " + "Check `gef missing` to know why") + except gdb.error as e: + err(f"failed: {e}") + return nb_added + directory = gef.config["gef.extra_plugins_dir"] or "" + if not directory: + return 0 + return load_plugins_from_directory(pathlib.Path(directory).expanduser().absolute()) + + def reload_extra_plugins(self) -> int: + try: + return self.load_extra_plugins() + except: + return -1 @property def loaded_command_names(self) -> Iterable[str]: @@ -10007,7 +9984,7 @@ def invoke(self, args: Any, from_tty: bool) -> None: # save the aliases cfg.add_section("aliases") for alias in gef.session.aliases: - cfg.set("aliases", alias._alias, alias._command) + cfg.set("aliases", alias.alias, alias.command) with GEF_RC.open("w") as fd: cfg.write(fd) @@ -10143,11 +10120,11 @@ def __init__(self, alias: str, command: str, completer_class: int = gdb.COMPLETE if not p: return - if any(x for x in gef.session.aliases if x._alias == alias): + if any(x for x in gef.session.aliases if x.alias == alias): return - self._command = command - self._alias = alias + self.command = command + self.alias = alias c = command.split()[0] r = self.lookup_command(c) self.__doc__ = f"Alias for '{Color.greenify(command)}'" @@ -10162,8 +10139,14 @@ def __init__(self, alias: str, command: str, completer_class: int = gdb.COMPLETE gef.session.aliases.append(self) return + def __repr__(self) -> str: + return f"GefAlias(from={self.alias}, to={self.command})" + + def __str__(self) -> str: + return f"GefAlias(from={self.alias}, to={self.command})" + def invoke(self, args: Any, from_tty: bool) -> None: - gdb.execute(f"{self._command} {args}", from_tty=from_tty) + gdb.execute(f"{self.command} {args}", from_tty=from_tty) return def lookup_command(self, cmd: str) -> Optional[Tuple[str, GenericCommand]]: @@ -10228,7 +10211,7 @@ def do_invoke(self, argv: List[str]) -> None: self.usage() return try: - alias_to_remove = next(filter(lambda x: x._alias == argv[0], gef.session.aliases)) + alias_to_remove = next(filter(lambda x: x.alias == argv[0], gef.session.aliases)) gef.session.aliases.remove(alias_to_remove) except (ValueError, StopIteration): err(f"{argv[0]} not found in aliases.") @@ -10251,7 +10234,7 @@ def __init__(self) -> None: def do_invoke(self, _: List[str]) -> None: ok("Aliases defined:") for a in gef.session.aliases: - gef_print(f"{a._alias:30s} {RIGHT_ARROW} {a._command}") + gef_print(f"{a.alias:30s} {RIGHT_ARROW} {a.command}") return @@ -10807,6 +10790,7 @@ def add_hook(self, access, func): if not callable(func): raise ValueError("hook is not callable") self.hooks[access].append(func) + return self @staticmethod def no_spaces(value): @@ -10901,7 +10885,7 @@ def auxiliary_vector(self) -> Optional[Dict[str, int]]: return None if not self._auxiliary_vector: auxiliary_vector = {} - auxv_info = gdb.execute("info auxv", to_string=True) + auxv_info = gdb.execute("info auxv", to_string=True) or "" if not auxv_info or "failed" in auxv_info: err("Failed to query auxiliary variables") return None @@ -11313,11 +11297,11 @@ def target_remote_posthook(): try: pyenv = which("pyenv") - PYENV_ROOT = gef_pystring(subprocess.check_output([pyenv, "root"]).strip()) - PYENV_VERSION = gef_pystring(subprocess.check_output([pyenv, "version-name"]).strip()) - site_packages_dir = os.path.join(PYENV_ROOT, "versions", PYENV_VERSION, "lib", - f"python{PYENV_VERSION[:3]}", "site-packages") - site.addsitedir(site_packages_dir) + pyenv_root = gef_pystring(subprocess.check_output([pyenv, "root"]).strip()) + pyenv_version = gef_pystring(subprocess.check_output([pyenv, "version-name"]).strip()) + site_packages_dir = pathlib.Path(pyenv_root) / f"versions/{pyenv_version}/lib/python{pyenv_version[:3]}/site-packages" + assert site_packages_dir.is_dir() + site.addsitedir(str(site_packages_dir.absolute())) except FileNotFoundError: pass @@ -11353,6 +11337,7 @@ def target_remote_posthook(): # load GEF, set up the managers and load the plugins, functions, reset() + assert isinstance(gef, Gef) gef.gdb.load() gef.gdb.show_banner() diff --git a/scripts/generate-coverage-docs.sh b/scripts/generate-coverage-docs.sh index f92a4588c..f9c9120da 100644 --- a/scripts/generate-coverage-docs.sh +++ b/scripts/generate-coverage-docs.sh @@ -13,7 +13,7 @@ PY_VER=$(gdb -q -nx -ex 'pi print(f"{sys.version_info.major}.{sys.version_info.m rm -f -- "${GEF_DOCS_DIR}"/* echo "[+] Generating coverage report in '${TMPDIR_RUN}'" -COVERAGE_DIR="${TMPDIR_RUN}" python${PY_VER} -m pytest -n ${NB_CORES} "${GEF_TESTS_DIR}" +COVERAGE_DIR="${TMPDIR_RUN}" python${PY_VER} -m pytest -n ${NB_CORES} "${GEF_TESTS_DIR}" -k "not benchmark" echo "[+] Combining data to '${TMPDIR_COV}'" python${PY_VER} -m coverage combine --data-file=${TMPDIR_COV} "${TMPDIR_RUN}"/* diff --git a/scripts/remote_debug.py b/scripts/remote_debug.py new file mode 100644 index 000000000..fe39556be --- /dev/null +++ b/scripts/remote_debug.py @@ -0,0 +1,36 @@ +import logging + +import rpyc +import rpyc.core.protocol +import rpyc.utils.server + +RPYC_PORT = 18812 + + +class RemoteDebugService(rpyc.Service): + def on_connect(self, conn: rpyc.core.protocol.Connection): + logging.info(f"connect open: {str(conn)}") + return + + def on_disconnect(self, conn: rpyc.core.protocol.Connection): + logging.info(f"connection closed: {str(conn)}") + return + + def exposed_eval(self, cmd): + return eval(cmd) + + exposed_gdb = gdb + + exposed_gef = gef + + +def start_rpyc_service(port: int = RPYC_PORT): + logging.info(f"RPYC service listening on tcp/{port}") + svc = rpyc.utils.server.OneShotServer( + RemoteDebugService, + port=port, + protocol_config={ + "allow_public_attrs": True, + }, + ) + svc.start() diff --git a/tests/api/deprecated.py b/tests/api/deprecated.py index 319a58cc0..f99522b96 100644 --- a/tests/api/deprecated.py +++ b/tests/api/deprecated.py @@ -2,18 +2,19 @@ test module for deprecated functions """ + import pytest +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import WARNING_DEPRECATION_MESSAGE -from tests.utils import ( - gdb_test_python_method, - GefUnitTestGeneric, -) -class GefFuncDeprecatedApi(GefUnitTestGeneric): +class GefFuncDeprecatedApi(RemoteGefUnitTestGeneric): """Test class for deprecated functions and variables. Each of those tests expect to receive a deprecation warning.""" def test_deprecated_elf_values(self): + gdb = self._gdb + old_stuff = ( "Elf.X86_64", "Elf.X86_32", @@ -28,8 +29,17 @@ def test_deprecated_elf_values(self): ) for item in old_stuff: - with pytest.warns(Warning) as record: - res = gdb_test_python_method(item) - self.assertNoException(res) - if not record: - pytest.fail(f"Expected a warning for '{item}'!") + output = gdb.execute(f"pi {item}", to_string=True) + assert WARNING_DEPRECATION_MESSAGE in output + + def test_deprecated_gef_attributes(self): + root = self._conn.root + old_attributes = ( + "gef.gdb.loaded_commands", + "gef.gdb.loaded_functions", + "gef.gdb.missing_commands", + ) + + for i in old_attributes: + with pytest.raises(Exception, match="ObsoleteException"): + root.eval(i) diff --git a/tests/api/gef_arch.py b/tests/api/gef_arch.py index fab74049d..0df2e08aa 100644 --- a/tests/api/gef_arch.py +++ b/tests/api/gef_arch.py @@ -5,20 +5,39 @@ import pytest -from tests.utils import ARCH, gdb_test_python_method -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, is_64b, debug_target -class GefArchApi(GefUnitTestGeneric): +class GefArchApi(RemoteGefUnitTestGeneric): """`gef.arch` test module.""" - def test_func_gef_arch_ptrsize(self): - res = gdb_test_python_method("gef.arch.ptrsize") - self.assertIn(res.splitlines()[-1], ("4", "8")) - - - @pytest.mark.skipif(ARCH not in ["x86_64", "i686"], reason=f"Skipped for {ARCH}") - def test_func_reset_architecture(self): - res = gdb_test_python_method("gef.arch.arch, gef.arch.mode", before="reset_architecture()") - res = (res.splitlines()[-1]) - self.assertIn("X86", res) + def setUp(self) -> None: + self._target = debug_target("default") + return super().setUp() + + def test_api_gef_arch_ptrsize(self): + if is_64b(): + self.assertEqual(self._gef.arch.ptrsize, 8) + else: + self.assertEqual(self._gef.arch.ptrsize, 4) + + @pytest.mark.skipif(ARCH != "x86_64", reason=f"Skipped for {ARCH}") + def test_api_gef_arch_x86_64(self): + arch = self._gef.arch + assert arch.arch == "X86" + assert arch.mode == "64" + + self._gdb.execute("start") + assert arch.flag_register_to_human(0).startswith("[zero carry parity adjust") + assert ( + arch.flag_register_to_human(None) + .lower() + .startswith("[zero carry parity adjust") + ) + + @pytest.mark.skipif(ARCH != "i686", reason=f"Skipped for {ARCH}") + def test_api_gef_arch_x86(self): + arch = self._gef.arch + assert arch.arch == "X86" + assert arch.mode == "32" diff --git a/tests/api/gef_disasemble.py b/tests/api/gef_disasemble.py deleted file mode 100644 index 329ac6a18..000000000 --- a/tests/api/gef_disasemble.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -`gef.heap` test module. -""" - -import pytest - -from tests.utils import ARCH, debug_target, gdb_run_silent_cmd -from tests.utils import GefUnitTestGeneric - - -class GefDisassembleApiFunction(GefUnitTestGeneric): - """`gef_disassemble` function test module.""" - - @pytest.mark.skipif(ARCH not in ("x86_64", "i686"), reason=f"Skipped for {ARCH}") - def test_func_gef_disassemble(self): - cmd = "gef_disassemble(0x2337100, 4, 4)" - res = gdb_run_silent_cmd(f"pi os.linesep.join([str(i) for i in {cmd}])", target=debug_target("mmap-known-address")) - self.assertNoException(res) - self.assertIn( - ' 0x23370fc int3 \\n 0x23370fd int3 \\n 0x23370fe int3 \\n 0x23370ff int3 \\n 0x2337100 int3 \\n 0x2337101 int3 \\n 0x2337102 int3 \\n 0x2337103 int3 ', res) - - @pytest.mark.skipif(ARCH not in ("x86_64", "i686"), reason=f"Skipped for {ARCH}") - def test_func_gef_disassemble_page_border(self): - # Regression test for issue #922 - cmd = "gef_disassemble(0x2337000, 4, 4)" - res = gdb_run_silent_cmd( - f"pi os.linesep.join([str(i) for i in {cmd}])", target=debug_target("mmap-known-address")) - self.assertNoException(res) - self.assertIn( - '0x2337000 int3 \\n 0x2337001 int3 \\n 0x2337002 int3 \\n 0x2337003 int3 ', res) diff --git a/tests/api/gef_disassemble.py b/tests/api/gef_disassemble.py new file mode 100644 index 000000000..0d339e46d --- /dev/null +++ b/tests/api/gef_disassemble.py @@ -0,0 +1,51 @@ +""" +`gef.heap` test module. +""" + +import pytest + +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, debug_target + + +class GefDisassembleApiFunction(RemoteGefUnitTestGeneric): + """`gef_disassemble` function test module.""" + + def setUp(self) -> None: + self._target = debug_target("mmap-known-address") + return super().setUp() + + @pytest.mark.skipif(ARCH not in ("x86_64", "i686"), reason=f"Skipped for {ARCH}") + def test_func_gef_disassemble(self): + self._gdb.execute("run") + output = self._conn.root.eval("list(map(str,gef_disassemble(0x2337100, 4, 4)))") + expected = [ + " 0x23370fc int3 ", + " 0x23370fd int3 ", + " 0x23370fe int3 ", + " 0x23370ff int3 ", + " 0x2337100 int3 ", + " 0x2337101 int3 ", + " 0x2337102 int3 ", + " 0x2337103 int3 ", + ] + assert len(output) == len(expected) + for i in range(len(output)): + assert output[i] == expected[i] + + @pytest.mark.skipif(ARCH not in ("x86_64", "i686"), reason=f"Skipped for {ARCH}") + def test_func_gef_disassemble_page_border(self): + # Regression test for issue #922 + self._gdb.execute("run") + output = self._conn.root.eval( + "list(map(str, gef_disassemble(0x2337000, 4, 4)))" + ) + expected = [ + " 0x2337000 int3 ", + " 0x2337001 int3 ", + " 0x2337002 int3 ", + " 0x2337003 int3 ", + ] + assert len(output) == len(expected) + for i in range(len(output)): + assert output[i] == expected[i] diff --git a/tests/api/gef_heap.py b/tests/api/gef_heap.py index 1a338b035..ece33c841 100644 --- a/tests/api/gef_heap.py +++ b/tests/api/gef_heap.py @@ -5,18 +5,18 @@ import pytest import random -from tests.utils import ARCH, debug_target, gdb_test_python_method, is_64b -from tests.utils import GefUnitTestGeneric - - -def result_as_int(res: str) -> int: - return int(gdb_test_python_method(res, target=debug_target("heap")).splitlines()[-1]) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, debug_target, is_64b TCACHE_BINS = 64 -class GefHeapApi(GefUnitTestGeneric): +class GefHeapApi(RemoteGefUnitTestGeneric): """`gef.heap` test module.""" + def setUp(self) -> None: + self._target = debug_target("heap") + return super().setUp() + # from https://elixir.bootlin.com/glibc/latest/source/malloc/malloc.c#L326 # With rounding and alignment, the bins are... # idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit) @@ -44,35 +44,44 @@ def valid_sizes(self): def test_func_gef_heap_tidx2size(self): + gdb, gef = self._gdb, self._gef + gdb.execute("run") for _ in range(5): idx = random.choice(range(TCACHE_BINS)) - size = result_as_int(f"gef.heap.tidx2size({idx})") - self.assertIn(size, self.valid_sizes, f"idx={idx}") + size = gef.heap.tidx2size(idx) + assert size in self.valid_sizes, f"idx={idx}" def test_func_gef_heap_csize2tidx(self): + gdb, gef = self._gdb, self._gef + gdb.execute("run") for _ in range(5): size = random.randint(0, 1032 if ARCH == "i686" or is_64b() else 516) - idx = result_as_int(f"gef.heap.csize2tidx({size})") - self.assertIn(idx, range(TCACHE_BINS), f"size={size}") + idx = gef.heap.csize2tidx(size) + assert idx in range(TCACHE_BINS), f"size={size}" @pytest.mark.skipif(ARCH not in ("x86_64",), reason=f"Skipped for {ARCH}") def test_func_gef_heap_malloc_align_address(self): + gdb, gef = self._gdb, self._gef + gdb.execute("run") values = ( (0x08, 0x10), (0x11, 0x20), (0x23, 0x30), (0x13371337, 0x13371340), ) + for x, y in values: - res = result_as_int(f"gef.heap.malloc_align_address({x})") - self.assertEqual(res, y) + res = gef.heap.malloc_align_address(x) + assert res == y def test_class_glibcarena_main_arena(self): - addr1 = result_as_int("GlibcArena('main_arena').addr") - addr2 = result_as_int("search_for_main_arena()") - addr3 = result_as_int("int(gef.heap.main_arena)") - self.assertEqual(addr1, addr2) - self.assertEqual(addr2, addr3) + gdb, gef = self._gdb, self._gef + gdb.execute("run") + addr1 = self._conn.root.eval("GlibcArena('main_arena').addr") + addr2 = self._conn.root.eval("search_for_main_arena()") + addr3 = gef.heap.main_arena + assert addr1 == addr2 + assert addr2 == addr3 diff --git a/tests/api/gef_session.py b/tests/api/gef_session.py index c5b190380..0a0e2ce7b 100644 --- a/tests/api/gef_session.py +++ b/tests/api/gef_session.py @@ -2,79 +2,83 @@ `gef.session` test module. """ - import os +import pathlib import random -import subprocess +import re + +from tests.base import RemoteGefUnitTestGeneric + from tests.utils import ( - TMPDIR, - gdb_test_python_method, debug_target, - GefUnitTestGeneric, gdbserver_session, - gdb_run_cmd, qemuuser_session, - GDBSERVER_DEFAULT_HOST + GDBSERVER_DEFAULT_HOST, ) -import re -class GefSessionApi(GefUnitTestGeneric): +class GefSessionApi(RemoteGefUnitTestGeneric): """`gef.session` test module.""" - def test_func_get_filepath(self): - res = gdb_test_python_method("gef.session.file", target=debug_target("default")) - self.assertNoException(res) - target = TMPDIR / "foo bar" - subprocess.call(["cp", debug_target("default"), target]) - res = gdb_test_python_method("gef.session.file", target=target) - self.assertNoException(res) - subprocess.call(["rm", target]) + def setUp(self) -> None: + self._target = debug_target("default") + return super().setUp() + def test_func_get_filepath(self): + gdb, gef = self._gdb, self._gef + gdb.execute("start") + assert isinstance(gef.session.file, pathlib.Path) + assert str(gef.session.file.absolute()) == str(self._target.absolute()) def test_func_get_pid(self): - res = gdb_test_python_method("gef.session.pid", target=debug_target("default")) - self.assertNoException(res) - self.assertTrue(int(res.splitlines()[-1])) + gdb, gef = self._gdb, self._gef + gdb.execute("start") + pid_from_gdb = int( + gdb.execute("info proc", to_string=True).splitlines()[0].split()[1] + ) + assert gef.session.pid == pid_from_gdb def test_func_auxiliary_vector(self): - func = "gef.session.auxiliary_vector" - res = gdb_test_python_method(func, target=debug_target("default")) - self.assertNoException(res) - # we need at least ("AT_PLATFORM", "AT_EXECFN") right now - self.assertTrue("'AT_PLATFORM'" in res) - self.assertTrue("'AT_EXECFN':" in res) - self.assertFalse("'AT_WHATEVER':" in res) + gdb, gef = self._gdb, self._gef + gdb.execute("start") + + assert "AT_PLATFORM" in gef.session.auxiliary_vector + assert "AT_EXECFN" in gef.session.auxiliary_vector + assert "AT_WHATEVER" not in gef.session.auxiliary_vector + assert gef.session.auxiliary_vector["AT_PAGESZ"] == 0x1000 def test_root_dir_local(self): - func = "(s.st_dev, s.st_ino)" - res = gdb_test_python_method(func, target=debug_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("/") + gdb, gef = self._gdb, self._gef + gdb.execute("start") + + assert gef.session.root + result = self._conn.root.eval("os.stat(gef.session.root)") + expected = 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) + assert (expected.st_dev == result.st_dev) and (expected.st_ino == result.st_ino) def test_root_dir_remote(self): - func = "(s.st_dev, s.st_ino)" - stat_root = os.stat("/") + gdb = self._gdb + gdb.execute("start") + + expected = os.stat("/") host = GDBSERVER_DEFAULT_HOST port = random.randint(1025, 65535) - before = [f"gef-remote {host} {port}", "pi s=os.stat(gef.session.root)"] with gdbserver_session(port=port): - res = gdb_run_cmd(f"pi {func}", target=debug_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) + gdb.execute(f"gef-remote {host} {port}") + result = self._conn.root.eval("os.stat(gef.session.root)") + assert (expected.st_dev == result.st_dev) and ( + expected.st_ino == result.st_ino + ) def test_root_dir_qemu(self): + gdb, gef = self._gdb, self._gef + host = GDBSERVER_DEFAULT_HOST port = random.randint(1025, 65535) with qemuuser_session(port=port): - target = debug_target("default") - before = [ - f"gef-remote --qemu-user --qemu-binary {target} {host} {port}"] - res = gdb_run_cmd(f"pi gef.session.root", target=debug_target("default"), before=before) - self.assertNoException(res) - assert re.search(r"\/proc\/[0-9]+/root", res) + gdb.execute( + f"gef-remote --qemu-user --qemu-binary {self._target} {host} {port}" + ) + assert re.search(r"\/proc\/[0-9]+/root", str(gef.session.root)) diff --git a/tests/api/misc.py b/tests/api/misc.py index d8de03f90..ebc4abacf 100644 --- a/tests/api/misc.py +++ b/tests/api/misc.py @@ -3,137 +3,140 @@ """ import pathlib -import tempfile -import subprocess -import os import pytest +import random + +from tests.base import RemoteGefUnitTestGeneric from tests.utils import ( debug_target, - gdb_start_silent_cmd, - gdb_test_python_method, - gdb_run_cmd, gdbserver_session, qemuuser_session, - GefUnitTestGeneric, GDBSERVER_DEFAULT_HOST, - GDBSERVER_DEFAULT_PORT, ) -class MiscFunctionTest(GefUnitTestGeneric): +class MiscFunctionTest(RemoteGefUnitTestGeneric): """Tests GEF internal functions.""" + def setUp(self) -> None: + self._target = debug_target("default") + return super().setUp() + def test_func_which(self): - res = gdb_test_python_method("which('gdb')") - lines = res.splitlines() - self.assertIn("/gdb", lines[-1]) - res = gdb_test_python_method("which('__IDontExist__')") - self.assertIn("Missing file `__IDontExist__`", res) + root = self._conn.root + + res = root.eval("which('gdb')") + assert isinstance(res, pathlib.Path) + assert res.name == "gdb" + + with pytest.raises(FileNotFoundError): + root.eval("which('__IDontExist__')") def test_func_gef_convenience(self): - func = "gef_convenience('meh')" - res = gdb_test_python_method(func, target=debug_target("default")) - self.assertNoException(res) + root = self._conn.root + root.eval("gef_convenience('meh')") def test_func_parse_address(self): - func = "parse_address('main+0x4')" - res = gdb_test_python_method(func) - self.assertNoException(res) + root = self._conn.root + assert isinstance(root.eval("parse_address('main+0x4')"), int) - func = "parse_address('meh')" - res = gdb_test_python_method(func) - self.assertException(res) + with pytest.raises(Exception): + root.eval("parse_address('meh')") def test_func_parse_permissions(self): - func = "Permission.from_info_sections('ALLOC LOAD READONLY CODE HAS_CONTENTS')" - res = gdb_test_python_method(func) - self.assertNoException(res) - - func = "Permission.from_process_maps('r--')" - res = gdb_test_python_method(func) - self.assertNoException(res) + root = self._conn.root + expected_values = [ + ( + "Permission.from_info_sections('ALLOC LOAD READONLY CODE HAS_CONTENTS')", + "r-x", + ), + ("Permission.from_process_maps('r--')", "r--"), + ("Permission.from_monitor_info_mem('-r-')", "r--"), + ("Permission.from_info_mem('rw')", "rw-"), + ] + for cmd, expected in expected_values: + assert str(root.eval(cmd)) == expected + + 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())") + + gdb.execute("start") + + sections = root.eval("list(GefMemoryManager.parse_procfs_maps())") + for section in sections: + assert section.page_start & ~0xFFF + assert section.page_end & ~0xFFF - func = "Permission.from_monitor_info_mem('-r-')" - res = gdb_test_python_method(func) - self.assertNoException(res) - - func = "Permission.from_info_mem('rw')" - res = gdb_test_python_method(func) - self.assertNoException(res) + # The parse maps function should automatically get called when we start + # up, and we should be able to view the maps via the `gef.memory.maps` + # property. So check the alias + assert gef.memory.maps == sections - def test_func_parse_maps(self): - func = "list(GefMemoryManager.parse_procfs_maps())" - res = gdb_test_python_method(func) - self.assertNoException(res) - assert "Section" in res + def test_func_parse_maps_local_info_section(self): + root, gdb = self._conn.root, self._gdb + gdb.execute("start") - func = "list(GefMemoryManager.parse_gdb_info_sections())" - res = gdb_test_python_method(func) - self.assertNoException(res) - assert "Section" in res + sections = root.eval("list(GefMemoryManager.parse_gdb_info_sections())") + assert len(sections) > 0 + @pytest.mark.slow + def test_func_parse_maps_remote_gdbserver(self): + root, gdb = self._conn.root, self._gdb # When in a gef-remote session `parse_gdb_info_sections` should work to # query the memory maps - port = GDBSERVER_DEFAULT_PORT + 1 - before = [f"gef-remote {GDBSERVER_DEFAULT_HOST} {port}"] + while True: + port = random.randint(1025, 65535) + if port != self._port: + break + + with pytest.raises(Exception): + gdb.execute(f"gef-remote {GDBSERVER_DEFAULT_HOST} {port}") + with gdbserver_session(port=port) as _: - func = "list(GefMemoryManager.parse_gdb_info_sections())" - res = gdb_test_python_method(func) - self.assertNoException(res) - assert "Section" in res + gdb.execute(f"gef-remote {GDBSERVER_DEFAULT_HOST} {port}") + sections = root.eval("list(GefMemoryManager.parse_gdb_info_sections())") + assert len(sections) > 0 + def test_func_parse_maps_remote_qemu(self): + root, gdb = self._conn.root, self._gdb # When in a gef-remote qemu-user session `parse_gdb_info_sections` # should work to query the memory maps - port = GDBSERVER_DEFAULT_PORT + 2 - target = debug_target("default") - before = [ - f"gef-remote --qemu-user --qemu-binary {target} {GDBSERVER_DEFAULT_HOST} {port}"] - with qemuuser_session(port=port) as _: - func = "list(GefMemoryManager.parse_gdb_info_sections())" - res = gdb_test_python_method(func) - self.assertNoException(res) - assert "Section" in res - - # Running the _parse_maps method should just find the correct one - func = "list(GefMemoryManager._parse_maps())" - res = gdb_test_python_method(func) - self.assertNoException(res) - assert "Section" in res - - # The parse maps function should automatically get called when we start - # up, and we should be able to view the maps via the `gef.memory.maps` - # property. - func = "gef.memory.maps" - res = gdb_test_python_method(func) - self.assertNoException(res) - assert "Section" in res - - - @pytest.mark.slow - @pytest.mark.online - @pytest.mark.skip - def test_func_update_gef(self): - bkp_home = os.environ["HOME"] - for branch in ("main", ): - with tempfile.TemporaryDirectory() as tmpdir: - dirpath = pathlib.Path(tmpdir) - os.environ["HOME"] = str(dirpath.absolute()) - url = f"https://api.github.com/repos/hugsy/gef/git/ref/heads/{branch}" - cmd = f"""wget -q -O- {url} | grep '"sha"' | tr -s ' ' | """ \ - """cut -d ' ' -f 3 | tr -d ',' | tr -d '"' """ - ref = subprocess.check_output(cmd, shell=True).decode("utf-8").strip() - res = gdb_test_python_method(f"update_gef(['--{branch}'])") - retcode = int(res.splitlines()[-1]) - self.assertEqual(retcode, 0) - home = pathlib.Path().home() - self.assertEqual(open(f"{home}/.gdbinit", "r").read(), f"source ~/.gef-{ref}.py\n") - fpath = home / f".gef-{ref}.py" - self.assertTrue(fpath.exists()) - os.environ["HOME"] = bkp_home + while True: + port = random.randint(1025, 65535) + if port != self._port: + break + with qemuuser_session(port=port) as _: + cmd = f"gef-remote --qemu-user --qemu-binary {self._target} {GDBSERVER_DEFAULT_HOST} {port}" + gdb.execute(cmd) + sections = root.eval("list(GefMemoryManager.parse_gdb_info_sections())") + assert len(sections) > 0 def test_func_show_last_exception(self): - cmd = "hexdump byte *0" - res = gdb_start_silent_cmd(cmd, before=("gef config gef.debug 1",)) - self.assertException(res) + gdb = self._gdb + gdb.execute("start") + + # + # Test debug info collection + # + gdb.execute("gef config gef.debug True") + gdb.execute("gef config gef.propagate_debug_exception False") + output: str = gdb.execute("hexdump byte *0", to_string=True) + for title in ( + "Exception raised", + "Version", + "Last 10 GDB commands", + "Runtime environment", + ): + assert title in output + + # + # Test exception propagation + # + gdb.execute("gef config gef.propagate_debug_exception True") + with pytest.raises(Exception): + gdb.execute("hexdump byte *0") diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 000000000..3bf4556a3 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,108 @@ +import os +import pathlib +import random +import subprocess +import tempfile +import time +import unittest + +import rpyc + +from .utils import debug_target + +COVERAGE_DIR = os.getenv("COVERAGE_DIR", "") +GEF_PATH = pathlib.Path(os.getenv("GEF_PATH", "gef.py")).absolute() +RPYC_GEF_PATH = GEF_PATH.parent / "scripts/remote_debug.py" +RPYC_HOST = "localhost" +RPYC_PORT = 18812 +RPYC_SPAWN_TIME = 1.0 +RPYC_MAX_REMOTE_CONNECTION_ATTEMPTS = 5 + + +class RemoteGefUnitTestGeneric(unittest.TestCase): + """ + The base class for GEF test cases. This will create the `rpyc` environment to programmatically interact with + GDB and GEF in the test. + """ + + def setUp(self) -> None: + + attempt = RPYC_MAX_REMOTE_CONNECTION_ATTEMPTS + while True: + try: + # + # Port collisions can happen, allow a few retries + # + self._coverage_file = None + self.__setup() + break + except ConnectionRefusedError: + attempt -= 1 + if attempt == 0: + raise + time.sleep(0.2) + continue + + self._gdb = self._conn.root.gdb + self._gef = self._conn.root.gef + return super().setUp() + + def __setup(self): + if not hasattr(self, "_target"): + setattr(self, "_target", debug_target("default")) + else: + assert isinstance(self._target, pathlib.Path) # type: ignore pylint: disable=E1101 + assert self._target.exists() # type: ignore pylint: disable=E1101 + + # + # Select a random tcp port for rpyc + # + self._port = random.randint(1025, 65535) + self._commands = "" + + if COVERAGE_DIR: + self._coverage_file = pathlib.Path(COVERAGE_DIR) / os.getenv( + "PYTEST_XDIST_WORKER", "gw0" + ) + self._commands += f""" +pi import coverage +pi cov = coverage.Coverage(data_file="{self._coverage_file}", auto_data=True, branch=True) +pi cov.start() +""" + + self._commands += f""" +source {GEF_PATH} +gef config gef.debug True +gef config gef.propagate_debug_exception True +gef config gef.disable_color True +source {RPYC_GEF_PATH} +pi start_rpyc_service({self._port}) +""" + + self._initfile = tempfile.NamedTemporaryFile(mode="w", delete=False) + self._initfile.write(self._commands) + self._initfile.flush() + self._command = [ + "gdb", + "-q", + "-nx", + "-ex", + f"source {self._initfile.name}", + "--", + str(self._target.absolute()), # type: ignore pylint: disable=E1101 + ] + self._process = subprocess.Popen(self._command) + assert self._process.pid > 0 + time.sleep(RPYC_SPAWN_TIME) + self._conn = rpyc.connect( + RPYC_HOST, + self._port, + ) + + def tearDown(self) -> None: + if COVERAGE_DIR: + self._gdb.execute("pi cov.stop()") + self._gdb.execute("pi cov.save()") + self._conn.close() + self._process.terminate() + return super().tearDown() diff --git a/tests/commands/aliases.py b/tests/commands/aliases.py index e4d4e2293..aa3f324f6 100644 --- a/tests/commands/aliases.py +++ b/tests/commands/aliases.py @@ -2,26 +2,46 @@ `aliases` command test module """ +from tests.base import RemoteGefUnitTestGeneric -from tests.utils import gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric - -class AliasesCommand(GefUnitTestGeneric): +class AliasesCommand(RemoteGefUnitTestGeneric): """`aliases` command test module""" - def test_cmd_aliases(self): - # test add functionality - add_res = gdb_start_silent_cmd("aliases add alias_function_test example") - self.assertNoException(add_res) + def test_cmd_aliases_add(self): + gdb = self._gdb + gef = self._gef + + initial_nb = len(gef.session.aliases) + gdb.execute("aliases add alias_function_test example") + assert initial_nb + 1 == len(gef.session.aliases) + + def test_cmd_aliases_list(self): + gdb = self._gdb + gef = self._gef + + gdb.execute("aliases add alias_function_test example") # test list functionality - list_res = gdb_start_silent_cmd("aliases ls", - before=["aliases add alias_function_test example"]) - self.assertNoException(list_res) - self.assertIn("alias_function_test", list_res) + list_res = gdb.execute("aliases ls", to_string=True) + assert "alias_function_test" in list_res + + matches = [x for x in gef.session.aliases if x.alias == "alias_function_test"] + assert len(matches) == 1 + assert matches[0].command == "example" + + def test_cmd_aliases_rm(self): + gdb = self._gdb + gef = self._gef + + gdb.execute("aliases add alias_function_test example") + matches = [x for x in gef.session.aliases if x.alias == "alias_function_test"] + assert len(matches) == 1 + assert matches[0].command == "example" + # test rm functionality - rm_res = gdb_start_silent_cmd("aliases ls", - before=["aliases add alias_function_test example", - "aliases rm alias_function_test"]) - self.assertNoException(rm_res) - self.assertNotIn("alias_function_test", rm_res) + gdb.execute("aliases rm alias_function_test") + rm_res = gdb.execute("aliases ls", to_string=True).splitlines() + assert any(map(lambda line: "alias_function_test" not in line, rm_res)) + + matches = [x for x in gef.session.aliases if x.alias == "alias_function_test"] + assert len(matches) == 0 diff --git a/tests/commands/aslr.py b/tests/commands/aslr.py index b5b298894..e5370cbea 100644 --- a/tests/commands/aslr.py +++ b/tests/commands/aslr.py @@ -3,48 +3,45 @@ """ -from tests.utils import GefUnitTestGeneric, findlines, gdb_start_silent_cmd, removeuntil +from tests.base import RemoteGefUnitTestGeneric -class AslrCommand(GefUnitTestGeneric): +class AslrCommand(RemoteGefUnitTestGeneric): """`aslr` command test module""" - cmd = "aslr" + def __is_alsr_on_gdb(self): + gdb = self._gdb + gdb_output = gdb.execute("show disable-randomization", to_string=True).strip() + return gdb_output.endswith("off.") # i.e. disabled def test_cmd_aslr_show(self): - # show - res = gdb_start_silent_cmd(self.cmd, after=("show disable-randomization",)) - self.assertNoException(res) - self.assertIn("ASLR is currently ", res) - self.assertEqual(res.count("ASLR is currently "), 1) - - # compare - pattern = "ASLR is currently " - cmd_output = removeuntil(pattern, findlines(pattern, res)[0]) - pattern = "virtual address space is " - gdb_output = findlines(pattern, res)[0].split()[-1] - if gdb_output == "on.": - self.assertEqual(cmd_output, "disabled") - else: - self.assertEqual(cmd_output, "enabled") + gdb = self._gdb + + gef_output = gdb.execute(self.cmd, to_string=True).strip() + # basic check + if self.__is_alsr_on_gdb(): + assert gef_output == "ASLR is currently enabled" + else: + assert gef_output == "ASLR is currently disabled" def test_cmd_aslr_toggle(self): + gdb = self._gdb + # current value - res = gdb_start_silent_cmd(self.cmd) - pattern = "ASLR is currently " - default_value = removeuntil(pattern, findlines(pattern, res)[0]) + enabled = self.__is_alsr_on_gdb() # toggle - if default_value == "enabled": - res = gdb_start_silent_cmd(f"{self.cmd} off", after=(f"{self.cmd}")) - cmd_output = removeuntil(pattern, findlines(pattern, res)[0]) - self.assertEqual(cmd_output, "disabled") - elif default_value == "disabled": - res = gdb_start_silent_cmd(f"{self.cmd} on", after=(f"{self.cmd}")) - cmd_output = removeuntil(pattern, findlines(pattern, res)[0]) - self.assertEqual(cmd_output, "enabled") + if enabled: + # switch off and check + gdb.execute(f"{self.cmd} off") + res = gdb.execute(self.cmd, to_string=True).strip() + assert res == "ASLR is currently disabled" + else: - raise Exception(f"incorrect value: {default_value}") + # switch on and check + gdb.execute(f"{self.cmd} on") + res = gdb.execute(self.cmd, to_string=True).strip() + assert res == "ASLR is currently enabled" diff --git a/tests/commands/canary.py b/tests/commands/canary.py index a0ce3be23..840664658 100644 --- a/tests/commands/canary.py +++ b/tests/commands/canary.py @@ -3,29 +3,32 @@ """ -from tests.utils import gdb_start_silent_cmd, gdb_run_cmd, debug_target, gdb_test_python_method -from tests.utils import GefUnitTestGeneric -import pytest -import platform +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, debug_target, p64, p32, is_64b, u32 +from tests.base import RemoteGefUnitTestGeneric -ARCH = platform.machine() - -class CanaryCommand(GefUnitTestGeneric): +class CanaryCommand(RemoteGefUnitTestGeneric): """`canary` command test module""" + def setUp(self) -> None: + self._target = debug_target("canary") + return super().setUp() + def test_cmd_canary(self): - self.assertFailIfInactiveSession(gdb_run_cmd("canary")) - res = gdb_start_silent_cmd("canary", target=debug_target("canary")) - self.assertNoException(res) - self.assertIn("The canary of process", res) - res = gdb_test_python_method("gef.session.canary[0] == gef.session.original_canary[0]") - self.assertNoException(res) - self.assertIn("True", res) - - @pytest.mark.skipif(ARCH != "x86_64", reason=f"Not implemented for {ARCH}") + assert ERROR_INACTIVE_SESSION_MESSAGE == self._gdb.execute("canary", to_string=True) + self._gdb.execute("start") + res = self._gdb.execute("canary", to_string=True) + assert "The canary of process" in res + assert self._gef.session.canary[0] == self._gef.session.original_canary[0] + + def test_overwrite_canary(self): - patch = r"pi gef.memory.write(gef.arch.canary_address(), p64(0xdeadbeef))" - res = gdb_start_silent_cmd(patch, target=debug_target("canary"), after=["canary"]) - self.assertNoException(res) - self.assertIn("0xdeadbeef", res) + gdb, gef = self._gdb, self._gef + + gdb.execute("start") + if is_64b(): + gef.memory.write(gef.arch.canary_address(), p64(0xdeadbeef)) + else: + gef.memory.write(gef.arch.canary_address(), p32(0xdeadbeef)) + res = u32(gef.memory.read(gef.arch.canary_address(), gef.arch.ptrsize)) + assert 0xdeadbeef == res diff --git a/tests/commands/checksec.py b/tests/commands/checksec.py index 4b2a194ba..087f20eb2 100644 --- a/tests/commands/checksec.py +++ b/tests/commands/checksec.py @@ -2,29 +2,46 @@ checksec command test module """ -from tests.utils import ( - gdb_run_cmd, - debug_target, - GefUnitTestGeneric -) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import debug_target -class ChecksecCommand(GefUnitTestGeneric): +class ChecksecCommandNoCanary(RemoteGefUnitTestGeneric): """`checksec` command test module""" + def setUp(self) -> None: + self._target = debug_target("checksec-no-canary") + return super().setUp() + + def test_cmd_checksec(self): + gdb = self._gdb + gef = self._gef + res = gdb.execute("checksec", to_string=True) + assert "Canary : ✘" in res + assert gef.binary.checksec["Canary"] == False + + +class ChecksecCommandNoNx(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("checksec-no-nx") + return super().setUp() + def test_cmd_checksec(self): - cmd = "checksec" - res = gdb_run_cmd(cmd) - self.assertNoException(res) + gdb = self._gdb + gef = self._gef + res = gdb.execute("checksec", to_string=True) + assert "NX : ✘" in res + assert gef.binary.checksec["NX"] == False - target = debug_target("checksec-no-canary") - res = gdb_run_cmd(cmd, target=target) - self.assertIn("Canary : ✘", res) - target = debug_target("checksec-no-nx") - res = gdb_run_cmd(cmd, target=target) - self.assertIn("NX : ✘", res) +class ChecksecCommandNoPie(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("checksec-no-pie") + return super().setUp() - target = debug_target("checksec-no-pie") - res = gdb_run_cmd(cmd, target=target) - self.assertIn("PIE : ✘", res) + def test_cmd_checksec(self): + gdb = self._gdb + gef = self._gef + res = gdb.execute("checksec", to_string=True) + assert "PIE : ✘" in res + assert gef.binary.checksec["PIE"] == False diff --git a/tests/commands/context.py b/tests/commands/context.py index cec52f238..04929089b 100644 --- a/tests/commands/context.py +++ b/tests/commands/context.py @@ -3,10 +3,10 @@ """ -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric -class ContextCommand(GefUnitTestGeneric): +class ContextCommand(RemoteGefUnitTestGeneric): """`context` command test module""" diff --git a/tests/commands/dereference.py b/tests/commands/dereference.py index 79bb19068..57d7b62fb 100644 --- a/tests/commands/dereference.py +++ b/tests/commands/dereference.py @@ -2,38 +2,46 @@ dereference command test module """ +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE -from tests.utils import gdb_run_cmd, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric - -class DereferenceCommand(GefUnitTestGeneric): +class DereferenceCommand(RemoteGefUnitTestGeneric): """`dereference` command test module""" - def test_cmd_dereference(self): - self.assertFailIfInactiveSession(gdb_run_cmd("dereference")) + gdb = self._gdb + + assert ( + gdb.execute("dereference", to_string=True) == ERROR_INACTIVE_SESSION_MESSAGE + ) + + gdb.execute("start") - res = gdb_start_silent_cmd("dereference $sp") - self.assertNoException(res) + res = gdb.execute("dereference $sp", to_string=True) self.assertTrue(len(res.splitlines()) > 2) - res = gdb_start_silent_cmd("dereference 0x0") - self.assertNoException(res) + res = gdb.execute("dereference 0x0", to_string=True) self.assertIn("Unmapped address", res) - def test_cmd_dereference_forwards(self): - self.assertFailIfInactiveSession(gdb_run_cmd("dereference")) + gdb = self._gdb - cmd = "dereference $sp -l 2" - setup = [ + assert ( + gdb.execute("dereference", to_string=True) == ERROR_INACTIVE_SESSION_MESSAGE + ) + + gdb.execute("start") + + for setup in [ "gef config context.grow_stack_down False", "set {char[9]} ($sp+0x8) = { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00 }", - "set {char[9]} ($sp-0x8) = { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00 }" - ] - res = gdb_start_silent_cmd(cmd=setup, after=cmd) - self.assertNoException(res) + "set {char[9]} ($sp-0x8) = { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00 }", + ]: + gdb.execute(setup) + + cmd = "dereference $sp -l 2" + res = gdb.execute(cmd, to_string=True) """ Assuming the default config of grow_stack_down = False, $sp should look like this: @@ -42,21 +50,27 @@ def test_cmd_dereference_forwards(self): Hence, we want to look at the last line of the output """ res = res.splitlines()[-1] - self.assertIn("AAAAAAAA", res) - self.assertNotIn("BBBBBBBB", res) - + assert "AAAAAAAA" in res + assert "BBBBBBBB" not in res def test_cmd_dereference_backwards(self): - self.assertFailIfInactiveSession(gdb_run_cmd("dereference")) + gdb = self._gdb - cmd = "dereference $sp -l -2" - setup = [ + assert ( + gdb.execute("dereference", to_string=True) == ERROR_INACTIVE_SESSION_MESSAGE + ) + + gdb.execute("start") + + for setup in [ "gef config context.grow_stack_down False", "set {char[9]} ($sp+0x8) = { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00 }", - "set {char[9]} ($sp-0x8) = { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00 }" - ] - res = gdb_start_silent_cmd(cmd=setup, after=cmd) - self.assertNoException(res) + "set {char[9]} ($sp-0x8) = { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00 }", + ]: + gdb.execute(setup) + + cmd = "dereference $sp -l -2" + res = gdb.execute(cmd, to_string=True) """ Assuming the default config of grow_stack_down = False, $sp should look like this: @@ -65,5 +79,5 @@ def test_cmd_dereference_backwards(self): Hence, we want to look at the second last line of the output """ res = res.splitlines()[-2] - self.assertNotIn("AAAAAAAA", res) - self.assertIn("BBBBBBBB", res) + assert "AAAAAAAA" not in res + assert "BBBBBBBB" in res diff --git a/tests/commands/edit_flags.py b/tests/commands/edit_flags.py index f2e0d1621..e082587f2 100644 --- a/tests/commands/edit_flags.py +++ b/tests/commands/edit_flags.py @@ -5,48 +5,50 @@ import pytest -from tests.utils import ( - ARCH, - GefUnitTestGeneric, - gdb_start_silent_cmd_last_line, - gdb_start_silent_cmd, -) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, ERROR_INACTIVE_SESSION_MESSAGE -@pytest.mark.skipif(ARCH not in ["i686", "x86_64", "armv7l", "aarch64"], - reason=f"Skipped for {ARCH}") -class EditFlagsCommand(GefUnitTestGeneric): +@pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") +class EditFlagsCommand(RemoteGefUnitTestGeneric): """`edit-flags` command test module""" - def setUp(self) -> None: - res = gdb_start_silent_cmd_last_line("edit-flags") - self.assertNoException(res) - flags = res[1:-1].split() - self.flag_name = "carry" - self.initial_value = [f for f in flags if f.lower() == self.flag_name][0] - return super().setUp() + def test_cmd_edit_flags_disable(self): + gdb = self._gdb + gef = self._gef + with pytest.raises(Exception, match="No debugging session active"): + gdb.execute("edit-flags") - def test_cmd_edit_flags_disable(self): - res = gdb_start_silent_cmd_last_line("edit-flags", - after=(f"edit-flags +{self.flag_name}", - f"edit-flags -{self.flag_name}")) - self.assertNoException(res) - self.assertIn(self.flag_name.lower(), res) + gdb.execute("start") + res: str = gdb.execute("edit-flags", to_string=True).strip() + assert res.startswith("[") and res.endswith("]") + # pick first flag + idx = list(gef.arch.flags_table.keys())[0] + name = gef.arch.flags_table[idx] + gdb.execute(f"edit-flags -{name}") + assert gef.arch.register(gef.arch.flag_register) & (1 << idx) == 0 def test_cmd_edit_flags_enable(self): - res = gdb_start_silent_cmd("edit-flags", - after=(f"edit-flags -{self.flag_name}", - f"edit-flags +{self.flag_name}")) - self.assertNoException(res) - self.assertIn(self.flag_name.upper(), res) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + idx = list(gef.arch.flags_table.keys())[0] + name = gef.arch.flags_table[idx] + gdb.execute(f"edit-flags +{name}") + assert gef.arch.register(gef.arch.flag_register) & (1 << idx) != 0 def test_cmd_edit_flags_toggle(self): - res = gdb_start_silent_cmd_last_line(f"edit-flags ~{self.flag_name}") - self.assertNoException(res) - if self.initial_value == self.flag_name.upper(): - self.assertIn(self.flag_name.lower(), res) - else: - self.assertIn(self.flag_name.upper(), res) + gdb = self._gdb + gef = self._gef + + gdb.execute("start") + idx = list(gef.arch.flags_table.keys())[0] + name = gef.arch.flags_table[idx] + init_val = gef.arch.register(gef.arch.flag_register) & (1 << idx) + + gdb.execute(f"edit-flags ~{name}") + new_val = gef.arch.register(gef.arch.flag_register) & (1 << idx) + assert init_val != new_val diff --git a/tests/commands/elf_info.py b/tests/commands/elf_info.py index 1a6708dd3..91901b9b5 100644 --- a/tests/commands/elf_info.py +++ b/tests/commands/elf_info.py @@ -3,14 +3,14 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd +from tests.base import RemoteGefUnitTestGeneric -class ElfInfoCommand(GefUnitTestGeneric): +class ElfInfoCommand(RemoteGefUnitTestGeneric): """`elf-info` command test module""" def test_cmd_elf_info(self): - res = gdb_run_cmd("elf-info") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("elf-info", to_string=True) self.assertIn("7f 45 4c 46", res) diff --git a/tests/commands/entry_break.py b/tests/commands/entry_break.py index 34ad43dc3..96a94965c 100644 --- a/tests/commands/entry_break.py +++ b/tests/commands/entry_break.py @@ -3,17 +3,19 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd +from tests.base import RemoteGefUnitTestGeneric -class EntryBreakCommand(GefUnitTestGeneric): +class EntryBreakCommand(RemoteGefUnitTestGeneric): """`entry-break` command test module""" - def test_cmd_entry_break(self): - res = gdb_run_cmd("entry-break") - self.assertNoException(res) + gdb = self._gdb + + # run once (ok) + res = gdb.execute("entry-break", to_string=True).strip() + assert res.startswith("[+] Breaking at") - res = gdb_run_cmd("entry-break", after=("entry-break",)) - self.assertNoException(res) - self.assertIn("gdb is already running", res) + # re-run while session running (nok) + res = gdb.execute("entry-break", to_string=True).strip() + assert "gdb is already running" in res diff --git a/tests/commands/format_string_helper.py b/tests/commands/format_string_helper.py index 7fb6b0821..6d11fcdcd 100644 --- a/tests/commands/format_string_helper.py +++ b/tests/commands/format_string_helper.py @@ -3,20 +3,25 @@ """ -from tests.utils import debug_target, gdb_run_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import debug_target -class FormatStringHelperCommand(GefUnitTestGeneric): +class FormatStringHelperCommand(RemoteGefUnitTestGeneric): """`format-string-helper` command test module""" + def setUp(self) -> None: + self._target = debug_target("format-string-helper") + return super().setUp() def test_cmd_format_string_helper(self): - cmd = "format-string-helper" - target = debug_target("format-string-helper") - res = gdb_run_cmd(cmd, - after=["set args testtest", - "run",], - target=target) - self.assertNoException(res) - self.assertIn("Possible insecure format string:", res) + gdb = self._gdb + + gdb.execute("set args testtest") + res = gdb.execute("format-string-helper", to_string=True) + assert res.endswith( + "[+] Enabled 5 FormatString breakpoints\n" + ) + + res = gdb.execute("run", to_string=True) + assert "Possible insecure format string:" in res diff --git a/tests/commands/functions.py b/tests/commands/functions.py index dbdd7fe7e..82f5a504a 100644 --- a/tests/commands/functions.py +++ b/tests/commands/functions.py @@ -3,16 +3,13 @@ """ -from tests.utils import gdb_run_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric -class FunctionsCommand(GefUnitTestGeneric): +class FunctionsCommand(RemoteGefUnitTestGeneric): """`functions` command test module""" - def test_cmd_functions(self): - cmd = "functions" - res = gdb_run_cmd(cmd) - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("functions", to_string=True) self.assertIn("$_heap", res) diff --git a/tests/commands/gef.py b/tests/commands/gef.py index e35ba20cd..9f8de2aec 100644 --- a/tests/commands/gef.py +++ b/tests/commands/gef.py @@ -5,27 +5,21 @@ import pytest import pathlib -from tests.utils import ( - gdb_run_cmd, - GefUnitTestGeneric, - gdb_start_silent_cmd_last_line, - removeuntil, -) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import removeuntil -class GefCommand(GefUnitTestGeneric): +class GefCommand(RemoteGefUnitTestGeneric): """`gef` command test module""" - def test_cmd_gef(self): - res = gdb_run_cmd("gef") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("gef", to_string=True) self.assertIn("GEF - GDB Enhanced Features", res) - def test_cmd_gef_config(self): - res = gdb_run_cmd("gef config") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("gef config", to_string=True) self.assertIn("GEF configuration settings", res) known_patterns = ( @@ -43,27 +37,28 @@ def test_cmd_gef_config(self): for pattern in known_patterns: self.assertIn(pattern, res) - def test_cmd_gef_config_get(self): - res = gdb_run_cmd("gef config gef.debug") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("gef config gef.debug", to_string=True) self.assertIn("GEF configuration setting: gef.debug", res) - # the `True` is automatically set by `gdb_run_cmd` so we know it's there - self.assertIn("""gef.debug (bool) = True\n\nDescription:\n\tEnable debug mode for gef""", - res) - + self.assertIn( + """gef.debug (bool) = True\n\nDescription:\n\tEnable debug mode for gef""", + res, + ) def test_cmd_gef_config_set(self): - res = gdb_start_silent_cmd_last_line("gef config gef.debug 0", - after=("pi print(is_debug())", )) - self.assertNoException(res) - self.assertEqual("False", res) + gdb = self._gdb + root = self._conn.root + gdb.execute("gef config gef.debug 1") + assert root.eval("is_debug()") - def test_cmd_gef_help(self): - res = gdb_run_cmd("help gef") - self.assertNoException(res) + gdb.execute("gef config gef.debug 0") + assert not root.eval("is_debug()") + def test_cmd_gef_help(self): + gdb = self._gdb + res = gdb.execute("help gef", to_string=True) known_patterns = ( "gef config", "gef help", @@ -77,46 +72,53 @@ def test_cmd_gef_help(self): for pattern in known_patterns: self.assertIn(pattern, res) - def test_cmd_gef_run_and_run(self): - res = gdb_run_cmd("gef set args $_gef0", - before=("pattern create -n 4", ), - after=("show args")) - self.assertNoException(res) - self.assertIn("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaan", res) - - res = gdb_run_cmd("gef set args $_gef42", - before=("pattern create -n 4", ), - after=("show args")) - self.assertException(res) + gdb = self._gdb + + # valid + pattern = gdb.execute("pattern create -n 4", to_string=True).splitlines()[1] + assert len(pattern) == 1024 + res = gdb.execute("gef set args $_gef0") + res = gdb.execute("show args", to_string=True).strip() + assert ( + res + == f'Argument list to give program being debugged when it is started is "{pattern}".' + ) + # invalid + with pytest.raises(Exception): + gdb.execute("gef set args $_gef42") def test_cmd_gef_save(self): + root = self._conn.root + gdb = self._gdb + # check - res = gdb_run_cmd("gef save") - self.assertNoException(res) + res = gdb.execute("gef save", to_string=True).strip() self.assertIn("Configuration saved to '", res) gefrc_file = removeuntil("Configuration saved to '", res.rstrip("'")) + assert gefrc_file == root.eval("str(GEF_RC)") # set & check for name in ("AAAABBBBCCCCDDDD", "gef"): - res = gdb_run_cmd("gef save", before=(f"gef config gef.tempdir /tmp/{name}", )) - self.assertNoException(res) + gdb.execute(f"gef config gef.tempdir /tmp/{name}") + res = gdb.execute("gef save") with pathlib.Path(gefrc_file).open() as f: config = f.read() - self.assertIn(f'tempdir = /tmp/{name}\n', config) - + self.assertIn(f"tempdir = /tmp/{name}\n", config) @pytest.mark.online def test_cmd_gef_install(self): + gdb = self._gdb test_commands = ("skel", "windbg", "stack") - res = gdb_run_cmd(f"gef install {' '.join(test_commands)}") - self.assertNoException(res) + res = gdb.execute(f"gef install {' '.join(test_commands)}", to_string=True) # we install 3 plugins, the pattern must be found 3 times pattern = "Installed file" for i in range(len(test_commands)): idx = res.find(pattern) - self.assertNotEqual(-1, idx, f"Check {i}/{3} failed: missing '{pattern}' in\n{res}") + self.assertNotEqual( + -1, idx, f"Check {i}/{3} failed: missing '{pattern}' in\n{res}" + ) self.assertIn("new command(s) available", res) res = res[idx:] diff --git a/tests/commands/gef_remote.py b/tests/commands/gef_remote.py index 5627b3e26..d2ca7d026 100644 --- a/tests/commands/gef_remote.py +++ b/tests/commands/gef_remote.py @@ -2,54 +2,66 @@ `gef_remote` command test module """ +import random +import pytest + +from tests.base import RemoteGefUnitTestGeneric from tests.utils import ( - GefUnitTestGeneric, debug_target, - gdb_run_cmd, gdbserver_session, qemuuser_session, GDBSERVER_DEFAULT_HOST, - GDBSERVER_DEFAULT_PORT, ) -class GefRemoteCommand(GefUnitTestGeneric): +class GefRemoteCommand(RemoteGefUnitTestGeneric): """`gef_remote` command test module""" + def setUp(self) -> None: + self._target = debug_target("default") + return super().setUp() def test_cmd_gef_remote(self): - port = GDBSERVER_DEFAULT_PORT + 1 - before = [f"gef-remote {GDBSERVER_DEFAULT_HOST} {port}"] - with gdbserver_session(port=port) as _: - res = gdb_run_cmd( - "pi print(gef.session.remote)", before=before) - self.assertNoException(res) + gdb = self._gdb + root = self._conn.root + while True: + port = random.randint(1025, 65535) + 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) + @pytest.mark.slow def test_cmd_gef_remote_qemu_user(self): - port = GDBSERVER_DEFAULT_PORT + 2 - target = debug_target("default") - before = [ - f"gef-remote --qemu-user --qemu-binary {target} {GDBSERVER_DEFAULT_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_DEFAULT_HOST}:{port}', local='/tmp/", res) - self.assertIn(", qemu_user=True)", res) + gdb = self._gdb + root = self._conn.root + while True: + port = random.randint(1025, 65535) + if port != self._port: break + + + with qemuuser_session(port=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 def test_cmd_target_remote(self): - port = GDBSERVER_DEFAULT_PORT + 3 - before = [f"target remote {GDBSERVER_DEFAULT_HOST}:{port}"] + gdb = self._gdb + root = self._conn.root + while True: + port = random.randint(1025, 65535) + if port != self._port: break + 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=':0', local='/tmp/", res) + 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) diff --git a/tests/commands/got.py b/tests/commands/got.py index e4eafdd66..29e49f60d 100644 --- a/tests/commands/got.py +++ b/tests/commands/got.py @@ -4,28 +4,34 @@ import pytest +from tests.base import RemoteGefUnitTestGeneric + from tests.utils import ( ARCH, + ERROR_INACTIVE_SESSION_MESSAGE, debug_target, - gdb_run_cmd, - gdb_start_silent_cmd, - GefUnitTestGeneric, ) @pytest.mark.skipif(ARCH in ("ppc64le",), reason=f"Skipped for {ARCH}") -class GotCommand(GefUnitTestGeneric): +class GotCommand(RemoteGefUnitTestGeneric): """`got` command test module""" + def setUp(self) -> None: + self._target = debug_target("format-string-helper") + return super().setUp() + def test_cmd_got(self): - cmd = "got" - target = debug_target("format-string-helper") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_start_silent_cmd(cmd, target=target) + gdb = self._gdb + + self.assertEqual(ERROR_INACTIVE_SESSION_MESSAGE,gdb.execute("got", to_string=True)) + + gdb.execute("start") + res = gdb.execute("got", to_string=True) self.assertIn("printf", res) self.assertIn("strcpy", res) - res = gdb_start_silent_cmd("got printf", target=target) + res = gdb.execute("got printf", to_string=True) self.assertIn("printf", res) self.assertNotIn("strcpy", res) diff --git a/tests/commands/heap.py b/tests/commands/heap.py index bd5314945..b3bce6f23 100644 --- a/tests/commands/heap.py +++ b/tests/commands/heap.py @@ -2,222 +2,302 @@ Heap commands test module """ -from tests.utils import (ARCH, GefUnitTestGeneric, debug_target, findlines, - gdb_run_cmd, gdb_run_silent_cmd, gdb_start_silent_cmd, - is_32b, is_64b) - - -class HeapCommand(GefUnitTestGeneric): +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ( + ARCH, + ERROR_INACTIVE_SESSION_MESSAGE, + debug_target, + findlines, + is_32b, + is_64b, +) + + +class HeapCommand(RemoteGefUnitTestGeneric): """Generic class for command testing, that defines all helpers""" + def setUp(self) -> None: - # ensure those values reflects the allocations in the C source - self.expected_tcache_bin_size = 0x20 if ARCH == "i686" or is_64b() else 0x18 - self.expected_small_bin_size = 0x20 if ARCH == "i686" or is_64b() else 0x18 - self.expected_large_bin_size = 0x420 if ARCH == "i686" or is_64b() else 0x418 - self.expected_unsorted_bin_size = 0x430 if ARCH == "i686" or is_64b() else 0x428 + self._target = debug_target("heap") return super().setUp() - def test_cmd_heap_arenas(self): + gdb = self._gdb cmd = "heap arenas" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_start_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("Arena(base=", res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) + self.assertIn("Arena(base=", res) def test_cmd_heap_set_arena(self): + gdb = self._gdb cmd = "heap set-arena &main_arena" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target, after=["heap arenas"]) - self.assertNoException(res) - self.assertIn("Arena(base=", res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("run") + gdb.execute(cmd) + res = gdb.execute("heap arenas", to_string=True) + self.assertIn("Arena(base=", res) def test_cmd_heap_chunk_no_arg(self): + gdb = self._gdb cmd = "heap chunk p1" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA", res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn("PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA", res) def test_cmd_heap_chunk_with_number(self): - target = debug_target("heap") + gdb = self._gdb cmd = "heap chunk --number 2 p1" - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + + gdb.execute("run") + res: str = gdb.execute(cmd, to_string=True) chunklines = findlines("Chunk(addr=", res) self.assertEqual(len(chunklines), 2) - def test_cmd_heap_chunks(self): + gdb = self._gdb cmd = "heap chunks" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("Chunk(addr=", res) - self.assertIn("top chunk", res) - - cmd = "heap chunks" - target = debug_target("heap-non-main") - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("Chunk(addr=", res) - self.assertIn("top chunk", res) - chunks = [line for line in res.splitlines() if "Chunk(addr=" in line] - - cmd = "python gdb.execute(f'heap chunks {int(list(gef.heap.arenas)[1]):#x}')" - target = debug_target("heap-non-main") - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertNotIn("using '&main_arena' instead", res) - self.assertIn("Chunk(addr=", res) - self.assertIn("top chunk", res) - non_main_chunks = [line for line in res.splitlines() if "Chunk(addr=" in line] - # make sure that the chunks of each arena are distinct - self.assertNotEqual(chunks, non_main_chunks) - + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) - def test_cmd_heap_chunks_mult_heaps(self): - py_cmd = 'gdb.execute(f"heap set-arena {int(list(gef.heap.arenas)[1]):#x}")' - before = ['run', 'python ' + py_cmd] - cmd = "heap chunks" - target = debug_target("heap-multiple-heaps") - res = gdb_run_silent_cmd(cmd, before=before, target=target) - self.assertNoException(res) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn("Chunk(addr=", res) self.assertIn("top chunk", res) def test_cmd_heap_chunks_summary(self): + gdb = self._gdb cmd = "heap chunks --summary" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("== Chunk distribution by size", res) - self.assertIn("== Chunk distribution by flag", res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) - def test_cmd_heap_chunks_summary_with_type_resolved(self): - cmd = "heap chunks --summary --resolve" - target = debug_target("class") - res = gdb_run_silent_cmd(cmd, target=target, before=["b B::Run()"]) - self.assertNoException(res) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn("== Chunk distribution by size", res) - self.assertIn("B", res) + self.assertIn("== Chunk distribution by flag", res) def test_cmd_heap_chunks_min_size_filter(self): + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("heap chunks", to_string=True) + ) + + gdb.execute("run") + cmd = "heap chunks --min-size 16" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True) self.assertIn("Chunk(addr=", res) cmd = "heap chunks --min-size 1048576" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True) self.assertNotIn("Chunk(addr=", res) def test_cmd_heap_chunks_max_size_filter(self): + gdb = self._gdb cmd = "heap chunks --max-size 160" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + + gdb.execute("run") + + res = gdb.execute(cmd, to_string=True) self.assertIn("Chunk(addr=", res) cmd = "heap chunks --max-size 16" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertNotIn("Chunk(addr=", res) def test_cmd_heap_chunks_with_count(self): + gdb = self._gdb cmd = "heap chunks --count 1" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn("Chunk(addr=", res) cmd = "heap chunks --count 0" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertNotIn("Chunk(addr=", res) + +class HeapCommandNonMain(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("heap-non-main") + return super().setUp() + + def test_cmd_heap_chunks(self): + gdb = self._gdb + cmd = "heap chunks" + + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn("Chunk(addr=", res) + self.assertIn("top chunk", res) + chunks = [line for line in res.splitlines() if "Chunk(addr=" in line] + + cmd = "python gdb.execute(f'heap chunks {int(list(gef.heap.arenas)[1]):#x}')" + + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertNotIn("using '&main_arena' instead", res) + self.assertIn("Chunk(addr=", res) + self.assertIn("top chunk", res) + non_main_chunks = [line for line in res.splitlines() if "Chunk(addr=" in line] + # make sure that the chunks of each arena are distinct + self.assertNotEqual(chunks, non_main_chunks) + + def test_cmd_heap_bins_non_main(self): + gdb = self._gdb + gef = self._gef + gdb.execute("set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0") + gdb.execute("run") + + next_arena: int = gef.heap.main_arena.next + cmd = f"python gdb.execute(f'heap bins fast {next_arena:#x}')" + res = gdb.execute(cmd, to_string=True) + self.assertIn("size=0x20", res) + + def test_cmd_heap_bins_tcache(self): + gdb = self._gdb + gdb.execute("run") + + cmd = "heap bins tcache" + res = gdb.execute(cmd, to_string=True) + tcachelines = findlines("Tcachebins[idx=", res) + self.assertEqual(len(tcachelines), 1) + if ARCH in ("i686",): + self.assertIn("Tcachebins[idx=1, size=0x20, count=1]", tcachelines[0]) + elif is_32b(): + self.assertIn("Tcachebins[idx=2, size=0x20, count=1]", tcachelines[0]) + else: + self.assertIn("Tcachebins[idx=0, size=0x20, count=1]", tcachelines[0]) + + +class HeapCommandMultipleHeaps(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("heap-multiple-heaps") + return super().setUp() + + def test_cmd_heap_chunks_mult_heaps(self): + gdb = self._gdb + + gdb.execute("run") + py_cmd = 'gdb.execute(f"heap set-arena {int(list(gef.heap.arenas)[1]):#x}")' + + gdb.execute(f"python {py_cmd}") + cmd = "heap chunks" + res = gdb.execute(cmd, to_string=True) + self.assertIn("Chunk(addr=", res) + self.assertIn("top chunk", res) + + +class HeapCommandClass(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("class") + return super().setUp() + + def test_cmd_heap_chunks_summary_with_type_resolved(self): + gdb = self._gdb + cmd = "heap chunks --summary --resolve" + gdb.execute("b B::Run()") + gdb.execute("run") + lines = gdb.execute(cmd, to_string=True).splitlines() + assert len(lines) > 0 + self.assertEqual("== Chunk distribution by size ==", lines[0]) + self.assertIn("== Chunk distribution by flag ==", lines) + assert any( map(lambda x: "B" in x, lines)) + + +class HeapCommandFastBins(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("heap-fastbins") + return super().setUp() + def test_cmd_heap_bins_fast(self): + gdb = self._gdb cmd = "heap bins fast" - before = ["set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0"] - target = debug_target("heap-fastbins") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, before=before, target=target)) - res = gdb_run_silent_cmd(cmd, before=before, target=target) - self.assertNoException(res) + gdb.execute("set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0") + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) # ensure fastbins is populated self.assertIn("Fastbins[idx=0, size=", res) self.assertIn("Chunk(addr=", res) +class HeapCommandBins(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("heap-bins") + self.expected_large_bin_size = 0x420 if ARCH == "i686" or is_64b() else 0x418 + self.expected_small_bin_size = 0x20 if ARCH == "i686" or is_64b() else 0x18 + self.expected_unsorted_bin_size = 0x430 if ARCH == "i686" or is_64b() else 0x428 + return super().setUp() + def test_cmd_heap_bins_large(self): + gdb = self._gdb + gdb.execute("run") cmd = "heap bins large" - target = debug_target("heap-bins") - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True) self.assertIn("Found 1 chunks in 1 large non-empty bins", res) self.assertIn("Chunk(addr=", res) self.assertIn(f"size={self.expected_large_bin_size:#x}", res) - - def test_cmd_heap_bins_non_main(self): - cmd = "python gdb.execute(f'heap bins fast {gef.heap.main_arena.next:#x}')" - before = ["set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0"] - target = debug_target("heap-non-main") - res = gdb_run_silent_cmd(cmd, before=before, target=target) - self.assertNoException(res) - self.assertIn("size=0x20", res) - - def test_cmd_heap_bins_small(self): + gdb = self._gdb cmd = "heap bins small" - before = ["set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0"] - target = debug_target("heap-bins") - res = gdb_run_silent_cmd(cmd, before=before, target=target) - self.assertNoException(res) + gdb.execute("set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn("Found 1 chunks in 1 small non-empty bins", res) self.assertIn("Chunk(addr=", res) self.assertIn(f"size={self.expected_small_bin_size:#x}", res) + def test_cmd_heap_bins_unsorted(self): + gdb = self._gdb + gdb.execute("run") + cmd = "heap bins unsorted" + res = gdb.execute(cmd, to_string=True) + self.assertIn("Found 1 chunks in unsorted bin", res) + self.assertIn("Chunk(addr=", res) + self.assertIn(f"size={self.expected_unsorted_bin_size:#x}", res) - def test_cmd_heap_bins_tcache(self): - cmd = "heap bins tcache" - target = debug_target("heap-non-main") - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - tcachelines = findlines("Tcachebins[idx=", res) - self.assertEqual(len(tcachelines), 1) - if ARCH in ("i686",): - self.assertIn("Tcachebins[idx=1, size=0x20, count=1]", tcachelines[0]) - elif is_32b(): - self.assertIn("Tcachebins[idx=2, size=0x20, count=1]", tcachelines[0]) - else: - self.assertIn("Tcachebins[idx=0, size=0x20, count=1]", tcachelines[0]) +class HeapCommandTcache(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("heap-tcache") + self.expected_tcache_bin_size = 0x20 if ARCH == "i686" or is_64b() else 0x18 + return super().setUp() def test_cmd_heap_bins_tcache_all(self): + gdb = self._gdb + gdb.execute("run") + cmd = "heap bins tcache all" - target = debug_target("heap-tcache") - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True) # ensure there's 2 tcachebins tcachelines = findlines("Tcachebins[idx=", res) self.assertEqual(len(tcachelines), 2) @@ -230,12 +310,3 @@ def test_cmd_heap_bins_tcache_all(self): else: self.assertIn("Tcachebins[idx=0, size=0x20, count=3]", tcachelines[0]) self.assertIn("Tcachebins[idx=1, size=0x30, count=3]", tcachelines[1]) - - def test_cmd_heap_bins_unsorted(self): - cmd = "heap bins unsorted" - target = debug_target("heap-bins") - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("Found 1 chunks in unsorted bin", res) - self.assertIn("Chunk(addr=", res) - self.assertIn(f"size={self.expected_unsorted_bin_size:#x}", res) diff --git a/tests/commands/heap_analysis.py b/tests/commands/heap_analysis.py index d0c55cde3..1b7b6250b 100644 --- a/tests/commands/heap_analysis.py +++ b/tests/commands/heap_analysis.py @@ -3,22 +3,32 @@ """ -from tests.utils import debug_target, gdb_run_cmd, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, debug_target -class HeapAnalysisCommand(GefUnitTestGeneric): +class HeapAnalysisCommand(RemoteGefUnitTestGeneric): """`heap-analysis` command test module""" + def setUp(self) -> None: + self._target = debug_target("heap-analysis") + return super().setUp() def test_cmd_heap_analysis(self): + gdb = self._gdb + cmd = "heap-analysis-helper" - target = debug_target("heap-analysis") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd)) - res = gdb_start_silent_cmd(cmd, after=["continue"], target=target) - self.assertNoException(res) + + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) self.assertIn("Tracking", res) self.assertIn("correctly setup", res) + + res = gdb.execute("continue", to_string=True) self.assertIn("malloc(16)=", res) self.assertIn("calloc(32)=", res) addr = int(res.split("calloc(32)=")[1].split("\n")[0], 0) diff --git a/tests/commands/hexdump.py b/tests/commands/hexdump.py index fb51cd3f6..17308e866 100644 --- a/tests/commands/hexdump.py +++ b/tests/commands/hexdump.py @@ -3,21 +3,20 @@ """ -from tests.utils import gdb_run_cmd, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE -class HexdumpCommand(GefUnitTestGeneric): +class HexdumpCommand(RemoteGefUnitTestGeneric): """`hexdump` command test module""" def test_cmd_hexdump(self): - self.assertFailIfInactiveSession(gdb_run_cmd("hexdump $pc")) - res = gdb_start_silent_cmd("hexdump qword $pc") - self.assertNoException(res) - res = gdb_start_silent_cmd("hexdump dword $pc -s 1") - self.assertNoException(res) - res = gdb_start_silent_cmd("hexdump word $pc -s 5 -r") - self.assertNoException(res) - res = gdb_start_silent_cmd("hexdump byte $sp -s 32") - self.assertNoException(res) + gdb = self._gdb + self.assertEqual(ERROR_INACTIVE_SESSION_MESSAGE,gdb.execute("hexdump $pc", to_string=True)) + + gdb.execute("start") + res = gdb.execute("hexdump qword $pc", to_string=True) + res = gdb.execute("hexdump dword $pc -s 1", to_string=True) + res = gdb.execute("hexdump word $pc -s 5 -r", to_string=True) + res = gdb.execute("hexdump byte $sp -s 32", to_string=True) diff --git a/tests/commands/highlight.py b/tests/commands/highlight.py index e11430883..615d02750 100644 --- a/tests/commands/highlight.py +++ b/tests/commands/highlight.py @@ -3,26 +3,31 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_start_silent_cmd, Color +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import Color -class HighlightCommand(GefUnitTestGeneric): +class HighlightCommand(RemoteGefUnitTestGeneric): """`highlight` command test module""" - def test_cmd_highlight(self): - cmds = [ + gdb = self._gdb + + gdb.execute("gef config context.layout stack") + gdb.execute("gef config gef.disable_color 0") + gdb.execute("start") + + for cmd in [ "highlight add 41414141 yellow", "highlight add 42424242 blue", "highlight add 43434343 green", "highlight add 44444444 pink", 'patch string $sp "AAAABBBBCCCCDDDD"', - "hexdump qword $sp -s 2" - ] - - res = gdb_start_silent_cmd('', after=cmds, strip_ansi=False) + "hexdump qword $sp -s 2", + ]: + gdb.execute(cmd) - self.assertNoException(res) + res = gdb.execute("context", to_string=True) self.assertIn(f"{Color.YELLOW.value}41414141{Color.NORMAL.value}", res) self.assertIn(f"{Color.BLUE.value}42424242{Color.NORMAL.value}", res) self.assertIn(f"{Color.GREEN.value}43434343{Color.NORMAL.value}", res) diff --git a/tests/commands/hijack_fd.py b/tests/commands/hijack_fd.py index 50955e6fb..704c09426 100644 --- a/tests/commands/hijack_fd.py +++ b/tests/commands/hijack_fd.py @@ -3,10 +3,10 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd +from tests.base import RemoteGefUnitTestGeneric -class HijackFdCommand(GefUnitTestGeneric): +class HijackFdCommand(RemoteGefUnitTestGeneric): """`hijack-fd` command test module""" @@ -14,5 +14,5 @@ class HijackFdCommand(GefUnitTestGeneric): def test_cmd_hijack_fd(self): - res = gdb_run_cmd(f"{self.cmd}") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute(f"{self.cmd}", to_string=True) diff --git a/tests/commands/ksymaddr.py b/tests/commands/ksymaddr.py deleted file mode 100644 index 8e0a44c8d..000000000 --- a/tests/commands/ksymaddr.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -`ksymaddr` command test module -""" - - -from tests.utils import GefUnitTestGeneric, gdb_run_cmd - - -class KsymaddrCommand(GefUnitTestGeneric): - """`ksymaddr` command test module""" - - - cmd = "ksymaddr" - - - def test_cmd_ksymaddr(self): - res = gdb_run_cmd(f"{self.cmd} prepare_kernel_cred") - self.assertNoException(res) - self.assertIn("Found matching symbol for 'prepare_kernel_cred'", res) diff --git a/tests/commands/memory.py b/tests/commands/memory.py index 0550dcc07..21550157f 100644 --- a/tests/commands/memory.py +++ b/tests/commands/memory.py @@ -2,62 +2,90 @@ Memory commands test module """ +from tests.base import RemoteGefUnitTestGeneric from tests.utils import ( - GefUnitTestGeneric, - gdb_run_cmd, - gdb_start_silent_cmd, + ERROR_INACTIVE_SESSION_MESSAGE, debug_target, ) -class MemoryCommand(GefUnitTestGeneric): - """ `memory` command testing module""" - - - def test_cmd_memory_watch(self): - self.assertFailIfInactiveSession(gdb_run_cmd("memory watch $pc")) - res = gdb_start_silent_cmd("memory watch $pc 0x100 byte") - self.assertNoException(res) - res = gdb_start_silent_cmd("memory watch $pc 0x40 word") - self.assertNoException(res) - res = gdb_start_silent_cmd("memory watch $pc 0x30 dword") - self.assertNoException(res) - res = gdb_start_silent_cmd("memory watch $pc 0x20 qword") - self.assertNoException(res) - res = gdb_start_silent_cmd("memory watch $pc 0x8 pointers") - self.assertNoException(res) - res = gdb_start_silent_cmd("memory watch $pc") - self.assertNoException(res) - target = debug_target("memwatch") - res = gdb_start_silent_cmd("memory watch &myglobal", - before=["set args 0xdeadbeef"], - after=["continue"], - target=target, - context='memory') +class MemoryCommand(RemoteGefUnitTestGeneric): + """`memory` command testing module""" + + def setUp(self) -> None: + self._target = debug_target("memwatch") + return super().setUp() + + def test_cmd_memory_watch_basic(self): + gdb = self._gdb + + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, + gdb.execute("memory watch $pc", to_string=True), + ) + + gdb.execute("start") + + # basic syntax checks + gdb.execute("memory watch $pc 0x100 byte") + gdb.execute("memory watch $pc 0x40 word") + gdb.execute("memory watch $pc 0x30 dword") + gdb.execute("memory watch $pc 0x20 qword") + gdb.execute("memory watch $pc 0x8 pointers") + gdb.execute("memory watch $pc") + + def test_cmd_memory_watch_global_variable(self): + gdb = self._gdb + + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, + gdb.execute("memory unwatch $pc", to_string=True), + ) + + gdb.execute("start") + + gdb.execute("set args 0xdeadbeef") + gdb.execute("memory watch &myglobal") + gdb.execute("gef config context.layout memory") + gdb.execute("run") + + res: str = gdb.execute("context", to_string=True) self.assertIn("deadbeef", res) self.assertNotIn("cafebabe", res) - res = gdb_start_silent_cmd("memory watch &myglobal", - before=["set args 0xcafebabe"], - after=["continue"], - target=target, - context="memory") + + gdb.execute("continue") + + gdb.execute("set args 0xcafebabe") + gdb.execute("memory watch &myglobal") + gdb.execute("gef config context.layout memory") + gdb.execute("run") + + res: str = gdb.execute("context", to_string=True) self.assertIn("cafebabe", res) self.assertNotIn("deadbeef", res) - def test_cmd_memory_unwatch(self): - self.assertFailIfInactiveSession(gdb_run_cmd("memory unwatch $pc")) - res = gdb_start_silent_cmd("memory unwatch $pc") - self.assertNoException(res) - + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, + gdb.execute("memory unwatch $pc", to_string=True), + ) + gdb.execute("start") + gdb.execute("memory unwatch $pc", to_string=True) def test_cmd_memory_list(self): - self.assertFailIfInactiveSession(gdb_run_cmd("memory list")) - res = gdb_start_silent_cmd("memory list") - self.assertNoException(res) - + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("memory list", to_string=True) + ) + gdb.execute("start") + res = gdb.execute("memory list", to_string=True).strip() + assert "[+] No memory watches" == res def test_cmd_memory_reset(self): - self.assertFailIfInactiveSession(gdb_run_cmd("memory reset")) - res = gdb_start_silent_cmd("memory reset") - self.assertNoException(res) + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("memory reset", to_string=True) + ) + gdb.execute("start") + res = gdb.execute("memory reset", to_string=True) diff --git a/tests/commands/name_break.py b/tests/commands/name_break.py index d7f0b1de9..2155b53ef 100644 --- a/tests/commands/name_break.py +++ b/tests/commands/name_break.py @@ -3,21 +3,18 @@ """ -from tests.utils import gdb_run_cmd, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric -class NameBreakCommand(GefUnitTestGeneric): +class NameBreakCommand(RemoteGefUnitTestGeneric): """`name-break` command test module""" - def test_cmd_name_break(self): - res = gdb_run_cmd("nb foobar *main+10") - self.assertNoException(res) - - res = gdb_run_cmd("nb foobar *0xcafebabe") - self.assertNoException(res) + gdb = self._gdb + gdb.execute("start") + res = gdb.execute("nb foobar *main+10", to_string=True) + res = gdb.execute("nb foobar *0xcafebabe", to_string=True) self.assertIn("at 0xcafebabe", res) - res = gdb_start_silent_cmd("nb foobar") - self.assertNoException(res) + res = gdb.execute("start") + gdb.execute("nb foobar", to_string=True) diff --git a/tests/commands/nop.py b/tests/commands/nop.py index 59b98ac9a..b404318a9 100644 --- a/tests/commands/nop.py +++ b/tests/commands/nop.py @@ -4,309 +4,273 @@ import pytest -from tests.utils import (ARCH, GefUnitTestGeneric, debug_target, findlines, - gdb_run_cmd, gdb_run_silent_cmd, gdb_start_silent_cmd) - - -class NopCommand(GefUnitTestGeneric): +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ( + ARCH, + ERROR_INACTIVE_SESSION_MESSAGE, + debug_target, + p16, + p32, + p64, + u16, + u32, + u64, +) + + +class NopCommand(RemoteGefUnitTestGeneric): """`nop` command test module""" - cmd = "nop" - def test_cmd_nop_inactive(self): - res = gdb_run_cmd(f"{self.cmd}") - self.assertFailIfInactiveSession(res) + gdb = self._gdb + res = gdb.execute(f"{self.cmd}", to_string=True) + self.assertEqual(ERROR_INACTIVE_SESSION_MESSAGE, res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_no_arg(self): + gdb = self._gdb + gef = self._gef - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))", - after=( - self.cmd, - "pi print(gef.memory.read(gef.arch.pc, 4))", - ) - ) - self.assertNoException(res) - self.assertIn(r"\x90\x90\xeb\xfe", res) - + gdb.execute("start") + gef.memory.write(gef.arch.pc, p32(0xFEEBFEEB)) + gdb.execute(self.cmd) + res = u32(gef.memory.read(gef.arch.pc, 4)) + self.assertEqual(0xFEEB9090, res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_check_b_and_n_same_time(self): - - res = gdb_start_silent_cmd(f"{self.cmd} --b --n") - self.assertNoException(res) - self.assertIn(r"--b and --n cannot be specified at the same time.", res) - + gdb = self._gdb + gdb.execute("start") + res = gdb.execute(f"{self.cmd} --b --n", to_string=True).strip() + self.assertEqual("[!] --b and --n cannot be specified at the same time.", res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_no_arg_break_instruction(self): - res = gdb_start_silent_cmd( - (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", - "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))"), - - after=( - self.cmd, - "pi print(gef.memory.read(gef.arch.pc, 4))", - ) - ) - self.assertNoException(res) - self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) - self.assertNotIn(r"\x90\x90\xeb\xfe", res) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute('pi gef.arch.nop_insn=b"\\x90\\x91\\x92"') + gef.memory.write(gef.arch.pc, p32(0xFEEBFEEB)) + + res = gdb.execute(self.cmd, to_string=True).strip() + mem = u32(gef.memory.read(gef.arch.pc, 4)) + self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) + self.assertNotEqual(0xFEEB9090, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_force_arg_break_instruction(self): - res = gdb_start_silent_cmd( - (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", - "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))"), - - after=( - f"{self.cmd} --f", - "pi print(gef.memory.read(gef.arch.pc, 4))", - ) - ) - self.assertNoException(res) + gdb = self._gdb + gef = self._gef + + gdb.execute("start") + gdb.execute('pi gef.arch.nop_insn=b"\\x90\\x91\\x92"') + gef.memory.write(gef.arch.pc, p32(0xFEEBFEEB)) + res = gdb.execute(f"{self.cmd} --f", to_string=True).strip() + mem = u32(gef.memory.read(gef.arch.pc, 4)) self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) - self.assertIn(r"\x90\x91\xeb\xfe", res) - + self.assertEqual(0xFEEB9190, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_i_arg(self): - - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc+1, p64(0xfeebfeebfeebfeeb))", - after=( - f"{self.cmd} --i 2 $pc+1", - "pi print(gef.memory.read(gef.arch.pc+1, 8))", - ) - ) - self.assertNoException(res) - self.assertIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res) - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc + 1, p64(0xFEEBFEEBFEEBFEEB)) + gdb.execute(f"{self.cmd} --i 2 $pc+1") + mem = u64(gef.memory.read(gef.arch.pc + 1, 8)) + self.assertEqual(0xFEEBFEEB90909090, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_i_arg_reaching_unmapped_area(self): - - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc+1, p64(0xfeebfeebfeebfeeb))", - after=( - f"{self.cmd} --i 2000000000000000000000000000000000000 $pc+1", - "pi print(gef.memory.read(gef.arch.pc+1, 8))", - ) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc + 1, p64(0xFEEBFEEBFEEBFEEB)) + res = gdb.execute( + f"{self.cmd} --i 2000000000000000000000000000000000000 $pc+1", + to_string=True, ) + mem = u64(gef.memory.read(gef.arch.pc + 1, 8)) self.assertIn(r"reaching unmapped area", res) - self.assertNoException(res) - self.assertNotIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res) - - - @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") - def test_cmd_nop_invalid_end_address(self): - res = gdb_run_silent_cmd( - f"{self.cmd} --i 5 0x1337000+0x1000-4", - target=debug_target("mmap-known-address") - ) - self.assertNoException(res) - self.assertIn("reaching unmapped area", res) - + self.assertNotEqual(0xFEEBFEEB90909090, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_nop(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p32(0x9191))", - after=( - f"{self.cmd} --n", - "pi print(gef.memory.read(gef.arch.pc, 2))", - ) - ) - self.assertIn(r"\x90\x91", res) - self.assertNoException(res) - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p32(0x9191)) + res = gdb.execute(f"{self.cmd} --n", to_string=True) or "" + assert res + mem = u16(gef.memory.read(gef.arch.pc, 2)) + self.assertEqual(0x9190, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_nop_break_instruction(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", - after=( - f"{self.cmd} --n", - "pi print(gef.memory.read(gef.arch.pc, 2))", - ) - ) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p16(0xFEEB)) + res = gdb.execute(f"{self.cmd} --n", to_string=True) + mem = u16(gef.memory.read(gef.arch.pc, 2)) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertIn(r"b'\xeb\xfe'", res) - self.assertNoException(res) - + self.assertEqual(0xFEEB, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_nop_break_instruction_force(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", - after=( - f"{self.cmd} --n --f", - "pi print(gef.memory.read(gef.arch.pc, 2))", - ) - ) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p16(0xFEEB)) + res = gdb.execute(f"{self.cmd} --n --f", to_string=True) + mem = u16(gef.memory.read(gef.arch.pc, 2)) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertIn(r"b'\x90\xfe'", res) - self.assertNoException(res) - + self.assertEqual(0xFE90, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_nop_arg(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))", - after=( - f"{self.cmd} --i 4 --n", - "pi print(gef.memory.read(gef.arch.pc, 8))", - ) - ) - self.assertIn(r"b'\x90\x90\x90\x90\xeb\xfe\xeb\xfe'", res) - self.assertNoException(res) - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p64(0xFEEBFEEBFEEBFEEB)) + gdb.execute(f"{self.cmd} --i 4 --n") + mem = u64(gef.memory.read(gef.arch.pc, 8)) + self.assertEqual(0xFEEBFEEB90909090, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_nop_arg_multibnop_breaks(self): - res = gdb_start_silent_cmd( - (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", - "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), - - after=( - f"{self.cmd} --n", - "pi print(gef.memory.read(gef.arch.pc, 8))", - ) - ) - self.assertNoException(res) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute('pi gef.arch.nop_insn = b"\\x90\\x91\\x92"') + gef.memory.write(gef.arch.pc, p64(0xFEEBFEEBFEEBFEEB)) + res = gdb.execute(f"{self.cmd} --n", to_string=True) + mem = u64(gef.memory.read(gef.arch.pc, 8)) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertIn(r"b'\xeb\xfe\xeb\xfe\xeb\xfe\xeb\xfe'", res) - + self.assertEqual(0xFEEBFEEBFEEBFEEB, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_nop_arg_multibnop_breaks_force(self): - res = gdb_start_silent_cmd( - (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", - "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), - - after=( - f"{self.cmd} --n --f", - "pi print(gef.memory.read(gef.arch.pc, 8))", - ) - ) - self.assertNoException(res) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute('pi gef.arch.nop_insn = b"\\x90\\x91\\x92"') + gef.memory.write(gef.arch.pc, p64(0xFEEBFEEBFEEBFEEB)) + res = gdb.execute(f"{self.cmd} --n --f", to_string=True) + mem = u64(gef.memory.read(gef.arch.pc, 8)) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertIn(r"b'\x90\x91\x92\xfe\xeb\xfe\xeb\xfe'", res) - + self.assertEqual(0xFEEBFEEBFE929190, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_bytes(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p16(0x9191))", - after=( - f"{self.cmd} --b", - "pi print(gef.memory.read(gef.arch.pc, 2))", - ) - ) - - self.assertIn(r"\x90\x91", res) - self.assertNoException(res) - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p16(0x9191)) + res = gdb.execute( + f"{self.cmd} --b", + to_string=True, + ) or "" + assert res + mem = u16(gef.memory.read(gef.arch.pc, 2)) + self.assertEqual(0x9190, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_bytes_break_instruction(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", - after=( - f"{self.cmd} --b", - "pi print(gef.memory.read(gef.arch.pc, 2))", - ) - ) - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p16(0xFEEB)) + res = gdb.execute(f"{self.cmd} --b", to_string=True) + mem = u16(gef.memory.read(gef.arch.pc, 2)) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertIn(r"b'\xeb\xfe'", res) - self.assertNoException(res) - + self.assertEqual(0xFEEB, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_bytes_break_instruction_force(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", - after=( - f"{self.cmd} --b --f", - "pi print(gef.memory.read(gef.arch.pc, 2))", - ) - ) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p16(0xFEEB)) + res = gdb.execute(f"{self.cmd} --b --f", to_string=True) + mem = u16(gef.memory.read(gef.arch.pc, 2)) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertIn(r"b'\x90\xfe'", res) - self.assertNoException(res) - + self.assertEqual(0xfe90, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_bytes_arg(self): - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))", - after=( - f"{self.cmd} --i 2 --b --f", - "pi print(gef.memory.read(gef.arch.pc, 8))", - ) - ) - self.assertIn(r"b'\x90\x90\xeb\xfe\xeb\xfe\xeb\xfe'", res) - self.assertNoException(res) - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gef.memory.write(gef.arch.pc, p64(0xFEEBFEEBFEEBFEEB)) + res = gdb.execute(f"{self.cmd} --i 2 --b --f", to_string=True) or "" + assert res + mem = u64(gef.memory.read(gef.arch.pc, 8)) + self.assertEqual(0xFEEBFEEBFEEB9090, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_bytes_arg_nops_no_fit(self): - res = gdb_start_silent_cmd( - (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", - "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), - - after=( - f"{self.cmd} --i 4 --b", - "pi print(gef.memory.read(gef.arch.pc, 8))", - ) - ) - self.assertIn(r"b'\xeb\xfe\xeb\xfe\xeb\xfe\xeb\xfe'", res) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute('pi gef.arch.nop_insn = b"\\x90\\x91\\x92"') + gef.memory.write(gef.arch.pc, p64(0xFEEBFEEBFEEBFEEB)) + res = gdb.execute(f"{self.cmd} --i 4 --b", to_string=True) self.assertIn(r"will result in LAST-NOP (byte nr 0x1)", res) - self.assertNoException(res) - + mem = u64(gef.memory.read(gef.arch.pc, 8)) + self.assertEqual(0xFEEBFEEBFEEBFEEB, mem) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_bytes_arg_nops_no_fit_force(self): - res = gdb_start_silent_cmd( - (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", - "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), - - after=( - f"{self.cmd} --i 5 --b --f", - "pi print(gef.memory.read(gef.arch.pc, 8))", - ) - ) - self.assertIn(r"b'\x90\x91\x92\x90\x91\xfe\xeb\xfe'", res) + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute('pi gef.arch.nop_insn = b"\\x90\\x91\\x92"') + gef.memory.write(gef.arch.pc, p64(0xFEEBFEEBFEEBFEEB)) + res = gdb.execute(f"{self.cmd} --i 5 --b --f", to_string=True) + mem = u64(gef.memory.read(gef.arch.pc, 8)) self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) self.assertIn(r"will result in LAST-INSTRUCTION", res) - self.assertNoException(res) + self.assertEqual(0xFEEBFE9190929190, mem) + +class NopCommandMmapKnownAddress(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("mmap-known-address") + return super().setUp() + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_invalid_end_address(self): + gdb = self._gdb + gdb.execute("run") + res = gdb.execute(f"nop --i 5 0x1337000+0x1000-4", to_string=True) + self.assertIn("reaching unmapped area", res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_as_bytes_invalid_end_address(self): + gdb = self._gdb + gef = self._gef # Make sure we error out if writing nops into an unmapped or RO area - res = gdb_run_silent_cmd( - f"{self.cmd} --b --i 5 0x1337000+0x1000-4", - target=debug_target("mmap-known-address") + gdb.execute("run") + res = gdb.execute(f"nop --b --i 5 0x1337000+0x1000-4", to_string=True) + self.assertIn( + "Cannot patch instruction at 0x1337ffc: reaching unmapped area", res ) - self.assertNoException(res) - self.assertIn("Cannot patch instruction at 0x1337ffc: reaching unmapped area", res) # We had an off-by-one bug where we couldn't write the last byte before # an unmapped area. Make sure that we can now. - res = gdb_run_silent_cmd( - f"{self.cmd} --b --i 4 0x1337000+0x1000-4", - target=debug_target("mmap-known-address"), - after="pi print(f'*** *mem={u32(gef.memory.read(0x1337ffc, 4)):#x}')", + res = gdb.execute("nop --b --i 4 0x1337000+0x1000-4", to_string=True) + self.assertNotIn( + "Cannot patch instruction at 0x1337ffc: reaching unmapped area", res ) - self.assertNoException(res) - self.assertNotIn("Cannot patch instruction at 0x1337ffc: reaching unmapped area", res) - lines = findlines("*** *mem=", res) - self.assertEqual(len(lines), 1) - self.assertEqual(lines[0], "*** *mem=0x90909090") + # after="pi print(f'*** *mem={u32(gef.memory.read(0x1337ffc, 4)):#x}')", + + mem = u32(gef.memory.read(0x1337FFC, 4)) + self.assertEqual(0x90909090, mem) + # lines = findlines("*** *mem=", res) + # self.assertEqual(len(lines), 1) + # self.assertEqual(lines[0], "*** *mem=0x90909090") diff --git a/tests/commands/patch.py b/tests/commands/patch.py index 5db8c56e8..324c6064d 100644 --- a/tests/commands/patch.py +++ b/tests/commands/patch.py @@ -3,60 +3,78 @@ """ -from tests.utils import debug_target, gdb_run_cmd, gdb_run_silent_cmd, gdb_start_silent_cmd_last_line -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, debug_target, u16, u32, u64, u8 -class PatchCommand(GefUnitTestGeneric): +class PatchCommand(RemoteGefUnitTestGeneric): """`patch` command test module""" - def test_cmd_patch(self): - self.assertFailIfInactiveSession(gdb_run_cmd("patch")) - + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("patch", to_string=True) + ) def test_cmd_patch_byte(self): - res = gdb_start_silent_cmd_last_line("patch byte $pc 0xcc", after=["display/8bx $pc",]) - self.assertNoException(res) - self.assertRegex(res, r"0xcc\s*0x[^c]{2}") + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute("patch byte $pc 0xcc") + mem = u8(gef.memory.read(gef.arch.pc, 1)) + self.assertEqual(mem, 0xCC) def test_cmd_patch_byte_bytearray(self): - res = gdb_start_silent_cmd_last_line("set $_gef69 = { 0xcc, 0xcc }", after=["patch byte $pc $_gef69", "display/8bx $pc",]) - self.assertNoException(res) - self.assertRegex(res, r"(0xcc\s*)(\1)0x[^c]{2}") + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute("set $_gef69 = { 0xcc, 0xcc }") + gdb.execute("patch byte $pc $_gef69") + mem = u16(gef.memory.read(gef.arch.pc, 2)) + self.assertEqual(mem, 0xCCCC) def test_cmd_patch_word(self): - res = gdb_start_silent_cmd_last_line("patch word $pc 0xcccc", after=["display/8bx $pc",]) - self.assertNoException(res) - self.assertRegex(res, r"(0xcc\s*)(\1)0x[^c]{2}") - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute("patch word $pc 0xcccc") + mem = u16(gef.memory.read(gef.arch.pc, 2)) + self.assertEqual(mem, 0xCCCC) def test_cmd_patch_dword(self): - res = gdb_start_silent_cmd_last_line("patch dword $pc 0xcccccccc", - after=["display/8bx $pc",]) - self.assertNoException(res) - self.assertRegex(res, r"(0xcc\s*)(\1\1\1)0x[^c]{2}") - + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute("patch dword $pc 0xcccccccc") + mem = u32(gef.memory.read(gef.arch.pc, 4)) + self.assertEqual(mem, 0xCCCCCCCC) def test_cmd_patch_qword(self): - res = gdb_start_silent_cmd_last_line("patch qword $pc 0xcccccccccccccccc", - after=["display/8bx $pc",]) - self.assertNoException(res) - self.assertRegex(res, r"(0xcc\s*)(\1\1\1\1\1\1)0xcc") + gdb = self._gdb + gef = self._gef + gdb.execute("start") + gdb.execute("patch qword $pc 0xcccccccccccccccc") + mem = u64(gef.memory.read(gef.arch.pc, 8)) + self.assertEqual(mem, 0xCCCCCCCCCCCCCCCC) + + def test_cmd_patch_string(self): + gdb = self._gdb + gdb.execute("start") + gdb.execute('patch string $sp "Gef!Gef!Gef!Gef!"') + res = gdb.execute("grep Gef!Gef!Gef!Gef!", to_string=True).strip() + self.assertIn("Gef!Gef!Gef!Gef!", res) + +class PatchCommandBss(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("bss") + return super().setUp() def test_cmd_patch_qword_symbol(self): - target = debug_target("bss") - before = gdb_run_silent_cmd("deref -l 1 $sp", target=target) - after = gdb_run_silent_cmd("patch qword $sp &msg", after=["deref -l 1 $sp"], target=target) - self.assertNoException(before) - self.assertNoException(after) + gdb = self._gdb + gdb.execute("run") + before = gdb.execute("deref -l 1 $sp", to_string=True) + gdb.execute("patch qword $sp &msg") + after = gdb.execute("deref -l 1 $sp", to_string=True) self.assertNotIn("Hello world!", before) self.assertIn("Hello world!", after) - - - def test_cmd_patch_string(self): - res = gdb_start_silent_cmd_last_line("patch string $sp \"Gef!Gef!Gef!Gef!\"", - after=["grep Gef!Gef!Gef!Gef!",]) - self.assertNoException(res) - self.assertIn("Gef!Gef!Gef!Gef!", res) diff --git a/tests/commands/pattern.py b/tests/commands/pattern.py index a981bb690..4f5f8f02a 100644 --- a/tests/commands/pattern.py +++ b/tests/commands/pattern.py @@ -3,28 +3,33 @@ """ import pytest -from tests.utils import ARCH, GefUnitTestGeneric, debug_target, gdb_run_cmd, is_64b +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, debug_target, is_64b -class PatternCommand(GefUnitTestGeneric): +class PatternCommand(RemoteGefUnitTestGeneric): """`pattern` command test module""" + def setUp(self) -> None: + self._target = debug_target("pattern") + return super().setUp() + def test_cmd_pattern_create(self): + gdb = self._gdb cmd = "pattern create -n 4 32" - res = gdb_run_cmd(cmd) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True).strip() self.assertIn("aaaabaaacaaadaaaeaaaf", res) cmd = "pattern create -n 8 32" - res = gdb_run_cmd(cmd) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True).strip() self.assertIn("aaaaaaaabaaaaaaacaaaaaaadaaaaaaa", res) - - @pytest.mark.skipif(ARCH not in ("x86_64", "aarch64", "i686", "armv7l"), - reason=f"Skipped for {ARCH}") + @pytest.mark.skipif( + ARCH not in ("x86_64", "aarch64", "i686", "armv7l"), + reason=f"Skipped for {ARCH}", + ) def test_cmd_pattern_search(self): - target = debug_target("pattern") + gdb = self._gdb if ARCH == "aarch64": lookup_register = "$x30" expected_offsets = (16, 16, 5, 9) @@ -41,39 +46,49 @@ def test_cmd_pattern_search(self): else: raise ValueError("Invalid architecture") - #0 + # 0 cmd = f"pattern search -n 4 {lookup_register}" - before = ("set args aaaabaaacaaadaaaeaaafaaagaaahaaa", "run") - res = gdb_run_cmd(cmd, before=before, target=target) - self.assertNoException(res) - self.assertIn(f"Found at offset {expected_offsets[0]} (little-endian search) likely", res) + gdb.execute("set args aaaabaaacaaadaaaeaaafaaagaaahaaa") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn( + f"Found at offset {expected_offsets[0]} (little-endian search) likely", res + ) - #1 + # 1 if is_64b(): cmd = f"pattern search -n 8 {lookup_register}" - before = ("set args aaaaaaaabaaaaaaacaaaaaaadaaaaaaa", "run") - res = gdb_run_cmd(cmd, before=before, target=target) - self.assertNoException(res) - self.assertIn(f"Found at offset {expected_offsets[1]} (little-endian search) likely", res) + gdb.execute("set args aaaaaaaabaaaaaaacaaaaaaadaaaaaaa") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn( + f"Found at offset {expected_offsets[1]} (little-endian search) likely", + res, + ) - #2 + # 2 cmd = "pattern search -n 4 caaa" - before = ("set args aaaabaaacaaadaaaeaaafaaagaaahaaa", "run") - res = gdb_run_cmd(cmd, before=before, target=target) - self.assertNoException(res) - self.assertIn(f"Found at offset {expected_offsets[2]} (little-endian search) likely", res) + gdb.execute("set args aaaabaaacaaadaaaeaaafaaagaaahaaa") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn( + f"Found at offset {expected_offsets[2]} (little-endian search) likely", res + ) - #3 + # 3 if is_64b(): cmd = "pattern search -n 8 caaaaaaa" - before = ("set args aaaaaaaabaaaaaaacaaaaaaadaaaaaaa", "run") - res = gdb_run_cmd(cmd, before=before, target=target) - self.assertNoException(res) - self.assertIn(f"Found at offset {expected_offsets[3]} (little-endian search) likely", res) + gdb.execute("set args aaaaaaaabaaaaaaacaaaaaaadaaaaaaa") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn( + f"Found at offset {expected_offsets[3]} (little-endian search) likely", + res, + ) - #4 + # 4 cmd = "pattern search -n 4 JUNK" - before = ("set args aaaabaaacaaadaaaeaaafaaagaaahaaa", "run") - res = gdb_run_cmd(cmd, before=before, target=target) - self.assertNoException(res) + gdb.execute("set args aaaabaaacaaadaaaeaaafaaagaaahaaa") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn(f"not found", res) diff --git a/tests/commands/pcustom.py b/tests/commands/pcustom.py index 9b66205c9..de4692ff1 100644 --- a/tests/commands/pcustom.py +++ b/tests/commands/pcustom.py @@ -5,13 +5,12 @@ import tempfile import pathlib +from tests.base import RemoteGefUnitTestGeneric + from tests.utils import ( - gdb_run_cmd, - gdb_run_silent_cmd, is_64b, debug_target, GEF_DEFAULT_TEMPDIR, - GefUnitTestGeneric, ) @@ -23,86 +22,119 @@ class goo_t(Structure): """ -class PcustomCommand(GefUnitTestGeneric): +class PcustomCommand(RemoteGefUnitTestGeneric): """`pcustom` command test module""" + def setUp(self) -> None: + self._target = debug_target("pcustom") + return super().setUp() + def test_cmd_pcustom(self): + gdb = self._gdb with tempfile.TemporaryDirectory(prefix=GEF_DEFAULT_TEMPDIR) as dd: dirpath = pathlib.Path(dd).absolute() - with tempfile.NamedTemporaryFile(dir = dirpath, suffix=".py") as fd: + with tempfile.NamedTemporaryFile(dir=dirpath, suffix=".py") as fd: fd.write(struct) fd.seek(0) fd.flush() - - res = gdb_run_cmd("gef config pcustom.struct_path", - before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) - self.assertIn(f"pcustom.struct_path (str) = \"{dirpath}\"", res) - - res = gdb_run_cmd("pcustom", before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) - structline = [x for x in res.splitlines() if x.startswith(f" → {dirpath}") ][0] - self.assertIn("goo_t", structline) - self.assertIn("foo_t", structline) - - # bad structure name with address - res = gdb_run_cmd("pcustom meh_t 0x1337100", - before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) - self.assertIn("Session is not active", res) - - + fpath = pathlib.Path(fd.name) + + # + # Assign the struct_path setting + # + gdb.execute(f"gef config pcustom.struct_path {dirpath}") + res = gdb.execute("gef config pcustom.struct_path", to_string=True) + self.assertIn(f'pcustom.struct_path (str) = "{dirpath}"', res) + + # + # List the structures in the files inside dirpath + # + lines = gdb.execute("pcustom list", to_string=True).splitlines()[1:] + assert len(lines) == 1 + assert lines[0] == f" → {fpath} (foo_t, goo_t)" + + # + # Test with a bad structure name with address + # + gdb.execute("run") + bad_struct_name = "meh_t" + res = gdb.execute(f"pcustom {bad_struct_name} 0x1337100", to_string=True).strip() + self.assertEqual(f"[!] No structure named '{bad_struct_name}' found", res) + print(res) def test_cmd_pcustom_show(self): + gdb = self._gdb with tempfile.TemporaryDirectory(prefix=GEF_DEFAULT_TEMPDIR) as dd: dirpath = pathlib.Path(dd).absolute() - with tempfile.NamedTemporaryFile(dir = dirpath, suffix=".py") as fd: + with tempfile.NamedTemporaryFile(dir=dirpath, suffix=".py") as fd: fd.write(struct) fd.seek(0) fd.flush() # no address - res = gdb_run_cmd("pcustom foo_t", - before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) + gdb.execute(f"gef config pcustom.struct_path {dirpath}") + gdb.execute("run") + lines = gdb.execute("pcustom foo_t", to_string=True).splitlines() if is_64b(): - self.assertIn("0000 a c_int /* size=0x4 */", res) - self.assertIn("0004 b c_int /* size=0x4 */", res) + self.assertEqual( + "0000 a c_int /* size=0x4 */", + lines[0], + ) + self.assertEqual( + "0004 b c_int /* size=0x4 */", + lines[1], + ) else: - self.assertIn("0000 a c_long /* size=0x4 */", res) - self.assertIn("0004 b c_long /* size=0x4 */", res) + self.assertEqual( + "0000 a c_long /* size=0x4 */", + lines[0], + ) + self.assertEqual( + "0004 b c_long /* size=0x4 */", + lines[1], + ) # with address - res = gdb_run_silent_cmd("pcustom goo_t 0x1337100", target=debug_target("pcustom"), - before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) + gdb.execute(f"gef config pcustom.struct_path {dirpath}") + gdb.execute("run") + res = gdb.execute("pcustom goo_t 0x1337100", to_string=True) if is_64b(): - self.assertIn(f"""0x1337100+0x00 a : 3 (c_int) + self.assertIn( + f"""0x1337100+0x00 a : 3 (c_int) 0x1337100+0x04 b : 4 (c_int) -0x1337100+0x08 c : """, res) - self.assertIn(f""" 0x1337000+0x00 a : 1 (c_int) +0x1337100+0x08 c : """, + res, + ) + self.assertIn( + f""" 0x1337000+0x00 a : 1 (c_int) 0x1337000+0x04 b : 2 (c_int) 0x1337100+0x10 d : 12 (c_int) -0x1337100+0x14 e : 13 (c_int)""", res) +0x1337100+0x14 e : 13 (c_int)""", + res, + ) else: - self.assertIn(f"""0x1337100+0x00 a : 3 (c_long) + self.assertIn( + f"""0x1337100+0x00 a : 3 (c_long) 0x1337100+0x04 b : 4 (c_long) -0x1337100+0x08 c : """, res) - self.assertIn(f""" 0x1337000+0x00 a : 1 (c_long) +0x1337100+0x08 c : """, + res, + ) + self.assertIn( + f""" 0x1337000+0x00 a : 1 (c_long) 0x1337000+0x04 b : 2 (c_long) 0x1337100+0x0c d : 12 (c_long) -0x1337100+0x10 e : 13 (c_long)""", res) +0x1337100+0x10 e : 13 (c_long)""", + res, + ) # bad structure name - res = gdb_run_cmd("pcustom meh_t", - before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) + gdb.execute(f"gef config pcustom.struct_path {dirpath}") + res = gdb.execute("pcustom meh_t", to_string=True) self.assertIn("No structure named 'meh_t' found", res) # bad structure name with address - res = gdb_run_silent_cmd("pcustom meh_t 0x1337100", target=debug_target("pcustom"), - before=[f"gef config pcustom.struct_path {dirpath}",]) - self.assertNoException(res) + gdb.execute(f"gef config pcustom.struct_path {dirpath}") + res = gdb.execute("pcustom meh_t 0x1337100", to_string=True) self.assertIn("No structure named 'meh_t' found", res) diff --git a/tests/commands/pie.py b/tests/commands/pie.py index 82bf6c79d..6b0d6fd59 100644 --- a/tests/commands/pie.py +++ b/tests/commands/pie.py @@ -2,52 +2,54 @@ `pie` command test module """ -from tests.utils import (GefUnitTestGeneric, debug_target, find_symbol, gdb_run_cmd, - removeuntil) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import debug_target, find_symbol, removeuntil -class PieCommand(GefUnitTestGeneric): +class PieCommand(RemoteGefUnitTestGeneric): """`pie` command test module""" - def setUp(self) -> None: target = debug_target("default") self.pie_offset = find_symbol(target, "main") self.assertGreater(self.pie_offset, 0) return super().setUp() - def test_cmd_pie(self): - res = gdb_run_cmd("pie") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("pie", to_string=True) self.assertIn("pie (breakpoint|info|delete|run|attach|remote)", res) - res = gdb_run_cmd("pie info 42") - self.assertNoException(res) - res = gdb_run_cmd("pie delete 42") - self.assertNoException(res) - + gdb.execute("pie info 42") + res = gdb.execute("pie delete 42", to_string=True).strip() + assert not res def test_cmd_pie_breakpoint_check(self): + gdb = self._gdb + # breakpoint at a random instruction and check - res = gdb_run_cmd(f"pie breakpoint {self.pie_offset}", after=("pie info")) - self.assertNoException(res) + gdb.execute(f"pie breakpoint {self.pie_offset}") + res = gdb.execute("pie info", to_string=True) last_line_addr = res.splitlines()[-1].strip().split() self.assertEqual(last_line_addr[0], "1") self.assertEqual(last_line_addr[-1], hex(self.pie_offset)) - def test_cmd_pie_breakpoint_delete(self): - res = gdb_run_cmd(f"pie breakpoint {self.pie_offset}", after=("pie delete 1", "pie info")) - self.assertNoException(res) + gdb = self._gdb + gdb.execute(f"pie breakpoint {self.pie_offset}") + gdb.execute("pie delete 1") + res = gdb.execute("pie info", to_string=True) self.assertNotIn(hex(self.pie_offset), res) - def test_cmd_pie_breakpoint_run(self): + gdb = self._gdb # breakpoint at a random instruction and run - res = gdb_run_cmd("pie run", before=(f"pie breakpoint {self.pie_offset}",)) - self.assertNoException(res) + gdb.execute(f"pie breakpoint {self.pie_offset}") + res = gdb.execute( + "pie run", + to_string=True + ) # check we stopped for a breakpoint - res = removeuntil("Name: \"default.out\", stopped ", res).splitlines()[0] + res = removeuntil('Name: "default.out", stopped ', res).splitlines()[0] self.assertIn("in main (), reason: BREAKPOINT", res) # check the mask of the breakpoint address address = int(res.split()[0], 16) diff --git a/tests/commands/print_format.py b/tests/commands/print_format.py index 6686199ac..5704c9569 100644 --- a/tests/commands/print_format.py +++ b/tests/commands/print_format.py @@ -3,38 +3,47 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, p32 -class PrintFormatCommand(GefUnitTestGeneric): +class PrintFormatCommand(RemoteGefUnitTestGeneric): """`print-format` command test module""" - def test_cmd_print_format(self): - self.assertFailIfInactiveSession(gdb_run_cmd("print-format")) - res = gdb_start_silent_cmd("print-format $sp") - self.assertNoException(res) - self.assertIn("buf = [" , res) - res = gdb_start_silent_cmd("print-format --lang js $sp") - self.assertNoException(res) - self.assertIn("var buf = [" , res) - res = gdb_start_silent_cmd("set *((int*)$sp) = 0x41414141", - after=["print-format --lang hex $sp"]) - self.assertNoException(res) + gdb = self._gdb + gef = self._gef + + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("print-format", to_string=True) + ) + + gdb.execute("start") + + res = gdb.execute("print-format $sp", to_string=True).strip() + self.assertIn("buf = [", res) + + res = gdb.execute("print-format --lang js $sp", to_string=True).strip() + self.assertIn("var buf = [", res) + + gef.memory.write(gef.arch.sp, p32(0x41414141)) + res = gdb.execute("print-format --lang hex $sp", to_string=True).strip() self.assertIn("41414141", res, f"{res}") - res = gdb_start_silent_cmd("print-format --lang iDontExist $sp") - self.assertNoException(res) - self.assertIn("Language must be in:" , res) + res = gdb.execute("print-format --lang iDontExist $sp", to_string=True).strip() + self.assertIn("Language must be in:", res) def test_cmd_print_format_bytearray(self): - res = gdb_start_silent_cmd("set *((int*)$sp) = 0x41414141", - after=["print-format --lang bytearray -l 4 $sp"]) - self.assertNoException(res) - gef_var = res.split('$_gef')[1].split("'")[0] - self.assertTrue("\x41\x41\x41\x41" in res) - res = gdb_start_silent_cmd("set *((int*)$sp) = 0x41414141", - after=["print-format --lang bytearray -l 4 $sp", "p $_gef" + gef_var]) - self.assertNoException(res) - self.assertIn( - f"Saved data b'AAAA'... in '$_gef{gef_var}'", res) + gdb = self._gdb + gef = self._gef + + res = gdb.execute("start") + + gef.memory.write(gef.arch.sp, p32(0x41414141)) + + res = gdb.execute("print-format --lang bytearray -l 4 $sp", to_string=True).strip() + gef_var = res.split("$_gef")[1].split("'")[0] + self.assertEqual(f"Saved data b'AAAA'... in '$_gef{gef_var}'", res) + + res = gdb.execute(f"p $_gef{gef_var}", to_string=True).strip() + self.assertEqual("$1 = {0x41, 0x41, 0x41, 0x41}", res) diff --git a/tests/commands/process_search.py b/tests/commands/process_search.py index 4fe58e985..a9eba50c0 100644 --- a/tests/commands/process_search.py +++ b/tests/commands/process_search.py @@ -3,27 +3,39 @@ """ -from tests.utils import debug_target, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import debug_target -class ProcessSearchCommand(GefUnitTestGeneric): +class ProcessSearchCommand(RemoteGefUnitTestGeneric): """`process-search` command test module""" - - def test_cmd_process_search(self): - target = debug_target("pattern") - res = gdb_start_silent_cmd("process-search", target=target, - before=["set args w00tw00t"]) - self.assertNoException(res) - self.assertIn(str(target), res) - - res = gdb_start_silent_cmd("process-search gdb.*fakefake", - target=target, before=["set args w00tw00t"]) - self.assertNoException(res) - self.assertIn("gdb", res) - - res = gdb_start_silent_cmd("process-search --smart-scan gdb.*fakefake", - target=target, before=["set args w00tw00t"]) - self.assertNoException(res) - self.assertNotIn("gdb", res) + def setUp(self) -> None: + self._target = debug_target("pattern") + return super().setUp() + + def test_cmd_process_search1(self): + gdb = self._gdb + gdb.execute("set args w00tw00t") + gdb.execute("start") + res = gdb.execute("process-search", to_string=True) + self.assertIn(str(self._target), res) + + def test_cmd_process_search_wildcart(self): + gdb = self._gdb + gdb.execute("set args w00tw00t") + gdb.execute("start") + lines = gdb.execute("process-search gdb.*fakefake", to_string=True).splitlines() + self.assertEqual(len(lines), 0) + + lines = gdb.execute( + f"process-search gdb.*", to_string=True + ).splitlines() + self.assertGreaterEqual(len(lines), 1) + + def test_cmd_process_search_smartscan(self): + gdb = self._gdb + gdb.execute("set args w00tw00t") + gdb.execute("start") + lines = gdb.execute("process-search gdb.*fakefake", to_string=True).splitlines() + self.assertEqual(len(lines), 0) diff --git a/tests/commands/process_status.py b/tests/commands/process_status.py index 1a7d91e06..ae9ff5f2b 100644 --- a/tests/commands/process_status.py +++ b/tests/commands/process_status.py @@ -3,18 +3,21 @@ """ -from tests.utils import gdb_run_cmd, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE -class ProcessStatusCommand(GefUnitTestGeneric): +class ProcessStatusCommand(RemoteGefUnitTestGeneric): """`process-status` command test module""" - def test_cmd_process_status(self): - self.assertFailIfInactiveSession(gdb_run_cmd("process-status")) - res = gdb_start_silent_cmd("process-status") - self.assertNoException(res) + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, + gdb.execute("process-status", to_string=True), + ) + gdb.execute("start") + res = gdb.execute("process-status", to_string=True) self.assertIn("Process Information", res) self.assertIn("No child process", res) self.assertIn("No open connections", res) diff --git a/tests/commands/registers.py b/tests/commands/registers.py index 3e9e3da36..3de10c5c2 100644 --- a/tests/commands/registers.py +++ b/tests/commands/registers.py @@ -4,29 +4,37 @@ import pytest -from tests.utils import ARCH, GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, ERROR_INACTIVE_SESSION_MESSAGE -class RegistersCommand(GefUnitTestGeneric): - """`registers` command test module""" +class RegistersCommand(RemoteGefUnitTestGeneric): + """`registers` command test module""" - @pytest.mark.skipif(ARCH not in ["aarch64", "armv7l", "x86_64", "i686"], - reason=f"Skipped for {ARCH}") + @pytest.mark.skipif( + ARCH not in ["aarch64", "armv7l", "x86_64", "i686"], + reason=f"Skipped for {ARCH}", + ) def test_cmd_registers(self): - self.assertFailIfInactiveSession(gdb_run_cmd("registers")) - res = gdb_start_silent_cmd("registers") - self.assertNoException(res) - if ARCH in ("aarch64",): + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("registers", to_string=True) + ) + + gdb.execute("start") + res = gdb.execute("registers", to_string=True) + + if ARCH in ("aarch64"): self.assertIn("$x0", res) self.assertIn("$cpsr", res) - elif ARCH in ("armv7l", ): + elif ARCH in ("armv7l",): self.assertIn("$r0", res) self.assertIn("$lr", res) self.assertIn("$cpsr", res) - elif ARCH in ("x86_64", ): + elif ARCH in ("x86_64",): self.assertIn("$rax", res) self.assertIn("$eflags", res) - elif ARCH in ("i686", ): + elif ARCH in ("i686",): self.assertIn("$eax", res) self.assertIn("$eflags", res) diff --git a/tests/commands/reset_cache.py b/tests/commands/reset_cache.py index fda8d4106..ac589f5d9 100644 --- a/tests/commands/reset_cache.py +++ b/tests/commands/reset_cache.py @@ -3,13 +3,16 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric -class ResetCacheCommand(GefUnitTestGeneric): +class ResetCacheCommand(RemoteGefUnitTestGeneric): """`reset-cache` command test module""" def test_cmd_reset_cache(self): - res = gdb_start_silent_cmd("reset-cache") - self.assertNoException(res) + gdb = self._gdb + gdb.execute("start") + res = gdb.execute("reset-cache", to_string=True) + assert not res + # TODO improve diff --git a/tests/commands/scan.py b/tests/commands/scan.py index 54b5b9f60..b32c636d7 100644 --- a/tests/commands/scan.py +++ b/tests/commands/scan.py @@ -3,21 +3,27 @@ """ -from tests.utils import GefUnitTestGeneric, debug_target, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, debug_target -class ScanCommand(GefUnitTestGeneric): +class ScanCommand(RemoteGefUnitTestGeneric): """`scan` command test module""" + def setUp(self) -> None: + self._target = debug_target("checksec-no-pie") + return super().setUp() def test_cmd_scan(self): + gdb = self._gdb cmd = "scan libc stack" - target = debug_target("checksec-no-pie") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd)) - res = gdb_start_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn(str(target), res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) + self.assertIn(str(self._target), res) - res = gdb_start_silent_cmd("scan binary libc") - self.assertNoException(res) + gdb.execute("start") + res = gdb.execute("scan binary libc", to_string=True) self.assertIn("__libc_start_main", res) diff --git a/tests/commands/search_pattern.py b/tests/commands/search_pattern.py index c8c3be6d1..7985be456 100644 --- a/tests/commands/search_pattern.py +++ b/tests/commands/search_pattern.py @@ -3,26 +3,37 @@ """ -from tests.utils import BIN_SH, GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd, gdb_start_silent_cmd_last_line +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import BIN_SH, ERROR_INACTIVE_SESSION_MESSAGE -class SearchPatternCommand(GefUnitTestGeneric): +class SearchPatternCommand(RemoteGefUnitTestGeneric): """`search_pattern` command test module""" - def test_cmd_search_pattern(self): - self.assertFailIfInactiveSession(gdb_run_cmd(f"grep {BIN_SH}")) - res = gdb_start_silent_cmd(f"grep {BIN_SH}") - self.assertNoException(res) + gdb = self._gdb + cmd = f"grep {BIN_SH}" + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("start") + res = gdb.execute(f"grep {BIN_SH}", to_string=True) self.assertIn("0x", res) def test_cmd_search_pattern_regex(self): - res = gdb_start_silent_cmd_last_line("set {char[6]} $sp = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x00 }", -after=[r"search-pattern --regex $sp $sp+7 ([\\x20-\\x7E]{2,})(?=\\x00)",]) - self.assertNoException(res) + gdb = self._gdb + gdb.execute("start") + gdb.execute("set {char[6]} $sp = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x00 }") + res = gdb.execute( + r"search-pattern --regex $sp $sp+7 ([\\x20-\\x7E]{2,})(?=\\x00)", + to_string=True + ) self.assertTrue(r"b'ABCDE'" in res) + # this should not match because binary string is not null ended: - res = gdb_start_silent_cmd_last_line("set {char[6]} $sp = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x03 }", -after=[r"search-pattern --regex $sp $sp+7 ([\\x20-\\x7E]{2,})(?=\\x00)",]) - self.assertNoException(res) - self.assertTrue(r"b'ABCDE'" not in res) + gdb.execute("set {char[6]} $sp = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x03 }") + res = gdb.execute( + r"search-pattern --regex $sp $sp+7 ([\\x20-\\x7E]{2,})(?=\\x00)", + to_string=True + ) + self.assertNotIn(r"b'ABCDE'", res) diff --git a/tests/commands/shellcode.py b/tests/commands/shellcode.py index c1db51aeb..85495996c 100644 --- a/tests/commands/shellcode.py +++ b/tests/commands/shellcode.py @@ -2,40 +2,42 @@ Shellcode commands test module """ import pytest +from tests.base import RemoteGefUnitTestGeneric -from tests.utils import gdb_start_silent_cmd, GefUnitTestGeneric, BIN_SH +from tests.utils import BIN_SH -class ShellcodeCommand(GefUnitTestGeneric): +class ShellcodeCommand(RemoteGefUnitTestGeneric): """`shellcode` command test module""" def test_cmd_shellcode(self): - res = gdb_start_silent_cmd("shellcode") - self.assertNoException(res) + gdb = self._gdb + gdb.execute("start") + res = gdb.execute("shellcode", to_string=True) self.assertIn("Missing sub-command (search|get)", res) - @pytest.mark.online @pytest.mark.slow def test_cmd_shellcode_search(self): + gdb = self._gdb cmd = f"shellcode search execve {BIN_SH}" - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) self.assertIn(f"setuid(0) + execve({BIN_SH}) 49 bytes", res) - @pytest.mark.online @pytest.mark.slow def test_cmd_shellcode_get_ok(self): - res = gdb_start_silent_cmd("shellcode get 77") - self.assertNoException(res) + gdb = self._gdb + gdb.execute("start") + res = gdb.execute("shellcode get 77", to_string=True) self.assertIn("Shellcode written to ", res) - @pytest.mark.online @pytest.mark.slow def test_cmd_shellcode_get_nok(self): + gdb = self._gdb n = 1111111111111 - res = gdb_start_silent_cmd(f"shellcode get {n}") - self.assertNoException(res) + gdb.execute("start") + res = gdb.execute(f"shellcode get {n}", to_string=True) self.assertIn(f"Failed to fetch shellcode #{n}", res) diff --git a/tests/commands/skipi.py b/tests/commands/skipi.py index 79f6faa2c..7975a1624 100644 --- a/tests/commands/skipi.py +++ b/tests/commands/skipi.py @@ -4,59 +4,54 @@ import pytest -from tests.utils import (ARCH, GefUnitTestGeneric, debug_target, findlines, - gdb_run_cmd, gdb_run_silent_cmd, gdb_start_silent_cmd) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ARCH, ERROR_INACTIVE_SESSION_MESSAGE, p32, p64, u16, u32 -class SkipiCommand(GefUnitTestGeneric): +class SkipiCommand(RemoteGefUnitTestGeneric): """`skipi` command test module""" - cmd = "skipi" - def test_cmd_nop_inactive(self): - res = gdb_run_cmd(f"{self.cmd}") - self.assertFailIfInactiveSession(res) - + gdb = self._gdb + res = gdb.execute(f"{self.cmd}", to_string=True) + self.assertEqual(ERROR_INACTIVE_SESSION_MESSAGE, res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_skipi_no_arg(self): + gdb = self._gdb + gef = self._gef - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p32(0x9090feeb))", # 1 short jumps to pc + 2 nops - after=( - self.cmd, - "pi print(gef.memory.read(gef.arch.pc, 2))", # read 2 bytes - ) - ) - self.assertNoException(res) - self.assertIn(r"\x90\x90", res) # 2 nops - + gdb.execute("start") + gef.memory.write(gef.arch.pc, p32(0x9090FEEB)) + res = gdb.execute(self.cmd, to_string=True) + assert res + mem = u16(gef.memory.read(gef.arch.pc, 2)) # read 2 bytes + self.assertEqual(0x9090, mem) # 2 nops @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_skipi_skip_two_instructions(self): + gdb = self._gdb + gef = self._gef - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p64(0x90909090feebfeeb))", # 2 short jumps to pc + 4 nops - after=( - f"{self.cmd} --n 2", - "pi print(gef.memory.read(gef.arch.pc, 4))", # read 4 bytes - ) - ) - self.assertNoException(res) - self.assertIn(r"\x90\x90\x90\x90", res) # 4 nops - + gdb.execute("start") + gef.memory.write(gef.arch.pc, p64(0x90909090FEEBFEEB)) + res = gdb.execute(f"{self.cmd} --n 2", to_string=True) + assert res + mem = u32(gef.memory.read(gef.arch.pc, 4)) # read 4 bytes + self.assertEqual(0x90909090, mem) # 4 nops @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_skipi_two_instructions_from_location(self): + gdb = self._gdb + gef = self._gef - res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p64(0x9090feebfeebfeeb))", # 2 short jumps to pc + 2 nops - after=( - f"{self.cmd} $pc+2 --n 2", # from the second short jump - "pi print(gef.memory.read(gef.arch.pc, 2))", # read 2 bytes - ) + gdb.execute("start") + gef.memory.write(gef.arch.pc, p64(0x9090FEEBFEEBFEEB)) + res = gdb.execute( + f"{self.cmd} $pc+2 --n 2", to_string=True # from the second short jump ) - self.assertNoException(res) - self.assertIn(r"\x90\x90", res) # 2 nops + assert res + mem = u16(gef.memory.read(gef.arch.pc, 2)) # read 2 bytes + self.assertEqual(0x9090, mem) # 2 nops diff --git a/tests/commands/smart_eval.py b/tests/commands/smart_eval.py index 923bba458..5e59bd48d 100644 --- a/tests/commands/smart_eval.py +++ b/tests/commands/smart_eval.py @@ -3,21 +3,25 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric -class SmartEvalCommand(GefUnitTestGeneric): +class SmartEvalCommand(RemoteGefUnitTestGeneric): """`smart_eval` command test module""" def test_cmd_smart_eval(self): + gdb = self._gdb + gef = self._gef + + gdb.execute("start") examples = ( - ("$ $pc+1", ""), + ("$ $pc+1", str(gef.arch.pc+1)), ("$ -0x1000", "-4096"), ("$ 0x00007ffff7812000 0x00007ffff79a7000", "1658880"), ("$ 1658880", "0b110010101000000000000"), ) + for cmd, expected_value in examples: - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) + res = gdb.execute(cmd, to_string=True).strip() self.assertIn(expected_value, res) diff --git a/tests/commands/stub.py b/tests/commands/stub.py index b87db50f7..9edc5e7e9 100644 --- a/tests/commands/stub.py +++ b/tests/commands/stub.py @@ -3,20 +3,49 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +import pytest +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE -class StubCommand(GefUnitTestGeneric): +class StubCommand(RemoteGefUnitTestGeneric): """`stub` command test module""" + @pytest.fixture(autouse=True) + def capfd(self, capfd): + self.capfd = capfd def test_cmd_stub(self): + gdb = self._gdb # due to compiler optimizations printf might be converted to puts - cmds = ["stub printf", "stub puts"] - self.assertFailIfInactiveSession(gdb_run_cmd(cmds)) - res = gdb_start_silent_cmd("continue") - self.assertNoException(res) - self.assertIn("Hello World!", res) - res = gdb_start_silent_cmd(cmds, after=["continue"]) - self.assertNoException(res) - self.assertNotIn("Hello World!", res) + cmds = ("stub printf", "stub puts") + for cmd in cmds: + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + + # + # Sanity Check - no exception + # + gdb.execute("start") + captured = self.capfd.readouterr() + gdb.execute("continue") + captured = self.capfd.readouterr() + + assert "Hello World!" in captured.out + + # + # Make sure the prints are stubbed out + # + gdb.execute("start") + for cmd in cmds: + gdb.execute(f"{cmd} --retval 42") + assert len(gdb.breakpoints()) == len(cmds) + + # + # Check again, make sure the stdout buffer is emptied + # + captured = self.capfd.readouterr() + gdb.execute("continue") + captured = self.capfd.readouterr() + assert "Hello World!" not in captured.out diff --git a/tests/commands/theme.py b/tests/commands/theme.py index 7c6ca07df..a09cfaa6f 100644 --- a/tests/commands/theme.py +++ b/tests/commands/theme.py @@ -3,16 +3,15 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd +from tests.base import RemoteGefUnitTestGeneric -class ThemeCommand(GefUnitTestGeneric): +class ThemeCommand(RemoteGefUnitTestGeneric): """`theme` command test module""" - def test_cmd_theme(self): - res = gdb_run_cmd("theme") - self.assertNoException(res) + gdb = self._gdb + res = gdb.execute("theme", to_string=True) possible_themes = ( "context_title_line", "context_title_message", @@ -34,16 +33,13 @@ def test_cmd_theme(self): ) for t in possible_themes: # testing command viewing - res = gdb_run_cmd(f"theme {t}") - self.assertNoException(res) + res = gdb.execute(f"theme {t}", to_string=True) self.assertNotIn("Invalid key", res, f"Invalid key '{t}'") # testing command setting v = "blue blah 10 -1 0xfff bold" - res = gdb_run_cmd(f"theme {t} {v}") - self.assertNoException(res) + gdb.execute(f"theme {t} {v}") + - res = gdb_run_cmd(f"theme ___I_DONT_EXIST___") - self.assertNoException(res) + res = gdb.execute(f"theme ___I_DONT_EXIST___", to_string=True) self.assertIn("Invalid key", res) - return diff --git a/tests/commands/trace_run.py b/tests/commands/trace_run.py index 5b49016b6..34b561353 100644 --- a/tests/commands/trace_run.py +++ b/tests/commands/trace_run.py @@ -3,22 +3,21 @@ """ -from tests.utils import TMPDIR, GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import TMPDIR, ERROR_INACTIVE_SESSION_MESSAGE -class TraceRunCommand(GefUnitTestGeneric): +class TraceRunCommand(RemoteGefUnitTestGeneric): """`trace-run` command test module""" - def test_cmd_trace_run(self): + gdb = self._gdb cmd = "trace-run" - res = gdb_run_cmd(cmd) - self.assertFailIfInactiveSession(res) + res = gdb.execute(cmd, to_string=True) + self.assertEqual(ERROR_INACTIVE_SESSION_MESSAGE, res) cmd = "trace-run $pc+1" - res = gdb_start_silent_cmd( - cmd, - before=[f"gef config trace-run.tracefile_prefix {TMPDIR / 'gef-trace-'}"] - ) - self.assertNoException(res) + gdb.execute("start") + gdb.execute(f"gef config trace-run.tracefile_prefix {TMPDIR / 'gef-trace-'}") + res = gdb.execute(cmd, to_string=True) self.assertIn("Tracing from", res) diff --git a/tests/commands/version.py b/tests/commands/version.py index 7dd48c10f..332abb6ae 100644 --- a/tests/commands/version.py +++ b/tests/commands/version.py @@ -3,16 +3,14 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd +from tests.base import RemoteGefUnitTestGeneric -class VersionCommand(GefUnitTestGeneric): +class VersionCommand(RemoteGefUnitTestGeneric): """`version` command test module""" - cmd = "version" - def test_cmd_version(self): - res = gdb_run_cmd(self.cmd) - self.assertNoException(res) + gdb = self._gdb + gdb.execute(self.cmd) diff --git a/tests/commands/vmmap.py b/tests/commands/vmmap.py index 225011b96..50dfeacaa 100644 --- a/tests/commands/vmmap.py +++ b/tests/commands/vmmap.py @@ -3,18 +3,21 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE -class VmmapCommand(GefUnitTestGeneric): +class VmmapCommand(RemoteGefUnitTestGeneric): """`vmmap` command test module""" def test_cmd_vmmap(self): - self.assertFailIfInactiveSession(gdb_run_cmd("vmmap")) - res = gdb_start_silent_cmd("vmmap") - self.assertNoException(res) - self.assertTrue(len(res.splitlines()) > 1) + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("vmmap", to_string=True) + ) + gdb.execute("start") + res = gdb.execute("vmmap", to_string=True) + self.assertGreater(len(res.splitlines()), 1) - res = gdb_start_silent_cmd("vmmap stack") - self.assertNoException(res) - self.assertTrue(len(res.splitlines()) > 1) + res = gdb.execute("vmmap stack", to_string=True) + self.assertGreater(len(res.splitlines()), 1) diff --git a/tests/commands/xfiles.py b/tests/commands/xfiles.py index 8551d11b2..0833da06c 100644 --- a/tests/commands/xfiles.py +++ b/tests/commands/xfiles.py @@ -3,15 +3,18 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE -class XfilesCommand(GefUnitTestGeneric): +class XfilesCommand(RemoteGefUnitTestGeneric): """`xfiles` command test module""" - def test_cmd_xfiles(self): - self.assertFailIfInactiveSession(gdb_run_cmd("xfiles")) - res = gdb_start_silent_cmd("xfiles") - self.assertNoException(res) - self.assertTrue(len(res.splitlines()) >= 3) + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("xfiles", to_string=True) + ) + gdb.execute("start") + res = gdb.execute("xfiles", to_string=True) + self.assertGreaterEqual(len(res.splitlines()) , 3) diff --git a/tests/commands/xinfo.py b/tests/commands/xinfo.py index ea50e23b9..c6029dadf 100644 --- a/tests/commands/xinfo.py +++ b/tests/commands/xinfo.py @@ -3,25 +3,36 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd, gdb_run_silent_cmd, debug_target +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, debug_target -class XinfoCommand(GefUnitTestGeneric): +class XinfoCommand(RemoteGefUnitTestGeneric): """`xinfo` command test module""" - def test_cmd_xinfo(self): - self.assertFailIfInactiveSession(gdb_run_cmd("xinfo $sp")) - res = gdb_start_silent_cmd("xinfo") + gdb = self._gdb + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute("xinfo $sp", to_string=True) + ) + + gdb.execute("start") + res = gdb.execute("xinfo", to_string=True) self.assertIn("At least one valid address must be specified", res) - res = gdb_start_silent_cmd("xinfo $sp") - self.assertNoException(res) - self.assertTrue(len(res.splitlines()) >= 7) + lines = gdb.execute("xinfo $sp", to_string=True).splitlines() + self.assertGreaterEqual(len(lines), 6) + + +class XinfoCommandClass(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("class") + return super().setUp() def test_cmd_xinfo_on_class(self): + gdb = self._gdb cmd = "xinfo $pc+4" - target = debug_target("class") - res = gdb_run_silent_cmd(cmd, target=target, before=["b *'B::Run'"]) - self.assertNoException(res) + gdb.execute("b *'B::Run'") + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn("Symbol: B::Run()+4", res) diff --git a/tests/commands/xor_memory.py b/tests/commands/xor_memory.py index c68b15665..a727a74cb 100644 --- a/tests/commands/xor_memory.py +++ b/tests/commands/xor_memory.py @@ -3,24 +3,28 @@ """ -from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE - -class XorMemoryCommand(GefUnitTestGeneric): +class XorMemoryCommand(RemoteGefUnitTestGeneric): """`xor-memory` command test module""" def test_cmd_xor_memory_display(self): + gdb = self._gdb cmd = "xor-memory display $sp 0x10 0x41" - self.assertFailIfInactiveSession(gdb_run_cmd(cmd)) - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) - self.assertIn("Original block", res) - self.assertIn("XOR-ed block", res) + self.assertEqual(ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True)) + + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) + assert "Original block" in res + assert "XOR-ed block" in res def test_cmd_xor_memory_patch(self): + gdb = self._gdb + gdb.execute("start") + cmd = "xor-memory patch $sp 0x10 0x41" - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) - self.assertIn("Patching XOR-ing ", res) + res = gdb.execute(cmd, to_string=True) + assert "Patching XOR-ing " in res diff --git a/tests/config/__init__.py b/tests/config/__init__.py index d2f59d67f..b509dfc12 100644 --- a/tests/config/__init__.py +++ b/tests/config/__init__.py @@ -2,70 +2,88 @@ Test GEF configuration parameters. """ -from tests.utils import gdb_run_cmd, gdb_start_silent_cmd -from tests.utils import GefUnitTestGeneric +from typing import List +import pytest +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import GEF_RIGHT_ARROW -class TestGefConfigUnit(GefUnitTestGeneric): - """Test GEF configuration parameters.""" +class TestGefConfigUnit(RemoteGefUnitTestGeneric): + """Test GEF configuration parameters.""" def test_config_show_opcodes_size(self): """Check opcodes are correctly shown.""" - res = gdb_run_cmd("entry-break", before=["gef config context.show_opcodes_size 4"]) - self.assertNoException(res) - self.assertGreater(len(res.splitlines()), 1) + gdb = self._gdb + + max_opcode_len = 4 + # + # Only show the code pane, limit the opcode size to 4 bytes (8 chars) + # + gdb.execute("gef config context.layout code") + gdb.execute(f"gef config context.show_opcodes_size {max_opcode_len}") - # output format: 0xaddress opcode mnemo [operands, ...] + gdb.execute("start") + lines: List[str] = gdb.execute("context", to_string=True).splitlines()[1:-1] + self.assertGreater(len(lines), 1) + + # + # For each line, check the format + # + # output format: 0xaddress opcode[...] mnemo [operands, ...] # example: 0x5555555546b2 897dec mov DWORD PTR [rbp-0x14], edi - self.assertRegex(res, r"(0x([0-9a-f]{2})+)\s+(([0-9a-f]{2})+)\s+<[^>]+>\s+(.*)") + # + for line in lines: + parts = line.replace(GEF_RIGHT_ARROW, "").split() + opcode: str = parts[1].replace("...", "").lower() + assert ( + len(opcode) <= max_opcode_len * 2 + ), f"Invalid length for {opcode=}: {len(opcode)}" + assert all(map(lambda c: c in "0123456789abcdef", opcode)) def test_config_hook_validator(self): """Check that a GefSetting hook can prevent setting a config.""" - res = gdb_run_cmd("gef config gef.tempdir '/tmp/path with space'") + gdb = self._gdb + + res = gdb.execute( + "gef config gef.tempdir '/tmp/path with space'", to_string=True + ) # Validators just use `err` to print an error - self.assertNoException(res) self.assertRegex(res, r"[!].+Cannot set.+setting cannot contain spaces") - res = gdb_run_cmd("gef config gef.tempdir '/tmp/valid-path'") - self.assertNoException(res) - self.assertNotIn("[!]", res) + gdb.execute("gef config gef.tempdir '/tmp/valid-path'") + res = gdb.execute("gef config gef.tempdir", to_string=True).splitlines()[1] + assert res == 'gef.tempdir (str) = "/tmp/valid-path"' def test_config_type_validator(self): """Check that a GefSetting type can prevent setting a config.""" - res = gdb_run_cmd("gef config gef.debug invalid") - self.assertNoException(res) + gdb = self._gdb + + res = gdb.execute("gef config gef.debug invalid", to_string=True) self.assertRegex(res, r"[!].+expects type 'bool'") - res = gdb_run_cmd("gef config gef.debug true") - self.assertNoException(res) - self.assertNotIn("[!]", res) - res = gdb_run_cmd("gef config gef.debug 1") - self.assertNoException(res) - self.assertNotIn("[!]", res) - res = gdb_run_cmd("gef config gef.debug F") - self.assertNoException(res) - self.assertNotIn("[!]", res) - res = gdb_run_cmd("gef config gef.debug 0") - self.assertNoException(res) - self.assertNotIn("[!]", res) + gdb.execute("gef config gef.debug true") + gdb.execute("gef config gef.debug 1") + gdb.execute("gef config gef.debug F") + gdb.execute("gef config gef.debug 0") + + output = gdb.execute("gef config gef.debug 'fooo'", to_string=True).strip() + assert output == "[!] 'gef.debug' expects type 'bool', got str: reason cannot parse 'fooo' as bool" + def test_config_libc_version(self): """Check setting libc version.""" - res = gdb_run_cmd("gef config gef.libc_version") - self.assertNoException(res) - self.assertNotIn("[!]", res) - - res = gdb_run_cmd("gef config gef.libc_version", before=["gef config gef.libc_version 2.31"]) - self.assertNoException(res) - self.assertNotIn("[!]", res) - self.assertIn('gef.libc_version (str) = "2.31"', res) - - res = gdb_run_cmd("gef config gef.libc_version", before=["gef config gef.libc_version 2.31", "gef config gef.libc_version ''"]) - self.assertNoException(res) - self.assertNotIn("[!]", res) - self.assertIn('gef.libc_version (str) = ""', res) - - res = gdb_start_silent_cmd("python print(gef.libc.version)", before=["gef config gef.libc_version 2.31"]) - self.assertNoException(res) - self.assertNotIn("[!]", res) + gdb = self._gdb + + # + # When starting, should be empty + # + res = gdb.execute("gef config gef.libc_version", to_string=True).splitlines()[1] + assert res == 'gef.libc_version (str) = ""' + + gdb.execute("gef config gef.libc_version 2.31") + gdb.execute("start") + + res = gdb.execute("gef config gef.libc_version 2.31", to_string=True) + res = gdb.execute("gef config gef.libc_version", to_string=True).splitlines()[1] + assert res == 'gef.libc_version (str) = "2.31"' diff --git a/tests/functions/elf_sections.py b/tests/functions/elf_sections.py index 0dcdebff9..c3f114373 100644 --- a/tests/functions/elf_sections.py +++ b/tests/functions/elf_sections.py @@ -3,79 +3,102 @@ """ -from tests.utils import debug_target, gdb_run_cmd, gdb_run_silent_cmd, gdb_start_silent_cmd, is_64b -from tests.utils import GefUnitTestGeneric +import pytest +from tests.utils import ERROR_INACTIVE_SESSION_MESSAGE, debug_target, is_64b +from tests.base import RemoteGefUnitTestGeneric -class ElfSectionGdbFunction(GefUnitTestGeneric): - """GDB functions test module""" +class ElfSectionGdbFunction(RemoteGefUnitTestGeneric): + """GDB functions test module""" def test_func_base(self): """`$_base()` GDB function test""" + gdb = self._gdb cmd = "x/s $_base()" - self.assertFailIfInactiveSession(gdb_run_cmd(cmd)) - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) + + with pytest.raises(Exception, match="No debugging session active"): + gdb.execute(cmd) + + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) self.assertIn("\\177ELF", res) addr = res.splitlines()[-1].split()[0][:-1] - cmd = "x/s $_base(\"libc\")" - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) + cmd = 'x/s $_base("libc")' + res = gdb.execute(cmd, to_string=True) self.assertIn("\\177ELF", res) addr2 = res.splitlines()[-1].split()[0][:-1] self.assertNotEqual(addr, addr2) + def test_func_stack(self): + """`$_stack()` GDB function test""" + gdb = self._gdb + cmd = "deref $_stack()" + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("start") + res = gdb.execute(cmd, to_string=True) + if is_64b(): + self.assertRegex(res, r"\+0x0*20: *0x0000000000000000\n") + else: + self.assertRegex(res, r"\+0x0.*20: *0x00000000\n") + + +class ElfSectionGdbFunctionBss(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("bss") + return super().setUp() def test_func_bss(self): """`$_bss()` GDB function test""" + gdb = self._gdb cmd = "deref $_bss()" - target = debug_target("bss") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) self.assertIn("Hello world!", res) +class ElfSectionGdbFunctionHeap(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("heap") + return super().setUp() + def test_func_got(self): """`$_got()` GDB function test""" + gdb = self._gdb cmd = "deref $_got()" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) - self.assertIn("malloc", res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) + self.assertIn("malloc", res) def test_func_heap(self): """`$_heap()` GDB function test""" + gdb = self._gdb cmd = "deref $_heap()" - target = debug_target("heap") - self.assertFailIfInactiveSession(gdb_run_cmd(cmd, target=target)) - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + self.assertEqual( + ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True) + ) + + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) if is_64b(): self.assertIn("+0x0048:", res) else: self.assertIn("+0x0024:", res) cmd = "deref $_heap(0x10+0x10)" - res = gdb_run_silent_cmd(cmd, target=target) - self.assertNoException(res) + gdb.execute("run") + res = gdb.execute(cmd, to_string=True) if is_64b(): self.assertIn("+0x0048:", res) else: self.assertIn("+0x0024:", res) - - - def test_func_stack(self): - """`$_stack()` GDB function test""" - cmd = "deref $_stack()" - self.assertFailIfInactiveSession(gdb_run_cmd(cmd)) - res = gdb_start_silent_cmd(cmd) - self.assertNoException(res) - if is_64b(): - self.assertRegex(res, r"\+0x0*20: *0x0000000000000000\n") - else: - self.assertRegex(res, r"\+0x0.*20: *0x00000000\n") diff --git a/tests/perf/benchmark.py b/tests/perf/benchmark.py index 738f4db3e..2b49b3f39 100644 --- a/tests/perf/benchmark.py +++ b/tests/perf/benchmark.py @@ -4,36 +4,36 @@ import pytest -from tests.utils import gdb_test_python_method, gdb_time_python_method, gdb_start_silent_cmd - - -def time_baseline(benchmark): - benchmark(gdb_test_python_method, "") - - -def time_elf_parsing(benchmark): - benchmark(gdb_test_python_method, "Elf('/bin/ls')") - - -def time_cmd_context(benchmark): - benchmark(gdb_start_silent_cmd, "context") - - -def _time_elf_parsing_using_timeit(): - with pytest.raises(ValueError): - res = gdb_time_python_method( - "Elf('/bin/ls')", - "from __main__ import Elf" - ) - pytest.fail(f"execution_time={res}s") - - -def _time_cmd_context_using_timeit(): - with pytest.raises(ValueError): - res = gdb_time_python_method( - "gdb.execute('context')", - "import gdb", - before=("entry-break",), - number=100 - ) - pytest.fail(f"execution_time={res}s") +from ..base import RemoteGefUnitTestGeneric + + +class BenchmarkBasicApi(RemoteGefUnitTestGeneric): + @pytest.fixture(autouse=True) + def benchmark(self, benchmark): + self.__benchmark = benchmark + + @pytest.mark.benchmark + def test_cmd_context(self): + gdb = self._gdb + gdb.execute("start") + self.__benchmark(gdb.execute, "context") + + @pytest.mark.benchmark + def test_gef_memory_maps(self): + gdb = self._gdb + gdb.execute("start") + gef = self._gef + assert self.__benchmark + + def vmmap(): + return gef.memory.maps + + self.__benchmark(vmmap) + + @pytest.mark.benchmark + def test_elf_parsing(self): + root = self._conn.root + ElfCls = root.eval("Elf") + assert ElfCls + assert self.__benchmark + self.__benchmark(ElfCls, "/bin/ls") diff --git a/tests/regressions/gdbserver_connection.py b/tests/regressions/gdbserver_connection.py index da3784581..6c46d9659 100644 --- a/tests/regressions/gdbserver_connection.py +++ b/tests/regressions/gdbserver_connection.py @@ -1,15 +1,17 @@ +from tests.base import RemoteGefUnitTestGeneric from tests.utils import ( - GefUnitTestGeneric, - gdb_run_cmd, gdbserver_session, ) -class RegressionGdbserverConnection(GefUnitTestGeneric): +class RegressionGdbserverConnection(RemoteGefUnitTestGeneric): def test_can_establish_connection_to_gdbserver_again_after_disconnect(self): """Ensure that gdb can connect to a gdbserver again after disconnecting (PR #896).""" + gdb = self._gdb with gdbserver_session(port=5001) as _, gdbserver_session(port=5002) as _: - buf = gdb_run_cmd("gef-remote 127.0.0.1 5001", - after=["detach", "gef-remote 127.0.0.1 5002", "continue"]) - self.assertNoException(buf) + gdb.execute("gef-remote 127.0.0.1 5001") + gdb.execute("detach") + + gdb.execute("gef-remote 127.0.0.1 5002") + gdb.execute("continue") diff --git a/tests/regressions/registers_register_order.py b/tests/regressions/registers_register_order.py index a8845d6c2..c3a3407aa 100644 --- a/tests/regressions/registers_register_order.py +++ b/tests/regressions/registers_register_order.py @@ -1,43 +1,81 @@ import pytest -from tests.utils import ( - GefUnitTestGeneric, - gdb_start_silent_cmd, - gdb_run_silent_cmd, - debug_target, - ARCH -) +from tests.base import RemoteGefUnitTestGeneric +from tests.utils import debug_target, ARCH -class RegressionRegisterOrder(GefUnitTestGeneric): +class RegressionRegisterOrder(RemoteGefUnitTestGeneric): """Tests for regression in the order registers are displayed by `registers` .""" - @pytest.mark.skipif(ARCH not in ["x86_64", "i686"], reason=f"Skipped for {ARCH}") + @pytest.mark.skipif(ARCH not in ("x86_64", "i686"), reason=f"Skipped for {ARCH}") def test_registers_show_registers_in_correct_order(self): """Ensure the registers are printed in the correct order (PR #670).""" - cmd = "registers" + gdb = self._gdb + if ARCH == "i686": - registers_in_correct_order = ["$eax", "$ebx", "$ecx", "$edx", "$esp", "$ebp", "$esi", - "$edi", "$eip", "$eflags", "$cs"] + registers_in_correct_order = [ + "$eax", + "$ebx", + "$ecx", + "$edx", + "$esp", + "$ebp", + "$esi", + "$edi", + "$eip", + "$eflags", + "$cs", + ] elif ARCH == "x86_64": - registers_in_correct_order = ["$rax", "$rbx", "$rcx", "$rdx", "$rsp", "$rbp", "$rsi", - "$rdi", "$rip", "$r8", "$r9", "$r10", "$r11", "$r12", - "$r13", "$r14", "$r15", "$eflags", "$cs"] + registers_in_correct_order = [ + "$rax", + "$rbx", + "$rcx", + "$rdx", + "$rsp", + "$rbp", + "$rsi", + "$rdi", + "$rip", + "$r8", + "$r9", + "$r10", + "$r11", + "$r12", + "$r13", + "$r14", + "$r15", + "$eflags", + "$cs", + ] else: raise ValueError("Unknown architecture") - lines = gdb_start_silent_cmd(cmd).splitlines()[-len(registers_in_correct_order):] - lines = [line.split()[0].replace(':', '') for line in lines] + + gdb.execute("start") + lines = gdb.execute("registers", to_string=True).splitlines() + lines = lines[-len(registers_in_correct_order) :] + lines = [line.split()[0].replace(":", "") for line in lines] self.assertEqual(registers_in_correct_order, lines) - @pytest.mark.skipif(ARCH not in ["x86_64",], reason=f"Skipped for {ARCH}") +class RegressionRegisterOrderNested(RemoteGefUnitTestGeneric): + def setUp(self) -> None: + self._target = debug_target("nested") + return super().setUp() + + @pytest.mark.skipif( + ARCH not in ("x86_64",), + reason=f"Skipped for {ARCH}", + ) def test_context_correct_registers_refresh_with_frames(self): """Ensure registers are correctly refreshed when changing frame (PR #668)""" - target = debug_target("nested") - lines = gdb_run_silent_cmd("registers", after=["frame 5", "registers"], - target=target).splitlines() - rips = [x for x in lines if x.startswith("$rip")] - self.assertEqual(len(rips), 2) # we must have only 2 entries - self.assertNotEqual(rips[0], rips[1]) # they must be different - self.assertIn("=5 diff --git a/tests/utils.py b/tests/utils.py index 371d953e6..fc35c77b1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,14 +8,14 @@ import pathlib import platform import re +import struct import subprocess import tempfile import time -import unittest -import warnings -from typing import Dict, Iterable, List, Optional, Union +from typing import Iterable, List, Optional, Union from urllib.request import urlopen + TMPDIR = pathlib.Path(tempfile.gettempdir()) ARCH = (os.getenv("GEF_CI_ARCH") or platform.machine()).lower() BIN_SH = pathlib.Path("/bin/sh") @@ -27,72 +27,39 @@ DEFAULT_TARGET = TMPDIR / "default.out" GEF_DEFAULT_PROMPT = "gef➤ " GEF_DEFAULT_TEMPDIR = "/tmp/gef" -GEF_PATH = pathlib.Path(os.getenv("GEF_PATH", "gef.py")) +GEF_PATH = pathlib.Path(os.getenv("GEF_PATH", "gef.py")).absolute() STRIP_ANSI_DEFAULT = True GDBSERVER_DEFAULT_HOST = "localhost" GDBSERVER_DEFAULT_PORT = 1234 +GEF_RIGHT_ARROW = " → " + CommandType = Union[str, Iterable[str]] + +ERROR_INACTIVE_SESSION_MESSAGE = "[*] No debugging session active\n" +WARNING_DEPRECATION_MESSAGE = "is deprecated and will be removed in a feature release." + + class Color(enum.Enum): """Used to colorify terminal output.""" - NORMAL = "\x1b[0m" - GRAY = "\x1b[1;38;5;240m" - LIGHT_GRAY = "\x1b[0;37m" - RED = "\x1b[31m" - GREEN = "\x1b[32m" - YELLOW = "\x1b[33m" - BLUE = "\x1b[34m" - PINK = "\x1b[35m" - CYAN = "\x1b[36m" - BOLD = "\x1b[1m" - UNDERLINE = "\x1b[4m" - UNDERLINE_OFF = "\x1b[24m" - HIGHLIGHT = "\x1b[3m" - HIGHLIGHT_OFF = "\x1b[23m" - BLINK = "\x1b[5m" - BLINK_OFF = "\x1b[25m" - - -class GdbAssertionError(AssertionError): - pass - - -class GefUnitTestGeneric(unittest.TestCase): - """Generic class for command testing, that defines all helpers""" - - @staticmethod - def assertException(buf): - """Assert that GEF raised an Exception.""" - if not ("Python Exception <" in buf - or "Traceback" in buf - or "'gdb.error'" in buf - or "Exception raised" in buf - or "failed to execute properly, reason:" in buf): - raise GdbAssertionError("GDB Exception expected, not raised") - - @staticmethod - def assertNoException(buf): - """Assert that no Exception was raised from GEF.""" - if ("Python Exception <" in buf - or "Traceback" in buf - or "'gdb.error'" in buf - or "Exception raised" in buf - or "failed to execute properly, reason:" in buf): - raise GdbAssertionError(f"Unexpected GDB Exception raised in {buf}") - - if "is deprecated and will be removed in a feature release." in buf: - lines = [l for l in buf.splitlines() - if "is deprecated and will be removed in a feature release." in l] - deprecated_api_names = {x.split()[1] for x in lines} - warnings.warn( - UserWarning(f"Use of deprecated API(s): {', '.join(deprecated_api_names)}") - ) - - @staticmethod - def assertFailIfInactiveSession(buf): - if "No debugging session active" not in buf: - raise AssertionError("No debugging session inactive warning") + + NORMAL = "\x1b[0m" + GRAY = "\x1b[1;38;5;240m" + LIGHT_GRAY = "\x1b[0;37m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + PINK = "\x1b[35m" + CYAN = "\x1b[36m" + BOLD = "\x1b[1m" + UNDERLINE = "\x1b[4m" + UNDERLINE_OFF = "\x1b[24m" + HIGHLIGHT = "\x1b[3m" + HIGHLIGHT_OFF = "\x1b[23m" + BLINK = "\x1b[5m" + BLINK_OFF = "\x1b[25m" def is_64b() -> bool: @@ -103,125 +70,6 @@ def is_32b() -> bool: return ARCH in CI_VALID_ARCHITECTURES_32B -def ansi_clean(s: str) -> str: - ansi_escape = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]") - return ansi_escape.sub("", s) - - -def gdb_run_cmd(cmd: CommandType, before: CommandType = (), after: CommandType = (), - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi: bool = STRIP_ANSI_DEFAULT) -> str: - """Execute a command inside GDB. `before` and `after` are lists of commands to be executed - before (resp. after) the command to test.""" - - def _add_command(commands: CommandType) -> List[str]: - if isinstance(commands, str): - commands = [commands] - return [_str for cmd in commands for _str in ["-ex", cmd]] - - command = ["gdb", "-q", "-nx"] - if COVERAGE_DIR: - coverage_file = pathlib.Path(COVERAGE_DIR) / os.getenv("PYTEST_XDIST_WORKER", "gw0") - command += _add_command([ - "pi from coverage import Coverage", - f"pi cov = Coverage(data_file=\"{coverage_file}\"," - "auto_data=True, branch=True)", - "pi cov.start()", - ]) - command += _add_command([ - f"source {GEF_PATH}", - "gef config gef.debug True", - ]) - command += _add_command(before) - command += _add_command(cmd) - command += _add_command(after) - if COVERAGE_DIR: - command += _add_command(["pi cov.stop()", "pi cov.save()"]) - command += ["-ex", "quit", "--", str(target)] - - lines = subprocess.check_output(command, stderr=subprocess.STDOUT).strip().splitlines() - output = b"\n".join(lines) - result = None - - # The following is necessary because ANSI escape sequences might have been - # added in the middle of multibyte characters, e.g. \x1b[H\x1b[2J is added - # into the middle of \xe2\x94\x80 to become \xe2\x1b[H\x1b[2J\x94\x80 which - # causes a UnicodeDecodeError when trying to decode \xe2. Such broken - # multibyte characters would need to be removed, otherwise the test will - # result in an error. - while not result: - try: - result = output.decode("utf-8") - except UnicodeDecodeError as ude: - faulty_idx_start = int(ude.start) - faulty_idx_end = int(ude.end) - output = output[:faulty_idx_start] + output[faulty_idx_end:] - - if strip_ansi: - result = ansi_clean(result) - - return result - - -def gdb_run_silent_cmd(cmd: CommandType, before: CommandType = (), after: CommandType = (), - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi: bool = STRIP_ANSI_DEFAULT) -> str: - """Disable the output and run entirely the `target` binary.""" - before = [*before, "gef config context.clear_screen False", - "gef config context.layout '-code -stack'", - "run"] - return gdb_run_cmd(cmd, before, after, target, strip_ansi) - - -def gdb_run_cmd_last_line(cmd: CommandType, before: CommandType = (), after: CommandType = (), - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi: bool = STRIP_ANSI_DEFAULT) -> str: - """Execute a command in GDB, and return only the last line of its output.""" - return gdb_run_cmd(cmd, before, after, target, strip_ansi).splitlines()[-1] - - -def gdb_start_silent_cmd(cmd: CommandType, before: CommandType = (), after: CommandType = (), - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi: bool = STRIP_ANSI_DEFAULT, - context: str = DEFAULT_CONTEXT) -> str: - """Execute a command in GDB by starting an execution context. This command - disables the `context` and sets a tbreak at the most convenient entry - point.""" - before = [*before, "gef config context.clear_screen False", - f"gef config context.layout '{context}'", - "entry-break"] - return gdb_run_cmd(cmd, before, after, target, strip_ansi) - - -def gdb_start_silent_cmd_last_line(cmd: CommandType, before: CommandType = (), - after: CommandType = (), - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi=STRIP_ANSI_DEFAULT) -> str: - """Execute `gdb_start_silent_cmd()` and return only the last line of its output.""" - return gdb_start_silent_cmd(cmd, before, after, target, strip_ansi).splitlines()[-1] - - -def gdb_test_python_method(meth: str, before: str = "", after: str = "", - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi: bool = STRIP_ANSI_DEFAULT) -> str: - brk = before + ";" if before else "" - cmd = f"pi {brk}print({meth});{after}" - return gdb_start_silent_cmd(cmd, target=target, strip_ansi=strip_ansi) - - -def gdb_time_python_method(meth: str, setup: str, - py_before: str = "", py_after: str = "", - before: CommandType = (), after: CommandType = (), - target: pathlib.Path = DEFAULT_TARGET, - strip_ansi: bool = STRIP_ANSI_DEFAULT, number: int = 1000) -> float: - brk = py_before + ";" if py_before else "" - cmd = f"""pi import timeit;{brk}print(timeit.timeit("{meth}", """\ - f"""setup="{setup}", number={number}));{py_after}""" - lines = gdb_run_cmd(cmd, before=before, after=after, - target=target, strip_ansi=strip_ansi).splitlines() - return float(lines[-1]) - - def debug_target(name: str, extension: str = ".out") -> pathlib.Path: target = TMPDIR / f"{name}{extension}" if not target.exists(): @@ -231,9 +79,11 @@ def debug_target(name: str, extension: str = ".out") -> pathlib.Path: return target -def start_gdbserver(exe: Union[str, pathlib.Path] = debug_target("default"), - host: str = GDBSERVER_DEFAULT_HOST, - port: int = GDBSERVER_DEFAULT_PORT) -> subprocess.Popen: +def start_gdbserver( + exe: Union[str, pathlib.Path] = debug_target("default"), + host: str = GDBSERVER_DEFAULT_HOST, + port: int = GDBSERVER_DEFAULT_PORT, +) -> subprocess.Popen: """Start a gdbserver on the target binary. Args: @@ -243,8 +93,11 @@ def start_gdbserver(exe: Union[str, pathlib.Path] = debug_target("default"), Returns: subprocess.Popen: a Popen object for the gdbserver process. """ - return subprocess.Popen(["gdbserver", f"{host}:{port}", exe], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return subprocess.Popen( + ["gdbserver", f"{host}:{port}", exe], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) def stop_gdbserver(gdbserver: subprocess.Popen) -> None: @@ -260,22 +113,28 @@ def stop_gdbserver(gdbserver: subprocess.Popen) -> None: @contextlib.contextmanager -def gdbserver_session(*args, **kwargs): - exe = kwargs.get("exe", "") or debug_target("default") - host = kwargs.get("host", GDBSERVER_DEFAULT_HOST) - port = kwargs.get("port", GDBSERVER_DEFAULT_PORT) +def gdbserver_session( + port: int = GDBSERVER_DEFAULT_PORT, + host: str = GDBSERVER_DEFAULT_HOST, + exe: Union[str, pathlib.Path] = debug_target("default"), +): sess = start_gdbserver(exe, host, port) try: - time.sleep(1) # forced delay to allow gdbserver to start listening + time.sleep(1) # forced delay to allow gdbserver to start listening yield sess finally: stop_gdbserver(sess) -def start_qemuuser(exe: Union[str, pathlib.Path] = debug_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 start_qemuuser( + exe: Union[str, pathlib.Path] = debug_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: @@ -295,7 +154,6 @@ def qemuuser_session(*args, **kwargs): stop_qemuuser(sess) - def find_symbol(binary: pathlib.Path, symbol: str) -> int: """Find a symbol by name in a ELF binary using `objdump`. The expect output syntax for `objdump` is: @@ -316,9 +174,13 @@ def find_symbol(binary: pathlib.Path, symbol: str) -> int: KeyError if the symbol is not found """ name = symbol.encode("utf8") - for line in [x.strip().split() for x in subprocess.check_output(["objdump", "-t", binary]).splitlines() if len(x.strip())]: - if line[-1] == name: - return int(line[0], 0x10) + for line in [ + x.strip().split() + for x in subprocess.check_output(["objdump", "-t", binary]).splitlines() + if len(x.strip()) + ]: + if line[-1] == name: + return int(line[0], 0x10) raise KeyError(f"`{symbol}` not found in {binary}") @@ -333,11 +195,7 @@ def findlines(substring: str, buffer: str) -> List[str]: Returns: List[str] """ - return [ - line.strip() - for line in buffer.splitlines() - if substring in line.strip() - ] + return [line.strip() for line in buffer.splitlines() if substring in line.strip()] def removeafter(substring: str, buffer: str, included: bool = False) -> str: @@ -384,7 +242,6 @@ def removeuntil(substring: str, buffer: str, included: bool = False) -> str: return buffer[idx:] - def download_file(url: str) -> Optional[bytes]: """Download a file from the internet. @@ -399,3 +256,35 @@ def download_file(url: str) -> Optional[bytes]: return http.read() if http.getcode() == 200 else None except Exception: return None + + +def u8(x: bytes) -> int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + return struct.pack("