diff --git a/gef.py b/gef.py index f8dadf9c1..0653da00c 100644 --- a/gef.py +++ b/gef.py @@ -82,7 +82,7 @@ from io import StringIO, TextIOWrapper from types import ModuleType from typing import (Any, ByteString, Callable, Dict, Generator, Iterable, - Iterator, List, NoReturn, Optional, Sequence, Tuple, Type, + Iterator, List, NoReturn, Optional, Sequence, Set, Tuple, Type, Union) from urllib.request import urlopen @@ -158,9 +158,10 @@ def update_gef(argv: List[str]) -> int: GEF_EXTRAS_DEFAULT_BRANCH = "main" gef : "Gef" -__registered_commands__ : List[Type["GenericCommand"]] = [] -__registered_functions__ : List[Type["GenericFunction"]] = [] -__registered_architectures__ : Dict[Union["Elf.Abi", str], Type["Architecture"]] = {} +__registered_commands__ : List[Type["GenericCommand"]] = [] +__registered_functions__ : List[Type["GenericFunction"]] = [] +__registered_architectures__ : Dict[Union["Elf.Abi", str], Type["Architecture"]] = {} +__registered_file_formats__ : Set[ Type["FileFormat"] ] = set() def reset_all_caches() -> None: @@ -289,44 +290,52 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: # Helpers # -def p8(x: int, s: bool = False) -> bytes: +def p8(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: """Pack one byte respecting the current architecture endianness.""" - return struct.pack(f"{gef.arch.endianness}B", x) if not s else struct.pack(f"{gef.arch.endianness}b", x) + 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) -> bytes: +def p16(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: """Pack one word respecting the current architecture endianness.""" - return struct.pack(f"{gef.arch.endianness}H", x) if not s else struct.pack(f"{gef.arch.endianness}h", x) + 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) -> bytes: +def p32(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: """Pack one dword respecting the current architecture endianness.""" - return struct.pack(f"{gef.arch.endianness}I", x) if not s else struct.pack(f"{gef.arch.endianness}i", x) + 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) -> bytes: +def p64(x: int, s: bool = False, e: Optional["Endianness"] = None) -> bytes: """Pack one qword respecting the current architecture endianness.""" - return struct.pack(f"{gef.arch.endianness}Q", x) if not s else struct.pack(f"{gef.arch.endianness}q", x) + 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) -> int: +def u8(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: """Unpack one byte respecting the current architecture endianness.""" - return struct.unpack(f"{gef.arch.endianness}B", x)[0] if not s else struct.unpack(f"{gef.arch.endianness}b", x)[0] + 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) -> int: +def u16(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: """Unpack one word respecting the current architecture endianness.""" - return struct.unpack(f"{gef.arch.endianness}H", x)[0] if not s else struct.unpack(f"{gef.arch.endianness}h", x)[0] + 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) -> int: +def u32(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: """Unpack one dword respecting the current architecture endianness.""" - return struct.unpack(f"{gef.arch.endianness}I", x)[0] if not s else struct.unpack(f"{gef.arch.endianness}i", x)[0] + 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) -> int: +def u64(x: bytes, s: bool = False, e: Optional["Endianness"] = None) -> int: """Unpack one qword respecting the current architecture endianness.""" - return struct.unpack(f"{gef.arch.endianness}Q", x)[0] if not s else struct.unpack(f"{gef.arch.endianness}q", x)[0] + 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: @@ -679,7 +688,39 @@ def __int__(self) -> int: return self.value -class Elf: +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 @@ -747,18 +788,25 @@ class OsAbi(enum.Enum): path: pathlib.Path phdrs : List["Phdr"] shdrs : List["Shdr"] + name: str = "ELF" - def __init__(self, path: Union[str, pathlib.Path] = "", minimalist: bool = False) -> None: - """Instantiate an ELF object. The default behavior is to create the object by parsing the ELF file. - But in some cases (QEMU-stub), we may just want a simple minimal object with default values.""" - if minimalist: - return + __checksec : Dict[str, bool] - self.path = pathlib.Path(path).expanduser() if isinstance(path, str) else path + 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 os.access(self.path, os.R_OK): + 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") @@ -823,6 +871,45 @@ def __repr__(self) -> str: 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 @@ -2009,40 +2096,9 @@ def gef_execute_gdb_script(commands: str) -> None: return -@lru_cache(32) +@deprecated("Use Elf(fname).checksec()") def checksec(filename: str) -> 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.""" - readelf = gef.session.constants["readelf"] - - 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 - - results = collections.OrderedDict() - results["Canary"] = __check_security_property("-rs", filename, r"__stack_chk_fail") is True - has_gnu_stack = __check_security_property("-W -l", filename, r"GNU_STACK") is True - if has_gnu_stack: - results["NX"] = __check_security_property("-W -l", filename, r"GNU_STACK.*RWE") is False - else: - results["NX"] = False - results["PIE"] = __check_security_property("-h", filename, r":.*EXEC") is False - results["Fortify"] = __check_security_property("-s", filename, r"_chk@GLIBC") is True - results["Partial RelRO"] = __check_security_property("-l", filename, r"GNU_RELRO") is True - results["Full RelRO"] = results["Partial RelRO"] and __check_security_property("-d", filename, r"BIND_NOW") is True - return results + return Elf(filename).checksec @lru_cache() @@ -2080,7 +2136,7 @@ def get_entry_point() -> Optional[int]: def is_pie(fpath: str) -> bool: - return checksec(fpath)["PIE"] + return Elf(fpath).checksec["PIE"] @deprecated("Prefer `gef.arch.endianness == Endianness.BIG_ENDIAN`") @@ -3392,29 +3448,21 @@ def hook_stop_handler(_: "gdb.StopEvent") -> None: def new_objfile_handler(evt: "gdb.Event") -> None: """GDB event handler for new object file cases.""" reset_all_caches() - if evt: - filename: str = evt.new_objfile.filename - if filename.startswith("target:"): - filename = filename[7:] - warn(f"Omitting remote file '{filename}'") - return - - try: - elf = Elf(filename) - if not gef.binary: - gef.binary = elf - reset_architecture() - load_libc_args() - else: - gef.session.modules.append(elf) - except FileNotFoundError: - pass - else: - elf = Elf(gdb.current_progspace().filename) + try: + target = pathlib.Path( evt.new_objfile.filename if evt else gdb.current_progspace().filename) + 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 = elf + gef.binary = binary reset_architecture() load_libc_args() + else: + gef.session.modules.append(binary) + except FileNotFoundError as fne: + 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 @@ -10400,7 +10448,7 @@ def __init__(self) -> None: self.pie_breakpoints: Dict[int, PieVirtualBreakpoint] = {} self.pie_counter: int = 1 self.aliases: List[GefAlias] = [] - self.modules: List[Elf] = [] + 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) @@ -10728,8 +10776,18 @@ def version(self) -> Optional[Tuple[int, int]]: 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[Elf] = 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() @@ -10797,9 +10855,6 @@ def reset_caches(self) -> None: [PYTHONBIN, "-c", "import os, sys;print(os.linesep.join(sys.path).strip())"]).decode("utf-8").split() sys.path.extend(SITE_PACKAGES_DIRS) - # setup prompt - gdb.prompt_hook = __gef_prompt__ - # setup config gdb_initial_settings = ( "set confirm off", @@ -10827,6 +10882,9 @@ def reset_caches(self) -> None: # 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)