diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..6cf1b6a --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,88 @@ +name: F0 style checking +run-name: Running style checks on ${{ github.ref_name }} following push by ${{ github.actor }} + +on: push +jobs: + Check-isort: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: python-isort + uses: isort/isort-action@v1.1.1 + with: + configuration: "--check-only --profile black --diff --verbose" + # Check-mypy: + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: python-mypy + # uses: jpetrucciani/mypy-check@master + # with: + # path: '.' + # mypy_flags: '--config-file .mypy.ini' + Check-black: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: python-black + uses: psf/black@stable + with: + options: "--check --line-length=120" + src: "." + Check-flake8: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python environment + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: flake8 Lint + uses: py-actions/flake8@v2.3.0 + with: + max-line-length: "120" + path: "." + ignore: "E203,W503" + # Check-pydocstyle: + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: pydocstyle + # uses: foundryzero/pydocstyle-action@v1.2.6 + # with: + # path: "." + + # Check-pylint: + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: pylint + # uses: foundryzero/pylint-action@v1.0.6 + # with: + # match: "binder_trace/**/*.py" + # requirements_file: "binder_trace/requirements.txt" + + Check-tox: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/.gitignore b/.gitignore index ba0430d..cafd598 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__/ \ No newline at end of file +__pycache__/ +.venv/ \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..7e4c92a --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +line_length = 120 +profile = black diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..062d415 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,8 @@ +# Global options: + +[mypy] +follow_imports = silent +ignore_missing_imports = True +show_column_numbers = True +pretty = True +strict = True \ No newline at end of file diff --git a/.pydocstyle b/.pydocstyle new file mode 100644 index 0000000..244c96a --- /dev/null +++ b/.pydocstyle @@ -0,0 +1,2 @@ +[pydocstyle] +match = (?!test_).*\.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..5a56a83 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[FORMAT] +max-line-length=120 diff --git a/README.md b/README.md index f453905..2b43dd3 100644 --- a/README.md +++ b/README.md @@ -59,20 +59,21 @@ Settings are stored in a file `.llef` located in your home directory formatted a ##### Available Settings -| Setting | Type | Description | -| ------------------ | ------- | -------------------------------------------------- | -| color_output | Boolean | Enable/disable color terminal output | -| register_coloring | Boolean | Enable/disable register coloring | -| show_legend | Boolean | Enable/disable legend output | -| show_registers | Boolean | Enable/disable registers output | -| show_stack | Boolean | Enable/disable stack output | -| show_code | Boolean | Enable/disable code output | -| show_threads | Boolean | Enable/disable threads output | -| show_trace | Boolean | Enable/disable trace output | -| force_arch | String | Force register display architecture (experimental) | -| rebase_addresses | Boolean | Enable/disable address rebase output | -| rebase_offset | Int | Set the rebase offset (default 0x100000) | -| show_all_registers | Boolean | Enable/disable extended register output | +| Setting | Type | Description | +| ----------------------- | ------- | -------------------------------------------------- | +| color_output | Boolean | Enable/disable color terminal output | +| register_coloring | Boolean | Enable/disable register coloring | +| show_legend | Boolean | Enable/disable legend output | +| show_registers | Boolean | Enable/disable registers output | +| show_stack | Boolean | Enable/disable stack output | +| show_code | Boolean | Enable/disable code output | +| show_threads | Boolean | Enable/disable threads output | +| show_trace | Boolean | Enable/disable trace output | +| force_arch | String | Force register display architecture (experimental) | +| rebase_addresses | Boolean | Enable/disable address rebase output | +| rebase_offset | Int | Set the rebase offset (default 0x100000) | +| show_all_registers | Boolean | Enable/disable extended register output | +| enable_darwin_heap_scan | Boolean | Enable/disable more accurate heap scanning for Darwin-based platforms. Uses the Darwin malloc introspection API, executing code in the address space of the target application using LLDB's evaluation engine. | #### llefcolorsettings Allows setting LLEF GUI colors: diff --git a/arch/__init__.py b/arch/__init__.py index 5837e9d..6ae3829 100644 --- a/arch/__init__.py +++ b/arch/__init__.py @@ -1,4 +1,5 @@ """Arch module __init__.py""" + from typing import Type from lldb import SBTarget @@ -7,10 +8,11 @@ from arch.arm import Arm from arch.base_arch import BaseArch from arch.i386 import I386 -from arch.x86_64 import X86_64 from arch.ppc import PPC +from arch.x86_64 import X86_64 from common.constants import MSG_TYPE -from common.util import extract_arch_from_triple, print_message +from common.output_util import print_message +from common.util import extract_arch_from_triple # macOS devices running arm chips identify as arm64. # aarch64 and arm64 backends have been merged, so alias arm64 to aarch64. @@ -23,7 +25,7 @@ "aarch64": Aarch64, "arm64": Aarch64, "arm64e": Aarch64, - "powerpc": PPC + "powerpc": PPC, } diff --git a/arch/aarch64.py b/arch/aarch64.py index 0459738..2e5d44d 100644 --- a/arch/aarch64.py +++ b/arch/aarch64.py @@ -68,6 +68,4 @@ class Aarch64(BaseArch): "m": 0xF, } - flag_registers = [ - FlagRegister("cpsr", _cpsr_register_bit_masks) - ] + flag_registers = [FlagRegister("cpsr", _cpsr_register_bit_masks)] diff --git a/arch/arm.py b/arch/arm.py index 2bd2379..cbc868f 100644 --- a/arch/arm.py +++ b/arch/arm.py @@ -47,6 +47,4 @@ class Arm(BaseArch): "t": 0x20, } - flag_registers = [ - FlagRegister("cpsr", _cpsr_register_bit_masks) - ] + flag_registers = [FlagRegister("cpsr", _cpsr_register_bit_masks)] diff --git a/arch/base_arch.py b/arch/base_arch.py index e7a195b..8a6bd19 100644 --- a/arch/base_arch.py +++ b/arch/base_arch.py @@ -8,6 +8,7 @@ @dataclass class FlagRegister: """FlagRegister dataclass to store register name / bitmask associations""" + name: str bit_masks: Dict[str, int] diff --git a/arch/i386.py b/arch/i386.py index 90cc976..4ad1dee 100644 --- a/arch/i386.py +++ b/arch/i386.py @@ -1,4 +1,5 @@ """i386 architecture definition.""" + from arch.base_arch import BaseArch, FlagRegister @@ -47,5 +48,5 @@ class I386(BaseArch): flag_registers = [ FlagRegister("eflags", _eflags_register_bit_masks), - FlagRegister("rflags", _eflags_register_bit_masks) + FlagRegister("rflags", _eflags_register_bit_masks), ] diff --git a/arch/ppc.py b/arch/ppc.py index c9933be..c0fd218 100644 --- a/arch/ppc.py +++ b/arch/ppc.py @@ -1,4 +1,5 @@ """PowerPC architecture definition.""" + from arch.base_arch import BaseArch, FlagRegister @@ -42,10 +43,10 @@ class PPC(BaseArch): "cr0_lt": 0x80000000, "cr0_gt": 0x40000000, "cr0_eq": 0x20000000, - "cr0_so": 0x10000000 + "cr0_so": 0x10000000, } flag_registers = [ FlagRegister("cr", _cr_register_bit_masks), - FlagRegister("xer", _xer_register_bit_masks) + FlagRegister("xer", _xer_register_bit_masks), ] diff --git a/arch/x86_64.py b/arch/x86_64.py index c34914f..97680da 100644 --- a/arch/x86_64.py +++ b/arch/x86_64.py @@ -1,4 +1,5 @@ """x86_64 architecture definition.""" + from arch.base_arch import BaseArch, FlagRegister @@ -51,5 +52,5 @@ class X86_64(BaseArch): # rflags and eflags bit masks are identical for the lower 32-bits flag_registers = [ FlagRegister("rflags", _eflag_register_bit_masks), - FlagRegister("eflags", _eflag_register_bit_masks) + FlagRegister("eflags", _eflag_register_bit_masks), ] diff --git a/commands/base_command.py b/commands/base_command.py index 870c216..f9b6d72 100644 --- a/commands/base_command.py +++ b/commands/base_command.py @@ -11,6 +11,8 @@ class BaseCommand(ABC): """An abstract base class for all commands.""" + alias_set = {} + @abstractmethod def __init__(self) -> None: pass @@ -52,8 +54,11 @@ def lldb_self_register(cls, debugger: SBDebugger, module_name: str) -> None: if cls.container is not None: command = f"command script add -c {module_name}.{cls.__name__} {cls.container.container_verb} {cls.program}" else: - command = ( - f"command script add -c {module_name}.{cls.__name__} {cls.program}" - ) + command = f"command script add -c {module_name}.{cls.__name__} {cls.program}" debugger.HandleCommand(command) + + # If alias_set exists, then load it into LLDB. + for alias, arguments in cls.alias_set.items(): + alias_command = f"command alias {alias} {cls.program} {arguments}" + debugger.HandleCommand(alias_command) diff --git a/commands/base_container.py b/commands/base_container.py index 501919e..4f0fdc4 100644 --- a/commands/base_container.py +++ b/commands/base_container.py @@ -1,4 +1,5 @@ """Base container definition.""" + from abc import ABC, abstractmethod from lldb import SBDebugger @@ -25,5 +26,7 @@ def get_long_help() -> str: @classmethod def lldb_self_register(cls, debugger: SBDebugger, _: str) -> None: """Automatically register a container.""" - container_command = f'command container add -h "{cls.get_long_help()}" -H "{cls.get_short_help()}" {cls.container_verb}' + container_command = ( + f'command container add -h "{cls.get_long_help()}" -H "{cls.get_short_help()}" {cls.container_verb}' + ) debugger.HandleCommand(container_command) diff --git a/commands/base_settings.py b/commands/base_settings.py index cee6a11..aa644fb 100644 --- a/commands/base_settings.py +++ b/commands/base_settings.py @@ -1,13 +1,14 @@ """Base settings command class.""" + import argparse import shlex -from typing import Any, Dict from abc import ABC, abstractmethod +from typing import Any, Dict from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext from commands.base_command import BaseCommand -from common.util import output_line +from common.output_util import output_line class BaseSettingsCommand(BaseCommand, ABC): diff --git a/commands/checksec.py b/commands/checksec.py new file mode 100644 index 0000000..ccedf73 --- /dev/null +++ b/commands/checksec.py @@ -0,0 +1,238 @@ +"""Checksec command class.""" + +import argparse +from typing import Any, Dict + +from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBTarget + +from arch import get_arch +from commands.base_command import BaseCommand +from common.constants import ( + ARCH_BITS, + DYNAMIC_ENTRY_TYPE, + DYNAMIC_ENTRY_VALUE, + EXECUTABLE_TYPE, + MSG_TYPE, + PERMISSION_SET, + PROGRAM_HEADER_TYPE, + SECURITY_CHECK, + SECURITY_FEATURE, + TERM_COLORS, +) +from common.context_handler import ContextHandler +from common.output_util import color_string, output_line, print_message +from common.util import check_elf, check_target, read_program_int + +PROGRAM_HEADER_OFFSET_32BIT_OFFSET = 0x1C +PROGRAM_HEADER_SIZE_32BIT_OFFSET = 0x2A +PROGRAM_HEADER_COUNT_32BIT_OFFSET = 0x2C +PROGRAM_HEADER_PERMISSION_OFFSET_32BIT_OFFSET = 0x18 + +PROGRAM_HEADER_OFFSET_64BIT_OFFSET = 0x20 +PROGRAM_HEADER_SIZE_64BIT_OFFSET = 0x36 +PROGRAM_HEADER_COUNT_64BIT_OFFSET = 0x38 +PROGRAM_HEADER_PERMISSION_OFFSET_64BIT_OFFSET = 0x04 + + +class ChecksecCommand(BaseCommand): + """Implements the checksec command""" + + program: str = "checksec" + container = None + context_handler = None + + def __init__(self, debugger: SBDebugger, __: Dict[Any, Any]) -> None: + super().__init__() + self.parser = self.get_command_parser() + self.context_handler = ContextHandler(debugger) + + @classmethod + def get_command_parser(cls) -> argparse.ArgumentParser: + """Get the command parser.""" + parser = argparse.ArgumentParser() + return parser + + @staticmethod + def get_short_help() -> str: + """Return a short help message""" + return "Usage: checksec" + + @staticmethod + def get_long_help() -> str: + """Return a longer help message""" + return ChecksecCommand.get_command_parser().format_help() + + def get_executable_type(self, target: SBTarget): + """ + Get executable type for a given @target ELF file. + + :param target: The target object file. + :return: An integer representing the executable type. + """ + return read_program_int(target, 0x10, 2) + + def get_program_header_permission(self, target: SBTarget, target_header_type: int): + """ + Get value of the permission field from a program header entry. + + :param target: The target object file. + :param target_header_type: The type of the program header entry. + :return: An integer between 0 and 7 representing the permission. Returns 'None' if program header is not found. + """ + arch = get_arch(target).bits + + if arch == ARCH_BITS.BITS_32: + program_header_offset = read_program_int(target, PROGRAM_HEADER_OFFSET_32BIT_OFFSET, 4) + program_header_entry_size = read_program_int(target, PROGRAM_HEADER_SIZE_32BIT_OFFSET, 2) + program_header_count = read_program_int(target, PROGRAM_HEADER_COUNT_32BIT_OFFSET, 2) + program_header_permission_offset = PROGRAM_HEADER_PERMISSION_OFFSET_32BIT_OFFSET + else: + program_header_offset = read_program_int(target, PROGRAM_HEADER_OFFSET_64BIT_OFFSET, 8) + program_header_entry_size = read_program_int(target, PROGRAM_HEADER_SIZE_64BIT_OFFSET, 2) + program_header_count = read_program_int(target, PROGRAM_HEADER_COUNT_64BIT_OFFSET, 2) + program_header_permission_offset = PROGRAM_HEADER_PERMISSION_OFFSET_64BIT_OFFSET + + permission = None + for i in range(program_header_count): + program_header_type = read_program_int(target, program_header_offset + program_header_entry_size * i, 4) + if program_header_type == target_header_type: + permission = read_program_int( + target, program_header_offset + program_header_entry_size * i + program_header_permission_offset, 4 + ) + break + + return permission + + def get_dynamic_entry(self, target: SBTarget, target_entry_type: int): + """ + Get value for a given entry type in the .dynamic section table. + + :param target: The target object file. + :param target_entry_type: The type of the entry in the .dynamic table. + :return: Value of the entry. Returns 'None' if entry type not found. + """ + target_entry_value = None + # Executable has always been observed at module 0, but isn't specifically stated in docs. + module = target.GetModuleAtIndex(0) + section = module.FindSection(".dynamic") + entry_count = int(section.GetByteSize() / 16) + for i in range(entry_count): + entry_type = section.GetSectionData(i * 16, 8).GetUnsignedInt64(SBError(), 0) + entry_value = section.GetSectionData(i * 16 + 8, 8).GetUnsignedInt64(SBError(), 0) + + if target_entry_type == entry_type: + target_entry_value = entry_value + break + + return target_entry_value + + def check_security(self, target: SBTarget) -> Dict[str, SECURITY_CHECK]: + """ + Checks the following security features on the target executable: + - Stack Canary + - NX Support + - PIE Support + - RPath + - RunPath + - Full/Partial RelRO + + :param target: The target executable. + :return: A dictionary showing whether each security feature is enabled or disabled. + """ + checks = { + SECURITY_FEATURE.STACK_CANARY: SECURITY_CHECK.NO, + SECURITY_FEATURE.NX_SUPPORT: SECURITY_CHECK.UNKNOWN, + SECURITY_FEATURE.PIE_SUPPORT: SECURITY_CHECK.UNKNOWN, + SECURITY_FEATURE.NO_RPATH: SECURITY_CHECK.UNKNOWN, + SECURITY_FEATURE.NO_RUNPATH: SECURITY_CHECK.UNKNOWN, + SECURITY_FEATURE.PARTIAL_RELRO: SECURITY_CHECK.UNKNOWN, + SECURITY_FEATURE.FULL_RELRO: SECURITY_CHECK.UNKNOWN, + } + + # Check for Stack Canary + for symbol in target.GetModuleAtIndex(0): + if symbol.GetName() in ["__stack_chk_fail", "__stack_chk_guard", "__intel_security_cookie"]: + checks[SECURITY_FEATURE.STACK_CANARY] = SECURITY_CHECK.YES + break + + # Check for NX Support + try: + if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_STACK) in PERMISSION_SET.NOT_EXEC: + checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.YES + else: + checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.NO + except MemoryError as error: + print_message(MSG_TYPE.ERROR, error) + checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.UNKNOWN + + # Check for PIE Support + try: + if self.get_executable_type(target) == EXECUTABLE_TYPE.DYN: + checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.YES + else: + checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.NO + except MemoryError as error: + print_message(MSG_TYPE.ERROR, error) + checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.UNKNOWN + + # Check for Partial RelRO + try: + if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_RELRO) is not None: + checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.YES + else: + checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.NO + except MemoryError as error: + print_message(MSG_TYPE.ERROR, error) + checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.UNKNOWN + + # Check for Full RelRO + if checks[SECURITY_FEATURE.PARTIAL_RELRO] == SECURITY_CHECK.UNKNOWN: + checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.UNKNOWN + elif ( + self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.FLAGS) == DYNAMIC_ENTRY_VALUE.BIND_NOW + and checks[SECURITY_FEATURE.PARTIAL_RELRO] == SECURITY_CHECK.YES + ): + checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.YES + else: + checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.NO + + # Check for No RPath + if self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RPATH) is None: + checks[SECURITY_FEATURE.NO_RPATH] = SECURITY_CHECK.YES + else: + checks[SECURITY_FEATURE.NO_RPATH] = SECURITY_CHECK.NO + + # Check for No RunPath + if self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RUNPATH) is None: + checks[SECURITY_FEATURE.NO_RUNPATH] = SECURITY_CHECK.YES + else: + checks[SECURITY_FEATURE.NO_RUNPATH] = SECURITY_CHECK.NO + + return checks + + @check_target + @check_elf + def __call__( + self, + debugger: SBDebugger, + command: str, + exe_ctx: SBExecutionContext, + result: SBCommandReturnObject, + ) -> None: + """Handles the invocation of the checksec command""" + + self.context_handler.refresh(exe_ctx) + + target = exe_ctx.GetTarget() + checks = self.check_security(target) + + for check, status in checks.items(): + if status == SECURITY_CHECK.YES: + color = TERM_COLORS.GREEN.name + elif status == SECURITY_CHECK.NO: + color = TERM_COLORS.RED.name + else: + color = TERM_COLORS.GREY.name + check_value_string = check.value + ": " + line = color_string(status.value, color, lwrap=f"{check_value_string:<20}") + output_line(line) diff --git a/commands/color_settings.py b/commands/color_settings.py index 8ccac6f..6c4d015 100644 --- a/commands/color_settings.py +++ b/commands/color_settings.py @@ -1,11 +1,12 @@ """llefcolorsettings command class.""" + import argparse from typing import Any, Dict from lldb import SBDebugger -from common.color_settings import LLEFColorSettings from commands.base_settings import BaseSettingsCommand +from common.color_settings import LLEFColorSettings class ColorSettingsCommand(BaseSettingsCommand): diff --git a/commands/context.py b/commands/context.py index ac85321..9685145 100644 --- a/commands/context.py +++ b/commands/context.py @@ -1,17 +1,14 @@ """Context command class.""" + import argparse import shlex from typing import Any, Dict from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext -from lldb import ( - SBDebugger, - SBExecutionContext, -) from commands.base_command import BaseCommand from common.context_handler import ContextHandler -from common.util import output_line +from common.output_util import output_line class ContextCommand(BaseCommand): @@ -33,9 +30,8 @@ def get_command_parser(cls) -> argparse.ArgumentParser: "sections", nargs="*", choices=["registers", "stack", "code", "threads", "trace", "all"], - default="all" + default="all", ) - return parser @staticmethod diff --git a/commands/dereference.py b/commands/dereference.py new file mode 100644 index 0000000..628d733 --- /dev/null +++ b/commands/dereference.py @@ -0,0 +1,182 @@ +"""Dereference command class.""" + +import argparse +import shlex +from typing import Any, Dict, List + +from lldb import ( + SBAddress, + SBCommandReturnObject, + SBDebugger, + SBError, + SBExecutionContext, + SBInstruction, + SBMemoryRegionInfoList, + SBProcess, + SBTarget, +) + +from commands.base_command import BaseCommand +from common.color_settings import LLEFColorSettings +from common.constants import GLYPHS, TERM_COLORS +from common.context_handler import ContextHandler +from common.output_util import color_string, output_line +from common.state import LLEFState +from common.util import attempt_to_read_string_from_memory, check_process, hex_int, hex_or_str, is_code, positive_int + + +class DereferenceCommand(BaseCommand): + """Implements the dereference command""" + + program: str = "dereference" + container = None + context_handler = None + + def __init__(self, debugger: SBDebugger, __: Dict[Any, Any]) -> None: + super().__init__() + self.parser = self.get_command_parser() + self.context_handler = ContextHandler(debugger) + self.color_settings = LLEFColorSettings() + self.state = LLEFState() + + @classmethod + def get_command_parser(cls) -> argparse.ArgumentParser: + """Get the command parser.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--lines", + type=positive_int, + default=10, + help="The number of consecutive addresses to dereference", + ) + parser.add_argument( + "-b", + "--base", + type=positive_int, + default=0, + help="An address to calculate offsets from. By default this is the stack pointer ($rsp)", + ) + parser.add_argument( + "address", + type=hex_int, + help="A value/address/symbol used as the location to print the dereference from", + ) + return parser + + @staticmethod + def get_short_help() -> str: + """Return a short help message""" + return "Usage: dereference [-h] [-l LINES] [-b OFFSET-BASE] [address]" + + @staticmethod + def get_long_help() -> str: + """Return a longer help message""" + return DereferenceCommand.get_command_parser().format_help() + + def read_instruction(self, target: SBTarget, address: int) -> SBInstruction: + """ + We disassemble an instruction at the given memory @address. + + :param target: The target object file. + :param address: The memory address of the instruction. + :return: An object of the disassembled instruction. + """ + instruction_address = SBAddress(address, target) + instruction_list = target.ReadInstructions(instruction_address, 1, self.state.disassembly_syntax) + return instruction_list.GetInstructionAtIndex(0) + + def dereference_last_address( + self, data: list, target: SBTarget, process: SBProcess, regions: SBMemoryRegionInfoList + ): + """ + Memory data at the last address (second to last in @data list) is + either disassembled to an instruction or converted to a string or neither. + + :param data: List of memory addresses/data. + :param target: The target object file. + :param process: The running process of the target. + :param regions: List of memory regions of the process. + """ + last_address = data[-2] + + if is_code(last_address, process, target, regions): + instruction = self.read_instruction(target, last_address) + if instruction.IsValid(): + data[-1] = color_string( + f"{instruction.GetMnemonic(target)}{instruction.GetOperands(target)}", + self.color_settings.instruction_color, + ) + else: + string = attempt_to_read_string_from_memory(process, last_address) + if string != "": + data[-1] = color_string(string, self.color_settings.string_color) + + def dereference(self, address: int, target: SBTarget, process: SBProcess, regions: SBMemoryRegionInfoList) -> List: + """ + Dereference a memory @address until it reaches data that cannot be resolved to an address. + Memory data at the last address is either disassembled to an instruction or converted to a string or neither. + The chain of dereferencing is output. + + :param address: The address to dereference + :param offset: The offset of address from a choosen base. + :param target: The target object file. + :param process: The running process of the target. + :param regions: List of memory regions of the process. + """ + + data = [] + + error = SBError() + while error.Success(): + data.append(address) + address = process.ReadPointerFromMemory(address, error) + if len(data) > 1 and data[-1] in data[:-2]: + data.append(color_string("[LOOPING]", TERM_COLORS.GREY.name)) + break + + if len(data) < 2: + data.append(color_string("NOT ACCESSIBLE", TERM_COLORS.RED.name)) + else: + self.dereference_last_address(data, target, process, regions) + + return data + + def print_dereference_result(self, result: List, offset: int): + """Format and output the results of dereferencing an address.""" + output = color_string(hex_or_str(result[0]), TERM_COLORS.CYAN.name, rwrap=GLYPHS.VERTICAL_LINE.value) + if offset >= 0: + output += f"+0x{offset:04x}: " + else: + output += f"-0x{-offset:04x}: " + output += " -> ".join(map(hex_or_str, result[1:])) + output_line(output) + + @check_process + def __call__( + self, + debugger: SBDebugger, + command: str, + exe_ctx: SBExecutionContext, + result: SBCommandReturnObject, + ) -> None: + """Handles the invocation of the dereference command""" + + args = self.parser.parse_args(shlex.split(command)) + + start_address = args.address + lines = args.lines + if args.base: + base = args.base + else: + base = start_address + + self.context_handler.refresh(exe_ctx) + + address_size = exe_ctx.target.GetAddressByteSize() + + end_address = start_address + address_size * lines + for address in range(start_address, end_address, address_size): + offset = address - base + result = self.dereference(address, exe_ctx.target, exe_ctx.process, self.context_handler.regions) + self.print_dereference_result(result, offset) diff --git a/commands/hexdump.py b/commands/hexdump.py index c9818bf..e70e316 100644 --- a/commands/hexdump.py +++ b/commands/hexdump.py @@ -1,4 +1,5 @@ """Hexdump command class.""" + import argparse import shlex from typing import Any, Dict @@ -6,8 +7,9 @@ from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext from commands.base_command import BaseCommand -from common.context_handler import ContextHandler from common.constants import SIZES +from common.context_handler import ContextHandler +from common.util import check_process, check_version, hex_int, positive_int class HexdumpCommand(BaseCommand): @@ -17,6 +19,10 @@ class HexdumpCommand(BaseCommand): container = None context_handler = None + # Define alias set, where each entry is an alias with any arguments the command should take. + # For example, 'dq' maps to 'hexdump qword'. + alias_set = {"dq": "qword", "dd": "dword", "dw": "word", "db": "byte"} + def __init__(self, debugger: SBDebugger, __: Dict[Any, Any]) -> None: super().__init__() self.parser = self.get_command_parser() @@ -30,18 +36,23 @@ def get_command_parser(cls) -> argparse.ArgumentParser: "type", choices=["qword", "dword", "word", "byte"], default="byte", - help="The format for presenting data" + help="The format for presenting data", ) parser.add_argument( "--reverse", action="store_true", - help="The direction of output lines. Low to high by default" + help="The direction of output lines. Low to high by default", + ) + parser.add_argument( + "--size", + type=positive_int, + default=16, + help="The number of qword/dword/word/bytes to display", ) - parser.add_argument("--size", type=positive_int, default=16, help="The number of qword/dword/word/bytes to display") parser.add_argument( "address", type=hex_int, - help="A value/address/symbol used as the location to print the hexdump from" + help="A value/address/symbol used as the location to print the hexdump from", ) return parser @@ -55,6 +66,8 @@ def get_long_help() -> str: """Return a longer help message""" return HexdumpCommand.get_command_parser().format_help() + @check_version("15.0.0") + @check_process def __call__( self, debugger: SBDebugger, @@ -63,6 +76,7 @@ def __call__( result: SBCommandReturnObject, ) -> None: """Handles the invocation of the hexdump command""" + args = self.parser.parse_args(shlex.split(command)) divisions = SIZES[args.type.upper()].value @@ -71,7 +85,7 @@ def __call__( self.context_handler.refresh(exe_ctx) - start = (size-1) * divisions if args.reverse else 0 + start = (size - 1) * divisions if args.reverse else 0 end = -divisions if args.reverse else size * divisions step = -divisions if args.reverse else divisions @@ -87,16 +101,3 @@ def __call__( else: for i in range(start, end, step): self.context_handler.print_memory_address(address + i, i, divisions) - - -def hex_int(x): - """A converter for input arguments in different bases to ints""" - return int(x, 0) - - -def positive_int(x): - """A converter for input arguments in different bases to positive ints""" - x = int(x, 0) - if x <= 0: - raise argparse.ArgumentTypeError("Must be positive") - return x diff --git a/commands/pattern.py b/commands/pattern.py index 8dfb317..013a2c0 100644 --- a/commands/pattern.py +++ b/commands/pattern.py @@ -12,8 +12,8 @@ from commands.base_container import BaseContainer from common.constants import MSG_TYPE, TERM_COLORS from common.de_bruijn import generate_cyclic_pattern +from common.output_util import output_line, print_message from common.state import LLEFState -from common.util import print_message, output_line class PatternContainer(BaseContainer): @@ -82,9 +82,7 @@ def __call__( args = self.parser.parse_args(shlex.split(command)) length = args.length num_chars = args.cycle_length or 4 # Hardcoded default value. - print_message( - MSG_TYPE.INFO, f"Generating a pattern of {length} bytes (n={num_chars})" - ) + print_message(MSG_TYPE.INFO, f"Generating a pattern of {length} bytes (n={num_chars})") pattern = generate_cyclic_pattern(length, num_chars) output_line(pattern.decode("utf-8")) @@ -94,9 +92,7 @@ def __call__( "Created pattern cannot be stored in a convenience variable as there is no running process", ) else: - value = exe_ctx.GetTarget().EvaluateExpression( - f'"{pattern.decode("utf-8")}"' - ) + value = exe_ctx.GetTarget().EvaluateExpression(f'"{pattern.decode("utf-8")}"') print_message( MSG_TYPE.INFO, f"Pattern saved in variable: {TERM_COLORS.RED.value}{value.GetName()}{TERM_COLORS.ENDC.value}", diff --git a/commands/scan.py b/commands/scan.py new file mode 100644 index 0000000..dd82ed5 --- /dev/null +++ b/commands/scan.py @@ -0,0 +1,174 @@ +"""Scan command class.""" + +import argparse +import shlex +from typing import Any, Dict, List, Tuple + +from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBMemoryRegionInfo, SBProcess, SBTarget + +from commands.base_command import BaseCommand +from common.constants import MSG_TYPE +from common.context_handler import ContextHandler +from common.output_util import print_message +from common.state import LLEFState +from common.util import check_process + + +class ScanCommand(BaseCommand): + """Implements the scan command""" + + program: str = "scan" + container = None + context_handler = None + + def __init__(self, debugger: SBDebugger, __: Dict[Any, Any]) -> None: + super().__init__() + self.parser = self.get_command_parser() + self.context_handler = ContextHandler(debugger) + self.state = LLEFState() + + @classmethod + def get_command_parser(cls) -> argparse.ArgumentParser: + """Get the command parser.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "search_region", + type=str, + help="Memory region to search through.", + ) + parser.add_argument( + "target_region", + type=str, + help="Memory address range to search for.", + ) + return parser + + @staticmethod + def get_short_help() -> str: + """Return a short help message""" + return "Usage: scan [search_region] [target_region]" + + @staticmethod + def get_long_help() -> str: + """Return a longer help message""" + return ScanCommand.get_command_parser().format_help() + + def parse_address_ranges(self, process: SBProcess, region_name: str) -> List[Tuple[int, int]]: + """ + Parse a custom address range (e.g., 0x7fffffffe208-0x7fffffffe240) + or extract address ranges from memory regions with a given name (e.g., libc). + + :param process: Running process of target executable. + :param region_name: A name that can be found in the pathname of memory regions or a custom address range. + :return: A list of address ranges. + """ + address_ranges = [] + + if "-" in region_name: + region_start_end = region_name.split("-") + if len(region_start_end) == 2: + try: + region_start = int(region_start_end[0], 16) + region_end = int(region_start_end[1], 16) + address_ranges.append((region_start, region_end)) + except ValueError: + print_message(MSG_TYPE.ERROR, "Invalid address range.") + else: + address_ranges = self.find_address_ranges(process, region_name) + + return address_ranges + + def find_address_ranges(self, process: SBProcess, region_name: str) -> List[Tuple[int, int]]: + """ + Extract address ranges from memory regions with @region_name. + + :param process: Running process of target executable. + :param region_name: A name that can be found in the pathname of memory regions. + :return: A list of address ranges. + """ + + address_ranges = [] + + memory_regions = process.GetMemoryRegions() + memory_region_count = memory_regions.GetSize() + for i in range(memory_region_count): + memory_region = SBMemoryRegionInfo() + if ( + memory_regions.GetMemoryRegionAtIndex(i, memory_region) + and memory_region.IsMapped() + and memory_region.GetName() is not None + and region_name in memory_region.GetName() + ): + region_start = memory_region.GetRegionBase() + region_end = memory_region.GetRegionEnd() + address_ranges.append((region_start, region_end)) + + return address_ranges + + def scan( + self, + search_address_ranges: List[Tuple[int, int]], + target_address_ranges: List[Tuple[int, int]], + address_size: int, + process: SBProcess, + target: SBTarget, + ) -> List[Tuple[int, int]]: + """ + Scan through a given search space in memory for addresses that point towards a target memory space. + + :param search_address_ranges: A list of start and end addresses of memory regions to search. + :param target_address_ranges: A list of start and end addresses defining the range of addresses to search for. + :param address_size: The expected address size for the architecture. + :param process: The running process of the target. + :param target: The target executable. + :return: A list of addresses (with their offsets) in the search space that point towards the target address + space. + """ + results = [] + error = SBError() + for search_start, search_end in search_address_ranges: + for search_address in range(search_start, search_end, address_size): + target_address = process.ReadUnsignedFromMemory(search_address, address_size, error) + if error.Success(): + for target_start, target_end in target_address_ranges: + if target_address >= target_start and target_address < target_end: + offset = search_address - search_start + search_address_value = target.EvaluateExpression(f"{search_address}") + results.append((search_address_value, offset)) + else: + print_message(MSG_TYPE.ERROR, f"Memory at {search_address} couldn't be read.") + return results + + @check_process + def __call__( + self, + debugger: SBDebugger, + command: str, + exe_ctx: SBExecutionContext, + result: SBCommandReturnObject, + ) -> None: + """Handles the invocation of the scan command""" + + args = self.parser.parse_args(shlex.split(command)) + search_region = args.search_region + target_region = args.target_region + + self.context_handler.refresh(exe_ctx) + + search_address_ranges = self.parse_address_ranges(exe_ctx.process, search_region) + target_address_ranges = self.parse_address_ranges(exe_ctx.process, target_region) + + if self.state.platform == "Darwin" and (search_address_ranges == [] or target_address_ranges == []): + print_message( + MSG_TYPE.ERROR, + "Memory region names cannot be resolved on macOS. Use memory address ranges instead.", + ) + return + + print_message(MSG_TYPE.INFO, f"Searching for addresses in '{search_region}' that point to '{target_region}'") + + address_size = exe_ctx.target.GetAddressByteSize() + + results = self.scan(search_address_ranges, target_address_ranges, address_size, exe_ctx.process, exe_ctx.target) + for address, offset in results: + self.context_handler.print_stack_addr(address, offset) diff --git a/commands/settings.py b/commands/settings.py index 5bb1dee..82cfc26 100644 --- a/commands/settings.py +++ b/commands/settings.py @@ -1,11 +1,12 @@ """llefsettings command class.""" + import argparse from typing import Any, Dict from lldb import SBDebugger -from common.settings import LLEFSettings from commands.base_settings import BaseSettingsCommand +from common.settings import LLEFSettings class SettingsCommand(BaseSettingsCommand): diff --git a/commands/xinfo.py b/commands/xinfo.py new file mode 100644 index 0000000..9493a8c --- /dev/null +++ b/commands/xinfo.py @@ -0,0 +1,159 @@ +"""Xinfo command class.""" + +import argparse +import os +import shlex +from typing import Any, Dict + +from lldb import ( + SBCommandReturnObject, + SBDebugger, + SBExecutionContext, + SBMemoryRegionInfo, + SBProcess, + SBStream, + SBTarget, +) + +from arch import get_arch +from commands.base_command import BaseCommand +from common.constants import MSG_TYPE, XINFO +from common.context_handler import ContextHandler +from common.output_util import print_message +from common.state import LLEFState +from common.util import check_process, hex_int + + +class XinfoCommand(BaseCommand): + """Implements the xinfo command""" + + program: str = "xinfo" + container = None + context_handler = None + + def __init__(self, debugger: SBDebugger, __: Dict[Any, Any]) -> None: + super().__init__() + self.parser = self.get_command_parser() + self.context_handler = ContextHandler(debugger) + self.state = LLEFState() + + @classmethod + def get_command_parser(cls) -> argparse.ArgumentParser: + """Get the command parser.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "address", + type=hex_int, + help="A value/address/symbol used as the location to print the xinfo from", + ) + return parser + + @staticmethod + def get_short_help() -> str: + """Return a short help message""" + return "Usage: xinfo [address]" + + @staticmethod + def get_long_help() -> str: + """Return a longer help message""" + return XinfoCommand.get_command_parser().format_help() + + def get_xinfo(self, process: SBProcess, target: SBTarget, address: int) -> Dict[str, Any]: + """ + Gets memory region information for a given `address`, including: + - `region_start` address + - `region_end` address + - `region_size` + - `region_offset` (offset of address from start of region) + - file `path` corrosponding to the address + - `inode` of corrosponding file + + :param state: The LLEF state containing platform variable. + :param process: The running process of the target to extract memory regions. + :param target: The target executable. + :param address: The address get information about. + :return: A dictionary containing the information about the address. + The function will return `None` if the address isn't mapped. + """ + memory_region = SBMemoryRegionInfo() + error = process.GetMemoryRegionInfo(address, memory_region) + + if error.Fail() or not memory_region.IsMapped(): + return None + + xinfo = { + XINFO.REGION_START: None, + XINFO.REGION_END: None, + XINFO.REGION_SIZE: None, + XINFO.REGION_OFFSET: None, + XINFO.PERMISSIONS: None, + XINFO.PATH: None, + XINFO.INODE: None, + } + + xinfo[XINFO.REGION_START] = memory_region.GetRegionBase() + xinfo[XINFO.REGION_END] = memory_region.GetRegionEnd() + xinfo[XINFO.REGION_SIZE] = xinfo[XINFO.REGION_END] - xinfo[XINFO.REGION_START] + xinfo[XINFO.REGION_OFFSET] = address - xinfo[XINFO.REGION_START] + + permissions = "" + permissions += "r" if memory_region.IsReadable() else "" + permissions += "w" if memory_region.IsWritable() else "" + permissions += "x" if memory_region.IsExecutable() else "" + xinfo[XINFO.PERMISSIONS] = permissions + + if self.state.platform == "Darwin": + sb_address = target.ResolveLoadAddress(address) + module = sb_address.GetModule() + filespec = module.GetFileSpec() + description = SBStream() + filespec.GetDescription(description) + xinfo[XINFO.PATH] = description.GetData() + else: + xinfo[XINFO.PATH] = memory_region.GetName() + + if xinfo[XINFO.PATH] is not None and os.path.exists(xinfo[XINFO.PATH]): + xinfo[XINFO.INODE] = os.stat(xinfo[XINFO.PATH]).st_ino + else: + xinfo[XINFO.INODE] = None + + return xinfo + + @check_process + def __call__( + self, + debugger: SBDebugger, + command: str, + exe_ctx: SBExecutionContext, + result: SBCommandReturnObject, + ) -> None: + """Handles the invocation of the xinfo command""" + + args = self.parser.parse_args(shlex.split(command)) + address = args.address + + if address < 0 or address > 2 ** get_arch(exe_ctx.target).bits: + print_message(MSG_TYPE.ERROR, "Invalid address.") + return + + xinfo = self.get_xinfo(exe_ctx.process, exe_ctx.target, address) + + if xinfo is not None: + print_message(MSG_TYPE.SUCCESS, f"Found: {hex(address)}") + print_message( + MSG_TYPE.INFO, + ( + f"Page/Region: {hex(xinfo[XINFO.REGION_START])}->{hex(xinfo[XINFO.REGION_END])}" + f" (size={hex(xinfo[XINFO.REGION_SIZE])})" + ), + ) + print_message(MSG_TYPE.INFO, f"Permissions: {xinfo[XINFO.PERMISSIONS]}") + print_message(MSG_TYPE.INFO, f"Pathname: {xinfo[XINFO.PATH]}") + print_message(MSG_TYPE.INFO, f"Offset (from page/region): +{hex(xinfo[XINFO.REGION_OFFSET])}") + + if xinfo[XINFO.INODE] is not None: + print_message(MSG_TYPE.INFO, f"Inode: {xinfo[XINFO.INODE]}") + else: + print_message(MSG_TYPE.ERROR, "No inode found: Path cannot be found locally.") + else: + print_message(MSG_TYPE.ERROR, f"Not Found: {hex(address)}") diff --git a/common/base_settings.py b/common/base_settings.py index eae4fd6..795ad46 100644 --- a/common/base_settings.py +++ b/common/base_settings.py @@ -1,17 +1,20 @@ """A base class for global settings""" + import configparser import os - from abc import abstractmethod + +from common.output_util import output_line from common.singleton import Singleton -from common.util import output_line +from common.state import LLEFState class BaseLLEFSettings(metaclass=Singleton): """ Global settings class - loaded from file defined in `LLEF_CONFIG_PATH` """ - LLEF_CONFIG_PATH = os.path.join(os.path.expanduser('~'), ".llef") + + LLEF_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".llef") GLOBAL_SECTION = "LLEF" _RAW_CONFIG: configparser.ConfigParser = configparser.ConfigParser() @@ -21,6 +24,7 @@ def _get_setting_names(cls): return [name for name, value in vars(cls).items() if isinstance(value, property)] def __init__(self): + self.state = LLEFState() self.load() @abstractmethod diff --git a/common/color_settings.py b/common/color_settings.py index 2adbefe..f4728fb 100644 --- a/common/color_settings.py +++ b/common/color_settings.py @@ -1,20 +1,20 @@ """Color settings module""" -import configparser -import os +import os from typing import List -from common.singleton import Singleton -from common.constants import TERM_COLORS from common.base_settings import BaseLLEFSettings -from common.util import output_line +from common.constants import TERM_COLORS +from common.output_util import output_line +from common.singleton import Singleton class LLEFColorSettings(BaseLLEFSettings, metaclass=Singleton): """ Color settings class - loaded from file defined in `LLEF_CONFIG_PATH` """ - LLEF_CONFIG_PATH = os.path.join(os.path.expanduser('~'), ".llef_colors") + + LLEF_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".llef_colors") GLOBAL_SECTION = "LLEF" supported_colors: List[str] = [] @@ -90,11 +90,15 @@ def dereferenced_register_color(self): @property def frame_argument_name_color(self): return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "frame_argument_name_color", fallback="YELLOW").upper() - + @property def read_memory_address_color(self): return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "read_memory_address_color", fallback="CYAN").upper() + @property + def address_operand_color(self): + return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "address_operand_color", fallback="RED").upper() + def __init__(self): self.supported_colors = [color.name for color in TERM_COLORS] self.supported_colors.remove(TERM_COLORS.ENDC.name) diff --git a/common/constants.py b/common/constants.py index 7784862..aaf6531 100644 --- a/common/constants.py +++ b/common/constants.py @@ -1,6 +1,6 @@ """Constant definitions.""" -from enum import Enum +from enum import Enum, IntEnum class TERM_COLORS(Enum): @@ -52,3 +52,86 @@ class SIZES(Enum): DWORD = 4 WORD = 2 BYTE = 1 + + +class XINFO(Enum): + REGION_START = "Region Start" + REGION_END = "Region End" + REGION_SIZE = "Region Size" + REGION_OFFSET = "Region Offset" + PERMISSIONS = "Permissions" + PATH = "Path" + INODE = "INode" + + +class SECURITY_FEATURE(Enum): + STACK_CANARY = "Stack Canary" + NX_SUPPORT = "NX Support" + PIE_SUPPORT = "PIE Support" + NO_RPATH = "No RPath" + NO_RUNPATH = "No RunPath" + PARTIAL_RELRO = "Partial RelRO" + FULL_RELRO = "Full RelRO" + + +class SECURITY_CHECK(Enum): + NO = "No" + YES = "Yes" + UNKNOWN = "Unknown" + + +class PERMISSION_SET: + """Values for 3bit permission sets.""" + + NOT_EXEC = [0, 2, 4, 6] + EXEC = [1, 3, 5, 7] + + +class PROGRAM_HEADER_TYPE(IntEnum): + """Program header type values (in ELF files).""" + + GNU_STACK = 0x6474E551 + GNU_RELRO = 0x6474E552 + + +class EXECUTABLE_TYPE(IntEnum): + """Executable ELF file types.""" + + DYN = 0x03 + + +class DYNAMIC_ENTRY_TYPE(IntEnum): + """Entry types in the .dynamic section table of the ELF file.""" + + FLAGS = 0x1E + RPATH = 0x0F + RUNPATH = 0x1D + + +class DYNAMIC_ENTRY_VALUE(IntEnum): + """Entry values in the .dynamic section table of the ELF file.""" + + BIND_NOW = 0x08 + + +class ARCH_BITS(IntEnum): + """32bit or 64bit architecture.""" + + BITS_32 = 1 + BITS_64 = 2 + + +class MAGIC_BYTES(Enum): + """Magic byte signatures for executable files.""" + + ELF = [b"\x7f\x45\x4c\x46"] + MACH = [ + b"\xfe\xed\xfa\xce", + b"\xfe\xed\xfa\xcf", + b"\xce\xfa\xed\xfe", + b"\xcf\xfa\xed\xfe", + ] + + +DEFAULT_TERMINAL_COLUMNS = 80 +DEFAULT_TERMINAL_LINES = 24 diff --git a/common/context_handler.py b/common/context_handler.py index 82373cf..0bfb53c 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -1,14 +1,15 @@ import os - -from typing import Dict, Type, Optional from string import printable +from typing import List, Optional, Tuple, Type from lldb import ( SBAddress, + SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBFrame, + SBMemoryRegionInfo, SBProcess, SBTarget, SBThread, @@ -17,23 +18,23 @@ from arch import get_arch, get_arch_from_str from arch.base_arch import BaseArch, FlagRegister +from common.color_settings import LLEFColorSettings from common.constants import GLYPHS, TERM_COLORS +from common.instruction_util import extract_instructions, print_instruction, print_instructions +from common.output_util import clear_page, color_string, output_line, print_line, print_line_with_string from common.settings import LLEFSettings -from common.color_settings import LLEFColorSettings from common.state import LLEFState from common.util import ( + address_to_filename, attempt_to_read_string_from_memory, - clear_page, + find_darwin_heap_regions, + find_stack_regions, get_frame_arguments, + get_frame_range, get_registers, is_code, is_heap, is_stack, - print_instruction, - print_line, - print_line_with_string, - change_use_color, - output_line ) @@ -50,6 +51,8 @@ class ContextHandler: settings: LLEFSettings color_settings: LLEFColorSettings state: LLEFState + darwin_stack_regions: List[SBMemoryRegionInfo] + darwin_heap_regions: List[Tuple[int, int]] def __init__( self, @@ -62,7 +65,9 @@ def __init__( self.settings = LLEFSettings(debugger) self.color_settings = LLEFColorSettings() self.state = LLEFState() - change_use_color(self.settings.color_output) + self.state.change_use_color(self.settings.color_output) + self.darwin_stack_regions = None + self.darwin_heap_regions = None def generate_rebased_address_string(self, address: SBAddress) -> str: module = address.GetModule() @@ -70,11 +75,7 @@ def generate_rebased_address_string(self, address: SBAddress) -> str: if module is not None and self.settings.rebase_addresses is True: file_name = os.path.basename(str(module.file)) rebased_address = address.GetFileAddress() + self.settings.rebase_offset - return ( - f" {TERM_COLORS[self.color_settings.rebased_address_color].value}" - f"({file_name} {rebased_address:#x})" - f"{TERM_COLORS.ENDC.value}" - ) + return color_string(f"({file_name} {rebased_address:#x})", self.color_settings.rebased_address_color) return "" @@ -91,28 +92,19 @@ def generate_printable_line_from_pointer( pointer_value = SBAddress(pointer, self.target) if pointer_value.symbol.IsValid(): - offset = ( - pointer_value.offset - pointer_value.symbol.GetStartAddress().offset - ) - line += ( - f"{self.generate_rebased_address_string(pointer_value)} {GLYPHS.RIGHT_ARROW.value}" - f"{TERM_COLORS[self.color_settings.dereferenced_value_color].value}" - f"<{pointer_value.symbol.name}+{offset}>" - f"{TERM_COLORS.ENDC.value}" + offset = pointer_value.offset - pointer_value.symbol.GetStartAddress().offset + line += f" {self.generate_rebased_address_string(pointer_value)} {GLYPHS.RIGHT_ARROW.value}" + line += color_string( + f"<{pointer_value.symbol.name}+{offset}>", self.color_settings.dereferenced_value_color ) - referenced_string = attempt_to_read_string_from_memory( - self.process, pointer_value.GetLoadAddress(self.target) - ) + referenced_string = attempt_to_read_string_from_memory(self.process, pointer_value.GetLoadAddress(self.target)) if len(referenced_string) > 0 and referenced_string.isprintable(): # Only add this to the line if there are any printable characters in refd_string referenced_string = referenced_string.replace("\n", " ") - line += ( - f' {GLYPHS.RIGHT_ARROW.value} ("' - f'{TERM_COLORS[self.color_settings.string_color].value}' - f'{referenced_string}' - f'{TERM_COLORS.ENDC.value}"?)' + line += color_string( + referenced_string, self.color_settings.string_color, f' {GLYPHS.RIGHT_ARROW.value} ("', "?)" ) if address_containing_pointer is not None: @@ -122,23 +114,21 @@ def generate_printable_line_from_pointer( registers_pointing_to_address.append(f"${register.GetName()}") if len(registers_pointing_to_address) > 0: reg_list = ", ".join(registers_pointing_to_address) - line += ( - f" {TERM_COLORS[self.color_settings.dereferenced_register_color].value}" - f"{GLYPHS.LEFT_ARROW.value}{reg_list}" - f"{TERM_COLORS.ENDC.value}" + line += color_string( + f"{GLYPHS.LEFT_ARROW.value}{reg_list}", self.color_settings.dereferenced_register_color ) return line def print_stack_addr(self, addr: SBValue, offset: int) -> None: """Produce a printable line containing information about a given stack @addr and print it""" - # Add stack address to line - line = ( - f"{TERM_COLORS[self.color_settings.stack_address_color].value}{hex(addr.GetValueAsUnsigned())}" - + f"{TERM_COLORS.ENDC.value}{GLYPHS.VERTICAL_LINE.value}" + # Add stack address and offset to line + + line = color_string( + hex(addr.GetValueAsUnsigned()), + self.color_settings.stack_address_color, + rwrap=f"{GLYPHS.VERTICAL_LINE.value}+{offset:04x}: ", ) - # Add offset to line - line += f"+{offset:04x}: " # Add value to line err = SBError() @@ -149,26 +139,24 @@ def print_stack_addr(self, addr: SBValue, offset: int) -> None: # Shouldn't happen as stack should always contain something line += str(err) - line += self.generate_printable_line_from_pointer( - stack_value, addr.GetValueAsUnsigned() - ) + line += self.generate_printable_line_from_pointer(stack_value, addr.GetValueAsUnsigned()) output_line(line) def print_memory_address(self, addr: int, offset: int, size: int) -> None: """Print a line containing information about @size bytes at @addr displaying @offset""" - # Add address to line - line = ( - f"{TERM_COLORS[self.color_settings.read_memory_address_color].value}{hex(addr)}" - + f"{TERM_COLORS.ENDC.value}{GLYPHS.VERTICAL_LINE.value}" + # Add address and offset to line + line = color_string( + hex(addr), + self.color_settings.read_memory_address_color, + rwrap=f"{GLYPHS.VERTICAL_LINE.value}+{offset:04x}: ", ) - # Add offset to line - line += f"+{offset:04x}: " # Add value to line err = SBError() - memory_value = int.from_bytes(self.process.ReadMemory(addr, size, err), 'little') + memory_value = self.process.ReadMemory(addr, size, err) + if err.Success(): - line += f"0x{memory_value:0{size * 2}x}" + line += f"0x{int.from_bytes(memory_value, 'little'):0{size * 2}x}" else: line += str(err) @@ -178,10 +166,7 @@ def print_bytes(self, addr: int, size: int) -> None: """Print a line containing information about @size individual bytes at @addr""" if size > 0: # Add address to line - line = ( - f"{TERM_COLORS[self.color_settings.read_memory_address_color].value}{hex(addr)}" - + f"{TERM_COLORS.ENDC.value} " - ) + line = color_string(hex(addr), self.color_settings.read_memory_address_color, "", "\t") # Add value to line err = SBError() @@ -210,24 +195,22 @@ def print_register(self, register: SBValue) -> None: if self.state.prev_registers.get(reg_name) == register.GetValueAsUnsigned(): # Register value as not changed - highlight = TERM_COLORS[self.color_settings.register_color] + highlight = self.color_settings.register_color else: # Register value has changed so highlight - highlight = TERM_COLORS[self.color_settings.modified_register_color] - - if is_code(reg_value, self.process, self.regions): - color = TERM_COLORS[self.color_settings.code_color] - elif is_stack(reg_value, self.process, self.regions): - color = TERM_COLORS[self.color_settings.stack_color] - elif is_heap(reg_value, self.process, self.regions): - color = TERM_COLORS[self.color_settings.heap_color] + highlight = self.color_settings.modified_register_color + + if is_code(reg_value, self.process, self.target, self.regions): + color = self.color_settings.code_color + elif is_stack(reg_value, self.regions, self.darwin_stack_regions): + color = self.color_settings.stack_color + elif is_heap(reg_value, self.target, self.regions, self.darwin_stack_regions, self.darwin_heap_regions): + color = self.color_settings.heap_color else: - color = TERM_COLORS.ENDC + color = None formatted_reg_value = f"{reg_value:x}".ljust(12) - line = ( - f"{highlight.value}{reg_name.ljust(7)}{TERM_COLORS.ENDC.value}: " - + f"{color.value}0x{formatted_reg_value}{TERM_COLORS.ENDC.value}" - ) + line = color_string(reg_name.ljust(7), highlight, "", ": ") + line += color_string(f"0x{formatted_reg_value}", color) line += self.generate_printable_line_from_pointer(reg_value) @@ -239,19 +222,15 @@ def print_flags_register(self, flag_register: FlagRegister) -> None: if self.state.prev_registers.get(flag_register.name) == flag_value: # No change - highlight = TERM_COLORS[self.color_settings.register_color] + highlight = self.color_settings.register_color else: # Change and highlight - highlight = TERM_COLORS[self.color_settings.modified_register_color] - - line = f"{highlight.value}{flag_register.name.ljust(7)}{TERM_COLORS.ENDC.value}: [" - line += " ".join( - [ - name.upper() if flag_value & bitmask else name - for name, bitmask in flag_register.bit_masks.items() - ] + highlight = self.color_settings.modified_register_color + + flags = " ".join( + [name.upper() if flag_value & bitmask else name for name, bitmask in flag_register.bit_masks.items()] ) - line += "]" + line = color_string(flag_register.name.ljust(7), highlight, rwrap=f": [{flags}]") output_line(line) def update_registers(self) -> None: @@ -268,23 +247,27 @@ def update_registers(self) -> None: def print_legend(self) -> None: """Print a line containing the color legend""" - output_line( - f"[ Legend: " - f"{TERM_COLORS[self.color_settings.modified_register_color].value}" - f"Modified register{TERM_COLORS.ENDC.value} | " - f"{TERM_COLORS[self.color_settings.code_color].value}Code{TERM_COLORS.ENDC.value} | " - f"{TERM_COLORS[self.color_settings.heap_color].value}Heap{TERM_COLORS.ENDC.value} | " - f"{TERM_COLORS[self.color_settings.stack_color].value}Stack{TERM_COLORS.ENDC.value} | " - f"{TERM_COLORS[self.color_settings.string_color].value}String{TERM_COLORS.ENDC.value} ]" - ) + legend = "[ Legend: " + legend += color_string("Modified register", self.color_settings.modified_register_color, rwrap=" | ") + legend += color_string("Code", self.color_settings.code_color, rwrap=" | ") + + # Only set when platform is Darwin (iOS, MacOS, etc) and darwin heap scan is enabled in settings. + if self.darwin_heap_regions is not None: + legend += color_string("Heap (Darwin heap scan)", self.color_settings.heap_color, rwrap=" | ") + else: + legend += color_string("Heap", self.color_settings.heap_color, rwrap=" | ") + + legend += color_string("Stack", self.color_settings.stack_color, rwrap=" | ") + legend += color_string("String", self.color_settings.string_color, rwrap=" ]") + output_line(legend) def display_registers(self) -> None: """Print the registers display section""" print_line_with_string( "registers", - line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + line_color=self.color_settings.line_color, + string_color=self.color_settings.section_header_color, ) if self.settings.show_all_registers: @@ -310,8 +293,8 @@ def display_stack(self) -> None: print_line_with_string( "stack", - line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + line_color=self.color_settings.line_color, + string_color=self.color_settings.section_header_color, ) for inc in range(0, self.arch().bits, 8): stack_pointer = self.frame.GetSP() @@ -324,58 +307,52 @@ def display_code(self) -> None: """ print_line_with_string( "code", - line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + line_color=self.color_settings.line_color, + string_color=self.color_settings.section_header_color, ) - if self.frame.disassembly: - instructions = self.frame.disassembly.split("\n") - - current_pc = hex(self.frame.GetPC()) - for i, item in enumerate(instructions): - if current_pc in item.split(':')[0]: - output_line(instructions[0]) - if i > 3: - print_instruction(instructions[i - 3], TERM_COLORS[self.color_settings.instruction_color]) - print_instruction(instructions[i - 2], TERM_COLORS[self.color_settings.instruction_color]) - print_instruction(instructions[i - 1], TERM_COLORS[self.color_settings.instruction_color]) - print_instruction(item, TERM_COLORS[self.color_settings.highlighted_instruction_color]) - # This slice notation (and the 4 below) are a buggy interaction of black and pycodestyle - # See: https://github.com/psf/black/issues/157 - # fmt: off - for instruction in instructions[i + 1:i + 6]: # noqa - # fmt: on - print_instruction(instruction) - if i == 3: - print_instruction(instructions[i - 2], TERM_COLORS[self.color_settings.instruction_color]) - print_instruction(instructions[i - 1], TERM_COLORS[self.color_settings.instruction_color]) - print_instruction(item, TERM_COLORS[self.color_settings.highlighted_instruction_color]) - # fmt: off - for instruction in instructions[i + 1:10]: # noqa - # fmt: on - print_instruction(instruction) - if i == 2: - print_instruction(instructions[i - 1], TERM_COLORS[self.color_settings.instruction_color]) - print_instruction(item, TERM_COLORS[self.color_settings.highlighted_instruction_color]) - # fmt: off - for instruction in instructions[i + 1:10]: # noqa - # fmt: on - print_instruction(instruction) - if i == 1: - print_instruction(item, TERM_COLORS[self.color_settings.highlighted_instruction_color]) - # fmt: off - for instruction in instructions[i + 1:10]: # noqa - # fmt: on - print_instruction(instruction) - else: - output_line("No disassembly to print") + pc = self.frame.GetPC() + + filename = address_to_filename(self.target, pc) + function_name = self.frame.GetFunctionName() + output_line(f"{filename}'{function_name}:") + + frame_start_address, frame_end_address = get_frame_range(self.frame, self.target) + + pre_instructions = extract_instructions(self.target, frame_start_address, pc - 1, self.state.disassembly_syntax) + print_instructions( + self.target, + pre_instructions[-3:], + frame_start_address, + self.color_settings, + ) + + post_instructions = extract_instructions(self.target, pc, frame_end_address, self.state.disassembly_syntax) + + if len(post_instructions) > 0: + pc_instruction = post_instructions[0] + print_instruction( + self.target, + pc_instruction, + frame_start_address, + self.color_settings, + True, + ) + + limit = 9 - min(len(pre_instructions), 3) + print_instructions( + self.target, + post_instructions[1:limit], + frame_start_address, + self.color_settings, + ) def display_threads(self) -> None: """Print LLDB formatted thread information""" print_line_with_string( "threads", - line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + line_color=self.color_settings.line_color, + string_color=self.color_settings.section_header_color, ) for thread in self.process: output_line(thread) @@ -386,16 +363,16 @@ def display_trace(self) -> None: """ print_line_with_string( "trace", - line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + line_color=self.color_settings.line_color, + string_color=self.color_settings.section_header_color, ) for i in range(self.thread.GetNumFrames()): if i == 0: - number_color = TERM_COLORS[self.color_settings.highlighted_index_color] + number_color = self.color_settings.highlighted_index_color else: - number_color = TERM_COLORS[self.color_settings.index_color] - line = f"[{number_color.value}#{i}{TERM_COLORS.ENDC.value}] " + number_color = self.color_settings.index_color + line = color_string(f"#{i}", number_color, "[", "]") current_frame = self.thread.GetFrameAtIndex(i) pc_address = current_frame.GetPCAddress() @@ -405,29 +382,42 @@ def display_trace(self) -> None: if func: line += ( f"{trace_address:#x}{self.generate_rebased_address_string(pc_address)} {GLYPHS.RIGHT_ARROW.value} " - f"{TERM_COLORS[self.color_settings.function_name_color].value}" - f"{func.GetName()}{TERM_COLORS.ENDC.value}" + f"{color_string(func.GetName(), self.color_settings.function_name_color)}" ) else: line += ( f"{trace_address:#x}{self.generate_rebased_address_string(pc_address)} {GLYPHS.RIGHT_ARROW.value} " - f"{TERM_COLORS[self.color_settings.function_name_color].value}" - f"{current_frame.GetSymbol().GetName()}{TERM_COLORS.ENDC.value}" + f"{color_string(current_frame.GetSymbol().GetName(), self.color_settings.function_name_color)}" ) line += get_frame_arguments( - current_frame, - frame_argument_name_color=TERM_COLORS[self.color_settings.frame_argument_name_color] + current_frame, frame_argument_name_color=TERM_COLORS[self.color_settings.frame_argument_name_color] ) output_line(line) + def load_disassembly_syntax(self, debugger: SBDebugger) -> None: + """Load the disassembly flavour from LLDB into LLEF's state.""" + self.state.disassembly_syntax = "default" + if LLEFState.version >= [16]: + self.state.disassembly_syntax = debugger.GetSetting("target.x86-disassembly-flavor").GetStringValue(100) + + if self.state.disassembly_syntax == "": + command_interpreter = debugger.GetCommandInterpreter() + result = SBCommandReturnObject() + command_interpreter.HandleCommand("settings show target.x86-disassembly-flavor", result) + if result.Succeeded(): + self.state.disassembly_syntax = result.GetOutput().split("=")[1][1:].replace("\n", "") + + if self.state.disassembly_syntax == "": + self.state.disassembly_syntax = "default" + def refresh(self, exe_ctx: SBExecutionContext) -> None: """Refresh stored values""" - self.frame = exe_ctx.GetFrame() self.process = exe_ctx.GetProcess() self.target = exe_ctx.GetTarget() self.thread = exe_ctx.GetThread() + self.frame = self.thread.GetFrameAtIndex(0) if self.settings.force_arch is not None: self.arch = get_arch_from_str(self.settings.force_arch) else: @@ -438,11 +428,19 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: else: self.regions = None - def display_context( - self, - exe_ctx: SBExecutionContext, - update_registers: bool - ) -> None: + if self.state.disassembly_syntax == "": + self.load_disassembly_syntax(self.debugger) + + if LLEFState.platform == "Darwin": + self.darwin_stack_regions = find_stack_regions(self.process) + if self.settings.enable_darwin_heap_scan: + self.darwin_heap_regions = find_darwin_heap_regions(self.process) + else: + # Setting darwin_heap_regions to None will cause the fallback heap + # scanning method to be used. + self.darwin_heap_regions = None + + def display_context(self, exe_ctx: SBExecutionContext, update_registers: bool) -> None: """For up to date documentation on args provided to this function run: `help target stop-hook add`""" # Refresh frame, process, target, and thread objects at each stop. @@ -453,24 +451,22 @@ def display_context( self.update_registers() # Hack to print cursor at the top of the screen - clear_page() + if self.debugger.GetUseColor(): + clear_page() if self.settings.show_legend: self.print_legend() - if self.settings.show_registers: - self.display_registers() - - if self.settings.show_stack: - self.display_stack() - - if self.settings.show_code: - self.display_code() - - if self.settings.show_threads: - self.display_threads() - - if self.settings.show_trace: - self.display_trace() - - print_line(color=TERM_COLORS[self.color_settings.line_color]) + for section in self.settings.output_order.split(","): + if section == "registers" and self.settings.show_registers: + self.display_registers() + elif section == "stack" and self.settings.show_stack: + self.display_stack() + elif section == "code" and self.settings.show_code: + self.display_code() + elif section == "threads" and self.settings.show_threads: + self.display_threads() + elif section == "trace" and self.settings.show_trace: + self.display_trace() + + print_line(color=self.color_settings.line_color) diff --git a/common/expressions/darwin_get_malloc_zones.mm b/common/expressions/darwin_get_malloc_zones.mm new file mode 100644 index 0000000..4018686 --- /dev/null +++ b/common/expressions/darwin_get_malloc_zones.mm @@ -0,0 +1,119 @@ +/* +This file is a template for an LLDB expression using Objective-C++ syntax. + +The Darwin malloc implementation provides an API to read heap metadata at runtime. +The function 'malloc_get_all_zones' is defined in '' and provides a way to +enumerate allocated heap regions using the malloc zone introspection API. + +Implementation for 'malloc_get_all_zones' can be found here: +https://github.com/apple-oss-distributions/libmalloc/blob/main/src/malloc.c + +Based on LLDB 'heap_find' command: https://github.com/llvm-mirror/lldb/blob/master/examples/darwin/heap_find/heap.py. + +This expression will return an array of structs, with 'lo_addr' and 'hi_addr' for each malloc region. +*/ + + +// The calling Python function replaces {{ MAX_MATCHES }} with an integer value. +#define MAX_MATCHES {{MAX_MATCHES}} + +#define KERN_SUCCESS 0 +/* For region containing pointers */ +#define MALLOC_PTR_REGION_RANGE_TYPE 2 + +// Store information about memory allocations. +typedef struct vm_range_t { + uintptr_t address; + unsigned long size; +} vm_range_t; + +// Function prototypes used for callback functions. +typedef void (*range_callback_t)(unsigned int task, void *baton, unsigned int type, uintptr_t ptr_addr, + uintptr_t ptr_size); + +typedef int (*memory_reader_t)(unsigned int task, uintptr_t remote_address, unsigned long size, void **local_memory); + +typedef void (*vm_range_recorder_t)(unsigned int task, void *baton, unsigned int type, vm_range_t *range, + unsigned int size); + +// We only care about the pointer to enumerator, which is the first pointer in the struct. +// Full definition of malloc_introspection_t available in libmalloc/blob/main/include/malloc/malloc.h +typedef struct malloc_introspection_t { + // Enumerates all the malloc pointers in use + int (*enumerator)(unsigned int task, void *, unsigned int type_mask, uintptr_t zone_address, memory_reader_t reader, + vm_range_recorder_t recorder); +} malloc_introspection_t; + +// We only care about the pointer to malloc_introspection_t which is the 13th pointer in the struct. +// Full definition of malloc_zone_t available in libmalloc/blob/main/include/malloc/malloc.h +typedef struct malloc_zone_t { + void *reserved1[12]; + struct malloc_introspection_t *introspect; +} malloc_zone_t; + +// Information about memory regions to be returned to LLEF. +struct malloc_region { + uintptr_t lo_addr; + uintptr_t hi_addr; +}; + +typedef struct callback_baton_t { + range_callback_t callback; + unsigned int num_matches; + malloc_region matches[MAX_MATCHES + 1]; // Null terminate +} callback_baton_t; + +// Memory read callback function. +memory_reader_t task_peek = [](unsigned int task, uintptr_t remote_address, uintptr_t size, + void **local_memory) -> int { + *local_memory = (void *)remote_address; + return KERN_SUCCESS; +}; + +// Callback to populate structure with low, high malloc addresses. +range_callback_t range_callback = [](unsigned int task, void *baton, unsigned int type, uintptr_t ptr_addr, + uintptr_t ptr_size) -> void { + callback_baton_t *lldb_info = (callback_baton_t *)baton; + // Upper limit for our array + if (lldb_info->num_matches < MAX_MATCHES) { + uintptr_t lo = ptr_addr; + uintptr_t hi = lo + ptr_size; + lldb_info->matches[lldb_info->num_matches].lo_addr = lo; + lldb_info->matches[lldb_info->num_matches].hi_addr = hi; + lldb_info->num_matches++; + } +}; + +// Callback function from introspect enumerator function. +vm_range_recorder_t range_recorder = [](unsigned int task, void *baton, unsigned int type, vm_range_t *ranges, + unsigned int size) -> void { + range_callback_t callback = ((callback_baton_t *)baton)->callback; + for (unsigned int i = 0; i < size; ++i) { + // Call range_callback to record each allocation in baton. + callback(task, baton, type, ranges[i].address, ranges[i].size); + } +}; + +uintptr_t *zones = 0; +unsigned int num_zones = 0; +unsigned int task = 0; + +// Populate zones with pointer to a malloc_zone_t array representing heap zones. +int err = (int)malloc_get_all_zones(task, task_peek, &zones, &num_zones); + +// baton struct used to store data on heap regions between callbacks. +callback_baton_t baton = {range_callback, 0, {0}}; + +if (KERN_SUCCESS == err) { + // Enumerate over all heap zones. + for (unsigned int i = 0; i < num_zones; ++i) { + const malloc_zone_t *zone = (const malloc_zone_t *)zones[i]; + /* Introspection API will call our callback for each heap region (rather than each allocation as in + * malloc_info) */ + if (zone && zone->introspect) + zone->introspect->enumerator(task, &baton, MALLOC_PTR_REGION_RANGE_TYPE, (uintptr_t)zone, task_peek, + range_recorder); + } +} +/* return the value */ +baton.matches \ No newline at end of file diff --git a/common/instruction_util.py b/common/instruction_util.py new file mode 100644 index 0000000..8928284 --- /dev/null +++ b/common/instruction_util.py @@ -0,0 +1,125 @@ +import re +from typing import List + +from lldb import SBAddress, SBInstruction, SBTarget + +from common.color_settings import LLEFColorSettings +from common.output_util import color_string, output_line + + +def extract_instructions( + target: SBTarget, start_address: int, end_address: int, disassembly_flavour: str +) -> List[SBInstruction]: + """ + Returns a list of instructions between a range of memory address defined by @start_address and @end_address. + + :param target: The target context. + :param start_address: The address to start reading instructions from memory. + :param end_address: The address to stop reading instruction from memory. + :return: A list of instructions. + """ + instructions = [] + current = start_address + while current <= end_address: + address = SBAddress(current, target) + instruction = target.ReadInstructions(address, 1, disassembly_flavour).GetInstructionAtIndex(0) + instructions.append(instruction) + instruction_size = instruction.GetByteSize() + if instruction_size > 0: + current += instruction_size + else: + break + + return instructions + + +def color_operands( + operands: str, + color_settings: LLEFColorSettings, +): + """ + Colors the registers and addresses in the instruction's operands. + + :param operands: A string of the instruction's operands returned from instruction.GetOperands(). + :param color_settings: Contains the color settings to color the instruction. + """ + + # Addresses can start with either '$0x', '#0x' or just '0x', followed by atleast one hex value. + address_pattern = r"(\$?|#?)-?0x[0-9a-fA-F]+" + + # Registers MAY start with '%'. + # Then there MUST be a sequence of letters, which CAN be followed by a number. + # A register can NEVER start with numbers or any other special character other than '%'. + register_pattern = r"(? None: + """ + Print formatted @instruction extracted from SBInstruction object. + + :param target: The target executable. + :param instruction: The instruction object. + :param base: The address base to calculate offsets from. + :param color_settings: Contains the color settings to color the instruction. + :param highlight: If true, highlight the whole instruction with the highlight color. + """ + + address = instruction.GetAddress().GetLoadAddress(target) + offset = address - base + + line = hex(address) + if offset >= 0: + line += f" <+{offset:02}>: " + else: + line += f" <-{abs(offset):02}>: " + + mnemonic = instruction.GetMnemonic(target) or "" + operands = instruction.GetOperands(target) or "" + comment = instruction.GetComment(target) or "" + + if not highlight: + operands = color_operands(operands, color_settings) + + if comment != "": + comment = f"; {comment}" + line += f"{mnemonic:<10}{operands:<30}{comment}" + + if highlight: + line = color_string(line, color_settings.highlighted_instruction_color) + + output_line(line) + + +def print_instructions( + target: SBTarget, + instructions: List[SBInstruction], + base: int, + color_settings: LLEFColorSettings, +) -> None: + """ + Print formatted @instructions extracting information from the SBInstruction objects. + + :param target: The target executable. + :param instructions: A list of instruction objects. + :param base: The address base to calculate offsets from. + :param color_settings: Contains the color settings to color the instruction. + """ + for instruction in instructions: + print_instruction(target, instruction, base, color_settings) diff --git a/common/output_util.py b/common/output_util.py new file mode 100644 index 0000000..f3a958d --- /dev/null +++ b/common/output_util.py @@ -0,0 +1,158 @@ +"""Utility functions related to terminal output.""" + +import re +import shutil +from textwrap import TextWrapper +from typing import Any + +from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, DEFAULT_TERMINAL_LINES, GLYPHS, MSG_TYPE, TERM_COLORS +from common.state import LLEFState + + +def color_string(string: str, color_setting: str, lwrap: str = "", rwrap: str = "") -> str: + """ + Colors a @string based on the @color_setting. + Optional: Wrap the string with uncolored strings @lwrap and @rwrap. + + :param string: The string to color. + :param color_setting: The color that will be fetched from TERM_COLORS (i.e., TERM_COLORS[color_setting]). + :param lwrap: Uncolored string prepended to the colored @string. + :param rwrap: Uncolored string appended to the colored @string. + :return: The resulting string. + """ + if color_setting is None: + result = f"{lwrap}{string}{rwrap}" + else: + result = f"{lwrap}{TERM_COLORS[color_setting].value}{string}{TERM_COLORS.ENDC.value}{rwrap}" + + return result + + +def terminal_columns() -> int: + """ + Returns the column width of the terminal. If this is not availble in the + terminal environment variables then DEFAULT_TERMINAL_COLUMNS we be returned. + """ + try: + columns = shutil.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + except OSError: + columns = DEFAULT_TERMINAL_COLUMNS + + return columns + + +def remove_color(string: str) -> str: + """Removes all ANSI color character sequences from string.""" + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", string) + + +def truncate_line(line: str) -> str: + """Truncates a line to fix within terminal width.""" + truncation_step = 10 + color_character_count = len(line) - len(remove_color(line)) + + w = TextWrapper( + width=terminal_columns() + color_character_count, + max_lines=1, + placeholder=f"{TERM_COLORS.ENDC.value}...", + ) + + while len(remove_color(line)) > terminal_columns(): + w.width -= truncation_step + line = w.fill(line) + + return line + + +def output_line(line: Any, never_truncate: bool = False) -> None: + """ + Format a line of output for printing. Print should not be used elsewhere. + Exception - clear_page would not function without terminal characters + """ + + line = str(line) + if LLEFState().use_color is False: + line = remove_color(line) + + if LLEFState().truncate_output and not never_truncate: + for s_line in line.split("\n"): + print(truncate_line(s_line)) + else: + print(line) + + +def clear_page() -> None: + """ + Used to clear the previously printed breakpoint information before + printing the next information. + """ + try: + num_lines = shutil.get_terminal_size().lines + except OSError: + num_lines = DEFAULT_TERMINAL_LINES + + for _ in range(num_lines): + print() + print("\033[0;0H") # Ansi escape code: Set cursor to 0,0 position + print("\033[J") # Ansi escape code: Clear contents from cursor to end of screen + + +def print_line_with_string( + string: str, + char: GLYPHS = GLYPHS.HORIZONTAL_LINE, + line_color: str = TERM_COLORS.GREY.name, + string_color: str = TERM_COLORS.BLUE.name, + align: ALIGN = ALIGN.RIGHT, +) -> None: + """ + Print a line with the provided @string padded with @char. + + :param string: The string to be embedded in the line. + :param char: The character that the line consist of. + :param line_color: The color setting to define the color of the line. + :param string_color: The color setting to define the color of the embedded string. + :align: Defines where the string will be embedded in the line. + """ + width = terminal_columns() + if align == ALIGN.RIGHT: + l_pad = (width - len(string) - 6) * char.value + r_pad = 4 * char.value + + elif align == ALIGN.CENTRE: + l_pad = (width - len(string)) * char.value + r_pad = 4 * char.value + + else: # align == ALIGN.LEFT: + l_pad = 4 * char.value + r_pad = (width - len(string) - 6) * char.value + + line = color_string(l_pad, line_color) + line += color_string(string, string_color, " ", " ") + line += color_string(r_pad, line_color) + + output_line(line, never_truncate=True) + + +def print_line(char: GLYPHS = GLYPHS.HORIZONTAL_LINE, color: str = TERM_COLORS.GREY.name) -> None: + """Print a line of @char""" + line = color_string(terminal_columns() * char.value, color) + output_line(line, never_truncate=True) + + +def print_message(msg_type: MSG_TYPE, message: str) -> None: + """Format, color and print a @message based on its @msg_type.""" + info_color = TERM_COLORS.BLUE.name + success_color = TERM_COLORS.GREEN.name + error_color = TERM_COLORS.RED.name + + if msg_type == MSG_TYPE.INFO: + message = color_string("[i] ", info_color, rwrap=message) + elif msg_type == MSG_TYPE.SUCCESS: + message = color_string("[+] ", success_color, rwrap=message) + elif msg_type == MSG_TYPE.ERROR: + message = color_string("[-] ", error_color, rwrap=message) + else: + raise KeyError(f"{msg_type} is an invalid MSG_TYPE.") + + output_line(message, never_truncate=True) diff --git a/common/settings.py b/common/settings.py index 380b728..a5e5669 100644 --- a/common/settings.py +++ b/common/settings.py @@ -1,12 +1,14 @@ """Global settings module""" + import os +from lldb import SBDebugger + from arch import supported_arch -from common.singleton import Singleton from common.base_settings import BaseLLEFSettings -from common.util import change_use_color, output_line - -from lldb import SBDebugger +from common.constants import MSG_TYPE +from common.output_util import output_line, print_message +from common.singleton import Singleton class LLEFSettings(BaseLLEFSettings, metaclass=Singleton): @@ -14,8 +16,9 @@ class LLEFSettings(BaseLLEFSettings, metaclass=Singleton): Global general settings class - loaded from file defined in `LLEF_CONFIG_PATH` """ - LLEF_CONFIG_PATH = os.path.join(os.path.expanduser('~'), ".llef") + LLEF_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".llef") GLOBAL_SECTION = "LLEF" + DEFAUL_OUTPUT_ORDER = "registers,stack,code,threads,trace" debugger: SBDebugger = None @property @@ -70,6 +73,32 @@ def rebase_offset(self): def show_all_registers(self): return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_all_registers", fallback=False) + @property + def output_order(self): + return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "output_order", fallback=self.DEFAUL_OUTPUT_ORDER) + + @property + def truncate_output(self): + return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "truncate_output", fallback=True) + + @property + def enable_darwin_heap_scan(self): + return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "enable_darwin_heap_scan", fallback=False) + + def validate_output_order(self, value: str): + default_sections = self.DEFAUL_OUTPUT_ORDER.split(",") + sections = value.split(",") + if len(sections) != len(default_sections): + raise ValueError(f"Requires {len(default_sections)} elements: '{','.join(default_sections)}'") + + missing_sections = [] + for section in default_sections: + if section not in sections: + missing_sections.append(section) + + if len(missing_sections) > 0: + raise ValueError(f"Missing '{','.join(missing_sections)}' from output order.") + def validate_settings(self, setting=None) -> bool: """ Validate settings by attempting to retrieve all properties thus executing any ConfigParser coverters @@ -92,12 +121,13 @@ def validate_settings(self, setting=None) -> bool: and self.debugger is not None and self.debugger.GetUseColor() is False ): - print("Colour is not supported by your terminal") - raise ValueError - except ValueError: + raise ValueError("Colour is not supported by your terminal") + + elif setting_name == "output_order": + self.validate_output_order(value) + except ValueError as e: valid = False - raw_value = self._RAW_CONFIG.get(self.GLOBAL_SECTION, setting_name) - output_line(f"Error parsing setting {setting_name}. Invalid value '{raw_value}'") + print_message(MSG_TYPE.ERROR, f"Invalid value for {setting_name}. {e}") return valid def __init__(self, debugger: SBDebugger): @@ -108,8 +138,11 @@ def set(self, setting: str, value: str): super().set(setting, value) if setting == "color_output": - change_use_color(self.color_output) + self.state.change_use_color(self.color_output) + elif setting == "truncate_output": + self.state.change_truncate_output(self.truncate_output) def load(self, reset=False): super().load(reset) - change_use_color(self.color_output) + self.state.change_use_color(self.color_output) + self.state.change_truncate_output(self.truncate_output) diff --git a/common/singleton.py b/common/singleton.py index 29ce238..947a249 100644 --- a/common/singleton.py +++ b/common/singleton.py @@ -5,6 +5,7 @@ class Singleton(type): """ Singleton class implementation. Use with metaclass=Singleton. """ + _instances = {} def __call__(cls, *args, **kwargs): diff --git a/common/state.py b/common/state.py index 6b5b3ed..0a742ff 100644 --- a/common/state.py +++ b/common/state.py @@ -1,4 +1,5 @@ """Global state module""" + from typing import Dict from common.singleton import Singleton @@ -20,3 +21,24 @@ class LLEFState(metaclass=Singleton): # Stores whether color should be used use_color = False + + # Stores whether output lines should be truncated + truncate_output = True + + # Stores version of LLDB if on Linux. Stores clang verion if on Mac + version = [] + + # Linux, Mac (Darwin) or Windows + platform = "" + + disassembly_syntax = "" + + def change_use_color(self, new_value: bool) -> None: + """ + Change the global use_color bool. use_color should not be written to directly + """ + self.use_color = new_value + + def change_truncate_output(self, new_value: bool) -> None: + """Change the global truncate_output bool.""" + self.truncate_output = new_value diff --git a/common/util.py b/common/util.py index e687079..4325f72 100644 --- a/common/util.py +++ b/common/util.py @@ -1,101 +1,61 @@ """Utility functions.""" -from typing import List, Any -import re +import os import shutil - -from lldb import SBError, SBFrame, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, SBValue - -from common.constants import ALIGN, GLYPHS, MSG_TYPE, TERM_COLORS +from argparse import ArgumentTypeError +from typing import List, Tuple + +from lldb import ( + SBAddress, + SBError, + SBExecutionContext, + SBExpressionOptions, + SBFrame, + SBMemoryRegionInfo, + SBMemoryRegionInfoList, + SBProcess, + SBTarget, + SBValue, + eLanguageTypeObjC_plus_plus, + eNoDynamicValues, + value, +) + +from common.constants import DEFAULT_TERMINAL_COLUMNS, MAGIC_BYTES, MSG_TYPE, TERM_COLORS +from common.output_util import print_message from common.state import LLEFState -def change_use_color(new_value: bool) -> None: - """ - Change the global use_color bool. use_color should not be written to directly - """ - LLEFState.use_color = new_value +def terminal_columns() -> int: + return shutil.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS -def output_line(line: Any) -> None: +def address_to_filename(target: SBTarget, address: int) -> str: """ - Format a line of output for printing. Print should not be used elsewhere. - Exception - clear_page would not function without terminal characters - """ - line = str(line) - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') - if LLEFState.use_color is False: - line = ansi_escape.sub('', line) - print(line) - + Maps a memory address to its corresponding executable/library and returns the filename. -def clear_page() -> None: - """ - Used to clear the previously printed breakpoint information before - printing the next information. + :param target: The target context. + :param address: The memory address to resolve. + :return: The filename. """ - num_lines = shutil.get_terminal_size().lines - for _ in range(num_lines): - print() - print("\033[0;0H") # Ansi escape code: Set cursor to 0,0 position - print("\033[J") # Ansi escape code: Clear contents from cursor to end of screen - - -def print_line_with_string( - string: str, - char: GLYPHS = GLYPHS.HORIZONTAL_LINE, - line_color: TERM_COLORS = TERM_COLORS.GREY, - string_color: TERM_COLORS = TERM_COLORS.BLUE, - align: ALIGN = ALIGN.RIGHT, -) -> None: - """Print a line with the provided @string padded with @char""" - width = shutil.get_terminal_size().columns - if align == ALIGN.RIGHT: - l_pad = (width - len(string) - 6) * char.value - r_pad = 4 * char.value - - elif align == ALIGN.CENTRE: - l_pad = (width - len(string)) * char.value - r_pad = 4 * char.value - - elif align == ALIGN.LEFT: - l_pad = 4 * char.value - r_pad = (width - len(string) - 6) * char.value - - output_line( - f"{line_color.value}{l_pad}{TERM_COLORS.ENDC.value} " - + f"{string_color.value}{string}{TERM_COLORS.ENDC.value} {line_color.value}{r_pad}{TERM_COLORS.ENDC.value}" - ) - - -def print_line( - char: GLYPHS = GLYPHS.HORIZONTAL_LINE, color: TERM_COLORS = TERM_COLORS.GREY -) -> None: - """Print a line of @char""" - output_line( - f"{color.value}{shutil.get_terminal_size().columns*char.value}{TERM_COLORS.ENDC.value}" - ) + sb_address = SBAddress(address, target) + module = sb_address.GetModule() + file_spec = module.GetSymbolFileSpec() + filename = file_spec.GetFilename() + return filename -def print_message(msg_type: MSG_TYPE, message: str) -> None: - """Format and print a @message""" - info_color = TERM_COLORS.BLUE - success_color = TERM_COLORS.GREEN - error_color = TERM_COLORS.GREEN - if msg_type == MSG_TYPE.INFO: - output_line(f"{info_color.value}[+]{TERM_COLORS.ENDC.value} {message}") - elif msg_type == MSG_TYPE.SUCCESS: - output_line(f"{success_color.value}[+]{TERM_COLORS.ENDC.value} {message}") - elif msg_type == MSG_TYPE.ERROR: - output_line(f"{error_color.value}[+]{TERM_COLORS.ENDC.value} {message}") +def get_frame_range(frame: SBFrame, target: SBTarget) -> Tuple[str, str]: + function = frame.GetFunction() + if function: + start_address = function.GetStartAddress().GetLoadAddress(target) + end_address = function.GetEndAddress().GetLoadAddress(target) + else: + start_address = frame.GetSymbol().GetStartAddress().GetLoadAddress(target) + end_address = frame.GetSymbol().GetEndAddress().GetLoadAddress(target) - 1 - -def print_instruction(line: str, color: TERM_COLORS = TERM_COLORS.ENDC) -> None: - """Format and print a line of disassembly returned from LLDB (SBFrame.disassembly)""" - loc_0x = line.find("0x") - start_idx = loc_0x if loc_0x >= 0 else 0 - output_line(f"{color.value}{line[start_idx:]}{TERM_COLORS.ENDC.value}") + return start_address, end_address def get_registers(frame: SBFrame, frame_type: str) -> List[SBValue]: @@ -128,15 +88,11 @@ def get_frame_arguments(frame: SBFrame, frame_argument_name_color: TERM_COLORS) value = f"{int(var.GetValue(), 0):#x}" except ValueError: pass - args.append( - f"{frame_argument_name_color.value}{var.GetName()}{TERM_COLORS.ENDC.value}={value}" - ) + args.append(f"{frame_argument_name_color.value}{var.GetName()}{TERM_COLORS.ENDC.value}={value}") return f"({' '.join(args)})" -def attempt_to_read_string_from_memory( - process: SBProcess, addr: SBValue, buffer_size: int = 256 -) -> str: +def attempt_to_read_string_from_memory(process: SBProcess, addr: SBValue, buffer_size: int = 256) -> str: """ Returns a string from a memory address if one can be read, else an empty string """ @@ -144,7 +100,7 @@ def attempt_to_read_string_from_memory( ret_string = "" try: string = process.ReadCStringFromMemory(addr, buffer_size, err) - if err.Success(): + if err.Success() and string.isprintable(): ret_string = string except SystemError: # This swallows an internal error that is sometimes generated by a bug in LLDB. @@ -152,41 +108,351 @@ def attempt_to_read_string_from_memory( return ret_string -def is_code(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoList) -> bool: +def is_ascii_string(address: SBValue, process: SBProcess) -> bool: + """ + Determines if a given memory @address contains a readable string. + + :param address: The memory address to read. + :param process: A running process of the target. + :return: A boolean of the check. + """ + return attempt_to_read_string_from_memory(process, address) != "" + + +def is_in_section(address: SBValue, target: SBTarget, target_section_name: str) -> bool: + """ + Determines whether a given memory @address exists within a @section of the executable file @target. + + The section's parents are searched to generate a full section name (e.g., __TEXT.__c_string). + + :param address: The memory address to check. + :param target: The target object file. + :param section: The section of the executable file. + :return: A boolean of the check. + """ + + sb_address = target.ResolveLoadAddress(address) + section = sb_address.GetSection() + full_section_name = "" + while section: + full_section_name = section.GetName() + "." + full_section_name + section = section.GetParent() + + return target_section_name in full_section_name + + +def is_text_region(address: SBValue, target: SBTarget, region: SBMemoryRegionInfo) -> bool: + """ + Determines if a given memory @address if within a '.text' section of the target executable. + + :param address: The memory address to check. + :param target: The target object file. + :param region: The memory region that the address exists in. + :return: A boolean of the check. + """ + + in_text = False + if is_file(target, MAGIC_BYTES.MACH.value): + if is_in_section(address, target, "__TEXT"): + in_text = True + else: + file = target.GetExecutable() + if is_in_section(address, target, ".text") or ( + file.GetFilename() in region.GetName() and file.GetDirectory() in region.GetName() + ): + in_text = True + + return in_text + + +def is_code(address: SBValue, process: SBProcess, target: SBTarget, regions: SBMemoryRegionInfoList) -> bool: """Determines whether an @address points to code""" - if regions is None: - return False region = SBMemoryRegionInfo() code_bool = False - if regions.GetMemoryRegionContainingAddress(address, region): - code_bool = region.IsExecutable() + if regions is not None and regions.GetMemoryRegionContainingAddress(address, region): + code_bool = region.IsExecutable() and is_text_region( + address, target, region + ) # and not is_ascii_string(address, process) return code_bool -def is_stack(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoList) -> bool: +def is_stack(address: SBValue, regions: SBMemoryRegionInfoList, darwin_stack_regions: List[SBMemoryRegionInfo]) -> bool: """Determines whether an @address points to the stack""" - if regions is None: - return False - region = SBMemoryRegionInfo() + stack_bool = False - if regions.GetMemoryRegionContainingAddress(address, region): - if region.GetName() == "[stack]": + region = SBMemoryRegionInfo() + if regions is not None and regions.GetMemoryRegionContainingAddress(address, region): + if LLEFState.platform == "Darwin" and region in darwin_stack_regions: + stack_bool = True + elif region.GetName() == "[stack]": stack_bool = True + return stack_bool -def is_heap(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoList) -> bool: +def is_heap( + address: SBValue, + target: SBTarget, + regions: SBMemoryRegionInfoList, + stack_regions: List[SBMemoryRegionInfo], + darwin_heap_regions: List[Tuple[int, int]], +) -> bool: """Determines whether an @address points to the heap""" - if regions is None: - return False - region = SBMemoryRegionInfo() heap_bool = False - if regions.GetMemoryRegionContainingAddress(address, region): - if region.GetName() == "[heap]": - heap_bool = True + + if darwin_heap_regions is not None: + # Only set when platform is Darwin (iOS, MacOS, etc) and darwin heap scan is enabled in settings. + for lo, hi in darwin_heap_regions: + if address >= lo and address < hi: + heap_bool = True + else: + region = SBMemoryRegionInfo() + if regions is not None and regions.GetMemoryRegionContainingAddress(address, region): + if LLEFState.platform == "Darwin": + sb_address = SBAddress(address, target) + filename = sb_address.GetModule().GetFileSpec().GetFilename() + if filename is None and not is_stack(address, regions, stack_regions) and region.IsWritable(): + heap_bool = True + elif region.GetName() == "[heap]": + heap_bool = True return heap_bool def extract_arch_from_triple(triple: str) -> str: """Extracts the architecture from triple string.""" return triple.split("-")[0] + + +def verify_version(version: List[int], target_version: List[int]) -> bool: + """Checks if the @version is greater than or equal to the @target_version.""" + length_difference = len(target_version) - len(version) + if length_difference > 0: + version += [0] * length_difference + elif length_difference < 0: + target_version += [0] * abs(length_difference) + + return version >= target_version + + +def lldb_version_to_clang(lldb_version): + """ + Convert an LLDB version to its corresponding Clang version. + + :param lldb_version: The LLDB version. + :return: The Clang version. + """ + + clang_version = [0] + if verify_version(lldb_version, [17, 0, 6]): + clang_version = [1600, 0, 26, 3] + elif verify_version(lldb_version, [16, 0, 0]): + clang_version = [1500, 0, 40, 1] + elif verify_version(lldb_version, [15, 0, 0]): + clang_version = [1403, 0, 22, 14, 1] + + return clang_version + + +def check_version(required_version_string): + def inner(func): + def wrapper(*args, **kwargs): + required_version = [int(x) for x in required_version_string.split(".")] + if LLEFState.platform == "Darwin": + required_version = lldb_version_to_clang(required_version) + if not verify_version(LLEFState.version, required_version): + print(f"error: requires LLDB version {required_version_string} to execute") + return + return func(*args, **kwargs) + + return wrapper + + return inner + + +def check_process(func): + """ + Checks that there's a running process before executing the wrapped function. Only to be used on + overrides of `__call__`. + + :param func: Wrapped function to be executed after successful check. + """ + + def wrapper(*args, **kwargs): + for arg in list(args) + list(kwargs.values()): + if isinstance(arg, SBExecutionContext): + if arg.process.is_alive: + return func(*args, **kwargs) + + print_message(MSG_TYPE.ERROR, "Requires a running process.") + return + + print_message(MSG_TYPE.ERROR, "Execution context not found.") + + return wrapper + + +def check_target(func): + def wrapper(*args, **kwargs): + for arg in list(args) + list(kwargs.values()): + if isinstance(arg, SBExecutionContext): + if arg.target.IsValid(): + return func(*args, **kwargs) + + print_message(MSG_TYPE.ERROR, "Requires a valid target.") + return + + print_message(MSG_TYPE.ERROR, "Execution context not found.") + + return wrapper + + +def is_file(target: SBTarget, expected_magic_bytes: List[bytes]): + """Read signature of @target file and compare to expected magic bytes.""" + magic_bytes = read_program(target, 0, 4) + return magic_bytes in expected_magic_bytes + + +def check_elf(func): + def wrapper(*args, **kwargs): + for arg in list(args) + list(kwargs.values()): + if isinstance(arg, SBExecutionContext): + try: + if is_file(arg.target, MAGIC_BYTES.ELF.value): + return func(*args, **kwargs) + else: + print_message(MSG_TYPE.ERROR, "Target must be an ELF file.") + return + except MemoryError: + print_message(MSG_TYPE.ERROR, "couldn't determine file type") + return + + print_message(MSG_TYPE.ERROR, "Execution context not found.") + + return wrapper + + +def hex_int(x): + """A converter for input arguments in different bases to ints. + For base 0, the base is determined by the prefix. So, numbers starting `0x` are hex + and numbers with no prefix are decimal. Base 0 also disallows leading zeros. + """ + return int(x, 0) + + +def positive_int(x): + """A converter for input arguments in different bases to positive ints""" + x = hex_int(x) + if x <= 0: + raise ArgumentTypeError("Must be positive") + return x + + +def hex_or_str(x): + """Convert to formatted hex if an integer, otherwise return the value.""" + if isinstance(x, int): + return f"0x{x:016x}" + + return x + + +def read_program(target: SBTarget, offset: int, n: int): + """ + Read @n bytes from a given @offset from the start of @target object file. + + :param target: The target object file. + :param offset: The byte offset of the file to start reading from. + :param n: The number of bytes to read from the offset. + :return: The read bytes convert to an integer with little endianness. + """ + + error = SBError() + # Executable has always been observed at module 0, but isn't specifically stated in docs. + program_module = target.GetModuleAtIndex(0) + address = program_module.GetObjectFileHeaderAddress() + address.OffsetAddress(offset) + data = target.ReadMemory(address, n, error) + + if error.Fail(): + raise MemoryError(f"Couldn't read memory at file offset {hex(address.GetOffset())}.") + + return data + + +def read_program_int(target: SBTarget, offset: int, n: int): + """ + Read @n bytes from a given @offset from the start of @target object file, + and convert to integer by little endian. + + :param target: The target object file. + :param offset: The byte offset of the file to start reading from. + :param n: The number of bytes to read from the offset. + :return: The read bytes convert to an integer with little endianness. + """ + + data = read_program(target, offset, n) + return int.from_bytes(data, "little") + + +def find_stack_regions(process: SBProcess) -> List[SBMemoryRegionInfo]: + """ + Find all memory regions containing the stack by looping through stack pointers in each frame. + + :return: A list of memory region objects. + """ + stack_regions = [] + for frame in process.GetSelectedThread().frames: + sp = frame.GetSP() + region = SBMemoryRegionInfo() + process.GetMemoryRegionInfo(sp, region) + stack_regions.append(region) + + return stack_regions + + +def find_darwin_heap_regions(process: SBProcess) -> List[Tuple[int, int]]: + """ + Find memory heap regions on Darwin. + + :return: List[Tuple[int, int]]: A list containing values for min and max ranges for heap regions on Darwin. + """ + + MAX_MATCHES = 128 + + # Define Objective C++ code to be run as an LLDB expression. + + # Read template file, replace MAX_MATCHES value. + common_dir = os.path.dirname(os.path.abspath(__file__)) + expr_file_path = os.path.join(common_dir, "expressions", "darwin_get_malloc_zones.mm") + + with open(expr_file_path, "r") as expr_file: + expr = expr_file.read().replace("{{MAX_MATCHES}}", str(MAX_MATCHES)) + + # Return SBFrame stack frame object from current thread. + frame = process.GetSelectedThread().GetSelectedFrame() + + # Set options for evaluating Objective C++ code. + expr_options = SBExpressionOptions() + expr_options.SetIgnoreBreakpoints(True) + expr_options.SetFetchDynamicValue(eNoDynamicValues) + # Set a 3 second timeout. + expr_options.SetTimeoutInMicroSeconds(3 * 1000 * 1000) + expr_options.SetTryAllThreads(False) + expr_options.SetLanguage(eLanguageTypeObjC_plus_plus) + + expr_sbvalue = frame.EvaluateExpression(expr, expr_options) + match_value = value(expr_sbvalue) + heap_regions = [] + + # Populate heap regions from expression result. + if expr_sbvalue.error.Success(): + for count in range(MAX_MATCHES): + match_entry = match_value[count] + lo_addr = match_entry.lo_addr.sbvalue.unsigned + hi_addr = match_entry.hi_addr.sbvalue.unsigned + if lo_addr != 0: + heap_regions.append((lo_addr, hi_addr)) + else: + # Fallback to default way to calculate heap regions in error condition. + heap_regions = None + + return heap_regions diff --git a/handlers/stop_hook.py b/handlers/stop_hook.py index d21d6a9..2ff8aca 100644 --- a/handlers/stop_hook.py +++ b/handlers/stop_hook.py @@ -1,13 +1,8 @@ """Break point handler.""" + from typing import Any, Dict -from lldb import ( - SBDebugger, - SBExecutionContext, - SBStream, - SBStructuredData, - SBTarget, -) +from lldb import SBDebugger, SBExecutionContext, SBStream, SBStructuredData, SBTarget from common.context_handler import ContextHandler @@ -24,9 +19,7 @@ def lldb_self_register(cls, debugger: SBDebugger, module_name: str) -> None: command = f"target stop-hook add -P {module_name}.{cls.__name__}" debugger.HandleCommand(command) - def __init__( - self, target: SBTarget, _: SBStructuredData, __: Dict[Any, Any] - ) -> None: + def __init__(self, target: SBTarget, _: SBStructuredData, __: Dict[Any, Any]) -> None: """ For up to date documentation on args provided to this function run: `help target stop-hook add` """ diff --git a/llef.py b/llef.py index d96ae39..7f3adf4 100644 --- a/llef.py +++ b/llef.py @@ -10,21 +10,23 @@ # The __lldb_init_module function automatically loads the stop-hook-handler # --------------------------------------------------------------------- +import platform from typing import Any, Dict, List, Type, Union from lldb import SBDebugger from commands.base_command import BaseCommand from commands.base_container import BaseContainer -from commands.pattern import ( - PatternContainer, - PatternCreateCommand, - PatternSearchCommand, -) -from commands.context import ContextCommand -from commands.settings import SettingsCommand +from commands.checksec import ChecksecCommand from commands.color_settings import ColorSettingsCommand +from commands.context import ContextCommand +from commands.dereference import DereferenceCommand from commands.hexdump import HexdumpCommand +from commands.pattern import PatternContainer, PatternCreateCommand, PatternSearchCommand +from commands.scan import ScanCommand +from commands.settings import SettingsCommand +from commands.xinfo import XinfoCommand +from common.state import LLEFState from handlers.stop_hook import StopHookHandler @@ -36,7 +38,11 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: ContextCommand, SettingsCommand, ColorSettingsCommand, - HexdumpCommand + HexdumpCommand, + ChecksecCommand, + XinfoCommand, + DereferenceCommand, + ScanCommand, ] handlers = [StopHookHandler] @@ -46,3 +52,11 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: for handler in handlers: handler.lldb_self_register(debugger, "llef") + + LLEFState.platform = platform.system() + if LLEFState.platform == "Darwin": + # Getting Clang version (e.g. lldb-1600.0.36.3) + LLEFState.version = [int(x) for x in debugger.GetVersionString().split()[0].split("-")[1].split(".")] + else: + # Getting LLDB version (e.g. lldb version 16.0.0) + LLEFState.version = [int(x) for x in debugger.GetVersionString().split("version")[1].split()[0].split(".")] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..db8e3fc --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +envlist = py39, py310, py311, py312, py313 + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + +[testenv] +deps = + isort + black + flake8 + # mypy + # pydocstyle + # pylint +commands = + isort -c --line-length=120 --profile black {toxinidir} + black --check --line-length=120 {toxinidir} + flake8 --max-line-length=120 --ignore=E203,W503 {toxinidir} + # mypy --follow-imports=silent --ignore-missing-imports --show-column-numbers --no-pretty --strict {toxinidir} + # pydocstyle --count {toxinidir} + # pylint **/*.py