From 03c1387c992b880fe0f8a75d0112a29dcc0df3f1 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:28:10 +0000 Subject: [PATCH 01/32] Add automatic CI linting to GitHub actions --- .github/workflows/style.yml | 84 +++++++++++++++++++++++++++++++++++++ .isort.cfg | 3 ++ .mypy.ini | 8 ++++ .pydocstyle | 2 + .pylintrc | 2 + arch/__init__.py | 5 ++- arch/aarch64.py | 4 +- arch/arm.py | 4 +- arch/base_arch.py | 1 + arch/i386.py | 3 +- arch/ppc.py | 13 ++---- arch/x86_64.py | 3 +- commands/base_command.py | 4 +- commands/base_container.py | 5 ++- commands/base_settings.py | 3 +- commands/color_settings.py | 3 +- commands/context.py | 10 +---- commands/hexdump.py | 22 ++++------ commands/pattern.py | 10 ++--- commands/settings.py | 3 +- common/base_settings.py | 6 ++- common/color_settings.py | 12 +++--- common/context_handler.py | 65 +++++++++------------------- common/settings.py | 9 ++-- common/singleton.py | 1 + common/state.py | 1 + common/util.py | 24 ++++------- handlers/stop_hook.py | 13 ++---- llef.py | 12 ++---- tox.ini | 25 +++++++++++ 30 files changed, 214 insertions(+), 146 deletions(-) create mode 100644 .github/workflows/style.yml create mode 100644 .isort.cfg create mode 100644 .mypy.ini create mode 100644 .pydocstyle create mode 100644 .pylintrc create mode 100644 tox.ini diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..59acaed --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,84 @@ +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.0 + 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: flake8 Lint + uses: py-actions/flake8@v2 + 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.0-rc.1'] + + 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/.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/arch/__init__.py b/arch/__init__.py index 5837e9d..a6f86f4 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,8 +8,8 @@ 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 @@ -23,7 +24,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..9433651 100644 --- a/arch/ppc.py +++ b/arch/ppc.py @@ -1,4 +1,5 @@ """PowerPC architecture definition.""" + from arch.base_arch import BaseArch, FlagRegister @@ -38,14 +39,6 @@ class PPC(BaseArch): "carry": 0x20000000, } - _cr_register_bit_masks = { - "cr0_lt": 0x80000000, - "cr0_gt": 0x40000000, - "cr0_eq": 0x20000000, - "cr0_so": 0x10000000 - } + _cr_register_bit_masks = {"cr0_lt": 0x80000000, "cr0_gt": 0x40000000, "cr0_eq": 0x20000000, "cr0_so": 0x10000000} - flag_registers = [ - FlagRegister("cr", _cr_register_bit_masks), - FlagRegister("xer", _xer_register_bit_masks) - ] + flag_registers = [FlagRegister("cr", _cr_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..5b93efa 100644 --- a/commands/base_command.py +++ b/commands/base_command.py @@ -52,8 +52,6 @@ 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) 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..ded6b08 100644 --- a/commands/base_settings.py +++ b/commands/base_settings.py @@ -1,8 +1,9 @@ """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 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..c9e83b0 100644 --- a/commands/context.py +++ b/commands/context.py @@ -1,13 +1,10 @@ """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 @@ -30,10 +27,7 @@ def get_command_parser(cls) -> argparse.ArgumentParser: """Get the command parser.""" parser = argparse.ArgumentParser(description="context command") parser.add_argument( - "sections", - nargs="*", - choices=["registers", "stack", "code", "threads", "trace", "all"], - default="all" + "sections", nargs="*", choices=["registers", "stack", "code", "threads", "trace", "all"], default="all" ) return parser diff --git a/commands/hexdump.py b/commands/hexdump.py index c9818bf..305fe1d 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,8 @@ 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 class HexdumpCommand(BaseCommand): @@ -27,21 +28,16 @@ def get_command_parser(cls) -> argparse.ArgumentParser: """Get the command parser.""" parser = argparse.ArgumentParser() parser.add_argument( - "type", - choices=["qword", "dword", "word", "byte"], - default="byte", - help="The format for presenting data" + "type", choices=["qword", "dword", "word", "byte"], default="byte", help="The format for presenting data" + ) + parser.add_argument( + "--reverse", action="store_true", help="The direction of output lines. Low to high by default" ) parser.add_argument( - "--reverse", - action="store_true", - help="The direction of output lines. Low to high by default" + "--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" + "address", type=hex_int, help="A value/address/symbol used as the location to print the hexdump from" ) return parser @@ -71,7 +67,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 diff --git a/commands/pattern.py b/commands/pattern.py index 8dfb317..fe79c29 100644 --- a/commands/pattern.py +++ b/commands/pattern.py @@ -13,7 +13,7 @@ from common.constants import MSG_TYPE, TERM_COLORS from common.de_bruijn import generate_cyclic_pattern from common.state import LLEFState -from common.util import print_message, output_line +from common.util import output_line, print_message 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/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/common/base_settings.py b/common/base_settings.py index eae4fd6..d09a1ee 100644 --- a/common/base_settings.py +++ b/common/base_settings.py @@ -1,8 +1,9 @@ """A base class for global settings""" + import configparser import os - from abc import abstractmethod + from common.singleton import Singleton from common.util import output_line @@ -11,7 +12,8 @@ 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() diff --git a/common/color_settings.py b/common/color_settings.py index 2adbefe..e8033b6 100644 --- a/common/color_settings.py +++ b/common/color_settings.py @@ -1,12 +1,11 @@ """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.constants import TERM_COLORS +from common.singleton import Singleton from common.util import output_line @@ -14,7 +13,8 @@ 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,7 +90,7 @@ 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() diff --git a/common/context_handler.py b/common/context_handler.py index 82373cf..d8fa890 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -1,39 +1,28 @@ import os - -from typing import Dict, Type, Optional from string import printable +from typing import Optional, Type -from lldb import ( - SBAddress, - SBDebugger, - SBError, - SBExecutionContext, - SBFrame, - SBProcess, - SBTarget, - SBThread, - SBValue, -) +from lldb import SBAddress, SBDebugger, SBError, SBExecutionContext, SBFrame, SBProcess, SBTarget, SBThread, SBValue 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.settings import LLEFSettings -from common.color_settings import LLEFColorSettings from common.state import LLEFState from common.util import ( attempt_to_read_string_from_memory, + change_use_color, clear_page, get_frame_arguments, get_registers, is_code, is_heap, is_stack, + output_line, print_instruction, print_line, print_line_with_string, - change_use_color, - output_line ) @@ -91,9 +80,7 @@ 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 - ) + 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}" @@ -101,17 +88,15 @@ def generate_printable_line_from_pointer( f"{TERM_COLORS.ENDC.value}" ) - 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[self.color_settings.string_color].value}" + f"{referenced_string}" f'{TERM_COLORS.ENDC.value}"?)' ) @@ -149,9 +134,7 @@ 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: @@ -166,7 +149,7 @@ def print_memory_address(self, addr: int, offset: int, size: int) -> None: # Add value to line err = SBError() - memory_value = int.from_bytes(self.process.ReadMemory(addr, size, err), 'little') + memory_value = int.from_bytes(self.process.ReadMemory(addr, size, err), "little") if err.Success(): line += f"0x{memory_value:0{size * 2}x}" else: @@ -246,10 +229,7 @@ def print_flags_register(self, flag_register: FlagRegister) -> None: 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() - ] + [name.upper() if flag_value & bitmask else name for name, bitmask in flag_register.bit_masks.items()] ) line += "]" output_line(line) @@ -284,7 +264,7 @@ def display_registers(self) -> None: print_line_with_string( "registers", line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + string_color=TERM_COLORS[self.color_settings.section_header_color], ) if self.settings.show_all_registers: @@ -311,7 +291,7 @@ 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] + string_color=TERM_COLORS[self.color_settings.section_header_color], ) for inc in range(0, self.arch().bits, 8): stack_pointer = self.frame.GetSP() @@ -325,7 +305,7 @@ 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] + string_color=TERM_COLORS[self.color_settings.section_header_color], ) if self.frame.disassembly: @@ -333,7 +313,7 @@ def display_code(self) -> None: current_pc = hex(self.frame.GetPC()) for i, item in enumerate(instructions): - if current_pc in item.split(':')[0]: + 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]) @@ -375,7 +355,7 @@ def display_threads(self) -> None: print_line_with_string( "threads", line_color=TERM_COLORS[self.color_settings.line_color], - string_color=TERM_COLORS[self.color_settings.section_header_color] + string_color=TERM_COLORS[self.color_settings.section_header_color], ) for thread in self.process: output_line(thread) @@ -387,7 +367,7 @@ 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] + string_color=TERM_COLORS[self.color_settings.section_header_color], ) for i in range(self.thread.GetNumFrames()): @@ -416,8 +396,7 @@ def display_trace(self) -> None: ) 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) @@ -438,11 +417,7 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: else: self.regions = None - def display_context( - self, - exe_ctx: SBExecutionContext, - update_registers: bool - ) -> 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. diff --git a/common/settings.py b/common/settings.py index 380b728..ea3bed4 100644 --- a/common/settings.py +++ b/common/settings.py @@ -1,20 +1,21 @@ """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.singleton import Singleton from common.util import change_use_color, output_line -from lldb import SBDebugger - 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" debugger: SBDebugger = None 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..cb023c3 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 diff --git a/common/util.py b/common/util.py index 4132e67..52bdc2d 100644 --- a/common/util.py +++ b/common/util.py @@ -1,8 +1,8 @@ """Utility functions.""" import os -from typing import List, Any import re +from typing import Any, List from lldb import SBError, SBFrame, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, SBValue @@ -23,9 +23,9 @@ def output_line(line: Any) -> None: Exception - clear_page would not function without terminal characters """ line = str(line) - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") if LLEFState.use_color is False: - line = ansi_escape.sub('', line) + line = ansi_escape.sub("", line) print(line) @@ -68,13 +68,9 @@ def print_line_with_string( ) -def print_line( - char: GLYPHS = GLYPHS.HORIZONTAL_LINE, color: TERM_COLORS = TERM_COLORS.GREY -) -> None: +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}{os.get_terminal_size().columns*char.value}{TERM_COLORS.ENDC.value}" - ) + output_line(f"{color.value}{os.get_terminal_size().columns * char.value}{TERM_COLORS.ENDC.value}") def print_message(msg_type: MSG_TYPE, message: str) -> None: @@ -128,15 +124,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 """ @@ -178,7 +170,7 @@ def is_stack(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoLi def is_heap(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoList) -> bool: """Determines whether an @address points to the heap""" if regions is None: - return False + return False region = SBMemoryRegionInfo() heap_bool = False if regions.GetMemoryRegionContainingAddress(address, region): 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..33dcf9e 100644 --- a/llef.py +++ b/llef.py @@ -16,15 +16,11 @@ 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.color_settings import ColorSettingsCommand +from commands.context import ContextCommand from commands.hexdump import HexdumpCommand +from commands.pattern import PatternContainer, PatternCreateCommand, PatternSearchCommand +from commands.settings import SettingsCommand from handlers.stop_hook import StopHookHandler @@ -36,7 +32,7 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: ContextCommand, SettingsCommand, ColorSettingsCommand, - HexdumpCommand + HexdumpCommand, ] handlers = [StopHookHandler] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2db60f0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,25 @@ +[tox] +envlist = py39, py310, py311, py312 + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[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 From 761f09546290d6b2a4f3f893e1f8019ef3568766 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:36:41 +0000 Subject: [PATCH 02/32] Add command aliases --- commands/base_command.py | 10 ++++++++-- commands/hexdump.py | 10 +++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/commands/base_command.py b/commands/base_command.py index 5b93efa..43db408 100644 --- a/commands/base_command.py +++ b/commands/base_command.py @@ -1,15 +1,16 @@ """Base command definition.""" from abc import ABC, abstractmethod -from typing import Type +from typing import Type, Dict from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext from commands.base_container import BaseContainer - class BaseCommand(ABC): """An abstract base class for all commands.""" + + alias_set = {} @abstractmethod def __init__(self) -> None: @@ -55,3 +56,8 @@ def lldb_self_register(cls, debugger: SBDebugger, module_name: str) -> None: 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) \ No newline at end of file diff --git a/commands/hexdump.py b/commands/hexdump.py index 305fe1d..61035a0 100644 --- a/commands/hexdump.py +++ b/commands/hexdump.py @@ -8,7 +8,6 @@ from commands.base_command import BaseCommand from common.constants import SIZES -from common.context_handler import ContextHandler class HexdumpCommand(BaseCommand): @@ -18,6 +17,15 @@ 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() From 90118e4ca4abebc6c722ebcdc254bf435914be12 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 12:52:08 +0000 Subject: [PATCH 03/32] LLDB version check before executing commands --- commands/base_command.py | 7 ++++--- commands/hexdump.py | 28 ++++++++++++++++++---------- common/state.py | 3 +++ common/util.py | 14 ++++++++++++++ llef.py | 3 +++ 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/commands/base_command.py b/commands/base_command.py index 43db408..f9b6d72 100644 --- a/commands/base_command.py +++ b/commands/base_command.py @@ -1,15 +1,16 @@ """Base command definition.""" from abc import ABC, abstractmethod -from typing import Type, Dict +from typing import Type from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext from commands.base_container import BaseContainer + class BaseCommand(ABC): """An abstract base class for all commands.""" - + alias_set = {} @abstractmethod @@ -60,4 +61,4 @@ def lldb_self_register(cls, debugger: SBDebugger, module_name: str) -> None: # 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) \ No newline at end of file + debugger.HandleCommand(alias_command) diff --git a/commands/hexdump.py b/commands/hexdump.py index 61035a0..e6a668f 100644 --- a/commands/hexdump.py +++ b/commands/hexdump.py @@ -8,6 +8,8 @@ from commands.base_command import BaseCommand from common.constants import SIZES +from common.context_handler import ContextHandler +from common.util import check_version class HexdumpCommand(BaseCommand): @@ -19,12 +21,7 @@ class HexdumpCommand(BaseCommand): # 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" - } + alias_set = {"dq": "qword", "dd": "dword", "dw": "word", "db": "byte"} def __init__(self, debugger: SBDebugger, __: Dict[Any, Any]) -> None: super().__init__() @@ -36,16 +33,26 @@ def get_command_parser(cls) -> argparse.ArgumentParser: """Get the command parser.""" parser = argparse.ArgumentParser() parser.add_argument( - "type", choices=["qword", "dword", "word", "byte"], default="byte", help="The format for presenting data" + "type", + choices=["qword", "dword", "word", "byte"], + default="byte", + help="The format for presenting data", ) parser.add_argument( - "--reverse", action="store_true", help="The direction of output lines. Low to high by default" + "--reverse", + action="store_true", + 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" + "--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" + "address", + type=hex_int, + help="A value/address/symbol used as the location to print the hexdump from", ) return parser @@ -59,6 +66,7 @@ def get_long_help() -> str: """Return a longer help message""" return HexdumpCommand.get_command_parser().format_help() + @check_version("15.2.0") def __call__( self, debugger: SBDebugger, diff --git a/common/state.py b/common/state.py index cb023c3..a83d8f9 100644 --- a/common/state.py +++ b/common/state.py @@ -21,3 +21,6 @@ class LLEFState(metaclass=Singleton): # Stores whether color should be used use_color = False + + # Stores version of LLDB. + version = [] diff --git a/common/util.py b/common/util.py index 52bdc2d..385f88c 100644 --- a/common/util.py +++ b/common/util.py @@ -182,3 +182,17 @@ def is_heap(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoLis def extract_arch_from_triple(triple: str) -> str: """Extracts the architecture from triple string.""" return triple.split("-")[0] + + +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.version < required_version: + print(f"error: requires LLDB version {required_version_string} to execute") + return + return func(*args, **kwargs) + + return wrapper + + return inner diff --git a/llef.py b/llef.py index 33dcf9e..7c522c7 100644 --- a/llef.py +++ b/llef.py @@ -21,6 +21,7 @@ from commands.hexdump import HexdumpCommand from commands.pattern import PatternContainer, PatternCreateCommand, PatternSearchCommand from commands.settings import SettingsCommand +from common.state import LLEFState from handlers.stop_hook import StopHookHandler @@ -42,3 +43,5 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: for handler in handlers: handler.lldb_self_register(debugger, "llef") + + LLEFState.version = [int(x) for x in debugger.GetVersionString().split()[2].split(".")] From 29d658afb3f5fedfa87df515ad1ceb04930ecae7 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 12:59:14 +0000 Subject: [PATCH 04/32] Fixed the error handling in print_memory_address. Added process check to hexdump. --- commands/hexdump.py | 7 ++++++- common/context_handler.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/commands/hexdump.py b/commands/hexdump.py index e6a668f..ca4c754 100644 --- a/commands/hexdump.py +++ b/commands/hexdump.py @@ -9,7 +9,7 @@ from commands.base_command import BaseCommand from common.constants import SIZES from common.context_handler import ContextHandler -from common.util import check_version +from common.util import check_version, output_line class HexdumpCommand(BaseCommand): @@ -75,6 +75,11 @@ def __call__( result: SBCommandReturnObject, ) -> None: """Handles the invocation of the hexdump command""" + + if not exe_ctx.process.is_alive: + output_line("hexdump requires a running process") + return + args = self.parser.parse_args(shlex.split(command)) divisions = SIZES[args.type.upper()].value diff --git a/common/context_handler.py b/common/context_handler.py index d8fa890..903eb3b 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -149,9 +149,9 @@ def print_memory_address(self, addr: int, offset: int, size: int) -> None: # 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) From 44bbf24158791bf8cc111a05b186456047cc6973 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:06:15 +0000 Subject: [PATCH 05/32] Fix some eggregious formatting --- .gitignore | 3 ++- arch/ppc.py | 12 ++++++++++-- commands/context.py | 6 ++++-- 3 files changed, 16 insertions(+), 5 deletions(-) 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/arch/ppc.py b/arch/ppc.py index 9433651..c0fd218 100644 --- a/arch/ppc.py +++ b/arch/ppc.py @@ -39,6 +39,14 @@ class PPC(BaseArch): "carry": 0x20000000, } - _cr_register_bit_masks = {"cr0_lt": 0x80000000, "cr0_gt": 0x40000000, "cr0_eq": 0x20000000, "cr0_so": 0x10000000} + _cr_register_bit_masks = { + "cr0_lt": 0x80000000, + "cr0_gt": 0x40000000, + "cr0_eq": 0x20000000, + "cr0_so": 0x10000000, + } - flag_registers = [FlagRegister("cr", _cr_register_bit_masks), FlagRegister("xer", _xer_register_bit_masks)] + flag_registers = [ + FlagRegister("cr", _cr_register_bit_masks), + FlagRegister("xer", _xer_register_bit_masks), + ] diff --git a/commands/context.py b/commands/context.py index c9e83b0..6ccfc2d 100644 --- a/commands/context.py +++ b/commands/context.py @@ -27,9 +27,11 @@ def get_command_parser(cls) -> argparse.ArgumentParser: """Get the command parser.""" parser = argparse.ArgumentParser(description="context command") parser.add_argument( - "sections", nargs="*", choices=["registers", "stack", "code", "threads", "trace", "all"], default="all" + "sections", + nargs="*", + choices=["registers", "stack", "code", "threads", "trace", "all"], + default="all", ) - return parser @staticmethod From 542de8b9e11f65b14a0e0a0b74989e44781e6eee Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:07:12 +0000 Subject: [PATCH 06/32] Modified colours and symbols for print_message function. --- common/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/util.py b/common/util.py index 385f88c..ff851c1 100644 --- a/common/util.py +++ b/common/util.py @@ -77,14 +77,14 @@ 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 + error_color = TERM_COLORS.RED if msg_type == MSG_TYPE.INFO: - output_line(f"{info_color.value}[+]{TERM_COLORS.ENDC.value} {message}") + output_line(f"{info_color.value}[i]{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}") + output_line(f"{error_color.value}[-]{TERM_COLORS.ENDC.value} {message}") def print_instruction(line: str, color: TERM_COLORS = TERM_COLORS.ENDC) -> None: From b6535ee89120f1a235209fc8c44fb6252a284545 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:12:54 +0000 Subject: [PATCH 07/32] Implemented the xinfo command --- commands/xinfo.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++ common/util.py | 38 +++++++++++++++++-- llef.py | 2 + 3 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 commands/xinfo.py diff --git a/commands/xinfo.py b/commands/xinfo.py new file mode 100644 index 0000000..b507e47 --- /dev/null +++ b/commands/xinfo.py @@ -0,0 +1,96 @@ +"""Xinfo command class.""" + +import argparse +import os +import shlex +from typing import Any, Dict + +from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext, SBMemoryRegionInfo + +from commands.base_command import BaseCommand +from common.constants import MSG_TYPE +from common.context_handler import ContextHandler +from common.util import check_process, hex_int, print_message + + +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) + + @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() + + @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 + + memory_region = SBMemoryRegionInfo() + error = exe_ctx.process.GetMemoryRegionInfo(address, memory_region) + + if error.Fail(): + print_message(MSG_TYPE.ERROR, "Couldn't obtain region info") + + if not memory_region.IsMapped(): + print_message(MSG_TYPE.ERROR, f"Not Found: {hex(address)}") + return + + print_message(MSG_TYPE.SUCCESS, f"Found: {hex(address)}") + + start = memory_region.GetRegionBase() + end = memory_region.GetRegionEnd() + size = end - start + print_message( + MSG_TYPE.INFO, + f"Page/Region: {hex(start)}->{hex(end)} (size={hex(size)})", + ) + + permissions = "" + permissions += "r" if memory_region.IsReadable() else "" + permissions += "w" if memory_region.IsWritable() else "" + permissions += "x" if memory_region.IsExecutable() else "" + print_message(MSG_TYPE.INFO, f"Permissions: {permissions}") + + path = memory_region.GetName() + print_message(MSG_TYPE.INFO, f"Pathname: {path}") + + print_message(MSG_TYPE.INFO, f"Offset (from page/region): +{hex(address - memory_region.GetRegionBase())}") + + if os.path.exists(path): + print_message(MSG_TYPE.INFO, f"Inode: {os.stat(path).st_ino}") + else: + print_message(MSG_TYPE.ERROR, "No inode found: Path cannot be found locally.") diff --git a/common/util.py b/common/util.py index ff851c1..7562a7e 100644 --- a/common/util.py +++ b/common/util.py @@ -4,7 +4,7 @@ import re from typing import Any, List -from lldb import SBError, SBFrame, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, SBValue +from lldb import SBError, SBExecutionContext, SBFrame, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, SBValue from common.constants import ALIGN, GLYPHS, MSG_TYPE, TERM_COLORS from common.state import LLEFState @@ -77,14 +77,14 @@ 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.RED + error_color = TERM_COLORS.GREEN if msg_type == MSG_TYPE.INFO: - output_line(f"{info_color.value}[i]{TERM_COLORS.ENDC.value} {message}") + 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}") + output_line(f"{error_color.value}[+]{TERM_COLORS.ENDC.value} {message}") def print_instruction(line: str, color: TERM_COLORS = TERM_COLORS.ENDC) -> None: @@ -196,3 +196,33 @@ def wrapper(*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 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) diff --git a/llef.py b/llef.py index 7c522c7..b34d483 100644 --- a/llef.py +++ b/llef.py @@ -21,6 +21,7 @@ from commands.hexdump import HexdumpCommand from commands.pattern import PatternContainer, PatternCreateCommand, PatternSearchCommand from commands.settings import SettingsCommand +from commands.xinfo import XinfoCommand from common.state import LLEFState from handlers.stop_hook import StopHookHandler @@ -34,6 +35,7 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: SettingsCommand, ColorSettingsCommand, HexdumpCommand, + XinfoCommand, ] handlers = [StopHookHandler] From 96bc3048bdb75d64e962a6fb5daccfbe7cf563b6 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:19:03 +0000 Subject: [PATCH 08/32] Check valid address range on `xinfo` command * Checks for valid address range. * Fixed libraru ordering. * Removed unused imported. --- commands/xinfo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/commands/xinfo.py b/commands/xinfo.py index b507e47..4082aea 100644 --- a/commands/xinfo.py +++ b/commands/xinfo.py @@ -7,6 +7,7 @@ from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext, SBMemoryRegionInfo +from arch import get_arch from commands.base_command import BaseCommand from common.constants import MSG_TYPE from common.context_handler import ContextHandler @@ -59,11 +60,16 @@ def __call__( 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 + memory_region = SBMemoryRegionInfo() error = exe_ctx.process.GetMemoryRegionInfo(address, memory_region) if error.Fail(): print_message(MSG_TYPE.ERROR, "Couldn't obtain region info") + return if not memory_region.IsMapped(): print_message(MSG_TYPE.ERROR, f"Not Found: {hex(address)}") From 74f643285403c32fe3b541333bfbed18b9301b94 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:21:31 +0000 Subject: [PATCH 09/32] User can order context sections with list in the output_order setting. * User can order context sections with the list in the output_order setting. * Fixed library order. * Moved validation for the output_order setting to validate_settings. * Defined a default output order. --- common/context_handler.py | 25 +++++++++++-------------- common/settings.py | 23 ++++++++++++++++++++++- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/common/context_handler.py b/common/context_handler.py index 903eb3b..3c9ac08 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -433,19 +433,16 @@ def display_context(self, exe_ctx: SBExecutionContext, update_registers: bool) - 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() + 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=TERM_COLORS[self.color_settings.line_color]) diff --git a/common/settings.py b/common/settings.py index ea3bed4..268a5b7 100644 --- a/common/settings.py +++ b/common/settings.py @@ -6,8 +6,9 @@ from arch import supported_arch from common.base_settings import BaseLLEFSettings +from common.constants import MSG_TYPE from common.singleton import Singleton -from common.util import change_use_color, output_line +from common.util import change_use_color, output_line, print_message class LLEFSettings(BaseLLEFSettings, metaclass=Singleton): @@ -17,6 +18,7 @@ class LLEFSettings(BaseLLEFSettings, metaclass=Singleton): 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 @@ -71,6 +73,22 @@ 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) + + def validate_output_order(self, value: str): + default_sections = self.DEFAUL_OUTPUT_ORDER.split(",") + sections = value.split(",") + if len(sections) != len(default_sections): + print_message(MSG_TYPE.ERROR, f"Requires {len(default_sections)} elements.") + raise ValueError + + for section in default_sections: + if section not in sections: + print_message(MSG_TYPE.ERROR, f"Missing '{section}' from output order.") + raise ValueError + def validate_settings(self, setting=None) -> bool: """ Validate settings by attempting to retrieve all properties thus executing any ConfigParser coverters @@ -95,6 +113,9 @@ def validate_settings(self, setting=None) -> bool: ): print("Colour is not supported by your terminal") raise ValueError + + elif setting_name == "output_order": + self.validate_output_order(value) except ValueError: valid = False raw_value = self._RAW_CONFIG.get(self.GLOBAL_SECTION, setting_name) From 64cbe0484ea6ea176aa2f68ac638cff42e848c09 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:22:16 +0000 Subject: [PATCH 10/32] Order context sections * User can order context sections with the list in the output_order setting. * Fixed library order. * Moved validation for the output_order setting to validate_settings. * Defined a default output order. * Modified error messages. --- common/settings.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/settings.py b/common/settings.py index 268a5b7..c871526 100644 --- a/common/settings.py +++ b/common/settings.py @@ -81,13 +81,13 @@ def validate_output_order(self, value: str): default_sections = self.DEFAUL_OUTPUT_ORDER.split(",") sections = value.split(",") if len(sections) != len(default_sections): - print_message(MSG_TYPE.ERROR, f"Requires {len(default_sections)} elements.") + print_message(MSG_TYPE.ERROR, f"Requires {len(default_sections)} elements: '{','.join(default_sections)}'") raise ValueError - for section in default_sections: - if section not in sections: - print_message(MSG_TYPE.ERROR, f"Missing '{section}' from output order.") - raise ValueError + missing_sections = set(default_sections) - set(sections) + if len(missing_sections) > 0: + print_message(MSG_TYPE.ERROR, f"Missing '{','.join(missing_sections)}' from output order.") + raise ValueError def validate_settings(self, setting=None) -> bool: """ From bea42fad5f09f861f8a268ebecbdd5125f197d7c Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:23:01 +0000 Subject: [PATCH 11/32] Checksec command * Implemented stack canary, NX, and PIE checks for checksec command. * Implemented no rpath, no runpath, partial relro and ful relro checks for checksec command. * Clean up. Using builtin architecture functions. Check for valid target executable. * Refactor checksec.py such that helper functions are in common/checksec_util.py * Improved python docs. Moved constants to constants.py * Modified constant definitions. * Improved some python docs. * Improved some python docs. * Finished python docs. * Added comment to the module 0 problem in the get_dynamic_entry function. * Added comment. * Defined constants for program header offsets. * Improved error handling on memory reads for checksec. * Minor refactoring. * Minor refactor. * Moved read_program to util.py. Added decorators to check for valid target and elf file type. * Fixed library ordering. --- commands/checksec.py | 137 ++++++++++++++++++++++++++++++++++++++++ common/checksec_util.py | 82 ++++++++++++++++++++++++ common/constants.py | 49 +++++++++++++- common/util.py | 83 +++++++++++++++++++++++- llef.py | 2 + 5 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 commands/checksec.py create mode 100644 common/checksec_util.py diff --git a/commands/checksec.py b/commands/checksec.py new file mode 100644 index 0000000..21c9309 --- /dev/null +++ b/commands/checksec.py @@ -0,0 +1,137 @@ +"""Checksec command class.""" + +import argparse +from typing import Any, Dict + +from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext + +from commands.base_command import BaseCommand +from common.checksec_util import get_dynamic_entry, get_executable_type, get_program_header_permission +from common.constants import ( + DYNAMIC_ENTRY_TYPE, + DYNAMIC_ENTRY_VALUE, + EXECUTABLE_TYPE, + MSG_TYPE, + PERMISSION_SET, + PROGRAM_HEADER_TYPE, + SECURITY_CHECK, + TERM_COLORS, +) +from common.context_handler import ContextHandler +from common.util import check_elf, check_target, output_line, print_message + + +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() + + @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 = { + "Canary": SECURITY_CHECK.NO, + "NX Support": SECURITY_CHECK.UNKNOWN, + "PIE Support": SECURITY_CHECK.UNKNOWN, + "No RPath": SECURITY_CHECK.UNKNOWN, + "No RunPath": SECURITY_CHECK.UNKNOWN, + "Partial RelRO": SECURITY_CHECK.UNKNOWN, + "Full RelRO": SECURITY_CHECK.UNKNOWN, + } + + for symbol in target.GetModuleAtIndex(0): + if symbol.GetName() in ["__stack_chk_fail", "__stack_chk_guard", "__intel_security_cookie"]: + checks["Canary"] = SECURITY_CHECK.YES + break + + try: + if get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_STACK) in PERMISSION_SET.NOT_EXEC: + checks["NX Support"] = SECURITY_CHECK.YES + else: + checks["NX Support"] = SECURITY_CHECK.NO + except MemoryError as error: + print_message(MSG_TYPE.ERROR, error) + checks["NX Support"] = SECURITY_CHECK.UNKNOWN + + try: + if get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_RELRO) is not None: + checks["Partial RelRO"] = SECURITY_CHECK.YES + else: + checks["Partial RelRO"] = SECURITY_CHECK.NO + except MemoryError as error: + print_message(MSG_TYPE.ERROR, error) + checks["Partial RelRO"] = SECURITY_CHECK.UNKNOWN + + try: + if get_executable_type(target) == EXECUTABLE_TYPE.DYN: + checks["PIE Support"] = SECURITY_CHECK.YES + else: + checks["PIE Support"] = SECURITY_CHECK.NO + except MemoryError as error: + print_message(MSG_TYPE.ERROR, error) + checks["PIE Support"] = SECURITY_CHECK.UNKNOWN + + if checks["Partial RelRO"] == SECURITY_CHECK.UNKNOWN: + checks["Full RelRO"] = SECURITY_CHECK.UNKNOWN + elif ( + get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.FLAGS) == DYNAMIC_ENTRY_VALUE.BIND_NOW + and checks["Partial RelRO"] == SECURITY_CHECK.YES + ): + checks["Full RelRO"] = SECURITY_CHECK.YES + else: + checks["Full RelRO"] = SECURITY_CHECK.NO + + if get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RPATH) is None: + checks["No RPath"] = SECURITY_CHECK.YES + else: + checks["No RPath"] = SECURITY_CHECK.NO + + if get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RUNPATH) is None: + checks["No RunPath"] = SECURITY_CHECK.YES + else: + checks["No RunPath"] = SECURITY_CHECK.NO + + for check, status in checks.items(): + if status == SECURITY_CHECK.YES: + color = TERM_COLORS.GREEN.value + elif status == SECURITY_CHECK.NO: + color = TERM_COLORS.RED.value + else: + color = TERM_COLORS.GREY.value + check += ": " + output_line(f"{check:<20} {color}{status.value}{TERM_COLORS.ENDC.value}") diff --git a/common/checksec_util.py b/common/checksec_util.py new file mode 100644 index 0000000..f818fd5 --- /dev/null +++ b/common/checksec_util.py @@ -0,0 +1,82 @@ +from lldb import SBError, SBTarget + +from arch import get_arch +from common.constants import ARCH_BITS +from common.util import 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 + + +def get_executable_type(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(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(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 diff --git a/common/constants.py b/common/constants.py index 7784862..fb7854a 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,50 @@ class SIZES(Enum): DWORD = 4 WORD = 2 BYTE = 1 + + +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 diff --git a/common/util.py b/common/util.py index 7562a7e..94f4b3f 100644 --- a/common/util.py +++ b/common/util.py @@ -4,7 +4,16 @@ import re from typing import Any, List -from lldb import SBError, SBExecutionContext, SBFrame, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, SBValue +from lldb import ( + SBError, + SBExecutionContext, + SBFrame, + SBMemoryRegionInfo, + SBMemoryRegionInfoList, + SBProcess, + SBTarget, + SBValue, +) from common.constants import ALIGN, GLYPHS, MSG_TYPE, TERM_COLORS from common.state import LLEFState @@ -220,9 +229,81 @@ def wrapper(*args, **kwargs): 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 check_elf(func): + def wrapper(*args, **kwargs): + for arg in list(args) + list(kwargs.values()): + if isinstance(arg, SBExecutionContext): + try: + if read_program(arg.target, 0, 4) == b"\x7F\x45\x4C\x46": + 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 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") diff --git a/llef.py b/llef.py index b34d483..c627822 100644 --- a/llef.py +++ b/llef.py @@ -16,6 +16,7 @@ from commands.base_command import BaseCommand from commands.base_container import BaseContainer +from commands.checksec import ChecksecCommand from commands.color_settings import ColorSettingsCommand from commands.context import ContextCommand from commands.hexdump import HexdumpCommand @@ -35,6 +36,7 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: SettingsCommand, ColorSettingsCommand, HexdumpCommand, + ChecksecCommand, XinfoCommand, ] From 65f7a5bcd7ed19b3f75eb1af16d2f5f3687000aa Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:24:12 +0000 Subject: [PATCH 12/32] Scan command. * Started implementation of the scan command. ReadUnsignedFromMemory function isn't returning any data. * Working version of the scan command. * Scan command now takes custom address ranges. * Added another python doc. * Fixed library import order. * Improved error handling. * Now using the existing 'print_stack_addr' to dereference address results. * Fixed address range parsing. --- commands/scan.py | 88 +++++++++++++++++++++++++++++++++++++++++++++ common/scan_util.py | 61 +++++++++++++++++++++++++++++++ llef.py | 2 ++ 3 files changed, 151 insertions(+) create mode 100644 commands/scan.py create mode 100644 common/scan_util.py diff --git a/commands/scan.py b/commands/scan.py new file mode 100644 index 0000000..f466a6d --- /dev/null +++ b/commands/scan.py @@ -0,0 +1,88 @@ +"""Scan command class.""" + +import argparse +import shlex +from typing import Any, Dict + +from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext + +from commands.base_command import BaseCommand +from common.constants import MSG_TYPE +from common.context_handler import ContextHandler +from common.scan_util import parse_address_ranges +from common.util import check_process, print_message + + +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) + + @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() + + @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 = parse_address_ranges(exe_ctx.process, search_region) + target_address_ranges = parse_address_ranges(exe_ctx.process, target_region) + + print_message(MSG_TYPE.INFO, f"Searching for addresses in '{search_region}' that point to '{target_region}'") + + address_size = exe_ctx.target.GetAddressByteSize() + + error = SBError() + for search_start, search_end in search_address_ranges: + for search_address in range(search_start, search_end, address_size): + target_address = exe_ctx.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 = exe_ctx.target.EvaluateExpression(f"{search_address}") + self.context_handler.print_stack_addr(search_address_value, offset) + else: + print_message(MSG_TYPE.ERROR, f"Memory at {search_address} couldn't be read.") diff --git a/common/scan_util.py b/common/scan_util.py new file mode 100644 index 0000000..e9d5305 --- /dev/null +++ b/common/scan_util.py @@ -0,0 +1,61 @@ +from lldb import SBMemoryRegionInfo, SBProcess + +from common.color_settings import LLEFColorSettings +from common.constants import MSG_TYPE +from common.util import print_message + +color_settings = LLEFColorSettings() + + +def parse_address_ranges(process: SBProcess, region_name: str): + """ + 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 = find_address_ranges(process, region_name) + + return address_ranges + + +def find_address_ranges(process: SBProcess, region_name: str): + """ + 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 diff --git a/llef.py b/llef.py index c627822..99852fa 100644 --- a/llef.py +++ b/llef.py @@ -21,6 +21,7 @@ from commands.context import ContextCommand 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 @@ -38,6 +39,7 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: HexdumpCommand, ChecksecCommand, XinfoCommand, + ScanCommand, ] handlers = [StopHookHandler] From 38695bd82a94861a3b1f812f57a2c534a34669c3 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:24:53 +0000 Subject: [PATCH 13/32] Improved is_code function. * is_code function now checks if the address is within a .text section and the data at the address is not ascii. * Fixed library ordering. * Minor fixes and refactoring. * Fixed library ordering. * Commented out 'not is_ascii_string' test on is_code function. --- common/context_handler.py | 2 +- common/util.py | 62 +++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/common/context_handler.py b/common/context_handler.py index 3c9ac08..74a7b50 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -198,7 +198,7 @@ def print_register(self, register: SBValue) -> None: # Register value has changed so highlight highlight = TERM_COLORS[self.color_settings.modified_register_color] - if is_code(reg_value, self.process, self.regions): + if is_code(reg_value, self.process, self.target, 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] diff --git a/common/util.py b/common/util.py index 94f4b3f..cc4f6e3 100644 --- a/common/util.py +++ b/common/util.py @@ -11,6 +11,7 @@ SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, + SBSection, SBTarget, SBValue, ) @@ -153,14 +154,65 @@ def attempt_to_read_string_from_memory(process: SBProcess, addr: SBValue, buffer 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, section: SBSection): + """ + Determines whether a given memory @address exists within a @section of the executable file @target. + + :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. + """ + + if section: + section_start = section.GetLoadAddress(target) + section_end = section_start + section.GetByteSize() + if section_start <= address < section_end: + return True + + return False + + +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. + """ + file = target.GetExecutable() + text_section = target.GetModuleAtIndex(0).FindSection(".text") + + in_text = False + if is_in_section(address, target, text_section): + in_text = True + elif 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 From afc518ede58420b573ab8246e45c1d18305500f9 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:26:28 +0000 Subject: [PATCH 14/32] Dereference command * Rough working version of dereference command. Follows address chain and decodes any instructions pointed to. * Good working version of dereference command. Fixed the attempt_to_read_string_from_memory function. * Implemented offset base option to the dereference command. * Remove change to xinfo command. * Added some python docs. Reference hex_int and positive_int from util.py * Moved hex_or_str function to util.py * Added python doc. * Fixed format issues. --- commands/dereference.py | 88 ++++++++++++++++++++++++++++++++++++++ commands/hexdump.py | 15 +------ common/dereference_util.py | 81 +++++++++++++++++++++++++++++++++++ common/util.py | 19 +++++++- llef.py | 2 + 5 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 commands/dereference.py create mode 100644 common/dereference_util.py diff --git a/commands/dereference.py b/commands/dereference.py new file mode 100644 index 0000000..0b01a92 --- /dev/null +++ b/commands/dereference.py @@ -0,0 +1,88 @@ +"""Dereference command class.""" + +import argparse +import shlex +from typing import Any, Dict + +from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext + +from commands.base_command import BaseCommand +from common.context_handler import ContextHandler +from common.dereference_util import dereference +from common.util import check_process, hex_int, 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) + + @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() + + @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 + dereference(address, offset, exe_ctx.target, exe_ctx.process, self.context_handler.regions) diff --git a/commands/hexdump.py b/commands/hexdump.py index ca4c754..385b158 100644 --- a/commands/hexdump.py +++ b/commands/hexdump.py @@ -9,7 +9,7 @@ from commands.base_command import BaseCommand from common.constants import SIZES from common.context_handler import ContextHandler -from common.util import check_version, output_line +from common.util import check_version, hex_int, output_line, positive_int class HexdumpCommand(BaseCommand): @@ -104,16 +104,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/common/dereference_util.py b/common/dereference_util.py new file mode 100644 index 0000000..1916532 --- /dev/null +++ b/common/dereference_util.py @@ -0,0 +1,81 @@ +from lldb import SBAddress, SBError, SBInstruction, SBMemoryRegionInfoList, SBProcess, SBTarget + +from common.color_settings import LLEFColorSettings +from common.constants import GLYPHS, MSG_TYPE, TERM_COLORS +from common.util import attempt_to_read_string_from_memory, hex_or_str, is_code, output_line, print_message + +color_settings = LLEFColorSettings() + + +def read_instruction(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, "intel") + return instruction_list.GetInstructionAtIndex(0) + + +def dereference_last_address(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, regions): + instruction = read_instruction(target, last_address) + if instruction.IsValid(): + data[-1] = ( + f"{TERM_COLORS[color_settings.instruction_color].value}{instruction.GetMnemonic(target)} " + + f"{instruction.GetOperands(target)}{TERM_COLORS.ENDC.value}" + ) + else: + string = attempt_to_read_string_from_memory(process, last_address) + if string != "": + data[-1] = f"{TERM_COLORS[color_settings.string_color].value}{string}{TERM_COLORS.ENDC.value}" + + +def dereference(address: int, offset: int, target: SBTarget, process: SBProcess, regions: SBMemoryRegionInfoList): + """ + 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) < 2: + print_message(MSG_TYPE.ERROR, f"{hex(data[0])} is not accessible.") + return + + dereference_last_address(data, target, process, regions) + + output = f"{TERM_COLORS.CYAN.value}{hex_or_str(data[0])}{TERM_COLORS.ENDC.value}{GLYPHS.VERTICAL_LINE.value}" + if offset >= 0: + output += f"+0x{offset:04x}: " + else: + output += f"-0x{-offset:04x}: " + output += " -> ".join(map(hex_or_str, data[1:])) + output_line(output) diff --git a/common/util.py b/common/util.py index cc4f6e3..a60d021 100644 --- a/common/util.py +++ b/common/util.py @@ -2,6 +2,7 @@ import os import re +from argparse import ArgumentTypeError from typing import Any, List from lldb import ( @@ -146,7 +147,7 @@ def attempt_to_read_string_from_memory(process: SBProcess, addr: SBValue, buffer 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. @@ -323,6 +324,22 @@ def hex_int(x): return int(x, 0) +def positive_int(x): + """A converter for input arguments in different bases to positive ints""" + x = hex_int(x, 0) + if x <= 0: + raise ArgumentTypeError("Must be positive") + return x + + +def hex_or_str(x): + """Convert to formated 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. diff --git a/llef.py b/llef.py index 99852fa..311401b 100644 --- a/llef.py +++ b/llef.py @@ -19,6 +19,7 @@ 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 @@ -39,6 +40,7 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: HexdumpCommand, ChecksecCommand, XinfoCommand, + DereferenceCommand, ScanCommand, ] From 71787f48600d3ce8ea311c2b19f7004dee792141 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:27:32 +0000 Subject: [PATCH 15/32] Added target to is_code call in dereference_util.py --- common/dereference_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/dereference_util.py b/common/dereference_util.py index 1916532..65b3797 100644 --- a/common/dereference_util.py +++ b/common/dereference_util.py @@ -32,7 +32,7 @@ def dereference_last_address(data: list, target: SBTarget, process: SBProcess, r """ last_address = data[-2] - if is_code(last_address, process, regions): + if is_code(last_address, process, target, regions): instruction = read_instruction(target, last_address) if instruction.IsValid(): data[-1] = ( From 37bc11a7219a50bf9793f9c1ac13b9422b53b837 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:28:10 +0000 Subject: [PATCH 16/32] Fixed LLDB version bug on Mac * Implemented LLDB to Clang version convertion on the check_version function. * Fixed library ordering. * Fixed formatting. --------- Co-authored-by: Foundry Zero --- common/state.py | 5 ++++- common/util.py | 21 +++++++++++++++++++++ llef.py | 9 ++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/common/state.py b/common/state.py index a83d8f9..c792f15 100644 --- a/common/state.py +++ b/common/state.py @@ -22,5 +22,8 @@ class LLEFState(metaclass=Singleton): # Stores whether color should be used use_color = False - # Stores version of LLDB. + # Stores version of LLDB if on Linux. Stores clang verion if on Mac version = [] + + # Linux, Mac (Darwin) or Windows + platform = "" diff --git a/common/util.py b/common/util.py index a60d021..2836613 100644 --- a/common/util.py +++ b/common/util.py @@ -246,10 +246,31 @@ def extract_arch_from_triple(triple: str) -> str: return triple.split("-")[0] +def lldb_version_to_clang(lldb_version): + """ + Convert an LLDB version to its corrosponding Clang version. + + :param lldb_version: The LLDB version. + :return: The Clang version. + """ + + clang_version = [0, 0, 0, 0] + if lldb_version >= [17, 0, 6]: + clang_version = [1600, 0, 26, 3] + elif lldb_version >= [16, 0, 0]: + clang_version = [1500, 0, 40, 1] + elif 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 LLEFState.version < required_version: print(f"error: requires LLDB version {required_version_string} to execute") return diff --git a/llef.py b/llef.py index 311401b..7f3adf4 100644 --- a/llef.py +++ b/llef.py @@ -10,6 +10,7 @@ # 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 @@ -52,4 +53,10 @@ def __lldb_init_module(debugger: SBDebugger, _: Dict[Any, Any]) -> None: for handler in handlers: handler.lldb_self_register(debugger, "llef") - LLEFState.version = [int(x) for x in debugger.GetVersionString().split()[2].split(".")] + 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(".")] From e912a2303aaf35343315e060017167f4551658a7 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:29:22 +0000 Subject: [PATCH 17/32] Fix py313 post release --- .github/workflows/style.yml | 2 +- tox.ini | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 59acaed..bf81ab2 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13.0-rc.1'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v3 diff --git a/tox.ini b/tox.ini index 2db60f0..db8e3fc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312 +envlist = py39, py310, py311, py312, py313 [gh-actions] python = @@ -7,6 +7,7 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [testenv] deps = From 690b02ff3aa8a704047f8d76a6cf197eb8f2887d Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:30:22 +0000 Subject: [PATCH 18/32] Bugfix/fixing xcode terminal issues * Fixed 'clear_page' function so that is doesn't cause issues in xcode terminal. * Added default line length for context headers. * Improved terminal column code. * Fixed library ordering. --------- Co-authored-by: michael --- common/constants.py | 3 +++ common/context_handler.py | 3 ++- common/util.py | 10 +++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/common/constants.py b/common/constants.py index fb7854a..3b5e12e 100644 --- a/common/constants.py +++ b/common/constants.py @@ -99,3 +99,6 @@ class ARCH_BITS(IntEnum): BITS_32 = 1 BITS_64 = 2 + + +DEFAULT_TERMINAL_COLUMNS = 80 diff --git a/common/context_handler.py b/common/context_handler.py index 74a7b50..36d968d 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -428,7 +428,8 @@ def display_context(self, exe_ctx: SBExecutionContext, update_registers: bool) - 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() diff --git a/common/util.py b/common/util.py index 2836613..8dbaa81 100644 --- a/common/util.py +++ b/common/util.py @@ -17,10 +17,14 @@ SBValue, ) -from common.constants import ALIGN, GLYPHS, MSG_TYPE, TERM_COLORS +from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, GLYPHS, MSG_TYPE, TERM_COLORS from common.state import LLEFState +def terminal_columns() -> int: + return os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + + def change_use_color(new_value: bool) -> None: """ Change the global use_color bool. use_color should not be written to directly @@ -60,7 +64,7 @@ def print_line_with_string( align: ALIGN = ALIGN.RIGHT, ) -> None: """Print a line with the provided @string padded with @char""" - width = os.get_terminal_size().columns + width = terminal_columns() if align == ALIGN.RIGHT: l_pad = (width - len(string) - 6) * char.value r_pad = 4 * char.value @@ -81,7 +85,7 @@ def print_line_with_string( 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}{os.get_terminal_size().columns * char.value}{TERM_COLORS.ENDC.value}") + output_line(f"{color.value}{terminal_columns() * char.value}{TERM_COLORS.ENDC.value}") def print_message(msg_type: MSG_TYPE, message: str) -> None: From edcc93159261602040f3458cdd6b7b9f846e1cc6 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:30:52 +0000 Subject: [PATCH 19/32] Bugfix/handle dirty memory regions * Fixed code and stack highlighting for mac. * Fixed ELF file check. * Fixed 'xinfo' command on mac. * Removed commented code. * Fixed looping problem in 'dereference' commands. * Added error message about memory region names for 'scan' command on mac. * Minor fixes. * Fixed heap highlighting for mac. * Changed method to find heap memory regions by using ellimation. * Added check for writable region on heap check. * Fixed positive_int argument function. * Fixed library ordering. * Fixed flake8 problems. * Modified output message. * Moved magic bytes of executable files to constants.py * Removed heap region code based on malloc allocation on the process. * is_in_section function now only has one return statement. * Refactored is_text_region function to reduce if statements. * Fixed previous commit. * Fixed library ordering. * Added method doc to is_file function. * Fixed stack_regions issue. * style fixes --------- Co-authored-by: Foundry Zero Co-authored-by: michael Co-authored-by: sam-f0 <116253255+sam-f0@users.noreply.github.com> Co-authored-by: Your Name --- commands/scan.py | 9 +++++ commands/xinfo.py | 16 +++++++-- common/constants.py | 12 +++++++ common/context_handler.py | 35 +++++++++++++++--- common/dereference_util.py | 4 ++- common/util.py | 72 +++++++++++++++++++++++--------------- 6 files changed, 110 insertions(+), 38 deletions(-) diff --git a/commands/scan.py b/commands/scan.py index f466a6d..2e944c5 100644 --- a/commands/scan.py +++ b/commands/scan.py @@ -10,6 +10,7 @@ from common.constants import MSG_TYPE from common.context_handler import ContextHandler from common.scan_util import parse_address_ranges +from common.state import LLEFState from common.util import check_process, print_message @@ -24,6 +25,7 @@ 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: @@ -70,6 +72,13 @@ def __call__( search_address_ranges = parse_address_ranges(exe_ctx.process, search_region) target_address_ranges = 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() diff --git a/commands/xinfo.py b/commands/xinfo.py index 4082aea..29103b9 100644 --- a/commands/xinfo.py +++ b/commands/xinfo.py @@ -5,12 +5,13 @@ import shlex from typing import Any, Dict -from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext, SBMemoryRegionInfo +from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext, SBMemoryRegionInfo, SBStream from arch import get_arch from commands.base_command import BaseCommand from common.constants import MSG_TYPE from common.context_handler import ContextHandler +from common.state import LLEFState from common.util import check_process, hex_int, print_message @@ -25,6 +26,7 @@ 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: @@ -91,12 +93,20 @@ def __call__( permissions += "x" if memory_region.IsExecutable() else "" print_message(MSG_TYPE.INFO, f"Permissions: {permissions}") - path = memory_region.GetName() + if self.state.platform == "Darwin": + sb_address = exe_ctx.target.ResolveLoadAddress(address) + module = sb_address.GetModule() + filespec = module.GetFileSpec() + description = SBStream() + filespec.GetDescription(description) + path = description.GetData() + else: + path = memory_region.GetName() print_message(MSG_TYPE.INFO, f"Pathname: {path}") print_message(MSG_TYPE.INFO, f"Offset (from page/region): +{hex(address - memory_region.GetRegionBase())}") - if os.path.exists(path): + if path is not None and os.path.exists(path): print_message(MSG_TYPE.INFO, f"Inode: {os.stat(path).st_ino}") else: print_message(MSG_TYPE.ERROR, "No inode found: Path cannot be found locally.") diff --git a/common/constants.py b/common/constants.py index 3b5e12e..706f33b 100644 --- a/common/constants.py +++ b/common/constants.py @@ -101,4 +101,16 @@ class ARCH_BITS(IntEnum): 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 diff --git a/common/context_handler.py b/common/context_handler.py index 36d968d..2d8e772 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -1,8 +1,19 @@ import os from string import printable -from typing import Optional, Type - -from lldb import SBAddress, SBDebugger, SBError, SBExecutionContext, SBFrame, SBProcess, SBTarget, SBThread, SBValue +from typing import List, Optional, Type + +from lldb import ( + SBAddress, + SBDebugger, + SBError, + SBExecutionContext, + SBFrame, + SBMemoryRegionInfo, + SBProcess, + SBTarget, + SBThread, + SBValue, +) from arch import get_arch, get_arch_from_str from arch.base_arch import BaseArch, FlagRegister @@ -51,6 +62,7 @@ def __init__( self.settings = LLEFSettings(debugger) self.color_settings = LLEFColorSettings() self.state = LLEFState() + self.stack_regions = List[SBMemoryRegionInfo] change_use_color(self.settings.color_output) def generate_rebased_address_string(self, address: SBAddress) -> str: @@ -200,9 +212,9 @@ def print_register(self, register: SBValue) -> None: if is_code(reg_value, self.process, self.target, self.regions): color = TERM_COLORS[self.color_settings.code_color] - elif is_stack(reg_value, self.process, self.regions): + elif is_stack(reg_value, self.regions, self.stack_regions): color = TERM_COLORS[self.color_settings.stack_color] - elif is_heap(reg_value, self.process, self.regions): + elif is_heap(reg_value, self.target, self.regions, self.stack_regions): color = TERM_COLORS[self.color_settings.heap_color] else: color = TERM_COLORS.ENDC @@ -401,6 +413,16 @@ def display_trace(self) -> None: output_line(line) + def find_stack_regions(self) -> List[SBMemoryRegionInfo]: + stack_regions = [] + for frame in self.process.GetSelectedThread().frames: + sp = frame.GetSP() + region = SBMemoryRegionInfo() + self.process.GetMemoryRegionInfo(sp, region) + stack_regions.append(region) + + return stack_regions + def refresh(self, exe_ctx: SBExecutionContext) -> None: """Refresh stored values""" self.frame = exe_ctx.GetFrame() @@ -417,6 +439,9 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: else: self.regions = None + if LLEFState.platform == "Darwin": + self.stack_regions = self.find_stack_regions() + 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`""" diff --git a/common/dereference_util.py b/common/dereference_util.py index 65b3797..4e683e3 100644 --- a/common/dereference_util.py +++ b/common/dereference_util.py @@ -61,10 +61,12 @@ def dereference(address: int, offset: int, target: SBTarget, process: SBProcess, 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("[LOOPING]") + break if len(data) < 2: print_message(MSG_TYPE.ERROR, f"{hex(data[0])} is not accessible.") diff --git a/common/util.py b/common/util.py index 8dbaa81..5149ad6 100644 --- a/common/util.py +++ b/common/util.py @@ -6,18 +6,18 @@ from typing import Any, List from lldb import ( + SBAddress, SBError, SBExecutionContext, SBFrame, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, - SBSection, SBTarget, SBValue, ) -from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, GLYPHS, MSG_TYPE, TERM_COLORS +from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, GLYPHS, MAGIC_BYTES, MSG_TYPE, TERM_COLORS from common.state import LLEFState @@ -170,7 +170,7 @@ def is_ascii_string(address: SBValue, process: SBProcess) -> bool: return attempt_to_read_string_from_memory(process, address) != "" -def is_in_section(address: SBValue, target: SBTarget, section: SBSection): +def is_in_section(address: SBValue, target: SBTarget, target_section_name: str): """ Determines whether a given memory @address exists within a @section of the executable file @target. @@ -180,13 +180,11 @@ def is_in_section(address: SBValue, target: SBTarget, section: SBSection): :return: A boolean of the check. """ - if section: - section_start = section.GetLoadAddress(target) - section_end = section_start + section.GetByteSize() - if section_start <= address < section_end: - return True + sb_address = target.ResolveLoadAddress(address) + section = sb_address.GetSection() + section_name = section.GetName() - return False + return section_name is not None and target_section_name in section_name def is_text_region(address: SBValue, target: SBTarget, region: SBMemoryRegionInfo) -> bool: @@ -198,14 +196,17 @@ def is_text_region(address: SBValue, target: SBTarget, region: SBMemoryRegionInf :param region: The memory region that the address exists in. :return: A boolean of the check. """ - file = target.GetExecutable() - text_section = target.GetModuleAtIndex(0).FindSection(".text") in_text = False - if is_in_section(address, target, text_section): - in_text = True - elif file.GetFilename() in region.GetName() and file.GetDirectory() in region.GetName(): - in_text = True + if is_file(target, MAGIC_BYTES.MACH.value): + if is_in_section(address, target, "__TEXT") or 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 @@ -221,26 +222,33 @@ def is_code(address: SBValue, process: SBProcess, target: SBTarget, regions: SBM return code_bool -def is_stack(address: SBValue, process: SBProcess, regions: SBMemoryRegionInfoList) -> bool: +def is_stack(address: SBValue, regions: SBMemoryRegionInfoList, 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 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] +) -> 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]": + 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 @@ -322,12 +330,18 @@ def wrapper(*args, **kwargs): 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 read_program(arg.target, 0, 4) == b"\x7F\x45\x4C\x46": + 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.") @@ -351,7 +365,7 @@ def hex_int(x): def positive_int(x): """A converter for input arguments in different bases to positive ints""" - x = hex_int(x, 0) + x = hex_int(x) if x <= 0: raise ArgumentTypeError("Must be positive") return x From 4b8402285ea2ab5c17ef9783464d78346292316b Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:31:31 +0000 Subject: [PATCH 20/32] Feature/improve display code * Failed attempt. * Working version. * Added some python docs. Added a check on extract_instructions function. * Fixed library ordering. * Minor bugfix on print_instruction function. * Changed return type from 'tuple' to 'Tuple'. * Removed a comment. * Supports multiple disassembly flavours. * Fixed library ordering. * Minor changes. * Removed debug print. * Disassembly syntax is now cached on context reflesh. * Using GetSetting function if LLDB version is >= 16. --- common/context_handler.py | 92 +++++++++++++++++++++------------------ common/state.py | 2 + common/util.py | 86 +++++++++++++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 47 deletions(-) diff --git a/common/context_handler.py b/common/context_handler.py index 2d8e772..cb8b016 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -4,6 +4,7 @@ from lldb import ( SBAddress, + SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, @@ -22,16 +23,20 @@ from common.settings import LLEFSettings from common.state import LLEFState from common.util import ( + address_to_filename, attempt_to_read_string_from_memory, change_use_color, clear_page, + extract_instructions, get_frame_arguments, + get_frame_range, get_registers, is_code, is_heap, is_stack, output_line, print_instruction, + print_instructions, print_line, print_line_with_string, ) @@ -320,47 +325,35 @@ def display_code(self) -> None: string_color=TERM_COLORS[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( + pre_instructions[-3:], + frame_start_address, + self.target, + TERM_COLORS[self.color_settings.instruction_color].value, + ) + + 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( + pc_instruction, + frame_start_address, + self.target, + TERM_COLORS[self.color_settings.highlighted_instruction_color].value, + ) + + limit = 9 - min(len(pre_instructions), 3) + print_instructions(post_instructions[1:limit], frame_start_address, self.target) def display_threads(self) -> None: """Print LLDB formatted thread information""" @@ -413,6 +406,18 @@ def display_trace(self) -> None: 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 >= [16]: + self.state.disassembly_syntax = debugger.GetSetting("target.x86-disassembly-flavor").GetStringValue(100) + else: + 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", "") + def find_stack_regions(self) -> List[SBMemoryRegionInfo]: stack_regions = [] for frame in self.process.GetSelectedThread().frames: @@ -425,10 +430,10 @@ def find_stack_regions(self) -> List[SBMemoryRegionInfo]: 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: @@ -439,6 +444,9 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: else: self.regions = None + if self.state.disassembly_syntax is None: + self.load_disassembly_syntax(self.debugger) + if LLEFState.platform == "Darwin": self.stack_regions = self.find_stack_regions() diff --git a/common/state.py b/common/state.py index c792f15..09d58a0 100644 --- a/common/state.py +++ b/common/state.py @@ -27,3 +27,5 @@ class LLEFState(metaclass=Singleton): # Linux, Mac (Darwin) or Windows platform = "" + + disassembly_syntax = None diff --git a/common/util.py b/common/util.py index 5149ad6..29ea23e 100644 --- a/common/util.py +++ b/common/util.py @@ -3,13 +3,14 @@ import os import re from argparse import ArgumentTypeError -from typing import Any, List +from typing import Any, List, Tuple from lldb import ( SBAddress, SBError, SBExecutionContext, SBFrame, + SBInstruction, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, @@ -102,11 +103,86 @@ def print_message(msg_type: MSG_TYPE, message: str) -> None: output_line(f"{error_color.value}[+]{TERM_COLORS.ENDC.value} {message}") -def print_instruction(line: str, color: TERM_COLORS = TERM_COLORS.ENDC) -> None: +def address_to_filename(target: SBTarget, address: int) -> str: + """ + Maps a memory address to its corrosponding executable/library and returns the filename. + + :param target: The target context. + :param address: The memory address to resolve. + :return: The filename. + """ + sb_address = SBAddress(address, target) + module = sb_address.GetModule() + file_spec = module.GetFileSpec() + filename = file_spec.GetFilename() + + return filename + + +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 print_instruction( + instruction: SBInstruction, base: int, target: SBTarget, color: TERM_COLORS = TERM_COLORS.ENDC.value +) -> 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}") + + address = instruction.GetAddress().GetLoadAddress(target) + offset = address - base + + if instruction is None: + output_line(f"{color}{hex(address)} <+{offset:02}>: INVALID INSTRUCTION") + return + + mnemonic = instruction.GetMnemonic(target) or "" + operands = instruction.GetOperands(target) or "" + comment = instruction.GetComment(target) or "" + if comment != "": + comment = f"; {comment}" + + output_line(f"{color}{hex(address)} <+{offset:02}>: {mnemonic:<10}{operands:<30}{comment}{TERM_COLORS.ENDC.value}") + + +def print_instructions( + instructions: List[SBInstruction], base: int, target: SBTarget, color: TERM_COLORS = TERM_COLORS.ENDC.value +) -> None: + for instruction in instructions: + print_instruction(instruction, base, target, color) + + +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 + + return start_address, end_address def get_registers(frame: SBFrame, frame_type: str) -> List[SBValue]: From 3f7867caad210e3452783d7cfe85620c18408f2d Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:32:11 +0000 Subject: [PATCH 21/32] Missing version --- common/context_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/context_handler.py b/common/context_handler.py index cb8b016..acd8083 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -409,7 +409,7 @@ def display_trace(self) -> None: 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 >= [16]: + if LLEFState.version >= [16]: self.state.disassembly_syntax = debugger.GetSetting("target.x86-disassembly-flavor").GetStringValue(100) else: command_interpreter = debugger.GetCommandInterpreter() From 3c6d685f073e64e1275c6ec89e8e1c89e7d7c029 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:34:09 +0000 Subject: [PATCH 22/32] Fix CI --- .github/workflows/style.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index bf81ab2..6cf1b6a 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -9,7 +9,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: python-isort - uses: isort/isort-action@v1.1.0 + uses: isort/isort-action@v1.1.1 with: configuration: "--check-only --profile black --diff --verbose" # Check-mypy: @@ -37,8 +37,12 @@ jobs: 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 + uses: py-actions/flake8@v2.3.0 with: max-line-length: "120" path: "." From ead00d5acb00ef6271acb029356be042f0fd5703 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:35:36 +0000 Subject: [PATCH 23/32] Refactor output utilities * Moved output functions from util.py to output_util.py * Fixed imports. Fixed LLEF state passing. Refactored context_handler.py to use color_string function. * Fixed library ordering. * Fixed flake8 issues. Hexdump now uses @check_process. * Minor fixes to python docs and inconsistent use of color_string. * Added back the missing find_stack_regions function. * Fixed spacing on print_message. * Moved change_use_color function from output_util.py to state.py * Fixed loading of LLEF state. # Conflicts: # common/util.py --- arch/__init__.py | 3 +- commands/base_settings.py | 2 +- commands/checksec.py | 3 +- commands/context.py | 2 +- commands/hexdump.py | 9 +- commands/pattern.py | 2 +- commands/scan.py | 3 +- common/base_settings.py | 4 +- common/color_settings.py | 2 +- common/context_handler.py | 181 +++++++++++++++++-------------------- common/dereference_util.py | 3 +- common/output_util.py | 166 ++++++++++++++++++++++++++++++++++ common/settings.py | 6 +- common/state.py | 6 ++ common/util.py | 111 +---------------------- 15 files changed, 277 insertions(+), 226 deletions(-) create mode 100644 common/output_util.py diff --git a/arch/__init__.py b/arch/__init__.py index a6f86f4..6ae3829 100644 --- a/arch/__init__.py +++ b/arch/__init__.py @@ -11,7 +11,8 @@ 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. diff --git a/commands/base_settings.py b/commands/base_settings.py index ded6b08..aa644fb 100644 --- a/commands/base_settings.py +++ b/commands/base_settings.py @@ -8,7 +8,7 @@ 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 index 21c9309..67497a2 100644 --- a/commands/checksec.py +++ b/commands/checksec.py @@ -18,7 +18,8 @@ TERM_COLORS, ) from common.context_handler import ContextHandler -from common.util import check_elf, check_target, output_line, print_message +from common.output_util import output_line, print_message +from common.util import check_elf, check_target class ChecksecCommand(BaseCommand): diff --git a/commands/context.py b/commands/context.py index 6ccfc2d..9685145 100644 --- a/commands/context.py +++ b/commands/context.py @@ -8,7 +8,7 @@ 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): diff --git a/commands/hexdump.py b/commands/hexdump.py index 385b158..e70e316 100644 --- a/commands/hexdump.py +++ b/commands/hexdump.py @@ -9,7 +9,7 @@ from commands.base_command import BaseCommand from common.constants import SIZES from common.context_handler import ContextHandler -from common.util import check_version, hex_int, output_line, positive_int +from common.util import check_process, check_version, hex_int, positive_int class HexdumpCommand(BaseCommand): @@ -66,7 +66,8 @@ def get_long_help() -> str: """Return a longer help message""" return HexdumpCommand.get_command_parser().format_help() - @check_version("15.2.0") + @check_version("15.0.0") + @check_process def __call__( self, debugger: SBDebugger, @@ -76,10 +77,6 @@ def __call__( ) -> None: """Handles the invocation of the hexdump command""" - if not exe_ctx.process.is_alive: - output_line("hexdump requires a running process") - return - args = self.parser.parse_args(shlex.split(command)) divisions = SIZES[args.type.upper()].value diff --git a/commands/pattern.py b/commands/pattern.py index fe79c29..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 output_line, print_message class PatternContainer(BaseContainer): diff --git a/commands/scan.py b/commands/scan.py index 2e944c5..446f85a 100644 --- a/commands/scan.py +++ b/commands/scan.py @@ -9,9 +9,10 @@ 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.scan_util import parse_address_ranges from common.state import LLEFState -from common.util import check_process, print_message +from common.util import check_process class ScanCommand(BaseCommand): diff --git a/common/base_settings.py b/common/base_settings.py index d09a1ee..795ad46 100644 --- a/common/base_settings.py +++ b/common/base_settings.py @@ -4,8 +4,9 @@ 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): @@ -23,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 e8033b6..291764b 100644 --- a/common/color_settings.py +++ b/common/color_settings.py @@ -5,8 +5,8 @@ from common.base_settings import BaseLLEFSettings from common.constants import TERM_COLORS +from common.output_util import output_line from common.singleton import Singleton -from common.util import output_line class LLEFColorSettings(BaseLLEFSettings, metaclass=Singleton): diff --git a/common/context_handler.py b/common/context_handler.py index acd8083..630d125 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -20,13 +20,20 @@ from arch.base_arch import BaseArch, FlagRegister from common.color_settings import LLEFColorSettings from common.constants import GLYPHS, TERM_COLORS +from common.output_util import ( + clear_page, + color_string, + output_line, + print_instruction, + print_instructions, + print_line, + print_line_with_string, +) from common.settings import LLEFSettings from common.state import LLEFState from common.util import ( address_to_filename, attempt_to_read_string_from_memory, - change_use_color, - clear_page, extract_instructions, get_frame_arguments, get_frame_range, @@ -34,11 +41,6 @@ is_code, is_heap, is_stack, - output_line, - print_instruction, - print_instructions, - print_line, - print_line_with_string, ) @@ -68,7 +70,7 @@ def __init__( self.color_settings = LLEFColorSettings() self.state = LLEFState() self.stack_regions = List[SBMemoryRegionInfo] - change_use_color(self.settings.color_output) + self.state.change_use_color(self.settings.color_output) def generate_rebased_address_string(self, address: SBAddress) -> str: module = address.GetModule() @@ -76,11 +78,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 "" @@ -98,11 +96,9 @@ def generate_printable_line_from_pointer( 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}" + 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)) @@ -110,11 +106,8 @@ def generate_printable_line_from_pointer( 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: @@ -124,23 +117,20 @@ 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() @@ -156,13 +146,12 @@ def print_stack_addr(self, addr: SBValue, offset: int) -> None: 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() @@ -178,10 +167,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 +196,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] + highlight = self.color_settings.modified_register_color if is_code(reg_value, self.process, self.target, self.regions): - color = TERM_COLORS[self.color_settings.code_color] + color = self.color_settings.code_color elif is_stack(reg_value, self.regions, self.stack_regions): - color = TERM_COLORS[self.color_settings.stack_color] + color = self.color_settings.stack_color elif is_heap(reg_value, self.target, self.regions, self.stack_regions): - color = TERM_COLORS[self.color_settings.heap_color] + 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,16 +223,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] + highlight = self.color_settings.modified_register_color - line = f"{highlight.value}{flag_register.name.ljust(7)}{TERM_COLORS.ENDC.value}: [" - line += " ".join( + 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: @@ -265,23 +248,21 @@ 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=" | ") + 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: @@ -307,8 +288,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() @@ -321,8 +302,8 @@ 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, ) pc = self.frame.GetPC() @@ -335,10 +316,7 @@ def display_code(self) -> None: pre_instructions = extract_instructions(self.target, frame_start_address, pc - 1, self.state.disassembly_syntax) print_instructions( - pre_instructions[-3:], - frame_start_address, - self.target, - TERM_COLORS[self.color_settings.instruction_color].value, + pre_instructions[-3:], frame_start_address, self.target, self.color_settings.instruction_color ) post_instructions = extract_instructions(self.target, pc, frame_end_address, self.state.disassembly_syntax) @@ -349,7 +327,7 @@ def display_code(self) -> None: pc_instruction, frame_start_address, self.target, - TERM_COLORS[self.color_settings.highlighted_instruction_color].value, + self.color_settings.highlighted_instruction_color, ) limit = 9 - min(len(pre_instructions), 3) @@ -359,8 +337,8 @@ 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) @@ -371,16 +349,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() @@ -390,14 +368,12 @@ 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( @@ -418,16 +394,6 @@ def load_disassembly_syntax(self, debugger: SBDebugger) -> None: if result.Succeeded(): self.state.disassembly_syntax = result.GetOutput().split("=")[1][1:].replace("\n", "") - def find_stack_regions(self) -> List[SBMemoryRegionInfo]: - stack_regions = [] - for frame in self.process.GetSelectedThread().frames: - sp = frame.GetSP() - region = SBMemoryRegionInfo() - self.process.GetMemoryRegionInfo(sp, region) - stack_regions.append(region) - - return stack_regions - def refresh(self, exe_ctx: SBExecutionContext) -> None: """Refresh stored values""" self.process = exe_ctx.GetProcess() @@ -479,4 +445,19 @@ def display_context(self, exe_ctx: SBExecutionContext, update_registers: bool) - elif section == "trace" and self.settings.show_trace: self.display_trace() - print_line(color=TERM_COLORS[self.color_settings.line_color]) + print_line(color=self.color_settings.line_color) + + def find_stack_regions(self) -> 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 self.process.GetSelectedThread().frames: + sp = frame.GetSP() + region = SBMemoryRegionInfo() + self.process.GetMemoryRegionInfo(sp, region) + stack_regions.append(region) + + return stack_regions diff --git a/common/dereference_util.py b/common/dereference_util.py index 4e683e3..0d22fea 100644 --- a/common/dereference_util.py +++ b/common/dereference_util.py @@ -2,7 +2,8 @@ from common.color_settings import LLEFColorSettings from common.constants import GLYPHS, MSG_TYPE, TERM_COLORS -from common.util import attempt_to_read_string_from_memory, hex_or_str, is_code, output_line, print_message +from common.output_util import output_line, print_message +from common.util import attempt_to_read_string_from_memory, hex_or_str, is_code color_settings = LLEFColorSettings() diff --git a/common/output_util.py b/common/output_util.py new file mode 100644 index 0000000..2edef00 --- /dev/null +++ b/common/output_util.py @@ -0,0 +1,166 @@ +"""Utility functions related to terminal output.""" + +import os +import re +from typing import Any, List + +from lldb import SBInstruction, SBTarget + +from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, 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 = string + 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. + """ + return os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + + +def output_line(line: Any) -> 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) + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + if LLEFState().use_color is False: + line = ansi_escape.sub("", line) + print(line) + + +def clear_page() -> None: + """ + Used to clear the previously printed breakpoint information before + printing the next information. + """ + num_lines = os.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: 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) + + +def print_line(char: GLYPHS = GLYPHS.HORIZONTAL_LINE, color: str = TERM_COLORS.GREY.name) -> None: + """Print a line of @char""" + output_line(color_string(terminal_columns() * char.value, color)) + + +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: + output_line(color_string("[i] ", info_color, rwrap=message)) + elif msg_type == MSG_TYPE.SUCCESS: + output_line(color_string("[+] ", success_color, rwrap=message)) + elif msg_type == MSG_TYPE.ERROR: + output_line(color_string("[-] ", error_color, rwrap=message)) + + +def print_instruction( + instruction: SBInstruction, + base: int, + target: SBTarget, + color_setting: str = TERM_COLORS.ENDC.name, +) -> None: + """ + Print formatted @instruction extracted from SBInstruction object. + + :param instruction: The instruction object. + :param base: The address base to calculate offsets from. + :param target: The target executable. + :param color_setting: The color that will be fetched from TERM_COLORS (i.e., TERM_COLORS[color_setting]). + """ + + address = instruction.GetAddress().GetLoadAddress(target) + offset = address - base + + line = f"{hex(address)} <+{offset:02}>: " + if instruction is None: + line += "INVALID INSTRUCTION" + else: + mnemonic = instruction.GetMnemonic(target) or "" + operands = instruction.GetOperands(target) or "" + comment = instruction.GetComment(target) or "" + if comment != "": + comment = f"; {comment}" + line += f"{mnemonic:<10}{operands:<30}{comment}" + + output_line(color_string(line, color_setting)) + + +def print_instructions( + instructions: List[SBInstruction], + base: int, + target: SBTarget, + color_setting: str = TERM_COLORS.ENDC.name, +) -> None: + """ + Print formatted @instructions extracting information from the SBInstruction objects. + + :param instructions: A list of instruction objects. + :param base: The address base to calculate offsets from. + :param target: The target executable. + :param color_setting: The color that will be fetched from TERM_COLORS (i.e., TERM_COLORS[color_setting]). + """ + for instruction in instructions: + print_instruction(instruction, base, target, color_setting) diff --git a/common/settings.py b/common/settings.py index c871526..e352fdd 100644 --- a/common/settings.py +++ b/common/settings.py @@ -7,8 +7,8 @@ from arch import supported_arch from common.base_settings import BaseLLEFSettings from common.constants import MSG_TYPE +from common.output_util import output_line, print_message from common.singleton import Singleton -from common.util import change_use_color, output_line, print_message class LLEFSettings(BaseLLEFSettings, metaclass=Singleton): @@ -130,8 +130,8 @@ 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) def load(self, reset=False): super().load(reset) - change_use_color(self.color_output) + self.state.change_use_color(self.color_output) diff --git a/common/state.py b/common/state.py index 09d58a0..39726a2 100644 --- a/common/state.py +++ b/common/state.py @@ -29,3 +29,9 @@ class LLEFState(metaclass=Singleton): platform = "" disassembly_syntax = None + + 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 diff --git a/common/util.py b/common/util.py index 29ea23e..47f779a 100644 --- a/common/util.py +++ b/common/util.py @@ -1,9 +1,8 @@ """Utility functions.""" import os -import re from argparse import ArgumentTypeError -from typing import Any, List, Tuple +from typing import List, Tuple from lldb import ( SBAddress, @@ -18,7 +17,8 @@ SBValue, ) -from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, GLYPHS, MAGIC_BYTES, MSG_TYPE, TERM_COLORS +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 @@ -26,83 +26,6 @@ def terminal_columns() -> int: return os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS -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 output_line(line: Any) -> 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) - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") - if LLEFState.use_color is False: - line = ansi_escape.sub("", line) - print(line) - - -def clear_page() -> None: - """ - Used to clear the previously printed breakpoint information before - printing the next information. - """ - num_lines = os.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 = 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 - - 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}{terminal_columns() * char.value}{TERM_COLORS.ENDC.value}") - - -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 address_to_filename(target: SBTarget, address: int) -> str: """ Maps a memory address to its corrosponding executable/library and returns the filename. @@ -145,34 +68,6 @@ def extract_instructions( return instructions -def print_instruction( - instruction: SBInstruction, base: int, target: SBTarget, color: TERM_COLORS = TERM_COLORS.ENDC.value -) -> None: - """Format and print a line of disassembly returned from LLDB (SBFrame.disassembly)""" - - address = instruction.GetAddress().GetLoadAddress(target) - offset = address - base - - if instruction is None: - output_line(f"{color}{hex(address)} <+{offset:02}>: INVALID INSTRUCTION") - return - - mnemonic = instruction.GetMnemonic(target) or "" - operands = instruction.GetOperands(target) or "" - comment = instruction.GetComment(target) or "" - if comment != "": - comment = f"; {comment}" - - output_line(f"{color}{hex(address)} <+{offset:02}>: {mnemonic:<10}{operands:<30}{comment}{TERM_COLORS.ENDC.value}") - - -def print_instructions( - instructions: List[SBInstruction], base: int, target: SBTarget, color: TERM_COLORS = TERM_COLORS.ENDC.value -) -> None: - for instruction in instructions: - print_instruction(instruction, base, target, color) - - def get_frame_range(frame: SBFrame, target: SBTarget) -> Tuple[str, str]: function = frame.GetFunction() if function: From 0c6b5d45267d0a4d568739bbf29b5363c7228124 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:36:38 +0000 Subject: [PATCH 24/32] Move command functions * Moved checksec command functions from checksec_util.py to checksec.py * Moved scan command functions from scan_util.py to scan.py * Moved dereference command functions from dereference_util.py to dereference.py. Modified some output code to use color_string function. * Fixed library ordering. * Removed broken import. * read_instruction function now uses the disassembly syntax/flavour setting --- commands/checksec.py | 93 ++++++++++++++++++++++++++++++---- commands/dereference.py | 100 +++++++++++++++++++++++++++++++++++-- commands/scan.py | 59 ++++++++++++++++++++-- common/checksec_util.py | 82 ------------------------------ common/dereference_util.py | 84 ------------------------------- common/scan_util.py | 61 ---------------------- 6 files changed, 235 insertions(+), 244 deletions(-) delete mode 100644 common/checksec_util.py delete mode 100644 common/dereference_util.py delete mode 100644 common/scan_util.py diff --git a/commands/checksec.py b/commands/checksec.py index 67497a2..f42af2e 100644 --- a/commands/checksec.py +++ b/commands/checksec.py @@ -3,11 +3,12 @@ import argparse from typing import Any, Dict -from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext +from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBTarget +from arch import get_arch from commands.base_command import BaseCommand -from common.checksec_util import get_dynamic_entry, get_executable_type, get_program_header_permission from common.constants import ( + ARCH_BITS, DYNAMIC_ENTRY_TYPE, DYNAMIC_ENTRY_VALUE, EXECUTABLE_TYPE, @@ -19,7 +20,17 @@ ) from common.context_handler import ContextHandler from common.output_util import output_line, print_message -from common.util import check_elf, check_target +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): @@ -50,6 +61,70 @@ 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 + @check_target @check_elf def __call__( @@ -81,7 +156,7 @@ def __call__( break try: - if get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_STACK) in PERMISSION_SET.NOT_EXEC: + if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_STACK) in PERMISSION_SET.NOT_EXEC: checks["NX Support"] = SECURITY_CHECK.YES else: checks["NX Support"] = SECURITY_CHECK.NO @@ -90,7 +165,7 @@ def __call__( checks["NX Support"] = SECURITY_CHECK.UNKNOWN try: - if get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_RELRO) is not None: + if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_RELRO) is not None: checks["Partial RelRO"] = SECURITY_CHECK.YES else: checks["Partial RelRO"] = SECURITY_CHECK.NO @@ -99,7 +174,7 @@ def __call__( checks["Partial RelRO"] = SECURITY_CHECK.UNKNOWN try: - if get_executable_type(target) == EXECUTABLE_TYPE.DYN: + if self.get_executable_type(target) == EXECUTABLE_TYPE.DYN: checks["PIE Support"] = SECURITY_CHECK.YES else: checks["PIE Support"] = SECURITY_CHECK.NO @@ -110,19 +185,19 @@ def __call__( if checks["Partial RelRO"] == SECURITY_CHECK.UNKNOWN: checks["Full RelRO"] = SECURITY_CHECK.UNKNOWN elif ( - get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.FLAGS) == DYNAMIC_ENTRY_VALUE.BIND_NOW + self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.FLAGS) == DYNAMIC_ENTRY_VALUE.BIND_NOW and checks["Partial RelRO"] == SECURITY_CHECK.YES ): checks["Full RelRO"] = SECURITY_CHECK.YES else: checks["Full RelRO"] = SECURITY_CHECK.NO - if get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RPATH) is None: + if self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RPATH) is None: checks["No RPath"] = SECURITY_CHECK.YES else: checks["No RPath"] = SECURITY_CHECK.NO - if get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RUNPATH) is None: + if self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RUNPATH) is None: checks["No RunPath"] = SECURITY_CHECK.YES else: checks["No RunPath"] = SECURITY_CHECK.NO diff --git a/commands/dereference.py b/commands/dereference.py index 0b01a92..09ada17 100644 --- a/commands/dereference.py +++ b/commands/dereference.py @@ -4,12 +4,25 @@ import shlex from typing import Any, Dict -from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext +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, MSG_TYPE, TERM_COLORS from common.context_handler import ContextHandler -from common.dereference_util import dereference -from common.util import check_process, hex_int, positive_int +from common.output_util import color_string, output_line, print_message +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): @@ -23,6 +36,8 @@ 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: @@ -59,6 +74,83 @@ 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, offset: int, target: SBTarget, process: SBProcess, regions: SBMemoryRegionInfoList + ): + """ + 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("[LOOPING]") + break + + if len(data) < 2: + print_message(MSG_TYPE.ERROR, f"{hex(data[0])} is not accessible.") + return + + self.dereference_last_address(data, target, process, regions) + + output = color_string(hex_or_str(data[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, data[1:])) + output_line(output) + @check_process def __call__( self, @@ -85,4 +177,4 @@ def __call__( end_address = start_address + address_size * lines for address in range(start_address, end_address, address_size): offset = address - base - dereference(address, offset, exe_ctx.target, exe_ctx.process, self.context_handler.regions) + self.dereference(address, offset, exe_ctx.target, exe_ctx.process, self.context_handler.regions) diff --git a/commands/scan.py b/commands/scan.py index 446f85a..d65563d 100644 --- a/commands/scan.py +++ b/commands/scan.py @@ -4,13 +4,12 @@ import shlex from typing import Any, Dict -from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext +from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBMemoryRegionInfo, SBProcess 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.scan_util import parse_address_ranges from common.state import LLEFState from common.util import check_process @@ -54,6 +53,58 @@ 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): + """ + 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): + """ + 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 + @check_process def __call__( self, @@ -70,8 +121,8 @@ def __call__( self.context_handler.refresh(exe_ctx) - search_address_ranges = parse_address_ranges(exe_ctx.process, search_region) - target_address_ranges = parse_address_ranges(exe_ctx.process, target_region) + 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( diff --git a/common/checksec_util.py b/common/checksec_util.py deleted file mode 100644 index f818fd5..0000000 --- a/common/checksec_util.py +++ /dev/null @@ -1,82 +0,0 @@ -from lldb import SBError, SBTarget - -from arch import get_arch -from common.constants import ARCH_BITS -from common.util import 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 - - -def get_executable_type(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(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(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 diff --git a/common/dereference_util.py b/common/dereference_util.py deleted file mode 100644 index 0d22fea..0000000 --- a/common/dereference_util.py +++ /dev/null @@ -1,84 +0,0 @@ -from lldb import SBAddress, SBError, SBInstruction, SBMemoryRegionInfoList, SBProcess, SBTarget - -from common.color_settings import LLEFColorSettings -from common.constants import GLYPHS, MSG_TYPE, TERM_COLORS -from common.output_util import output_line, print_message -from common.util import attempt_to_read_string_from_memory, hex_or_str, is_code - -color_settings = LLEFColorSettings() - - -def read_instruction(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, "intel") - return instruction_list.GetInstructionAtIndex(0) - - -def dereference_last_address(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 = read_instruction(target, last_address) - if instruction.IsValid(): - data[-1] = ( - f"{TERM_COLORS[color_settings.instruction_color].value}{instruction.GetMnemonic(target)} " - + f"{instruction.GetOperands(target)}{TERM_COLORS.ENDC.value}" - ) - else: - string = attempt_to_read_string_from_memory(process, last_address) - if string != "": - data[-1] = f"{TERM_COLORS[color_settings.string_color].value}{string}{TERM_COLORS.ENDC.value}" - - -def dereference(address: int, offset: int, target: SBTarget, process: SBProcess, regions: SBMemoryRegionInfoList): - """ - 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("[LOOPING]") - break - - if len(data) < 2: - print_message(MSG_TYPE.ERROR, f"{hex(data[0])} is not accessible.") - return - - dereference_last_address(data, target, process, regions) - - output = f"{TERM_COLORS.CYAN.value}{hex_or_str(data[0])}{TERM_COLORS.ENDC.value}{GLYPHS.VERTICAL_LINE.value}" - if offset >= 0: - output += f"+0x{offset:04x}: " - else: - output += f"-0x{-offset:04x}: " - output += " -> ".join(map(hex_or_str, data[1:])) - output_line(output) diff --git a/common/scan_util.py b/common/scan_util.py deleted file mode 100644 index e9d5305..0000000 --- a/common/scan_util.py +++ /dev/null @@ -1,61 +0,0 @@ -from lldb import SBMemoryRegionInfo, SBProcess - -from common.color_settings import LLEFColorSettings -from common.constants import MSG_TYPE -from common.util import print_message - -color_settings = LLEFColorSettings() - - -def parse_address_ranges(process: SBProcess, region_name: str): - """ - 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 = find_address_ranges(process, region_name) - - return address_ranges - - -def find_address_ranges(process: SBProcess, region_name: str): - """ - 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 From 99d57f296f5865e68d3768e1196a6a71ea592059 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:37:14 +0000 Subject: [PATCH 25/32] Seperate data and output * Seperate xinfo data into seperate function get_xinfo. * Seperate checksec data into seperate function check_security. * Define each security feature in an enum. Used color_string to color output results. * Seperate dereference output and data into seperate functions dereference and print_dereference_result. * Seperate scan data into seperate function called scan. Added return types to functions. * Fixed Flake8 issue. * Fixed typo. * Define each xinfo information in an enum. * Minor output change. --- commands/checksec.py | 123 ++++++++++++++++++++++-------------- commands/dereference.py | 28 ++++---- commands/scan.py | 61 ++++++++++++------ commands/xinfo.py | 137 ++++++++++++++++++++++++++-------------- common/constants.py | 20 ++++++ 5 files changed, 243 insertions(+), 126 deletions(-) diff --git a/commands/checksec.py b/commands/checksec.py index f42af2e..ccedf73 100644 --- a/commands/checksec.py +++ b/commands/checksec.py @@ -16,10 +16,11 @@ PERMISSION_SET, PROGRAM_HEADER_TYPE, SECURITY_CHECK, + SECURITY_FEATURE, TERM_COLORS, ) from common.context_handler import ContextHandler -from common.output_util import output_line, print_message +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 @@ -125,89 +126,113 @@ def get_dynamic_entry(self, target: SBTarget, target_entry_type: int): return target_entry_value - @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() - + 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 = { - "Canary": SECURITY_CHECK.NO, - "NX Support": SECURITY_CHECK.UNKNOWN, - "PIE Support": SECURITY_CHECK.UNKNOWN, - "No RPath": SECURITY_CHECK.UNKNOWN, - "No RunPath": SECURITY_CHECK.UNKNOWN, - "Partial RelRO": SECURITY_CHECK.UNKNOWN, - "Full RelRO": SECURITY_CHECK.UNKNOWN, + 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["Canary"] = SECURITY_CHECK.YES + 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["NX Support"] = SECURITY_CHECK.YES + checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.YES else: - checks["NX Support"] = SECURITY_CHECK.NO + checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.NO except MemoryError as error: print_message(MSG_TYPE.ERROR, error) - checks["NX Support"] = SECURITY_CHECK.UNKNOWN + checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.UNKNOWN + # Check for PIE Support try: - if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_RELRO) is not None: - checks["Partial RelRO"] = SECURITY_CHECK.YES + if self.get_executable_type(target) == EXECUTABLE_TYPE.DYN: + checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.YES else: - checks["Partial RelRO"] = SECURITY_CHECK.NO + checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.NO except MemoryError as error: print_message(MSG_TYPE.ERROR, error) - checks["Partial RelRO"] = SECURITY_CHECK.UNKNOWN + checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.UNKNOWN + # Check for Partial RelRO try: - if self.get_executable_type(target) == EXECUTABLE_TYPE.DYN: - checks["PIE Support"] = SECURITY_CHECK.YES + 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["PIE Support"] = SECURITY_CHECK.NO + checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.NO except MemoryError as error: print_message(MSG_TYPE.ERROR, error) - checks["PIE Support"] = SECURITY_CHECK.UNKNOWN + checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.UNKNOWN - if checks["Partial RelRO"] == SECURITY_CHECK.UNKNOWN: - checks["Full 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["Partial RelRO"] == SECURITY_CHECK.YES + and checks[SECURITY_FEATURE.PARTIAL_RELRO] == SECURITY_CHECK.YES ): - checks["Full RelRO"] = SECURITY_CHECK.YES + checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.YES else: - checks["Full RelRO"] = SECURITY_CHECK.NO + 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["No RPath"] = SECURITY_CHECK.YES + checks[SECURITY_FEATURE.NO_RPATH] = SECURITY_CHECK.YES else: - checks["No RPath"] = SECURITY_CHECK.NO + 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["No RunPath"] = SECURITY_CHECK.YES + checks[SECURITY_FEATURE.NO_RUNPATH] = SECURITY_CHECK.YES else: - checks["No RunPath"] = SECURITY_CHECK.NO + 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.value + color = TERM_COLORS.GREEN.name elif status == SECURITY_CHECK.NO: - color = TERM_COLORS.RED.value + color = TERM_COLORS.RED.name else: - color = TERM_COLORS.GREY.value - check += ": " - output_line(f"{check:<20} {color}{status.value}{TERM_COLORS.ENDC.value}") + 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/dereference.py b/commands/dereference.py index 09ada17..628d733 100644 --- a/commands/dereference.py +++ b/commands/dereference.py @@ -2,7 +2,7 @@ import argparse import shlex -from typing import Any, Dict +from typing import Any, Dict, List from lldb import ( SBAddress, @@ -18,9 +18,9 @@ from commands.base_command import BaseCommand from common.color_settings import LLEFColorSettings -from common.constants import GLYPHS, MSG_TYPE, TERM_COLORS +from common.constants import GLYPHS, TERM_COLORS from common.context_handler import ContextHandler -from common.output_util import color_string, output_line, print_message +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 @@ -112,9 +112,7 @@ def dereference_last_address( if string != "": data[-1] = color_string(string, self.color_settings.string_color) - def dereference( - self, address: int, offset: int, target: SBTarget, process: SBProcess, regions: SBMemoryRegionInfoList - ): + 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. @@ -134,21 +132,24 @@ def dereference( data.append(address) address = process.ReadPointerFromMemory(address, error) if len(data) > 1 and data[-1] in data[:-2]: - data.append("[LOOPING]") + data.append(color_string("[LOOPING]", TERM_COLORS.GREY.name)) break if len(data) < 2: - print_message(MSG_TYPE.ERROR, f"{hex(data[0])} is not accessible.") - return + data.append(color_string("NOT ACCESSIBLE", TERM_COLORS.RED.name)) + else: + self.dereference_last_address(data, target, process, regions) - self.dereference_last_address(data, target, process, regions) + return data - output = color_string(hex_or_str(data[0]), TERM_COLORS.CYAN.name, rwrap=GLYPHS.VERTICAL_LINE.value) + 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, data[1:])) + output += " -> ".join(map(hex_or_str, result[1:])) output_line(output) @check_process @@ -177,4 +178,5 @@ def __call__( end_address = start_address + address_size * lines for address in range(start_address, end_address, address_size): offset = address - base - self.dereference(address, offset, exe_ctx.target, exe_ctx.process, self.context_handler.regions) + result = self.dereference(address, exe_ctx.target, exe_ctx.process, self.context_handler.regions) + self.print_dereference_result(result, offset) diff --git a/commands/scan.py b/commands/scan.py index d65563d..dd82ed5 100644 --- a/commands/scan.py +++ b/commands/scan.py @@ -2,9 +2,9 @@ import argparse import shlex -from typing import Any, Dict +from typing import Any, Dict, List, Tuple -from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBMemoryRegionInfo, SBProcess +from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBMemoryRegionInfo, SBProcess, SBTarget from commands.base_command import BaseCommand from common.constants import MSG_TYPE @@ -53,7 +53,7 @@ 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): + 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). @@ -70,7 +70,7 @@ def parse_address_ranges(self, process: SBProcess, region_name: str): try: region_start = int(region_start_end[0], 16) region_end = int(region_start_end[1], 16) - address_ranges.append([region_start, region_end]) + address_ranges.append((region_start, region_end)) except ValueError: print_message(MSG_TYPE.ERROR, "Invalid address range.") else: @@ -78,7 +78,7 @@ def parse_address_ranges(self, process: SBProcess, region_name: str): return address_ranges - def find_address_ranges(self, process: SBProcess, region_name: str): + def find_address_ranges(self, process: SBProcess, region_name: str) -> List[Tuple[int, int]]: """ Extract address ranges from memory regions with @region_name. @@ -101,10 +101,44 @@ def find_address_ranges(self, process: SBProcess, region_name: str): ): region_start = memory_region.GetRegionBase() region_end = memory_region.GetRegionEnd() - address_ranges.append([region_start, region_end]) + 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, @@ -135,15 +169,6 @@ def __call__( address_size = exe_ctx.target.GetAddressByteSize() - error = SBError() - for search_start, search_end in search_address_ranges: - for search_address in range(search_start, search_end, address_size): - target_address = exe_ctx.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 = exe_ctx.target.EvaluateExpression(f"{search_address}") - self.context_handler.print_stack_addr(search_address_value, offset) - else: - print_message(MSG_TYPE.ERROR, f"Memory at {search_address} couldn't be read.") + 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/xinfo.py b/commands/xinfo.py index 29103b9..ce757b1 100644 --- a/commands/xinfo.py +++ b/commands/xinfo.py @@ -5,14 +5,23 @@ import shlex from typing import Any, Dict -from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext, SBMemoryRegionInfo, SBStream +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 +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, print_message +from common.util import check_process, hex_int class XinfoCommand(BaseCommand): @@ -49,6 +58,65 @@ 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 + + return xinfo + @check_process def __call__( self, @@ -66,47 +134,24 @@ def __call__( print_message(MSG_TYPE.ERROR, "Invalid address.") return - memory_region = SBMemoryRegionInfo() - error = exe_ctx.process.GetMemoryRegionInfo(address, memory_region) - - if error.Fail(): - print_message(MSG_TYPE.ERROR, "Couldn't obtain region info") - return - - if not memory_region.IsMapped(): - print_message(MSG_TYPE.ERROR, f"Not Found: {hex(address)}") - return - - print_message(MSG_TYPE.SUCCESS, f"Found: {hex(address)}") - - start = memory_region.GetRegionBase() - end = memory_region.GetRegionEnd() - size = end - start - print_message( - MSG_TYPE.INFO, - f"Page/Region: {hex(start)}->{hex(end)} (size={hex(size)})", - ) - - permissions = "" - permissions += "r" if memory_region.IsReadable() else "" - permissions += "w" if memory_region.IsWritable() else "" - permissions += "x" if memory_region.IsExecutable() else "" - print_message(MSG_TYPE.INFO, f"Permissions: {permissions}") - - if self.state.platform == "Darwin": - sb_address = exe_ctx.target.ResolveLoadAddress(address) - module = sb_address.GetModule() - filespec = module.GetFileSpec() - description = SBStream() - filespec.GetDescription(description) - path = description.GetData() + 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["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: - path = memory_region.GetName() - print_message(MSG_TYPE.INFO, f"Pathname: {path}") - - print_message(MSG_TYPE.INFO, f"Offset (from page/region): +{hex(address - memory_region.GetRegionBase())}") - - if path is not None and os.path.exists(path): - print_message(MSG_TYPE.INFO, f"Inode: {os.stat(path).st_ino}") - else: - print_message(MSG_TYPE.ERROR, "No inode found: Path cannot be found locally.") + print_message(MSG_TYPE.ERROR, f"Not Found: {hex(address)}") diff --git a/common/constants.py b/common/constants.py index 706f33b..e9a0ee6 100644 --- a/common/constants.py +++ b/common/constants.py @@ -54,6 +54,26 @@ class SIZES(Enum): 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" From db33981ae732fff40348778c3fe94e6943575b1a Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:38:45 +0000 Subject: [PATCH 26/32] Code changes from test branch --- common/constants.py | 1 + common/context_handler.py | 18 ++---------------- common/output_util.py | 39 ++++++++++++++++++++++++++------------- common/settings.py | 20 ++++++++++---------- common/util.py | 39 +++++++++++++++++++++++++++++++++------ 5 files changed, 72 insertions(+), 45 deletions(-) diff --git a/common/constants.py b/common/constants.py index e9a0ee6..d554422 100644 --- a/common/constants.py +++ b/common/constants.py @@ -134,3 +134,4 @@ class MAGIC_BYTES(Enum): DEFAULT_TERMINAL_COLUMNS = 80 +DEFAULT_TERMINAL_LINES = 24 diff --git a/common/context_handler.py b/common/context_handler.py index 630d125..907037b 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -35,6 +35,7 @@ address_to_filename, attempt_to_read_string_from_memory, extract_instructions, + find_stack_regions, get_frame_arguments, get_frame_range, get_registers, @@ -414,7 +415,7 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: self.load_disassembly_syntax(self.debugger) if LLEFState.platform == "Darwin": - self.stack_regions = self.find_stack_regions() + self.stack_regions = find_stack_regions(self.process) 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`""" @@ -446,18 +447,3 @@ def display_context(self, exe_ctx: SBExecutionContext, update_registers: bool) - self.display_trace() print_line(color=self.color_settings.line_color) - - def find_stack_regions(self) -> 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 self.process.GetSelectedThread().frames: - sp = frame.GetSP() - region = SBMemoryRegionInfo() - self.process.GetMemoryRegionInfo(sp, region) - stack_regions.append(region) - - return stack_regions diff --git a/common/output_util.py b/common/output_util.py index 2edef00..4341c47 100644 --- a/common/output_util.py +++ b/common/output_util.py @@ -6,7 +6,7 @@ from lldb import SBInstruction, SBTarget -from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, GLYPHS, MSG_TYPE, TERM_COLORS +from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, DEFAULT_TERMINAL_LINES, GLYPHS, MSG_TYPE, TERM_COLORS from common.state import LLEFState @@ -22,7 +22,7 @@ def color_string(string: str, color_setting: str, lwrap: str = "", rwrap: str = :return: The resulting string. """ if color_setting is None: - result = string + result = f"{lwrap}{string}{rwrap}" else: result = f"{lwrap}{TERM_COLORS[color_setting].value}{string}{TERM_COLORS.ENDC.value}{rwrap}" @@ -34,7 +34,12 @@ 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. """ - return os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + try: + columns = os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + except OSError: + columns = DEFAULT_TERMINAL_COLUMNS + + return columns def output_line(line: Any) -> None: @@ -54,7 +59,11 @@ def clear_page() -> None: Used to clear the previously printed breakpoint information before printing the next information. """ - num_lines = os.get_terminal_size().lines + try: + num_lines = os.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 @@ -114,6 +123,8 @@ def print_message(msg_type: MSG_TYPE, message: str) -> None: output_line(color_string("[+] ", success_color, rwrap=message)) elif msg_type == MSG_TYPE.ERROR: output_line(color_string("[-] ", error_color, rwrap=message)) + else: + raise KeyError(f"{msg_type} is an invalid MSG_TYPE.") def print_instruction( @@ -134,16 +145,18 @@ def print_instruction( address = instruction.GetAddress().GetLoadAddress(target) offset = address - base - line = f"{hex(address)} <+{offset:02}>: " - if instruction is None: - line += "INVALID INSTRUCTION" + line = hex(address) + if offset >= 0: + line += f" <+{offset:02}>: " else: - mnemonic = instruction.GetMnemonic(target) or "" - operands = instruction.GetOperands(target) or "" - comment = instruction.GetComment(target) or "" - if comment != "": - comment = f"; {comment}" - line += f"{mnemonic:<10}{operands:<30}{comment}" + line += f" <-{abs(offset):02}>: " + + mnemonic = instruction.GetMnemonic(target) or "" + operands = instruction.GetOperands(target) or "" + comment = instruction.GetComment(target) or "" + if comment != "": + comment = f"; {comment}" + line += f"{mnemonic:<10}{operands:<30}{comment}" output_line(color_string(line, color_setting)) diff --git a/common/settings.py b/common/settings.py index e352fdd..b07204f 100644 --- a/common/settings.py +++ b/common/settings.py @@ -81,13 +81,15 @@ def validate_output_order(self, value: str): default_sections = self.DEFAUL_OUTPUT_ORDER.split(",") sections = value.split(",") if len(sections) != len(default_sections): - print_message(MSG_TYPE.ERROR, f"Requires {len(default_sections)} elements: '{','.join(default_sections)}'") - raise ValueError + 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) - missing_sections = set(default_sections) - set(sections) if len(missing_sections) > 0: - print_message(MSG_TYPE.ERROR, f"Missing '{','.join(missing_sections)}' from output order.") - raise ValueError + raise ValueError(f"Missing '{','.join(missing_sections)}' from output order.") def validate_settings(self, setting=None) -> bool: """ @@ -111,15 +113,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 + raise ValueError("Colour is not supported by your terminal") elif setting_name == "output_order": self.validate_output_order(value) - except ValueError: + 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): diff --git a/common/util.py b/common/util.py index 47f779a..a1d7996 100644 --- a/common/util.py +++ b/common/util.py @@ -36,7 +36,7 @@ def address_to_filename(target: SBTarget, address: int) -> str: """ sb_address = SBAddress(address, target) module = sb_address.GetModule() - file_spec = module.GetFileSpec() + file_spec = module.GetSymbolFileSpec() filename = file_spec.GetFilename() return filename @@ -229,6 +229,17 @@ def extract_arch_from_triple(triple: str) -> str: 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 corrosponding Clang version. @@ -237,12 +248,12 @@ def lldb_version_to_clang(lldb_version): :return: The Clang version. """ - clang_version = [0, 0, 0, 0] - if lldb_version >= [17, 0, 6]: + clang_version = [0] + if verify_version(lldb_version, [17, 0, 6]): clang_version = [1600, 0, 26, 3] - elif lldb_version >= [16, 0, 0]: + elif verify_version(lldb_version, [16, 0, 0]): clang_version = [1500, 0, 40, 1] - elif lldb_version >= [15, 0, 0]: + elif verify_version(lldb_version, [15, 0, 0]): clang_version = [1403, 0, 22, 14, 1] return clang_version @@ -254,7 +265,7 @@ 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 LLEFState.version < 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) @@ -386,3 +397,19 @@ def read_program_int(target: SBTarget, offset: int, n: int): 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 From 6824faa3e0622e5fe109de4bb81329cad9c25c03 Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:39:35 +0000 Subject: [PATCH 27/32] Bugfix/colouring instructions * Added new colour setting which colours the address operands in the code section. * Refactoring instructions functions into separate file. Added logic to color instruction operands. * Updated python doc comments. * Minor fix. * Operand registers are highlighted using a regex that includes all register names. * Fixed the load disassembly syntax flavor bug. * Fixed the regex for operand highlighting on macos. * Fixed regex for register highlighting. * Fixed library ordering. * Removed unused imports. * Fixed a bug to correctly identify section names by searching section parents. * Removed un-needed register list logic. * Added comments to explain color operand regex patterns. * Fixed a doc comment. --------- Co-authored-by: michael --- common/color_settings.py | 4 ++ common/context_handler.py | 37 ++++++----- common/instruction_util.py | 125 +++++++++++++++++++++++++++++++++++++ common/output_util.py | 56 +---------------- common/state.py | 2 +- common/util.py | 40 +++--------- 6 files changed, 161 insertions(+), 103 deletions(-) create mode 100644 common/instruction_util.py diff --git a/common/color_settings.py b/common/color_settings.py index 291764b..f4728fb 100644 --- a/common/color_settings.py +++ b/common/color_settings.py @@ -95,6 +95,10 @@ def frame_argument_name_color(self): 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/context_handler.py b/common/context_handler.py index 907037b..90189b5 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -20,21 +20,13 @@ from arch.base_arch import BaseArch, FlagRegister from common.color_settings import LLEFColorSettings from common.constants import GLYPHS, TERM_COLORS -from common.output_util import ( - clear_page, - color_string, - output_line, - print_instruction, - print_instructions, - print_line, - print_line_with_string, -) +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.state import LLEFState from common.util import ( address_to_filename, attempt_to_read_string_from_memory, - extract_instructions, find_stack_regions, get_frame_arguments, get_frame_range, @@ -317,7 +309,10 @@ def display_code(self) -> None: pre_instructions = extract_instructions(self.target, frame_start_address, pc - 1, self.state.disassembly_syntax) print_instructions( - pre_instructions[-3:], frame_start_address, self.target, self.color_settings.instruction_color + 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) @@ -325,14 +320,20 @@ def display_code(self) -> None: if len(post_instructions) > 0: pc_instruction = post_instructions[0] print_instruction( + self.target, pc_instruction, frame_start_address, - self.target, - self.color_settings.highlighted_instruction_color, + self.color_settings, + True, ) limit = 9 - min(len(pre_instructions), 3) - print_instructions(post_instructions[1:limit], frame_start_address, self.target) + print_instructions( + self.target, + post_instructions[1:limit], + frame_start_address, + self.color_settings, + ) def display_threads(self) -> None: """Print LLDB formatted thread information""" @@ -388,13 +389,17 @@ def load_disassembly_syntax(self, debugger: SBDebugger) -> None: self.state.disassembly_syntax = "default" if LLEFState.version >= [16]: self.state.disassembly_syntax = debugger.GetSetting("target.x86-disassembly-flavor").GetStringValue(100) - else: + + 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.process = exe_ctx.GetProcess() @@ -411,7 +416,7 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: else: self.regions = None - if self.state.disassembly_syntax is None: + if self.state.disassembly_syntax == "": self.load_disassembly_syntax(self.debugger) if LLEFState.platform == "Darwin": 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 index 4341c47..e48849c 100644 --- a/common/output_util.py +++ b/common/output_util.py @@ -2,9 +2,7 @@ import os import re -from typing import Any, List - -from lldb import SBInstruction, SBTarget +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 @@ -125,55 +123,3 @@ def print_message(msg_type: MSG_TYPE, message: str) -> None: output_line(color_string("[-] ", error_color, rwrap=message)) else: raise KeyError(f"{msg_type} is an invalid MSG_TYPE.") - - -def print_instruction( - instruction: SBInstruction, - base: int, - target: SBTarget, - color_setting: str = TERM_COLORS.ENDC.name, -) -> None: - """ - Print formatted @instruction extracted from SBInstruction object. - - :param instruction: The instruction object. - :param base: The address base to calculate offsets from. - :param target: The target executable. - :param color_setting: The color that will be fetched from TERM_COLORS (i.e., TERM_COLORS[color_setting]). - """ - - 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 comment != "": - comment = f"; {comment}" - line += f"{mnemonic:<10}{operands:<30}{comment}" - - output_line(color_string(line, color_setting)) - - -def print_instructions( - instructions: List[SBInstruction], - base: int, - target: SBTarget, - color_setting: str = TERM_COLORS.ENDC.name, -) -> None: - """ - Print formatted @instructions extracting information from the SBInstruction objects. - - :param instructions: A list of instruction objects. - :param base: The address base to calculate offsets from. - :param target: The target executable. - :param color_setting: The color that will be fetched from TERM_COLORS (i.e., TERM_COLORS[color_setting]). - """ - for instruction in instructions: - print_instruction(instruction, base, target, color_setting) diff --git a/common/state.py b/common/state.py index 39726a2..8d053ab 100644 --- a/common/state.py +++ b/common/state.py @@ -28,7 +28,7 @@ class LLEFState(metaclass=Singleton): # Linux, Mac (Darwin) or Windows platform = "" - disassembly_syntax = None + disassembly_syntax = "" def change_use_color(self, new_value: bool) -> None: """ diff --git a/common/util.py b/common/util.py index a1d7996..1ad54fc 100644 --- a/common/util.py +++ b/common/util.py @@ -9,7 +9,6 @@ SBError, SBExecutionContext, SBFrame, - SBInstruction, SBMemoryRegionInfo, SBMemoryRegionInfoList, SBProcess, @@ -42,32 +41,6 @@ def address_to_filename(target: SBTarget, address: int) -> str: return filename -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 get_frame_range(frame: SBFrame, target: SBTarget) -> Tuple[str, str]: function = frame.GetFunction() if function: @@ -141,10 +114,12 @@ def is_ascii_string(address: SBValue, process: SBProcess) -> bool: return attempt_to_read_string_from_memory(process, address) != "" -def is_in_section(address: SBValue, target: SBTarget, target_section_name: str): +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. @@ -153,9 +128,12 @@ def is_in_section(address: SBValue, target: SBTarget, target_section_name: str): sb_address = target.ResolveLoadAddress(address) section = sb_address.GetSection() - section_name = section.GetName() + full_section_name = "" + while section: + full_section_name = section.GetName() + "." + full_section_name + section = section.GetParent() - return section_name is not None and target_section_name in section_name + return target_section_name in full_section_name def is_text_region(address: SBValue, target: SBTarget, region: SBMemoryRegionInfo) -> bool: @@ -170,7 +148,7 @@ def is_text_region(address: SBValue, target: SBTarget, region: SBMemoryRegionInf in_text = False if is_file(target, MAGIC_BYTES.MACH.value): - if is_in_section(address, target, "__TEXT") or is_in_section(address, target, "__text"): + if is_in_section(address, target, "__TEXT"): in_text = True else: file = target.GetExecutable() From 60a239951d5a4ab0467f62de3ecb7c4815d624ba Mon Sep 17 00:00:00 2001 From: F0Michael Date: Thu, 6 Feb 2025 13:40:11 +0000 Subject: [PATCH 28/32] Added a setting to truncate output lines to fit terminal width. * Added a setting to truncate output lines to fit terminal width. * Fixed library ordering. * Fixed formatting. * Added missing doc comment. Added missing type annotation. * Fixed library ordering. * Removed unused imports. * Improved the truncate_line function. * Improved the truncate_line function. * Info, success and error messages are now never truncated. * Fixed formatting. --- common/output_util.py | 51 +++++++++++++++++++++++++++++++++++-------- common/settings.py | 7 ++++++ common/state.py | 7 ++++++ 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/common/output_util.py b/common/output_util.py index e48849c..94cccd2 100644 --- a/common/output_util.py +++ b/common/output_util.py @@ -2,6 +2,7 @@ import os import re +from textwrap import TextWrapper from typing import Any from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, DEFAULT_TERMINAL_LINES, GLYPHS, MSG_TYPE, TERM_COLORS @@ -40,16 +41,45 @@ def terminal_columns() -> int: return columns -def output_line(line: Any) -> None: +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) - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") if LLEFState().use_color is False: - line = ansi_escape.sub("", line) - print(line) + 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: @@ -101,12 +131,13 @@ def print_line_with_string( line += color_string(string, string_color, " ", " ") line += color_string(r_pad, line_color) - output_line(line) + 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""" - output_line(color_string(terminal_columns() * char.value, color)) + line = color_string(terminal_columns() * char.value, color) + output_line(line, never_truncate=True) def print_message(msg_type: MSG_TYPE, message: str) -> None: @@ -116,10 +147,12 @@ def print_message(msg_type: MSG_TYPE, message: str) -> None: error_color = TERM_COLORS.RED.name if msg_type == MSG_TYPE.INFO: - output_line(color_string("[i] ", info_color, rwrap=message)) + message = color_string("[i] ", info_color, rwrap=message) elif msg_type == MSG_TYPE.SUCCESS: - output_line(color_string("[+] ", success_color, rwrap=message)) + message = color_string("[+] ", success_color, rwrap=message) elif msg_type == MSG_TYPE.ERROR: - output_line(color_string("[-] ", error_color, rwrap=message)) + 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 b07204f..75c4d2a 100644 --- a/common/settings.py +++ b/common/settings.py @@ -77,6 +77,10 @@ def show_all_registers(self): 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) + def validate_output_order(self, value: str): default_sections = self.DEFAUL_OUTPUT_ORDER.split(",") sections = value.split(",") @@ -131,7 +135,10 @@ def set(self, setting: str, value: str): if setting == "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) self.state.change_use_color(self.color_output) + self.state.change_truncate_output(self.truncate_output) diff --git a/common/state.py b/common/state.py index 8d053ab..0a742ff 100644 --- a/common/state.py +++ b/common/state.py @@ -22,6 +22,9 @@ 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 = [] @@ -35,3 +38,7 @@ 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 From c856bfd2c6c1f2c3a920ea583d4cbf18e7bf0eba Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:50:37 +0000 Subject: [PATCH 29/32] Formatting cleanup --- common/constants.py | 10 +++++----- common/context_handler.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/constants.py b/common/constants.py index d554422..aaf6531 100644 --- a/common/constants.py +++ b/common/constants.py @@ -124,12 +124,12 @@ class ARCH_BITS(IntEnum): class MAGIC_BYTES(Enum): """Magic byte signatures for executable files.""" - ELF = [b"\x7F\x45\x4C\x46"] + 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", + b"\xfe\xed\xfa\xce", + b"\xfe\xed\xfa\xcf", + b"\xce\xfa\xed\xfe", + b"\xcf\xfa\xed\xfe", ] diff --git a/common/context_handler.py b/common/context_handler.py index 90189b5..fff5c50 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -149,6 +149,7 @@ def print_memory_address(self, addr: int, offset: int, size: int) -> None: # Add value to line err = SBError() memory_value = self.process.ReadMemory(addr, size, err) + if err.Success(): line += f"0x{int.from_bytes(memory_value, 'little'):0{size * 2}x}" else: From 6d34d1ae20f886ebef851f7ae96fe1d20fce16e1 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:12:22 +0000 Subject: [PATCH 30/32] Handle missing inode info gracefully --- commands/xinfo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commands/xinfo.py b/commands/xinfo.py index ce757b1..9493a8c 100644 --- a/commands/xinfo.py +++ b/commands/xinfo.py @@ -114,6 +114,8 @@ def get_xinfo(self, process: SBProcess, target: SBTarget, address: int) -> Dict[ 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 @@ -149,7 +151,7 @@ def __call__( 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["inode"] is not None: + 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.") From cd4ac7e7b58f9de7d4c9671dbce2d4f7a3af9545 Mon Sep 17 00:00:00 2001 From: sam-f0 <116253255+sam-f0@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:22:58 +0000 Subject: [PATCH 31/32] Switch to using shutil.get_terminal_size() for compatibility --- common/output_util.py | 6 +++--- common/util.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/output_util.py b/common/output_util.py index 94cccd2..f3a958d 100644 --- a/common/output_util.py +++ b/common/output_util.py @@ -1,7 +1,7 @@ """Utility functions related to terminal output.""" -import os import re +import shutil from textwrap import TextWrapper from typing import Any @@ -34,7 +34,7 @@ def terminal_columns() -> int: terminal environment variables then DEFAULT_TERMINAL_COLUMNS we be returned. """ try: - columns = os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + columns = shutil.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS except OSError: columns = DEFAULT_TERMINAL_COLUMNS @@ -88,7 +88,7 @@ def clear_page() -> None: printing the next information. """ try: - num_lines = os.get_terminal_size().lines + num_lines = shutil.get_terminal_size().lines except OSError: num_lines = DEFAULT_TERMINAL_LINES diff --git a/common/util.py b/common/util.py index 1ad54fc..d5e7315 100644 --- a/common/util.py +++ b/common/util.py @@ -1,6 +1,6 @@ """Utility functions.""" -import os +import shutil from argparse import ArgumentTypeError from typing import List, Tuple @@ -22,7 +22,7 @@ def terminal_columns() -> int: - return os.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS + return shutil.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS def address_to_filename(target: SBTarget, address: int) -> str: From 2a20ebe18e32891cf07860113d53846904b9636f Mon Sep 17 00:00:00 2001 From: f0alex <139959988+f0alex@users.noreply.github.com> Date: Mon, 7 Jul 2025 08:56:40 +0100 Subject: [PATCH 32/32] Add feature for heap enumeration (#91) * Add feature for heap enumeration * Fix import ordering * Fix ordering of typedefs causing type error, add fallback to default heap identification technique in error condition, add visual indicator when using Darwin heap scan feature. * Tweaks --- README.md | 29 ++--- common/context_handler.py | 29 ++++- common/expressions/darwin_get_malloc_zones.mm | 119 ++++++++++++++++++ common/settings.py | 4 + common/util.py | 93 +++++++++++--- 5 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 common/expressions/darwin_get_malloc_zones.mm 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/common/context_handler.py b/common/context_handler.py index fff5c50..0bfb53c 100644 --- a/common/context_handler.py +++ b/common/context_handler.py @@ -1,6 +1,6 @@ import os from string import printable -from typing import List, Optional, Type +from typing import List, Optional, Tuple, Type from lldb import ( SBAddress, @@ -27,6 +27,7 @@ from common.util import ( address_to_filename, attempt_to_read_string_from_memory, + find_darwin_heap_regions, find_stack_regions, get_frame_arguments, get_frame_range, @@ -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,8 +65,9 @@ def __init__( self.settings = LLEFSettings(debugger) self.color_settings = LLEFColorSettings() self.state = LLEFState() - self.stack_regions = List[SBMemoryRegionInfo] 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() @@ -119,6 +123,7 @@ def generate_printable_line_from_pointer( 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 and offset to line + line = color_string( hex(addr.GetValueAsUnsigned()), self.color_settings.stack_address_color, @@ -197,9 +202,9 @@ def print_register(self, register: SBValue) -> None: 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.stack_regions): + 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.stack_regions): + 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 = None @@ -245,7 +250,13 @@ def print_legend(self) -> None: legend = "[ Legend: " legend += color_string("Modified register", self.color_settings.modified_register_color, rwrap=" | ") legend += color_string("Code", self.color_settings.code_color, rwrap=" | ") - legend += color_string("Heap", self.color_settings.heap_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) @@ -421,7 +432,13 @@ def refresh(self, exe_ctx: SBExecutionContext) -> None: self.load_disassembly_syntax(self.debugger) if LLEFState.platform == "Darwin": - self.stack_regions = find_stack_regions(self.process) + 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`""" 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/settings.py b/common/settings.py index 75c4d2a..a5e5669 100644 --- a/common/settings.py +++ b/common/settings.py @@ -81,6 +81,10 @@ def output_order(self): 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(",") diff --git a/common/util.py b/common/util.py index d5e7315..4325f72 100644 --- a/common/util.py +++ b/common/util.py @@ -1,5 +1,6 @@ """Utility functions.""" +import os import shutil from argparse import ArgumentTypeError from typing import List, Tuple @@ -8,12 +9,16 @@ 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 @@ -27,7 +32,7 @@ def terminal_columns() -> int: def address_to_filename(target: SBTarget, address: int) -> str: """ - Maps a memory address to its corrosponding executable/library and returns the filename. + Maps a memory address to its corresponding executable/library and returns the filename. :param target: The target context. :param address: The memory address to resolve. @@ -171,13 +176,13 @@ def is_code(address: SBValue, process: SBProcess, target: SBTarget, regions: SBM return code_bool -def is_stack(address: SBValue, regions: SBMemoryRegionInfoList, stack_regions: List[SBMemoryRegionInfo]) -> bool: +def is_stack(address: SBValue, regions: SBMemoryRegionInfoList, darwin_stack_regions: List[SBMemoryRegionInfo]) -> bool: """Determines whether an @address points to the stack""" stack_bool = False region = SBMemoryRegionInfo() if regions is not None and regions.GetMemoryRegionContainingAddress(address, region): - if LLEFState.platform == "Darwin" and region in stack_regions: + if LLEFState.platform == "Darwin" and region in darwin_stack_regions: stack_bool = True elif region.GetName() == "[stack]": stack_bool = True @@ -186,19 +191,30 @@ def is_stack(address: SBValue, regions: SBMemoryRegionInfoList, stack_regions: L def is_heap( - address: SBValue, target: SBTarget, regions: SBMemoryRegionInfoList, stack_regions: List[SBMemoryRegionInfo] + 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""" heap_bool = False - 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(): + + 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 - elif region.GetName() == "[heap]": - heap_bool = True return heap_bool @@ -220,7 +236,7 @@ def verify_version(version: List[int], target_version: List[int]) -> bool: def lldb_version_to_clang(lldb_version): """ - Convert an LLDB version to its corrosponding Clang version. + Convert an LLDB version to its corresponding Clang version. :param lldb_version: The LLDB version. :return: The Clang version. @@ -332,7 +348,7 @@ def positive_int(x): def hex_or_str(x): - """Convert to formated hex if an integer, otherwise return the value.""" + """Convert to formatted hex if an integer, otherwise return the value.""" if isinstance(x, int): return f"0x{x:016x}" @@ -391,3 +407,52 @@ def find_stack_regions(process: SBProcess) -> List[SBMemoryRegionInfo]: 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