From 6add28dba1ad14c25e707be79f776476813b14c3 Mon Sep 17 00:00:00 2001 From: gurkenhabicht Date: Mon, 24 Jul 2023 20:53:54 +0200 Subject: [PATCH] added gdb --- gdb/.gdbinit | 1 + gdb/.gdbinit-gef.py | 11097 ++++++++++++++++++++++++++++++++++++++++++ vim/.vimrc | 5 + 3 files changed, 11103 insertions(+) create mode 100644 gdb/.gdbinit create mode 100644 gdb/.gdbinit-gef.py diff --git a/gdb/.gdbinit b/gdb/.gdbinit new file mode 100644 index 0000000..4addd5b --- /dev/null +++ b/gdb/.gdbinit @@ -0,0 +1 @@ +source ~/.gdbinit-gef.py diff --git a/gdb/.gdbinit-gef.py b/gdb/.gdbinit-gef.py new file mode 100644 index 0000000..1a409c3 --- /dev/null +++ b/gdb/.gdbinit-gef.py @@ -0,0 +1,11097 @@ +####################################################################################### +# GEF - Multi-Architecture GDB Enhanced Features for Exploiters & Reverse-Engineers +# +# by @_hugsy_ +####################################################################################### +# +# GEF is a kick-ass set of commands for X86, ARM, MIPS, PowerPC and SPARC to +# make GDB cool again for exploit dev. It is aimed to be used mostly by exploit +# devs and reversers, to provides additional features to GDB using the Python +# API to assist during the process of dynamic analysis. +# +# GEF fully relies on GDB API and other Linux-specific sources of information +# (such as /proc/). As a consequence, some of the features might not work +# on custom or hardened systems such as GrSec. +# +# Since January 2020, GEF solely support GDB compiled with Python3 and was tested on +# * x86-32 & x86-64 +# * arm v5,v6,v7 +# * aarch64 (armv8) +# * mips & mips64 +# * powerpc & powerpc64 +# * sparc & sparc64(v9) +# +# For GEF with Python2 (only) support was moved to the GEF-Legacy +# (https://github.com/hugsy/gef-legacy) +# +# To start: in gdb, type `source /path/to/gef.py` +# +####################################################################################### +# +# gef is distributed under the MIT License (MIT) +# Copyright (c) 2013-2022 crazy rabbidz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import abc +import argparse +import ast +import atexit +import binascii +import codecs +import collections +import configparser +import ctypes +import enum +import functools +import hashlib +import importlib +import importlib.util +import inspect +import itertools +import os +import pathlib +import platform +import re +import shutil +import site +import socket +import string +import struct +import subprocess +import sys +import tempfile +import time +import traceback +import warnings +from functools import lru_cache +from io import StringIO, TextIOWrapper +from types import ModuleType +from typing import (Any, ByteString, Callable, Dict, Generator, Iterable, + Iterator, List, NoReturn, Optional, Sequence, Set, Tuple, Type, + Union) +from urllib.request import urlopen + +GEF_DEFAULT_BRANCH = "main" +GEF_EXTRAS_DEFAULT_BRANCH = "main" + +def http_get(url: str) -> Optional[bytes]: + """Basic HTTP wrapper for GET request. Return the body of the page if HTTP code is OK, + otherwise return None.""" + try: + http = urlopen(url) + return http.read() if http.getcode() == 200 else None + except Exception: + return None + + +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 = "dev" if "--dev" in argv else 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 + + +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("[-] gef cannot run as standalone") + sys.exit(0) + + +GDB_MIN_VERSION = (8, 0) +GDB_VERSION = tuple(map(int, re.search(r"(\d+)[^\d]+(\d+)", gdb.VERSION).groups())) +PYTHON_MIN_VERSION = (3, 6) +PYTHON_VERSION = sys.version_info[0:2] + +DEFAULT_PAGE_ALIGN_SHIFT = 12 +DEFAULT_PAGE_SIZE = 1 << DEFAULT_PAGE_ALIGN_SHIFT + +GEF_RC = (pathlib.Path(os.getenv("GEF_RC", "")).absolute() + if os.getenv("GEF_RC") + else pathlib.Path().home() / ".gef.rc") +GEF_TEMP_DIR = os.path.join(tempfile.gettempdir(), "gef") +GEF_MAX_STRING_LENGTH = 50 + +LIBC_HEAP_MAIN_ARENA_DEFAULT_NAME = "main_arena" +ANSI_SPLIT_RE = r"(\033\[[\d;]*m)" + +LEFT_ARROW = " ← " +RIGHT_ARROW = " → " +DOWN_ARROW = "↳" +HORIZONTAL_LINE = "─" +VERTICAL_LINE = "│" +CROSS = "✘ " +TICK = "✓ " +BP_GLYPH = "●" +GEF_PROMPT = "gef➤ " +GEF_PROMPT_ON = f"\001\033[1;32m\002{GEF_PROMPT}\001\033[0m\002" +GEF_PROMPT_OFF = f"\001\033[1;31m\002{GEF_PROMPT}\001\033[0m\002" + +PATTERN_LIBC_VERSION = re.compile(rb"glibc (\d+)\.(\d+)") + +gef : "Gef" +__registered_commands__ : Set[Type["GenericCommand"]] = set() +__registered_functions__ : Set[Type["GenericFunction"]] = set() +__registered_architectures__ : Dict[Union["Elf.Abi", str], Type["Architecture"]] = {} +__registered_file_formats__ : Set[ Type["FileFormat"] ] = set() + + +def reset_all_caches() -> None: + """Free all caches. If an object is cached, it will have a callable attribute `cache_clear` + which will be invoked to purge the function cache.""" + + for mod in dir(sys.modules["__main__"]): + obj = getattr(sys.modules["__main__"], mod) + if hasattr(obj, "cache_clear"): + obj.cache_clear() + + gef.reset_caches() + return + + +def reset() -> None: + global gef + + arch = None + if "gef" in locals().keys(): + reset_all_caches() + arch = gef.arch + del gef + + gef = Gef() + gef.setup() + + if arch: + gef.arch = arch + return + + +def highlight_text(text: str) -> str: + """ + Highlight text using `gef.ui.highlight_table` { match -> color } settings. + + If RegEx is enabled it will create a match group around all items in the + `gef.ui.highlight_table` and wrap the specified color in the `gef.ui.highlight_table` + around those matches. + + If RegEx is disabled, split by ANSI codes and 'colorify' each match found + within the specified string. + """ + global gef + + if not gef.ui.highlight_table: + return text + + if gef.config["highlight.regex"]: + for match, color in gef.ui.highlight_table.items(): + text = re.sub("(" + match + ")", Color.colorify("\\1", color), text) + return text + + ansiSplit = re.split(ANSI_SPLIT_RE, text) + + for match, color in gef.ui.highlight_table.items(): + for index, val in enumerate(ansiSplit): + found = val.find(match) + if found > -1: + ansiSplit[index] = val.replace(match, Color.colorify(match, color)) + break + text = "".join(ansiSplit) + ansiSplit = re.split(ANSI_SPLIT_RE, text) + + return "".join(ansiSplit) + + +def gef_print(*args: str, end="\n", sep=" ", **kwargs: Any) -> None: + """Wrapper around print(), using string buffering feature.""" + parts = [highlight_text(a) for a in args] + if buffer_output() and gef.ui.stream_buffer and not is_debug(): + gef.ui.stream_buffer.write(sep.join(parts) + end) + return + + print(*parts, sep=sep, end=end, **kwargs) + return + + +def bufferize(f: Callable) -> Callable: + """Store the content to be printed for a function in memory, and flush it on function exit.""" + + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + global gef + + if gef.ui.stream_buffer: + return f(*args, **kwargs) + + gef.ui.stream_buffer = StringIO() + try: + rv = f(*args, **kwargs) + finally: + redirect = gef.config["context.redirect"] + if redirect.startswith("/dev/pts/"): + if not gef.ui.redirect_fd: + # if the FD has never been open, open it + fd = open(redirect, "wt") + gef.ui.redirect_fd = fd + elif redirect != gef.ui.redirect_fd.name: + # if the user has changed the redirect setting during runtime, update the state + gef.ui.redirect_fd.close() + fd = open(redirect, "wt") + gef.ui.redirect_fd = fd + else: + # otherwise, keep using it + fd = gef.ui.redirect_fd + else: + fd = sys.stdout + gef.ui.redirect_fd = None + + if gef.ui.redirect_fd and fd.closed: + # if the tty was closed, revert back to stdout + fd = sys.stdout + gef.ui.redirect_fd = None + gef.config["context.redirect"] = "" + + fd.write(gef.ui.stream_buffer.getvalue()) + fd.flush() + gef.ui.stream_buffer = None + return rv + + return wrapper + + +# +# Helpers +# + +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 + return struct.pack(f"{endian}B", x) if not s else struct.pack(f"{endian:s}b", x) + + +def p16(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: + """Pack one word respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.pack(f"{endian}H", x) if not s else struct.pack(f"{endian:s}h", x) + + +def p32(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: + """Pack one dword respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.pack(f"{endian}I", x) if not s else struct.pack(f"{endian:s}i", x) + + +def p64(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: + """Pack one qword respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.pack(f"{endian}Q", x) if not s else struct.pack(f"{endian:s}q", x) + + +def u8(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: + """Unpack one byte respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.unpack(f"{endian}B", x)[0] if not s else struct.unpack(f"{endian:s}b", x)[0] + + +def u16(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: + """Unpack one word respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.unpack(f"{endian}H", x)[0] if not s else struct.unpack(f"{endian:s}h", x)[0] + + +def u32(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: + """Unpack one dword respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.unpack(f"{endian}I", x)[0] if not s else struct.unpack(f"{endian:s}i", x)[0] + + +def u64(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: + """Unpack one qword respecting the current architecture endianness.""" + endian = e or gef.arch.endianness + return struct.unpack(f"{endian}Q", x)[0] if not s else struct.unpack(f"{endian:s}q", x)[0] + + +def is_ascii_string(address: int) -> bool: + """Helper function to determine if the buffer pointed by `address` is an ASCII string (in GDB)""" + try: + return gef.memory.read_ascii_string(address) is not None + except Exception: + return False + + +def is_alive() -> bool: + """Check if GDB is running.""" + try: + return gdb.selected_inferior().pid > 0 + except Exception: + return False + + +def calling_function() -> Optional[str]: + """Return the name of the calling function""" + try: + stack_info = traceback.extract_stack()[-3] + return stack_info.name + except: + return None + + +# +# Decorators +# +def only_if_gdb_running(f: Callable) -> Callable: + """Decorator wrapper to check if GDB is running.""" + + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + if is_alive(): + return f(*args, **kwargs) + else: + warn("No debugging session active") + + return wrapper + + +def only_if_gdb_target_local(f: Callable) -> Callable: + """Decorator wrapper to check if GDB is running locally (target not remote).""" + + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + if not is_remote_debug(): + return f(*args, **kwargs) + else: + warn("This command cannot work for remote sessions.") + + return wrapper + + +def deprecated(solution: str = "") -> Callable: + """Decorator to add a warning when a command is obsolete and will be removed.""" + def decorator(f: Callable) -> Callable: + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + caller = inspect.stack()[1] + caller_file = pathlib.Path(caller.filename) + caller_loc = caller.lineno + msg = f"{caller_file.name}:L{caller_loc} '{f.__name__}' is deprecated and will be removed in a feature release. " + if not gef: + print(msg) + elif gef.config["gef.show_deprecation_warnings"] is True: + if solution: + msg += solution + warn(msg) + return f(*args, **kwargs) + + if not wrapper.__doc__: + wrapper.__doc__ = "" + wrapper.__doc__ += f"\r\n`{f.__name__}` is **DEPRECATED** and will be removed in the future.\r\n{solution}" + return wrapper + return decorator + + +def experimental_feature(f: Callable) -> Callable: + """Decorator to add a warning when a feature is experimental.""" + + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + warn("This feature is under development, expect bugs and unstability...") + return f(*args, **kwargs) + + return wrapper + + +def only_if_events_supported(event_type: str) -> Callable: + """Checks if GDB supports events without crashing.""" + def wrap(f: Callable) -> Callable: + def wrapped_f(*args: Any, **kwargs: Any) -> Any: + if getattr(gdb, "events") and getattr(gdb.events, event_type): + return f(*args, **kwargs) + warn("GDB events cannot be set") + return wrapped_f + return wrap + + +class classproperty(property): + """Make the attribute a `classproperty`.""" + def __get__(self, cls, owner): + return classmethod(self.fget).__get__(None, owner)() + + +def FakeExit(*args: Any, **kwargs: Any) -> NoReturn: + raise RuntimeWarning + + +sys.exit = FakeExit + + +def parse_arguments(required_arguments: Dict[Union[str, Tuple[str, str]], Any], + optional_arguments: Dict[Union[str, Tuple[str, str]], Any]) -> Callable: + """Argument parsing decorator.""" + + def int_wrapper(x: str) -> int: return int(x, 0) + + def decorator(f: Callable) -> Optional[Callable]: + def wrapper(*args: Any, **kwargs: Any) -> Callable: + parser = argparse.ArgumentParser(prog=args[0]._cmdline_, add_help=True) + for argname in required_arguments: + argvalue = required_arguments[argname] + argtype = type(argvalue) + if argtype is int: + argtype = int_wrapper + + argname_is_list = not isinstance(argname, str) + assert not argname_is_list and isinstance(argname, str) + if not argname_is_list and argname.startswith("-"): + # optional args + if argtype is bool: + parser.add_argument(argname, action="store_true" if argvalue else "store_false") + else: + parser.add_argument(argname, type=argtype, required=True, default=argvalue) + else: + if argtype in (list, tuple): + nargs = "*" + argtype = type(argvalue[0]) + else: + nargs = "?" + # positional args + parser.add_argument(argname, type=argtype, default=argvalue, nargs=nargs) + + for argname in optional_arguments: + if isinstance(argname, str) and not argname.startswith("-"): + # refuse positional arguments + continue + argvalue = optional_arguments[argname] + argtype = type(argvalue) + if isinstance(argname, str): + argname = [argname,] + if argtype is int: + argtype = int_wrapper + if argtype is bool: + parser.add_argument(*argname, action="store_true" if argvalue else "store_false") + else: + parser.add_argument(*argname, type=argtype, default=argvalue) + + parsed_args = parser.parse_args(*(args[1:])) + kwargs["arguments"] = parsed_args + return f(*args, **kwargs) + return wrapper + return decorator + + +class Color: + """Used to colorify terminal output.""" + colors = { + "normal" : "\033[0m", + "gray" : "\033[1;38;5;240m", + "light_gray" : "\033[0;37m", + "red" : "\033[31m", + "green" : "\033[32m", + "yellow" : "\033[33m", + "blue" : "\033[34m", + "pink" : "\033[35m", + "cyan" : "\033[36m", + "bold" : "\033[1m", + "underline" : "\033[4m", + "underline_off" : "\033[24m", + "highlight" : "\033[3m", + "highlight_off" : "\033[23m", + "blink" : "\033[5m", + "blink_off" : "\033[25m", + } + + @staticmethod + def redify(msg: str) -> str: return Color.colorify(msg, "red") + @staticmethod + def greenify(msg: str) -> str: return Color.colorify(msg, "green") + @staticmethod + def blueify(msg: str) -> str: return Color.colorify(msg, "blue") + @staticmethod + def yellowify(msg: str) -> str: return Color.colorify(msg, "yellow") + @staticmethod + def grayify(msg: str) -> str: return Color.colorify(msg, "gray") + @staticmethod + def light_grayify(msg: str) -> str: return Color.colorify(msg, "light_gray") + @staticmethod + def pinkify(msg: str) -> str: return Color.colorify(msg, "pink") + @staticmethod + def cyanify(msg: str) -> str: return Color.colorify(msg, "cyan") + @staticmethod + def boldify(msg: str) -> str: return Color.colorify(msg, "bold") + @staticmethod + def underlinify(msg: str) -> str: return Color.colorify(msg, "underline") + @staticmethod + def highlightify(msg: str) -> str: return Color.colorify(msg, "highlight") + @staticmethod + def blinkify(msg: str) -> str: return Color.colorify(msg, "blink") + + @staticmethod + def colorify(text: str, attrs: str) -> str: + """Color text according to the given attributes.""" + if gef.config["gef.disable_color"] is True: return text + + colors = Color.colors + msg = [colors[attr] for attr in attrs.split() if attr in colors] + msg.append(str(text)) + if colors["highlight"] in msg: msg.append(colors["highlight_off"]) + if colors["underline"] in msg: msg.append(colors["underline_off"]) + if colors["blink"] in msg: msg.append(colors["blink_off"]) + msg.append(colors["normal"]) + return "".join(msg) + + +class Address: + """GEF representation of memory addresses.""" + def __init__(self, **kwargs: Any) -> None: + self.value: int = kwargs.get("value", 0) + self.section: "Section" = kwargs.get("section", None) + self.info: "Zone" = kwargs.get("info", None) + return + + def __str__(self) -> str: + value = format_address(self.value) + code_color = gef.config["theme.address_code"] + stack_color = gef.config["theme.address_stack"] + heap_color = gef.config["theme.address_heap"] + if self.is_in_text_segment(): + return Color.colorify(value, code_color) + if self.is_in_heap_segment(): + return Color.colorify(value, heap_color) + if self.is_in_stack_segment(): + return Color.colorify(value, stack_color) + return value + + def __int__(self) -> int: + return self.value + + def is_in_text_segment(self) -> bool: + return (hasattr(self.info, "name") and ".text" in self.info.name) or \ + (hasattr(self.section, "path") and get_filepath() == self.section.path and self.section.is_executable()) + + def is_in_stack_segment(self) -> bool: + return hasattr(self.section, "path") and "[stack]" == self.section.path + + def is_in_heap_segment(self) -> bool: + return hasattr(self.section, "path") and "[heap]" == self.section.path + + def dereference(self) -> Optional[int]: + addr = align_address(int(self.value)) + derefed = dereference(addr) + return None if derefed is None else int(derefed) + + @property + def valid(self) -> bool: + return any(map(lambda x: x.page_start <= self.value < x.page_end, gef.memory.maps)) + + +class Permission(enum.Flag): + """GEF representation of Linux permission.""" + NONE = 0 + EXECUTE = 1 + WRITE = 2 + READ = 4 + ALL = 7 + + def __str__(self) -> str: + perm_str = "" + perm_str += "r" if self & Permission.READ else "-" + perm_str += "w" if self & Permission.WRITE else "-" + perm_str += "x" if self & Permission.EXECUTE else "-" + return perm_str + + @classmethod + def from_info_sections(cls, *args: str) -> "Permission": + perm = cls(0) + for arg in args: + if "READONLY" in arg: perm |= Permission.READ + if "DATA" in arg: perm |= Permission.WRITE + if "CODE" in arg: perm |= Permission.EXECUTE + return perm + + @classmethod + def from_process_maps(cls, perm_str: str) -> "Permission": + perm = cls(0) + if perm_str[0] == "r": perm |= Permission.READ + if perm_str[1] == "w": perm |= Permission.WRITE + if perm_str[2] == "x": perm |= Permission.EXECUTE + return perm + + @classmethod + def from_info_mem(cls, perm_str: str) -> "Permission": + perm = cls(0) + # perm_str[0] shows if this is a user page, which + # we don't track + if perm_str[1] == "r": perm |= Permission.READ + if perm_str[2] == "w": perm |= Permission.WRITE + return perm + + +class Section: + """GEF representation of process memory sections.""" + + def __init__(self, **kwargs: Any) -> None: + self.page_start: int = kwargs.get("page_start", 0) + self.page_end: int = kwargs.get("page_end", 0) + self.offset: int = kwargs.get("offset", 0) + self.permission: Permission = kwargs.get("permission", Permission(0)) + self.inode: int = kwargs.get("inode", 0) + self.path: str = kwargs.get("path", "") + return + + def is_readable(self) -> bool: + return (self.permission & Permission.READ) != 0 + + def is_writable(self) -> bool: + return (self.permission & Permission.WRITE) != 0 + + def is_executable(self) -> bool: + return (self.permission & Permission.EXECUTE) != 0 + + @property + def size(self) -> int: + if self.page_end is None or self.page_start is None: + return -1 + return self.page_end - self.page_start + + @property + def realpath(self) -> str: + # when in a `gef-remote` session, realpath returns the path to the binary on the local disk, not remote + return self.path if gef.session.remote is None else f"/tmp/gef/{gef.session.remote:d}/{self.path}" + + def __str__(self) -> str: + return (f"Section(page_start={self.page_start:#x}, page_end={self.page_end:#x}, " + f"permissions={self.permission!s})") + + +Zone = collections.namedtuple("Zone", ["name", "zone_start", "zone_end", "filename"]) + + +class Endianness(enum.Enum): + LITTLE_ENDIAN = 1 + BIG_ENDIAN = 2 + + def __str__(self) -> str: + return "<" if self == Endianness.LITTLE_ENDIAN else ">" + + def __repr__(self) -> str: + return self.name + + def __int__(self) -> int: + return self.value + + +class FileFormatSection: + misc: Any + + +class FileFormat: + name: str + path: pathlib.Path + entry_point: int + checksec: Dict[str, bool] + sections: List[FileFormatSection] + + def __init__(self, path: Union[str, pathlib.Path]) -> None: + raise NotImplemented + + def __init_subclass__(cls: Type["FileFormat"], **kwargs): + global __registered_file_formats__ + super().__init_subclass__(**kwargs) + required_attributes = ("name", "entry_point", "is_valid", "checksec",) + for attr in required_attributes: + if not hasattr(cls, attr): + raise NotImplementedError(f"File format '{cls.__name__}' is invalid: missing attribute '{attr}'") + __registered_file_formats__.add(cls) + return + + @classmethod + def is_valid(cls, path: pathlib.Path) -> bool: + raise NotImplemented + + def __str__(self) -> str: + return f"{self.name}('{self.path.absolute()}', entry @ {self.entry_point:#x})" + + +class Elf(FileFormat): + """Basic ELF parsing. + Ref: + - http://www.skyfree.org/linux/references/ELF_Format.pdf + - https://refspecs.linuxfoundation.org/elf/elfspec_ppc.pdf + - https://refspecs.linuxfoundation.org/ELF/ppc64/PPC-elf64abi.html + """ + class Class(enum.Enum): + ELF_32_BITS = 0x01 + ELF_64_BITS = 0x02 + + ELF_MAGIC = 0x7f454c46 + + class Abi(enum.Enum): + X86_64 = 0x3e + X86_32 = 0x03 + ARM = 0x28 + MIPS = 0x08 + POWERPC = 0x14 + POWERPC64 = 0x15 + SPARC = 0x02 + SPARC64 = 0x2b + AARCH64 = 0xb7 + RISCV = 0xf3 + IA64 = 0x32 + M68K = 0x04 + + class Type(enum.Enum): + ET_RELOC = 1 + ET_EXEC = 2 + ET_DYN = 3 + ET_CORE = 4 + + class OsAbi(enum.Enum): + SYSTEMV = 0x00 + HPUX = 0x01 + NETBSD = 0x02 + LINUX = 0x03 + SOLARIS = 0x06 + AIX = 0x07 + IRIX = 0x08 + FREEBSD = 0x09 + OPENBSD = 0x0C + + e_magic: int = ELF_MAGIC + e_class: "Elf.Class" = Class.ELF_32_BITS + e_endianness: Endianness = Endianness.LITTLE_ENDIAN + e_eiversion: int + e_osabi: "Elf.OsAbi" + e_abiversion: int + e_pad: bytes + e_type: "Elf.Type" = Type.ET_EXEC + e_machine: Abi = Abi.X86_32 + e_version: int + e_entry: int + e_phoff: int + e_shoff: int + e_flags: int + e_ehsize: int + e_phentsize: int + e_phnum: int + e_shentsize: int + e_shnum: int + e_shstrndx: int + + path: pathlib.Path + phdrs : List["Phdr"] + shdrs : List["Shdr"] + name: str = "ELF" + + __checksec : Dict[str, bool] + + def __init__(self, path: Union[str, pathlib.Path]) -> None: + """Instantiate an ELF object. A valid ELF must be provided, or an exception will be thrown.""" + + if isinstance(path, str): + self.path = pathlib.Path(path).expanduser() + elif isinstance(path, pathlib.Path): + self.path = path + else: + raise TypeError + + if not self.path.exists(): + raise FileNotFoundError(f"'{self.path}' not found/readable, most gef features will not work") + + self.__checksec = {} + + with self.path.open("rb") as self.fd: + # off 0x0 + self.e_magic, e_class, e_endianness, self.e_eiversion = self.read_and_unpack(">IBBB") + if self.e_magic != Elf.ELF_MAGIC: + # The ELF is corrupted, GDB won't handle it, no point going further + raise RuntimeError("Not a valid ELF file (magic)") + + self.e_class, self.e_endianness = Elf.Class(e_class), Endianness(e_endianness) + + if self.e_endianness != gef.arch.endianness: + warn("Unexpected endianness for architecture") + + endian = self.e_endianness + + # off 0x7 + e_osabi, self.e_abiversion = self.read_and_unpack(f"{endian}BB") + self.e_osabi = Elf.OsAbi(e_osabi) + + # off 0x9 + self.e_pad = self.read(7) + + # off 0x10 + e_type, e_machine, self.e_version = self.read_and_unpack(f"{endian}HHI") + self.e_type, self.e_machine = Elf.Type(e_type), Elf.Abi(e_machine) + + # off 0x18 + if self.e_class == Elf.Class.ELF_64_BITS: + self.e_entry, self.e_phoff, self.e_shoff = self.read_and_unpack(f"{endian}QQQ") + else: + self.e_entry, self.e_phoff, self.e_shoff = self.read_and_unpack(f"{endian}III") + + self.e_flags, self.e_ehsize, self.e_phentsize, self.e_phnum = self.read_and_unpack(f"{endian}IHHH") + self.e_shentsize, self.e_shnum, self.e_shstrndx = self.read_and_unpack(f"{endian}HHH") + + self.phdrs = [] + for i in range(self.e_phnum): + self.phdrs.append(Phdr(self, self.e_phoff + self.e_phentsize * i)) + + self.shdrs = [] + for i in range(self.e_shnum): + self.shdrs.append(Shdr(self, self.e_shoff + self.e_shentsize * i)) + return + + def read(self, size: int) -> bytes: + return self.fd.read(size) + + def read_and_unpack(self, fmt: str) -> Tuple[Any, ...]: + size = struct.calcsize(fmt) + data = self.fd.read(size) + return struct.unpack(fmt, data) + + def seek(self, off: int) -> None: + self.fd.seek(off, 0) + + def __str__(self) -> str: + return f"ELF('{self.path.absolute()}', {self.e_class.name}, {self.e_machine.name})" + + def __repr__(self) -> str: + return f"ELF('{self.path.absolute()}', {self.e_class.name}, {self.e_machine.name})" + + @property + def entry_point(self) -> int: + return self.e_entry + + @classmethod + def is_valid(cls, path: pathlib.Path) -> bool: + return u32(path.open("rb").read(4), e = Endianness.BIG_ENDIAN) == Elf.ELF_MAGIC + + @property + def checksec(self) -> Dict[str, bool]: + """Check the security property of the ELF binary. The following properties are: + - Canary + - NX + - PIE + - Fortify + - Partial/Full RelRO. + Return a dict() with the different keys mentioned above, and the boolean + associated whether the protection was found.""" + if not self.__checksec: + def __check_security_property(opt: str, filename: str, pattern: str) -> bool: + cmd = [readelf,] + cmd += opt.split() + cmd += [filename,] + lines = gef_execute_external(cmd, as_list=True) + for line in lines: + if re.search(pattern, line): + return True + return False + + abspath = str(self.path.absolute()) + readelf = gef.session.constants["readelf"] + self.__checksec["Canary"] = __check_security_property("-rs", abspath, r"__stack_chk_fail") is True + has_gnu_stack = __check_security_property("-W -l", abspath, r"GNU_STACK") is True + if has_gnu_stack: + self.__checksec["NX"] = __check_security_property("-W -l", abspath, r"GNU_STACK.*RWE") is False + else: + self.__checksec["NX"] = False + self.__checksec["PIE"] = __check_security_property("-h", abspath, r":.*EXEC") is False + self.__checksec["Fortify"] = __check_security_property("-s", abspath, r"_chk@GLIBC") is True + self.__checksec["Partial RelRO"] = __check_security_property("-l", abspath, r"GNU_RELRO") is True + self.__checksec["Full RelRO"] = self.__checksec["Partial RelRO"] and __check_security_property("-d", abspath, r"BIND_NOW") is True + return self.__checksec + + @classproperty + @deprecated("use `Elf.Abi.X86_64`") + def X86_64(cls) -> int: return Elf.Abi.X86_64.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.X86_32`") + def X86_32(cls) -> int : return Elf.Abi.X86_32.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.ARM`") + def ARM(cls) -> int : return Elf.Abi.ARM.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.MIPS`") + def MIPS(cls) -> int : return Elf.Abi.MIPS.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.POWERPC`") + def POWERPC(cls) -> int : return Elf.Abi.POWERPC.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.POWERPC64`") + def POWERPC64(cls) -> int : return Elf.Abi.POWERPC64.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.SPARC`") + def SPARC(cls) -> int : return Elf.Abi.SPARC.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.SPARC64`") + def SPARC64(cls) -> int : return Elf.Abi.SPARC64.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.AARCH64`") + def AARCH64(cls) -> int : return Elf.Abi.AARCH64.value # pylint: disable=no-self-argument + + @classproperty + @deprecated("use `Elf.Abi.RISCV`") + def RISCV(cls) -> int : return Elf.Abi.RISCV.value # pylint: disable=no-self-argument + + +class Phdr: + class Type(enum.IntEnum): + PT_NULL = 0 + PT_LOAD = 1 + PT_DYNAMIC = 2 + PT_INTERP = 3 + PT_NOTE = 4 + PT_SHLIB = 5 + PT_PHDR = 6 + PT_TLS = 7 + PT_LOOS = 0x60000000 + PT_GNU_EH_FRAME = 0x6474e550 + PT_GNU_STACK = 0x6474e551 + PT_GNU_RELRO = 0x6474e552 + PT_GNU_PROPERTY = 0x6474e553 + PT_LOSUNW = 0x6ffffffa + PT_SUNWBSS = 0x6ffffffa + PT_SUNWSTACK = 0x6ffffffb + PT_HISUNW = PT_HIOS = 0x6fffffff + PT_LOPROC = 0x70000000 + PT_ARM_EIDX = 0x70000001 + PT_MIPS_ABIFLAGS= 0x70000003 + PT_HIPROC = 0x7fffffff + UNKNOWN_PHDR = 0xffffffff + + @classmethod + def _missing_(cls, _:int) -> Type: + return cls.UNKNOWN_PHDR + + class Flags(enum.IntFlag): + PF_X = 1 + PF_W = 2 + PF_R = 4 + + p_type: "Phdr.Type" + p_flags: "Phdr.Flags" + p_offset: int + p_vaddr: int + p_paddr: int + p_filesz: int + p_memsz: int + p_align: int + + def __init__(self, elf: Elf, off: int) -> None: + if not elf: + return + elf.seek(off) + self.offset = off + endian = elf.e_endianness + if elf.e_class == Elf.Class.ELF_64_BITS: + p_type, p_flags, self.p_offset = elf.read_and_unpack(f"{endian}IIQ") + self.p_vaddr, self.p_paddr = elf.read_and_unpack(f"{endian}QQ") + self.p_filesz, self.p_memsz, self.p_align = elf.read_and_unpack(f"{endian}QQQ") + else: + p_type, self.p_offset = elf.read_and_unpack(f"{endian}II") + self.p_vaddr, self.p_paddr = elf.read_and_unpack(f"{endian}II") + self.p_filesz, self.p_memsz, p_flags, self.p_align = elf.read_and_unpack(f"{endian}IIII") + + self.p_type, self.p_flags = Phdr.Type(p_type), Phdr.Flags(p_flags) + return + + def __str__(self) -> str: + return (f"Phdr(offset={self.offset}, type={self.p_type.name}, flags={self.p_flags.name}, " + f"vaddr={self.p_vaddr}, paddr={self.p_paddr}, filesz={self.p_filesz}, " + f"memsz={self.p_memsz}, align={self.p_align})") + + +class Shdr: + class Type(enum.IntEnum): + SHT_NULL = 0 + SHT_PROGBITS = 1 + SHT_SYMTAB = 2 + SHT_STRTAB = 3 + SHT_RELA = 4 + SHT_HASH = 5 + SHT_DYNAMIC = 6 + SHT_NOTE = 7 + SHT_NOBITS = 8 + SHT_REL = 9 + SHT_SHLIB = 10 + SHT_DYNSYM = 11 + SHT_NUM = 12 + SHT_INIT_ARRAY = 14 + SHT_FINI_ARRAY = 15 + SHT_PREINIT_ARRAY = 16 + SHT_GROUP = 17 + SHT_SYMTAB_SHNDX = 18 + SHT_LOOS = 0x60000000 + SHT_GNU_ATTRIBUTES = 0x6ffffff5 + SHT_GNU_HASH = 0x6ffffff6 + SHT_GNU_LIBLIST = 0x6ffffff7 + SHT_CHECKSUM = 0x6ffffff8 + SHT_LOSUNW = 0x6ffffffa + SHT_SUNW_move = 0x6ffffffa + SHT_SUNW_COMDAT = 0x6ffffffb + SHT_SUNW_syminfo = 0x6ffffffc + SHT_GNU_verdef = 0x6ffffffd + SHT_GNU_verneed = 0x6ffffffe + SHT_GNU_versym = 0x6fffffff + SHT_LOPROC = 0x70000000 + SHT_ARM_EXIDX = 0x70000001 + SHT_X86_64_UNWIND = 0x70000001 + SHT_ARM_ATTRIBUTES = 0x70000003 + SHT_MIPS_OPTIONS = 0x7000000d + DT_MIPS_INTERFACE = 0x7000002a + SHT_HIPROC = 0x7fffffff + SHT_LOUSER = 0x80000000 + SHT_HIUSER = 0x8fffffff + UNKNOWN_SHDR = 0xffffffff + + @classmethod + def _missing_(cls, _:int) -> Type: + return cls.UNKNOWN_SHDR + + class Flags(enum.IntFlag): + WRITE = 1 + ALLOC = 2 + EXECINSTR = 4 + MERGE = 0x10 + STRINGS = 0x20 + INFO_LINK = 0x40 + LINK_ORDER = 0x80 + OS_NONCONFORMING = 0x100 + GROUP = 0x200 + TLS = 0x400 + COMPRESSED = 0x800 + RELA_LIVEPATCH = 0x00100000 + RO_AFTER_INIT = 0x00200000 + ORDERED = 0x40000000 + EXCLUDE = 0x80000000 + UNKNOWN_FLAG = 0xffffffff + + @classmethod + def _missing_(cls, _:int): + return cls.UNKNOWN_FLAG + + sh_name: int + sh_type: "Shdr.Type" + sh_flags: "Shdr.Flags" + sh_addr: int + sh_offset: int + sh_size: int + sh_link: int + sh_info: int + sh_addralign: int + sh_entsize: int + name: str + + def __init__(self, elf: Optional[Elf], off: int) -> None: + if elf is None: + return + elf.seek(off) + endian = elf.e_endianness + if elf.e_class == Elf.Class.ELF_64_BITS: + self.sh_name, sh_type, sh_flags = elf.read_and_unpack(f"{endian}IIQ") + self.sh_addr, self.sh_offset = elf.read_and_unpack(f"{endian}QQ") + self.sh_size, self.sh_link, self.sh_info = elf.read_and_unpack(f"{endian}QII") + self.sh_addralign, self.sh_entsize = elf.read_and_unpack(f"{endian}QQ") + else: + self.sh_name, sh_type, sh_flags = elf.read_and_unpack(f"{endian}III") + self.sh_addr, self.sh_offset = elf.read_and_unpack(f"{endian}II") + self.sh_size, self.sh_link, self.sh_info = elf.read_and_unpack(f"{endian}III") + self.sh_addralign, self.sh_entsize = elf.read_and_unpack(f"{endian}II") + + self.sh_type = Shdr.Type(sh_type) + self.sh_flags = Shdr.Flags(sh_flags) + stroff = elf.e_shoff + elf.e_shentsize * elf.e_shstrndx + + if elf.e_class == Elf.Class.ELF_64_BITS: + elf.seek(stroff + 16 + 8) + offset = u64(elf.read(8)) + else: + elf.seek(stroff + 12 + 4) + offset = u32(elf.read(4)) + elf.seek(offset + self.sh_name) + self.name = "" + while True: + c = u8(elf.read(1)) + if c == 0: + break + self.name += chr(c) + return + + def __str__(self) -> str: + return (f"Shdr(name={self.name}, type={self.sh_type.name}, flags={self.sh_flags.name}, " + f"addr={self.sh_addr:#x}, offset={self.sh_offset}, size={self.sh_size}, link={self.sh_link}, " + f"info={self.sh_info}, addralign={self.sh_addralign}, entsize={self.sh_entsize})") + + +class Instruction: + """GEF representation of a CPU instruction.""" + + def __init__(self, address: int, location: str, mnemo: str, operands: List[str], opcodes: bytes) -> None: + self.address, self.location, self.mnemonic, self.operands, self.opcodes = \ + address, location, mnemo, operands, opcodes + return + + # Allow formatting an instruction with {:o} to show opcodes. + # The number of bytes to display can be configured, e.g. {:4o} to only show 4 bytes of the opcodes + def __format__(self, format_spec: str) -> str: + if len(format_spec) == 0 or format_spec[-1] != "o": + return str(self) + + if format_spec == "o": + opcodes_len = len(self.opcodes) + else: + opcodes_len = int(format_spec[:-1]) + + opcodes_text = "".join(f"{b:02x}" for b in self.opcodes[:opcodes_len]) + if opcodes_len < len(self.opcodes): + opcodes_text += "..." + return (f"{self.address:#10x} {opcodes_text:{opcodes_len * 2 + 3:d}s} {self.location:16} " + f"{self.mnemonic:6} {', '.join(self.operands)}") + + def __str__(self) -> str: + return f"{self.address:#10x} {self.location:16} {self.mnemonic:6} {', '.join(self.operands)}" + + def is_valid(self) -> bool: + return "(bad)" not in self.mnemonic + + def size(self) -> int: + return len(self.opcodes) + +@deprecated("Use GefHeapManager.find_main_arena_addr()") +def search_for_main_arena() -> int: + return GefHeapManager.find_main_arena_addr() + +class GlibcHeapInfo: + """Glibc heap_info struct""" + + @staticmethod + def heap_info_t() -> Type[ctypes.Structure]: + class heap_info_cls(ctypes.Structure): + pass + pointer = ctypes.c_uint64 if gef.arch.ptrsize == 8 else ctypes.c_uint32 + pad_size = -5 * gef.arch.ptrsize & (gef.heap.malloc_alignment - 1) + fields = [ + ("ar_ptr", ctypes.POINTER(GlibcArena.malloc_state_t())), + ("prev", ctypes.POINTER(heap_info_cls)), + ("size", pointer) + ] + if gef.libc.version >= (2, 5): + fields += [ + ("mprotect_size", pointer) + ] + pad_size = -6 * gef.arch.ptrsize & (gef.heap.malloc_alignment - 1) + if gef.libc.version >= (2, 34): + fields += [ + ("pagesize", pointer) + ] + pad_size = -3 * gef.arch.ptrsize & (gef.heap.malloc_alignment - 1) + fields += [ + ("pad", ctypes.c_uint8*pad_size) + ] + heap_info_cls._fields_ = fields + return heap_info_cls + + def __init__(self, addr: Union[str, int]) -> None: + self.__address : int = parse_address(f"&{addr}") if isinstance(addr, str) else addr + self.reset() + return + + def reset(self): + self._sizeof = ctypes.sizeof(GlibcHeapInfo.heap_info_t()) + self._data = gef.memory.read(self.__address, ctypes.sizeof(GlibcHeapInfo.heap_info_t())) + self._heap_info = GlibcHeapInfo.heap_info_t().from_buffer_copy(self._data) + return + + def __getattr__(self, item: Any) -> Any: + if item in dir(self._heap_info): + return ctypes.cast(getattr(self._heap_info, item), ctypes.c_void_p).value + return getattr(self, item) + + def __abs__(self) -> int: + return self.__address + + def __int__(self) -> int: + return self.__address + + @property + def address(self) -> int: + return self.__address + + @property + def sizeof(self) -> int: + return self._sizeof + + @property + def addr(self) -> int: + return int(self) + + @property + def heap_start(self) -> int: + # check special case: first heap of non-main-arena + if self.ar_ptr - self.address < 0x60: + # the first heap of a non-main-arena starts with a `heap_info` + # struct, which should fit easily into 0x60 bytes throughout + # all architectures and glibc versions. If this check succeeds + # then we are currently looking at such a "first heap" + arena = GlibcArena(f"*{self.ar_ptr:#x}") + heap_addr = arena.heap_addr() + if heap_addr: + return heap_addr + else: + err(f"Cannot find heap address for arena {self.ar_ptr:#x}") + return 0 + return self.address + self.sizeof + + @property + def heap_end(self) -> int: + return self.address + self.size + + +class GlibcArena: + """Glibc arena class""" + + NFASTBINS = 10 + NBINS = 128 + NSMALLBINS = 64 + BINMAPSHIFT = 5 + BITSPERMAP = 1 << BINMAPSHIFT + BINMAPSIZE = NBINS // BITSPERMAP + + @staticmethod + def malloc_state_t() -> Type[ctypes.Structure]: + pointer = ctypes.c_uint64 if gef and gef.arch.ptrsize == 8 else ctypes.c_uint32 + fields = [ + ("mutex", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ] + if gef and gef.libc.version and gef.libc.version >= (2, 27): + # https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L1684 + fields += [ + ("have_fastchunks", ctypes.c_uint32), + ("UNUSED_c", ctypes.c_uint32), # padding to align to 0x10 + ] + fields += [ + ("fastbinsY", GlibcArena.NFASTBINS * pointer), + ("top", pointer), + ("last_remainder", pointer), + ("bins", (GlibcArena.NBINS * 2 - 2) * pointer), + ("binmap", GlibcArena.BINMAPSIZE * ctypes.c_uint32), + ("next", pointer), + ("next_free", pointer) + ] + if gef and gef.libc.version and gef.libc.version >= (2, 23): + # https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L1719 + fields += [ + ("attached_threads", pointer) + ] + fields += [ + ("system_mem", pointer), + ("max_system_mem", pointer), + ] + class malloc_state_cls(ctypes.Structure): + _fields_ = fields + return malloc_state_cls + + def __init__(self, addr: str) -> None: + try: + self.__address : int = parse_address(f"&{addr}") + except gdb.error: + self.__address : int = GefHeapManager.find_main_arena_addr() + # if `find_main_arena_addr` throws `gdb.error` on symbol lookup: + # it means the session is not started, so just propagate the exception + self.reset() + return + + def reset(self): + self._sizeof = ctypes.sizeof(GlibcArena.malloc_state_t()) + self._data = gef.memory.read(self.__address, ctypes.sizeof(GlibcArena.malloc_state_t())) + self.__arena = GlibcArena.malloc_state_t().from_buffer_copy(self._data) + return + + def __abs__(self) -> int: + return self.__address + + def __int__(self) -> int: + return self.__address + + def __iter__(self) -> Generator["GlibcArena", None, None]: + main_arena = int(gef.heap.main_arena) + + current_arena = self + yield current_arena + + while True: + if current_arena.next == 0 or current_arena.next == main_arena: + break + + current_arena = GlibcArena(f"*{current_arena.next:#x} ") + yield current_arena + return + + def __eq__(self, other: "GlibcArena") -> bool: + return self.__address == int(other) + + def __str__(self) -> str: + properties = f"base={self.__address:#x}, top={self.top:#x}, " \ + f"last_remainder={self.last_remainder:#x}, next={self.next:#x}" + return (f"{Color.colorify('Arena', 'blue bold underline')}({properties})") + + def __repr__(self) -> str: + return f"GlibcArena(address={self.__address:#x}, size={self._sizeof})" + + @property + def address(self) -> int: + return self.__address + + @property + def sizeof(self) -> int: + return self._sizeof + + @property + def addr(self) -> int: + return int(self) + + @property + def top(self) -> int: + return self.__arena.top + + @property + def last_remainder(self) -> int: + return self.__arena.last_remainder + + @property + def fastbinsY(self) -> ctypes.Array: + return self.__arena.fastbinsY + + @property + def bins(self) -> ctypes.Array: + return self.__arena.bins + + @property + def binmap(self) -> ctypes.Array: + return self.__arena.binmap + + @property + def next(self) -> int: + return self.__arena.next + + @property + def next_free(self) -> int: + return self.__arena.next_free + + @property + def attached_threads(self) -> int: + return self.__arena.attached_threads + + @property + def system_mem(self) -> int: + return self.__arena.system_mem + + @property + def max_system_mem(self) -> int: + return self.__arena.max_system_mem + + def fastbin(self, i: int) -> Optional["GlibcFastChunk"]: + """Return head chunk in fastbinsY[i].""" + addr = int(self.fastbinsY[i]) + if addr == 0: + return None + return GlibcFastChunk(addr + 2 * gef.arch.ptrsize) + + def bin(self, i: int) -> Tuple[int, int]: + idx = i * 2 + fd = int(self.bins[idx]) + bk = int(self.bins[idx + 1]) + return fd, bk + + def bin_at(self, i) -> int: + header_sz = 2 * gef.arch.ptrsize + offset = ctypes.addressof(self.__arena.bins) - ctypes.addressof(self.__arena) + return self.__address + offset + (i-1) * 2 * gef.arch.ptrsize + header_sz + + def is_main_arena(self) -> bool: + return gef.heap.main_arena is not None and int(self) == int(gef.heap.main_arena) + + def heap_addr(self, allow_unaligned: bool = False) -> Optional[int]: + if self.is_main_arena(): + heap_section = gef.heap.base_address + if not heap_section: + return None + return heap_section + _addr = int(self) + self.sizeof + if allow_unaligned: + return _addr + return gef.heap.malloc_align_address(_addr) + + def get_heap_info_list(self) -> Optional[List[GlibcHeapInfo]]: + if self.is_main_arena(): + return None + heap_addr = self.get_heap_for_ptr(self.top) + heap_infos = [GlibcHeapInfo(heap_addr)] + while heap_infos[-1].prev is not None: + prev = int(heap_infos[-1].prev) + heap_info = GlibcHeapInfo(prev) + heap_infos.append(heap_info) + return heap_infos[::-1] + + @staticmethod + def get_heap_for_ptr(ptr: int) -> int: + """Find the corresponding heap for a given pointer (int). + See https://github.com/bminor/glibc/blob/glibc-2.34/malloc/arena.c#L129""" + if is_32bit(): + default_mmap_threshold_max = 512 * 1024 + else: # 64bit + default_mmap_threshold_max = 4 * 1024 * 1024 * cached_lookup_type("long").sizeof + heap_max_size = 2 * default_mmap_threshold_max + return ptr & ~(heap_max_size - 1) + + @staticmethod + def verify(addr: int) -> bool: + """Verify that the address matches a possible valid GlibcArena""" + try: + test_arena = GlibcArena(f"*{addr:#x}") + cur_arena = GlibcArena(f"*{test_arena.next:#x}") + while cur_arena != test_arena: + if cur_arena == 0: + return False + cur_arena = GlibcArena(f"*{cur_arena.next:#x}") + except Exception as e: + return False + return True + + +class GlibcChunk: + """Glibc chunk class. The default behavior (from_base=False) is to interpret the data starting at the memory + address pointed to as the chunk data. Setting from_base to True instead treats that data as the chunk header. + Ref: https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/.""" + + class ChunkFlags(enum.IntFlag): + PREV_INUSE = 1 + IS_MMAPPED = 2 + NON_MAIN_ARENA = 4 + + def __str__(self) -> str: + return f" | ".join([ + Color.greenify("PREV_INUSE") if self.value & self.PREV_INUSE else Color.redify("PREV_INUSE"), + Color.greenify("IS_MMAPPED") if self.value & self.IS_MMAPPED else Color.redify("IS_MMAPPED"), + Color.greenify("NON_MAIN_ARENA") if self.value & self.NON_MAIN_ARENA else Color.redify("NON_MAIN_ARENA") + ]) + + @staticmethod + def malloc_chunk_t() -> Type[ctypes.Structure]: + pointer = ctypes.c_uint64 if gef and gef.arch.ptrsize == 8 else ctypes.c_uint32 + class malloc_chunk_cls(ctypes.Structure): + pass + + malloc_chunk_cls._fields_ = [ + ("prev_size", pointer), + ("size", pointer), + ("fd", pointer), + ("bk", pointer), + ("fd_nextsize", ctypes.POINTER(malloc_chunk_cls)), + ("bk_nextsize", ctypes.POINTER(malloc_chunk_cls)), + ] + return malloc_chunk_cls + + def __init__(self, addr: int, from_base: bool = False, allow_unaligned: bool = True) -> None: + ptrsize = gef.arch.ptrsize + self.data_address = addr + 2 * ptrsize if from_base else addr + self.base_address = addr if from_base else addr - 2 * ptrsize + if not allow_unaligned: + self.data_address = gef.heap.malloc_align_address(self.data_address) + self.size_addr = int(self.data_address - ptrsize) + self.prev_size_addr = self.base_address + self.reset() + return + + def reset(self): + self._sizeof = ctypes.sizeof(GlibcChunk.malloc_chunk_t()) + self._data = gef.memory.read( + self.base_address, ctypes.sizeof(GlibcChunk.malloc_chunk_t())) + self._chunk = GlibcChunk.malloc_chunk_t().from_buffer_copy(self._data) + return + + @property + def prev_size(self) -> int: + return self._chunk.prev_size + + @property + def size(self) -> int: + return self._chunk.size & (~0x07) + + @property + def flags(self) -> ChunkFlags: + return GlibcChunk.ChunkFlags(self._chunk.size & 0x07) + + @property + def fd(self) -> int: + return self._chunk.fd + + @property + def bk(self) -> int: + return self._chunk.bk + + @property + def fd_nextsize(self) -> int: + return self._chunk.fd_nextsize + + @property + def bk_nextsize(self) -> int: + return self._chunk.bk_nextsize + + def get_usable_size(self) -> int: + # https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L4537 + ptrsz = gef.arch.ptrsize + cursz = self.size + if cursz == 0: return cursz + if self.has_m_bit(): return cursz - 2 * ptrsz + return cursz - ptrsz + + @property + def usable_size(self) -> int: + return self.get_usable_size() + + def get_prev_chunk_size(self) -> int: + return gef.memory.read_integer(self.prev_size_addr) + + def __iter__(self) -> Generator["GlibcChunk", None, None]: + current_chunk = self + top = gef.heap.main_arena.top + + while True: + yield current_chunk + + if current_chunk.base_address == top: + break + + if current_chunk.size == 0: + break + + next_chunk_addr = current_chunk.get_next_chunk_addr() + + if not Address(value=next_chunk_addr).valid: + break + + next_chunk = current_chunk.get_next_chunk() + if next_chunk is None: + break + + current_chunk = next_chunk + return + + def get_next_chunk(self, allow_unaligned: bool = False) -> "GlibcChunk": + addr = self.get_next_chunk_addr() + return GlibcChunk(addr, allow_unaligned=allow_unaligned) + + def get_next_chunk_addr(self) -> int: + return self.data_address + self.size + + def has_p_bit(self) -> bool: + return bool(self.flags & GlibcChunk.ChunkFlags.PREV_INUSE) + + def has_m_bit(self) -> bool: + return bool(self.flags & GlibcChunk.ChunkFlags.IS_MMAPPED) + + def has_n_bit(self) -> bool: + return bool(self.flags & GlibcChunk.ChunkFlags.NON_MAIN_ARENA) + + def is_used(self) -> bool: + """Check if the current block is used by: + - checking the M bit is true + - or checking that next chunk PREV_INUSE flag is true""" + if self.has_m_bit(): + return True + + next_chunk = self.get_next_chunk() + return True if next_chunk.has_p_bit() else False + + def __str_sizes(self) -> str: + msg = [] + failed = False + + try: + msg.append("Chunk size: {0:d} ({0:#x})".format(self.size)) + msg.append("Usable size: {0:d} ({0:#x})".format(self.usable_size)) + failed = True + except gdb.MemoryError: + msg.append(f"Chunk size: Cannot read at {self.size_addr:#x} (corrupted?)") + + try: + msg.append("Previous chunk size: {0:d} ({0:#x})".format(self.get_prev_chunk_size())) + failed = True + except gdb.MemoryError: + msg.append(f"Previous chunk size: Cannot read at {self.base_address:#x} (corrupted?)") + + if failed: + msg.append(str(self.flags)) + + return "\n".join(msg) + + def _str_pointers(self) -> str: + fwd = self.data_address + bkw = self.data_address + gef.arch.ptrsize + + msg = [] + try: + msg.append(f"Forward pointer: {self.fd:#x}") + except gdb.MemoryError: + msg.append(f"Forward pointer: {fwd:#x} (corrupted?)") + + try: + msg.append(f"Backward pointer: {self.bk:#x}") + except gdb.MemoryError: + msg.append(f"Backward pointer: {bkw:#x} (corrupted?)") + + return "\n".join(msg) + + def __str__(self) -> str: + return (f"{Color.colorify('Chunk', 'yellow bold underline')}(addr={self.data_address:#x}, " + f"size={self.size:#x}, flags={self.flags!s})") + + def psprint(self) -> str: + msg = [ + str(self), + self.__str_sizes(), + ] + if not self.is_used(): + msg.append(f"\n\n{self._str_pointers()}") + return "\n".join(msg) + "\n" + + +class GlibcFastChunk(GlibcChunk): + + @property + def fd(self) -> int: + assert(gef and gef.libc.version) + if gef.libc.version < (2, 32): + return self._chunk.fd + return self.reveal_ptr(self.data_address) + + def protect_ptr(self, pos: int, pointer: int) -> int: + """https://elixir.bootlin.com/glibc/glibc-2.32/source/malloc/malloc.c#L339""" + assert(gef and gef.libc.version) + if gef.libc.version < (2, 32): + return pointer + return (pos >> 12) ^ pointer + + def reveal_ptr(self, pointer: int) -> int: + """https://elixir.bootlin.com/glibc/glibc-2.32/source/malloc/malloc.c#L341""" + assert(gef and gef.libc.version) + if gef.libc.version < (2, 32): + return pointer + return gef.memory.read_integer(pointer) ^ (pointer >> 12) + +class GlibcTcacheChunk(GlibcFastChunk): + + pass + +@deprecated("Use GefLibcManager.find_libc_version()") +def get_libc_version() -> Tuple[int, ...]: + return GefLibcManager.find_libc_version() + +def titlify(text: str, color: Optional[str] = None, msg_color: Optional[str] = None) -> str: + """Print a centered title.""" + _, cols = get_terminal_size() + nb = (cols - len(text) - 2) // 2 + line_color = color or gef.config["theme.default_title_line"] + text_color = msg_color or gef.config["theme.default_title_message"] + + msg = [Color.colorify(f"{HORIZONTAL_LINE * nb} ", line_color), + Color.colorify(text, text_color), + Color.colorify(f" {HORIZONTAL_LINE * nb}", line_color)] + return "".join(msg) + + +def dbg(msg: str) -> None: + if gef.config["gef.debug"] is True: + gef_print(f"{Color.colorify('[=]', 'bold cyan')} {msg}") + return + + +def err(msg: str) -> None: + gef_print(f"{Color.colorify('[!]', 'bold red')} {msg}") + return + + +def warn(msg: str) -> None: + gef_print(f"{Color.colorify('[*]', 'bold yellow')} {msg}") + return + + +def ok(msg: str) -> None: + gef_print(f"{Color.colorify('[+]', 'bold green')} {msg}") + return + + +def info(msg: str) -> None: + gef_print(f"{Color.colorify('[+]', 'bold blue')} {msg}") + return + + +def push_context_message(level: str, message: str) -> None: + """Push the message to be displayed the next time the context is invoked.""" + if level not in ("error", "warn", "ok", "info"): + err(f"Invalid level '{level}', discarding message") + return + gef.ui.context_messages.append((level, message)) + return + + +def show_last_exception() -> None: + """Display the last Python exception.""" + + def _show_code_line(fname: str, idx: int) -> str: + fname = os.path.expanduser(os.path.expandvars(fname)) + with open(fname, "r") as f: + _data = f.readlines() + return _data[idx - 1] if 0 < idx < len(_data) else "" + + gef_print("") + exc_type, exc_value, exc_traceback = sys.exc_info() + + gef_print(" Exception raised ".center(80, HORIZONTAL_LINE)) + gef_print(f"{Color.colorify(exc_type.__name__, 'bold underline red')}: {exc_value}") + gef_print(" Detailed stacktrace ".center(80, HORIZONTAL_LINE)) + + for fs in traceback.extract_tb(exc_traceback)[::-1]: + filename, lineno, method, code = fs + + if not code or not code.strip(): + code = _show_code_line(filename, lineno) + + gef_print(f"""{DOWN_ARROW} File "{Color.yellowify(filename)}", line {lineno:d}, in {Color.greenify(method)}()""") + gef_print(f" {RIGHT_ARROW} {code}") + + gef_print(" Version ".center(80, HORIZONTAL_LINE)) + gdb.execute("version full") + gef_print(" Last 10 GDB commands ".center(80, HORIZONTAL_LINE)) + gdb.execute("show commands") + gef_print(" Runtime environment ".center(80, HORIZONTAL_LINE)) + gef_print(f"* GDB: {gdb.VERSION}") + gef_print(f"* Python: {sys.version_info.major:d}.{sys.version_info.minor:d}.{sys.version_info.micro:d} - {sys.version_info.releaselevel}") + gef_print(f"* OS: {platform.system()} - {platform.release()} ({platform.machine()})") + + try: + lsb_release = which("lsb_release") + gdb.execute(f"!{lsb_release} -a") + except FileNotFoundError: + gef_print("lsb_release is missing, cannot collect additional debug information") + + gef_print(HORIZONTAL_LINE*80) + gef_print("") + return + + +def gef_pystring(x: bytes) -> str: + """Returns a sanitized version as string of the bytes list given in input.""" + res = str(x, encoding="utf-8") + substs = [("\n", "\\n"), ("\r", "\\r"), ("\t", "\\t"), ("\v", "\\v"), ("\b", "\\b"), ] + for x, y in substs: res = res.replace(x, y) + return res + + +def gef_pybytes(x: str) -> bytes: + """Returns an immutable bytes list from the string given as input.""" + return bytes(str(x), encoding="utf-8") + + +@lru_cache() +def which(program: str) -> pathlib.Path: + """Locate a command on the filesystem.""" + for path in os.environ["PATH"].split(os.pathsep): + dirname = pathlib.Path(path) + fpath = dirname / program + if os.access(fpath, os.X_OK): + return fpath + + raise FileNotFoundError(f"Missing file `{program}`") + + +def style_byte(b: int, color: bool = True) -> str: + style = { + "nonprintable": "yellow", + "printable": "white", + "00": "gray", + "0a": "blue", + "ff": "green", + } + sbyte = f"{b:02x}" + if not color or gef.config["highlight.regex"]: + return sbyte + + if sbyte in style: + st = style[sbyte] + elif chr(b) in (string.ascii_letters + string.digits + string.punctuation + " "): + st = style.get("printable") + else: + st = style.get("nonprintable") + if st: + sbyte = Color.colorify(sbyte, st) + return sbyte + + +def hexdump(source: ByteString, length: int = 0x10, separator: str = ".", show_raw: bool = False, show_symbol: bool = True, base: int = 0x00) -> str: + """Return the hexdump of `src` argument. + @param source *MUST* be of type bytes or bytearray + @param length is the length of items per line + @param separator is the default character to use if one byte is not printable + @param show_raw if True, do not add the line nor the text translation + @param base is the start address of the block being hexdump + @return a string with the hexdump""" + result = [] + align = gef.arch.ptrsize * 2 + 2 if is_alive() else 18 + + for i in range(0, len(source), length): + chunk = bytearray(source[i : i + length]) + hexa = " ".join([style_byte(b, color=not show_raw) for b in chunk]) + + if show_raw: + result.append(hexa) + continue + + text = "".join([chr(b) if 0x20 <= b < 0x7F else separator for b in chunk]) + if show_symbol: + sym = gdb_get_location_from_symbol(base + i) + sym = "<{:s}+{:04x}>".format(*sym) if sym else "" + else: + sym = "" + + result.append(f"{base + i:#0{align}x} {sym} {hexa:<{3 * length}} {text}") + return "\n".join(result) + + +def is_debug() -> bool: + """Check if debug mode is enabled.""" + return gef.config["gef.debug"] is True + + +def buffer_output() -> bool: + """Check if output should be buffered until command completion.""" + return gef.config["gef.buffer"] is True + + +def hide_context() -> bool: + """Helper function to hide the context pane.""" + gef.ui.context_hidden = True + return True + + +def unhide_context() -> bool: + """Helper function to unhide the context pane.""" + gef.ui.context_hidden = False + return True + + +class DisableContextOutputContext: + def __enter__(self) -> None: + hide_context() + return + + def __exit__(self, *exc: Any) -> None: + unhide_context() + return + + +class RedirectOutputContext: + def __init__(self, to: str = "/dev/null") -> None: + self.redirection_target_file = to + return + + def __enter__(self) -> None: + """Redirect all GDB output to `to_file` parameter. By default, `to_file` redirects to `/dev/null`.""" + gdb.execute("set logging overwrite") + gdb.execute(f"set logging file {self.redirection_target_file}") + gdb.execute("set logging redirect on") + gdb.execute("set logging on") + return + + def __exit__(self, *exc: Any) -> None: + """Disable the output redirection, if any.""" + gdb.execute("set logging off") + gdb.execute("set logging redirect off") + return + + +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`.""" + gdb.execute("set logging overwrite") + gdb.execute(f"set logging file {to_file}") + gdb.execute("set logging redirect on") + gdb.execute("set logging on") + return + + +def disable_redirect_output() -> None: + """Disable the output redirection, if any.""" + gdb.execute("set logging off") + gdb.execute("set logging redirect off") + return + + +def gef_makedirs(path: str, mode: int = 0o755) -> pathlib.Path: + """Recursive mkdir() creation. If successful, return the absolute path of the directory created.""" + fpath = pathlib.Path(path) + if not fpath.is_dir(): + fpath.mkdir(mode=mode, exist_ok=True, parents=True) + return fpath.absolute() + + +@lru_cache() +def gdb_lookup_symbol(sym: str) -> Optional[Tuple[Optional[str], Optional[Tuple[gdb.Symtab_and_line, ...]]]]: + """Fetch the proper symbol or None if not defined.""" + try: + return gdb.decode_line(sym)[1] + except gdb.error: + return None + + +@lru_cache(maxsize=512) +def gdb_get_location_from_symbol(address: int) -> Optional[Tuple[str, int]]: + """Retrieve the location of the `address` argument from the symbol table. + Return a tuple with the name and offset if found, None otherwise.""" + # this is horrible, ugly hack and shitty perf... + # find a *clean* way to get gdb.Location from an address + sym = str(gdb.execute(f"info symbol {address:#x}", to_string=True)) + if sym.startswith("No symbol matches"): + return None + + i = sym.find(" in section ") + sym = sym[:i].split() + name, offset = sym[0], 0 + if len(sym) == 3 and sym[2].isdigit(): + offset = int(sym[2]) + return name, offset + + +def gdb_disassemble(start_pc: int, **kwargs: int) -> Generator[Instruction, None, None]: + """Disassemble instructions from `start_pc` (Integer). Accepts the following named + parameters: + - `end_pc` (Integer) only instructions whose start address fall in the interval from + start_pc to end_pc are returned. + - `count` (Integer) list at most this many disassembled instructions + If `end_pc` and `count` are not provided, the function will behave as if `count=1`. + Return an iterator of Instruction objects + """ + frame = gdb.selected_frame() + arch = frame.architecture() + + for insn in arch.disassemble(start_pc, **kwargs): + address = insn["addr"] + asm = insn["asm"].rstrip().split(None, 1) + if len(asm) > 1: + mnemo, operands = asm + operands = operands.split(",") + else: + mnemo, operands = asm[0], [] + + loc = gdb_get_location_from_symbol(address) + location = "<{}+{}>".format(*loc) if loc else "" + + opcodes = gef.memory.read(insn["addr"], insn["length"]) + + yield Instruction(address, location, mnemo, operands, opcodes) + + +def gdb_get_nth_previous_instruction_address(addr: int, n: int) -> Optional[int]: + """Return the address (Integer) of the `n`-th instruction before `addr`.""" + # fixed-length ABI + if gef.arch.instruction_length: + return max(0, addr - n * gef.arch.instruction_length) + + # variable-length ABI + cur_insn_addr = gef_current_instruction(addr).address + + # we try to find a good set of previous instructions by "guessing" disassembling backwards + # the 15 comes from the longest instruction valid size + for i in range(15 * n, 0, -1): + try: + insns = list(gdb_disassemble(addr - i, end_pc=cur_insn_addr)) + except gdb.MemoryError: + # this is because we can hit an unmapped page trying to read backward + break + + # 1. check that the disassembled instructions list size can satisfy + if len(insns) < n + 1: # we expect the current instruction plus the n before it + continue + + # If the list of instructions is longer than what we need, then we + # could get lucky and already have more than what we need, so slice down + insns = insns[-n - 1 :] + + # 2. check that the sequence ends with the current address + if insns[-1].address != cur_insn_addr: + continue + + # 3. check all instructions are valid + if all(insn.is_valid() for insn in insns): + return insns[0].address + + return None + + +def gdb_get_nth_next_instruction_address(addr: int, n: int) -> int: + """Return the address (Integer) of the `n`-th instruction after `addr`.""" + # fixed-length ABI + if gef.arch.instruction_length: + return addr + n * gef.arch.instruction_length + + # variable-length ABI + insn = list(gdb_disassemble(addr, count=n))[-1] + return insn.address + + +def gef_instruction_n(addr: int, n: int) -> Instruction: + """Return the `n`-th instruction after `addr` as an Instruction object.""" + return list(gdb_disassemble(addr, count=n + 1))[n] + + +def gef_get_instruction_at(addr: int) -> Instruction: + """Return the full Instruction found at the specified address.""" + insn = next(gef_disassemble(addr, 1)) + return insn + + +def gef_current_instruction(addr: int) -> Instruction: + """Return the current instruction as an Instruction object.""" + return gef_instruction_n(addr, 0) + + +def gef_next_instruction(addr: int) -> Instruction: + """Return the next instruction as an Instruction object.""" + return gef_instruction_n(addr, 1) + + +def gef_disassemble(addr: int, nb_insn: int, nb_prev: int = 0) -> Generator[Instruction, None, None]: + """Disassemble `nb_insn` instructions after `addr` and `nb_prev` before `addr`. + Return an iterator of Instruction objects.""" + nb_insn = max(1, nb_insn) + + if nb_prev: + try: + start_addr = gdb_get_nth_previous_instruction_address(addr, nb_prev) + if start_addr: + for insn in gdb_disassemble(start_addr, count=nb_prev): + if insn.address == addr: break + yield insn + except gdb.MemoryError: + # If the address pointing to the previous instruction(s) is not mapped, simply skip them + pass + + for insn in gdb_disassemble(addr, count=nb_insn): + yield insn + + +def gef_execute_external(command: Sequence[str], as_list: bool = False, **kwargs: Any) -> Union[str, List[str]]: + """Execute an external command and return the result.""" + res = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=kwargs.get("shell", False)) + return [gef_pystring(_) for _ in res.splitlines()] if as_list else gef_pystring(res) + + +def gef_execute_gdb_script(commands: str) -> None: + """Execute the parameter `source` as GDB command. This is done by writing `commands` to + a temporary file, which is then executed via GDB `source` command. The tempfile is then deleted.""" + fd, fname = tempfile.mkstemp(suffix=".gdb", prefix="gef_") + with os.fdopen(fd, "w") as f: + f.write(commands) + f.flush() + + fname = pathlib.Path(fname) + if fname.is_file() and os.access(fname, os.R_OK): + gdb.execute(f"source {fname}") + fname.unlink() + return + + +@deprecated("Use Elf(fname).checksec()") +def checksec(filename: str) -> Dict[str, bool]: + return Elf(filename).checksec + + +@lru_cache() +def get_arch() -> str: + """Return the binary's architecture.""" + if is_alive(): + arch = gdb.selected_frame().architecture() + return arch.name() + + arch_str = gdb.execute("show architecture", to_string=True).strip() + pat = "The target architecture is set automatically (currently " + if arch_str.startswith(pat): + arch_str = arch_str[len(pat):].rstrip(")") + return arch_str + + pat = "The target architecture is assumed to be " + if arch_str.startswith(pat): + return arch_str[len(pat):] + + pat = "The target architecture is set to " + if arch_str.startswith(pat): + # GDB version >= 10.1 + if '"auto"' in arch_str: + return re.findall(r"currently \"(.+)\"", arch_str)[0] + return re.findall(r"\"(.+)\"", arch_str)[0] + + # Unknown, we throw an exception to be safe + raise RuntimeError(f"Unknown architecture: {arch_str}") + + +@deprecated("Use `gef.binary.entry_point` instead") +def get_entry_point() -> Optional[int]: + """Return the binary entry point.""" + return gef.binary.entry_point if gef.binary else None + + +def is_pie(fpath: str) -> bool: + return Elf(fpath).checksec["PIE"] + + +@deprecated("Prefer `gef.arch.endianness == Endianness.BIG_ENDIAN`") +def is_big_endian() -> bool: + return gef.arch.endianness == Endianness.BIG_ENDIAN + + +@deprecated("gef.arch.endianness == Endianness.LITTLE_ENDIAN") +def is_little_endian() -> bool: + return gef.arch.endianness == Endianness.LITTLE_ENDIAN + + +def flags_to_human(reg_value: int, value_table: Dict[int, str]) -> str: + """Return a human readable string showing the flag states.""" + flags = [] + for bit_index, name in value_table.items(): + flags.append(Color.boldify(name.upper()) if reg_value & (1< Optional[int]: + section = process_lookup_path(name) + return section.page_start if section else None + + +@lru_cache() +def get_zone_base_address(name: str) -> Optional[int]: + zone = file_lookup_name_path(name, get_filepath()) + return zone.zone_start if zone else None + + +# +# Architecture classes +# +@deprecated("Using the decorator `register_architecture` is unecessary") +def register_architecture(cls: Type["Architecture"]) -> Type["Architecture"]: + return cls + +class ArchitectureBase: + """Class decorator for declaring an architecture to GEF.""" + aliases: Union[Tuple[()], Tuple[Union[str, Elf.Abi], ...]] = () + + def __init_subclass__(cls: Type["ArchitectureBase"], **kwargs): + global __registered_architectures__ + super().__init_subclass__(**kwargs) + for key in getattr(cls, "aliases"): + if issubclass(cls, Architecture): + __registered_architectures__[key] = cls + return + + +class Architecture(ArchitectureBase): + """Generic metaclass for the architecture supported by GEF.""" + + # Mandatory defined attributes by inheriting classes + arch: str + mode: str + all_registers: Union[Tuple[()], Tuple[str, ...]] + nop_insn: bytes + return_register: str + flag_register: Optional[str] + instruction_length: Optional[int] + flags_table: Dict[int, str] + syscall_register: Optional[str] + syscall_instructions: Union[Tuple[()], Tuple[str, ...]] + function_parameters: Union[Tuple[()], Tuple[str, ...]] + + # Optionally defined attributes + _ptrsize: Optional[int] = None + _endianness: Optional[Endianness] = None + special_registers: Union[Tuple[()], Tuple[str, ...]] = () + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + attributes = ("arch", "mode", "aliases", "all_registers", "nop_insn", + "return_register", "flag_register", "instruction_length", "flags_table", + "function_parameters",) + if not all(map(lambda x: hasattr(cls, x), attributes)): + raise NotImplementedError + + def __str__(self) -> str: + return f"Architecture({self.arch}, {self.mode or 'None'}, {repr(self.endianness)})" + + @staticmethod + def supports_gdb_arch(gdb_arch: str) -> Optional[bool]: + """If implemented by a child `Architecture`, this function dictates if the current class + supports the loaded ELF file (which can be accessed via `gef.binary`). This callback + function will override any assumption made by GEF to determine the architecture.""" + return None + + def flag_register_to_human(self, val: Optional[int] = None) -> str: + raise NotImplementedError + + def is_call(self, insn: Instruction) -> bool: + raise NotImplementedError + + def is_ret(self, insn: Instruction) -> bool: + raise NotImplementedError + + def is_conditional_branch(self, insn: Instruction) -> bool: + raise NotImplementedError + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + raise NotImplementedError + + def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> Optional[int]: + raise NotImplementedError + + def canary_address(self) -> int: + raise NotImplementedError + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + raise NotImplementedError + + def reset_caches(self) -> None: + self.__get_register_for_selected_frame.cache_clear() + return + + def __get_register(self, regname: str) -> int: + """Return a register's value.""" + curframe = gdb.selected_frame() + key = curframe.pc() ^ int(curframe.read_register('sp')) # todo: check when/if gdb.Frame implements `level()` + return self.__get_register_for_selected_frame(regname, key) + + @lru_cache() + def __get_register_for_selected_frame(self, regname: str, hash_key: int) -> int: + # 1st chance + try: + return parse_address(regname) + except gdb.error: + pass + + # 2nd chance - if an exception, propagate it + regname = regname.lstrip("$") + value = gdb.selected_frame().read_register(regname) + return int(value) + + def register(self, name: str) -> int: + if not is_alive(): + raise gdb.error("No debugging session active") + return self.__get_register(name) + + @property + def registers(self) -> Generator[str, None, None]: + yield from self.all_registers + + @property + def pc(self) -> int: + return self.register("$pc") + + @property + def sp(self) -> int: + return self.register("$sp") + + @property + def fp(self) -> int: + return self.register("$fp") + + @property + def ptrsize(self) -> int: + if not self._ptrsize: + res = cached_lookup_type("size_t") + if res is not None: + self._ptrsize = res.sizeof + else: + self._ptrsize = gdb.parse_and_eval("$pc").type.sizeof + return self._ptrsize + + @property + def endianness(self) -> Endianness: + if not self._endianness: + output = gdb.execute("show endian", to_string=True).strip().lower() + if "little endian" in output: + self._endianness = Endianness.LITTLE_ENDIAN + elif "big endian" in output: + self._endianness = Endianness.BIG_ENDIAN + else: + raise OSError(f"No valid endianess found in '{output}'") + return self._endianness + + def get_ith_parameter(self, i: int, in_func: bool = True) -> Tuple[str, Optional[int]]: + """Retrieves the correct parameter used for the current function call.""" + reg = self.function_parameters[i] + val = self.register(reg) + key = reg + return key, val + + +class GenericArchitecture(Architecture): + arch = "Generic" + mode = "" + aliases = ("GenericArchitecture",) + all_registers = () + instruction_length = 0 + return_register = "" + function_parameters = () + syscall_register = "" + syscall_instructions = () + nop_insn = b"" + flag_register = None + flags_table = {} + + +class RISCV(Architecture): + arch = "RISCV" + mode = "RISCV" + aliases = ("RISCV", Elf.Abi.RISCV) + all_registers = ("$zero", "$ra", "$sp", "$gp", "$tp", "$t0", "$t1", + "$t2", "$fp", "$s1", "$a0", "$a1", "$a2", "$a3", + "$a4", "$a5", "$a6", "$a7", "$s2", "$s3", "$s4", + "$s5", "$s6", "$s7", "$s8", "$s9", "$s10", "$s11", + "$t3", "$t4", "$t5", "$t6",) + return_register = "$a0" + function_parameters = ("$a0", "$a1", "$a2", "$a3", "$a4", "$a5", "$a6", "$a7") + syscall_register = "$a7" + syscall_instructions = ("ecall",) + nop_insn = b"\x00\x00\x00\x13" + # RISC-V has no flags registers + flag_register = None + flags_table = {} + + @property + def instruction_length(self) -> int: + return 4 + + def is_call(self, insn: Instruction) -> bool: + return insn.mnemonic == "call" + + def is_ret(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + if mnemo == "ret": + return True + elif (mnemo == "jalr" and insn.operands[0] == "zero" and + insn.operands[1] == "ra" and insn.operands[2] == 0): + return True + elif (mnemo == "c.jalr" and insn.operands[0] == "ra"): + return True + return False + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + raise OSError(f"Architecture {cls.arch} not supported yet") + + @property + def ptrsize(self) -> int: + if self._ptrsize is not None: + return self._ptrsize + if is_alive(): + self._ptrsize = gdb.parse_and_eval("$pc").type.sizeof + return self._ptrsize + return 4 + + def is_conditional_branch(self, insn: Instruction) -> bool: + return insn.mnemonic.startswith("b") + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + def long_to_twos_complement(v: int) -> int: + """Convert a python long value to its two's complement.""" + if is_32bit(): + if v & 0x80000000: + return v - 0x100000000 + elif is_64bit(): + if v & 0x8000000000000000: + return v - 0x10000000000000000 + else: + raise OSError("RISC-V: ELF file is not ELF32 or ELF64. This is not currently supported") + return v + + mnemo = insn.mnemonic + condition = mnemo[1:] + + if condition.endswith("z"): + # r2 is the zero register if we are comparing to 0 + rs1 = gef.arch.register(insn.operands[0]) + rs2 = gef.arch.register("$zero") + condition = condition[:-1] + elif len(insn.operands) > 2: + # r2 is populated with the second operand + rs1 = gef.arch.register(insn.operands[0]) + rs2 = gef.arch.register(insn.operands[1]) + else: + raise OSError(f"RISC-V: Failed to get rs1 and rs2 for instruction: `{insn}`") + + # If the conditional operation is not unsigned, convert the python long into + # its two's complement + if not condition.endswith("u"): + rs2 = long_to_twos_complement(rs2) + rs1 = long_to_twos_complement(rs1) + else: + condition = condition[:-1] + + if condition == "eq": + if rs1 == rs2: taken, reason = True, f"{rs1}={rs2}" + else: taken, reason = False, f"{rs1}!={rs2}" + elif condition == "ne": + if rs1 != rs2: taken, reason = True, f"{rs1}!={rs2}" + else: taken, reason = False, f"{rs1}={rs2}" + elif condition == "lt": + if rs1 < rs2: taken, reason = True, f"{rs1}<{rs2}" + else: taken, reason = False, f"{rs1}>={rs2}" + elif condition == "le": + if rs1 <= rs2: taken, reason = True, f"{rs1}<={rs2}" + else: taken, reason = False, f"{rs1}>{rs2}" + elif condition == "ge": + if rs1 < rs2: taken, reason = True, f"{rs1}>={rs2}" + else: taken, reason = False, f"{rs1}<{rs2}" + else: + raise OSError(f"RISC-V: Conditional instruction `{insn}` not supported yet") + + return taken, reason + + def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> Optional[int]: + ra = None + if self.is_ret(insn): + ra = gef.arch.register("$ra") + elif frame.older(): + ra = frame.older().pc() + return ra + + +class ARM(Architecture): + aliases = ("ARM", Elf.Abi.ARM) + arch = "ARM" + all_registers = ("$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6", + "$r7", "$r8", "$r9", "$r10", "$r11", "$r12", "$sp", + "$lr", "$pc", "$cpsr",) + + # https://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/Caccegih.html + nop_insn = b"\x01\x10\xa0\xe1" # mov r1, r1 + return_register = "$r0" + flag_register: str = "$cpsr" + flags_table = { + 31: "negative", + 30: "zero", + 29: "carry", + 28: "overflow", + 7: "interrupt", + 6: "fast", + 5: "thumb", + } + function_parameters = ("$r0", "$r1", "$r2", "$r3") + syscall_register = "$r7" + syscall_instructions = ("swi 0x0", "swi NR") + _endianness = Endianness.LITTLE_ENDIAN + + def is_thumb(self) -> bool: + """Determine if the machine is currently in THUMB mode.""" + return is_alive() and (self.cpsr & (1 << 5) == 1) + + @property + def pc(self) -> Optional[int]: + pc = gef.arch.register("$pc") + if self.is_thumb(): + pc += 1 + return pc + + @property + def cpsr(self) -> int: + if not is_alive(): + raise RuntimeError("Cannot get CPSR, program not started?") + return gef.arch.register(self.flag_register) + + @property + def mode(self) -> str: + return "THUMB" if self.is_thumb() else "ARM" + + @property + def instruction_length(self) -> Optional[int]: + # Thumb instructions have variable-length (2 or 4-byte) + return None if self.is_thumb() else 4 + + @property + def ptrsize(self) -> int: + return 4 + + def is_call(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + call_mnemos = {"bl", "blx"} + return mnemo in call_mnemos + + def is_ret(self, insn: Instruction) -> bool: + pop_mnemos = {"pop"} + branch_mnemos = {"bl", "bx"} + write_mnemos = {"ldr", "add"} + if insn.mnemonic in pop_mnemos: + return insn.operands[-1] == " pc}" + if insn.mnemonic in branch_mnemos: + return insn.operands[-1] == "lr" + if insn.mnemonic in write_mnemos: + return insn.operands[0] == "pc" + return False + + def flag_register_to_human(self, val: Optional[int] = None) -> str: + # https://www.botskool.com/user-pages/tutorials/electronics/arm-7-tutorial-part-1 + if val is None: + reg = self.flag_register + val = gef.arch.register(reg) + return flags_to_human(val, self.flags_table) + + def is_conditional_branch(self, insn: Instruction) -> bool: + conditions = {"eq", "ne", "lt", "le", "gt", "ge", "vs", "vc", "mi", "pl", "hi", "ls", "cc", "cs"} + return insn.mnemonic[-2:] in conditions + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + mnemo = insn.mnemonic + # ref: https://www.davespace.co.uk/arm/introduction-to-arm/conditional.html + flags = dict((self.flags_table[k], k) for k in self.flags_table) + val = gef.arch.register(self.flag_register) + taken, reason = False, "" + + if mnemo.endswith("eq"): taken, reason = bool(val&(1< int: + ra = None + if self.is_ret(insn): + # If it's a pop, we have to peek into the stack, otherwise use lr + if insn.mnemonic == "pop": + ra_addr = gef.arch.sp + (len(insn.operands)-1) * self.ptrsize + ra = to_unsigned_long(dereference(ra_addr)) + elif insn.mnemonic == "ldr": + return to_unsigned_long(dereference(gef.arch.sp)) + else: # 'bx lr' or 'add pc, lr, #0' + return gef.arch.register("$lr") + elif frame.older(): + ra = frame.older().pc() + return ra + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + _NR_mprotect = 125 + insns = [ + "push {r0-r2, r7}", + f"mov r1, {addr & 0xffff:d}", + f"mov r0, {(addr & 0xffff0000) >> 16:d}", + "lsl r0, r0, 16", + "add r0, r0, r1", + f"mov r1, {size & 0xffff:d}", + f"mov r2, {perm.value & 0xff:d}", + f"mov r7, {_NR_mprotect:d}", + "svc 0", + "pop {r0-r2, r7}", + ] + return "; ".join(insns) + + +class AARCH64(ARM): + aliases = ("ARM64", "AARCH64", Elf.Abi.AARCH64) + arch = "ARM64" + mode: str = "" + + all_registers = ( + "$x0", "$x1", "$x2", "$x3", "$x4", "$x5", "$x6", "$x7", + "$x8", "$x9", "$x10", "$x11", "$x12", "$x13", "$x14","$x15", + "$x16", "$x17", "$x18", "$x19", "$x20", "$x21", "$x22", "$x23", + "$x24", "$x25", "$x26", "$x27", "$x28", "$x29", "$x30", "$sp", + "$pc", "$cpsr", "$fpsr", "$fpcr",) + return_register = "$x0" + flag_register = "$cpsr" + flags_table = { + 31: "negative", + 30: "zero", + 29: "carry", + 28: "overflow", + 7: "interrupt", + 9: "endian", + 6: "fast", + 5: "t32", + 4: "m[4]", + } + function_parameters = ("$x0", "$x1", "$x2", "$x3", "$x4", "$x5", "$x6", "$x7",) + syscall_register = "$x8" + syscall_instructions = ("svc $x0",) + + def is_call(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + call_mnemos = {"bl", "blr"} + return mnemo in call_mnemos + + def flag_register_to_human(self, val: Optional[int] = None) -> str: + # https://events.linuxfoundation.org/sites/events/files/slides/KoreaLinuxForum-2014.pdf + reg = self.flag_register + if not val: + val = gef.arch.register(reg) + return flags_to_human(val, self.flags_table) + + def is_aarch32(self) -> bool: + """Determine if the CPU is currently in AARCH32 mode from runtime.""" + return (self.cpsr & (1 << 4) != 0) and (self.cpsr & (1 << 5) == 0) + + def is_thumb32(self) -> bool: + """Determine if the CPU is currently in THUMB32 mode from runtime.""" + return (self.cpsr & (1 << 4) == 1) and (self.cpsr & (1 << 5) == 1) + + @property + def ptrsize(self) -> int: + """Determine the size of pointer from the current CPU mode""" + if not is_alive(): + return 8 + if self.is_aarch32(): + return 4 + if self.is_thumb32(): + return 2 + return 8 + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + _NR_mprotect = 226 + insns = [ + "str x8, [sp, -16]!", + "str x0, [sp, -16]!", + "str x1, [sp, -16]!", + "str x2, [sp, -16]!", + f"mov x8, {_NR_mprotect:d}", + f"movz x0, {addr & 0xFFFF:#x}", + f"movk x0, {(addr >> 16) & 0xFFFF:#x}, lsl 16", + f"movk x0, {(addr >> 32) & 0xFFFF:#x}, lsl 32", + f"movk x0, {(addr >> 48) & 0xFFFF:#x}, lsl 48", + f"movz x1, {size & 0xFFFF:#x}", + f"movk x1, {(size >> 16) & 0xFFFF:#x}, lsl 16", + f"mov x2, {perm.value:d}", + "svc 0", + "ldr x2, [sp], 16", + "ldr x1, [sp], 16", + "ldr x0, [sp], 16", + "ldr x8, [sp], 16", + ] + return "; ".join(insns) + + def is_conditional_branch(self, insn: Instruction) -> bool: + # https://www.element14.com/community/servlet/JiveServlet/previewBody/41836-102-1-229511/ARM.Reference_Manual.pdf + # sect. 5.1.1 + mnemo = insn.mnemonic + branch_mnemos = {"cbnz", "cbz", "tbnz", "tbz"} + return mnemo.startswith("b.") or mnemo in branch_mnemos + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + mnemo, operands = insn.mnemonic, insn.operands + taken, reason = False, "" + + if mnemo in {"cbnz", "cbz", "tbnz", "tbz"}: + reg = f"${operands[0]}" + op = gef.arch.register(reg) + if mnemo == "cbnz": + if op!=0: taken, reason = True, f"{reg}!=0" + else: taken, reason = False, f"{reg}==0" + elif mnemo == "cbz": + if op == 0: taken, reason = True, f"{reg}==0" + else: taken, reason = False, f"{reg}!=0" + elif mnemo == "tbnz": + # operands[1] has one or more white spaces in front, then a #, then the number + # so we need to eliminate them + i = int(operands[1].strip().lstrip("#")) + if (op & 1< str: + reg = self.flag_register + if not val: + val = gef.arch.register(reg) + return flags_to_human(val, self.flags_table) + + def is_call(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + call_mnemos = {"call", "callq"} + return mnemo in call_mnemos + + def is_ret(self, insn: Instruction) -> bool: + return insn.mnemonic == "ret" + + def is_conditional_branch(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + branch_mnemos = { + "ja", "jnbe", "jae", "jnb", "jnc", "jb", "jc", "jnae", "jbe", "jna", + "jcxz", "jecxz", "jrcxz", "je", "jz", "jg", "jnle", "jge", "jnl", + "jl", "jnge", "jle", "jng", "jne", "jnz", "jno", "jnp", "jpo", "jns", + "jo", "jp", "jpe", "js" + } + return mnemo in branch_mnemos + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + mnemo = insn.mnemonic + # all kudos to fG! (https://github.com/gdbinit/Gdbinit/blob/master/gdbinit#L1654) + flags = dict((self.flags_table[k], k) for k in self.flags_table) + val = gef.arch.register(self.flag_register) + + taken, reason = False, "" + + if mnemo in ("ja", "jnbe"): + taken, reason = not val&(1< Optional[int]: + ra = None + if self.is_ret(insn): + ra = to_unsigned_long(dereference(gef.arch.sp)) + if frame.older(): + ra = frame.older().pc() + + return ra + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + _NR_mprotect = 125 + insns = [ + "pushad", + "pushfd", + f"mov eax, {_NR_mprotect:d}", + f"mov ebx, {addr:d}", + f"mov ecx, {size:d}", + f"mov edx, {perm.value:d}", + "int 0x80", + "popfd", + "popad", + ] + return "; ".join(insns) + + def get_ith_parameter(self, i: int, in_func: bool = True) -> Tuple[str, Optional[int]]: + if in_func: + i += 1 # Account for RA being at the top of the stack + sp = gef.arch.sp + sz = gef.arch.ptrsize + loc = sp + (i * sz) + val = gef.memory.read_integer(loc) + key = f"[sp + {i * sz:#x}]" + return key, val + + +class X86_64(X86): + aliases = ("X86_64", Elf.Abi.X86_64, "i386:x86-64") + arch = "X86" + mode = "64" + + gpr_registers = ( + "$rax", "$rbx", "$rcx", "$rdx", "$rsp", "$rbp", "$rsi", "$rdi", "$rip", + "$r8", "$r9", "$r10", "$r11", "$r12", "$r13", "$r14", "$r15", ) + all_registers = gpr_registers + ( X86.flag_register, ) + X86.special_registers + return_register = "$rax" + function_parameters = ["$rdi", "$rsi", "$rdx", "$rcx", "$r8", "$r9"] + syscall_register = "$rax" + syscall_instructions = ["syscall"] + # We don't want to inherit x86's stack based param getter + get_ith_parameter = Architecture.get_ith_parameter + _ptrsize = 8 + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + _NR_mprotect = 10 + insns = [ + "pushfq", + "push rax", + "push rdi", + "push rsi", + "push rdx", + "push rcx", + "push r11", + f"mov rax, {_NR_mprotect:d}", + f"mov rdi, {addr:d}", + f"mov rsi, {size:d}", + f"mov rdx, {perm.value:d}", + "syscall", + "pop r11", + "pop rcx", + "pop rdx", + "pop rsi", + "pop rdi", + "pop rax", + "popfq", + ] + return "; ".join(insns) + + def canary_address(self) -> int: + return self.register("fs_base") + 0x28 + +class PowerPC(Architecture): + aliases = ("PowerPC", Elf.Abi.POWERPC, "PPC") + arch = "PPC" + mode = "PPC32" + + all_registers = ( + "$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6", "$r7", + "$r8", "$r9", "$r10", "$r11", "$r12", "$r13", "$r14", "$r15", + "$r16", "$r17", "$r18", "$r19", "$r20", "$r21", "$r22", "$r23", + "$r24", "$r25", "$r26", "$r27", "$r28", "$r29", "$r30", "$r31", + "$pc", "$msr", "$cr", "$lr", "$ctr", "$xer", "$trap",) + instruction_length = 4 + nop_insn = b"\x60\x00\x00\x00" # https://developer.ibm.com/articles/l-ppc/ + return_register = "$r0" + flag_register: str = "$cr" + flags_table = { + 3: "negative[0]", + 2: "positive[0]", + 1: "equal[0]", + 0: "overflow[0]", + # cr7 + 31: "less[7]", + 30: "greater[7]", + 29: "equal[7]", + 28: "overflow[7]", + } + function_parameters = ("$i0", "$i1", "$i2", "$i3", "$i4", "$i5") + syscall_register = "$r0" + syscall_instructions = ("sc",) + ptrsize = 4 + + + 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: + reg = self.flag_register + val = gef.arch.register(reg) + return flags_to_human(val, self.flags_table) + + def is_call(self, insn: Instruction) -> bool: + return False + + def is_ret(self, insn: Instruction) -> bool: + return insn.mnemonic == "blr" + + def is_conditional_branch(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + branch_mnemos = {"beq", "bne", "ble", "blt", "bgt", "bge"} + return mnemo in branch_mnemos + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + mnemo = insn.mnemonic + flags = dict((self.flags_table[k], k) for k in self.flags_table) + val = gef.arch.register(self.flag_register) + taken, reason = False, "" + if mnemo == "beq": taken, reason = bool(val&(1< Optional[int]: + ra = None + if self.is_ret(insn): + ra = gef.arch.register("$lr") + elif frame.older(): + ra = frame.older().pc() + return ra + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + # Ref: https://developer.ibm.com/articles/l-ppc/ + _NR_mprotect = 125 + insns = [ + "addi 1, 1, -16", # 1 = r1 = sp + "stw 0, 0(1)", + "stw 3, 4(1)", # r0 = syscall_code | r3, r4, r5 = args + "stw 4, 8(1)", + "stw 5, 12(1)", + f"li 0, {_NR_mprotect:d}", + f"lis 3, {addr:#x}@h", + f"ori 3, 3, {addr:#x}@l", + f"lis 4, {size:#x}@h", + f"ori 4, 4, {size:#x}@l", + f"li 5, {perm.value:d}", + "sc", + "lwz 0, 0(1)", + "lwz 3, 4(1)", + "lwz 4, 8(1)", + "lwz 5, 12(1)", + "addi 1, 1, 16", + ] + return ";".join(insns) + + +class PowerPC64(PowerPC): + aliases = ("PowerPC64", Elf.Abi.POWERPC64, "PPC64") + arch = "PPC" + mode = "PPC64" + ptrsize = 8 + + +class SPARC(Architecture): + """ Refs: + - https://www.cse.scu.edu/~atkinson/teaching/sp05/259/sparc.pdf + """ + aliases = ("SPARC", Elf.Abi.SPARC) + arch = "SPARC" + mode = "" + + all_registers = ( + "$g0", "$g1", "$g2", "$g3", "$g4", "$g5", "$g6", "$g7", + "$o0", "$o1", "$o2", "$o3", "$o4", "$o5", "$o7", + "$l0", "$l1", "$l2", "$l3", "$l4", "$l5", "$l6", "$l7", + "$i0", "$i1", "$i2", "$i3", "$i4", "$i5", "$i7", + "$pc", "$npc", "$sp ", "$fp ", "$psr",) + instruction_length = 4 + nop_insn = b"\x00\x00\x00\x00" # sethi 0, %g0 + return_register = "$i0" + flag_register: str = "$psr" + flags_table = { + 23: "negative", + 22: "zero", + 21: "overflow", + 20: "carry", + 7: "supervisor", + 5: "trap", + } + function_parameters = ("$o0 ", "$o1 ", "$o2 ", "$o3 ", "$o4 ", "$o5 ", "$o7 ",) + syscall_register = "%g1" + syscall_instructions = ("t 0x10",) + + 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: + val = gef.arch.register(reg) + return flags_to_human(val, self.flags_table) + + def is_call(self, insn: Instruction) -> bool: + return False + + def is_ret(self, insn: Instruction) -> bool: + return insn.mnemonic == "ret" + + def is_conditional_branch(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + # http://moss.csc.ncsu.edu/~mueller/codeopt/codeopt00/notes/condbranch.html + branch_mnemos = { + "be", "bne", "bg", "bge", "bgeu", "bgu", "bl", "ble", "blu", "bleu", + "bneg", "bpos", "bvs", "bvc", "bcs", "bcc" + } + return mnemo in branch_mnemos + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + mnemo = insn.mnemonic + flags = dict((self.flags_table[k], k) for k in self.flags_table) + val = gef.arch.register(self.flag_register) + taken, reason = False, "" + + if mnemo == "be": taken, reason = bool(val&(1< Optional[int]: + ra = None + if self.is_ret(insn): + ra = gef.arch.register("$o7") + elif frame.older(): + ra = frame.older().pc() + return ra + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + hi = (addr & 0xffff0000) >> 16 + lo = (addr & 0x0000ffff) + _NR_mprotect = 125 + insns = ["add %sp, -16, %sp", + "st %g1, [ %sp ]", "st %o0, [ %sp + 4 ]", + "st %o1, [ %sp + 8 ]", "st %o2, [ %sp + 12 ]", + f"sethi %hi({hi}), %o0", + f"or %o0, {lo}, %o0", + "clr %o1", + "clr %o2", + f"mov {_NR_mprotect}, %g1", + "t 0x10", + "ld [ %sp ], %g1", "ld [ %sp + 4 ], %o0", + "ld [ %sp + 8 ], %o1", "ld [ %sp + 12 ], %o2", + "add %sp, 16, %sp",] + return "; ".join(insns) + + +class SPARC64(SPARC): + """Refs: + - http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_sparc.pdf + - https://cr.yp.to/2005-590/sparcv9.pdf + """ + aliases = ("SPARC64", Elf.Abi.SPARC64) + arch = "SPARC" + mode = "V9" + + all_registers = [ + "$g0", "$g1", "$g2", "$g3", "$g4", "$g5", "$g6", "$g7", + "$o0", "$o1", "$o2", "$o3", "$o4", "$o5", "$o7", + "$l0", "$l1", "$l2", "$l3", "$l4", "$l5", "$l6", "$l7", + "$i0", "$i1", "$i2", "$i3", "$i4", "$i5", "$i7", + "$pc", "$npc", "$sp", "$fp", "$state", ] + + flag_register = "$state" # sparcv9.pdf, 5.1.5.1 (ccr) + flags_table = { + 35: "negative", + 34: "zero", + 33: "overflow", + 32: "carry", + } + + syscall_instructions = ["t 0x6d"] + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + hi = (addr & 0xffff0000) >> 16 + lo = (addr & 0x0000ffff) + _NR_mprotect = 125 + insns = ["add %sp, -16, %sp", + "st %g1, [ %sp ]", "st %o0, [ %sp + 4 ]", + "st %o1, [ %sp + 8 ]", "st %o2, [ %sp + 12 ]", + f"sethi %hi({hi}), %o0", + f"or %o0, {lo}, %o0", + "clr %o1", + "clr %o2", + f"mov {_NR_mprotect}, %g1", + "t 0x6d", + "ld [ %sp ], %g1", "ld [ %sp + 4 ], %o0", + "ld [ %sp + 8 ], %o1", "ld [ %sp + 12 ], %o2", + "add %sp, 16, %sp",] + return "; ".join(insns) + + +class MIPS(Architecture): + aliases: Tuple[Union[str, Elf.Abi], ...] = ("MIPS", Elf.Abi.MIPS) + arch = "MIPS" + mode = "MIPS32" + + # https://vhouten.home.xs4all.nl/mipsel/r3000-isa.html + all_registers = ( + "$zero", "$at", "$v0", "$v1", "$a0", "$a1", "$a2", "$a3", + "$t0", "$t1", "$t2", "$t3", "$t4", "$t5", "$t6", "$t7", + "$s0", "$s1", "$s2", "$s3", "$s4", "$s5", "$s6", "$s7", + "$t8", "$t9", "$k0", "$k1", "$s8", "$pc", "$sp", "$hi", + "$lo", "$fir", "$ra", "$gp", ) + instruction_length = 4 + _ptrsize = 4 + nop_insn = b"\x00\x00\x00\x00" # sll $0,$0,0 + return_register = "$v0" + flag_register = "$fcsr" + flags_table = {} + function_parameters = ("$a0", "$a1", "$a2", "$a3") + syscall_register = "$v0" + syscall_instructions = ("syscall",) + + def flag_register_to_human(self, val: Optional[int] = None) -> str: + return Color.colorify("No flag register", "yellow underline") + + def is_call(self, insn: Instruction) -> bool: + return False + + def is_ret(self, insn: Instruction) -> bool: + return insn.mnemonic == "jr" and insn.operands[0] == "ra" + + def is_conditional_branch(self, insn: Instruction) -> bool: + mnemo = insn.mnemonic + branch_mnemos = {"beq", "bne", "beqz", "bnez", "bgtz", "bgez", "bltz", "blez"} + return mnemo in branch_mnemos + + def is_branch_taken(self, insn: Instruction) -> Tuple[bool, str]: + mnemo, ops = insn.mnemonic, insn.operands + taken, reason = False, "" + + if mnemo == "beq": + taken, reason = gef.arch.register(ops[0]) == gef.arch.register(ops[1]), "{0[0]} == {0[1]}".format(ops) + elif mnemo == "bne": + taken, reason = gef.arch.register(ops[0]) != gef.arch.register(ops[1]), "{0[0]} != {0[1]}".format(ops) + elif mnemo == "beqz": + taken, reason = gef.arch.register(ops[0]) == 0, "{0[0]} == 0".format(ops) + elif mnemo == "bnez": + taken, reason = gef.arch.register(ops[0]) != 0, "{0[0]} != 0".format(ops) + elif mnemo == "bgtz": + taken, reason = gef.arch.register(ops[0]) > 0, "{0[0]} > 0".format(ops) + elif mnemo == "bgez": + taken, reason = gef.arch.register(ops[0]) >= 0, "{0[0]} >= 0".format(ops) + elif mnemo == "bltz": + taken, reason = gef.arch.register(ops[0]) < 0, "{0[0]} < 0".format(ops) + elif mnemo == "blez": + taken, reason = gef.arch.register(ops[0]) <= 0, "{0[0]} <= 0".format(ops) + return taken, reason + + def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> Optional[int]: + ra = None + if self.is_ret(insn): + ra = gef.arch.register("$ra") + elif frame.older(): + ra = frame.older().pc() + return ra + + @classmethod + def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str: + _NR_mprotect = 4125 + insns = ["addi $sp, $sp, -16", + "sw $v0, 0($sp)", "sw $a0, 4($sp)", + "sw $a3, 8($sp)", "sw $a3, 12($sp)", + f"li $v0, {_NR_mprotect:d}", + f"li $a0, {addr:d}", + f"li $a1, {size:d}", + f"li $a2, {perm.value:d}", + "syscall", + "lw $v0, 0($sp)", "lw $a1, 4($sp)", + "lw $a3, 8($sp)", "lw $a3, 12($sp)", + "addi $sp, $sp, 16",] + return "; ".join(insns) + + +class MIPS64(MIPS): + aliases = ("MIPS64",) + arch = "MIPS" + mode = "MIPS64" + _ptrsize = 8 + + @staticmethod + def supports_gdb_arch(gdb_arch: str) -> Optional[bool]: + return gdb_arch.startswith("mips") and gef.binary.e_class == Elf.Class.ELF_64_BITS + + +def copy_to_clipboard(data: bytes) -> None: + """Helper function to submit data to the clipboard""" + if sys.platform == "linux": + xclip = which("xclip") + prog = [xclip, "-selection", "clipboard", "-i"] + elif sys.platform == "darwin": + pbcopy = which("pbcopy") + prog = [pbcopy] + else: + raise NotImplementedError("copy: Unsupported OS") + + with subprocess.Popen(prog, stdin=subprocess.PIPE) as p: + p.stdin.write(data) + p.stdin.close() + p.wait() + return + + +def use_stdtype() -> str: + if is_32bit(): return "uint32_t" + elif is_64bit(): return "uint64_t" + return "uint16_t" + + +def use_default_type() -> str: + if is_32bit(): return "unsigned int" + elif is_64bit(): return "unsigned long" + return "unsigned short" + + +def use_golang_type() -> str: + if is_32bit(): return "uint32" + elif is_64bit(): return "uint64" + return "uint16" + + +def use_rust_type() -> str: + if is_32bit(): return "u32" + elif is_64bit(): return "u64" + return "u16" + + +def to_unsigned_long(v: gdb.Value) -> int: + """Cast a gdb.Value to unsigned long.""" + mask = (1 << 64) - 1 + return int(v.cast(gdb.Value(mask).type)) & mask + + +def get_path_from_info_proc() -> Optional[str]: + for x in gdb.execute("info proc", to_string=True).splitlines(): + if x.startswith("exe = "): + return x.split(" = ")[1].replace("'", "") + return None + + +@deprecated("Use `gef.session.os`") +def get_os() -> str: + return gef.session.os + + +@lru_cache() +def is_qemu() -> bool: + if not is_remote_debug(): + return False + response = gdb.execute("maintenance packet Qqemu.sstepbits", to_string=True, from_tty=False) + return "ENABLE=" in response + + +@lru_cache() +def is_qemu_usermode() -> bool: + if not is_qemu(): + return False + response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) + return "Text=" in response + + +@lru_cache() +def is_qemu_system() -> bool: + if not is_qemu(): + return False + response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) + return "received: \"\"" in response + + +def get_filepath() -> Optional[str]: + """Return the local absolute path of the file currently debugged.""" + if gef.session.remote: + return str(gef.session.remote.lfile.absolute()) + if gef.session.file: + return str(gef.session.file.absolute()) + return None + + +def get_function_length(sym: str) -> int: + """Attempt to get the length of the raw bytes of a function.""" + dis = gdb.execute(f"disassemble {sym}", to_string=True).splitlines() + start_addr = int(dis[1].split()[0], 16) + end_addr = int(dis[-2].split()[0], 16) + return end_addr - start_addr + + +@lru_cache() +def get_info_files() -> List[Zone]: + """Retrieve all the files loaded by debuggee.""" + lines = gdb.execute("info files", to_string=True).splitlines() + infos = [] + for line in lines: + line = line.strip() + if not line: + break + + if not line.startswith("0x"): + continue + + blobs = [x.strip() for x in line.split(" ")] + addr_start = int(blobs[0], 16) + addr_end = int(blobs[2], 16) + section_name = blobs[4] + + if len(blobs) == 7: + filename = blobs[6] + else: + filename = get_filepath() + + infos.append(Zone(section_name, addr_start, addr_end, filename)) + return infos + + +def process_lookup_address(address: int) -> Optional[Section]: + """Look up for an address in memory. + Return an Address object if found, None otherwise.""" + if not is_alive(): + err("Process is not running") + return None + + if is_x86(): + if is_in_x86_kernel(address): + return None + + for sect in gef.memory.maps: + if sect.page_start <= address < sect.page_end: + return sect + + return None + + +@lru_cache() +def process_lookup_path(name: str, perm: Permission = Permission.ALL) -> Optional[Section]: + """Look up for a path in the process memory mapping. + Return a Section object if found, None otherwise.""" + if not is_alive(): + err("Process is not running") + return None + + for sect in gef.memory.maps: + if name in sect.path and sect.permission & perm: + return sect + + return None + + +@lru_cache() +def file_lookup_name_path(name: str, path: str) -> Optional[Zone]: + """Look up a file by name and path. + Return a Zone object if found, None otherwise.""" + for xfile in get_info_files(): + if path == xfile.filename and name == xfile.name: + return xfile + return None + + +@lru_cache() +def file_lookup_address(address: int) -> Optional[Zone]: + """Look up for a file by its address. + Return a Zone object if found, None otherwise.""" + for info in get_info_files(): + if info.zone_start <= address < info.zone_end: + return info + return None + + +@lru_cache() +def lookup_address(address: int) -> Address: + """Try to find the address in the process address space. + Return an Address object, with validity flag set based on success.""" + sect = process_lookup_address(address) + info = file_lookup_address(address) + if sect is None and info is None: + # i.e. there is no info on this address + return Address(value=address, valid=False) + return Address(value=address, section=sect, info=info) + + +def xor(data: ByteString, key: str) -> bytearray: + """Return `data` xor-ed with `key`.""" + key_raw = binascii.unhexlify(key.lstrip("0x")) + return bytearray(x ^ y for x, y in zip(data, itertools.cycle(key_raw))) + + +def is_hex(pattern: str) -> bool: + """Return whether provided string is a hexadecimal value.""" + if not pattern.lower().startswith("0x"): + return False + return len(pattern) % 2 == 0 and all(c in string.hexdigits for c in pattern[2:]) + + +def continue_handler(_: "gdb.Event") -> None: + """GDB event handler for new object continue cases.""" + return + + +def hook_stop_handler(_: "gdb.StopEvent") -> None: + """GDB event handler for stop cases.""" + reset_all_caches() + gdb.execute("context") + return + + +def new_objfile_handler(evt: Optional["gdb.NewObjFileEvent"]) -> None: + """GDB event handler for new object file cases.""" + reset_all_caches() + path = evt.new_objfile.filename if evt else gdb.current_progspace().filename + try: + if gef.session.root and path.startswith("target:"): + # If the process is in a container, replace the "target:" prefix + # with the actual root directory of the process. + path = path.replace("target:", str(gef.session.root), 1) + target = pathlib.Path(path) + FileFormatClasses = list(filter(lambda fmtcls: fmtcls.is_valid(target), __registered_file_formats__)) + GuessedFileFormatClass : Type[FileFormat] = FileFormatClasses.pop() if len(FileFormatClasses) else Elf + binary = GuessedFileFormatClass(target) + if not gef.binary: + gef.binary = binary + reset_architecture() + else: + gef.session.modules.append(binary) + except FileNotFoundError as fne: + # Linux automatically maps the vDSO into our process, and GDB + # will give us the string 'system-supplied DSO' as a path. + # This is normal, so we shouldn't warn the user about it + if "system-supplied DSO" not in path: + warn(f"Failed to find objfile or not a valid file format: {str(fne)}") + except RuntimeError as re: + warn(f"Not a valid file format: {str(re)}") + return + + +def exit_handler(_: "gdb.ExitedEvent") -> None: + """GDB event handler for exit cases.""" + global gef + reset_all_caches() + gef.session.qemu_mode = False + if gef.session.remote: + gef.session.remote.close() + del gef.session.remote + gef.session.remote = None + gef.session.remote_initializing = False + return + + +def memchanged_handler(_: "gdb.MemoryChangedEvent") -> None: + """GDB event handler for mem changes cases.""" + reset_all_caches() + return + + +def regchanged_handler(_: "gdb.RegisterChangedEvent") -> None: + """GDB event handler for reg changes cases.""" + reset_all_caches() + return + + +def get_terminal_size() -> Tuple[int, int]: + """Return the current terminal size.""" + if is_debug(): + return 600, 100 + + if platform.system() == "Windows": + from ctypes import create_string_buffer, windll + hStdErr = -12 + herr = windll.kernel32.GetStdHandle(hStdErr) + csbi = create_string_buffer(22) + res = windll.kernel32.GetConsoleScreenBufferInfo(herr, csbi) + if res: + _, _, _, _, _, left, top, right, bottom, _, _ = struct.unpack("hhhhHhhhhhh", csbi.raw) + tty_columns = right - left + 1 + tty_rows = bottom - top + 1 + return tty_rows, tty_columns + else: + return 600, 100 + else: + import fcntl + import termios + try: + tty_rows, tty_columns = struct.unpack("hh", fcntl.ioctl(1, termios.TIOCGWINSZ, "1234")) # type: ignore + return tty_rows, tty_columns + except OSError: + return 600, 100 + + +@lru_cache() +def is_64bit() -> bool: + """Checks if current target is 64bit.""" + return gef.arch.ptrsize == 8 + + +@lru_cache() +def is_32bit() -> bool: + """Checks if current target is 32bit.""" + return gef.arch.ptrsize == 4 + + +@lru_cache() +def is_x86_64() -> bool: + """Checks if current target is x86-64""" + return Elf.Abi.X86_64 in gef.arch.aliases + + +@lru_cache() +def is_x86_32(): + """Checks if current target is an x86-32""" + return Elf.Abi.X86_32 in gef.arch.aliases + + +@lru_cache() +def is_x86() -> bool: + return is_x86_32() or is_x86_64() + + +@lru_cache() +def is_arch(arch: Elf.Abi) -> bool: + return arch in gef.arch.aliases + + +def reset_architecture(arch: Optional[str] = None) -> None: + """Sets the current architecture. + If an architecture is explicitly specified by parameter, try to use that one. If this fails, an `OSError` + exception will occur. + If no architecture is specified, then GEF will attempt to determine automatically based on the current + ELF target. If this fails, an `OSError` exception will occur. + """ + global gef + arches = __registered_architectures__ + + # check if the architecture is forced by parameter + if arch: + try: + gef.arch = arches[arch]() + except KeyError: + raise OSError(f"Specified arch {arch.upper()} is not supported") + return + + gdb_arch = get_arch() + + preciser_arch = next((a for a in arches.values() if a.supports_gdb_arch(gdb_arch)), None) + if preciser_arch: + gef.arch = preciser_arch() + return + + # last resort, use the info from elf header to find it from the known architectures + try: + arch_name = gef.binary.e_machine if gef.binary else gdb_arch + gef.arch = arches[arch_name]() + except KeyError: + raise OSError(f"CPU type is currently not supported: {get_arch()}") + return + + +@lru_cache() +def cached_lookup_type(_type: str) -> Optional[gdb.Type]: + try: + return gdb.lookup_type(_type).strip_typedefs() + except RuntimeError: + return None + + +@deprecated("Use `gef.arch.ptrsize` instead") +def get_memory_alignment(in_bits: bool = False) -> int: + """Try to determine the size of a pointer on this system. + First, try to parse it out of the ELF header. + Next, use the size of `size_t`. + Finally, try the size of $pc. + If `in_bits` is set to True, the result is returned in bits, otherwise in + bytes.""" + res = cached_lookup_type("size_t") + if res is not None: + return res.sizeof if not in_bits else res.sizeof * 8 + + try: + return gdb.parse_and_eval("$pc").type.sizeof + except: + pass + + raise OSError("GEF is running under an unsupported mode") + + +def clear_screen(tty: str = "") -> None: + """Clear the screen.""" + global gef + if not tty: + gdb.execute("shell clear -x") + return + + # Since the tty can be closed at any time, a PermissionError exception can + # occur when `clear_screen` is called. We handle this scenario properly + try: + with open(tty, "wt") as f: + f.write("\x1b[H\x1b[J") + except PermissionError: + gef.ui.redirect_fd = None + gef.config["context.redirect"] = "" + return + + +def format_address(addr: int) -> str: + """Format the address according to its size.""" + memalign_size = gef.arch.ptrsize + addr = align_address(addr) + + if memalign_size == 4: + return f"0x{addr:08x}" + + return f"0x{addr:016x}" + + +def format_address_spaces(addr: int, left: bool = True) -> str: + """Format the address according to its size, but with spaces instead of zeroes.""" + width = gef.arch.ptrsize * 2 + 2 + addr = align_address(addr) + + if not left: + return f"{addr:#x}".rjust(width) + + return f"{addr:#x}".ljust(width) + + +def align_address(address: int) -> int: + """Align the provided address to the process's native length.""" + if gef.arch.ptrsize == 4: + return address & 0xFFFFFFFF + + return address & 0xFFFFFFFFFFFFFFFF + + +def align_address_to_size(address: int, align: int) -> int: + """Align the address to the given size.""" + return address + ((align - (address % align)) % align) + + +def align_address_to_page(address: int) -> int: + """Align the address to a page.""" + a = align_address(address) >> DEFAULT_PAGE_ALIGN_SHIFT + return a << DEFAULT_PAGE_ALIGN_SHIFT + + +def parse_address(address: str) -> int: + """Parse an address and return it as an Integer.""" + if is_hex(address): + return int(address, 16) + return to_unsigned_long(gdb.parse_and_eval(address)) + + +def is_in_x86_kernel(address: int) -> bool: + address = align_address(address) + memalign = gef.arch.ptrsize*8 - 1 + return (address >> memalign) == 0xF + + +def is_remote_debug() -> bool: + """"Return True is the current debugging session is running through GDB remote session.""" + return gef.session.remote_initializing or gef.session.remote is not None + + +def de_bruijn(alphabet: bytes, n: int) -> Generator[str, None, None]: + """De Bruijn sequence for alphabet and subsequences of length n (for compat. w/ pwnlib).""" + k = len(alphabet) + a = [0] * k * n + + def db(t: int, p: int) -> Generator[str, None, None]: + if t > n: + if n % p == 0: + for j in range(1, p + 1): + yield alphabet[a[j]] + else: + a[t] = a[t - p] + yield from db(t + 1, p) + + for j in range(a[t - p] + 1, k): + a[t] = j + yield from db(t + 1, t) + + return db(1, 1) + + +def generate_cyclic_pattern(length: int, cycle: int = 4) -> bytearray: + """Create a `length` byte bytearray of a de Bruijn cyclic pattern.""" + charset = bytearray(b"abcdefghijklmnopqrstuvwxyz") + return bytearray(itertools.islice(de_bruijn(charset, cycle), length)) + + +def safe_parse_and_eval(value: str) -> Optional["gdb.Value"]: + """GEF wrapper for gdb.parse_and_eval(): this function returns None instead of raising + gdb.error if the eval failed.""" + try: + return gdb.parse_and_eval(value) + except gdb.error: + pass + return None + + +@lru_cache() +def dereference(addr: int) -> Optional["gdb.Value"]: + """GEF wrapper for gdb dereference function.""" + try: + ulong_t = cached_lookup_type(use_stdtype()) or \ + cached_lookup_type(use_default_type()) or \ + cached_lookup_type(use_golang_type()) or \ + cached_lookup_type(use_rust_type()) + unsigned_long_type = ulong_t.pointer() + res = gdb.Value(addr).cast(unsigned_long_type).dereference() + # GDB does lazy fetch by default so we need to force access to the value + res.fetch_lazy() + return res + except gdb.MemoryError: + pass + return None + + +def gef_convenience(value: Union[str, bytes]) -> str: + """Defines a new convenience value.""" + global gef + var_name = f"$_gef{gef.session.convenience_vars_index:d}" + gef.session.convenience_vars_index += 1 + if isinstance(value, str): + gdb.execute(f"""set {var_name} = "{value}" """) + elif isinstance(value, bytes): + value_as_array = "{" + ", ".join(["%#.02x" % x for x in value]) + "}" + gdb.execute(f"""set {var_name} = {value_as_array} """) + else: + raise TypeError + return var_name + + +def parse_string_range(s: str) -> Iterator[int]: + """Parses an address range (e.g. 0x400000-0x401000)""" + addrs = s.split("-") + return map(lambda x: int(x, 16), addrs) + + +@lru_cache() +def is_syscall(instruction: Union[Instruction,int]) -> bool: + """Checks whether an instruction or address points to a system call.""" + if isinstance(instruction, int): + instruction = gef_current_instruction(instruction) + insn_str = instruction.mnemonic + if len(instruction.operands): + insn_str += f" {', '.join(instruction.operands)}" + return insn_str in gef.arch.syscall_instructions + + +# +# Deprecated API +# + +@deprecated("Use `gef.session.pie_breakpoints[num]`") +def gef_get_pie_breakpoint(num: int) -> "PieVirtualBreakpoint": + return gef.session.pie_breakpoints[num] + + +@deprecated("Use `str(gef.arch.endianness)` instead") +def endian_str() -> str: + return str(gef.arch.endianness) + + +@deprecated("Use `gef.config[key]`") +def get_gef_setting(name: str) -> Any: + return gef.config[name] + + +@deprecated("Use `gef.config[key] = value`") +def set_gef_setting(name: str, value: Any) -> None: + gef.config[name] = value + return + + +@deprecated("Use `gef.session.pagesize`") +def gef_getpagesize() -> int: + return gef.session.pagesize + + +@deprecated("Use `gef.session.canary`") +def gef_read_canary() -> Optional[Tuple[int, int]]: + return gef.session.canary + + +@deprecated("Use `gef.session.pid`") +def get_pid() -> int: + return gef.session.pid + + +@deprecated("Use `gef.session.file.name`") +def get_filename() -> str: + return gef.session.file.name + + +@deprecated("Use `gef.heap.main_arena`") +def get_glibc_arena() -> Optional[GlibcArena]: + return gef.heap.main_arena + + +@deprecated("Use `gef.arch.register(regname)`") +def get_register(regname) -> Optional[int]: + return gef.arch.register(regname) + + +@deprecated("Use `gef.memory.maps`") +def get_process_maps() -> List[Section]: + return gef.memory.maps + + +@deprecated("Use `reset_architecture`") +def set_arch(arch: Optional[str] = None, _: Optional[str] = None) -> None: + return reset_architecture(arch) + +# +# GDB event hooking +# + +@only_if_events_supported("cont") +def gef_on_continue_hook(func: Callable[["gdb.ThreadEvent"], None]) -> None: + gdb.events.cont.connect(func) + + +@only_if_events_supported("cont") +def gef_on_continue_unhook(func: Callable[["gdb.ThreadEvent"], None]) -> None: + gdb.events.cont.disconnect(func) + + +@only_if_events_supported("stop") +def gef_on_stop_hook(func: Callable[["gdb.StopEvent"], None]) -> None: + gdb.events.stop.connect(func) + + +@only_if_events_supported("stop") +def gef_on_stop_unhook(func: Callable[["gdb.StopEvent"], None]) -> None: + gdb.events.stop.disconnect(func) + + +@only_if_events_supported("exited") +def gef_on_exit_hook(func: Callable[["gdb.ExitedEvent"], None]) -> None: + gdb.events.exited.connect(func) + + +@only_if_events_supported("exited") +def gef_on_exit_unhook(func: Callable[["gdb.ExitedEvent"], None]) -> None: + gdb.events.exited.disconnect(func) + + +@only_if_events_supported("new_objfile") +def gef_on_new_hook(func: Callable[["gdb.NewObjFileEvent"], None]) -> None: + gdb.events.new_objfile.connect(func) + + +@only_if_events_supported("new_objfile") +def gef_on_new_unhook(func: Callable[["gdb.NewObjFileEvent"], None]) -> None: + gdb.events.new_objfile.disconnect(func) + + +@only_if_events_supported("clear_objfiles") +def gef_on_unload_objfile_hook(func: Callable[["gdb.ClearObjFilesEvent"], None]) -> None: + gdb.events.clear_objfiles.connect(func) + + +@only_if_events_supported("clear_objfiles") +def gef_on_unload_objfile_unhook(func: Callable[["gdb.ClearObjFilesEvent"], None]) -> None: + gdb.events.clear_objfiles.disconnect(func) + + +@only_if_events_supported("memory_changed") +def gef_on_memchanged_hook(func: Callable[["gdb.MemoryChangedEvent"], None]) -> None: + gdb.events.memory_changed.connect(func) + + +@only_if_events_supported("memory_changed") +def gef_on_memchanged_unhook(func: Callable[["gdb.MemoryChangedEvent"], None]) -> None: + gdb.events.memory_changed.disconnect(func) + + +@only_if_events_supported("register_changed") +def gef_on_regchanged_hook(func: Callable[["gdb.RegisterChangedEvent"], None]) -> None: + gdb.events.register_changed.connect(func) + + +@only_if_events_supported("register_changed") +def gef_on_regchanged_unhook(func: Callable[["gdb.RegisterChangedEvent"], None]) -> None: + gdb.events.register_changed.disconnect(func) + + +# +# Virtual breakpoints +# + +class PieVirtualBreakpoint: + """PIE virtual breakpoint (not real breakpoint).""" + + def __init__(self, set_func: Callable[[int], str], vbp_num: int, addr: int) -> None: + # set_func(base): given a base address return a + # "set breakpoint" gdb command string + self.set_func = set_func + self.vbp_num = vbp_num + # breakpoint num, 0 represents not instantiated yet + self.bp_num = 0 + self.bp_addr = 0 + # this address might be a symbol, just to know where to break + if isinstance(addr, int): + self.addr: Union[int, str] = hex(addr) + else: + self.addr = addr + return + + def instantiate(self, base: int) -> None: + if self.bp_num: + self.destroy() + + try: + res = gdb.execute(self.set_func(base), to_string=True) + except gdb.error as e: + err(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] + return + + def destroy(self) -> None: + if not self.bp_num: + err("Destroy PIE breakpoint not even set") + return + gdb.execute(f"delete {self.bp_num}") + self.bp_num = 0 + return + + +# +# Breakpoints +# + +class FormatStringBreakpoint(gdb.Breakpoint): + """Inspect stack for format string.""" + def __init__(self, spec: str, num_args: int) -> None: + super().__init__(spec, type=gdb.BP_BREAKPOINT, internal=False) + self.num_args = num_args + self.enabled = True + return + + def stop(self) -> bool: + reset_all_caches() + msg = [] + ptr, addr = gef.arch.get_ith_parameter(self.num_args) + addr = lookup_address(addr) + + if not addr.valid: + return False + + if addr.section.is_writable(): + content = gef.memory.read_cstring(addr.value) + name = addr.info.name if addr.info else addr.section.path + msg.append(Color.colorify("Format string helper", "yellow bold")) + msg.append(f"Possible insecure format string: {self.location}('{ptr}' {RIGHT_ARROW} {addr.value:#x}: '{content}')") + msg.append(f"Reason: Call to '{self.location}()' with format string argument in position " + f"#{self.num_args:d} is in page {addr.section.page_start:#x} ({name}) that has write permission") + push_context_message("warn", "\n".join(msg)) + return True + + return False + + +class StubBreakpoint(gdb.Breakpoint): + """Create a breakpoint to permanently disable a call (fork/alarm/signal/etc.).""" + + def __init__(self, func: str, retval: Optional[int]) -> None: + super().__init__(func, gdb.BP_BREAKPOINT, internal=False) + self.func = func + self.retval = retval + + m = f"All calls to '{self.func}' will be skipped" + if self.retval is not None: + m += f" (with return value set to {self.retval:#x})" + info(m) + return + + def stop(self) -> bool: + gdb.execute(f"return (unsigned int){self.retval:#x}") + ok(f"Ignoring call to '{self.func}' " + f"(setting return value to {self.retval:#x})") + return False + + +class ChangePermissionBreakpoint(gdb.Breakpoint): + """When hit, this temporary breakpoint will restore the original code, and position + $pc correctly.""" + + def __init__(self, loc: str, code: ByteString, pc: int) -> None: + super().__init__(loc, gdb.BP_BREAKPOINT, internal=False) + self.original_code = code + self.original_pc = pc + return + + def stop(self) -> bool: + info("Restoring original context") + gef.memory.write(self.original_pc, self.original_code, len(self.original_code)) + info("Restoring $pc") + gdb.execute(f"set $pc = {self.original_pc:#x}") + return True + + +class TraceMallocBreakpoint(gdb.Breakpoint): + """Track allocations done with malloc() or calloc().""" + + def __init__(self, name: str) -> None: + super().__init__(name, gdb.BP_BREAKPOINT, internal=True) + self.silent = True + self.name = name + return + + def stop(self) -> bool: + reset_all_caches() + _, size = gef.arch.get_ith_parameter(0) + self.retbp = TraceMallocRetBreakpoint(size, self.name) + return False + + +class TraceMallocRetBreakpoint(gdb.FinishBreakpoint): + """Internal temporary breakpoint to retrieve the return value of malloc().""" + + def __init__(self, size: int, name: str) -> None: + super().__init__(gdb.newest_frame(), internal=True) + self.size = size + self.name = name + self.silent = True + return + + def stop(self) -> bool: + if self.return_value: + loc = int(self.return_value) + else: + loc = parse_address(gef.arch.return_register) + + size = self.size + ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - {self.name}({size})={loc:#x}") + check_heap_overlap = gef.config["heap-analysis-helper.check_heap_overlap"] + + # pop from free-ed list if it was in it + if gef.session.heap_freed_chunks: + idx = 0 + for item in gef.session.heap_freed_chunks: + addr = item[0] + if addr == loc: + gef.session.heap_freed_chunks.remove(item) + continue + idx += 1 + + # pop from uaf watchlist + if gef.session.heap_uaf_watchpoints: + idx = 0 + for wp in gef.session.heap_uaf_watchpoints: + wp_addr = wp.address + if loc <= wp_addr < loc + size: + gef.session.heap_uaf_watchpoints.remove(wp) + wp.enabled = False + continue + idx += 1 + + item = (loc, size) + + if check_heap_overlap: + # seek all the currently allocated chunks, read their effective size and check for overlap + msg = [] + align = gef.arch.ptrsize + for chunk_addr, _ in gef.session.heap_allocated_chunks: + current_chunk = GlibcChunk(chunk_addr) + current_chunk_size = current_chunk.size + + if chunk_addr <= loc < chunk_addr + current_chunk_size: + offset = loc - chunk_addr - 2*align + if offset < 0: continue # false positive, discard + + msg.append(Color.colorify("Heap-Analysis", "yellow bold")) + msg.append("Possible heap overlap detected") + msg.append(f"Reason {RIGHT_ARROW} new allocated chunk {loc:#x} (of size {size:d}) overlaps in-used chunk {chunk_addr:#x} (of size {current_chunk_size:#x})") + msg.append(f"Writing {offset:d} bytes from {chunk_addr:#x} will reach chunk {loc:#x}") + msg.append(f"Payload example for chunk {chunk_addr:#x} (to overwrite {loc:#x} headers):") + msg.append(" data = 'A'*{0:d} + 'B'*{1:d} + 'C'*{1:d}".format(offset, align)) + push_context_message("warn", "\n".join(msg)) + return True + + # add it to alloc-ed list + gef.session.heap_allocated_chunks.append(item) + return False + + +class TraceReallocBreakpoint(gdb.Breakpoint): + """Track re-allocations done with realloc().""" + + def __init__(self) -> None: + super().__init__("__libc_realloc", gdb.BP_BREAKPOINT, internal=True) + self.silent = True + return + + def stop(self) -> bool: + _, ptr = gef.arch.get_ith_parameter(0) + _, size = gef.arch.get_ith_parameter(1) + self.retbp = TraceReallocRetBreakpoint(ptr, size) + return False + + +class TraceReallocRetBreakpoint(gdb.FinishBreakpoint): + """Internal temporary breakpoint to retrieve the return value of realloc().""" + + def __init__(self, ptr: int, size: int) -> None: + super().__init__(gdb.newest_frame(), internal=True) + self.ptr = ptr + self.size = size + self.silent = True + return + + def stop(self) -> bool: + if self.return_value: + newloc = int(self.return_value) + else: + newloc = parse_address(gef.arch.return_register) + + if newloc != self: + ok("{} - realloc({:#x}, {})={}".format(Color.colorify("Heap-Analysis", "yellow bold"), + self.ptr, self.size, + Color.colorify(f"{newloc:#x}", "green"),)) + else: + ok("{} - realloc({:#x}, {})={}".format(Color.colorify("Heap-Analysis", "yellow bold"), + self.ptr, self.size, + Color.colorify(f"{newloc:#x}", "red"),)) + + item = (newloc, self.size) + + try: + # check if item was in alloc-ed list + idx = [x for x, y in gef.session.heap_allocated_chunks].index(self.ptr) + # if so pop it out + item = gef.session.heap_allocated_chunks.pop(idx) + except ValueError: + if is_debug(): + warn(f"Chunk {self.ptr:#x} was not in tracking list") + finally: + # add new item to alloc-ed list + gef.session.heap_allocated_chunks.append(item) + + return False + + +class TraceFreeBreakpoint(gdb.Breakpoint): + """Track calls to free() and attempts to detect inconsistencies.""" + + def __init__(self) -> None: + super().__init__("__libc_free", gdb.BP_BREAKPOINT, internal=True) + self.silent = True + return + + def stop(self) -> bool: + reset_all_caches() + _, addr = gef.arch.get_ith_parameter(0) + msg = [] + check_free_null = gef.config["heap-analysis-helper.check_free_null"] + check_double_free = gef.config["heap-analysis-helper.check_double_free"] + check_weird_free = gef.config["heap-analysis-helper.check_weird_free"] + check_uaf = gef.config["heap-analysis-helper.check_uaf"] + + ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - free({addr:#x})") + if addr == 0: + if check_free_null: + msg.append(Color.colorify("Heap-Analysis", "yellow bold")) + msg.append(f"Attempting to free(NULL) at {gef.arch.pc:#x}") + msg.append("Reason: if NULL page is allocatable, this can lead to code execution.") + push_context_message("warn", "\n".join(msg)) + return True + return False + + if addr in [x for (x, y) in gef.session.heap_freed_chunks]: + if check_double_free: + msg.append(Color.colorify("Heap-Analysis", "yellow bold")) + msg.append(f"Double-free detected {RIGHT_ARROW} free({addr:#x}) is called at {gef.arch.pc:#x} but is already in the free-ed list") + msg.append("Execution will likely crash...") + push_context_message("warn", "\n".join(msg)) + return True + return False + + # if here, no error + # 1. move alloc-ed item to free list + try: + # pop from alloc-ed list + idx = [x for x, y in gef.session.heap_allocated_chunks].index(addr) + item = gef.session.heap_allocated_chunks.pop(idx) + + except ValueError: + if check_weird_free: + msg.append(Color.colorify("Heap-Analysis", "yellow bold")) + msg.append("Heap inconsistency detected:") + msg.append(f"Attempting to free an unknown value: {addr:#x}") + push_context_message("warn", "\n".join(msg)) + return True + return False + + # 2. add it to free-ed list + gef.session.heap_freed_chunks.append(item) + + self.retbp = None + if check_uaf: + # 3. (opt.) add a watchpoint on pointer + self.retbp = TraceFreeRetBreakpoint(addr) + return False + + +class TraceFreeRetBreakpoint(gdb.FinishBreakpoint): + """Internal temporary breakpoint to track free()d values.""" + + def __init__(self, addr: int) -> None: + super().__init__(gdb.newest_frame(), internal=True) + self.silent = True + self.addr = addr + return + + def stop(self) -> bool: + reset_all_caches() + wp = UafWatchpoint(self.addr) + gef.session.heap_uaf_watchpoints.append(wp) + return False + + +class UafWatchpoint(gdb.Breakpoint): + """Custom watchpoints set TraceFreeBreakpoint() to monitor free()d pointers being used.""" + + def __init__(self, addr: int) -> None: + super().__init__(f"*{addr:#x}", gdb.BP_WATCHPOINT, internal=True) + self.address = addr + self.silent = True + self.enabled = True + return + + def stop(self) -> bool: + """If this method is triggered, we likely have a UaF. Break the execution and report it.""" + reset_all_caches() + frame = gdb.selected_frame() + if frame.name() in ("_int_malloc", "malloc_consolidate", "__libc_calloc", ): + return False + + # software watchpoints stop after the next statement (see + # https://sourceware.org/gdb/onlinedocs/gdb/Set-Watchpoints.html) + pc = gdb_get_nth_previous_instruction_address(gef.arch.pc, 2) + insn = gef_current_instruction(pc) + msg = [] + msg.append(Color.colorify("Heap-Analysis", "yellow bold")) + msg.append(f"Possible Use-after-Free in '{get_filepath()}': " + f"pointer {self.address:#x} was freed, but is attempted to be used at {pc:#x}") + msg.append(f"{insn.address:#x} {insn.mnemonic} {Color.yellowify(', '.join(insn.operands))}") + push_context_message("warn", "\n".join(msg)) + return True + + +class EntryBreakBreakpoint(gdb.Breakpoint): + """Breakpoint used internally to stop execution at the most convenient entry point.""" + + def __init__(self, location: str) -> None: + super().__init__(location, gdb.BP_BREAKPOINT, internal=True, temporary=True) + self.silent = True + return + + def stop(self) -> bool: + reset_all_caches() + return True + + +class NamedBreakpoint(gdb.Breakpoint): + """Breakpoint which shows a specified name, when hit.""" + + def __init__(self, location: str, name: str) -> None: + super().__init__(spec=location, type=gdb.BP_BREAKPOINT, internal=False, temporary=False) + self.name = name + self.loc = location + return + + def stop(self) -> bool: + reset_all_caches() + push_context_message("info", f"Hit breakpoint {self.loc} ({Color.colorify(self.name, 'red bold')})") + return True + + +# +# Context Panes +# + +def register_external_context_pane(pane_name: str, display_pane_function: Callable[[], None], pane_title_function: Callable[[], Optional[str]], condition : Optional[Callable[[], bool]] = None) -> None: + """ + Registering function for new GEF Context View. + pane_name: a string that has no spaces (used in settings) + display_pane_function: a function that uses gef_print() to print strings + pane_title_function: a function that returns a string or None, which will be displayed as the title. + If None, no title line is displayed. + condition: an optional callback: if not None, the callback will be executed first. If it returns true, + then only the pane title and content will displayed. Otherwise, it's simply skipped. + + Example usage for a simple text to show when we hit a syscall: + def only_syscall(): return gef_current_instruction(gef.arch.pc).is_syscall() + def display_pane(): + gef_print("Wow, I am a context pane!") + def pane_title(): + return "example:pane" + register_external_context_pane("example_pane", display_pane, pane_title, only_syscall) + """ + gef.gdb.add_context_pane(pane_name, display_pane_function, pane_title_function, condition) + return + + +# +# Commands +# +@deprecated("Use `register()`, and inherit from `GenericCommand` instead") +def register_external_command(cls: Type["GenericCommand"]) -> Type["GenericCommand"]: + """Registering function for new GEF (sub-)command to GDB.""" + return cls + +@deprecated("Use `register()`, and inherit from `GenericCommand` instead") +def register_command(cls: Type["GenericCommand"]) -> Type["GenericCommand"]: + """Decorator for registering new GEF (sub-)command to GDB.""" + return cls + +@deprecated("") +def register_priority_command(cls: Type["GenericCommand"]) -> Type["GenericCommand"]: + """Decorator for registering new command with priority, meaning that it must + loaded before the other generic commands.""" + return cls + + +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__))) + __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__))) + __registered_functions__.add(cls) + return cls + + raise TypeError(f"`{cls.__class__}` is an illegal class for `register`") + + +class GenericCommand(gdb.Command): + """This is an abstract class for invoking commands, should not be instantiated.""" + + _cmdline_: str + _syntax_: str + _example_: Union[str, List[str]] = "" + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + attributes = ("_cmdline_", "_syntax_", ) + if not all(map(lambda x: hasattr(cls, x), attributes)): + raise NotImplementedError + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.pre_load() + syntax = Color.yellowify("\nSyntax: ") + self._syntax_ + example = Color.yellowify("\nExamples: \n\t") + if isinstance(self._example_, list): + example += "\n\t".join(self._example_) + elif isinstance(self._example_, str): + example += self._example_ + self.__doc__ = self.__doc__.replace(" "*4, "") + syntax + example + self.repeat = False + self.repeat_count = 0 + self.__last_command = None + command_type = kwargs.setdefault("command", gdb.COMMAND_OBSCURE) + complete_type = kwargs.setdefault("complete", gdb.COMPLETE_NONE) + prefix = kwargs.setdefault("prefix", False) + super().__init__(self._cmdline_, command_type, complete_type, prefix) + self.post_load() + return + + def invoke(self, args: str, from_tty: bool) -> None: + try: + argv = gdb.string_to_argv(args) + self.__set_repeat_count(argv, from_tty) + bufferize(self.do_invoke)(argv) + except Exception as e: + # Note: since we are intercepting cleaning exceptions here, commands preferably should avoid + # catching generic Exception, but rather specific ones. This is allows a much cleaner use. + if is_debug(): + show_last_exception() + else: + err(f"Command '{self._cmdline_}' failed to execute properly, reason: {e}") + return + + def usage(self) -> None: + err(f"Syntax\n{self._syntax_}") + return + + def do_invoke(self, argv: List[str]) -> None: + raise NotImplementedError + + def pre_load(self) -> None: + return + + def post_load(self) -> None: + return + + def __get_setting_name(self, name: str) -> str: + clsname = self.__class__._cmdline_.replace(" ", "-") + return f"{clsname}.{name}" + + def __iter__(self) -> Generator[str, None, None]: + for key in gef.config.keys(): + if key.startswith(self._cmdline_): + yield key.replace(f"{self._cmdline_}.", "", 1) + + @property + def settings(self) -> List[str]: + """Return the list of settings for this command.""" + return list(iter(self)) + + @deprecated(f"Use `self[setting_name]` instead") + def get_setting(self, name: str) -> Any: + return self.__getitem__(name) + + def __getitem__(self, name: str) -> Any: + key = self.__get_setting_name(name) + return gef.config[key] + + @deprecated(f"Use `setting_name in self` instead") + def has_setting(self, name: str) -> bool: + return self.__contains__(name) + + def __contains__(self, name: str) -> bool: + return self.__get_setting_name(name) in gef.config + + @deprecated(f"Use `self[setting_name] = value` instead") + def add_setting(self, name: str, value: Tuple[Any, type, str], description: str = "") -> None: + return self.__setitem__(name, (value, type(value), description)) + + def __setitem__(self, name: str, value: Union[Any, Tuple[Any, str]]) -> None: + # make sure settings are always associated to the root command (which derives from GenericCommand) + if "GenericCommand" not in [x.__name__ for x in self.__class__.__bases__]: + return + key = self.__get_setting_name(name) + if key in gef.config: + setting = gef.config.raw_entry(key) + setting.value = value + else: + if len(value) == 1: + gef.config[key] = GefSetting(value[0]) + elif len(value) == 2: + gef.config[key] = GefSetting(value[0], description=value[1]) + return + + @deprecated(f"Use `del self[setting_name]` instead") + def del_setting(self, name: str) -> None: + return self.__delitem__(name) + + def __delitem__(self, name: str) -> None: + del gef.config[self.__get_setting_name(name)] + return + + def __set_repeat_count(self, argv: List[str], from_tty: bool) -> None: + if not from_tty: + self.repeat = False + self.repeat_count = 0 + return + + command = gdb.execute("show commands", to_string=True).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 + return + + +@register +class VersionCommand(GenericCommand): + """Display GEF version info.""" + + _cmdline_ = "version" + _syntax_ = f"{_cmdline_}" + _example_ = f"{_cmdline_}" + + def do_invoke(self, argv: List[str]) -> None: + gef_fpath = pathlib.Path(inspect.stack()[0][1]).expanduser().absolute() + gef_dir = gef_fpath.parent + with gef_fpath.open("rb") as f: + gef_hash = hashlib.sha256(f.read()).hexdigest() + + if os.access(f"{gef_dir}/.git", os.X_OK): + ver = subprocess.check_output("git log --format='%H' -n 1 HEAD", cwd=gef_dir, shell=True).decode("utf8").strip() + extra = "dirty" if len(subprocess.check_output("git ls-files -m", cwd=gef_dir, shell=True).decode("utf8").strip()) else "clean" + gef_print(f"GEF: rev:{ver} (Git - {extra})") + else: + gef_blob_hash = subprocess.check_output(f"git hash-object {gef_fpath}", shell=True).decode().strip() + gef_print("GEF: (Standalone)") + gef_print(f"Blob Hash({gef_fpath}): {gef_blob_hash}") + gef_print(f"SHA256({gef_fpath}): {gef_hash}") + gef_print(f"GDB: {gdb.VERSION}") + py_ver = f"{sys.version_info.major:d}.{sys.version_info.minor:d}" + gef_print(f"GDB-Python: {py_ver}") + + if "full" in argv: + gef_print(f"Loaded commands: {', '.join(gef.gdb.loaded_command_names)}") + return + + +@register +class PrintFormatCommand(GenericCommand): + """Print bytes format in commonly used formats, such as literals in high level languages.""" + + valid_formats = ("py", "c", "js", "asm", "hex", "bytearray") + valid_bitness = (8, 16, 32, 64) + + _cmdline_ = "print-format" + _aliases_ = ["pf",] + _syntax_ = (f"{_cmdline_} [--lang LANG] [--bitlen SIZE] [(--length,-l) LENGTH] [--clip] LOCATION" + f"\t--lang LANG specifies the output format for programming language (available: {valid_formats!s}, default 'py')." + f"\t--bitlen SIZE specifies size of bit (possible values: {valid_bitness!s}, default is 8)." + "\t--length LENGTH specifies length of array (default is 256)." + "\t--clip The output data will be copied to clipboard" + "\tLOCATION specifies where the address of bytes is stored.") + _example_ = f"{_cmdline_} --lang py -l 16 $rsp" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + self["max_size_preview"] = (10, "max size preview of bytes") + return + + @property + def format_matrix(self) -> Dict[int, Tuple[str, str, str]]: + # `gef.arch.endianness` is a runtime property, should not be defined as a class property + return { + 8: (f"{gef.arch.endianness}B", "char", "db"), + 16: (f"{gef.arch.endianness}H", "short", "dw"), + 32: (f"{gef.arch.endianness}I", "int", "dd"), + 64: (f"{gef.arch.endianness}Q", "long long", "dq"), + } + + @only_if_gdb_running + @parse_arguments({"location": "$pc", }, {("--length", "-l"): 256, "--bitlen": 0, "--lang": "py", "--clip": True,}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + """Default value for print-format command.""" + args: argparse.Namespace = kwargs["arguments"] + args.bitlen = args.bitlen or gef.arch.ptrsize * 2 + + valid_bitlens = self.format_matrix.keys() + if args.bitlen not in valid_bitlens: + err(f"Size of bit must be in: {valid_bitlens!s}") + return + + if args.lang not in self.valid_formats: + err(f"Language must be in: {self.valid_formats!s}") + return + + start_addr = parse_address(args.location) + size = int(args.bitlen / 8) + end_addr = start_addr + args.length * size + fmt = self.format_matrix[args.bitlen][0] + data = [] + + if args.lang != "bytearray": + for addr in range(start_addr, end_addr, size): + value = struct.unpack(fmt, gef.memory.read(addr, size))[0] + data += [value] + sdata = ", ".join(map(hex, data)) + else: + sdata = "" + + if args.lang == "bytearray": + data = gef.memory.read(start_addr, args.length) + preview = str(data[0:self["max_size_preview"]]) + out = f"Saved data {preview}... in '{gef_convenience(data)}'" + elif args.lang == "py": + out = f"buf = [{sdata}]" + elif args.lang == "c": + c_type = self.format_matrix[args.bitlen][1] + out = f"unsigned {c_type} buf[{args.length}] = {{{sdata}}};" + elif args.lang == "js": + 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) + elif args.lang == "hex": + out = binascii.hexlify(gef.memory.read(start_addr, end_addr-start_addr)).decode() + else: + raise ValueError(f"Invalid format: {args.lang}") + + if args.clip: + if copy_to_clipboard(gef_pybytes(out)): + info("Copied to clipboard") + else: + warn("There's a problem while copying") + + gef_print(out) + return + + +@register +class PieCommand(GenericCommand): + """PIE breakpoint support.""" + + _cmdline_ = "pie" + _syntax_ = f"{_cmdline_} (breakpoint|info|delete|run|attach|remote)" + + def __init__(self) -> None: + super().__init__(prefix=True) + return + + def do_invoke(self, argv: List[str]) -> None: + if not argv: + self.usage() + return + + +@register +class PieBreakpointCommand(GenericCommand): + """Set a PIE breakpoint at an offset from the target binaries base address.""" + + _cmdline_ = "pie breakpoint" + _syntax_ = f"{_cmdline_} OFFSET" + + @parse_arguments({"offset": ""}, {}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if not args.offset: + self.usage() + return + + addr = parse_address(args.offset) + self.set_pie_breakpoint(lambda base: f"b *{base + addr}", addr) + + # When the process is already on, set real breakpoints immediately + if is_alive(): + vmmap = gef.memory.maps + base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0] + for bp_ins in gef.session.pie_breakpoints.values(): + bp_ins.instantiate(base_address) + return + + @staticmethod + def set_pie_breakpoint(set_func: Callable[[int], str], addr: int) -> None: + gef.session.pie_breakpoints[gef.session.pie_counter] = PieVirtualBreakpoint(set_func, gef.session.pie_counter, addr) + gef.session.pie_counter += 1 + return + + +@register +class PieInfoCommand(GenericCommand): + """Display breakpoint info.""" + + _cmdline_ = "pie info" + _syntax_ = f"{_cmdline_} BREAKPOINT" + + @parse_arguments({"breakpoints": [-1,]}, {}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if args.breakpoints[0] == -1: + # No breakpoint info needed + bps = gef.session.pie_breakpoints.values() + else: + bps = [gef.session.pie_breakpoints[x] + for x in args.breakpoints + if x in gef.session.pie_breakpoints] + + lines = ["{:6s} {:6s} {:18s}".format("VNum","Num","Addr")] + lines += [ + f"{x.vbp_num:6d} {str(x.bp_num) if x.bp_num else 'N/A':6s} {x.addr:18s}" for x in bps + ] + gef_print("\n".join(lines)) + return + + +@register +class PieDeleteCommand(GenericCommand): + """Delete a PIE breakpoint.""" + + _cmdline_ = "pie delete" + _syntax_ = f"{_cmdline_} [BREAKPOINT]" + + @parse_arguments({"breakpoints": [-1,]}, {}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + global gef + args : argparse.Namespace = kwargs["arguments"] + if args.breakpoints[0] == -1: + # no arg, delete all + to_delete = list(gef.session.pie_breakpoints.values()) + self.delete_bp(to_delete) + else: + self.delete_bp([gef.session.pie_breakpoints[x] + for x in args.breakpoints + if x in gef.session.pie_breakpoints]) + return + + + @staticmethod + def delete_bp(breakpoints: List[PieVirtualBreakpoint]) -> None: + global gef + for bp in breakpoints: + # delete current real breakpoints if exists + if bp.bp_num: + gdb.execute(f"delete {bp.bp_num}") + # delete virtual breakpoints + del gef.session.pie_breakpoints[bp.vbp_num] + return + + +@register +class PieRunCommand(GenericCommand): + """Run process with PIE breakpoint support.""" + + _cmdline_ = "pie run" + _syntax_ = _cmdline_ + + def do_invoke(self, argv: List[str]) -> None: + global gef + fpath = get_filepath() + if not fpath: + warn("No executable to debug, use `file` to load a binary") + return + + if not os.access(fpath, os.X_OK): + warn(f"The file '{fpath}' is not executable.") + return + + if is_alive(): + warn("gdb is already running. Restart process.") + + # get base address + gdb.execute("set stop-on-solib-events 1") + hide_context() + gdb.execute(f"run {' '.join(argv)}") + unhide_context() + gdb.execute("set stop-on-solib-events 0") + vmmap = gef.memory.maps + base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0] + info(f"base address {hex(base_address)}") + + # modify all breakpoints + for bp_ins in gef.session.pie_breakpoints.values(): + bp_ins.instantiate(base_address) + + try: + gdb.execute("continue") + except gdb.error as e: + err(e) + gdb.execute("kill") + return + + +@register +class PieAttachCommand(GenericCommand): + """Do attach with PIE breakpoint support.""" + + _cmdline_ = "pie attach" + _syntax_ = f"{_cmdline_} PID" + + 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) + return + # after attach, we are stopped so that we can + # get base address to modify our breakpoint + vmmap = gef.memory.maps + base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0] + + for bp_ins in gef.session.pie_breakpoints.values(): + bp_ins.instantiate(base_address) + gdb.execute("context") + return + + +@register +class PieRemoteCommand(GenericCommand): + """Attach to a remote connection with PIE breakpoint support.""" + + _cmdline_ = "pie remote" + _syntax_ = f"{_cmdline_} REMOTE" + + def do_invoke(self, argv: List[str]) -> None: + try: + gdb.execute(f"gef-remote {' '.join(argv)}") + except gdb.error as e: + err(e) + return + # after remote attach, we are stopped so that we can + # get base address to modify our breakpoint + vmmap = gef.memory.maps + base_address = [x.page_start for x in vmmap if x.realpath == get_filepath()][0] + + for bp_ins in gef.session.pie_breakpoints.values(): + bp_ins.instantiate(base_address) + gdb.execute("context") + return + + +@register +class SmartEvalCommand(GenericCommand): + """SmartEval: Smart eval (vague approach to mimic WinDBG `?`).""" + + _cmdline_ = "$" + _syntax_ = f"{_cmdline_} EXPR\n{_cmdline_} ADDRESS1 ADDRESS2" + _example_ = (f"\n{_cmdline_} $pc+1" + f"\n{_cmdline_} 0x00007ffff7a10000 0x00007ffff7bce000") + + def do_invoke(self, argv: List[str]) -> None: + argc = len(argv) + if argc == 1: + self.evaluate(argv) + return + + if argc == 2: + self.distance(argv) + return + + def evaluate(self, expr: List[str]) -> None: + def show_as_int(i: int) -> None: + off = gef.arch.ptrsize*8 + def comp2_x(x: Any) -> str: return f"{(x + (1 << off)) % (1 << off):x}" + def comp2_b(x: Any) -> str: return f"{(x + (1 << off)) % (1 << off):b}" + + try: + s_i = comp2_x(res) + s_i = s_i.rjust(len(s_i)+1, "0") if len(s_i)%2 else s_i + gef_print(f"{i:d}") + gef_print("0x" + comp2_x(res)) + gef_print("0b" + comp2_b(res)) + gef_print(f"{binascii.unhexlify(s_i)}") + gef_print(f"{binascii.unhexlify(s_i)[::-1]}") + except: + pass + return + + parsed_expr = [] + for xp in expr: + try: + xp = gdb.parse_and_eval(xp) + xp = int(xp) + parsed_expr.append(f"{xp:d}") + except gdb.error: + parsed_expr.append(str(xp)) + + try: + res = eval(" ".join(parsed_expr)) + if isinstance(res, int): + show_as_int(res) + else: + gef_print(f"{res}") + except SyntaxError: + gef_print(" ".join(parsed_expr)) + return + + def distance(self, args: Tuple[str, str]) -> None: + try: + x = int(args[0], 16) if is_hex(args[0]) else int(args[0]) + y = int(args[1], 16) if is_hex(args[1]) else int(args[1]) + gef_print(f"{abs(x - y)}") + except ValueError: + warn(f"Distance requires 2 numbers: {self._cmdline_} 0 0xffff") + return + + +@register +class CanaryCommand(GenericCommand): + """Shows the canary value of the current process.""" + + _cmdline_ = "canary" + _syntax_ = _cmdline_ + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + self.dont_repeat() + + has_canary = Elf(get_filepath()).checksec["Canary"] + if not has_canary: + warn("This binary was not compiled with SSP.") + return + + res = gef.session.canary + if not res: + err("Failed to get the canary") + return + + canary, location = res + info(f"The canary of process {gef.session.pid} is at {location:#x}, value is {canary:#x}") + return + + +@register +class ProcessStatusCommand(GenericCommand): + """Extends the info given by GDB `info proc`, by giving an exhaustive description of the + process status (file descriptors, ancestor, descendants, etc.).""" + + _cmdline_ = "process-status" + _syntax_ = _cmdline_ + _aliases_ = ["status", ] + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_NONE) + return + + @only_if_gdb_running + @only_if_gdb_target_local + def do_invoke(self, argv: List[str]) -> None: + self.show_info_proc() + self.show_ancestor() + self.show_descendants() + self.show_fds() + self.show_connections() + return + + def get_state_of(self, pid: int) -> Dict[str, str]: + res = {} + with open(f"/proc/{pid}/status", "r") as f: + file = f.readlines() + for line in file: + key, value = line.split(":", 1) + res[key.strip()] = value.strip() + return res + + def get_cmdline_of(self, pid: int) -> str: + with open(f"/proc/{pid}/cmdline", "r") as f: + return f.read().replace("\x00", "\x20").strip() + + def get_process_path_of(self, pid: int) -> str: + return os.readlink(f"/proc/{pid}/exe") + + def get_children_pids(self, pid: int) -> List[int]: + cmd = [gef.session.constants["ps"], "-o", "pid", "--ppid", f"{pid}", "--noheaders"] + try: + return [int(x) for x in gef_execute_external(cmd, as_list=True)] + except Exception: + return [] + + def show_info_proc(self) -> None: + info("Process Information") + pid = gef.session.pid + cmdline = self.get_cmdline_of(pid) + gef_print(f"\tPID {RIGHT_ARROW} {pid}", + f"\tExecutable {RIGHT_ARROW} {self.get_process_path_of(pid)}", + f"\tCommand line {RIGHT_ARROW} '{cmdline}'", sep="\n") + return + + def show_ancestor(self) -> None: + info("Parent Process Information") + ppid = int(self.get_state_of(gef.session.pid)["PPid"]) + state = self.get_state_of(ppid) + cmdline = self.get_cmdline_of(ppid) + gef_print(f"\tParent PID {RIGHT_ARROW} {state['Pid']}", + f"\tCommand line {RIGHT_ARROW} '{cmdline}'", sep="\n") + return + + def show_descendants(self) -> None: + info("Children Process Information") + children = self.get_children_pids(gef.session.pid) + if not children: + gef_print("\tNo child process") + return + + for child_pid in children: + state = self.get_state_of(child_pid) + pid = state["Pid"] + gef_print(f"\tPID {RIGHT_ARROW} {pid} (Name: '{self.get_process_path_of(pid)}', CmdLine: '{self.get_cmdline_of(pid)}')") + return + + def show_fds(self) -> None: + pid = gef.session.pid + path = f"/proc/{pid:d}/fd" + + info("File Descriptors:") + items = os.listdir(path) + if not items: + gef_print("\tNo FD opened") + return + + for fname in items: + fullpath = os.path.join(path, fname) + if os.path.islink(fullpath): + gef_print(f"\t{fullpath} {RIGHT_ARROW} {os.readlink(fullpath)}") + return + + def list_sockets(self, pid: int) -> List[int]: + sockets = [] + path = f"/proc/{pid:d}/fd" + items = os.listdir(path) + for fname in items: + fullpath = os.path.join(path, fname) + if os.path.islink(fullpath) and os.readlink(fullpath).startswith("socket:"): + p = os.readlink(fullpath).replace("socket:", "")[1:-1] + sockets.append(int(p)) + return sockets + + def parse_ip_port(self, addr: str) -> Tuple[str, int]: + ip, port = addr.split(":") + return socket.inet_ntoa(struct.pack(" None: + # https://github.com/torvalds/linux/blob/v4.7/include/net/tcp_states.h#L16 + tcp_states_str = { + 0x01: "TCP_ESTABLISHED", + 0x02: "TCP_SYN_SENT", + 0x03: "TCP_SYN_RECV", + 0x04: "TCP_FIN_WAIT1", + 0x05: "TCP_FIN_WAIT2", + 0x06: "TCP_TIME_WAIT", + 0x07: "TCP_CLOSE", + 0x08: "TCP_CLOSE_WAIT", + 0x09: "TCP_LAST_ACK", + 0x0A: "TCP_LISTEN", + 0x0B: "TCP_CLOSING", + 0x0C: "TCP_NEW_SYN_RECV", + } + + udp_states_str = { + 0x07: "UDP_LISTEN", + } + + info("Network Connections") + pid = gef.session.pid + sockets = self.list_sockets(pid) + if not sockets: + gef_print("\tNo open connections") + return + + entries = dict() + with open(f"/proc/{pid:d}/net/tcp", "r") as tcp: + entries["TCP"] = [x.split() for x in tcp.readlines()[1:]] + with open(f"/proc/{pid:d}/net/udp", "r") as udp: + entries["UDP"] = [x.split() for x in udp.readlines()[1:]] + + for proto in entries: + for entry in entries[proto]: + local, remote, state = entry[1:4] + inode = int(entry[9]) + if inode in sockets: + local = self.parse_ip_port(local) + remote = self.parse_ip_port(remote) + state = int(state, 16) + state_str = tcp_states_str[state] if proto == "TCP" else udp_states_str[state] + + gef_print(f"\t{local[0]}:{local[1]} {RIGHT_ARROW} {remote[0]}:{remote[1]} ({state_str})") + return + + +@register +class GefThemeCommand(GenericCommand): + """Customize GEF appearance.""" + + _cmdline_ = "theme" + _syntax_ = f"{_cmdline_} [KEY [VALUE]]" + + def __init__(self) -> None: + super().__init__(self._cmdline_) + self["context_title_line"] = ("gray", "Color of the borders in context window") + self["context_title_message"] = ("cyan", "Color of the title in context window") + self["default_title_line"] = ("gray", "Default color of borders") + self["default_title_message"] = ("cyan", "Default color of title") + self["table_heading"] = ("blue", "Color of the column headings to tables (e.g. vmmap)") + self["old_context"] = ("gray", "Color to use to show things such as code that is not immediately relevant") + self["disassemble_current_instruction"] = ("green", "Color to use to highlight the current $pc when disassembling") + self["dereference_string"] = ("yellow", "Color of dereferenced string") + self["dereference_code"] = ("gray", "Color of dereferenced code") + self["dereference_base_address"] = ("cyan", "Color of dereferenced address") + self["dereference_register_value"] = ("bold blue", "Color of dereferenced register") + self["registers_register_name"] = ("blue", "Color of the register name in the register window") + self["registers_value_changed"] = ("bold red", "Color of the changed register in the register window") + self["address_stack"] = ("pink", "Color to use when a stack address is found") + self["address_heap"] = ("green", "Color to use when a heap address is found") + self["address_code"] = ("red", "Color to use when a code address is found") + self["source_current_line"] = ("green", "Color to use for the current code line in the source window") + return + + def do_invoke(self, args: List[str]) -> None: + self.dont_repeat() + argc = len(args) + + if argc == 0: + for key in self.settings: + setting = self[key] + value = Color.colorify(setting, setting) + gef_print(f"{key:40s}: {value}") + return + + setting_name = args[0] + if not setting_name in self: + err("Invalid key") + return + + if argc == 1: + value = self[setting_name] + gef_print(f"{setting_name:40s}: {Color.colorify(value, value)}") + return + + colors = [color for color in args[1:] if color in Color.colors] + self[setting_name] = " ".join(colors) + return + + +class ExternalStructureManager: + class Structure: + def __init__(self, manager: "ExternalStructureManager", mod_path: pathlib.Path, struct_name: str) -> None: + self.manager = manager + self.module_path = mod_path + self.name = struct_name + self.class_type = self.__get_structure_class() + # if the symbol points to a class factory method and not a class + if not hasattr(self.class_type, "_fields_") and callable(self.class_type): + self.class_type = self.class_type(gef) + return + + def __str__(self) -> str: + return self.name + + def pprint(self) -> None: + res = [] + for _name, _type in self.class_type._fields_: + size = ctypes.sizeof(_type) + name = Color.colorify(_name, gef.config["pcustom.structure_name"]) + type = Color.colorify(_type.__name__, gef.config["pcustom.structure_type"]) + size = Color.colorify(hex(size), gef.config["pcustom.structure_size"]) + offset = Color.boldify(f"{getattr(self.class_type, _name).offset:04x}") + res.append(f"{offset} {name:32s} {type:16s} /* size={size} */") + gef_print("\n".join(res)) + return + + def __get_structure_class(self) -> Type: + """Returns a tuple of (class, instance) if modname!classname exists""" + fpath = self.module_path + spec = importlib.util.spec_from_file_location(fpath.stem, fpath) + module = importlib.util.module_from_spec(spec) + sys.modules[fpath.stem] = module + spec.loader.exec_module(module) + _class = getattr(module, self.name) + return _class + + def apply_at(self, address: int, max_depth: int, depth: int = 0) -> None: + """Apply (recursively if possible) the structure format to the given address.""" + if depth >= max_depth: + warn("maximum recursion level reached") + return + + # read the data at the specified address + _structure = self.class_type() + _sizeof_structure = ctypes.sizeof(_structure) + + try: + data = gef.memory.read(address, _sizeof_structure) + except gdb.MemoryError: + err(f"{' ' * depth}Cannot read memory {address:#x}") + return + + # deserialize the data + length = min(len(data), _sizeof_structure) + ctypes.memmove(ctypes.addressof(_structure), data, length) + + # pretty print all the fields (and call recursively if possible) + ptrsize = gef.arch.ptrsize + unpack = u32 if ptrsize == 4 else u64 + for field in _structure._fields_: + _name, _type = field + _value = getattr(_structure, _name) + _offset = getattr(self.class_type, _name).offset + + if ((ptrsize == 4 and _type is ctypes.c_uint32) + or (ptrsize == 8 and _type is ctypes.c_uint64) + or (ptrsize == ctypes.sizeof(ctypes.c_void_p) and _type is ctypes.c_void_p)): + # try to dereference pointers + _value = RIGHT_ARROW.join(dereference_from(_value)) + + line = f"{' ' * depth}" + line += f"{address:#x}+{_offset:#04x} {_name} : ".ljust(40) + line += f"{_value} ({_type.__name__})" + parsed_value = self.__get_ctypes_value(_structure, _name, _value) + if parsed_value: + line += f"{RIGHT_ARROW} {parsed_value}" + gef_print(line) + + if issubclass(_type, ctypes.Structure): + self.apply_at(address + _offset, max_depth, depth + 1) + elif _type.__name__.startswith("LP_"): + # Pointer to a structure of a different type + __sub_type_name = _type.__name__.lstrip("LP_") + result = self.manager.find(__sub_type_name) + if result: + _, __structure = result + __address = unpack(gef.memory.read(address + _offset, ptrsize)) + __structure.apply_at(__address, max_depth, depth + 1) + return + + def __get_ctypes_value(self, struct, item, value) -> str: + if not hasattr(struct, "_values_"): return "" + default = "" + for name, values in struct._values_: + if name != item: continue + if callable(values): + return values(value) + try: + for val, desc in values: + if value == val: return desc + if val is None: default = desc + except Exception as e: + err(f"Error parsing '{name}': {e}") + return default + + class Module(dict): + def __init__(self, manager: "ExternalStructureManager", path: pathlib.Path) -> None: + self.manager = manager + self.path = path + self.name = path.stem + self.raw = self.__load() + + for entry in self: + structure = ExternalStructureManager.Structure(manager, self.path, entry) + self[structure.name] = structure + return + + def __load(self) -> ModuleType: + """Load a custom module, and return it.""" + fpath = self.path + spec = importlib.util.spec_from_file_location(fpath.stem, fpath) + module = importlib.util.module_from_spec(spec) + sys.modules[fpath.stem] = module + spec.loader.exec_module(module) + return module + + def __str__(self) -> str: + return self.name + + def __iter__(self) -> Generator[str, None, None]: + _invalid = {"BigEndianStructure", "LittleEndianStructure", "Structure"} + for x in dir(self.raw): + if x in _invalid: continue + _attr = getattr(self.raw, x) + + # if it's a ctypes.Structure class, add it + if inspect.isclass(_attr) and issubclass(_attr, ctypes.Structure): + yield x + continue + + # also accept class factory functions + if callable(_attr) and _attr.__module__ == self.name and x.endswith("_t"): + yield x + continue + return + + class Modules(dict): + def __init__(self, manager: "ExternalStructureManager") -> None: + self.manager: "ExternalStructureManager" = manager + self.root: pathlib.Path = manager.path + + for entry in self.root.iterdir(): + if not entry.is_file(): continue + if entry.suffix != ".py": continue + if entry.name == "__init__.py": continue + module = ExternalStructureManager.Module(manager, entry) + self[module.name] = module + return + + def __contains__(self, structure_name: str) -> bool: + """Return True if the structure name is found in any of the modules""" + for module in self.values(): + if structure_name in module: + return True + return False + + def __init__(self) -> None: + self.clear_caches() + return + + def clear_caches(self) -> None: + self._path = None + self._modules = None + return + + @property + def modules(self) -> "ExternalStructureManager.Modules": + if not self._modules: + self._modules = ExternalStructureManager.Modules(self) + return self._modules + + @property + def path(self) -> pathlib.Path: + if not self._path: + self._path = pathlib.Path(gef.config["pcustom.struct_path"]).expanduser().absolute() + return self._path + + @property + def structures(self) -> Generator[Tuple["ExternalStructureManager.Module", "ExternalStructureManager.Structure"], None, None]: + for module in self.modules.values(): + for structure in module.values(): + yield module, structure + return + + @lru_cache() + def find(self, structure_name: str) -> Optional[Tuple["ExternalStructureManager.Module", "ExternalStructureManager.Structure"]]: + """Return the module and structure for the given structure name; `None` if the structure name was not found.""" + for module in self.modules.values(): + if structure_name in module: + return module, module[structure_name] + return None + + +@register +class PCustomCommand(GenericCommand): + """Dump user defined structure. + This command attempts to reproduce WinDBG awesome `dt` command for GDB and allows + to apply structures (from symbols or custom) directly to an address. + Custom structures can be defined in pure Python using ctypes, and should be stored + in a specific directory, whose path must be stored in the `pcustom.struct_path` + configuration setting.""" + + _cmdline_ = "pcustom" + _syntax_ = f"{_cmdline_} [list|edit |show ]| 0xADDRESS]" + + def __init__(self) -> None: + super().__init__(prefix=True) + self["struct_path"] = (str(pathlib.Path(gef.config["gef.tempdir"]) / "structs"), "Path to store/load the structure ctypes files") + self["max_depth"] = (4, "Maximum level of recursion supported") + self["structure_name"] = ("bold blue", "Color of the structure name") + self["structure_type"] = ("bold red", "Color of the attribute type") + self["structure_size"] = ("green", "Color of the attribute size") + return + + @parse_arguments({"type": "", "address": ""}, {}) + def do_invoke(self, *_: Any, **kwargs: Dict[str, Any]) -> None: + args : argparse.Namespace = kwargs["arguments"] + if not args.type: + gdb.execute("pcustom list") + return + + _, structname = self.explode_type(args.type) + + if not args.address: + gdb.execute(f"pcustom show {structname}") + return + + if not is_alive(): + err("Session is not active") + return + + manager = ExternalStructureManager() + address = parse_address(args.address) + result = manager.find(structname) + if not result: + err(f"No structure named '{structname}' found") + return + + _, structure = result + structure.apply_at(address, self["max_depth"]) + return + + def explode_type(self, arg: str) -> Tuple[str, str]: + modname, structname = arg.split(":", 1) if ":" in arg else (arg, arg) + structname = structname.split(".", 1)[0] if "." in structname else structname + return modname, structname + + +@register +class PCustomListCommand(PCustomCommand): + """PCustom: list available structures""" + + _cmdline_ = "pcustom list" + _syntax_ = f"{_cmdline_}" + + def __init__(self) -> None: + super().__init__() + return + + def do_invoke(self, _: List) -> None: + """Dump the list of all the structures and their respective.""" + manager = ExternalStructureManager() + info(f"Listing custom structures from '{manager.path}'") + 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()]) + __filename = Color.colorify(str(module.path), filename_color) + gef_print(f"{RIGHT_ARROW} {__filename} ({__modules})") + return + + +@register +class PCustomShowCommand(PCustomCommand): + """PCustom: show the content of a given structure""" + + _cmdline_ = "pcustom show" + _syntax_ = f"{_cmdline_} StructureName" + __aliases__ = ["pcustom create", "pcustom update"] + + def __init__(self) -> None: + super().__init__() + return + + def do_invoke(self, argv: List[str]) -> None: + if len(argv) == 0: + self.usage() + return + + _, structname = self.explode_type(argv[0]) + manager = ExternalStructureManager() + result = manager.find(structname) + if result: + _, structure = result + structure.pprint() + else: + err(f"No structure named '{structname}' found") + return + + +@register +class PCustomEditCommand(PCustomCommand): + """PCustom: edit the content of a given structure""" + + _cmdline_ = "pcustom edit" + _syntax_ = f"{_cmdline_} StructureName" + __aliases__ = ["pcustom create", "pcustom new", "pcustom update"] + + def __init__(self) -> None: + super().__init__() + return + + def do_invoke(self, argv: List[str]) -> None: + if len(argv) == 0: + self.usage() + return + + modname, structname = self.explode_type(argv[0]) + self.__create_or_edit_structure(modname, structname) + return + + def __create_or_edit_structure(self, mod_name: str, struct_name: str) -> int: + path = pathlib.Path(gef.config["pcustom.struct_path"]).expanduser() / f"{mod_name}.py" + if path.is_file(): + info(f"Editing '{path}'") + else: + ok(f"Creating '{path}' from template") + self.__create_template(struct_name, path) + + cmd = (os.getenv("EDITOR") or "nano").split() + cmd.append(str(path.absolute())) + return subprocess.call(cmd) + + def __create_template(self, structname: str, fpath: pathlib.Path) -> None: + template = f"""from ctypes import * + +class {structname}(Structure): + _fields_ = [] + + _values_ = [] +""" + with fpath.open("w") as f: + f.write(template) + return + + +@register +class ChangeFdCommand(GenericCommand): + """ChangeFdCommand: redirect file descriptor during runtime.""" + + _cmdline_ = "hijack-fd" + _syntax_ = f"{_cmdline_} FD_NUM NEW_OUTPUT" + _example_ = f"{_cmdline_} 2 /tmp/stderr_output.txt" + + @only_if_gdb_running + @only_if_gdb_target_local + def do_invoke(self, argv: List[str]) -> None: + if len(argv) != 2: + self.usage() + return + + if not os.access(f"/proc/{gef.session.pid:d}/fd/{argv[0]}", os.R_OK): + self.usage() + return + + old_fd = int(argv[0]) + new_output = argv[1] + + if ":" in new_output: + address = socket.gethostbyname(new_output.split(":")[0]) + port = int(new_output.split(":")[1]) + + AF_INET = 2 + SOCK_STREAM = 1 + res = gdb.execute(f"""call (int)socket({AF_INET}, {SOCK_STREAM}, 0)""", to_string=True) + new_fd = self.get_fd_from_result(res) + + # fill in memory with sockaddr_in struct contents + # we will do this in the stack, since connect() wants a pointer to a struct + vmmap = gef.memory.maps + stack_addr = [entry.page_start for entry in vmmap if entry.path == "[stack]"][0] + original_contents = gef.memory.read(stack_addr, 8) + + gef.memory.write(stack_addr, b"\x02\x00", 2) + gef.memory.write(stack_addr + 0x2, struct.pack(" int: + # Output example: $1 = 3 + res = gdb.execute(f"""p/d {int(res.split()[2], 0)}""", to_string=True) + return int(res.split()[2], 0) + + +@register +class ScanSectionCommand(GenericCommand): + """Search for addresses that are located in a memory mapping (haystack) that belonging + to another (needle).""" + + _cmdline_ = "scan" + _syntax_ = f"{_cmdline_} HAYSTACK NEEDLE" + _aliases_ = ["lookup",] + _example_ = f"\n{_cmdline_} stack libc" + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if len(argv) != 2: + self.usage() + return + + haystack = argv[0] + needle = argv[1] + + info(f"Searching for addresses in '{Color.yellowify(haystack)}' " + f"that point to '{Color.yellowify(needle)}'") + + if haystack == "binary": + haystack = get_filepath() + + if needle == "binary": + needle = get_filepath() + + needle_sections = [] + haystack_sections = [] + + if "0x" in haystack: + start, end = parse_string_range(haystack) + haystack_sections.append((start, end, "")) + + if "0x" in needle: + start, end = parse_string_range(needle) + needle_sections.append((start, end)) + + for sect in gef.memory.maps: + if haystack in sect.path: + haystack_sections.append((sect.page_start, sect.page_end, os.path.basename(sect.path))) + if needle in sect.path: + needle_sections.append((sect.page_start, sect.page_end)) + + step = gef.arch.ptrsize + unpack = u32 if step == 4 else u64 + + for hstart, hend, hname in haystack_sections: + try: + mem = gef.memory.read(hstart, hend - hstart) + except gdb.MemoryError: + continue + + for i in range(0, len(mem), step): + target = unpack(mem[i:i+step]) + for nstart, nend in needle_sections: + if target >= nstart and target < nend: + deref = DereferenceCommand.pprint_dereferenced(hstart, int(i / step)) + if hname != "": + name = Color.colorify(hname, "yellow") + gef_print(f"{name}: {deref}") + else: + gef_print(f" {deref}") + + return + + +@register +class SearchPatternCommand(GenericCommand): + """SearchPatternCommand: search a pattern in memory. If given an hex value (starting with 0x) + the command will also try to look for upwards cross-references to this address.""" + + _cmdline_ = "search-pattern" + _syntax_ = f"{_cmdline_} PATTERN [little|big] [section]" + _aliases_ = ["grep", "xref"] + _example_ = [f"{_cmdline_} AAAAAAAA", + f"{_cmdline_} 0x555555554000 little stack", + f"{_cmdline_} AAAA 0x600000-0x601000", + f"{_cmdline_} --regex 0x401000 0x401500 ([\\\\x20-\\\\x7E]{{2,}})(?=\\\\x00) <-- It matchs null-end-printable(from x20-x7e) C strings (min size 2 bytes)"] + + def __init__(self) -> None: + super().__init__() + self["max_size_preview"] = (10, "max size preview of bytes") + self["nr_pages_chunk"] = (0x400, "number of pages readed for each memory read chunk") + return + + def print_section(self, section: Section) -> None: + title = "In " + if section.path: + title += f"'{Color.blueify(section.path)}'" + + title += f"({section.page_start:#x}-{section.page_end:#x})" + title += f", permission={section.permission}" + ok(title) + return + + def print_loc(self, loc: Tuple[int, int, str]) -> None: + gef_print(f""" {loc[0]:#x} - {loc[1]:#x} {RIGHT_ARROW} "{Color.pinkify(loc[2])}" """) + return + + def search_pattern_by_address(self, pattern: str, start_address: int, end_address: int) -> List[Tuple[int, int, Optional[str]]]: + """Search a pattern within a range defined by arguments.""" + _pattern = gef_pybytes(pattern) + step = self["nr_pages_chunk"] * gef.session.pagesize + locations = [] + + for chunk_addr in range(start_address, end_address, step): + if chunk_addr + step > end_address: + chunk_size = end_address - chunk_addr + else: + chunk_size = step + + try: + mem = gef.memory.read(chunk_addr, chunk_size) + except gdb.MemoryError as e: + return [] + + for match in re.finditer(_pattern, mem): + start = chunk_addr + match.start() + if is_ascii_string(start): + ustr = gef.memory.read_ascii_string(start) or "" + end = start + len(ustr) + else: + ustr = gef_pystring(_pattern) + "[...]" + end = start + len(_pattern) + locations.append((start, end, ustr)) + + del mem + + return locations + + def search_binpattern_by_address(self, binpattern: bytes, start_address: int, end_address: int) -> List[Tuple[int, int, Optional[str]]]: + """Search a binary pattern within a range defined by arguments.""" + + step = self["nr_pages_chunk"] * gef.session.pagesize + locations = [] + + for chunk_addr in range(start_address, end_address, step): + if chunk_addr + step > end_address: + chunk_size = end_address - chunk_addr + else: + chunk_size = step + + try: + mem = gef.memory.read(chunk_addr, chunk_size) + except gdb.MemoryError as e: + return [] + preview_size = self["max_size_preview"] + for match in re.finditer(binpattern, mem): + start = chunk_addr + match.start() + preview = str(mem[slice(*match.span())][0:preview_size]) + "..." + size_match = match.span()[1] - match.span()[0] + if size_match > 0: + size_match -= 1 + end = start + size_match + + locations.append((start, end, preview)) + + del mem + + return locations + + def search_pattern(self, pattern: str, section_name: str) -> None: + """Search a pattern within the whole userland memory.""" + for section in gef.memory.maps: + if not section.permission & Permission.READ: continue + if section.path == "[vvar]": continue + if not section_name in section.path: continue + + start = section.page_start + end = section.page_end - 1 + old_section = None + + for loc in self.search_pattern_by_address(pattern, start, end): + addr_loc_start = lookup_address(loc[0]) + if addr_loc_start and addr_loc_start.section: + if old_section != addr_loc_start.section: + self.print_section(addr_loc_start.section) + old_section = addr_loc_start.section + + self.print_loc(loc) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + argc = len(argv) + if argc < 1: + self.usage() + return + + if argc > 3 and argv[0].startswith("--regex"): + pattern = ' '.join(argv[3:]) + pattern = ast.literal_eval("b'" + pattern + "'") + + addr_start = parse_address(argv[1]) + addr_end = parse_address(argv[2]) + + for loc in self.search_binpattern_by_address(pattern, addr_start, addr_end): + self.print_loc(loc) + + return + + pattern = argv[0] + endian = gef.arch.endianness + + if argc >= 2: + if argv[1].lower() == "big": endian = Endianness.BIG_ENDIAN + elif argv[1].lower() == "little": endian = Endianness.LITTLE_ENDIAN + + if is_hex(pattern): + if endian == Endianness.BIG_ENDIAN: + pattern = "".join(["\\x" + pattern[i:i + 2] for i in range(2, len(pattern), 2)]) + else: + pattern = "".join(["\\x" + pattern[i:i + 2] for i in range(len(pattern) - 2, 0, -2)]) + + if argc == 3: + info(f"Searching '{Color.yellowify(pattern)}' in {argv[2]}") + + if "0x" in argv[2]: + start, end = parse_string_range(argv[2]) + + loc = lookup_address(start) + if loc.valid: + self.print_section(loc.section) + + for loc in self.search_pattern_by_address(pattern, start, end): + self.print_loc(loc) + else: + section_name = argv[2] + if section_name == "binary": + section_name = get_filepath() or "" + + self.search_pattern(pattern, section_name) + else: + info(f"Searching '{Color.yellowify(pattern)}' in memory") + self.search_pattern(pattern, "") + return + + +@register +class FlagsCommand(GenericCommand): + """Edit flags in a human friendly way.""" + + _cmdline_ = "edit-flags" + _syntax_ = f"{_cmdline_} [(+|-|~)FLAGNAME ...]" + _aliases_ = ["flags",] + _example_ = (f"\n{_cmdline_}" + f"\n{_cmdline_} +zero # sets ZERO flag") + + def do_invoke(self, argv: List[str]) -> None: + if not gef.arch.flag_register: + warn(f"The architecture {gef.arch.arch}:{gef.arch.mode} doesn't have flag register.") + return + + for flag in argv: + if len(flag) < 2: + continue + + action = flag[0] + name = flag[1:].lower() + + if action not in ("+", "-", "~"): + err(f"Invalid action for flag '{flag}'") + continue + + if name not in gef.arch.flags_table.values(): + err(f"Invalid flag name '{flag[1:]}'") + continue + + for off in gef.arch.flags_table: + if gef.arch.flags_table[off] == name: + old_flag = gef.arch.register(gef.arch.flag_register) + if action == "+": + new_flags = old_flag | (1 << off) + elif action == "-": + new_flags = old_flag & ~(1 << off) + else: + new_flags = old_flag ^ (1 << off) + + gdb.execute(f"set ({gef.arch.flag_register}) = {new_flags:#x}") + + gef_print(gef.arch.flag_register_to_human()) + return + + +@register +class RemoteCommand(GenericCommand): + """GDB `target remote` command on steroids. This command will use the remote procfs to create + a local copy of the execution environment, including the target binary and its libraries + in the local temporary directory (the value by default is in `gef.config.tempdir`). Additionally, it + will fetch all the /proc/PID/maps and loads all its information. If procfs is not available remotely, the command + will likely fail. You can however still use the limited command provided by GDB `target remote`.""" + + _cmdline_ = "gef-remote" + _syntax_ = f"{_cmdline_} [OPTIONS] TARGET" + _example_ = [f"{_cmdline_} localhost 1234", + f"{_cmdline_} --pid 6789 localhost 1234", + f"{_cmdline_} --qemu-user --qemu-binary /bin/debugme localhost 4444 "] + + def __init__(self) -> None: + super().__init__(prefix=False) + return + + @parse_arguments({"host": "", "port": 0}, {"--pid": -1, "--qemu-user": True, "--qemu-binary": ""}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + if gef.session.remote is not None: + err("You already are in remote session. Close it first before opening a new one...") + return + + # argument check + args : argparse.Namespace = kwargs["arguments"] + if not args.host or not args.port: + err("Missing parameters") + return + + # qemu-user support + qemu_binary: Optional[pathlib.Path] = None + try: + if args.qemu_user: + qemu_binary = pathlib.Path(args.qemu_binary).expanduser().absolute() if args.qemu_binary else gef.session.file + if not qemu_binary or not qemu_binary.exists(): + raise FileNotFoundError(f"{qemu_binary} does not exist") + except Exception as e: + err(f"Failed to initialize qemu-user mode, reason: {str(e)}") + return + + # try to establish the remote session, throw on error + # Set `.remote_initializing` to True here - `GefRemoteSessionManager` invokes code which + # calls `is_remote_debug` which checks if `remote_initializing` is True or `.remote` is None + # This prevents some spurious errors being thrown during startup + gef.session.remote_initializing = True + gef.session.remote = GefRemoteSessionManager(args.host, args.port, args.pid, qemu_binary) + gef.session.remote_initializing = False + reset_all_caches() + gdb.execute("context") + return + + +@register +class NopCommand(GenericCommand): + """Patch the instruction(s) pointed by parameters with NOP. Note: this command is architecture + aware.""" + + _cmdline_ = "nop" + _syntax_ = ("{_cmdline_} [LOCATION] [--nb NUM_BYTES]" + "\n\tLOCATION\taddress/symbol to patch" + "\t--nb NUM_BYTES\tInstead of writing one instruction, patch the specified number of bytes") + _example_ = f"{_cmdline_} $pc" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + @parse_arguments({"address": "$pc"}, {"--nb": 0, }) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + address = parse_address(args.address) + nop = gef.arch.nop_insn + number_of_bytes = args.nb or 1 + insn = gef_get_instruction_at(address) + + if insn.size() != number_of_bytes: + warn(f"Patching {number_of_bytes} bytes at {address:#x} might result in corruption") + + nops = bytearray(nop * number_of_bytes) + end_address = Address(value=address + len(nops)) + if not end_address.valid: + err(f"Cannot patch instruction at {address:#x}: reaching unmapped area") + return + + ok(f"Patching {len(nops)} bytes from {address:#x}") + gef.memory.write(address, nops, len(nops)) + return + + +@register +class StubCommand(GenericCommand): + """Stub out the specified function. This function is useful when needing to skip one + function to be called and disrupt your runtime flow (ex. fork).""" + + _cmdline_ = "stub" + _syntax_ = (f"{_cmdline_} [--retval RETVAL] [address]" + "\taddress\taddress/symbol to stub out" + "\t--retval RETVAL\tSet the return value") + _example_ = f"{_cmdline_} --retval 0 fork" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + @parse_arguments({"address": ""}, {("-r", "--retval"): 0}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + loc = args.address if args.address else f"*{gef.arch.pc:#x}" + StubBreakpoint(loc, args.retval) + return + + +@register +class GlibcHeapCommand(GenericCommand): + """Base command to get information about the Glibc heap structure.""" + + _cmdline_ = "heap" + _syntax_ = f"{_cmdline_} (chunk|chunks|bins|arenas|set-arena)" + + def __init__(self) -> None: + super().__init__(prefix=True) + return + + @only_if_gdb_running + def do_invoke(self, _: List[str]) -> None: + self.usage() + return + + +@register +class GlibcHeapSetArenaCommand(GenericCommand): + """Set the address of the main_arena or the currently selected arena.""" + + _cmdline_ = "heap set-arena" + _syntax_ = f"{_cmdline_} [address|&symbol]" + _example_ = f"{_cmdline_} 0x001337001337" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + @parse_arguments({"addr": ""}, {"--reset": True}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + global gef + + args: argparse.Namespace = kwargs["arguments"] + + if args.reset: + gef.heap.reset_caches() + return + + if not args.addr: + ok(f"Current arena set to: '{gef.heap.selected_arena}'") + return + + try: + new_arena_address = parse_address(args.addr) + except gdb.error: + err("Invalid symbol for arena") + return + + new_arena = GlibcArena( f"*{new_arena_address:#x}") + if new_arena in gef.heap.arenas: + # if entered arena is in arena list then just select it + gef.heap.selected_arena = new_arena + else: + # otherwise set the main arena to the entered arena + gef.heap.main_arena = new_arena + return + + +@register +class GlibcHeapArenaCommand(GenericCommand): + """Display information on a heap chunk.""" + + _cmdline_ = "heap arenas" + _syntax_ = _cmdline_ + + @only_if_gdb_running + def do_invoke(self, _: List[str]) -> None: + for arena in gef.heap.arenas: + gef_print(str(arena)) + return + + +@register +class GlibcHeapChunkCommand(GenericCommand): + """Display information on a heap chunk. + See https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1123.""" + + _cmdline_ = "heap chunk" + _syntax_ = f"{_cmdline_} [-h] [--allow-unaligned] [--number] address" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @parse_arguments({"address": ""}, {"--allow-unaligned": True, "--number": 1}) + @only_if_gdb_running + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if not args.address: + err("Missing chunk address") + self.usage() + return + + addr = parse_address(args.address) + current_chunk = GlibcChunk(addr, allow_unaligned=args.allow_unaligned) + + if args.number > 1: + for _i in range(args.number): + if current_chunk.size == 0: + break + + gef_print(str(current_chunk)) + next_chunk_addr = current_chunk.get_next_chunk_addr() + if not Address(value=next_chunk_addr).valid: + break + + next_chunk = current_chunk.get_next_chunk() + if next_chunk is None: + break + + current_chunk = next_chunk + else: + gef_print(current_chunk.psprint()) + return + + +@register +class GlibcHeapChunksCommand(GenericCommand): + """Display all heap chunks for the current arena. As an optional argument + the base address of a different arena can be passed""" + + _cmdline_ = "heap chunks" + _syntax_ = f"{_cmdline_} [-h] [--all] [--allow-unaligned] [arena_address]" + _example_ = (f"\n{_cmdline_}" + f"\n{_cmdline_} 0x555555775000") + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + self["peek_nb_byte"] = (16, "Hexdump N first byte(s) inside the chunk data (0 to disable)") + return + + @parse_arguments({"arena_address": ""}, {("--all", "-a"): True, "--allow-unaligned": True}) + @only_if_gdb_running + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args = kwargs["arguments"] + if args.all or not args.arena_address: + for arena in gef.heap.arenas: + self.dump_chunks_arena(arena, print_arena=args.all, allow_unaligned=args.allow_unaligned) + if not args.all: + return + try: + arena_addr = parse_address(args.arena_address) + arena = GlibcArena(f"*{arena_addr:#x}") + self.dump_chunks_arena(arena, allow_unaligned=args.allow_unaligned) + except gdb.error: + err("Invalid arena") + return + + def dump_chunks_arena(self, arena: GlibcArena, print_arena: bool = False, allow_unaligned: bool = False) -> None: + heap_addr = arena.heap_addr(allow_unaligned=allow_unaligned) + if heap_addr is None: + err("Could not find heap for arena") + return + if print_arena: + gef_print(str(arena)) + if arena.is_main_arena(): + heap_end = arena.top + GlibcChunk(arena.top, from_base=True).size + self.dump_chunks_heap(heap_addr, heap_end, arena, allow_unaligned=allow_unaligned) + else: + heap_info_structs = arena.get_heap_info_list() or [] + for heap_info in heap_info_structs: + if not self.dump_chunks_heap(heap_info.heap_start, heap_info.heap_end, arena, allow_unaligned=allow_unaligned): + break + return + + def dump_chunks_heap(self, start: int, end: int, arena: GlibcArena, allow_unaligned: bool = False) -> bool: + nb = self["peek_nb_byte"] + chunk_iterator = GlibcChunk(start, from_base=True, allow_unaligned=allow_unaligned) + for chunk in chunk_iterator: + if chunk.base_address == arena.top: + gef_print( + f"{chunk!s} {LEFT_ARROW} {Color.greenify('top chunk')}") + break + + if chunk.base_address > end: + err("Corrupted heap, cannot continue.") + return False + + line = str(chunk) + if nb: + line += f"\n [{hexdump(gef.memory.read(chunk.data_address, nb), nb, base=chunk.data_address)}]" + gef_print(line) + return True + + +@register +class GlibcHeapBinsCommand(GenericCommand): + """Display information on the bins on an arena (default: main_arena). + See https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1123.""" + + _bin_types_ = ("tcache", "fast", "unsorted", "small", "large") + _cmdline_ = "heap bins" + _syntax_ = f"{_cmdline_} [{'|'.join(_bin_types_)}]" + + def __init__(self) -> None: + super().__init__(prefix=True, complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if not argv: + for bin_t in self._bin_types_: + gdb.execute(f"heap bins {bin_t}") + return + + bin_t = argv[0] + if bin_t not in self._bin_types_: + self.usage() + return + + gdb.execute(f"heap bins {bin_t}") + return + + @staticmethod + def pprint_bin(arena_addr: str, index: int, _type: str = "") -> int: + arena = GlibcArena(arena_addr) + + fd, bk = arena.bin(index) + if (fd, bk) == (0x00, 0x00): + warn("Invalid backward and forward bin pointers(fw==bk==NULL)") + return -1 + + if _type == "tcache": + chunkClass = GlibcTcacheChunk + elif _type == "fast": + chunkClass = GlibcFastChunk + else: + chunkClass = GlibcChunk + + nb_chunk = 0 + head = chunkClass(bk, from_base=True).fd + if fd == head: + return nb_chunk + + ok(f"{_type}bins[{index:d}]: fw={fd:#x}, bk={bk:#x}") + + m = [] + while fd != head: + chunk = chunkClass(fd, from_base=True) + m.append(f"{RIGHT_ARROW} {chunk!s}") + fd = chunk.fd + nb_chunk += 1 + + if m: + gef_print(" ".join(m)) + return nb_chunk + + +@register +class GlibcHeapTcachebinsCommand(GenericCommand): + """Display information on the Tcachebins on an arena (default: main_arena). + See https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc.""" + + _cmdline_ = "heap bins tcache" + _syntax_ = f"{_cmdline_} [all] [thread_ids...]" + + TCACHE_MAX_BINS = 0x40 + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + # Determine if we are using libc with tcache built in (2.26+) + if gef.libc.version and gef.libc.version < (2, 26): + info("No Tcache in this version of libc") + return + + current_thread = gdb.selected_thread() + if current_thread is None: + err("Couldn't find current thread") + return + + # As a nicety, we want to display threads in ascending order by gdb number + threads = sorted(gdb.selected_inferior().threads(), key=lambda t: t.num) + if argv: + if "all" in argv: + tids = [t.num for t in threads] + else: + tids = self.check_thread_ids(argv) + else: + tids = [current_thread.num] + + for thread in threads: + if thread.num not in tids: + continue + + thread.switch() + + tcache_addr = self.find_tcache() + if tcache_addr == 0: + info(f"Uninitialized tcache for thread {thread.num:d}") + continue + + gef_print(titlify(f"Tcachebins for thread {thread.num:d}")) + tcache_empty = True + for i in range(self.TCACHE_MAX_BINS): + chunk, count = self.tcachebin(tcache_addr, i) + chunks = set() + msg = [] + chunk_size = 0 + + # Only print the entry if there are valid chunks. Don't trust count + while True: + if chunk is None: + break + + try: + msg.append(f"{LEFT_ARROW} {chunk!s} ") + if not chunk_size: + chunk_size = chunk.usable_size + + if chunk.data_address in chunks: + msg.append(f"{RIGHT_ARROW} [loop detected]") + break + + chunks.add(chunk.data_address) + + next_chunk = chunk.fd + if next_chunk == 0: + break + + chunk = GlibcTcacheChunk(next_chunk) + except gdb.MemoryError: + msg.append(f"{LEFT_ARROW} [Corrupted chunk at {chunk.data_address:#x}]") + break + + if msg: + tcache_empty = False + tidx = gef.heap.csize2tidx(chunk_size) + size = gef.heap.tidx2size(tidx) + count = len(chunks) + gef_print(f"Tcachebins[idx={tidx:d}, size={size:#x}, count={count}]", end="") + gef_print("".join(msg)) + + if tcache_empty: + gef_print("All tcachebins are empty") + + current_thread.switch() + return + + @staticmethod + def find_tcache() -> int: + """Return the location of the current thread's tcache.""" + try: + # For multithreaded binaries, the tcache symbol (in thread local + # storage) will give us the correct address. + tcache_addr = parse_address("(void *) tcache") + except gdb.error: + # In binaries not linked with pthread (and therefore there is only + # one thread), we can't use the tcache symbol, but we can guess the + # correct address because the tcache is consistently the first + # allocation in the main arena. + heap_base = gef.heap.base_address + if heap_base is None: + err("No heap section") + return 0x0 + tcache_addr = heap_base + 0x10 + return tcache_addr + + @staticmethod + def check_thread_ids(tids: List[int]) -> List[int]: + """Check the validity, dedup, and return all valid tids.""" + existing_tids = [t.num for t in gdb.selected_inferior().threads()] + valid_tids = set() + for tid in tids: + try: + tid = int(tid) + except ValueError: + err(f"Invalid thread id {tid:d}") + continue + if tid in existing_tids: + valid_tids.add(tid) + else: + err(f"Unknown thread {tid}") + + return list(valid_tids) + + @staticmethod + def tcachebin(tcache_base: int, i: int) -> Tuple[Optional[GlibcTcacheChunk], int]: + """Return the head chunk in tcache[i] and the number of chunks in the bin.""" + if i >= GlibcHeapTcachebinsCommand.TCACHE_MAX_BINS: + err("Incorrect index value, index value must be between 0 and {}-1, given {}".format(GlibcHeapTcachebinsCommand.TCACHE_MAX_BINS, i)) + return None, 0 + + tcache_chunk = GlibcTcacheChunk(tcache_base) + + # Glibc changed the size of the tcache in version 2.30; this fix has + # been backported inconsistently between distributions. We detect the + # difference by checking the size of the allocated chunk for the + # tcache. + # Minimum usable size of allocated tcache chunk = ? + # For new tcache: + # TCACHE_MAX_BINS * _2_ + TCACHE_MAX_BINS * ptrsize + # For old tcache: + # TCACHE_MAX_BINS * _1_ + TCACHE_MAX_BINS * ptrsize + new_tcache_min_size = ( + GlibcHeapTcachebinsCommand.TCACHE_MAX_BINS * 2 + + GlibcHeapTcachebinsCommand.TCACHE_MAX_BINS * gef.arch.ptrsize) + + if tcache_chunk.usable_size < new_tcache_min_size: + tcache_count_size = 1 + count = ord(gef.memory.read(tcache_base + tcache_count_size*i, 1)) + else: + tcache_count_size = 2 + count = u16(gef.memory.read(tcache_base + tcache_count_size*i, 2)) + + chunk = dereference(tcache_base + tcache_count_size*GlibcHeapTcachebinsCommand.TCACHE_MAX_BINS + i*gef.arch.ptrsize) + chunk = GlibcTcacheChunk(int(chunk)) if chunk else None + return chunk, count + + +@register +class GlibcHeapFastbinsYCommand(GenericCommand): + """Display information on the fastbinsY on an arena (default: main_arena). + See https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1123.""" + + _cmdline_ = "heap bins fast" + _syntax_ = f"{_cmdline_} [ARENA_ADDRESS]" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @parse_arguments({"arena_address": ""}, {}) + @only_if_gdb_running + def do_invoke(self, *_: Any, **kwargs: Any) -> None: + def fastbin_index(sz: int) -> int: + return (sz >> 4) - 2 if SIZE_SZ == 8 else (sz >> 3) - 2 + + args : argparse.Namespace = kwargs["arguments"] + if not gef.heap.main_arena: + err("Heap not initialized") + return + + SIZE_SZ = gef.arch.ptrsize + MAX_FAST_SIZE = 80 * SIZE_SZ // 4 + NFASTBINS = fastbin_index(MAX_FAST_SIZE) - 1 + + arena = GlibcArena(f"*{args.arena_address}") if args.arena_address else gef.heap.selected_arena + if arena is None: + err("Invalid Glibc arena") + return + + gef_print(titlify(f"Fastbins for arena at {arena.addr:#x}")) + for i in range(NFASTBINS): + gef_print(f"Fastbins[idx={i:d}, size={(i+2)*SIZE_SZ*2:#x}] ", end="") + chunk = arena.fastbin(i) + chunks = set() + + while True: + if chunk is None: + gef_print("0x00", end="") + break + + try: + gef_print(f"{LEFT_ARROW} {chunk!s} ", end="") + if chunk.data_address in chunks: + gef_print(f"{RIGHT_ARROW} [loop detected]", end="") + break + + if fastbin_index(chunk.size) != i: + gef_print("[incorrect fastbin_index] ", end="") + + chunks.add(chunk.data_address) + + next_chunk = chunk.fd + if next_chunk == 0: + break + + chunk = GlibcFastChunk(next_chunk, from_base=True) + except gdb.MemoryError: + gef_print(f"{LEFT_ARROW} [Corrupted chunk at {chunk.data_address:#x}]", end="") + break + gef_print() + return + + +@register +class GlibcHeapUnsortedBinsCommand(GenericCommand): + """Display information on the Unsorted Bins of an arena (default: main_arena). + See: https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1689.""" + + _cmdline_ = "heap bins unsorted" + _syntax_ = f"{_cmdline_} [ARENA_ADDRESS]" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @parse_arguments({"arena_address": ""}, {}) + @only_if_gdb_running + def do_invoke(self, *_: Any, **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if gef.heap.main_arena is None: + err("Heap not initialized") + return + arena_addr = args.arena_address if args.arena_address else f"{gef.heap.selected_arena.addr:#x}" + gef_print(titlify(f"Unsorted Bin for arena at {arena_addr}")) + nb_chunk = GlibcHeapBinsCommand.pprint_bin(f"*{arena_addr}", 0, "unsorted_") + if nb_chunk >= 0: + info(f"Found {nb_chunk:d} chunks in unsorted bin.") + return + + +@register +class GlibcHeapSmallBinsCommand(GenericCommand): + """Convenience command for viewing small bins.""" + + _cmdline_ = "heap bins small" + _syntax_ = f"{_cmdline_} [ARENA_ADDRESS]" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @parse_arguments({"arena_address": ""}, {}) + @only_if_gdb_running + def do_invoke(self, *_: Any, **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if not gef.heap.main_arena: + err("Heap not initialized") + return + + arena_address = args.arena_address or f"{gef.heap.selected_arena.address:#x}" + gef_print(titlify(f"Small Bins for arena at {arena_address}")) + bins = {} + for i in range(1, 63): + nb_chunk = GlibcHeapBinsCommand.pprint_bin(f"*{arena_address}", i, "small_") + if nb_chunk < 0: + break + if nb_chunk > 0: + bins[i] = nb_chunk + info(f"Found {sum(bins.values()):d} chunks in {len(bins):d} small non-empty bins.") + return + + +@register +class GlibcHeapLargeBinsCommand(GenericCommand): + """Convenience command for viewing large bins.""" + + _cmdline_ = "heap bins large" + _syntax_ = f"{_cmdline_} [ARENA_ADDRESS]" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @parse_arguments({"arena_address": ""}, {}) + @only_if_gdb_running + def do_invoke(self, *_: Any, **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if gef.heap.main_arena is None: + err("Heap not initialized") + return + + arena_addr = args.arena_address if args.arena_address else f"{gef.heap.selected_arena.addr:#x}" + gef_print(titlify(f"Large Bins for arena at {arena_addr}")) + bins = {} + for i in range(63, 126): + nb_chunk = GlibcHeapBinsCommand.pprint_bin(f"*{arena_addr}", i, "large_") + if nb_chunk < 0: + break + if nb_chunk > 0: + bins[i] = nb_chunk + info(f"Found {sum(bins.values()):d} chunks in {len(bins):d} large non-empty bins.") + 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.""" + + _cmdline_ = "registers" + _syntax_ = f"{_cmdline_} [[Register1][Register2] ... [RegisterN]]" + _example_ = (f"\n{_cmdline_}" + f"\n{_cmdline_} $eax $eip $esp") + + @only_if_gdb_running + @parse_arguments({"registers": [""]}, {}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + unchanged_color = gef.config["theme.registers_register_name"] + changed_color = gef.config["theme.registers_value_changed"] + string_color = gef.config["theme.dereference_string"] + regs = gef.arch.all_registers + + args : argparse.Namespace = kwargs["arguments"] + if args.registers and args.registers[0]: + required_regs = set(args.registers) + valid_regs = [reg for reg in gef.arch.all_registers if reg in required_regs] + if valid_regs: + regs = valid_regs + invalid_regs = [reg for reg in required_regs if reg not in valid_regs] + if invalid_regs: + err(f"invalid registers for architecture: {', '.join(invalid_regs)}") + + memsize = gef.arch.ptrsize + endian = str(gef.arch.endianness) + charset = string.printable + widest = max(map(len, gef.arch.all_registers)) + special_line = "" + + for regname in regs: + reg = gdb.parse_and_eval(regname) + if reg.type.code == gdb.TYPE_CODE_VOID: + continue + + padreg = regname.ljust(widest, " ") + + if str(reg) == "": + gef_print(f"{Color.colorify(padreg, unchanged_color)}: " + f"{Color.colorify('no value', 'yellow underline')}") + continue + + value = align_address(int(reg)) + old_value = ContextCommand.old_registers.get(regname, 0) + if value == old_value: + color = unchanged_color + else: + color = changed_color + + # Special (e.g. segment) registers go on their own line + if regname in gef.arch.special_registers: + special_line += f"{Color.colorify(regname, color)}: " + special_line += f"{gef.arch.register(regname):#04x} " + continue + + line = f"{Color.colorify(padreg, color)}: " + + if regname == gef.arch.flag_register: + line += gef.arch.flag_register_to_human() + gef_print(line) + continue + + addr = lookup_address(align_address(int(value))) + if addr.valid: + line += str(addr) + else: + line += format_address_spaces(value) + addrs = dereference_from(value) + + if len(addrs) > 1: + sep = f" {RIGHT_ARROW} " + line += sep + line += sep.join(addrs[1:]) + + # check to see if reg value is ascii + try: + fmt = f"{endian}{'I' if memsize == 4 else 'Q'}" + last_addr = int(addrs[-1], 16) + val = gef_pystring(struct.pack(fmt, last_addr)) + if all([_ in charset for _ in val]): + line += f" (\"{Color.colorify(val, string_color)}\"?)" + except ValueError: + pass + + gef_print(line) + + if special_line: + gef_print(special_line) + return + + +@register +class ShellcodeCommand(GenericCommand): + """ShellcodeCommand uses @JonathanSalwan simple-yet-awesome shellcode API to + download shellcodes.""" + + _cmdline_ = "shellcode" + _syntax_ = f"{_cmdline_} (search|get)" + + def __init__(self) -> None: + super().__init__(prefix=True) + return + + def do_invoke(self, _: List[str]) -> None: + err("Missing sub-command (search|get)") + self.usage() + return + + +@register +class ShellcodeSearchCommand(GenericCommand): + """Search pattern in shell-storm's shellcode database.""" + + _cmdline_ = "shellcode search" + _syntax_ = f"{_cmdline_} PATTERN1 PATTERN2" + _aliases_ = ["sc-search",] + + api_base = "http://shell-storm.org" + search_url = f"{api_base}/api/?s=" + + def do_invoke(self, argv: List[str]) -> None: + if not argv: + err("Missing pattern to search") + self.usage() + return + + self.search_shellcode(argv) + return + + def search_shellcode(self, search_options: List) -> None: + # API : http://shell-storm.org/shellcode/ + args = "*".join(search_options) + + res = http_get(self.search_url + args) + if res is None: + err("Could not query search page") + return + + ret = gef_pystring(res) + + # format: [author, OS/arch, cmd, id, link] + lines = ret.split("\\n") + refs = [line.split("::::") for line in lines] + + if refs: + info("Showing matching shellcodes") + info("\t".join(["Id", "Platform", "Description"])) + for ref in refs: + try: + _, arch, cmd, sid, _ = ref + gef_print("\t".join([sid, arch, cmd])) + except ValueError: + continue + + info("Use `shellcode get ` to fetch shellcode") + return + + +@register +class ShellcodeGetCommand(GenericCommand): + """Download shellcode from shell-storm's shellcode database.""" + + _cmdline_ = "shellcode get" + _syntax_ = f"{_cmdline_} SHELLCODE_ID" + _aliases_ = ["sc-get",] + + api_base = "http://shell-storm.org" + get_url = f"{api_base}/shellcode/files/shellcode-{{:d}}.html" + + def do_invoke(self, argv: List[str]) -> None: + if len(argv) != 1: + err("Missing ID to download") + self.usage() + return + + if not argv[0].isdigit(): + err("ID is not a number") + self.usage() + return + + self.get_shellcode(int(argv[0])) + return + + def get_shellcode(self, sid: int) -> None: + info(f"Downloading shellcode id={sid}") + res = http_get(self.get_url.format(sid)) + if res is None: + err(f"Failed to fetch shellcode #{sid}") + return + + ok("Downloaded, written to disk...") + with tempfile.NamedTemporaryFile(prefix="sc-", suffix=".txt", mode='w+b', delete=False, dir=gef.config["gef.tempdir"]) as fd: + shellcode = res.split(b"
")[1].split(b"
")[0] + shellcode = shellcode.replace(b""", b'"') + fd.write(shellcode) + ok(f"Shellcode written to '{fd.name}'") + return + + +@register +class ProcessListingCommand(GenericCommand): + """List and filter process. If a PATTERN is given as argument, results shown will be grepped + by this pattern.""" + + _cmdline_ = "process-search" + _syntax_ = f"{_cmdline_} [-h] [--attach] [--smart-scan] [REGEX_PATTERN]" + _aliases_ = ["ps"] + _example_ = f"{_cmdline_} gdb.*" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + self["ps_command"] = (f"{gef.session.constants['ps']} auxww", "`ps` command to get process information") + return + + @parse_arguments({"pattern": ""}, {"--attach": True, "--smart-scan": True}) + def do_invoke(self, _: List, **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + do_attach = args.attach + smart_scan = args.smart_scan + pattern = args.pattern + pattern = re.compile("^.*$") if not args else re.compile(pattern) + + for process in self.get_processes(): + pid = int(process["pid"]) + command = process["command"] + + if not re.search(pattern, command): + continue + + if smart_scan: + if command.startswith("[") and command.endswith("]"): continue + if command.startswith("socat "): continue + if command.startswith("grep "): continue + if command.startswith("gdb "): continue + + if args and do_attach: + ok(f"Attaching to process='{process['command']}' pid={pid:d}") + gdb.execute(f"attach {pid:d}") + return None + + line = [process[i] for i in ("pid", "user", "cpu", "mem", "tty", "command")] + gef_print("\t\t".join(line)) + + return None + + def get_processes(self) -> Generator[Dict[str, str], None, None]: + output = gef_execute_external(self["ps_command"].split(), True) + names = [x.lower().replace("%", "") for x in output[0].split()] + + for line in output[1:]: + fields = line.split() + t = {} + + for i, name in enumerate(names): + if i == len(names) - 1: + t[name] = " ".join(fields[i:]) + else: + t[name] = fields[i] + + yield t + + return + + +@register +class ElfInfoCommand(GenericCommand): + """Display a limited subset of ELF header information. If no argument is provided, the command will + show information about the current ELF being debugged.""" + + _cmdline_ = "elf-info" + _syntax_ = f"{_cmdline_} [FILE]" + _example_ = f"{_cmdline_} /bin/ls" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @parse_arguments({}, {"--filename": ""}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + + if is_qemu_system(): + err("Unsupported") + return + + filename = args.filename or get_filepath() + if filename is None: + return + + try: + elf = Elf(filename) + except ValueError as ve: + err(f"`{filename}` is an invalid value for ELF file") + return + + data = [ + ("Magic", f"{hexdump(struct.pack('>I', elf.e_magic), show_raw=True)}"), + ("Class", f"{elf.e_class.value:#x} - {elf.e_class.name}"), + ("Endianness", f"{elf.e_endianness.value:#x} - {Endianness(elf.e_endianness).name}"), + ("Version", f"{elf.e_eiversion:#x}"), + ("OS ABI", f"{elf.e_osabi.value:#x} - {elf.e_osabi.name if elf.e_osabi else ''}"), + ("ABI Version", f"{elf.e_abiversion:#x}"), + ("Type", f"{elf.e_type.value:#x} - {elf.e_type.name}"), + ("Machine", f"{elf.e_machine.value:#x} - {elf.e_machine.name}"), + ("Program Header Table", f"{format_address(elf.e_phoff)}"), + ("Section Header Table", f"{format_address(elf.e_shoff)}"), + ("Header Table", f"{format_address(elf.e_phoff)}"), + ("ELF Version", f"{elf.e_version:#x}"), + ("Header size", "{0} ({0:#x})".format(elf.e_ehsize)), + ("Entry point", f"{format_address(elf.e_entry)}"), + ] + + for title, content in data: + gef_print(f"{Color.boldify(f'{title:<22}')}: {content}") + + gef_print("") + gef_print(titlify("Program Header")) + + gef_print(" [{:>2s}] {:12s} {:>8s} {:>10s} {:>10s} {:>8s} {:>8s} {:5s} {:>8s}".format( + "#", "Type", "Offset", "Virtaddr", "Physaddr", "FileSiz", "MemSiz", "Flags", "Align")) + + for i, p in enumerate(elf.phdrs): + p_type = p.p_type.name if p.p_type else "" + p_flags = str(p.p_flags.name).lstrip("Flag.") if p.p_flags else "???" + + gef_print(" [{:2d}] {:12s} {:#8x} {:#10x} {:#10x} {:#8x} {:#8x} {:5s} {:#8x}".format( + i, p_type, p.p_offset, p.p_vaddr, p.p_paddr, p.p_filesz, p.p_memsz, p_flags, p.p_align)) + + gef_print("") + gef_print(titlify("Section Header")) + gef_print(" [{:>2s}] {:20s} {:>15s} {:>10s} {:>8s} {:>8s} {:>8s} {:5s} {:4s} {:4s} {:>8s}".format( + "#", "Name", "Type", "Address", "Offset", "Size", "EntSiz", "Flags", "Link", "Info", "Align")) + + for i, s in enumerate(elf.shdrs): + sh_type = s.sh_type.name if s.sh_type else "UNKN" + sh_flags = str(s.sh_flags).lstrip("Flags.") if s.sh_flags else "UNKN" + + gef_print(f" [{i:2d}] {s.name:20s} {sh_type:>15s} {s.sh_addr:#10x} {s.sh_offset:#8x} " + f"{s.sh_size:#8x} {s.sh_entsize:#8x} {sh_flags:5s} {s.sh_link:#4x} {s.sh_info:#4x} {s.sh_addralign:#8x}") + return + + +@register +class EntryPointBreakCommand(GenericCommand): + """Tries to find best entry point and sets a temporary breakpoint on it. The command will test for + well-known symbols for entry points, such as `main`, `_main`, `__libc_start_main`, etc. defined by + the setting `entrypoint_symbols`.""" + + _cmdline_ = "entry-break" + _syntax_ = _cmdline_ + _aliases_ = ["start",] + + def __init__(self) -> None: + super().__init__() + self["entrypoint_symbols"] = ("main _main __libc_start_main __uClibc_main start _start", "Possible symbols for entry points") + return + + def do_invoke(self, argv: List[str]) -> None: + fpath = get_filepath() + if fpath is None: + warn("No executable to debug, use `file` to load a binary") + return + + if not os.access(fpath, os.X_OK): + warn(f"The file '{fpath}' is not executable.") + return + + if is_alive() and not gef.session.qemu_mode: + warn("gdb is already running") + return + + bp = None + entrypoints = self["entrypoint_symbols"].split() + + for sym in entrypoints: + try: + value = parse_address(sym) + info(f"Breaking at '{value:#x}'") + bp = EntryBreakBreakpoint(sym) + gdb.execute(f"run {' '.join(argv)}") + return + + except gdb.error as gdb_error: + if 'The "remote" target does not support "run".' in str(gdb_error): + # this case can happen when doing remote debugging + gdb.execute("continue") + return + continue + + # if here, clear the breakpoint if any set + if bp: + bp.delete() + + # break at entry point + entry = gef.binary.entry_point + + if is_pie(fpath): + self.set_init_tbreak_pie(entry, argv) + gdb.execute("continue") + return + + self.set_init_tbreak(entry) + gdb.execute(f"run {' '.join(argv)}") + return + + def set_init_tbreak(self, addr: int) -> EntryBreakBreakpoint: + info(f"Breaking at entry-point: {addr:#x}") + bp = EntryBreakBreakpoint(f"*{addr:#x}") + return bp + + def set_init_tbreak_pie(self, addr: int, argv: List[str]) -> EntryBreakBreakpoint: + warn("PIC binary detected, retrieving text base address") + gdb.execute("set stop-on-solib-events 1") + hide_context() + gdb.execute(f"run {' '.join(argv)}") + unhide_context() + gdb.execute("set stop-on-solib-events 0") + vmmap = gef.memory.maps + base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0] + return self.set_init_tbreak(base_address + addr) + + +@register +class NamedBreakpointCommand(GenericCommand): + """Sets a breakpoint and assigns a name to it, which will be shown, when it's hit.""" + + _cmdline_ = "name-break" + _syntax_ = f"{_cmdline_} name [address]" + _aliases_ = ["nb",] + _example = f"{_cmdline_} main *0x4008a9" + + def __init__(self) -> None: + super().__init__() + return + + @parse_arguments({"name": "", "address": "*$pc"}, {}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if not args.name: + err("Missing name for breakpoint") + self.usage() + return + + NamedBreakpoint(args.address, args.name) + return + + +@register +class ContextCommand(GenericCommand): + """Displays a comprehensive and modular summary of runtime context. Unless setting `enable` is + set to False, this command will be spawned automatically every time GDB hits a breakpoint, a + watchpoint, or any kind of interrupt. By default, it will show panes that contain the register + states, the stack, and the disassembly code around $pc.""" + + _cmdline_ = "context" + _syntax_ = f"{_cmdline_} [legend|regs|stack|code|args|memory|source|trace|threads|extra]" + _aliases_ = ["ctx",] + + old_registers: Dict[str, Optional[int]] = {} + + def __init__(self) -> None: + super().__init__() + self["enable"] = (True, "Enable/disable printing the context when breaking") + self["show_source_code_variable_values"] = (True, "Show extra PC context info in the source code") + self["show_stack_raw"] = (False, "Show the stack pane as raw hexdump (no dereference)") + self["show_registers_raw"] = (False, "Show the registers pane with raw values (no dereference)") + self["show_opcodes_size"] = (0, "Number of bytes of opcodes to display next to the disassembly") + self["peek_calls"] = (True, "Peek into calls") + self["peek_ret"] = (True, "Peek at return address") + self["nb_lines_stack"] = (8, "Number of line in the stack pane") + self["grow_stack_down"] = (False, "Order of stack downward starts at largest down to stack pointer") + self["nb_lines_backtrace"] = (10, "Number of line in the backtrace pane") + self["nb_lines_backtrace_before"] = (2, "Number of line in the backtrace pane before selected frame") + self["nb_lines_threads"] = (-1, "Number of line in the threads pane") + self["nb_lines_code"] = (6, "Number of instruction after $pc") + self["nb_lines_code_prev"] = (3, "Number of instruction before $pc") + self["ignore_registers"] = ("", "Space-separated list of registers not to display (e.g. '$cs $ds $gs')") + self["clear_screen"] = (True, "Clear the screen before printing the context") + self["layout"] = ("legend regs stack code args source memory threads trace extra", "Change the order/presence of the context sections") + self["redirect"] = ("", "Redirect the context information to another TTY") + self["libc_args"] = (False, "[DEPRECATED - Unused] Show libc function call args description") + self["libc_args_path"] = ("", "[DEPRECATED - Unused] Path to libc function call args json files, provided via gef-extras") + + self.layout_mapping = { + "legend": (self.show_legend, None, None), + "regs": (self.context_regs, None, None), + "stack": (self.context_stack, None, None), + "code": (self.context_code, None, None), + "args": (self.context_args, None, None), + "memory": (self.context_memory, None, None), + "source": (self.context_source, None, None), + "trace": (self.context_trace, None, None), + "threads": (self.context_threads, None, None), + "extra": (self.context_additional_information, None, None), + } + + self.instruction_iterator = gef_disassemble + return + + def post_load(self) -> None: + gef_on_continue_hook(self.update_registers) + gef_on_continue_hook(self.empty_extra_messages) + return + + def show_legend(self) -> None: + if gef.config["gef.disable_color"] is True: + return + str_color = gef.config["theme.dereference_string"] + code_addr_color = gef.config["theme.address_code"] + stack_addr_color = gef.config["theme.address_stack"] + heap_addr_color = gef.config["theme.address_heap"] + changed_register_color = gef.config["theme.registers_value_changed"] + + gef_print("[ Legend: {} | {} | {} | {} | {} ]".format(Color.colorify("Modified register", changed_register_color), + Color.colorify("Code", code_addr_color), + Color.colorify("Heap", heap_addr_color), + Color.colorify("Stack", stack_addr_color), + Color.colorify("String", str_color))) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if not self["enable"] or gef.ui.context_hidden: + return + + if not all(_ in self.layout_mapping for _ in argv): + self.usage() + return + + if len(argv) > 0: + current_layout = argv + else: + current_layout = self["layout"].strip().split() + + if not current_layout: + return + + self.tty_rows, self.tty_columns = get_terminal_size() + + redirect = self["redirect"] + if redirect and os.access(redirect, os.W_OK): + enable_redirect_output(to_file=redirect) + + for section in current_layout: + if section[0] == "-": + continue + + try: + display_pane_function, pane_title_function, condition = self.layout_mapping[section] + if condition: + if not condition(): + continue + if pane_title_function: + self.context_title(pane_title_function()) + display_pane_function() + except gdb.MemoryError as e: + # a MemoryError will happen when $pc is corrupted (invalid address) + err(str(e)) + except IndexError: + # the `section` is not present, just skip + pass + + self.context_title("") + + if self["clear_screen"] and len(argv) == 0: + clear_screen(redirect) + + if redirect and os.access(redirect, os.W_OK): + disable_redirect_output() + return + + def context_title(self, m: Optional[str]) -> None: + # allow for not displaying a title line + if m is None: + return + + line_color = gef.config["theme.context_title_line"] + msg_color = gef.config["theme.context_title_message"] + + # print an empty line in case of "" + if not m: + gef_print(Color.colorify(HORIZONTAL_LINE * self.tty_columns, line_color)) + return + + trail_len = len(m) + 6 + title = "" + title += Color.colorify("{:{padd}<{width}} ".format("", + width=max(self.tty_columns - trail_len, 0), + padd=HORIZONTAL_LINE), + line_color) + title += Color.colorify(m, msg_color) + title += Color.colorify(" {:{padd}<4}".format("", padd=HORIZONTAL_LINE), + line_color) + gef_print(title) + return + + def context_regs(self) -> None: + self.context_title("registers") + ignored_registers = set(self["ignore_registers"].split()) + + if self["show_registers_raw"] is False: + regs = set(gef.arch.all_registers) + printable_registers = " ".join(list(regs - ignored_registers)) + gdb.execute(f"registers {printable_registers}") + return + + widest = l = max(map(len, gef.arch.all_registers)) + l += 5 + l += gef.arch.ptrsize * 2 + nb = get_terminal_size()[1] // l + i = 1 + line = "" + changed_color = gef.config["theme.registers_value_changed"] + regname_color = gef.config["theme.registers_register_name"] + + for reg in gef.arch.all_registers: + if reg in ignored_registers: + continue + + try: + r = gdb.parse_and_eval(reg) + if r.type.code == gdb.TYPE_CODE_VOID: + continue + + new_value_type_flag = r.type.code == gdb.TYPE_CODE_FLAGS + new_value = int(r) + + except (gdb.MemoryError, gdb.error): + # If this exception is triggered, it means that the current register + # is corrupted. Just use the register "raw" value (not eval-ed) + new_value = gef.arch.register(reg) + new_value_type_flag = False + + except Exception: + new_value = 0 + new_value_type_flag = False + + old_value = self.old_registers.get(reg, 0) + + padreg = reg.ljust(widest, " ") + value = align_address(new_value) + old_value = align_address(old_value or 0) + if value == old_value: + line += f"{Color.colorify(padreg, regname_color)}: " + else: + line += f"{Color.colorify(padreg, changed_color)}: " + if new_value_type_flag: + line += f"{format_address_spaces(value)} " + else: + addr = lookup_address(align_address(int(value))) + if addr.valid: + line += f"{addr!s} " + else: + line += f"{format_address_spaces(value)} " + + if i % nb == 0: + gef_print(line) + line = "" + i += 1 + + if line: + gef_print(line) + + gef_print(f"Flags: {gef.arch.flag_register_to_human()}") + return + + def context_stack(self) -> None: + self.context_title("stack") + + show_raw = self["show_stack_raw"] + nb_lines = self["nb_lines_stack"] + + try: + sp = gef.arch.sp + if show_raw is True: + mem = gef.memory.read(sp, 0x10 * nb_lines) + gef_print(hexdump(mem, base=sp)) + else: + gdb.execute(f"dereference -l {nb_lines:d} {sp:#x}") + + except gdb.MemoryError: + err("Cannot read memory from $SP (corrupted stack pointer?)") + + return + + def addr_has_breakpoint(self, address: int, bp_locations: List[str]) -> bool: + return any(hex(address) in b for b in bp_locations) + + def context_code(self) -> None: + nb_insn = self["nb_lines_code"] + nb_insn_prev = self["nb_lines_code_prev"] + show_opcodes_size = "show_opcodes_size" in self and self["show_opcodes_size"] + past_insns_color = gef.config["theme.old_context"] + cur_insn_color = gef.config["theme.disassemble_current_instruction"] + pc = gef.arch.pc + breakpoints = gdb.breakpoints() or [] + bp_locations = [b.location for b in breakpoints if b.location and b.location.startswith("*")] + + frame = gdb.selected_frame() + arch_name = f"{gef.arch.arch.lower()}:{gef.arch.mode}" + + self.context_title(f"code:{arch_name}") + + try: + + + for insn in self.instruction_iterator(pc, nb_insn, nb_prev=nb_insn_prev): + line = [] + is_taken = False + target = None + bp_prefix = Color.redify(BP_GLYPH) if self.addr_has_breakpoint(insn.address, bp_locations) else " " + + if show_opcodes_size == 0: + text = str(insn) + else: + insn_fmt = f"{{:{show_opcodes_size}o}}" + text = insn_fmt.format(insn) + + if insn.address < pc: + line += f"{bp_prefix} {Color.colorify(text, past_insns_color)}" + + elif insn.address == pc: + line += f"{bp_prefix}{Color.colorify(f'{RIGHT_ARROW[1:]}{text}', cur_insn_color)}" + + if gef.arch.is_conditional_branch(insn): + is_taken, reason = gef.arch.is_branch_taken(insn) + if is_taken: + target = insn.operands[-1].split()[0] + reason = f"[Reason: {reason}]" if reason else "" + line += Color.colorify(f"\tTAKEN {reason}", "bold green") + else: + reason = f"[Reason: !({reason})]" if reason else "" + line += Color.colorify(f"\tNOT taken {reason}", "bold red") + elif gef.arch.is_call(insn) and self["peek_calls"] is True: + target = insn.operands[-1].split()[0] + elif gef.arch.is_ret(insn) and self["peek_ret"] is True: + target = gef.arch.get_ra(insn, frame) + + else: + line += f"{bp_prefix} {text}" + + gef_print("".join(line)) + + if target: + try: + address = int(target, 0) if isinstance(target, str) else target + except ValueError: + # If the operand isn't an address right now we can't parse it + continue + for i, tinsn in enumerate(self.instruction_iterator(address, nb_insn)): + text= f" {DOWN_ARROW if i == 0 else ' '} {tinsn!s}" + gef_print(text) + break + + except gdb.MemoryError: + err("Cannot disassemble from $PC") + return + + def context_args(self) -> None: + insn = gef_current_instruction(gef.arch.pc) + if not gef.arch.is_call(insn): + return + + self.size2type = { + 1: "BYTE", + 2: "WORD", + 4: "DWORD", + 8: "QWORD", + } + + if insn.operands[-1].startswith(self.size2type[gef.arch.ptrsize]+" PTR"): + target = "*" + insn.operands[-1].split()[-1] + elif "$"+insn.operands[0] in gef.arch.all_registers: + target = f"*{gef.arch.register('$' + insn.operands[0]):#x}" + else: + # is there a symbol? + ops = " ".join(insn.operands) + if "<" in ops and ">" in ops: + # extract it + target = re.sub(r".*<([^\(> ]*).*", r"\1", ops) + else: + # it's an address, just use as is + target = re.sub(r".*(0x[a-fA-F0-9]*).*", r"\1", ops) + + sym = gdb.lookup_global_symbol(target) + if sym is None: + self.print_guessed_arguments(target) + return + + if sym.type.code != gdb.TYPE_CODE_FUNC: + err(f"Symbol '{target}' is not a function: type={sym.type.code}") + return + + self.print_arguments_from_symbol(target, sym) + return + + def print_arguments_from_symbol(self, function_name: str, symbol: "gdb.Symbol") -> None: + """If symbols were found, parse them and print the argument adequately.""" + args = [] + + for i, f in enumerate(symbol.type.fields()): + _value = gef.arch.get_ith_parameter(i, in_func=False)[1] + _value = RIGHT_ARROW.join(dereference_from(_value)) + _name = f.name or f"var_{i}" + _type = f.type.name or self.size2type[f.type.sizeof] + args.append(f"{_type} {_name} = {_value}") + + self.context_title("arguments") + + if not args: + gef_print(f"{function_name} ()") + return + + gef_print(f"{function_name} (\n "+",\n ".join(args)+"\n)") + return + + def print_guessed_arguments(self, function_name: str) -> None: + """When no symbol, read the current basic block and look for "interesting" instructions.""" + + def __get_current_block_start_address() -> Optional[int]: + pc = gef.arch.pc + try: + block = gdb.block_for_pc(pc) + block_start = block.start if block else gdb_get_nth_previous_instruction_address(pc, 5) + except RuntimeError: + block_start = gdb_get_nth_previous_instruction_address(pc, 5) + return block_start + + parameter_set = set() + pc = gef.arch.pc + block_start = __get_current_block_start_address() + if not block_start: + return + + function_parameters = gef.arch.function_parameters + arg_key_color = gef.config["theme.registers_register_name"] + + for insn in self.instruction_iterator(block_start, pc - block_start): + if not insn.operands: + continue + + if is_x86_32(): + if insn.mnemonic == "push": + parameter_set.add(insn.operands[0]) + else: + op = "$" + insn.operands[0] + if op in function_parameters: + parameter_set.add(op) + + if is_x86_64(): + # also consider extended registers + extended_registers = {"$rdi": ["$edi", "$di"], + "$rsi": ["$esi", "$si"], + "$rdx": ["$edx", "$dx"], + "$rcx": ["$ecx", "$cx"], + } + for exreg in extended_registers: + if op in extended_registers[exreg]: + parameter_set.add(exreg) + + if is_x86_32(): + nb_argument = len(parameter_set) + else: + nb_argument = max([function_parameters.index(p)+1 for p in parameter_set], default=0) + + args = [] + for i in range(nb_argument): + _key, _values = gef.arch.get_ith_parameter(i, in_func=False) + _values = RIGHT_ARROW.join(dereference_from(_values)) + args.append(f"{Color.colorify(_key, arg_key_color)} = {_values}") + + self.context_title("arguments (guessed)") + gef_print(f"{function_name} (") + if args: + gef_print(" " + ",\n ".join(args)) + gef_print(")") + return + + def line_has_breakpoint(self, file_name: str, line_number: int, bp_locations: List[str]) -> bool: + filename_line = f"{file_name}:{line_number}" + return any(filename_line in loc for loc in bp_locations) + + def context_source(self) -> None: + try: + pc = gef.arch.pc + symtabline = gdb.find_pc_line(pc) + symtab = symtabline.symtab + # we subtract one because the line number returned by gdb start at 1 + line_num = symtabline.line - 1 + if not symtab.is_valid(): + return + + fpath = symtab.fullname() + with open(fpath, "r") as f: + lines = [l.rstrip() for l in f.readlines()] + + except Exception: + return + + file_base_name = os.path.basename(symtab.filename) + breakpoints = gdb.breakpoints() or [] + bp_locations = [b.location for b in breakpoints if b.location and file_base_name in b.location] + past_lines_color = gef.config["theme.old_context"] + + nb_line = self["nb_lines_code"] + fn = symtab.filename + if len(fn) > 20: + fn = f"{fn[:15]}[...]{os.path.splitext(fn)[1]}" + title = f"source:{fn}+{line_num + 1}" + cur_line_color = gef.config["theme.source_current_line"] + self.context_title(title) + show_extra_info = self["show_source_code_variable_values"] + + for i in range(line_num - nb_line + 1, line_num + nb_line): + if i < 0: + continue + + bp_prefix = Color.redify(BP_GLYPH) if self.line_has_breakpoint(file_base_name, i + 1, bp_locations) else " " + + if i < line_num: + gef_print("{}{}".format(bp_prefix, Color.colorify(f" {i + 1:4d}\t {lines[i]}", past_lines_color))) + + if i == line_num: + prefix = f"{bp_prefix}{RIGHT_ARROW[1:]}{i + 1:4d}\t " + leading = len(lines[i]) - len(lines[i].lstrip()) + if show_extra_info: + extra_info = self.get_pc_context_info(pc, lines[i]) + if extra_info: + gef_print(f"{' ' * (len(prefix) + leading)}{extra_info}") + gef_print(Color.colorify(f"{prefix}{lines[i]}", cur_line_color)) + + if i > line_num: + try: + gef_print(f"{bp_prefix} {i + 1:4d}\t {lines[i]}") + except IndexError: + break + return + + def get_pc_context_info(self, pc: int, line: str) -> str: + try: + current_block = gdb.block_for_pc(pc) + if not current_block or not current_block.is_valid(): return "" + m = collections.OrderedDict() + while current_block and not current_block.is_static: + for sym in current_block: + symbol = sym.name + if not sym.is_function and re.search(fr"\W{symbol}\W", line): + val = gdb.parse_and_eval(symbol) + if val.type.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_ARRAY): + addr = int(val.address) + addrs = dereference_from(addr) + if len(addrs) > 2: + addrs = [addrs[0], "[...]", addrs[-1]] + + f = f" {RIGHT_ARROW} " + val = f.join(addrs) + elif val.type.code == gdb.TYPE_CODE_INT: + val = hex(int(val)) + else: + continue + + if symbol not in m: + m[symbol] = val + current_block = current_block.superblock + + if m: + return "// " + ", ".join([f"{Color.yellowify(a)}={b}" for a, b in m.items()]) + except Exception: + pass + return "" + + def context_trace(self) -> None: + self.context_title("trace") + + nb_backtrace = self["nb_lines_backtrace"] + if nb_backtrace <= 0: + return + + # backward compat for gdb (gdb < 7.10) + if not hasattr(gdb, "FrameDecorator"): + gdb.execute(f"backtrace {nb_backtrace:d}") + return + + orig_frame = gdb.selected_frame() + current_frame = gdb.newest_frame() + frames = [current_frame] + while current_frame != orig_frame: + current_frame = current_frame.older() + frames.append(current_frame) + + nb_backtrace_before = self["nb_lines_backtrace_before"] + level = max(len(frames) - nb_backtrace_before - 1, 0) + current_frame = frames[level] + + while current_frame: + current_frame.select() + if not current_frame.is_valid(): + continue + + pc = current_frame.pc() + name = current_frame.name() + items = [] + items.append(f"{pc:#x}") + if name: + frame_args = gdb.FrameDecorator.FrameDecorator(current_frame).frame_args() or [] + m = "{}({})".format(Color.greenify(name), + ", ".join(["{}={!s}".format(Color.yellowify(x.sym), + x.sym.value(current_frame)) for x in frame_args])) + items.append(m) + else: + try: + insn = next(gef_disassemble(pc, 1)) + except gdb.MemoryError: + break + + # check if the gdb symbol table may know the address + sym_found = gdb_get_location_from_symbol(pc) + symbol = "" + if sym_found: + sym_name, offset = sym_found + symbol = f" <{sym_name}+{offset:x}> " + + items.append(Color.redify(f"{symbol}{insn.mnemonic} {', '.join(insn.operands)}")) + + gef_print("[{}] {}".format(Color.colorify(f"#{level}", "bold green" if current_frame == orig_frame else "bold pink"), + RIGHT_ARROW.join(items))) + current_frame = current_frame.older() + level += 1 + nb_backtrace -= 1 + if nb_backtrace == 0: + break + + orig_frame.select() + return + + def context_threads(self) -> None: + def reason() -> str: + res = gdb.execute("info program", to_string=True) + if not res: + return "NOT RUNNING" + + for line in res.splitlines(): + line = line.strip() + if line.startswith("It stopped with signal "): + return line.replace("It stopped with signal ", "").split(",", 1)[0] + if line == "The program being debugged is not being run.": + return "NOT RUNNING" + if line == "It stopped at a breakpoint that has since been deleted.": + return "TEMPORARY BREAKPOINT" + if line.startswith("It stopped at breakpoint "): + return "BREAKPOINT" + if line == "It stopped after being stepped.": + return "SINGLE STEP" + + return "STOPPED" + + self.context_title("threads") + + threads = gdb.selected_inferior().threads()[::-1] + idx = self["nb_lines_threads"] + if idx > 0: + threads = threads[0:idx] + + if idx == 0: + return + + if not threads: + err("No thread selected") + return + + selected_thread = gdb.selected_thread() + selected_frame = gdb.selected_frame() + + for i, thread in enumerate(threads): + line = f"[{Color.colorify(f'#{i:d}', 'bold green' if thread == selected_thread else 'bold pink')}] Id {thread.num:d}, " + if thread.name: + line += f"""Name: "{thread.name}", """ + if thread.is_running(): + line += Color.colorify("running", "bold green") + elif thread.is_stopped(): + line += Color.colorify("stopped", "bold red") + thread.switch() + frame = gdb.selected_frame() + frame_name = frame.name() + + # check if the gdb symbol table may know the address + if not frame_name: + sym_found = gdb_get_location_from_symbol(frame.pc()) + if sym_found: + sym_name, offset = sym_found + frame_name = f"<{sym_name}+{offset:x}>" + + line += (f" {Color.colorify(f'{frame.pc():#x}', 'blue')} in " + f"{Color.colorify(frame_name or '??', 'bold yellow')} (), " + f"reason: {Color.colorify(reason(), 'bold pink')}") + elif thread.is_exited(): + line += Color.colorify("exited", "bold yellow") + gef_print(line) + i += 1 + + selected_thread.switch() + selected_frame.select() + return + + def context_additional_information(self) -> None: + if not gef.ui.context_messages: + return + + self.context_title("extra") + for level, text in gef.ui.context_messages: + if level == "error": err(text) + elif level == "warn": warn(text) + elif level == "success": ok(text) + else: info(text) + return + + def context_memory(self) -> None: + for address, opt in sorted(gef.ui.watches.items()): + sz, fmt = opt[0:2] + self.context_title(f"memory:{address:#x}") + if fmt == "pointers": + gdb.execute(f"dereference -l {sz:d} {address:#x}") + else: + gdb.execute(f"hexdump {fmt} -s {sz:d} {address:#x}") + + @classmethod + def update_registers(cls, _) -> None: + for reg in gef.arch.all_registers: + try: + cls.old_registers[reg] = gef.arch.register(reg) + except Exception: + cls.old_registers[reg] = 0 + return + + def empty_extra_messages(self, _) -> None: + gef.ui.context_messages.clear() + return + + +@register +class MemoryCommand(GenericCommand): + """Add or remove address ranges to the memory view.""" + _cmdline_ = "memory" + _syntax_ = f"{_cmdline_} (watch|unwatch|reset|list)" + + def __init__(self) -> None: + super().__init__(prefix=True) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + self.usage() + return + + +@register +class MemoryWatchCommand(GenericCommand): + """Adds address ranges to the memory view.""" + _cmdline_ = "memory watch" + _syntax_ = f"{_cmdline_} ADDRESS [SIZE] [(qword|dword|word|byte|pointers)]" + _example_ = (f"\n{_cmdline_} 0x603000 0x100 byte" + f"\n{_cmdline_} $sp") + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if len(argv) not in (1, 2, 3): + self.usage() + return + + address = parse_address(argv[0]) + size = parse_address(argv[1]) if len(argv) > 1 else 0x10 + group = "byte" + + if len(argv) == 3: + group = argv[2].lower() + if group not in ("qword", "dword", "word", "byte", "pointers"): + warn(f"Unexpected grouping '{group}'") + self.usage() + return + else: + if gef.arch.ptrsize == 4: + group = "dword" + elif gef.arch.ptrsize == 8: + group = "qword" + + gef.ui.watches[address] = (size, group) + ok(f"Adding memwatch to {address:#x}") + return + + +@register +class MemoryUnwatchCommand(GenericCommand): + """Removes address ranges to the memory view.""" + _cmdline_ = "memory unwatch" + _syntax_ = f"{_cmdline_} ADDRESS" + _example_ = (f"\n{_cmdline_} 0x603000" + f"\n{_cmdline_} $sp") + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if not argv: + self.usage() + return + + address = parse_address(argv[0]) + res = gef.ui.watches.pop(address, None) + if not res: + warn(f"You weren't watching {address:#x}") + else: + ok(f"Removed memwatch of {address:#x}") + return + + +@register +class MemoryWatchResetCommand(GenericCommand): + """Removes all watchpoints.""" + _cmdline_ = "memory reset" + _syntax_ = f"{_cmdline_}" + + @only_if_gdb_running + def do_invoke(self, _: List[str]) -> None: + gef.ui.watches.clear() + ok("Memory watches cleared") + return + + +@register +class MemoryWatchListCommand(GenericCommand): + """Lists all watchpoints to display in context layout.""" + _cmdline_ = "memory list" + _syntax_ = f"{_cmdline_}" + + @only_if_gdb_running + def do_invoke(self, _: List[str]) -> None: + if not gef.ui.watches: + info("No memory watches") + return + + info("Memory watches:") + for address, opt in sorted(gef.ui.watches.items()): + gef_print(f"- {address:#x} ({opt[0]}, {opt[1]})") + return + + +@register +class HexdumpCommand(GenericCommand): + """Display SIZE lines of hexdump from the memory location pointed by LOCATION.""" + + _cmdline_ = "hexdump" + _syntax_ = f"{_cmdline_} (qword|dword|word|byte) [LOCATION] [--size SIZE] [--reverse]" + _example_ = f"{_cmdline_} byte $rsp --size 16 --reverse" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION, prefix=True) + self["always_show_ascii"] = (False, "If true, hexdump will always display the ASCII dump") + self.format: Optional[str] = None + self.__last_target = "$sp" + return + + @only_if_gdb_running + @parse_arguments({"address": "",}, {("--reverse", "-r"): True, ("--size", "-s"): 0}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + valid_formats = ["byte", "word", "dword", "qword"] + if not self.format or self.format not in valid_formats: + err("Invalid command") + return + + args : argparse.Namespace = kwargs["arguments"] + target = args.address or self.__last_target + start_addr = parse_address(target) + read_from = align_address(start_addr) + + if self.format == "byte": + read_len = args.size or 0x40 + read_from += self.repeat_count * read_len + mem = gef.memory.read(read_from, read_len) + lines = hexdump(mem, base=read_from).splitlines() + else: + read_len = args.size or 0x10 + lines = self._hexdump(read_from, read_len, self.format, self.repeat_count * read_len) + + if args.reverse: + lines.reverse() + + self.__last_target = target + gef_print("\n".join(lines)) + return + + def _hexdump(self, start_addr: int, length: int, arrange_as: str, offset: int = 0) -> List[str]: + endianness = gef.arch.endianness + + base_address_color = gef.config["theme.dereference_base_address"] + show_ascii = gef.config["hexdump.always_show_ascii"] + + formats = { + "qword": ("Q", 8), + "dword": ("I", 4), + "word": ("H", 2), + } + + r, l = formats[arrange_as] + fmt_str = f"{{base}}{VERTICAL_LINE}+{{offset:#06x}} {{sym}}{{val:#0{l*2+2}x}} {{text}}" + fmt_pack = f"{endianness!s}{r}" + lines = [] + + i = 0 + text = "" + while i < length: + cur_addr = start_addr + (i + offset) * l + sym = gdb_get_location_from_symbol(cur_addr) + sym = "<{:s}+{:04x}> ".format(*sym) if sym else "" + mem = gef.memory.read(cur_addr, l) + val = struct.unpack(fmt_pack, mem)[0] + if show_ascii: + text = "".join([chr(b) if 0x20 <= b < 0x7F else "." for b in mem]) + lines.append(fmt_str.format(base=Color.colorify(format_address(cur_addr), base_address_color), + offset=(i + offset) * l, sym=sym, val=val, text=text)) + i += 1 + + return lines + + +@register +class HexdumpQwordCommand(HexdumpCommand): + """Display SIZE lines of hexdump as QWORD from the memory location pointed by ADDRESS.""" + + _cmdline_ = "hexdump qword" + _syntax_ = f"{_cmdline_} [ADDRESS] [[L][SIZE]] [REVERSE]" + _example_ = f"{_cmdline_} qword $rsp L16 REVERSE" + + def __init__(self) -> None: + super().__init__() + self.format = "qword" + return + + +@register +class HexdumpDwordCommand(HexdumpCommand): + """Display SIZE lines of hexdump as DWORD from the memory location pointed by ADDRESS.""" + + _cmdline_ = "hexdump dword" + _syntax_ = f"{_cmdline_} [ADDRESS] [[L][SIZE]] [REVERSE]" + _example_ = f"{_cmdline_} $esp L16 REVERSE" + + def __init__(self) -> None: + super().__init__() + self.format = "dword" + return + + +@register +class HexdumpWordCommand(HexdumpCommand): + """Display SIZE lines of hexdump as WORD from the memory location pointed by ADDRESS.""" + + _cmdline_ = "hexdump word" + _syntax_ = f"{_cmdline_} [ADDRESS] [[L][SIZE]] [REVERSE]" + _example_ = f"{_cmdline_} $esp L16 REVERSE" + + def __init__(self) -> None: + super().__init__() + self.format = "word" + return + + +@register +class HexdumpByteCommand(HexdumpCommand): + """Display SIZE lines of hexdump as BYTE from the memory location pointed by ADDRESS.""" + + _cmdline_ = "hexdump byte" + _syntax_ = f"{_cmdline_} [ADDRESS] [[L][SIZE]] [REVERSE]" + _example_ = f"{_cmdline_} $rsp L16" + + def __init__(self) -> None: + super().__init__() + self.format = "byte" + return + + +@register +class PatchCommand(GenericCommand): + """Write specified values to the specified address.""" + + _cmdline_ = "patch" + _syntax_ = (f"{_cmdline_} (qword|dword|word|byte) LOCATION VALUES\n" + f"{_cmdline_} string LOCATION \"double-escaped string\"") + SUPPORTED_SIZES = { + "qword": (8, "Q"), + "dword": (4, "L"), + "word": (2, "H"), + "byte": (1, "B"), + } + + def __init__(self) -> None: + super().__init__(prefix=True, complete=gdb.COMPLETE_LOCATION) + self.format: Optional[str] = None + return + + @only_if_gdb_running + @parse_arguments({"location": "", "values": ["", ]}, {}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + if not self.format or self.format not in self.SUPPORTED_SIZES: + self.usage() + return + + if not args.location or not args.values: + self.usage() + return + + addr = align_address(parse_address(args.location)) + size, fcode = self.SUPPORTED_SIZES[self.format] + values = args.values + + if size == 1: + if values[0].startswith("$_gef"): + var_name = values[0] + try: + values = str(gdb.parse_and_eval(var_name)).lstrip("{").rstrip("}").replace(",","").split(" ") + except: + gef_print(f"Bad variable specified, check value with command: p {var_name}") + return + + d = str(gef.arch.endianness) + for value in values: + value = parse_address(value) & ((1 << size * 8) - 1) + vstr = struct.pack(d + fcode, value) + gef.memory.write(addr, vstr, length=size) + addr += size + return + + +@register +class PatchQwordCommand(PatchCommand): + """Write specified QWORD to the specified address.""" + + _cmdline_ = "patch qword" + _syntax_ = f"{_cmdline_} LOCATION QWORD1 [QWORD2 [QWORD3..]]" + _example_ = f"{_cmdline_} $rip 0x4141414141414141" + + def __init__(self) -> None: + super().__init__() + self.format = "qword" + return + + +@register +class PatchDwordCommand(PatchCommand): + """Write specified DWORD to the specified address.""" + + _cmdline_ = "patch dword" + _syntax_ = f"{_cmdline_} LOCATION DWORD1 [DWORD2 [DWORD3..]]" + _example_ = f"{_cmdline_} $rip 0x41414141" + + def __init__(self) -> None: + super().__init__() + self.format = "dword" + return + + +@register +class PatchWordCommand(PatchCommand): + """Write specified WORD to the specified address.""" + + _cmdline_ = "patch word" + _syntax_ = f"{_cmdline_} LOCATION WORD1 [WORD2 [WORD3..]]" + _example_ = f"{_cmdline_} $rip 0x4141" + + def __init__(self) -> None: + super().__init__() + self.format = "word" + return + + +@register +class PatchByteCommand(PatchCommand): + """Write specified BYTE to the specified address.""" + + _cmdline_ = "patch byte" + _syntax_ = f"{_cmdline_} LOCATION BYTE1 [BYTE2 [BYTE3..]]" + _example_ = f"{_cmdline_} $pc 0x41 0x41 0x41 0x41 0x41" + + def __init__(self) -> None: + super().__init__() + self.format = "byte" + return + + +@register +class PatchStringCommand(GenericCommand): + """Write specified string to the specified memory location pointed by ADDRESS.""" + + _cmdline_ = "patch string" + _syntax_ = f"{_cmdline_} ADDRESS \"double backslash-escaped string\"" + _example_ = f"{_cmdline_} $sp \"GEFROCKS\"" + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + argc = len(argv) + if argc != 2: + self.usage() + return + + location, s = argv[0:2] + addr = align_address(parse_address(location)) + + try: + s = codecs.escape_decode(s)[0] + except binascii.Error: + gef_print(f"Could not decode '\\xXX' encoded string \"{s}\"") + return + + gef.memory.write(addr, s, len(s)) + return + + +@lru_cache() +def dereference_from(address: int) -> List[str]: + if not is_alive(): + return [format_address(address),] + + code_color = gef.config["theme.dereference_code"] + string_color = gef.config["theme.dereference_string"] + max_recursion = gef.config["dereference.max_recursion"] or 10 + addr = lookup_address(align_address(address)) + msg = [format_address(addr.value),] + seen_addrs = set() + + while addr.section and max_recursion: + if addr.value in seen_addrs: + msg.append("[loop detected]") + break + seen_addrs.add(addr.value) + + max_recursion -= 1 + + # Is this value a pointer or a value? + # -- If it's a pointer, dereference + deref = addr.dereference() + if deref is None: + # if here, dereferencing addr has triggered a MemoryError, no need to go further + msg.append(str(addr)) + break + + new_addr = lookup_address(deref) + if new_addr.valid: + addr = new_addr + msg.append(str(addr)) + continue + + # -- Otherwise try to parse the value + if addr.section: + if addr.section.is_executable() and addr.is_in_text_segment() and not is_ascii_string(addr.value): + insn = gef_current_instruction(addr.value) + insn_str = f"{insn.location} {insn.mnemonic} {', '.join(insn.operands)}" + msg.append(Color.colorify(insn_str, code_color)) + break + + elif addr.section.permission & Permission.READ: + if is_ascii_string(addr.value): + s = gef.memory.read_cstring(addr.value) + if len(s) < gef.arch.ptrsize: + txt = f'{format_address(deref)} ("{Color.colorify(s, string_color)}"?)' + elif len(s) > 50: + txt = Color.colorify(f'"{s[:50]}[...]"', string_color) + else: + txt = Color.colorify(f'"{s}"', string_color) + + msg.append(txt) + break + + # if not able to parse cleanly, simply display and break + val = "{:#0{ma}x}".format(int(deref & 0xFFFFFFFFFFFFFFFF), ma=(gef.arch.ptrsize * 2 + 2)) + msg.append(val) + break + + return msg + + +@register +class DereferenceCommand(GenericCommand): + """Dereference recursively from an address and display information. This acts like WinDBG `dps` + command.""" + + _cmdline_ = "dereference" + _syntax_ = f"{_cmdline_} [-h] [--length LENGTH] [--reference REFERENCE] [address]" + _aliases_ = ["telescope", ] + _example_ = f"{_cmdline_} --length 20 --reference $sp+0x10 $sp" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + self["max_recursion"] = (7, "Maximum level of pointer recursion") + return + + @staticmethod + def pprint_dereferenced(addr: int, idx: int, base_offset: int = 0) -> str: + base_address_color = gef.config["theme.dereference_base_address"] + registers_color = gef.config["theme.dereference_register_value"] + + sep = f" {RIGHT_ARROW} " + memalign = gef.arch.ptrsize + + offset = idx * memalign + current_address = align_address(addr + offset) + addrs = dereference_from(current_address) + l = "" + addr_l = format_address(int(addrs[0], 16)) + l += "{}{}{:+#07x}: {:{ma}s}".format(Color.colorify(addr_l, base_address_color), + VERTICAL_LINE, base_offset+offset, + sep.join(addrs[1:]), ma=(memalign*2 + 2)) + + register_hints = [] + + for regname in gef.arch.all_registers: + regvalue = gef.arch.register(regname) + if current_address == regvalue: + register_hints.append(regname) + + if register_hints: + m = f"\t{LEFT_ARROW}{', '.join(list(register_hints))}" + l += Color.colorify(m, registers_color) + + offset += memalign + return l + + @only_if_gdb_running + @parse_arguments({"address": "$sp"}, {("-r", "--reference"): "", ("-l", "--length"): 10}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + nb = args.length + + target = args.address + target_addr = parse_address(target) + + reference = args.reference or target + ref_addr = parse_address(reference) + + if process_lookup_address(target_addr) is None: + err(f"Unmapped address: '{target}'") + return + + if process_lookup_address(ref_addr) is None: + err(f"Unmapped address: '{reference}'") + return + + if gef.config["context.grow_stack_down"] is True: + insnum_step = -1 + if nb > 0: + from_insnum = nb * (self.repeat_count + 1) - 1 + to_insnum = self.repeat_count * nb - 1 + else: + from_insnum = self.repeat_count * nb + to_insnum = nb * (self.repeat_count + 1) + else: + insnum_step = 1 + if nb > 0: + from_insnum = self.repeat_count * nb + to_insnum = nb * (self.repeat_count + 1) + else: + from_insnum = nb * (self.repeat_count + 1) + 1 + to_insnum = (self.repeat_count * nb) + 1 + + start_address = align_address(target_addr) + base_offset = start_address - align_address(ref_addr) + + for i in range(from_insnum, to_insnum, insnum_step): + gef_print(DereferenceCommand.pprint_dereferenced(start_address, i, base_offset)) + + return + + +@register +class ASLRCommand(GenericCommand): + """View/modify the ASLR setting of GDB. By default, GDB will disable ASLR when it starts the process. (i.e. not + attached). This command allows to change that setting.""" + + _cmdline_ = "aslr" + _syntax_ = f"{_cmdline_} [(on|off)]" + + def do_invoke(self, argv: List[str]) -> None: + argc = len(argv) + + if argc == 0: + ret = gdb.execute("show disable-randomization", to_string=True) or "" + i = ret.find("virtual address space is ") + if i < 0: + return + + msg = "ASLR is currently " + if ret[i + 25:].strip() == "on.": + msg += Color.redify("disabled") + else: + msg += Color.greenify("enabled") + + gef_print(msg) + return + + elif argc == 1: + if argv[0] == "on": + info("Enabling ASLR") + gdb.execute("set disable-randomization off") + return + elif argv[0] == "off": + info("Disabling ASLR") + gdb.execute("set disable-randomization on") + return + + warn("Invalid command") + + self.usage() + return + + +@register +class ResetCacheCommand(GenericCommand): + """Reset cache of all stored data. This command is here for debugging and test purposes, GEF + handles properly the cache reset under "normal" scenario.""" + + _cmdline_ = "reset-cache" + _syntax_ = _cmdline_ + + def do_invoke(self, _: List[str]) -> None: + reset_all_caches() + return + + +@register +class VMMapCommand(GenericCommand): + """Display a comprehensive layout of the virtual memory mapping. If a filter argument, GEF will + filter out the mapping whose pathname do not match that filter.""" + + _cmdline_ = "vmmap" + _syntax_ = f"{_cmdline_} [FILTER]" + _example_ = f"{_cmdline_} libc" + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + vmmap = gef.memory.maps + if not vmmap: + err("No address mapping information found") + return + + if not gef.config["gef.disable_color"]: + self.show_legend() + + color = gef.config["theme.table_heading"] + + headers = ["Start", "End", "Offset", "Perm", "Path"] + gef_print(Color.colorify("{:<{w}s}{:<{w}s}{:<{w}s}{:<4s} {:s}".format(*headers, w=gef.arch.ptrsize*2+3), color)) + + for entry in vmmap: + if not argv: + self.print_entry(entry) + continue + if argv[0] in entry.path: + self.print_entry(entry) + elif self.is_integer(argv[0]): + addr = int(argv[0], 0) + if addr >= entry.page_start and addr < entry.page_end: + self.print_entry(entry) + return + + def print_entry(self, entry: Section) -> None: + line_color = "" + if entry.path == "[stack]": + line_color = gef.config["theme.address_stack"] + elif entry.path == "[heap]": + line_color = gef.config["theme.address_heap"] + elif entry.permission & Permission.READ and entry.permission & Permission.EXECUTE: + line_color = gef.config["theme.address_code"] + + l = [ + Color.colorify(format_address(entry.page_start), line_color), + Color.colorify(format_address(entry.page_end), line_color), + Color.colorify(format_address(entry.offset), line_color), + ] + if entry.permission == Permission.ALL: + l.append(Color.colorify(str(entry.permission), "underline " + line_color)) + else: + l.append(Color.colorify(str(entry.permission), line_color)) + + l.append(Color.colorify(entry.path, line_color)) + line = " ".join(l) + + gef_print(line) + return + + def show_legend(self) -> None: + code_addr_color = gef.config["theme.address_code"] + stack_addr_color = gef.config["theme.address_stack"] + heap_addr_color = gef.config["theme.address_heap"] + + gef_print("[ Legend: {} | {} | {} ]".format(Color.colorify("Code", code_addr_color), + Color.colorify("Heap", heap_addr_color), + Color.colorify("Stack", stack_addr_color) + )) + return + + def is_integer(self, n: str) -> bool: + try: + int(n, 0) + except ValueError: + return False + return True + + +@register +class XFilesCommand(GenericCommand): + """Shows all libraries (and sections) loaded by binary. This command extends the GDB command + `info files`, by retrieving more information from extra sources, and providing a better + display. If an argument FILE is given, the output will grep information related to only that file. + If an argument name is also given, the output will grep to the name within FILE.""" + + _cmdline_ = "xfiles" + _syntax_ = f"{_cmdline_} [FILE [NAME]]" + _example_ = f"\n{_cmdline_} libc\n{_cmdline_} libc IO_vtables" + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + color = gef.config["theme.table_heading"] + headers = ["Start", "End", "Name", "File"] + gef_print(Color.colorify("{:<{w}s}{:<{w}s}{:<21s} {:s}".format(*headers, w=gef.arch.ptrsize*2+3), color)) + + filter_by_file = argv[0] if argv and argv[0] else None + filter_by_name = argv[1] if len(argv) > 1 and argv[1] else None + + for xfile in get_info_files(): + if filter_by_file: + if filter_by_file not in xfile.filename: + continue + if filter_by_name and filter_by_name not in xfile.name: + continue + + l = [ + format_address(xfile.zone_start), + format_address(xfile.zone_end), + f"{xfile.name:<21s}", + xfile.filename, + ] + gef_print(" ".join(l)) + return + + +@register +class XAddressInfoCommand(GenericCommand): + """Retrieve and display runtime information for the location(s) given as parameter.""" + + _cmdline_ = "xinfo" + _syntax_ = f"{_cmdline_} LOCATION" + _example_ = f"{_cmdline_} $pc" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_LOCATION) + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if not argv: + err("At least one valid address must be specified") + self.usage() + return + + for sym in argv: + try: + addr = align_address(parse_address(sym)) + gef_print(titlify(f"xinfo: {addr:#x}")) + self.infos(addr) + + except gdb.error as gdb_err: + err(f"{gdb_err}") + return + + def infos(self, address: int) -> None: + addr = lookup_address(address) + if not addr.valid: + warn(f"Cannot reach {address:#x} in memory space") + return + + sect = addr.section + info = addr.info + + if sect: + gef_print(f"Page: {format_address(sect.page_start)} {RIGHT_ARROW} " + f"{format_address(sect.page_end)} (size={sect.page_end-sect.page_start:#x})" + f"\nPermissions: {sect.permission}" + f"\nPathname: {sect.path}" + f"\nOffset (from page): {addr.value-sect.page_start:#x}" + f"\nInode: {sect.inode}") + + if info: + gef_print(f"Segment: {info.name} " + f"({format_address(info.zone_start)}-{format_address(info.zone_end)})" + f"\nOffset (from segment): {addr.value-info.zone_start:#x}") + + sym = gdb_get_location_from_symbol(address) + if sym: + name, offset = sym + msg = f"Symbol: {name}" + if offset: + msg += f"+{offset:d}" + gef_print(msg) + + return + + +@register +class XorMemoryCommand(GenericCommand): + """XOR a block of memory. The command allows to simply display the result, or patch it + runtime at runtime.""" + + _cmdline_ = "xor-memory" + _syntax_ = f"{_cmdline_} (display|patch) ADDRESS SIZE KEY" + + def __init__(self) -> None: + super().__init__(prefix=True) + return + + def do_invoke(self, _: List[str]) -> None: + self.usage() + return + + +@register +class XorMemoryDisplayCommand(GenericCommand): + """Display a block of memory pointed by ADDRESS by xor-ing each byte with KEY. The key must be + provided in hexadecimal format.""" + + _cmdline_ = "xor-memory display" + _syntax_ = f"{_cmdline_} ADDRESS SIZE KEY" + _example_ = f"{_cmdline_} $sp 16 41414141" + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if len(argv) != 3: + self.usage() + return + + address = parse_address(argv[0]) + length = int(argv[1], 0) + key = argv[2] + block = gef.memory.read(address, length) + info(f"Displaying XOR-ing {address:#x}-{address + len(block):#x} with {key!r}") + + gef_print(titlify("Original block")) + gef_print(hexdump(block, base=address)) + + gef_print(titlify("XOR-ed block")) + gef_print(hexdump(xor(block, key), base=address)) + return + + +@register +class XorMemoryPatchCommand(GenericCommand): + """Patch a block of memory pointed by ADDRESS by xor-ing each byte with KEY. The key must be + provided in hexadecimal format.""" + + _cmdline_ = "xor-memory patch" + _syntax_ = f"{_cmdline_} ADDRESS SIZE KEY" + _example_ = f"{_cmdline_} $sp 16 41414141" + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if len(argv) != 3: + self.usage() + return + + address = parse_address(argv[0]) + length = int(argv[1], 0) + key = argv[2] + block = gef.memory.read(address, length) + info(f"Patching XOR-ing {address:#x}-{address + len(block):#x} with {key!r}") + xored_block = xor(block, key) + gef.memory.write(address, xored_block, length) + return + + +@register +class TraceRunCommand(GenericCommand): + """Create a runtime trace of all instructions executed from $pc to LOCATION specified. The + trace is stored in a text file that can be next imported in IDA Pro to visualize the runtime + path.""" + + _cmdline_ = "trace-run" + _syntax_ = f"{_cmdline_} LOCATION [MAX_CALL_DEPTH]" + _example_ = f"{_cmdline_} 0x555555554610" + + def __init__(self) -> None: + super().__init__(self._cmdline_, complete=gdb.COMPLETE_LOCATION) + self["max_tracing_recursion"] = (1, "Maximum depth of tracing") + self["tracefile_prefix"] = ("./gef-trace-", "Specify the tracing output file prefix") + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + if len(argv) not in (1, 2): + self.usage() + return + + if len(argv) == 2 and argv[1].isdigit(): + depth = int(argv[1]) + else: + depth = 1 + + try: + loc_start = gef.arch.pc + loc_end = parse_address(argv[0]) + except gdb.error as e: + err(f"Invalid location: {e}") + return + + self.trace(loc_start, loc_end, depth) + return + + def get_frames_size(self) -> int: + n = 0 + f = gdb.newest_frame() + while f: + n += 1 + f = f.older() + return n + + def trace(self, loc_start: int, loc_end: int, depth: int) -> None: + info(f"Tracing from {loc_start:#x} to {loc_end:#x} (max depth={depth:d})") + logfile = f"{self['tracefile_prefix']}{loc_start:#x}-{loc_end:#x}.txt" + with RedirectOutputContext(to=logfile): + hide_context() + self.start_tracing(loc_start, loc_end, depth) + unhide_context() + ok(f"Done, logfile stored as '{logfile}'") + info("Hint: import logfile with `ida_color_gdb_trace.py` script in IDA to visualize path") + return + + def start_tracing(self, loc_start: int, loc_end: int, depth: int) -> None: + loc_cur = loc_start + frame_count_init = self.get_frames_size() + + gef_print("#", + f"# Execution tracing of {get_filepath()}", + f"# Start address: {format_address(loc_start)}", + f"# End address: {format_address(loc_end)}", + f"# Recursion level: {depth:d}", + "# automatically generated by gef.py", + "#\n", sep="\n") + + while loc_cur != loc_end: + try: + delta = self.get_frames_size() - frame_count_init + + if delta <= depth: + gdb.execute("stepi") + else: + gdb.execute("finish") + + loc_cur = gef.arch.pc + gdb.flush() + + except gdb.error as e: + gef_print("#", + f"# Execution interrupted at address {format_address(loc_cur)}", + f"# Exception: {e}", + "#\n", sep="\n") + break + + return + + +@register +class PatternCommand(GenericCommand): + """Generate or Search a De Bruijn Sequence of unique substrings of length N + and a total length of LENGTH. The default value of N is set to match the + currently loaded architecture.""" + + _cmdline_ = "pattern" + _syntax_ = f"{_cmdline_} (create|search) ARGS" + + def __init__(self) -> None: + super().__init__(prefix=True) + self["length"] = (1024, "Default length of a cyclic buffer to generate") + return + + def do_invoke(self, _: List[str]) -> None: + self.usage() + return + + +@register +class PatternCreateCommand(GenericCommand): + """Generate a De Bruijn Sequence of unique substrings of length N and a + total length of LENGTH. The default value of N is set to match the currently + loaded architecture.""" + + _cmdline_ = "pattern create" + _syntax_ = f"{_cmdline_} [-h] [-n N] [length]" + _example_ = f"{_cmdline_} 4096" + + @parse_arguments({"length": 0}, {("-n", "--n"): 0}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args : argparse.Namespace = kwargs["arguments"] + length = args.length or gef.config["pattern.length"] + n = args.n or gef.arch.ptrsize + info(f"Generating a pattern of {length:d} bytes (n={n:d})") + pattern_str = gef_pystring(generate_cyclic_pattern(length, n)) + gef_print(pattern_str) + ok(f"Saved as '{gef_convenience(pattern_str)}'") + return + + +@register +class PatternSearchCommand(GenericCommand): + """Search a De Bruijn Sequence of unique substrings of length N and a + maximum total length of MAX_LENGTH. The default value of N is set to match + the currently loaded architecture. The PATTERN argument can be a GDB symbol + (such as a register name), a string or a hexadecimal value""" + + _cmdline_ = "pattern search" + _syntax_ = f"{_cmdline_} [-h] [-n N] [--max-length MAX_LENGTH] [pattern]" + _example_ = [f"{_cmdline_} $pc", + f"{_cmdline_} 0x61616164", + f"{_cmdline_} aaab"] + _aliases_ = ["pattern offset"] + + @only_if_gdb_running + @parse_arguments({"pattern": ""}, {("--period", "-n"): 0, ("--max-length", "-l"): 0}) + def do_invoke(self, _: List[str], **kwargs: Any) -> None: + args = kwargs["arguments"] + if not args.pattern: + warn("No pattern provided") + return + + max_length = args.max_length or gef.config["pattern.length"] + n = args.period or gef.arch.ptrsize + if n not in (2, 4, 8) or n > gef.arch.ptrsize: + err("Incorrect value for period") + return + self.search(args.pattern, max_length, n) + return + + def search(self, pattern: str, size: int, period: int) -> None: + pattern_be, pattern_le = None, None + + # 1. check if it's a symbol (like "$sp" or "0x1337") + symbol = safe_parse_and_eval(pattern) + if symbol: + addr = int(abs(symbol)) + dereferenced_value = dereference(addr) + if dereferenced_value: + addr = int(abs(dereferenced_value)) + mask = (1<<(8 * period))-1 + addr &= mask + pattern_le = addr.to_bytes(period, 'little') + pattern_be = addr.to_bytes(period, 'big') + else: + # 2. assume it's a plain string + pattern_be = gef_pybytes(pattern) + pattern_le = gef_pybytes(pattern[::-1]) + + info(f"Searching for '{pattern_le.hex()}'/'{pattern_be.hex()}' with period={period}") + cyclic_pattern = generate_cyclic_pattern(size, period) + off = cyclic_pattern.find(pattern_le) + if off >= 0: + ok(f"Found at offset {off:d} (little-endian search) " + f"{Color.colorify('likely', 'bold red') if gef.arch.endianness == Endianness.LITTLE_ENDIAN else ''}") + return + + off = cyclic_pattern.find(pattern_be) + if off >= 0: + ok(f"Found at offset {off:d} (big-endian search) " + f"{Color.colorify('likely', 'bold green') if gef.arch.endianness == Endianness.BIG_ENDIAN else ''}") + return + + err(f"Pattern '{pattern}' not found") + return + + +@register +class ChecksecCommand(GenericCommand): + """Checksec the security properties of the current executable or passed as argument. The + command checks for the following protections: + - PIE + - NX + - RelRO + - Glibc Stack Canaries + - Fortify Source""" + + _cmdline_ = "checksec" + _syntax_ = f"{_cmdline_} [FILENAME]" + _example_ = f"{_cmdline_} /bin/ls" + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_FILENAME) + return + + def do_invoke(self, argv: List[str]) -> None: + argc = len(argv) + + if argc == 0: + filename = get_filepath() + if filename is None: + warn("No executable/library specified") + return + elif argc == 1: + filename = os.path.realpath(os.path.expanduser(argv[0])) + if not os.access(filename, os.R_OK): + err("Invalid filename") + return + else: + self.usage() + return + + info(f"{self._cmdline_} for '{filename}'") + self.print_security_properties(filename) + return + + def print_security_properties(self, filename: str) -> None: + sec = Elf(filename).checksec + for prop in sec: + if prop in ("Partial RelRO", "Full RelRO"): continue + val = sec[prop] + msg = Color.greenify(Color.boldify(TICK)) if val is True else Color.redify(Color.boldify(CROSS)) + if val and prop == "Canary" and is_alive(): + canary = gef.session.canary[0] if gef.session.canary else 0 + msg += f"(value: {canary:#x})" + + gef_print(f"{prop:<30s}: {msg}") + + if sec["Full RelRO"]: + gef_print(f"{'RelRO':<30s}: {Color.greenify('Full')}") + elif sec["Partial RelRO"]: + gef_print(f"{'RelRO':<30s}: {Color.yellowify('Partial')}") + else: + gef_print(f"{'RelRO':<30s}: {Color.redify(Color.boldify(CROSS))}") + return + + +@register +class GotCommand(GenericCommand): + """Display current status of the got inside the process.""" + + _cmdline_ = "got" + _syntax_ = f"{_cmdline_} [FUNCTION_NAME ...] " + _example_ = "got read printf exit" + + def __init__(self): + super().__init__() + self["function_resolved"] = ("green", + "Line color of the got command output for resolved function") + self["function_not_resolved"] = ("yellow", + "Line color of the got command output for unresolved function") + return + + @only_if_gdb_running + def do_invoke(self, argv: List[str]) -> None: + readelf = gef.session.constants["readelf"] + + if is_remote_debug(): + elf_file = str(gef.session.remote.lfile) + elf_virtual_path = str(gef.session.remote.file) + else: + elf_file = str(gef.session.file) + elf_virtual_path = str(gef.session.file) + + func_names_filter = argv if argv else [] + vmmap = gef.memory.maps + base_address = min(x.page_start for x in vmmap if x.path == elf_virtual_path) + end_address = max(x.page_end for x in vmmap if x.path == elf_virtual_path) + + # get the checksec output. + checksec_status = Elf(elf_file).checksec + relro_status = "Full RelRO" + full_relro = checksec_status["Full RelRO"] + pie = checksec_status["PIE"] # if pie we will have offset instead of abs address. + + if not full_relro: + relro_status = "Partial RelRO" + partial_relro = checksec_status["Partial RelRO"] + + if not partial_relro: + relro_status = "No RelRO" + + # retrieve jump slots using readelf + lines = gef_execute_external([readelf, "--relocs", elf_file], as_list=True) + jmpslots = [line for line in lines if "JUMP" in line] + + gef_print(f"\nGOT protection: {relro_status} | GOT functions: {len(jmpslots)}\n ") + + for line in jmpslots: + address, _, _, _, name = line.split()[:5] + + # if we have a filter let's skip the entries that are not requested. + if func_names_filter: + if not any(map(lambda x: x in name, func_names_filter)): + continue + + address_val = int(address, 16) + + # address_val is an offset from the base_address if we have PIE. + if pie or is_remote_debug(): + address_val = base_address + address_val + + # read the address of the function. + got_address = gef.memory.read_integer(address_val) + + # for the swag: different colors if the function has been resolved or not. + if base_address < got_address < end_address: + color = self["function_not_resolved"] + else: + color = self["function_resolved"] + + line = f"[{hex(address_val)}] " + line += Color.colorify(f"{name} {RIGHT_ARROW} {hex(got_address)}", color) + gef_print(line) + return + + +@register +class HighlightCommand(GenericCommand): + """Highlight user-defined text matches in GEF output universally.""" + _cmdline_ = "highlight" + _syntax_ = f"{_cmdline_} (add|remove|list|clear)" + _aliases_ = ["hl"] + + def __init__(self) -> None: + super().__init__(prefix=True) + self["regex"] = (False, "Enable regex highlighting") + + def do_invoke(self, _: List[str]) -> None: + return self.usage() + + +@register +class HighlightListCommand(GenericCommand): + """Show the current highlight table with matches to colors.""" + _cmdline_ = "highlight list" + _aliases_ = ["highlight ls", "hll"] + _syntax_ = _cmdline_ + + def print_highlight_table(self) -> None: + if not gef.ui.highlight_table: + err("no matches found") + return + + left_pad = max(map(len, gef.ui.highlight_table.keys())) + for match, color in sorted(gef.ui.highlight_table.items()): + print(f"{Color.colorify(match.ljust(left_pad), color)} {VERTICAL_LINE} " + f"{Color.colorify(color, color)}") + return + + def do_invoke(self, _: List[str]) -> None: + return self.print_highlight_table() + + +@register +class HighlightClearCommand(GenericCommand): + """Clear the highlight table, remove all matches.""" + _cmdline_ = "highlight clear" + _aliases_ = ["hlc"] + _syntax_ = _cmdline_ + + def do_invoke(self, _: List[str]) -> None: + return gef.ui.highlight_table.clear() + + +@register +class HighlightAddCommand(GenericCommand): + """Add a match to the highlight table.""" + _cmdline_ = "highlight add" + _syntax_ = f"{_cmdline_} MATCH COLOR" + _aliases_ = ["highlight set", "hla"] + _example_ = f"{_cmdline_} 41414141 yellow" + + def do_invoke(self, argv: List[str]) -> None: + if len(argv) < 2: + return self.usage() + + match, color = argv + gef.ui.highlight_table[match] = color + return + + +@register +class HighlightRemoveCommand(GenericCommand): + """Remove a match in the highlight table.""" + _cmdline_ = "highlight remove" + _syntax_ = f"{_cmdline_} MATCH" + _aliases_ = [ + "highlight delete", + "highlight del", + "highlight unset", + "highlight rm", + "hlr", + ] + _example_ = f"{_cmdline_} remove 41414141" + + def do_invoke(self, argv: List[str]) -> None: + if not argv: + return self.usage() + + gef.ui.highlight_table.pop(argv[0], None) + return + + +@register +class FormatStringSearchCommand(GenericCommand): + """Exploitable format-string helper: this command will set up specific breakpoints + at well-known dangerous functions (printf, snprintf, etc.), and check if the pointer + holding the format string is writable, and therefore susceptible to format string + attacks if an attacker can control its content.""" + _cmdline_ = "format-string-helper" + _syntax_ = _cmdline_ + _aliases_ = ["fmtstr-helper",] + + def do_invoke(self, _: List[str]) -> None: + dangerous_functions = { + "printf": 0, + "sprintf": 1, + "fprintf": 1, + "snprintf": 2, + "vsnprintf": 2, + } + + nb_installed_breaks = 0 + + with RedirectOutputContext(to="/dev/null"): + for function_name in dangerous_functions: + argument_number = dangerous_functions[function_name] + FormatStringBreakpoint(function_name, argument_number) + nb_installed_breaks += 1 + + ok(f"Enabled {nb_installed_breaks} FormatString " + f"breakpoint{'s' if nb_installed_breaks > 1 else ''}") + return + + +@register +class HeapAnalysisCommand(GenericCommand): + """Heap vulnerability analysis helper: this command aims to track dynamic heap allocation + done through malloc()/free() to provide some insights on possible heap vulnerabilities. The + following vulnerabilities are checked: + - NULL free + - Use-after-Free + - Double Free + - Heap overlap""" + _cmdline_ = "heap-analysis-helper" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__(complete=gdb.COMPLETE_NONE) + self["check_free_null"] = (False, "Break execution when a free(NULL) is encountered") + self["check_double_free"] = (True, "Break execution when a double free is encountered") + self["check_weird_free"] = (True, "Break execution when free() is called against a non-tracked pointer") + self["check_uaf"] = (True, "Break execution when a possible Use-after-Free condition is found") + self["check_heap_overlap"] = (True, "Break execution when a possible overlap in allocation is found") + + self.bp_malloc = None + self.bp_calloc = None + self.bp_free = None + self.bp_realloc = None + return + + @only_if_gdb_running + @experimental_feature + def do_invoke(self, argv: List[str]) -> None: + if not argv: + self.setup() + return + + if argv[0] == "show": + self.dump_tracked_allocations() + return + + def setup(self) -> None: + ok("Tracking malloc() & calloc()") + self.bp_malloc = TraceMallocBreakpoint("__libc_malloc") + self.bp_calloc = TraceMallocBreakpoint("__libc_calloc") + ok("Tracking free()") + self.bp_free = TraceFreeBreakpoint() + ok("Tracking realloc()") + self.bp_realloc = TraceReallocBreakpoint() + + ok("Disabling hardware watchpoints (this may increase the latency)") + gdb.execute("set can-use-hw-watchpoints 0") + + info("Dynamic breakpoints correctly setup, " + "GEF will break execution if a possible vulnerabity is found.") + warn(f"{Color.colorify('Note', 'bold underline yellow')}: " + "The heap analysis slows down the execution noticeably.") + + # when inferior quits, we need to clean everything for a next execution + gef_on_exit_hook(self.clean) + return + + def dump_tracked_allocations(self) -> None: + global gef + + if gef.session.heap_allocated_chunks: + ok("Tracked as in-use chunks:") + for addr, sz in gef.session.heap_allocated_chunks: + gef_print(f"{CROSS} malloc({sz:d}) = {addr:#x}") + else: + ok("No malloc() chunk tracked") + + if gef.session.heap_freed_chunks: + ok("Tracked as free-ed chunks:") + for addr, sz in gef.session.heap_freed_chunks: + gef_print(f"{TICK} free({sz:d}) = {addr:#x}") + else: + ok("No free() chunk tracked") + return + + def clean(self, _: "gdb.Event") -> None: + global gef + + ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - Cleaning up") + for bp in [self.bp_malloc, self.bp_calloc, self.bp_free, self.bp_realloc]: + if hasattr(bp, "retbp") and bp.retbp: + try: + bp.retbp.delete() + except RuntimeError: + # in some cases, gdb was found failing to correctly remove the retbp + # but they can be safely ignored since the debugging session is over + pass + + bp.delete() + + for wp in gef.session.heap_uaf_watchpoints: + wp.delete() + + gef.session.heap_allocated_chunks = [] + gef.session.heap_freed_chunks = [] + gef.session.heap_uaf_watchpoints = [] + + ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - Re-enabling hardware watchpoints") + gdb.execute("set can-use-hw-watchpoints 1") + + gef_on_exit_unhook(self.clean) + return + + +# +# GDB Function declaration +# +@deprecated("") +def register_function(cls: Type["GenericFunction"]) -> Type["GenericFunction"]: + """Decorator for registering a new convenience function to GDB.""" + return cls + + +class GenericFunction(gdb.Function): + """This is an abstract class for invoking convenience functions, should not be instantiated.""" + + _function_ : str + _syntax_: str = "" + _example_ : str = "" + + def __init__(self) -> None: + super().__init__(self._function_) + + def invoke(self, *args: Any) -> int: + if not is_alive(): + raise gdb.GdbError("No debugging session active") + return self.do_invoke(args) + + def arg_to_long(self, args: List, index: int, default: int = 0) -> int: + try: + addr = args[index] + return int(addr) if addr.address is None else int(addr.address) + except IndexError: + return default + + def do_invoke(self, args: Any) -> int: + raise NotImplementedError + + +@register +class StackOffsetFunction(GenericFunction): + """Return the current stack base address plus an optional offset.""" + _function_ = "_stack" + _syntax_ = f"${_function_}()" + + def do_invoke(self, args: List) -> int: + base = get_section_base_address("[stack]") + if not base: + raise gdb.GdbError("Stack not found") + + return self.arg_to_long(args, 0) + base + + +@register +class HeapBaseFunction(GenericFunction): + """Return the current heap base address plus an optional offset.""" + _function_ = "_heap" + _syntax_ = f"${_function_}()" + + def do_invoke(self, args: List) -> int: + base = gef.heap.base_address + if not base: + base = get_section_base_address("[heap]") + if not base: + raise gdb.GdbError("Heap not found") + return self.arg_to_long(args, 0) + base + + +@register +class SectionBaseFunction(GenericFunction): + """Return the matching file's base address plus an optional offset. + Defaults to current file. Note that quotes need to be escaped""" + _function_ = "_base" + _syntax_ = "$_base([filepath])" + _example_ = "p $_base(\\\"/usr/lib/ld-2.33.so\\\")" + + def do_invoke(self, args: List) -> int: + addr = 0 + try: + name = args[0].string() + except IndexError: + name = gef.session.file.name + except gdb.error: + err(f"Invalid arg: {args[0]}") + return 0 + + try: + base = get_section_base_address(name) + if base: + addr = int(base) + except TypeError: + err(f"Cannot find section {name}") + return 0 + return addr + + +@register +class BssBaseFunction(GenericFunction): + """Return the current bss base address plus the given offset.""" + _function_ = "_bss" + _syntax_ = f"${_function_}([OFFSET])" + _example_ = "deref $_bss(0x20)" + + def do_invoke(self, args: List) -> int: + base = get_zone_base_address(".bss") + if not base: + raise gdb.GdbError("BSS not found") + return self.arg_to_long(args, 0) + base + + +@register +class GotBaseFunction(GenericFunction): + """Return the current GOT base address plus the given offset.""" + _function_ = "_got" + _syntax_ = f"${_function_}([OFFSET])" + _example_ = "deref $_got(0x20)" + + def do_invoke(self, args: List) -> int: + base = get_zone_base_address(".got") + if not base: + raise gdb.GdbError("GOT not found") + return base + self.arg_to_long(args, 0) + + +@register +class GefFunctionsCommand(GenericCommand): + """List the convenience functions provided by GEF.""" + _cmdline_ = "functions" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__() + self.docs = [] + self.should_refresh = True + return + + def __add__(self, function: GenericFunction): + """Add function to documentation.""" + doc = getattr(function, "__doc__", "").lstrip() + if not hasattr(function, "_syntax_"): + raise ValueError("Function is invalid") + syntax = getattr(function, "_syntax_").lstrip() + msg = f"{Color.colorify(syntax, 'bold cyan')}\n {doc}" + example = getattr(function, "_example_", "").strip() + if example: + msg += f"\n {Color.yellowify('Example:')} {example}" + self.docs.append(msg) + return self + + def __radd__(self, function: GenericFunction): + return self.__add__(function) + + def __str__(self) -> str: + if self.should_refresh: + self.__rebuild() + return self.__doc__ or "" + + def __rebuild(self) -> None: + """Rebuild the documentation for functions.""" + for function in gef.gdb.functions.values(): + self += function + + self.command_size = len(gef.gdb.commands) + _, cols = get_terminal_size() + separator = HORIZONTAL_LINE*cols + self.__doc__ = f"\n{separator}\n".join(sorted(self.docs)) + self.should_refresh = False + return + + def do_invoke(self, argv) -> None: + self.dont_repeat() + gef_print(titlify("GEF - Convenience Functions")) + gef_print("These functions can be used as arguments to other " + "commands to dynamically calculate values\n") + gef_print(str(self)) + return + + +# +# GEF internal command classes +# +class GefCommand(gdb.Command): + """GEF main command: view all new commands by typing `gef`.""" + + _cmdline_ = "gef" + _syntax_ = f"{_cmdline_} (missing|config|save|restore|set|run)" + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, True) + gef.config["gef.follow_child"] = GefSetting(True, bool, "Automatically set GDB to follow child when forking") + gef.config["gef.readline_compat"] = GefSetting(False, bool, "Workaround for readline SOH/ETX issue (SEGV)") + gef.config["gef.debug"] = GefSetting(False, bool, "Enable debug mode for gef") + gef.config["gef.autosave_breakpoints_file"] = GefSetting("", str, "Automatically save and restore breakpoints") + gef.config["gef.extra_plugins_dir"] = GefSetting("", str, "Autoload additional GEF commands from external directory", hooks={"on_write": self.load_extra_plugins}) + 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") + gef.config["gef.show_deprecation_warnings"] = GefSetting(True, bool, "Toggle the display of the `deprecated` warnings") + gef.config["gef.buffer"] = GefSetting(True, bool, "Internally buffer command output until completion") + gef.config["gef.bruteforce_main_arena"] = GefSetting(False, bool, "Allow bruteforcing main_arena symbol if everything else 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.") + + self.commands : Dict[str, GenericCommand] = collections.OrderedDict() + self.functions : Dict[str, GenericFunction] = collections.OrderedDict() + self.missing: Dict[str, Exception] = {} + return + + @property + def loaded_commands(self) -> List[Tuple[str, Type[GenericCommand], Any]]: + print("Obsolete loaded_commands") + raise + + @property + def loaded_functions(self) -> List[Type[GenericFunction]]: + print("Obsolete loaded_functions") + raise + + @property + def missing_commands(self) -> Dict[str, Exception]: + print("Obsolete missing_commands") + raise + + def setup(self) -> None: + self.load() + + GefHelpCommand() + GefConfigCommand() + GefSaveCommand() + GefMissingCommand() + GefSetCommand() + GefRunCommand() + GefInstallExtraScriptCommand() + + # restore the settings from config file if any + GefRestoreCommand() + return + + def load_extra_plugins(self) -> int: + def load_plugin(fpath: pathlib.Path) -> bool: + try: + dbg(f"Loading '{fpath}'") + gdb.execute(f"source {fpath}") + 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: + 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") + + except gdb.error as e: + err(f"failed: {e}") + return nb_added + + @property + def loaded_command_names(self) -> Iterable[str]: + print("obsolete loaded_command_names") + return self.commands.keys() + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + gdb.execute("gef help") + return + + def add_context_pane(self, pane_name: str, display_pane_function: Callable, pane_title_function: Callable, condition: Optional[Callable]) -> None: + """Add a new context pane to ContextCommand.""" + for _, class_instance in self.commands.items(): + if isinstance(class_instance, ContextCommand): + context = class_instance + break + else: + err("Cannot find ContextCommand") + return + + # assure users can toggle the new context + corrected_settings_name = pane_name.replace(" ", "_") + gef.config["context.layout"] += f" {corrected_settings_name}" + + # overload the printing of pane title + context.layout_mapping[corrected_settings_name] = (display_pane_function, pane_title_function, condition) + + def load(self) -> None: + """Load all the commands and functions defined by GEF into GDB.""" + current_commands = set( self.commands.keys() ) + new_commands = set( [x._cmdline_ for x in __registered_commands__] ) - current_commands + current_functions = set( self.functions.keys() ) + new_functions = set([x._function_ for x in __registered_functions__]) - current_functions + self.missing.clear() + self.__load_time_ms = time.time()* 1000 + + # load all new functions + for name in sorted(new_functions): + for function_cls in __registered_functions__: + if function_cls._function_ == name: + self.functions[name] = function_cls() + break + + # load all new commands + for name in sorted(new_commands): + try: + for command_cls in __registered_commands__: + if command_cls._cmdline_ == name: + command_instance = command_cls() + + # create the aliases if any + if hasattr(command_instance, "_aliases_"): + aliases = getattr(command_instance, "_aliases_") + for alias in aliases: + GefAlias(alias, name) + + self.commands[name] = command_instance + break + + except Exception as reason: + self.missing[name] = reason + + self.__load_time_ms = (time.time()* 1000) - self.__load_time_ms + return + + + def show_banner(self) -> None: + gef_print(f"{Color.greenify('GEF')} for {gef.session.os} ready, " + f"type `{Color.colorify('gef', 'underline yellow')}' to start, " + f"`{Color.colorify('gef config', 'underline pink')}' to configure") + + ver = f"{sys.version_info.major:d}.{sys.version_info.minor:d}" + gef_print(f"{Color.colorify(str(len(self.commands)), 'bold green')} commands loaded " + f"and {Color.colorify(str(len(self.functions)), 'bold blue')} functions added for " + f"GDB {Color.colorify(gdb.VERSION, 'bold yellow')} in {self.__load_time_ms:.2f}ms " + f"using Python engine {Color.colorify(ver, 'bold red')}") + + nb_missing = len(self.missing) + if nb_missing: + warn(f"{Color.colorify(str(nb_missing), 'bold red')} " + f"command{'s' if nb_missing > 1 else ''} could not be loaded, " + f"run `{Color.colorify('gef missing', 'underline pink')}` to know why.") + return + + +class GefHelpCommand(gdb.Command): + """GEF help sub-command.""" + _cmdline_ = "gef help" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False) + self.docs = [] + self.should_refresh = True + self.command_size = 0 + return + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + gef_print(titlify("GEF - GDB Enhanced Features")) + gef_print(str(self)) + return + + def __rebuild(self) -> None: + """Rebuild the documentation.""" + for name, cmd in gef.gdb.commands.items(): + self += (name, cmd) + + self.command_size = len(gef.gdb.commands) + _, cols = get_terminal_size() + separator = HORIZONTAL_LINE*cols + self.__doc__ = f"\n{separator}\n".join(sorted(self.docs)) + self.should_refresh = False + return + + def __add__(self, command: Tuple[str, GenericCommand]): + """Add command to GEF documentation.""" + cmd, class_obj = command + if " " in cmd: + # do not print subcommands in gef help + return self + doc = getattr(class_obj, "__doc__", "").lstrip() + aliases = f"Aliases: {', '.join(class_obj._aliases_)}" if hasattr(class_obj, "_aliases_") else "" + msg = f"{Color.colorify(cmd, 'bold red')}\n{doc}\n{aliases}" + self.docs.append(msg) + return self + + def __radd__(self, command: Tuple[str, GenericCommand]): + return self.__add__(command) + + def __str__(self) -> str: + """Lazily regenerate the `gef help` object if it was modified""" + # quick check in case the docs have changed + if self.should_refresh or self.command_size != len(gef.gdb.commands): + self.__rebuild() + return self.__doc__ or "" + + +class GefConfigCommand(gdb.Command): + """GEF configuration sub-command + This command will help set/view GEF settings for the current debugging session. + It is possible to make those changes permanent by running `gef save` (refer + to this command help), and/or restore previously saved settings by running + `gef restore` (refer help). + """ + _cmdline_ = "gef config" + _syntax_ = f"{_cmdline_} [setting_name] [setting_value]" + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_NONE, prefix=False) + return + + def invoke(self, args: str, from_tty: bool) -> None: + self.dont_repeat() + argv = gdb.string_to_argv(args) + argc = len(argv) + + if not (0 <= argc <= 2): + err("Invalid number of arguments") + return + + if argc == 0: + gef_print(titlify("GEF configuration settings")) + self.print_settings() + return + + if argc == 1: + prefix = argv[0] + names = [x for x in gef.config.keys() if x.startswith(prefix)] + if names: + if len(names) == 1: + gef_print(titlify(f"GEF configuration setting: {names[0]}")) + self.print_setting(names[0], verbose=True) + else: + gef_print(titlify(f"GEF configuration settings matching '{argv[0]}'")) + for name in names: self.print_setting(name) + return + + self.set_setting(argv) + return + + def print_setting(self, plugin_name: str, verbose: bool = False) -> None: + res = gef.config.raw_entry(plugin_name) + string_color = gef.config["theme.dereference_string"] + misc_color = gef.config["theme.dereference_base_address"] + + if not res: + return + + _setting = Color.colorify(plugin_name, "green") + _type = res.type.__name__ + if _type == "str": + _value = f'"{Color.colorify(res.value, string_color)}"' + else: + _value = Color.colorify(res.value, misc_color) + + gef_print(f"{_setting} ({_type}) = {_value}") + + if verbose: + gef_print(Color.colorify("\nDescription:", "bold underline")) + gef_print(f"\t{res.description}") + return + + def print_settings(self) -> None: + for x in sorted(gef.config): + self.print_setting(x) + return + + def set_setting(self, argv: Tuple[str, Any]) -> None: + global gef + key, new_value = argv + + if "." not in key: + err("Invalid command format") + return + + loaded_commands = list( gef.gdb.commands.keys()) + ["gef"] + plugin_name = key.split(".", 1)[0] + if plugin_name not in loaded_commands: + err(f"Unknown plugin '{plugin_name}'") + return + + if key not in gef.config: + err(f"'{key}' is not a valid configuration setting") + return + + _type = gef.config.raw_entry(key).type + try: + if _type == bool: + _newval = True if new_value.upper() in ("TRUE", "T", "1") else False + else: + _newval = new_value + + gef.config[key] = _newval + except Exception as e: + err(f"'{key}' expects type '{_type.__name__}', got {type(new_value).__name__}: reason {str(e)}") + return + + reset_all_caches() + return + + def complete(self, text: str, word: str) -> List[str]: + settings = sorted(gef.config) + + if text == "": + # no prefix: example: `gef config TAB` + return [s for s in settings if word in s] + + if "." not in text: + # if looking for possible prefix + return [s for s in settings if s.startswith(text.strip())] + + # finally, look for possible values for given prefix + return [s.split(".", 1)[1] for s in settings if s.startswith(text.strip())] + + +class GefSaveCommand(gdb.Command): + """GEF save sub-command. + Saves the current configuration of GEF to disk (by default in file '~/.gef.rc').""" + _cmdline_ = "gef save" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False) + return + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + cfg = configparser.RawConfigParser() + old_sect = None + + # save the configuration + for key in sorted(gef.config): + sect, optname = key.split(".", 1) + value = gef.config[key] + + if old_sect != sect: + cfg.add_section(sect) + old_sect = sect + + cfg.set(sect, optname, value) + + # save the aliases + cfg.add_section("aliases") + for alias in gef.session.aliases: + cfg.set("aliases", alias._alias, alias._command) + + with GEF_RC.open("w") as fd: + cfg.write(fd) + + ok(f"Configuration saved to '{GEF_RC}'") + return + + +class GefRestoreCommand(gdb.Command): + """GEF restore sub-command. + Loads settings from file '~/.gef.rc' and apply them to the configuration of GEF.""" + _cmdline_ = "gef restore" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False) + self.reload(True) + return + + def invoke(self, args: str, from_tty: bool) -> None: + self.dont_repeat() + if GEF_RC.is_file(): + quiet = (args.lower() == "quiet") + self.reload(quiet) + return + + def reload(self, quiet: bool): + cfg = configparser.ConfigParser() + cfg.read(GEF_RC) + + for section in cfg.sections(): + if section == "aliases": + # load the aliases + for key in cfg.options(section): + try: + GefAlias(key, cfg.get(section, key)) + except: + pass + continue + + # load the other options + for optname in cfg.options(section): + key = f"{section}.{optname}" + try: + setting = gef.config.raw_entry(key) + except Exception: + continue + new_value = cfg.get(section, optname) + if setting.type == bool: + new_value = True if new_value.upper() in ("TRUE", "T", "1") else False + setting.value = setting.type(new_value) + + if not quiet: + ok(f"Configuration from '{Color.colorify(str(GEF_RC), 'bold blue')}' restored") + return + + +class GefMissingCommand(gdb.Command): + """GEF missing sub-command + Display the GEF commands that could not be loaded, along with the reason of why + they could not be loaded. + """ + _cmdline_ = "gef missing" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False) + return + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + missing_commands = gef.gdb.missing + if not missing_commands: + ok("No missing command") + return + + for missing_command, reason in missing_commands.items(): + warn(f"Command `{missing_command}` is missing, reason {RIGHT_ARROW} {reason}") + return + + +class GefSetCommand(gdb.Command): + """Override GDB set commands with the context from GEF.""" + _cmdline_ = "gef set" + _syntax_ = f"{_cmdline_} [GDB_SET_ARGUMENTS]" + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_SYMBOL, False) + return + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + args = args.split() + cmd = ["set", args[0],] + for p in args[1:]: + if p.startswith("$_gef"): + c = gdb.parse_and_eval(p) + cmd.append(c.string()) + else: + cmd.append(p) + + gdb.execute(" ".join(cmd)) + return + + +class GefRunCommand(gdb.Command): + """Override GDB run commands with the context from GEF. + Simple wrapper for GDB run command to use arguments set from `gef set args`.""" + _cmdline_ = "gef run" + _syntax_ = f"{_cmdline_} [GDB_RUN_ARGUMENTS]" + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_FILENAME, False) + return + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + if is_alive(): + gdb.execute("continue") + return + + argv = args.split() + gdb.execute(f"gef set args {' '.join(argv)}") + gdb.execute("run") + return + + +class GefAlias(gdb.Command): + """Simple aliasing wrapper because GDB doesn't do what it should.""" + + def __init__(self, alias: str, command: str, completer_class: int = gdb.COMPLETE_NONE, command_class: int = gdb.COMMAND_NONE) -> None: + p = command.split() + if not p: + return + + if any(x for x in gef.session.aliases if x._alias == alias): + return + + self._command = command + self._alias = alias + c = command.split()[0] + r = self.lookup_command(c) + self.__doc__ = f"Alias for '{Color.greenify(command)}'" + if r is not None: + _instance = r[1] + self.__doc__ += f": {_instance.__doc__}" + + if hasattr(_instance, "complete"): + self.complete = _instance.complete + + super().__init__(alias, command_class, completer_class=completer_class) + gef.session.aliases.append(self) + return + + def invoke(self, args: Any, from_tty: bool) -> None: + gdb.execute(f"{self._command} {args}", from_tty=from_tty) + return + + def lookup_command(self, cmd: str) -> Optional[Tuple[str, GenericCommand]]: + global gef + for _name, _instance in gef.gdb.commands.items(): + if cmd == _name: + return _name, _instance + + return None + + +@register +class AliasesCommand(GenericCommand): + """Base command to add, remove, or list aliases.""" + + _cmdline_ = "aliases" + _syntax_ = f"{_cmdline_} (add|rm|ls)" + + def __init__(self) -> None: + super().__init__(prefix=True) + return + + def do_invoke(self, _: List[str]) -> None: + self.usage() + return + + +@register +class AliasesAddCommand(AliasesCommand): + """Command to add aliases.""" + + _cmdline_ = "aliases add" + _syntax_ = f"{_cmdline_} [ALIAS] [COMMAND]" + _example_ = f"{_cmdline_} scope telescope" + + def __init__(self) -> None: + super().__init__() + return + + def do_invoke(self, argv: List[str]) -> None: + if len(argv) < 2: + self.usage() + return + GefAlias(argv[0], " ".join(argv[1:])) + return + + +@register +class AliasesRmCommand(AliasesCommand): + """Command to remove aliases.""" + + _cmdline_ = "aliases rm" + _syntax_ = f"{_cmdline_} [ALIAS]" + + def __init__(self) -> None: + super().__init__() + return + + def do_invoke(self, argv: List[str]) -> None: + global gef + if len(argv) != 1: + self.usage() + return + try: + 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.") + return + gef_print("You must reload GEF for alias removals to apply.") + return + + +@register +class AliasesListCommand(AliasesCommand): + """Command to list aliases.""" + + _cmdline_ = "aliases ls" + _syntax_ = _cmdline_ + + def __init__(self) -> None: + super().__init__() + return + + 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}") + return + + +class GefTmuxSetup(gdb.Command): + """Setup a confortable tmux debugging environment.""" + + def __init__(self) -> None: + super().__init__("tmux-setup", gdb.COMMAND_NONE, gdb.COMPLETE_NONE) + GefAlias("screen-setup", "tmux-setup") + return + + def invoke(self, args: Any, from_tty: bool) -> None: + self.dont_repeat() + + tmux = os.getenv("TMUX") + if tmux: + self.tmux_setup() + return + + screen = os.getenv("TERM") + if screen is not None and screen == "screen": + self.screen_setup() + return + + warn("Not in a tmux/screen session") + return + + def tmux_setup(self) -> None: + """Prepare the tmux environment by vertically splitting the current pane, and + forcing the context to be redirected there.""" + tmux = which("tmux") + ok("tmux session found, splitting window...") + + pane, pty = subprocess.check_output([tmux, "splitw", "-h", '-F#{session_name}:#{window_index}.#{pane_index}-#{pane_tty}', "-P"]).decode().strip().split("-") + atexit.register(lambda : subprocess.run([tmux, "kill-pane", "-t", pane])) + # clear the screen and let it wait for input forever + gdb.execute(f"! {tmux} send-keys -t {pane} 'clear ; cat' C-m") + gdb.execute(f"! {tmux} select-pane -L") + + ok(f"Setting `context.redirect` to '{pty}'...") + gdb.execute(f"gef config context.redirect {pty}") + ok("Done!") + return + + def screen_setup(self) -> None: + """Hackish equivalent of the tmux_setup() function for screen.""" + screen = which("screen") + sty = os.getenv("STY") + ok("screen session found, splitting window...") + fd_script, script_path = tempfile.mkstemp() + fd_tty, tty_path = tempfile.mkstemp() + os.close(fd_tty) + + with os.fdopen(fd_script, "w") as f: + f.write("startup_message off\n") + f.write("split -v\n") + f.write("focus right\n") + f.write(f"screen bash -c 'tty > {tty_path}; clear; cat'\n") + f.write("focus left\n") + + gdb.execute(f"! {screen} -r {sty} -m -d -X source {script_path}") + # artificial delay to make sure `tty_path` is populated + time.sleep(0.25) + with open(tty_path, "r") as f: + pty = f.read().strip() + ok(f"Setting `context.redirect` to '{pty}'...") + gdb.execute(f"gef config context.redirect {pty}") + ok("Done!") + os.unlink(script_path) + os.unlink(tty_path) + return + + +class GefInstallExtraScriptCommand(gdb.Command): + """`gef install` command: installs one or more scripts from the `gef-extras` script repo. Note that the command + doesn't check for external dependencies the script(s) might require.""" + _cmdline_ = "gef install" + _syntax_ = f"{_cmdline_} SCRIPTNAME [SCRIPTNAME [SCRIPTNAME...]]" + + def __init__(self) -> None: + super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False) + self.branch = gef.config.get("gef.extras_default_branch", GEF_EXTRAS_DEFAULT_BRANCH) + return + + def invoke(self, argv: str, from_tty: bool) -> None: + self.dont_repeat() + if not argv: + err("No script name provided") + return + + args = argv.split() + + if "--list" in args or "-l" in args: + subprocess.run(["xdg-open", f"https://github.com/hugsy/gef-extras/{self.branch}/"]) + return + + self.dirpath = pathlib.Path(gef.config["gef.tempdir"]).expanduser().absolute() + if not self.dirpath.is_dir(): + err("'gef.tempdir' is not a valid directory") + return + + for script in args: + script = script.lower() + if not self.__install_extras_script(script): + warn(f"Failed to install '{script}', skipping...") + return + + + def __install_extras_script(self, script: str) -> bool: + fpath = self.dirpath / f"{script}.py" + if not fpath.exists(): + url = f"https://raw.githubusercontent.com/hugsy/gef-extras/{self.branch}/scripts/{script}.py" + info(f"Searching for '{script}.py' in `gef-extras@{self.branch}`...") + data = http_get(url) + if not data: + warn("Not found") + return False + + with fpath.open("wb") as fd: + fd.write(data) + fd.flush() + + old_command_set = set(gef.gdb.commands) + gdb.execute(f"source {fpath}") + new_command_set = set(gef.gdb.commands) + new_commands = [f"`{c[0]}`" for c in (new_command_set - old_command_set)] + ok(f"Installed file '{fpath}', new command(s) available: {', '.join(new_commands)}") + return True + + +# +# GEF internal classes +# + +def __gef_prompt__(current_prompt: Callable[[Callable], str]) -> str: + """GEF custom prompt function.""" + if gef.config["gef.readline_compat"] is True: return GEF_PROMPT + if gef.config["gef.disable_color"] is True: return GEF_PROMPT + prompt = "" + if gef.session.remote: + prompt += Color.boldify("(remote) ") + prompt += GEF_PROMPT_ON if is_alive() else GEF_PROMPT_OFF + return prompt + + +class GefManager(metaclass=abc.ABCMeta): + def reset_caches(self) -> None: + """Reset the LRU-cached attributes""" + for attr in dir(self): + try: + obj = getattr(self, attr) + if not hasattr(obj, "cache_clear"): + continue + obj.cache_clear() + except: # we're reseting the cache here, we don't care if (or which) exception triggers + continue + return + + +class GefMemoryManager(GefManager): + """Class that manages memory access for gef.""" + def __init__(self) -> None: + self.reset_caches() + return + + def reset_caches(self) -> None: + super().reset_caches() + self.__maps = None + return + + def write(self, address: int, buffer: ByteString, length: int = 0x10) -> None: + """Write `buffer` at address `address`.""" + gdb.selected_inferior().write_memory(address, buffer, length) + + def read(self, addr: int, length: int = 0x10) -> bytes: + """Return a `length` long byte array with the copy of the process memory at `addr`.""" + return gdb.selected_inferior().read_memory(addr, length).tobytes() + + def read_integer(self, addr: int) -> int: + """Return an integer read from memory.""" + sz = gef.arch.ptrsize + mem = self.read(addr, sz) + unpack = u32 if sz == 4 else u64 + return unpack(mem) + + def read_cstring(self, + address: int, + max_length: int = GEF_MAX_STRING_LENGTH, + encoding: Optional[str] = None) -> str: + """Return a C-string read from memory.""" + encoding = encoding or "unicode-escape" + length = min(address | (DEFAULT_PAGE_SIZE-1), max_length+1) + + try: + res_bytes = self.read(address, length) + except gdb.error: + err(f"Can't read memory at '{address}'") + return "" + try: + with warnings.catch_warnings(): + # ignore DeprecationWarnings (see #735) + warnings.simplefilter("ignore") + res = res_bytes.decode(encoding, "strict") + except UnicodeDecodeError: + # latin-1 as fallback due to its single-byte to glyph mapping + res = res_bytes.decode("latin-1", "replace") + + res = res.split("\x00", 1)[0] + ustr = res.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + if max_length and len(res) > max_length: + return f"{ustr[:max_length]}[...]" + return ustr + + def read_ascii_string(self, address: int) -> Optional[str]: + """Read an ASCII string from memory""" + cstr = self.read_cstring(address) + if isinstance(cstr, str) and cstr and all(x in string.printable for x in cstr): + return cstr + return None + + @property + def maps(self) -> List[Section]: + if not self.__maps: + self.__maps = self.__parse_maps() + return self.__maps + + def __parse_maps(self) -> List[Section]: + """Return the mapped memory sections""" + try: + if is_qemu_system(): + return list(self.__parse_info_mem()) + except gdb.error: + # Target may not support this command + pass + try: + return list(self.__parse_procfs_maps()) + except FileNotFoundError: + return list(self.__parse_gdb_info_sections()) + + def __parse_procfs_maps(self) -> Generator[Section, None, None]: + """Get the memory mapping from procfs.""" + procfs_mapfile = gef.session.maps + if not procfs_mapfile: + is_remote = gef.session.remote is not None + raise FileNotFoundError(f"Missing {'remote ' if is_remote else ''}procfs map file") + with procfs_mapfile.open("r") as fd: + for line in fd: + line = line.strip() + addr, perm, off, _, rest = line.split(" ", 4) + rest = rest.split(" ", 1) + if len(rest) == 1: + inode = rest[0] + pathname = "" + else: + inode = rest[0] + pathname = rest[1].lstrip() + + addr_start, addr_end = parse_string_range(addr) + off = int(off, 16) + perm = Permission.from_process_maps(perm) + inode = int(inode) + yield Section(page_start=addr_start, + page_end=addr_end, + offset=off, + permission=perm, + inode=inode, + path=pathname) + return + + def __parse_gdb_info_sections(self) -> Generator[Section, None, None]: + """Get the memory mapping from GDB's command `maintenance info sections` (limited info).""" + stream = StringIO(gdb.execute("maintenance info sections", to_string=True)) + + for line in stream: + if not line: + break + + try: + parts = [x for x in line.split()] + addr_start, addr_end = [int(x, 16) for x in parts[1].split("->")] + off = int(parts[3][:-1], 16) + path = parts[4] + perm = Permission.from_info_sections(parts[5:]) + yield Section( + page_start=addr_start, + page_end=addr_end, + offset=off, + permission=perm, + inode="", + path=path + ) + + except IndexError: + continue + except ValueError: + continue + return + + def __parse_info_mem(self) -> Generator[Section, None, None]: + """Get the memory mapping from GDB's command `monitor info mem`""" + for line in StringIO(gdb.execute("monitor info mem", to_string=True)): + if not line: + break + try: + ranges, off, perms = line.split() + off = int(off, 16) + start, end = [int(s, 16) for s in ranges.split("-")] + except ValueError as e: + continue + + perm = Permission.from_info_mem(perms) + yield Section( + page_start=start, + page_end=end, + offset=off, + permission=perm, + inode="", + ) + + +class GefHeapManager(GefManager): + """Class managing session heap.""" + def __init__(self) -> None: + self.reset_caches() + return + + def reset_caches(self) -> None: + self.__libc_main_arena: Optional[GlibcArena] = None + self.__libc_selected_arena: Optional[GlibcArena] = None + self.__heap_base = None + return + + @property + def main_arena(self) -> Optional[GlibcArena]: + if not self.__libc_main_arena: + try: + __main_arena_addr = GefHeapManager.find_main_arena_addr() + self.__libc_main_arena = GlibcArena(f"*{__main_arena_addr:#x}") + # the initialization of `main_arena` also defined `selected_arena`, so + # by default, `main_arena` == `selected_arena` + self.selected_arena = self.__libc_main_arena + except: + # the search for arena can fail when the session is not started + pass + return self.__libc_main_arena + + @main_arena.setter + def main_arena(self, value: GlibcArena) -> None: + self.__libc_main_arena = value + return + + @staticmethod + @lru_cache() + def find_main_arena_addr() -> int: + """A helper function to find the glibc `main_arena` address, either from + symbol, from its offset from `__malloc_hook` or by brute force.""" + # Before anything else, use libc offset from config if available + if gef.config["gef.main_arena_offset"]: + try: + libc_base = get_section_base_address("libc") + offset = parse_address(gef.config["gef.main_arena_offset"]) + if libc_base: + dbg(f"Using main_arena_offset={offset:#x} from config") + addr = libc_base + offset + + # Verify the found address before returning + if GlibcArena.verify(addr): + return addr + except gdb.error: + pass + + # First, try to find `main_arena` symbol directly + try: + return parse_address(f"&{LIBC_HEAP_MAIN_ARENA_DEFAULT_NAME}") + except gdb.error: + pass + + # Second, try to find it by offset from `__malloc_hook` + if gef.libc.version < (2, 34): + try: + malloc_hook_addr = parse_address("(void *)&__malloc_hook") + + struct_size = ctypes.sizeof(GlibcArena.malloc_state_t()) + + if is_x86(): + addr = align_address_to_size(malloc_hook_addr + gef.arch.ptrsize, 0x20) + elif is_arch(Elf.Abi.AARCH64): + addr = malloc_hook_addr - gef.arch.ptrsize*2 - struct_size + elif is_arch(Elf.Abi.ARM): + addr = malloc_hook_addr - gef.arch.ptrsize - struct_size + else: + addr = None + + # Verify the found address before returning + if addr and GlibcArena.verify(addr): + return addr + + except gdb.error: + pass + + # Last resort, try to find it via brute force if enabled in settings + if gef.config["gef.bruteforce_main_arena"]: + alignment = 0x8 + try: + dbg("Trying to bruteforce main_arena address") + # setup search_range for `main_arena` to `.data` of glibc + search_filter = lambda f: "libc" in f.filename and f.name == ".data" + dotdata = list(filter(search_filter, get_info_files()))[0] + search_range = range(dotdata.zone_start, dotdata.zone_end, alignment) + # find first possible candidate + for addr in search_range: + if GlibcArena.verify(addr): + dbg(f"Found candidate at {addr:#x}") + return addr + dbg("Bruteforce not successful") + except Exception: + pass + + # Nothing helped + err_msg = f"Cannot find main_arena for {gef.arch.arch}. You might want to set a manually found libc offset " + if not gef.config["gef.bruteforce_main_arena"]: + err_msg += "or allow bruteforcing " + err_msg += "through the GEF config." + raise OSError(err_msg) + + @property + def selected_arena(self) -> Optional[GlibcArena]: + if not self.__libc_selected_arena: + # `selected_arena` must default to `main_arena` + self.__libc_selected_arena = self.main_arena + return self.__libc_selected_arena + + @selected_arena.setter + def selected_arena(self, value: GlibcArena) -> None: + self.__libc_selected_arena = value + return + + @property + def arenas(self) -> Union[List, Iterator[GlibcArena]]: + if not self.main_arena: + return [] + return iter(self.main_arena) + + @property + def base_address(self) -> Optional[int]: + if not self.__heap_base: + base = 0 + try: + base = parse_address("mp_->sbrk_base") + base = self.malloc_align_address(base) + except gdb.error: + # missing symbol, try again + base = 0 + if not base: + base = get_section_base_address("[heap]") + self.__heap_base = base + return self.__heap_base + + @property + def chunks(self) -> Union[List, Iterator]: + if not self.base_address: + return [] + return iter(GlibcChunk(self.base_address, from_base=True)) + + @property + def min_chunk_size(self) -> int: + return 4 * gef.arch.ptrsize + + @property + def malloc_alignment(self) -> int: + __default_malloc_alignment = 0x10 + if gef.libc.version >= (2, 26) and is_x86_32(): + # Special case introduced in Glibc 2.26: + # https://elixir.bootlin.com/glibc/glibc-2.26/source/sysdeps/i386/malloc-alignment.h#L22 + return __default_malloc_alignment + # Generic case: + # https://elixir.bootlin.com/glibc/glibc-2.26/source/sysdeps/generic/malloc-alignment.h#L22 + return 2 * gef.arch.ptrsize + + def csize2tidx(self, size: int) -> int: + return abs((size - self.min_chunk_size + self.malloc_alignment - 1)) // self.malloc_alignment + + def tidx2size(self, idx: int) -> int: + return idx * self.malloc_alignment + self.min_chunk_size + + def malloc_align_address(self, address: int) -> int: + """Align addresses according to glibc's MALLOC_ALIGNMENT. See also Issue #689 on Github""" + malloc_alignment = self.malloc_alignment + ceil = lambda n: int(-1 * n // 1 * -1) + return malloc_alignment * ceil((address / malloc_alignment)) + + +class GefSetting: + """Basic class for storing gef settings as objects""" + READ_ACCESS = 0 + WRITE_ACCESS = 1 + + def __init__(self, value: Any, cls: Optional[type] = None, description: Optional[str] = None, hooks: Optional[Dict[str, Callable]] = None) -> None: + self.value = value + self.type = cls or type(value) + self.description = description or "" + self.hooks: Tuple[List[Callable], List[Callable]] = ([], []) + if hooks: + for access, func in hooks.items(): + if access == "on_read": + idx = GefSetting.READ_ACCESS + elif access == "on_write": + idx = GefSetting.WRITE_ACCESS + else: + raise ValueError + if not callable(func): + raise ValueError(f"hook is not callable") + self.hooks[idx].append(func) + return + + def __str__(self) -> str: + return f"Setting(type={self.type.__name__}, value='{self.value}', desc='{self.description[:10]}...', "\ + f"read_hooks={len(self.hooks[GefSetting.READ_ACCESS])}, write_hooks={len(self.hooks[GefSetting.READ_ACCESS])})" + + +class GefSettingsManager(dict): + """ + GefSettings acts as a dict where the global settings are stored and can be read, written or deleted as any other dict. + For instance, to read a specific command setting: `gef.config[mycommand.mysetting]` + """ + def __getitem__(self, name: str) -> Any: + setting : GefSetting = super().__getitem__(name) + self.__invoke_read_hooks(setting) + return setting.value + + def __setitem__(self, name: str, value: Any) -> None: + # check if the key exists + if super().__contains__(name): + # if so, update its value directly + setting = super().__getitem__(name) + if not isinstance(setting, GefSetting): raise ValueError + setting.value = setting.type(value) + else: + # if not, `value` must be a GefSetting + if not isinstance(value, GefSetting): raise Exception("Invalid argument") + if not value.type: raise Exception("Invalid type") + if not value.description: raise Exception("Invalid description") + setting = value + super().__setitem__(name, setting) + self.__invoke_write_hooks(setting) + return + + def __delitem__(self, name: str) -> None: + super().__delitem__(name) + return + + def raw_entry(self, name: str) -> GefSetting: + return super().__getitem__(name) + + def __invoke_read_hooks(self, setting: GefSetting) -> None: + self.__invoke_hooks(is_write=False, setting=setting) + return + + def __invoke_write_hooks(self, setting: GefSetting) -> None: + self.__invoke_hooks(is_write=True, setting=setting) + return + + def __invoke_hooks(self, is_write: bool, setting: GefSetting) -> None: + if not setting.hooks: + return + idx = int(is_write) + if setting.hooks[idx]: + for callback in setting.hooks[idx]: + callback() + return + + +class GefSessionManager(GefManager): + """Class managing the runtime properties of GEF. """ + def __init__(self) -> None: + self.reset_caches() + self.remote: Optional["GefRemoteSessionManager"] = None + self.remote_initializing: bool = False + self.qemu_mode: bool = False + self.convenience_vars_index: int = 0 + self.heap_allocated_chunks: List[Tuple[int, int]] = [] + self.heap_freed_chunks: List[Tuple[int, int]] = [] + self.heap_uaf_watchpoints: List[UafWatchpoint] = [] + self.pie_breakpoints: Dict[int, PieVirtualBreakpoint] = {} + self.pie_counter: int = 1 + self.aliases: List[GefAlias] = [] + self.modules: List[FileFormat] = [] + self.constants = {} # a dict for runtime constants (like 3rd party file paths) + for constant in ("python3", "readelf", "file", "ps"): + self.constants[constant] = which(constant) + return + + def reset_caches(self) -> None: + super().reset_caches() + self._auxiliary_vector = None + self._pagesize = None + self._os = None + self._pid = None + self._file = None + self._maps: Optional[pathlib.Path] = None + self._root: Optional[pathlib.Path] = None + return + + def __str__(self) -> str: + return f"Session({'Local' if self.remote is None else 'Remote'}, pid={self.pid or 'Not running'}, os='{self.os}')" + + @property + def auxiliary_vector(self) -> Optional[Dict[str, int]]: + if not is_alive(): + return None + if is_qemu_system(): + return None + if not self._auxiliary_vector: + auxiliary_vector = {} + auxv_info = gdb.execute("info auxv", to_string=True) + if not auxv_info or "failed" in auxv_info: + err("Failed to query auxiliary variables") + return None + for line in auxv_info.splitlines(): + line = line.split('"')[0].strip() # remove the ending string (if any) + line = line.split() # split the string by whitespace(s) + if len(line) < 4: + continue + __av_type = line[1] + __av_value = line[-1] + auxiliary_vector[__av_type] = int(__av_value, base=0) + self._auxiliary_vector = auxiliary_vector + return self._auxiliary_vector + + @property + def os(self) -> str: + """Return the current OS.""" + if not self._os: + self._os = platform.system().lower() + return self._os + + @property + def pid(self) -> int: + """Return the PID of the target process.""" + if not self._pid: + pid = gdb.selected_inferior().pid if not gef.session.qemu_mode else gdb.selected_thread().ptid[1] + if not pid: + raise RuntimeError("cannot retrieve PID for target process") + self._pid = pid + return self._pid + + @property + def file(self) -> Optional[pathlib.Path]: + """Return a Path object of the target process.""" + if gef.session.remote is not None: + return gef.session.remote.file + fpath: str = gdb.current_progspace().filename + if fpath and not self._file: + self._file = pathlib.Path(fpath).expanduser() + return self._file + + @property + def cwd(self) -> Optional[pathlib.Path]: + if gef.session.remote is not None: + return gef.session.remote.root + return self.file.parent if self.file else None + + @property + def pagesize(self) -> int: + """Get the system page size""" + auxval = self.auxiliary_vector + if not auxval: + return DEFAULT_PAGE_SIZE + self._pagesize = auxval["AT_PAGESZ"] + return self._pagesize + + @property + def canary(self) -> Optional[Tuple[int, int]]: + """Return a tuple of the canary address and value, read from the canonical + location if supported by the architecture. Otherwise, read from the auxiliary + vector.""" + try: + canary_location = gef.arch.canary_address() + canary = gef.memory.read_integer(canary_location) + except NotImplementedError: + # Fall back to `AT_RANDOM`, which is the original source + # of the canary value but not the canonical location + return self.original_canary + return canary, canary_location + + @property + def original_canary(self) -> Optional[Tuple[int, int]]: + """Return a tuple of the initial canary address and value, read from the + auxiliary vector.""" + auxval = self.auxiliary_vector + if not auxval: + return None + canary_location = auxval["AT_RANDOM"] + canary = gef.memory.read_integer(canary_location) + canary &= ~0xFF + return canary, canary_location + + + @property + def maps(self) -> Optional[pathlib.Path]: + """Returns the Path to the procfs entry for the memory mapping.""" + if not is_alive(): + return None + if not self._maps: + if gef.session.remote is not None: + self._maps = gef.session.remote.maps + else: + self._maps = pathlib.Path(f"/proc/{self.pid}/maps") + return self._maps + + @property + def root(self) -> Optional[pathlib.Path]: + """Returns the path to the process's root directory.""" + if not is_alive(): + return None + if not self._root: + self._root = pathlib.Path(f"/proc/{self.pid}/root") + return self._root + +class GefRemoteSessionManager(GefSessionManager): + """Class for managing remote sessions with GEF. It will create a temporary environment + designed to clone the remote one.""" + def __init__(self, host: str, port: int, pid: int =-1, qemu: Optional[pathlib.Path] = None) -> None: + super().__init__() + self.__host = host + self.__port = port + self.__local_root_fd = tempfile.TemporaryDirectory() + self.__local_root_path = pathlib.Path(self.__local_root_fd.name) + self.__qemu = qemu + dbg(f"[remote] initializing remote session with {self.target} under {self.root}") + if not self.connect(pid): + raise EnvironmentError(f"Cannot connect to remote target {self.target}") + if not self.setup(): + raise EnvironmentError(f"Failed to create a proper environment for {self.target}") + return + + def close(self) -> None: + self.__local_root_fd.cleanup() + try: + gef_on_new_unhook(self.remote_objfile_event_handler) + gef_on_new_hook(new_objfile_handler) + except Exception as e: + warn(f"Exception while restoring local context: {str(e)}") + return + + def in_qemu_user(self) -> bool: + return self.__qemu is not None + + def __str__(self) -> str: + return f"RemoteSession(target='{self.target}', local='{self.root}', pid={self.pid}, qemu_user={bool(self.in_qemu_user())})" + + @property + def target(self) -> str: + return f"{self.__host}:{self.__port}" + + @property + def root(self) -> pathlib.Path: + return self.__local_root_path.absolute() + + @property + def file(self) -> pathlib.Path: + """Path to the file being debugged as seen by the remote endpoint.""" + if not self._file: + filename = gdb.current_progspace().filename + if not filename: + raise RuntimeError("No session started") + start_idx = len("target:") if filename.startswith("target:") else 0 + self._file = pathlib.Path(filename[start_idx:]) + return self._file + + @property + def lfile(self) -> pathlib.Path: + """Local path to the file being debugged.""" + return self.root / str(self.file).lstrip("/") + + @property + def maps(self) -> pathlib.Path: + if not self._maps: + self._maps = self.root / f"proc/{self.pid}/maps" + return self._maps + + def sync(self, src: str, dst: Optional[str] = None) -> bool: + """Copy the `src` into the temporary chroot. If `dst` is provided, that path will be + used instead of `src`.""" + if not dst: + dst = src + tgt = self.root / dst.lstrip("/") + if tgt.exists(): + return True + tgt.parent.mkdir(parents=True, exist_ok=True) + dbg(f"[remote] downloading '{src}' -> '{tgt}'") + gdb.execute(f"remote get {src} {tgt.absolute()}") + return tgt.exists() + + def connect(self, pid: int) -> bool: + """Connect to remote target. If in extended mode, also attach to the given PID.""" + # before anything, register our new hook to download files from the remote target + dbg(f"[remote] Installing new objfile handlers") + gef_on_new_unhook(new_objfile_handler) + gef_on_new_hook(self.remote_objfile_event_handler) + + # then attempt to connect + is_extended_mode = (pid > -1) + dbg(f"[remote] Enabling extended remote: {bool(is_extended_mode)}") + try: + with DisableContextOutputContext(): + cmd = f"target {'extended-' if is_extended_mode else ''}remote {self.target}" + dbg(f"[remote] Executing '{cmd}'") + gdb.execute(cmd) + if is_extended_mode: + gdb.execute(f"attach {pid:d}") + return True + except Exception as e: + err(f"Failed to connect to {self.target}: {e}") + + # a failure will trigger the cleanup, deleting our hook anyway + return False + + def setup(self) -> bool: + # setup remote adequately depending on remote or qemu mode + if self.in_qemu_user(): + dbg(f"Setting up as qemu session, target={self.__qemu}") + self.__setup_qemu() + else: + dbg(f"Setting up as remote session") + self.__setup_remote() + + # refresh gef to consider the binary + reset_all_caches() + gef.binary = Elf(self.lfile) + reset_architecture() + return True + + def __setup_qemu(self) -> bool: + # setup emulated file in the chroot + assert self.__qemu + target = self.root / str(self.__qemu.parent).lstrip("/") + target.mkdir(parents=True, exist_ok=False) + shutil.copy2(self.__qemu, target) + self._file = self.__qemu + assert self.lfile.exists() + + # create a procfs + procfs = self.root / f"proc/{self.pid}/" + procfs.mkdir(parents=True, exist_ok=True) + + ## /proc/pid/cmdline + cmdline = procfs / "cmdline" + if not cmdline.exists(): + with cmdline.open("w") as fd: + fd.write("") + + ## /proc/pid/environ + environ = procfs / "environ" + if not environ.exists(): + with environ.open("wb") as fd: + fd.write(b"PATH=/bin\x00HOME=/tmp\x00") + + ## /proc/pid/maps + maps = procfs / "maps" + if not maps.exists(): + with maps.open("w") as fd: + fname = self.file.absolute() + mem_range = "00000000-ffffffff" if is_32bit() else "0000000000000000-ffffffffffffffff" + fd.write(f"{mem_range} rwxp 00000000 00:00 0 {fname}\n") + return True + + def __setup_remote(self) -> bool: + # get the file + fpath = f"/proc/{self.pid}/exe" + if not self.sync(fpath, str(self.file)): + err(f"'{fpath}' could not be fetched on the remote system.") + return False + + # pseudo procfs + for _file in ("maps", "environ", "cmdline"): + fpath = f"/proc/{self.pid}/{_file}" + if not self.sync(fpath): + err(f"'{fpath}' could not be fetched on the remote system.") + return False + + # makeup a fake mem mapping in case we failed to retrieve it + maps = self.root / f"proc/{self.pid}/maps" + if not maps.exists(): + with maps.open("w") as fd: + fname = self.file.absolute() + mem_range = "00000000-ffffffff" if is_32bit() else "0000000000000000-ffffffffffffffff" + fd.write(f"{mem_range} rwxp 00000000 00:00 0 {fname}\n") + return True + + def remote_objfile_event_handler(self, evt: "gdb.NewObjFileEvent") -> None: + dbg(f"[remote] in remote_objfile_handler({evt.new_objfile.filename if evt else 'None'}))") + if not evt or not evt.new_objfile.filename: + return + if not evt.new_objfile.filename.startswith("target:") and not evt.new_objfile.filename.startswith("/"): + warn(f"[remote] skipping '{evt.new_objfile.filename}'") + return + if evt.new_objfile.filename.startswith("target:"): + src: str = evt.new_objfile.filename[len("target:"):] + if not self.sync(src): + raise FileNotFoundError(f"Failed to sync '{src}'") + return + + +class GefUiManager(GefManager): + """Class managing UI settings.""" + def __init__(self) -> None: + self.redirect_fd : Optional[TextIOWrapper] = None + self.context_hidden = False + self.stream_buffer : Optional[StringIO] = None + self.highlight_table: Dict[str, str] = {} + self.watches: Dict[int, Tuple[int, str]] = {} + self.context_messages: List[Tuple[str, str]] = [] + return + + +class GefLibcManager(GefManager): + """Class managing everything libc-related (except heap).""" + def __init__(self) -> None: + self._version : Optional[Tuple[int, int]] = None + return + + def __str__(self) -> str: + return f"Libc(version='{self.version}')" + + @property + def version(self) -> Optional[Tuple[int, int]]: + if not is_alive(): + return None + if not self._version: + self._version = GefLibcManager.find_libc_version() + return self._version + + @staticmethod + @lru_cache() + def find_libc_version() -> Tuple[int, ...]: + sections = gef.memory.maps + for section in sections: + match = re.search(r"libc6?[-_](\d+)\.(\d+)\.so", section.path) + if match: + return tuple(int(_) for _ in match.groups()) + if "libc" in section.path: + try: + with open(section.path, "rb") as f: + data = f.read() + except OSError: + continue + match = re.search(PATTERN_LIBC_VERSION, data) + if match: + return tuple(int(_) for _ in match.groups()) + return 0, 0 + + + + +class Gef: + """The GEF root class, which serves as a entrypoint for all the debugging session attributes (architecture, + memory, settings, etc.).""" + binary: Optional[FileFormat] + arch: Architecture + config : GefSettingsManager + ui: GefUiManager + libc: GefLibcManager + memory : GefMemoryManager + heap : GefHeapManager + session : GefSessionManager + gdb: GefCommand + + def __init__(self) -> None: + self.binary: Optional[FileFormat] = None + self.arch: Architecture = GenericArchitecture() # see PR #516, will be reset by `new_objfile_handler` + self.config = GefSettingsManager() + self.ui = GefUiManager() + self.libc = GefLibcManager() + return + + def __str__(self) -> str: + return f"Gef(binary='{self.binary or 'None'}', arch={self.arch})" + + def reinitialize_managers(self) -> None: + """Reinitialize the managers. Avoid calling this function directly, using `pi reset()` is preferred""" + self.memory = GefMemoryManager() + self.heap = GefHeapManager() + self.session = GefSessionManager() + return + + def setup(self) -> None: + """Setup initialize the runtime setup, which may require for the `gef` to be not None.""" + self.reinitialize_managers() + self.gdb = GefCommand() + self.gdb.setup() + tempdir = self.config["gef.tempdir"] + gef_makedirs(tempdir) + gdb.execute(f"save gdb-index {tempdir}") + return + + def reset_caches(self) -> None: + """Recursively clean the cache of all the managers. Avoid calling this function directly, using `reset-cache` + is preferred""" + for mgr in (self.memory, self.heap, self.session, self.arch): + mgr.reset_caches() + return + + +if __name__ == "__main__": + if sys.version_info[0] == 2: + err("GEF has dropped Python2 support for GDB when it reached EOL on 2020/01/01.") + err("If you require GEF for GDB+Python2, use https://github.com/hugsy/gef-legacy.") + exit(1) + + if GDB_VERSION < GDB_MIN_VERSION or PYTHON_VERSION < PYTHON_MIN_VERSION: + err("You're using an old version of GDB. GEF will not work correctly. " + f"Consider updating to GDB {'.'.join(map(str, GDB_MIN_VERSION))} or higher " + f"(with Python {'.'.join(map(str, PYTHON_MIN_VERSION))} or higher).") + exit(1) + + 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) + except FileNotFoundError: + pass + + # When using a Python virtual environment, GDB still loads the system-installed Python + # so GEF doesn't load site-packages dir from environment + # In order to fix it, from the shell with venv activated we run the python binary, + # take and parse its path, add the path to the current python process using sys.path.extend + PYTHONBIN = which("python3") + PREFIX = gef_pystring(subprocess.check_output([PYTHONBIN, '-c', 'import os, sys;print((sys.prefix))'])).strip("\\n") + if PREFIX != sys.base_prefix: + SITE_PACKAGES_DIRS = subprocess.check_output( + [PYTHONBIN, "-c", "import os, sys;print(os.linesep.join(sys.path).strip())"]).decode("utf-8").split() + sys.path.extend(SITE_PACKAGES_DIRS) + + # setup config + gdb_initial_settings = ( + "set confirm off", + "set verbose off", + "set pagination off", + "set print elements 0", + "set history save on", + f"set history filename {os.getenv('GDBHISTFILE', '~/.gdb_history')}", + "set output-radix 0x10", + "set print pretty on", + "set disassembly-flavor intel", + "handle SIGALRM print nopass", + ) + for cmd in gdb_initial_settings: + try: + gdb.execute(cmd) + except gdb.error: + pass + + # load GEF, set up the managers and load the plugins, functions, + reset() + gef.gdb.load() + gef.gdb.show_banner() + + # load config + gef.gdb.load_extra_plugins() + + # setup gdb prompt + gdb.prompt_hook = __gef_prompt__ + + # gdb events configuration + gef_on_continue_hook(continue_handler) + gef_on_stop_hook(hook_stop_handler) + gef_on_new_hook(new_objfile_handler) + gef_on_exit_hook(exit_handler) + gef_on_memchanged_hook(memchanged_handler) + gef_on_regchanged_hook(regchanged_handler) + + if gdb.current_progspace().filename is not None: + # if here, we are sourcing gef from a gdb session already attached, force call to new_objfile (see issue #278) + new_objfile_handler(None) + + GefTmuxSetup() + + # `target remote` commands cannot be disabled, so print a warning message instead + errmsg = "Using `target remote` with GEF does not work, use `gef-remote` instead. You've been warned." + gdb.execute(f"define target hook-remote\n pi if calling_function() != \"connect\": err(\"{errmsg}\") \nend") + gdb.execute(f"define target hook-extended-remote\n pi if calling_function() != \"connect\": err(\"{errmsg}\") \nend") diff --git a/vim/.vimrc b/vim/.vimrc index c2486e6..a8bc88a 100644 --- a/vim/.vimrc +++ b/vim/.vimrc @@ -180,3 +180,8 @@ endif " Remove conceal from Markdown when Insert mode is set autocmd InsertEnter *.{markdown,md} set conceallevel=0 autocmd InsertLeave *.{markdown,md} set conceallevel=2 + +" Remove conceal from JSON when Insert mode is on +autocmd InsertEnter *.{json} set conceallevel=0 +autocmd InsertLeave *.{json} set conceallevel=2 +