├── common ├── __init__.py ├── singleton.py ├── de_bruijn.py ├── golang │ ├── state.py │ ├── interfaces.py │ ├── constants.py │ ├── data.py │ ├── util_stateless.py │ ├── util.py │ ├── static.py │ ├── moduledata_parser.py │ └── type_getter.py ├── state.py ├── base_settings.py ├── constants.py ├── instruction_util.py ├── expressions │ └── darwin_get_malloc_zones.mm ├── color_settings.py ├── output_util.py └── settings.py ├── commands ├── __init__.py ├── base_container.py ├── settings.py ├── base_command.py ├── color_settings.py ├── base_settings.py ├── context.py ├── hexdump.py ├── xinfo.py ├── pattern.py ├── dereference.py ├── scan.py └── checksec.py ├── handlers ├── __init__.py └── stop_hook.py ├── .gitignore ├── .pylintrc ├── .pydocstyle ├── .isort.cfg ├── .lldbinit ├── assets ├── rebase-feature.png ├── llef-dragon-small.png └── go-examples │ ├── go-get-type.png │ ├── go-mode-enabled.gif │ ├── json-mashal-old.png │ ├── go-mode-disabled.gif │ ├── json-marshal-new.png │ ├── go-backtrace-command.png │ ├── go-find-func-command.png │ ├── function-name-recovery.png │ └── go-unpack-type-command.png ├── .mypy.ini ├── .github ├── ISSUE_TEMPLATE │ ├── FEATURE REQUEST.md │ └── BUG REPORT.md ├── workflows │ └── style.yml ├── PYTHON STYLE.md ├── CODE OF CONDUCT.md └── CONTRIBUTING.md ├── tox.ini ├── arch ├── base_arch.py ├── arm.py ├── ppc.py ├── i386.py ├── x86_64.py ├── __init__.py └── aarch64.py ├── LICENSE ├── install.sh ├── .vscode └── settings.json ├── llef.py ├── Go-features-readme.md └── README.md /common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .venv/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length=120 3 | -------------------------------------------------------------------------------- /.pydocstyle: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | match = (?!test_).*\.py 3 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length = 120 3 | profile = black 4 | -------------------------------------------------------------------------------- /.lldbinit: -------------------------------------------------------------------------------- 1 | command script import llef.py 2 | settings set stop-disassembly-display never -------------------------------------------------------------------------------- /assets/rebase-feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/rebase-feature.png -------------------------------------------------------------------------------- /assets/llef-dragon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/llef-dragon-small.png -------------------------------------------------------------------------------- /assets/go-examples/go-get-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/go-get-type.png -------------------------------------------------------------------------------- /assets/go-examples/go-mode-enabled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/go-mode-enabled.gif -------------------------------------------------------------------------------- /assets/go-examples/json-mashal-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/json-mashal-old.png -------------------------------------------------------------------------------- /assets/go-examples/go-mode-disabled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/go-mode-disabled.gif -------------------------------------------------------------------------------- /assets/go-examples/json-marshal-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/json-marshal-new.png -------------------------------------------------------------------------------- /assets/go-examples/go-backtrace-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/go-backtrace-command.png -------------------------------------------------------------------------------- /assets/go-examples/go-find-func-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/go-find-func-command.png -------------------------------------------------------------------------------- /assets/go-examples/function-name-recovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/function-name-recovery.png -------------------------------------------------------------------------------- /assets/go-examples/go-unpack-type-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundryzero/llef/HEAD/assets/go-examples/go-unpack-type-command.png -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | # Global options: 2 | 3 | [mypy] 4 | follow_imports = silent 5 | ignore_missing_imports = True 6 | show_column_numbers = True 7 | pretty = True 8 | strict = True 9 | mypy_path=$MYPY_CONFIG_FILE_DIR/typings -------------------------------------------------------------------------------- /common/singleton.py: -------------------------------------------------------------------------------- 1 | """Singleton module""" 2 | 3 | from typing import Any 4 | 5 | 6 | class Singleton(type): 7 | """ 8 | Singleton class implementation. Use with metaclass=Singleton. 9 | """ 10 | 11 | _instances: dict[type, Any] = {} 12 | 13 | def __call__(cls, *args: Any, **kwargs: Any) -> Any: 14 | if cls not in cls._instances: 15 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 16 | return cls._instances[cls] 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE REQUEST.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Motivation 6 | 7 | 8 | 9 | ## Describe alternatives you've considered 10 | 11 | 12 | 13 | ## Additional context 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG REPORT.md: -------------------------------------------------------------------------------- 1 | ### Environment 2 | 3 | Please list the OS, version of LLDB and version of LLEF under which this bug was experienced. 4 | 5 | ### Description 6 | 7 | 8 | 9 | ### Steps to Reproduce 10 | 11 | 1. 12 | 2. 13 | 3. 14 | 15 | **Expected behavior:** 16 | 17 | What you expect to happen 18 | 19 | **Actual behavior:** 20 | 21 | What actually happens 22 | 23 | ### Additional Information 24 | 25 | Any additional information, configuration or data that might be necessary to reproduce the issue. -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py39, py310, py311, py312, py313 3 | 4 | [gh-actions] 5 | python = 6 | 3.9: py39 7 | 3.10: py310 8 | 3.11: py311 9 | 3.12: py312 10 | 3.13: py313 11 | 12 | [testenv] 13 | deps = 14 | isort 15 | black 16 | flake8 17 | # mypy 18 | # pydocstyle 19 | # pylint 20 | commands = 21 | isort -c --line-length=120 --profile black {toxinidir} 22 | black --check --line-length=120 {toxinidir} 23 | flake8 --max-line-length=120 --ignore=E203,W503 {toxinidir} 24 | # mypy --follow-imports=silent --ignore-missing-imports --show-column-numbers --no-pretty --strict {toxinidir} 25 | # pydocstyle --count {toxinidir} 26 | # pylint **/*.py 27 | -------------------------------------------------------------------------------- /commands/base_container.py: -------------------------------------------------------------------------------- 1 | """Base container definition.""" 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | from lldb import SBDebugger 6 | 7 | 8 | class BaseContainer(ABC): 9 | """Base container class.""" 10 | 11 | @property 12 | @abstractmethod 13 | def container_verb(self) -> str: 14 | """Container verb property.""" 15 | 16 | @staticmethod 17 | @abstractmethod 18 | def get_short_help() -> str: 19 | """Get short help message.""" 20 | 21 | @staticmethod 22 | @abstractmethod 23 | def get_long_help() -> str: 24 | """Get long help message.""" 25 | 26 | @classmethod 27 | def lldb_self_register(cls, debugger: SBDebugger, _: str) -> None: 28 | """Automatically register a container.""" 29 | container_command = ( 30 | f'command container add -h "{cls.get_long_help()}" -H "{cls.get_short_help()}" {cls.container_verb}' 31 | ) 32 | debugger.HandleCommand(container_command) 33 | -------------------------------------------------------------------------------- /arch/base_arch.py: -------------------------------------------------------------------------------- 1 | """Base arch abstract class definition.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class FlagRegister: 9 | """FlagRegister dataclass to store register name / bitmask associations""" 10 | 11 | name: str 12 | bit_masks: dict[str, int] 13 | 14 | 15 | class BaseArch(ABC): 16 | """BaseArch abstract class definition.""" 17 | 18 | @property 19 | @abstractmethod 20 | def bits(self) -> int: 21 | """Bit count property""" 22 | 23 | @property 24 | @abstractmethod 25 | def max_instr_size(self) -> int: 26 | """Max instruction size (bytes) property""" 27 | 28 | @property 29 | @abstractmethod 30 | def gpr_registers(self) -> list[str]: 31 | """GPR register property""" 32 | 33 | @property 34 | @abstractmethod 35 | def gpr_key(self) -> str: 36 | """GPR key property""" 37 | 38 | @property 39 | @abstractmethod 40 | def flag_registers(self) -> list[FlagRegister]: 41 | """List of flag registers with associated bit masks""" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Foundry Zero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /arch/arm.py: -------------------------------------------------------------------------------- 1 | """arm architecture definition.""" 2 | 3 | from arch.base_arch import BaseArch, FlagRegister 4 | 5 | 6 | class Arm(BaseArch): 7 | """ 8 | arm support file 9 | """ 10 | 11 | bits = 32 12 | 13 | max_instr_size = 4 14 | 15 | gpr_registers = [ 16 | "r0", 17 | "r1", 18 | "r2", 19 | "r3", 20 | "r4", 21 | "r5", 22 | "r6", 23 | "r7", 24 | "r8", 25 | "r9", 26 | "r10", 27 | "r11", 28 | "r12", 29 | "sp", 30 | "lr", 31 | "pc", 32 | ] 33 | 34 | gpr_key = "general" 35 | 36 | # Bitmasks used to extract flag bits from cpsr register value 37 | _cpsr_register_bit_masks = { 38 | "n": 0x80000000, 39 | "z": 0x40000000, 40 | "c": 0x20000000, 41 | "v": 0x10000000, 42 | "q": 0x8000000, 43 | "j": 0x1000000, 44 | "ge": 0xF0000, 45 | "e": 0x200, 46 | "a": 0x100, 47 | "i": 0x80, 48 | "f": 0x40, 49 | "t": 0x20, 50 | } 51 | 52 | flag_registers = [FlagRegister("cpsr", _cpsr_register_bit_masks)] 53 | -------------------------------------------------------------------------------- /handlers/stop_hook.py: -------------------------------------------------------------------------------- 1 | """Break point handler.""" 2 | 3 | from typing import Any 4 | 5 | from lldb import SBDebugger, SBExecutionContext, SBStream, SBStructuredData, SBTarget 6 | 7 | from common.context_handler import ContextHandler 8 | 9 | 10 | class StopHookHandler: 11 | """Stop Hook handler.""" 12 | 13 | context_handler: ContextHandler 14 | 15 | @classmethod 16 | def lldb_self_register(cls, debugger: SBDebugger, module_name: str) -> None: 17 | """Register the Stop Hook Handler""" 18 | 19 | command = f"target stop-hook add -P {module_name}.{cls.__name__}" 20 | debugger.HandleCommand(command) 21 | 22 | def __init__(self, target: SBTarget, _: SBStructuredData, __: dict[Any, Any]) -> None: 23 | """ 24 | For up to date documentation on args provided to this function run: `help target stop-hook add` 25 | """ 26 | self.context_handler = ContextHandler(target.debugger) 27 | 28 | def handle_stop(self, exe_ctx: SBExecutionContext, _: SBStream) -> None: 29 | """For up to date documentation on args provided to this function run: `help target stop-hook add`""" 30 | self.context_handler.display_context(exe_ctx, True) 31 | -------------------------------------------------------------------------------- /common/de_bruijn.py: -------------------------------------------------------------------------------- 1 | """De Bruijn sequence utilities.""" 2 | 3 | import itertools 4 | from collections.abc import Iterator 5 | 6 | 7 | def de_bruijn(alphabet: bytearray, n: int) -> Iterator[int]: 8 | """ 9 | Generate De Bruijn sequence for alphabet and subsequences of length n (for compatibility. w/ pwnlib). 10 | Taken from GEF gef.py L3728 (2022.06). 11 | """ 12 | 13 | k = len(alphabet) 14 | a = [0] * k * n 15 | 16 | def db(t: int, p: int) -> Iterator[int]: 17 | if t > n: 18 | if n % p == 0: 19 | for j in range(1, p + 1): 20 | yield alphabet[a[j]] 21 | else: 22 | a[t] = a[t - p] 23 | yield from db(t + 1, p) 24 | 25 | for j in range(a[t - p] + 1, k): 26 | a[t] = j 27 | yield from db(t + 1, t) 28 | 29 | return db(1, 1) 30 | 31 | 32 | def generate_cyclic_pattern(length: int, cycle: int = 4) -> bytearray: 33 | """ 34 | Create a @length byte bytearray of a de Bruijn cyclic pattern. 35 | Taken from GEF gef.py L3749 (2022.06) 36 | """ 37 | charset = bytearray(b"abcdefghijklmnopqrstuvwxyz") 38 | return bytearray(itertools.islice(de_bruijn(charset, cycle), length)) 39 | -------------------------------------------------------------------------------- /arch/ppc.py: -------------------------------------------------------------------------------- 1 | """PowerPC architecture definition.""" 2 | 3 | from arch.base_arch import BaseArch, FlagRegister 4 | 5 | 6 | class PPC(BaseArch): 7 | """ 8 | These are currently hardcoded for PowerPC. 9 | """ 10 | 11 | bits = 32 12 | 13 | max_instr_size = 4 14 | 15 | gpr_registers = [ 16 | "r0", 17 | "r1", 18 | "r2", 19 | "r3", 20 | "r4", 21 | "r5", 22 | "r6", 23 | "r7", 24 | "r8", 25 | "r9", 26 | "r10", 27 | "r11", 28 | "r12", 29 | "r13", 30 | "pc", # program counter 31 | "msr", # machine state register 32 | "lr", # link register 33 | "ctr", # counter 34 | ] 35 | 36 | gpr_key = "general purpose" 37 | 38 | _xer_register_bit_masks = { 39 | "summary_overflow": 0x80000000, 40 | "overflow": 0x40000000, 41 | "carry": 0x20000000, 42 | } 43 | 44 | _cr_register_bit_masks = { 45 | "cr0_lt": 0x80000000, 46 | "cr0_gt": 0x40000000, 47 | "cr0_eq": 0x20000000, 48 | "cr0_so": 0x10000000, 49 | } 50 | 51 | flag_registers = [ 52 | FlagRegister("cr", _cr_register_bit_masks), 53 | FlagRegister("xer", _xer_register_bit_masks), 54 | ] 55 | -------------------------------------------------------------------------------- /arch/i386.py: -------------------------------------------------------------------------------- 1 | """i386 architecture definition.""" 2 | 3 | from arch.base_arch import BaseArch, FlagRegister 4 | 5 | 6 | class I386(BaseArch): 7 | """ 8 | These are currently hardcoded for i386. 9 | """ 10 | 11 | bits = 32 12 | 13 | max_instr_size = 15 14 | 15 | gpr_registers = [ 16 | "eax", 17 | "ebx", 18 | "ecx", 19 | "edx", 20 | "edi", 21 | "esi", 22 | "ebp", 23 | "esp", 24 | "eip", 25 | "cs", 26 | "fs", 27 | "gs", 28 | "ss", 29 | "ds", 30 | "es", 31 | ] 32 | 33 | gpr_key = "general purpose" 34 | 35 | # Bitmasks used to extract flag bits from eflags register value 36 | _eflags_register_bit_masks = { 37 | "zero": 0x40, 38 | "carry": 0x1, 39 | "parity": 0x4, 40 | "adjust": 0x10, 41 | "sign": 0x80, 42 | "trap": 0x100, 43 | "interrupt": 0x200, 44 | "direction": 0x400, 45 | "overflow": 0x800, 46 | "resume": 0x10000, 47 | "virtual8086": 0x20000, 48 | "identification": 0x200000, 49 | } 50 | 51 | flag_registers = [ 52 | FlagRegister("eflags", _eflags_register_bit_masks), 53 | FlagRegister("rflags", _eflags_register_bit_masks), 54 | ] 55 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | install () { 4 | echo "command script import \"$( dirname -- "$( readlink -f -- "$0"; )"; )/llef.py\"" > $HOME/.lldbinit; 5 | echo "settings set stop-disassembly-display never" >> $HOME/.lldbinit; 6 | 7 | while true; do 8 | read -p "LLDB uses AT&T disassembly syntax for x86 binaries would you like to force intel syntax? [y/n] " yn 9 | case $yn in 10 | [Yy]* ) echo "settings set target.x86-disassembly-flavor intel" >> $HOME/.lldbinit; break;; 11 | [Nn]* ) exit ; break;; 12 | * ) echo "Please answer 'y' or 'n'.";; 13 | esac 14 | done 15 | } 16 | 17 | manualinstall () { 18 | echo "Paste these lines into ~/.lldbinit"; 19 | echo ""; 20 | echo "command script import \"$( dirname -- "$( readlink -f -- "$0"; )"; )/llef.py\""; 21 | echo "settings set stop-disassembly-display never"; 22 | echo ""; 23 | echo "Optionally include the following line to use intel disassembly syntax for x86 binaries rather than AT&T"; 24 | echo ""; 25 | echo "settings set target.x86-disassembly-flavor intel"; 26 | } 27 | 28 | while true; do 29 | read -p "Automatic install will overwrite $HOME/.lldbinit - Enter 'y' continue or 'n' to view manual instructions? [y/n] " yn 30 | case $yn in 31 | [Yy]* ) install; break;; 32 | [Nn]* ) manualinstall; break;; 33 | * ) echo "Please answer 'y' or 'n'.";; 34 | esac 35 | done 36 | 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "aaaabaaaca", 4 | "aabacadaea", 5 | "aarch", 6 | "Atomatically", 7 | "bitmask", 8 | "Bitmasks", 9 | "cmdtemplate", 10 | "colour", 11 | "colourify", 12 | "COLOURS", 13 | "cpsr", 14 | "ENDC", 15 | "hlyeff", 16 | "inscope", 17 | "libc", 18 | "lldb", 19 | "lldbinit", 20 | "llef", 21 | "pwnlib", 22 | "refd", 23 | "regs", 24 | "rflags", 25 | "ssbs", 26 | "virtualx" 27 | ], 28 | "python.linting.flake8Enabled": true, 29 | "python.linting.prospectorEnabled": true, 30 | "python.linting.pycodestyleEnabled": true, 31 | "python.linting.pylamaEnabled": true, 32 | "python.linting.pylintEnabled": true, 33 | "python.linting.mypyEnabled": true, 34 | "python.linting.mypyArgs": [ 35 | "--follow-imports=silent", 36 | "--ignore-missing-imports", 37 | "--show-column-numbers", 38 | "--no-pretty", 39 | "--strict" 40 | ], 41 | "python.linting.pycodestyleArgs": [ 42 | "--max-line-length=120" 43 | ], 44 | "python.linting.pylamaArgs": [ 45 | "--max-line-length=120" 46 | ], 47 | "python.linting.pylintArgs": [ 48 | "--max-line-length=120" 49 | ], 50 | "python.linting.ignorePatterns": [ 51 | "**/de_bruijn.py", 52 | "**/site-packages/**/*.py" 53 | ] 54 | } -------------------------------------------------------------------------------- /arch/x86_64.py: -------------------------------------------------------------------------------- 1 | """x86_64 architecture definition.""" 2 | 3 | from arch.base_arch import BaseArch, FlagRegister 4 | 5 | 6 | class X86_64(BaseArch): 7 | """ 8 | These are currently hardcoded for X86_64. 9 | """ 10 | 11 | bits = 64 12 | 13 | max_instr_size = 15 14 | 15 | gpr_registers = [ 16 | "rax", 17 | "rbx", 18 | "rcx", 19 | "rdx", 20 | "rdi", 21 | "rsi", 22 | "r8", 23 | "r9", 24 | "r10", 25 | "r11", 26 | "r12", 27 | "r13", 28 | "r14", 29 | "r15", 30 | "rsp", 31 | "rbp", 32 | "rip", 33 | ] 34 | 35 | gpr_key = "general purpose" 36 | 37 | # Bitmasks used to extract flag bits from eflags register value 38 | _eflag_register_bit_masks = { 39 | "zero": 0x40, 40 | "carry": 0x1, 41 | "parity": 0x4, 42 | "adjust": 0x10, 43 | "sign": 0x80, 44 | "trap": 0x100, 45 | "interrupt": 0x200, 46 | "direction": 0x400, 47 | "overflow": 0x800, 48 | "resume": 0x10000, 49 | "virtualx86": 0x20000, 50 | "identification": 0x200000, 51 | } 52 | 53 | # Whether LLDB exposes eflags or rflags varies depending on the platform 54 | # rflags and eflags bit masks are identical for the lower 32-bits 55 | flag_registers = [ 56 | FlagRegister("rflags", _eflag_register_bit_masks), 57 | FlagRegister("eflags", _eflag_register_bit_masks), 58 | ] 59 | -------------------------------------------------------------------------------- /common/golang/state.py: -------------------------------------------------------------------------------- 1 | """Go state module.""" 2 | 3 | from typing import Union 4 | 5 | from common.golang.constants import GO_STRING_GUESS_CAPACITY, GO_TYPE_GUESS_CAPACITY 6 | from common.golang.interfaces import ModuleDataInfo, PCLnTabInfo 7 | from common.golang.util_stateless import LeastRecentlyAddedDictionary 8 | 9 | 10 | class GoState: 11 | """ 12 | State class, encapsulated by global LLEF state - stores Go-specific analysis. 13 | """ 14 | 15 | is_go_binary: bool # set once, based on static analysis 16 | 17 | analysed: bool # ensures we only run the analysis once 18 | 19 | moduledata_info: Union[ModuleDataInfo, None] # moduledata_info might be None, e.g. legacy Go version 20 | pclntab_info: PCLnTabInfo # if is_go_binary is True, then pclntab_info is always valid 21 | 22 | # maps a raw pointer to its guessed datatype 23 | type_guesses: LeastRecentlyAddedDictionary # dict[int, GoType] 24 | # maps a string base address to its guessed length 25 | string_guesses: LeastRecentlyAddedDictionary # dict[int, int] 26 | 27 | prev_func: int # the entry address of the function executing in the previous stop 28 | 29 | def __init__(self) -> None: 30 | self.is_go_binary = False 31 | self.analysed = False 32 | self.moduledata_info = None 33 | self.type_guesses = LeastRecentlyAddedDictionary(capacity=GO_TYPE_GUESS_CAPACITY) 34 | self.string_guesses = LeastRecentlyAddedDictionary(capacity=GO_STRING_GUESS_CAPACITY) 35 | self.prev_func = 0 36 | -------------------------------------------------------------------------------- /arch/__init__.py: -------------------------------------------------------------------------------- 1 | """Arch module __init__.py""" 2 | 3 | from lldb import SBTarget 4 | 5 | from arch.aarch64 import Aarch64 6 | from arch.arm import Arm 7 | from arch.base_arch import BaseArch 8 | from arch.i386 import I386 9 | from arch.ppc import PPC 10 | from arch.x86_64 import X86_64 11 | from common.constants import MSG_TYPE 12 | from common.output_util import print_message 13 | 14 | 15 | def extract_arch_from_triple(triple: str) -> str: 16 | """Extracts the architecture from triple string.""" 17 | return triple.split("-")[0] 18 | 19 | 20 | # macOS devices running arm chips identify as arm64. 21 | # aarch64 and arm64 backends have been merged, so alias arm64 to aarch64. 22 | # There's also arm64e architecture, which is basically ARMv8.3 23 | # but includes pointer authentication and for now is Apple-specific. 24 | supported_arch = { 25 | "arm": Arm, 26 | "i386": I386, 27 | "x86_64": X86_64, 28 | "aarch64": Aarch64, 29 | "arm64": Aarch64, 30 | "arm64e": Aarch64, 31 | "powerpc": PPC, 32 | } 33 | 34 | 35 | def get_arch(target: SBTarget) -> type[BaseArch]: 36 | """Get the architecture of a given target""" 37 | arch = extract_arch_from_triple(target.triple) 38 | return get_arch_from_str(arch) 39 | 40 | 41 | def get_arch_from_str(arch: str) -> type[BaseArch]: 42 | """Get the architecture class from string""" 43 | if arch in supported_arch: 44 | return supported_arch[arch] 45 | 46 | print_message(MSG_TYPE.ERROR, f"Unknown Architecture: {arch}") 47 | raise TypeError(f"Unknown target architecture: {arch}") 48 | -------------------------------------------------------------------------------- /arch/aarch64.py: -------------------------------------------------------------------------------- 1 | """aarch64 architecture definition.""" 2 | 3 | from arch.base_arch import BaseArch, FlagRegister 4 | 5 | 6 | class Aarch64(BaseArch): 7 | """ 8 | aarch64 support file 9 | """ 10 | 11 | bits = 64 12 | 13 | max_instr_size = 4 14 | 15 | gpr_registers = [ 16 | "x0", 17 | "x1", 18 | "x2", 19 | "x3", 20 | "x4", 21 | "x5", 22 | "x6", 23 | "x7", 24 | "x8", 25 | "x9", 26 | "x10", 27 | "x11", 28 | "x12", 29 | "x13", 30 | "x14", 31 | "x15", 32 | "x16", 33 | "x17", 34 | "x18", 35 | "x19", 36 | "x20", 37 | "x21", 38 | "x22", 39 | "x23", 40 | "x24", 41 | "x25", 42 | "x26", 43 | "x27", 44 | "x28", 45 | "x29", 46 | "x30", 47 | "fp", 48 | "lr", 49 | "sp", 50 | "pc", 51 | ] 52 | 53 | gpr_key = "general" 54 | 55 | # Bitmasks used to extract flag bits from cpsr register value 56 | _cpsr_register_bit_masks = { 57 | "n": 0x80000000, 58 | "z": 0x40000000, 59 | "c": 0x20000000, 60 | "v": 0x10000000, 61 | "q": 0x8000000, 62 | "ssbs": 0x800000, 63 | "pan": 0x400000, 64 | "dit": 0x200000, 65 | "ge": 0xF0000, 66 | "e": 0x200, 67 | "a": 0x100, 68 | "i": 0x80, 69 | "f": 0x40, 70 | "m": 0xF, 71 | } 72 | 73 | flag_registers = [FlagRegister("cpsr", _cpsr_register_bit_masks)] 74 | -------------------------------------------------------------------------------- /common/state.py: -------------------------------------------------------------------------------- 1 | """Global state module""" 2 | 3 | from typing import Any 4 | 5 | from common.golang.state import GoState 6 | from common.singleton import Singleton 7 | 8 | 9 | class LLEFState(metaclass=Singleton): 10 | """ 11 | Global state class - stores state accessible across any LLEF command/handler 12 | """ 13 | 14 | # Stores previous register state at the last breakpoint 15 | prev_registers: dict[str, int] = {} 16 | 17 | # Stores register state at the current breakpoint (caches the contents of the current frame as frame is mutable) 18 | current_registers: dict[str, int] = {} 19 | 20 | # Stores patterns created by the `pattern` command 21 | created_patterns: list[dict[str, Any]] = [] 22 | 23 | # Stores whether color should be used 24 | use_color = False 25 | 26 | # Stores whether output lines should be truncated 27 | truncate_output = True 28 | 29 | # Stores version of LLDB if on Linux. Stores clang verion if on Mac 30 | version: list[int] = [] 31 | 32 | # Linux, Mac (Darwin) or Windows 33 | platform = "" 34 | 35 | disassembly_syntax = "" 36 | 37 | # Stores Go-specific analysis. None means we are yet to analyse. 38 | go_state = GoState() 39 | 40 | def change_use_color(self, new_value: bool) -> None: 41 | """ 42 | Change the global use_color bool. use_color should not be written to directly 43 | """ 44 | self.use_color = new_value 45 | 46 | def change_truncate_output(self, new_value: bool) -> None: 47 | """Change the global truncate_output bool.""" 48 | self.truncate_output = new_value 49 | -------------------------------------------------------------------------------- /commands/settings.py: -------------------------------------------------------------------------------- 1 | """llefsettings command class.""" 2 | 3 | import argparse 4 | from typing import Any 5 | 6 | from lldb import SBDebugger 7 | 8 | from commands.base_settings import BaseSettingsCommand 9 | from common.settings import LLEFSettings 10 | 11 | 12 | class SettingsCommand(BaseSettingsCommand): 13 | """Implements the llefsettings command""" 14 | 15 | program: str = "llefsettings" 16 | container = None 17 | 18 | def __init__(self, debugger: SBDebugger, dictionary: dict[Any, Any]) -> None: 19 | super().__init__(debugger, dictionary) 20 | self.settings = LLEFSettings(debugger) 21 | 22 | @classmethod 23 | def get_command_parser(cls) -> argparse.ArgumentParser: 24 | """Get the command parser.""" 25 | parser = argparse.ArgumentParser(description="LLEF settings command") 26 | subparsers = parser.add_subparsers() 27 | 28 | list_parser = subparsers.add_parser("list", help="list all settings") 29 | list_parser.set_defaults(action="list") 30 | 31 | save_parser = subparsers.add_parser("save", help="Save settings to config file") 32 | save_parser.set_defaults(action="save") 33 | 34 | reload_parser = subparsers.add_parser("reload", help="Reload settings from config file (retain session values)") 35 | reload_parser.set_defaults(action="reload") 36 | 37 | reset_parser = subparsers.add_parser("reset", help="Reload settings from config file (purge session values)") 38 | reset_parser.set_defaults(action="reset") 39 | 40 | set_parser = subparsers.add_parser("set", help="Set LLEF settings") 41 | set_parser.add_argument("setting", type=str, help="LLEF setting name") 42 | set_parser.add_argument("value", type=str, help="New setting value") 43 | set_parser.set_defaults(action="set") 44 | 45 | return parser 46 | 47 | @staticmethod 48 | def get_short_help() -> str: 49 | return "Usage: llefsettings \n" 50 | -------------------------------------------------------------------------------- /common/golang/interfaces.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from common.constants import pointer 4 | from common.golang.types import GoType 5 | 6 | 7 | @dataclass(frozen=True) 8 | class GoFunc: 9 | """ 10 | A data structure for describing an individual Go function. 11 | """ 12 | 13 | # The name of the function. Can be recovered even if the binary is stripped. 14 | name: str 15 | # The location of the function as an offset into the binary. 16 | file_addr: int 17 | 18 | # A list of pairs of program counter followed by the stack delta at that point in execution - this is the 19 | # difference between the current stack pointer register and the stack pointer as it was at function entry. 20 | # Used to calculate a base frame pointer. 21 | stack_deltas: list[tuple[pointer, int]] 22 | 23 | 24 | @dataclass(frozen=True) 25 | class PCLnTabInfo: 26 | """ 27 | A data structure that stores information retrieved from the PCLNTAB section. 28 | """ 29 | 30 | # The last address in the text section, or the highest possible value the program counter can take. 31 | # Available both as relative to the binary (file) and address at runtime. 32 | max_pc_file: int 33 | max_pc_runtime: pointer 34 | 35 | # A list of pairs of program counter, then GoFunc - the program counter is the entry address of that function. 36 | func_mapping: list[tuple[pointer, GoFunc]] 37 | 38 | # A tuple describing (min_version, max_version). We guarantee that min_version <= actual go version <= max_version. 39 | version_bounds: tuple[int, int] 40 | 41 | # The size of a pointer on this architecture in bytes. 42 | ptr_size: int 43 | 44 | 45 | @dataclass(frozen=True) 46 | class ModuleDataInfo: 47 | """ 48 | A data structure that stores information retrieved from the ModuleData structure. 49 | """ 50 | 51 | # A mapping from type structure address (offset into the binary) to a parsed python GoType struct. 52 | type_structs: dict[int, GoType] 53 | -------------------------------------------------------------------------------- /commands/base_command.py: -------------------------------------------------------------------------------- 1 | """Base command definition.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import Any, Union 5 | 6 | from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext 7 | 8 | from commands.base_container import BaseContainer 9 | 10 | 11 | class BaseCommand(ABC): 12 | """An abstract base class for all commands.""" 13 | 14 | alias_set: dict[Any, Any] = {} 15 | 16 | @abstractmethod 17 | def __init__(self) -> None: 18 | pass 19 | 20 | @property 21 | @abstractmethod 22 | def container(self) -> Union[type[BaseContainer], None]: 23 | """Container property.""" 24 | 25 | @property 26 | @abstractmethod 27 | def program(self) -> str: 28 | """Program property.""" 29 | 30 | @abstractmethod 31 | def __call__( 32 | self, 33 | debugger: SBDebugger, 34 | command: str, 35 | exe_ctx: SBExecutionContext, 36 | result: SBCommandReturnObject, 37 | ) -> None: 38 | pass 39 | 40 | @staticmethod 41 | @abstractmethod 42 | def get_short_help() -> str: 43 | """Get short help string.""" 44 | 45 | @staticmethod 46 | @abstractmethod 47 | def get_long_help() -> str: 48 | """Get long help string.""" 49 | 50 | @classmethod 51 | def lldb_self_register(cls, debugger: SBDebugger, module_name: str) -> None: 52 | """Automatically register a subcommand.""" 53 | 54 | if cls.container is not None: 55 | command = f"command script add -c {module_name}.{cls.__name__} {cls.container.container_verb} {cls.program}" 56 | else: 57 | command = f"command script add -c {module_name}.{cls.__name__} {cls.program}" 58 | 59 | debugger.HandleCommand(command) 60 | 61 | # If alias_set exists, then load it into LLDB. 62 | for alias, arguments in cls.alias_set.items(): 63 | alias_command = f"command alias {alias} {cls.program} {arguments}" 64 | debugger.HandleCommand(alias_command) 65 | -------------------------------------------------------------------------------- /commands/color_settings.py: -------------------------------------------------------------------------------- 1 | """llefcolorsettings command class.""" 2 | 3 | import argparse 4 | from typing import Any 5 | 6 | from lldb import SBDebugger 7 | 8 | from commands.base_settings import BaseSettingsCommand 9 | from common.color_settings import LLEFColorSettings 10 | 11 | 12 | class ColorSettingsCommand(BaseSettingsCommand): 13 | """Implements the llefcolorsettings commands""" 14 | 15 | program: str = "llefcolorsettings" 16 | container = None 17 | 18 | def __init__(self, debugger: SBDebugger, dictionary: dict[Any, Any]) -> None: 19 | super().__init__(debugger, dictionary) 20 | self.settings = LLEFColorSettings() 21 | 22 | @classmethod 23 | def get_command_parser(cls) -> argparse.ArgumentParser: 24 | """Get the command parser.""" 25 | parser = argparse.ArgumentParser(description="LLEF settings command for colors") 26 | subparsers = parser.add_subparsers() 27 | 28 | list_parser = subparsers.add_parser("list", help="list all color settings") 29 | list_parser.set_defaults(action="list") 30 | 31 | save_parser = subparsers.add_parser("save", help="Save settings to config file") 32 | save_parser.set_defaults(action="save") 33 | 34 | reload_parser = subparsers.add_parser("reload", help="Reload settings from config file (retain session values)") 35 | reload_parser.set_defaults(action="reload") 36 | 37 | reset_parser = subparsers.add_parser("reset", help="Reload settings from config file (purge session values)") 38 | reset_parser.set_defaults(action="reset") 39 | 40 | set_parser = subparsers.add_parser("set", help="Set LLEF color settings") 41 | set_parser.add_argument("setting", type=str, help="LLEF color setting name") 42 | set_parser.add_argument("value", type=str, help="New color") 43 | set_parser.set_defaults(action="set") 44 | 45 | return parser 46 | 47 | @staticmethod 48 | def get_short_help() -> str: 49 | return "Usage: llefcolorsettings \n" 50 | -------------------------------------------------------------------------------- /commands/base_settings.py: -------------------------------------------------------------------------------- 1 | """Base settings command class.""" 2 | 3 | import argparse 4 | import shlex 5 | from abc import ABC, abstractmethod 6 | from typing import Any, Union 7 | 8 | from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext 9 | 10 | from commands.base_command import BaseCommand 11 | from common.base_settings import BaseLLEFSettings 12 | from common.output_util import output_line 13 | 14 | 15 | class BaseSettingsCommand(BaseCommand, ABC): 16 | """Base class for generic settings command""" 17 | 18 | program: str = "" 19 | container = None 20 | settings: Union[BaseLLEFSettings, None] = None 21 | 22 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 23 | super().__init__() 24 | self.parser = self.get_command_parser() 25 | 26 | @classmethod 27 | @abstractmethod 28 | def get_command_parser(cls) -> argparse.ArgumentParser: 29 | """Get the command parser.""" 30 | 31 | @staticmethod 32 | @abstractmethod 33 | def get_short_help() -> str: 34 | """Return a short help message""" 35 | 36 | @classmethod 37 | def get_long_help(cls) -> str: 38 | """Return a longer help message""" 39 | return cls.get_command_parser().format_help() 40 | 41 | def __call__( 42 | self, 43 | debugger: SBDebugger, 44 | command: str, 45 | exe_ctx: SBExecutionContext, 46 | result: SBCommandReturnObject, 47 | ) -> None: 48 | """Handles the invocation of the command""" 49 | args = self.parser.parse_args(shlex.split(command)) 50 | 51 | if not hasattr(args, "action"): 52 | output_line(self.__class__.get_long_help()) 53 | return 54 | 55 | if self.settings is None: 56 | raise AttributeError("Class not properly initialised: self.settings is None") 57 | 58 | if args.action == "list": 59 | self.settings.list_settings() 60 | elif args.action == "save": 61 | self.settings.save() 62 | elif args.action == "reload": 63 | self.settings.load() 64 | elif args.action == "reset": 65 | self.settings.load(reset=True) 66 | elif args.action == "set": 67 | self.settings.set(args.setting, args.value) 68 | -------------------------------------------------------------------------------- /commands/context.py: -------------------------------------------------------------------------------- 1 | """Context command class.""" 2 | 3 | import argparse 4 | import shlex 5 | from typing import Any 6 | 7 | from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext 8 | 9 | from commands.base_command import BaseCommand 10 | from common.context_handler import ContextHandler 11 | from common.output_util import output_line 12 | 13 | 14 | class ContextCommand(BaseCommand): 15 | """Implements the context""" 16 | 17 | program: str = "context" 18 | container = None 19 | 20 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 21 | super().__init__() 22 | self.parser = self.get_command_parser() 23 | self.context_handler = ContextHandler(debugger) 24 | 25 | @classmethod 26 | def get_command_parser(cls) -> argparse.ArgumentParser: 27 | """Get the command parser.""" 28 | parser = argparse.ArgumentParser(description="context command") 29 | parser.add_argument( 30 | "sections", 31 | nargs="*", 32 | choices=["registers", "stack", "code", "threads", "trace", "all"], 33 | default="all", 34 | ) 35 | return parser 36 | 37 | @staticmethod 38 | def get_short_help() -> str: 39 | return "Usage: context [section (optional)]\n" 40 | 41 | @staticmethod 42 | def get_long_help() -> str: 43 | return "Refresh and print the context\n" 44 | 45 | def __call__( 46 | self, 47 | debugger: SBDebugger, 48 | command: str, 49 | exe_ctx: SBExecutionContext, 50 | result: SBCommandReturnObject, 51 | ) -> None: 52 | """Handles the invocation of 'context' command""" 53 | 54 | if not exe_ctx.frame: 55 | output_line("Program not running") 56 | return 57 | 58 | args = self.parser.parse_args(shlex.split(command)) 59 | 60 | if not hasattr(args, "sections"): 61 | output_line(self.__class__.get_long_help()) 62 | return 63 | 64 | self.context_handler.refresh(exe_ctx) 65 | 66 | if "all" in args.sections: 67 | self.context_handler.display_context(exe_ctx, False) 68 | else: 69 | if "registers" in args.sections: 70 | self.context_handler.display_registers() 71 | if "stack" in args.sections: 72 | self.context_handler.display_stack() 73 | if "code" in args.sections: 74 | self.context_handler.display_code() 75 | if "threads" in args.sections: 76 | self.context_handler.display_threads() 77 | if "trace" in args.sections: 78 | self.context_handler.display_trace() 79 | -------------------------------------------------------------------------------- /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: F0 style checking 2 | run-name: Running style checks on ${{ github.ref_name }} following push by ${{ github.actor }} 3 | 4 | on: push 5 | jobs: 6 | Check-isort: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | - name: python-isort 12 | uses: isort/isort-action@v1.1.1 13 | with: 14 | configuration: "--check-only --profile black --diff --verbose" 15 | # Check-mypy: 16 | # runs-on: ubuntu-latest 17 | # steps: 18 | # - name: Checkout 19 | # uses: actions/checkout@v3 20 | # - name: python-mypy 21 | # uses: jpetrucciani/mypy-check@master 22 | # with: 23 | # path: '.' 24 | # mypy_flags: '--config-file .mypy.ini' 25 | Check-black: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: python-black 31 | uses: psf/black@stable 32 | with: 33 | options: "--check --line-length=120" 34 | src: "." 35 | Check-flake8: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v3 40 | - name: Set up Python environment 41 | uses: actions/setup-python@v4 42 | with: 43 | python-version: "3.11" 44 | - name: flake8 Lint 45 | uses: py-actions/flake8@v2.3.0 46 | with: 47 | max-line-length: "120" 48 | path: "." 49 | ignore: "E203,W503" 50 | # Check-pydocstyle: 51 | # runs-on: ubuntu-latest 52 | # steps: 53 | # - name: Checkout 54 | # uses: actions/checkout@v3 55 | # - name: pydocstyle 56 | # uses: foundryzero/pydocstyle-action@v1.2.6 57 | # with: 58 | # path: "." 59 | 60 | # Check-pylint: 61 | # runs-on: ubuntu-latest 62 | # steps: 63 | # - name: Checkout 64 | # uses: actions/checkout@v3 65 | # - name: pylint 66 | # uses: foundryzero/pylint-action@v1.0.6 67 | # with: 68 | # match: "binder_trace/**/*.py" 69 | # requirements_file: "binder_trace/requirements.txt" 70 | 71 | Check-tox: 72 | runs-on: ubuntu-latest 73 | strategy: 74 | matrix: 75 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 76 | 77 | steps: 78 | - uses: actions/checkout@v3 79 | - name: Set up Python ${{ matrix.python-version }} 80 | uses: actions/setup-python@v4 81 | with: 82 | python-version: ${{ matrix.python-version }} 83 | - name: Install dependencies 84 | run: | 85 | python -m pip install --upgrade pip 86 | python -m pip install tox tox-gh-actions 87 | - name: Test with tox 88 | run: tox 89 | -------------------------------------------------------------------------------- /.github/PYTHON STYLE.md: -------------------------------------------------------------------------------- 1 | # Foundry Zero Open Source Python Style Guide 2 | 3 | The LLEF project comes with a .vscode workspace settings file which should enforce some of these style guidelines for you. However for completeness, the guidelines against which pull requests will be reviewed are included below. 4 | 5 | ## Code formatting 6 | 7 | `black` should be used for code formatting. `black` is best described by the project themselves: 8 | 9 | > Black is the uncompromising Python code formatter. By using it, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from `pycodestyle` nagging about formatting. You will save time and mental energy for more important matters. 10 | 11 | Python repositories should specify a `requirements.txt` file in the root of the project directory containing all the external pip dependies. 12 | 13 | ## Documentation 14 | All public functions and classes should be documented in the standard Python docstring style, detailing the intention of the function, the arguments, any exceptions it may raise, and the return value. 15 | 16 | Private functions should ideally be documented too, for ease of maintainability. 17 | 18 | When using type hints, it is not necessary to include the argument types in the documentation. 19 | 20 | The `sphinx-notypes` style is recommended. 21 | 22 | ``` 23 | def function(arg1: int, arg2: str) -> str: 24 | """ 25 | This is a function 26 | :param arg1: An argument 27 | :param arg2: Another argument 28 | :raises KeyError: description of error condition 29 | :return: The return string 30 | """ 31 | ``` 32 | 33 | ## Linting 34 | Set up VS Code (or your IDE of choice) to make use of pylint to check your project for easily catchable issues. 35 | 36 | ## Type hints 37 | When writing complex Python code, consider using type hints and mypy to statically check them. 38 | 39 | Remember: Type hints are not enforced by the Python interpreter, so only static analysis tools like mypy will inform you of errors in your code caught by type hinting. 40 | 41 | # Import ordering 42 | Imports should be ordered in alphabetical order, grouped from most widely applicable (e.g. language built ins) to most specific (e.g. project specified) 43 | 44 | ``` 45 | import json 46 | import time 47 | 48 | from django.contrib import messages 49 | from django.contrib.auth.decorators import login_required 50 | from django.forms.formsets import formset_factory 51 | from django.forms.models import inlineformset_factory 52 | from django.views.decorators.http import require_POST 53 | 54 | from bibtexparser.bparser import BibTexParser 55 | from bibtexparser.customization import convert_to_unicode 56 | 57 | from .forms import KeywordForm, SynonymForm 58 | ``` 59 | 60 | A tool such as `isort` should be used to do this automatically for you and to ensure consistency. 61 | 62 | -------------------------------------------------------------------------------- /llef.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """LLEF main handler.""" 3 | 4 | # --------------------------------------------------------------------- 5 | # To use this in the embedded python interpreter using "lldb" just 6 | # import it with the full path using the "command script import" 7 | # command`` 8 | # (lldb) command script import /path/to/cmdtemplate.py 9 | # 10 | # The __lldb_init_module function automatically loads the stop-hook-handler 11 | # --------------------------------------------------------------------- 12 | 13 | import platform 14 | from typing import Any, Union 15 | 16 | from lldb import SBDebugger 17 | 18 | from commands.base_command import BaseCommand 19 | from commands.base_container import BaseContainer 20 | from commands.checksec import ChecksecCommand 21 | from commands.color_settings import ColorSettingsCommand 22 | from commands.context import ContextCommand 23 | from commands.dereference import DereferenceCommand 24 | from commands.golang import ( 25 | GolangBacktraceCommand, 26 | GolangContainer, 27 | GolangFindFuncCommand, 28 | GolangGetTypeCommand, 29 | GolangReanalyseCommand, 30 | GolangUnpackTypeCommand, 31 | ) 32 | from commands.hexdump import HexdumpCommand 33 | from commands.pattern import PatternContainer, PatternCreateCommand, PatternSearchCommand 34 | from commands.scan import ScanCommand 35 | from commands.settings import SettingsCommand 36 | from commands.xinfo import XinfoCommand 37 | from common.state import LLEFState 38 | from handlers.stop_hook import StopHookHandler 39 | 40 | 41 | def __lldb_init_module(debugger: SBDebugger, _: dict[Any, Any]) -> None: 42 | commands: list[Union[type[BaseCommand], type[BaseContainer]]] = [ 43 | PatternContainer, 44 | PatternCreateCommand, 45 | PatternSearchCommand, 46 | ContextCommand, 47 | SettingsCommand, 48 | ColorSettingsCommand, 49 | HexdumpCommand, 50 | ChecksecCommand, 51 | XinfoCommand, 52 | DereferenceCommand, 53 | ScanCommand, 54 | GolangContainer, 55 | GolangBacktraceCommand, 56 | GolangFindFuncCommand, 57 | GolangGetTypeCommand, 58 | GolangUnpackTypeCommand, 59 | GolangReanalyseCommand, 60 | ] 61 | 62 | handlers = [StopHookHandler] 63 | 64 | for command in commands: 65 | command.lldb_self_register(debugger, "llef") 66 | 67 | for handler in handlers: 68 | handler.lldb_self_register(debugger, "llef") 69 | 70 | LLEFState.platform = platform.system() 71 | if LLEFState.platform == "Darwin": 72 | # Getting Clang version (e.g. lldb-1600.0.36.3) 73 | LLEFState.version = [int(x) for x in debugger.GetVersionString().split()[0].split("-")[1].split(".")] 74 | else: 75 | # Getting LLDB version (e.g. lldb version 16.0.0) 76 | LLEFState.version = [int(x) for x in debugger.GetVersionString().split("version")[1].split()[0].split(".")] 77 | -------------------------------------------------------------------------------- /common/base_settings.py: -------------------------------------------------------------------------------- 1 | """A base class for global settings""" 2 | 3 | import configparser 4 | import os 5 | from abc import abstractmethod 6 | 7 | from common.output_util import output_line 8 | from common.singleton import Singleton 9 | from common.state import LLEFState 10 | 11 | 12 | class BaseLLEFSettings(metaclass=Singleton): 13 | """ 14 | Global settings class - loaded from file defined in `LLEF_CONFIG_PATH` 15 | """ 16 | 17 | LLEF_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".llef") 18 | GLOBAL_SECTION = "LLEF" 19 | 20 | _RAW_CONFIG: configparser.ConfigParser = configparser.ConfigParser() 21 | 22 | @classmethod 23 | def _get_setting_names(cls) -> list[str]: 24 | return [name for name, value in vars(cls).items() if isinstance(value, property)] 25 | 26 | def __init__(self) -> None: 27 | self.state = LLEFState() 28 | self.load() 29 | 30 | @abstractmethod 31 | def validate_settings(self, setting: str = "") -> bool: 32 | """ 33 | Validate settings 34 | """ 35 | 36 | def load_default_settings(self) -> None: 37 | """ 38 | Reset settings and use default values 39 | """ 40 | self._RAW_CONFIG = configparser.ConfigParser() 41 | self._RAW_CONFIG.add_section(self.GLOBAL_SECTION) 42 | 43 | def load(self, reset: bool = False) -> None: 44 | """ 45 | Load settings from file 46 | """ 47 | if reset: 48 | self._RAW_CONFIG = configparser.ConfigParser() 49 | 50 | if not os.path.isfile(self.LLEF_CONFIG_PATH): 51 | self.load_default_settings() 52 | return 53 | 54 | output_line(f"Loading LLEF settings from {self.LLEF_CONFIG_PATH}") 55 | 56 | self._RAW_CONFIG.read(self.LLEF_CONFIG_PATH) 57 | 58 | if not self._RAW_CONFIG.has_section(self.GLOBAL_SECTION): 59 | self.load_default_settings() 60 | output_line("Settings file missing 'LLEF' section. Default settings loaded.") 61 | 62 | if not self.validate_settings(): 63 | self.load_default_settings() 64 | output_line("Error parsing config. Default settings loaded.") 65 | 66 | def list_settings(self) -> None: 67 | """ 68 | List all settings and their current values 69 | """ 70 | settings_names = self._get_setting_names() 71 | for setting_name in settings_names: 72 | output_line(f"{setting_name}={getattr(self, setting_name)}") 73 | 74 | def save(self) -> None: 75 | """ 76 | Save LLEF setting to file defined in `LLEF_CONFIG_PATH` 77 | """ 78 | with open(self.LLEF_CONFIG_PATH, "w") as configfile: 79 | self._RAW_CONFIG.write(configfile) 80 | 81 | def set(self, setting: str, value: str) -> None: 82 | """ 83 | Set a LLEF setting 84 | """ 85 | if not hasattr(self, setting): 86 | output_line(f"Invalid LLEF setting {setting}") 87 | 88 | restore_value = getattr(self, setting) 89 | self._RAW_CONFIG.set(self.GLOBAL_SECTION, setting, value) 90 | 91 | if not self.validate_settings(setting=setting): 92 | self._RAW_CONFIG.set(self.GLOBAL_SECTION, setting, str(restore_value)) 93 | else: 94 | output_line(f"Set {setting} to {getattr(self, setting)}") 95 | -------------------------------------------------------------------------------- /common/constants.py: -------------------------------------------------------------------------------- 1 | """Constant definitions.""" 2 | 3 | from enum import Enum, IntEnum 4 | 5 | # Pointers are stored using int, however, this can lead to confusion between an offset, an absolute address, 6 | # and just a plain old integer value. The llef_pointer alias is intended to improve readability. 7 | pointer = int 8 | 9 | 10 | class TERM_COLORS(Enum): 11 | """Used to colorify terminal output.""" 12 | 13 | BLUE = "\033[34m" 14 | GREEN = "\033[32m" 15 | YELLOW = "\033[33m" 16 | RED = "\033[31m" 17 | PINK = "\033[35m" 18 | CYAN = "\033[36m" 19 | GREY = "\033[1;38;5;240m" 20 | ENDC = "\033[0m" 21 | 22 | 23 | class MSG_TYPE(Enum): 24 | """Log message types.""" 25 | 26 | INFO = 1 27 | SUCCESS = 2 28 | ERROR = 3 29 | 30 | 31 | class GLYPHS(Enum): 32 | """Various characters required to match GEF output.""" 33 | 34 | LEFT_ARROW = " ← " 35 | RIGHT_ARROW = " → " 36 | DOWN_ARROW = "↳" 37 | HORIZONTAL_LINE = "─" 38 | VERTICAL_LINE = "│" 39 | CROSS = "✘ " 40 | TICK = "✓ " 41 | BP_GLYPH = "●" 42 | 43 | 44 | class ALIGN(Enum): 45 | """Alignment values.""" 46 | 47 | LEFT = 1 48 | CENTRE = 2 49 | RIGHT = 3 50 | 51 | 52 | class SIZES(Enum): 53 | """Size of data types""" 54 | 55 | QWORD = 8 56 | DWORD = 4 57 | WORD = 2 58 | BYTE = 1 59 | 60 | 61 | class XINFO(Enum): 62 | REGION_START = "Region Start" 63 | REGION_END = "Region End" 64 | REGION_SIZE = "Region Size" 65 | REGION_OFFSET = "Region Offset" 66 | PERMISSIONS = "Permissions" 67 | PATH = "Path" 68 | INODE = "INode" 69 | 70 | 71 | class SECURITY_FEATURE(Enum): 72 | STACK_CANARY = "Stack Canary" 73 | NX_SUPPORT = "NX Support" 74 | PIE_SUPPORT = "PIE Support" 75 | NO_RPATH = "No RPath" 76 | NO_RUNPATH = "No RunPath" 77 | PARTIAL_RELRO = "Partial RelRO" 78 | FULL_RELRO = "Full RelRO" 79 | 80 | 81 | class SECURITY_CHECK(Enum): 82 | NO = "No" 83 | YES = "Yes" 84 | UNKNOWN = "Unknown" 85 | 86 | 87 | class PERMISSION_SET: 88 | """Values for 3bit permission sets.""" 89 | 90 | NOT_EXEC = [0, 2, 4, 6] 91 | EXEC = [1, 3, 5, 7] 92 | 93 | 94 | class PROGRAM_HEADER_TYPE(IntEnum): 95 | """Program header type values (in ELF files).""" 96 | 97 | GNU_STACK = 0x6474E551 98 | GNU_RELRO = 0x6474E552 99 | 100 | 101 | class EXECUTABLE_TYPE(IntEnum): 102 | """Executable ELF file types.""" 103 | 104 | DYN = 0x03 105 | 106 | 107 | class DYNAMIC_ENTRY_TYPE(IntEnum): 108 | """Entry types in the .dynamic section table of the ELF file.""" 109 | 110 | FLAGS = 0x1E 111 | RPATH = 0x0F 112 | RUNPATH = 0x1D 113 | 114 | 115 | class DYNAMIC_ENTRY_VALUE(IntEnum): 116 | """Entry values in the .dynamic section table of the ELF file.""" 117 | 118 | BIND_NOW = 0x08 119 | 120 | 121 | class ARCH_BITS(IntEnum): 122 | """32bit or 64bit architecture.""" 123 | 124 | BITS_32 = 1 125 | BITS_64 = 2 126 | 127 | 128 | class MAGIC_BYTES(Enum): 129 | """Magic byte signatures for executable files.""" 130 | 131 | ELF = [b"\x7f\x45\x4c\x46"] 132 | MACH = [ 133 | b"\xfe\xed\xfa\xce", 134 | b"\xfe\xed\xfa\xcf", 135 | b"\xce\xfa\xed\xfe", 136 | b"\xcf\xfa\xed\xfe", 137 | ] 138 | 139 | 140 | DEFAULT_TERMINAL_COLUMNS = 80 141 | DEFAULT_TERMINAL_LINES = 24 142 | -------------------------------------------------------------------------------- /common/golang/constants.py: -------------------------------------------------------------------------------- 1 | """Go-specific constant definitions.""" 2 | 3 | from collections import namedtuple 4 | 5 | # GO_MAGIC_* enumerates the possibilities for the first 4 bytes of the PCLNTAB. 6 | GO_MAGIC_2_TO_15 = 0xFFFFFFFB 7 | GO_MAGIC_16_TO_17 = 0xFFFFFFFA 8 | GO_MAGIC_18_TO_19 = 0xFFFFFFF0 9 | GO_MAGIC_20_TO_24 = 0xFFFFFFF1 10 | 11 | # This list must include all the above magic numbers. 12 | GO_MAGICS = [GO_MAGIC_2_TO_15, GO_MAGIC_16_TO_17, GO_MAGIC_18_TO_19, GO_MAGIC_20_TO_24] 13 | 14 | # Sections in which the PCLNTAB can live. 15 | GO_PCLNTAB_NAMES = [".gopclntab", "__gopclntab"] 16 | # Sections in which the ModuleData structure can live. 17 | GO_NOPTRDATA_NAMES = [".noptrdata", "__noptrdata", ".data"] 18 | 19 | # GO_MD_* enumerates the offsets where useful fields live in the ModuleData section. They are version-specific. 20 | # The offset is an index into the ModuleData structure, cast as an array of pointer-sized ints. 21 | 22 | # Description of fields within ModuleData: 23 | # minpc, maxpc are lower/upper bounds for the program counter - i.e. denotes the text section. 24 | # types, etypes denote the bounds of the types section (storing type information structures) 25 | # typelinks is an array of offsets to these type information structures. The length is typelinks_len. 26 | ModuleDataOffsets = namedtuple("ModuleDataOffsets", ["minpc", "maxpc", "types", "etypes", "typelinks", "typelinks_len"]) 27 | GO_MD_7_ONLY = ModuleDataOffsets(minpc=10, maxpc=11, types=25, etypes=26, typelinks=27, typelinks_len=28) 28 | GO_MD_8_TO_15 = ModuleDataOffsets(minpc=10, maxpc=11, types=25, etypes=26, typelinks=30, typelinks_len=31) 29 | GO_MD_16_TO_17 = ModuleDataOffsets(minpc=20, maxpc=21, types=35, etypes=36, typelinks=40, typelinks_len=41) 30 | GO_MD_18_TO_19 = ModuleDataOffsets(minpc=20, maxpc=21, types=35, etypes=36, typelinks=42, typelinks_len=43) 31 | GO_MD_20_TO_24 = ModuleDataOffsets(minpc=20, maxpc=21, types=37, etypes=38, typelinks=44, typelinks_len=45) 32 | 33 | GO_MAX_SLICE_EXTRACT = 100 # don't extract more than this many elements of a slice 34 | GO_MAX_STRING_READ = 1000 # don't extract more than this many bytes of string 35 | 36 | # Parameters for rate_candidate_length when calculating heuristics. 37 | GO_TUNE_SLICE_THRESHOLD = 1000 38 | GO_TUNE_SLICE_RATE = 100 39 | GO_TUNE_STRING_THRESHOLD = 40 40 | GO_TUNE_STRING_RATE = 5 41 | 42 | # We'll truncate strings if they're longer than this. 43 | GO_STR_TRUNCATE_LEN = 32 44 | 45 | # Threshold to separate pointer guesses from numbers by value. 46 | GO_MIN_PTR = 0x1000 47 | 48 | # Maximum number of directories in swiss map to extract. 49 | GO_MAX_SWISSMAP_DIRS = 65536 50 | 51 | # Exponent of probability for bitstring entropy, to permit more extraordinary strings. 52 | GO_ENTROPY_SOFTNESS = 0.3 53 | 54 | GO_DEFAULT_UNPACK_DEPTH = 3 55 | 56 | # The depth to decode types found and annotated inline (substituting name for type constructors). 57 | GO_TYPE_DECODE_DEPTH = 2 58 | # The depth to unpack objects found and annotated inline. Strings will always be truncated if too long. 59 | GO_OBJECT_UNPACK_DEPTH = 3 60 | 61 | # These two control the capacities of least-recently-added dictionaries that store guess information. 62 | # This is a balancing act of: 63 | # 1. Not forgetting types / strings too quickly, possibly even with the same context display. 64 | # 2. Hanging onto types for too long, when the pointer has been garbage collected and is now something else. 65 | # Err on the side of (1), given that a bit of junk being displayed is okay. 66 | GO_TYPE_GUESS_CAPACITY = 64 67 | GO_STRING_GUESS_CAPACITY = 128 68 | -------------------------------------------------------------------------------- /.github/CODE OF CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as 7 | contributors and maintainers pledge to make participation in our project and 8 | our community a harassment-free experience for everyone, regardless of age, body 9 | size, disability, ethnicity, sex characteristics, gender identity and expression, 10 | level of experience, education, socio-economic status, nationality, personal 11 | appearance, race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all project spaces, and it also applies when 50 | an individual is representing the project or its community in public spaces. 51 | Examples of representing a project or community include using an official 52 | project e-mail address, posting via an official social media account, or acting 53 | as an appointed representative at an online or offline event. Representation of 54 | a project may be further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the project team at info@foundryzero.co.uk . All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /Go-features-readme.md: -------------------------------------------------------------------------------- 1 | # LLEF Experimental Go Features Readme 2 | 3 | LLEF now has experimental support for stripped Go binaries. This document contains examples of LLEF's Go features, including screenshots of what LLEF's analysis output displays compared with previous versions of LLEF. These features are tested across a range of Go versions since 1.7, which was released in 2016. 4 | 5 | ## Resolving and displaying Go types 6 | 7 | Go binaries store metadata on complex data types which are used in the program, even when the binary has been stripped. From this metadata, field names and types can be recovered for `struct`, `map`, `array` and `slice` types. As an example, consider the following `struct` definition in a Go source file: 8 | 9 | ```go 10 | type Tool struct { 11 | Name string `json:"name"` 12 | Developer string `json:"developer"` 13 | Score int `json:"score"` 14 | } 15 | func demo() { 16 | tools := []Tool{ 17 | {Name: "LLEF", Developer: "Foundry Zero", Score: 10}, 18 | {Name: "Binder Trace", Developer: "Foundry Zero", Score: 10}, 19 | } 20 | jsonData, _ := json.Marshal(tools) 21 | fmt.Println(string(jsonData)) 22 | } 23 | ``` 24 | 25 | In the above program listing, `tools` is an `array` type of `Tool` objects, which is passed into `json.Marshal`. The definition of `json.Marshal` includes an `any` type for the input argument. To keep track of the input data type, the Go compiler loads a pointer to read-only metadata for the `[]Tool` data type at runtime, followed by a pointer to the raw object data in memory. 26 | 27 | In previous versions of LLEF, when a breakpoint is set on the call to `json.Marshal`, the register view shows the following: 28 | 29 | ![Previous LLEF analysis output.](./assets/go-examples/json-mashal-old.png) 30 | 31 | With Go features enabled, LLEF can now identify the pointer `0x4c3300` points to a data type definition for `[]Tool`, and used that definition to display the contents of the data pointer in `ebx`: 32 | 33 | ![New LLEF analysis output.](./assets/go-examples/json-marshal-new.png) 34 | 35 | A new command `go get-type` has been added to find Go data type definitions present within the binary. When an argument is given to the command, LLEF will to match the name of the type to the definition present in the Go binary. If no match can be found, LLEF will display similar entries: 36 | 37 | ![Go get-type command.](./assets/go-examples/go-get-type.png) 38 | 39 | When no arguments are given, all data types are displayed. 40 | 41 | If a data type pointer is not passed into a called function alongside the raw data type pointer, LLEF will not be able to determine the matching data type. In this case, an analyst can manually use the command `go unpack-type` to attempt to display a data pointer using a given data type definition. 42 | 43 | ![Manually unpacking types.](./assets/go-examples/go-unpack-type-command.png) 44 | 45 | ## Resolving Go Function Names 46 | 47 | Go binaries store function symbols which are used during exception handling, even for stripped binaries. LLEF can now recover these function symbols and display them: 48 | 49 | ![Display of function symbols in listing view.](./assets/go-examples/function-name-recovery.png) 50 | 51 | A new command `go find-func` has been added to search through function symbols, which is especially useful for finding where to set breakpoints. When no argument is given, all function symbols are displayed. 52 | 53 | ![Finding functions by name.](./assets/go-examples/go-find-func-command.png) 54 | 55 | ## Go call stack unwinding 56 | 57 | LLEF can now display the Go call stack on x86 and x86_64 architectures. This is displayed in the listing view, but can also be displayed with the command `go backtrace`. 58 | 59 | ![Go call stack display.](./assets/go-examples/go-backtrace-command.png) 60 | 61 | ## Before and After 62 | 63 | ### Before 64 | 65 | ![GIF Before](./assets/go-examples/go-mode-disabled.gif) 66 | 67 | ### After 68 | 69 | ![GIF After](./assets/go-examples/go-mode-enabled.gif) -------------------------------------------------------------------------------- /commands/hexdump.py: -------------------------------------------------------------------------------- 1 | """Hexdump command class.""" 2 | 3 | import argparse 4 | import shlex 5 | from typing import Any, Union 6 | 7 | from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext 8 | 9 | from commands.base_command import BaseCommand 10 | from common.constants import SIZES 11 | from common.context_handler import ContextHandler 12 | from common.util import check_process, check_version, hex_int, positive_int 13 | 14 | 15 | class HexdumpCommand(BaseCommand): 16 | """Implements the hexdump command""" 17 | 18 | program: str = "hexdump" 19 | container = None 20 | context_handler: Union[ContextHandler, None] = None 21 | 22 | # Define alias set, where each entry is an alias with any arguments the command should take. 23 | # For example, 'dq' maps to 'hexdump qword'. 24 | alias_set = {"dq": "qword", "dd": "dword", "dw": "word", "db": "byte"} 25 | 26 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 27 | super().__init__() 28 | self.parser = self.get_command_parser() 29 | self.context_handler = ContextHandler(debugger) 30 | 31 | @classmethod 32 | def get_command_parser(cls) -> argparse.ArgumentParser: 33 | """Get the command parser.""" 34 | parser = argparse.ArgumentParser() 35 | parser.add_argument( 36 | "type", 37 | choices=["qword", "dword", "word", "byte"], 38 | default="byte", 39 | help="The format for presenting data", 40 | ) 41 | parser.add_argument( 42 | "--reverse", 43 | action="store_true", 44 | help="The direction of output lines. Low to high by default", 45 | ) 46 | parser.add_argument( 47 | "--size", 48 | type=positive_int, 49 | default=16, 50 | help="The number of qword/dword/word/bytes to display", 51 | ) 52 | parser.add_argument( 53 | "address", 54 | type=hex_int, 55 | help="A value/address/symbol used as the location to print the hexdump from", 56 | ) 57 | return parser 58 | 59 | @staticmethod 60 | def get_short_help() -> str: 61 | """Return a short help message""" 62 | return "Usage: hexdump (qword|dword|word|byte) [-h] [--reverse] [--size SIZE] [address]" 63 | 64 | @staticmethod 65 | def get_long_help() -> str: 66 | """Return a longer help message""" 67 | return HexdumpCommand.get_command_parser().format_help() 68 | 69 | @check_version("15.0.0") 70 | @check_process 71 | def __call__( 72 | self, 73 | debugger: SBDebugger, 74 | command: str, 75 | exe_ctx: SBExecutionContext, 76 | result: SBCommandReturnObject, 77 | ) -> None: 78 | """Handles the invocation of the hexdump command""" 79 | 80 | args = self.parser.parse_args(shlex.split(command)) 81 | 82 | divisions = SIZES[args.type.upper()].value 83 | address = args.address 84 | size = args.size 85 | 86 | if self.context_handler is None: 87 | raise AttributeError("Class not properly initialised: self.context_handler is None") 88 | 89 | self.context_handler.refresh(exe_ctx) 90 | 91 | start = (size - 1) * divisions if args.reverse else 0 92 | end = -divisions if args.reverse else size * divisions 93 | step = -divisions if args.reverse else divisions 94 | 95 | if divisions == SIZES.BYTE.value: 96 | if args.reverse: 97 | self.context_handler.print_bytes(address + size - (size % 16), size % 16) 98 | start = size - (size % 16) - 16 99 | end = -1 100 | step = -16 101 | 102 | for i in range(start, end, -16 if args.reverse else 16): 103 | self.context_handler.print_bytes(address + i, min(16, size - abs(start - i))) 104 | else: 105 | for i in range(start, end, step): 106 | self.context_handler.print_memory_address(address + i, i, divisions) 107 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Foundry Zero Open Source Project Contributing Guide 2 | 3 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | 6 | The following is a set of guidelines for contributing to LLEF which is hosted in the Foundry Zero organisation on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 7 | 8 | ## Code of Conduct 9 | This project and everyone participating in it is governed by the Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to info@foundryzero.co.uk. 10 | 11 | ## What should I know before I get started? 12 | 13 | Take a look at the [README.md](https://github.com/foundryzero/llef/blob/main/README.md) for more information about LLEF. 14 | 15 | ## How Can I Contribute? 16 | 17 | ### Your First Code Contribution 18 | 19 | Unsure where to begin contributing to LLEF? You can start by looking through these `beginner` and `help-wanted` issues: 20 | 21 | * [Beginner issues](https://github.com/foundryzero/llef/labels/good%20first%20issue) - issues which should only require a few lines of code, and a test or two. 22 | * [Help wanted issues](https://github.com/foundryzero/llef/labels/help-wanted) - issues which should be a bit more involved than `beginner` issues. 23 | 24 | #### Pull Requests 25 | 26 | The process described here has several goals: 27 | 28 | - Maintain LLEF's quality 29 | - Fix problems that are important to users 30 | - Engage the community in working toward the best possible LLEF 31 | 32 | Please follow the [styleguides](https://github.com/foundryzero/llef/blob/main/.CONTRIBUTING/PYTHON%20STYLE.md) 33 | 34 | While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. 35 | 36 | 37 | ### Reporting Bugs 38 | 39 | #### Before Submitting A Bug Report 40 | 41 | Before submitting a bug report, please check to see if the bug has already been raised as an issue by searching [our github issues](https://github.com/foundryzero/llef/labels/bug) 42 | 43 | #### How Do I Submit A (Good) Bug Report? 44 | 45 | Bugs are tracked as GitHub issues. Please raise your bug as a GitHub issue using our [enhancement template](https://github.com/foundryzero/llef/blob/main/.github/ISSUE_TEMPLATE/BUG%20REPORT.md). 46 | 47 | Explain the problem and include additional details to help maintainers reproduce the problem: 48 | 49 | * Use a clear and descriptive title for the issue to identify the problem. 50 | * Describe the exact steps which reproduce the problem in as many details as possible. 51 | * Provide specific examples to demonstrate the steps. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use Markdown code blocks. 52 | * Describe the behavior you observed after following the steps and point out what exactly is the problem with that behavior. 53 | * Explain which behavior you expected to see instead and why. 54 | 55 | ### Suggesting Enhancements 56 | 57 | This section guides you through submitting an enhancement suggestion for LLEF, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion 📝 and find related suggestions 🔎. 58 | 59 | #### Before Submitting A Feature Request 60 | 61 | Before submitting a feature request, please check to see if the bug has already been raised as an issue by searching [our github issues](https://github.com/foundryzero/llef/labels/enhancement) 62 | 63 | #### How Do I Submit A (Good) Enhancement Suggestion? 64 | 65 | Features are tracked as GitHub issues. Please raise your bug as a GitHub issue using our [bug issue template](https://github.com/foundryzero/llef/blob/main/.github/ISSUE_TEMPLATE/FEATURE%20REQUEST.md). 66 | 67 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 68 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 69 | * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 70 | * **Describe the current behavior** and **explain which behavior you would like to see instead** and why. 71 | * **Explain why this enhancement would be useful** 72 | * **Specify the name and version of the OS you're using.** 73 | 74 | 75 | ## Attribution 76 | 77 | This contributor guide is based on the [guide](https://github.com/atom/atom/blob/master/.CONTRIBUTING/CONTRIBUTING.md) developed by the Atom project -------------------------------------------------------------------------------- /common/instruction_util.py: -------------------------------------------------------------------------------- 1 | import re 2 | from re import Match 3 | 4 | from lldb import SBAddress, SBInstruction, SBTarget 5 | 6 | from common.color_settings import LLEFColorSettings 7 | from common.golang.analysis import go_annotate_jumps 8 | from common.golang.util import go_context_analysis 9 | from common.output_util import color_string, output_line 10 | from common.settings import LLEFSettings 11 | 12 | 13 | def extract_instructions( 14 | target: SBTarget, start_address: int, end_address: int, disassembly_flavour: str 15 | ) -> list[SBInstruction]: 16 | """ 17 | Returns a list of instructions between a range of memory address defined by @start_address and @end_address. 18 | 19 | :param target: The target context. 20 | :param start_address: The address to start reading instructions from memory. 21 | :param end_address: The address to stop reading instruction from memory. 22 | :return: A list of instructions. 23 | """ 24 | instructions = [] 25 | current = start_address 26 | while current <= end_address: 27 | address = SBAddress(current, target) 28 | instruction = target.ReadInstructions(address, 1, disassembly_flavour).GetInstructionAtIndex(0) 29 | instructions.append(instruction) 30 | instruction_size = instruction.GetByteSize() 31 | if instruction_size > 0: 32 | current += instruction_size 33 | else: 34 | break 35 | 36 | return instructions 37 | 38 | 39 | def color_operands( 40 | operands: str, 41 | color_settings: LLEFColorSettings, 42 | ) -> str: 43 | """ 44 | Colors the registers and addresses in the instruction's operands. 45 | 46 | :param operands: A string of the instruction's operands returned from instruction.GetOperands(). 47 | :param color_settings: Contains the color settings to color the instruction. 48 | """ 49 | 50 | # Addresses can start with either '$0x', '#0x' or just '0x', followed by atleast one hex value. 51 | address_pattern = r"(\$?|#?)-?0x[0-9a-fA-F]+" 52 | 53 | # Registers MAY start with '%'. 54 | # Then there MUST be a sequence of letters, which CAN be followed by a number. 55 | # A register can NEVER start with numbers or any other special character other than '%'. 56 | register_pattern = r"(? str: 59 | return color_string(match.group(0), color_settings.register_color) 60 | 61 | def color_address(match: Match[str]) -> str: 62 | return color_string(match.group(0), color_settings.address_operand_color) 63 | 64 | operands = re.sub(register_pattern, color_register, operands) 65 | operands = re.sub(address_pattern, color_address, operands) 66 | 67 | return operands 68 | 69 | 70 | def print_instruction( 71 | target: SBTarget, 72 | instruction: SBInstruction, 73 | lldb_frame_start: int, 74 | function_start: int, 75 | settings: LLEFSettings, 76 | color_settings: LLEFColorSettings, 77 | highlight: bool = False, 78 | ) -> None: 79 | """ 80 | Print formatted @instruction extracted from SBInstruction object. 81 | 82 | :param target: The target executable. 83 | :param instruction: The instruction object. 84 | :param base: The address base to calculate offsets from. 85 | :param color_settings: Contains the color settings to color the instruction. 86 | :param highlight: If true, highlight the whole instruction with the highlight color. 87 | """ 88 | 89 | address = instruction.GetAddress().GetLoadAddress(target) 90 | offset = address - function_start 91 | 92 | line = hex(address) 93 | if offset >= 0: 94 | line += f" <+{offset:02}>: " 95 | else: 96 | line += f" <-{abs(offset):02}>: " 97 | 98 | mnemonic = instruction.GetMnemonic(target) or "" 99 | operands = instruction.GetOperands(target) or "" 100 | comment = instruction.GetComment(target) or "" 101 | 102 | ops_width = len(operands) # visible length, for spacing (before colouring) 103 | if go_context_analysis(settings): 104 | comment = go_annotate_jumps(target, instruction, lldb_frame_start, comment) 105 | 106 | if not highlight: 107 | operands = color_operands(operands, color_settings) 108 | 109 | if comment != "": 110 | comment = f"; {comment}" 111 | line += f"{mnemonic:<10}{operands.ljust(35 + len(operands) - ops_width)}{comment}" 112 | 113 | if highlight: 114 | line = color_string(line, color_settings.highlighted_instruction_color) 115 | 116 | output_line(line) 117 | 118 | 119 | def print_instructions( 120 | target: SBTarget, 121 | instructions: list[SBInstruction], 122 | lldb_frame_start: int, 123 | function_start: int, 124 | settings: LLEFSettings, 125 | color_settings: LLEFColorSettings, 126 | ) -> None: 127 | """ 128 | Print formatted @instructions extracting information from the SBInstruction objects. 129 | 130 | :param target: The target executable. 131 | :param instructions: A list of instruction objects. 132 | :param base: The address base to calculate offsets from. 133 | :param color_settings: Contains the color settings to color the instruction. 134 | """ 135 | for instruction in instructions: 136 | print_instruction(target, instruction, lldb_frame_start, function_start, settings, color_settings) 137 | -------------------------------------------------------------------------------- /common/expressions/darwin_get_malloc_zones.mm: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a template for an LLDB expression using Objective-C++ syntax. 3 | 4 | The Darwin malloc implementation provides an API to read heap metadata at runtime. 5 | The function 'malloc_get_all_zones' is defined in '' and provides a way to 6 | enumerate allocated heap regions using the malloc zone introspection API. 7 | 8 | Implementation for 'malloc_get_all_zones' can be found here: 9 | https://github.com/apple-oss-distributions/libmalloc/blob/main/src/malloc.c 10 | 11 | Based on LLDB 'heap_find' command: https://github.com/llvm-mirror/lldb/blob/master/examples/darwin/heap_find/heap.py. 12 | 13 | This expression will return an array of structs, with 'lo_addr' and 'hi_addr' for each malloc region. 14 | */ 15 | 16 | 17 | // The calling Python function replaces {{ MAX_MATCHES }} with an integer value. 18 | #define MAX_MATCHES {{MAX_MATCHES}} 19 | 20 | #define KERN_SUCCESS 0 21 | /* For region containing pointers */ 22 | #define MALLOC_PTR_REGION_RANGE_TYPE 2 23 | 24 | // Store information about memory allocations. 25 | typedef struct vm_range_t { 26 | uintptr_t address; 27 | unsigned long size; 28 | } vm_range_t; 29 | 30 | // Function prototypes used for callback functions. 31 | typedef void (*range_callback_t)(unsigned int task, void *baton, unsigned int type, uintptr_t ptr_addr, 32 | uintptr_t ptr_size); 33 | 34 | typedef int (*memory_reader_t)(unsigned int task, uintptr_t remote_address, unsigned long size, void **local_memory); 35 | 36 | typedef void (*vm_range_recorder_t)(unsigned int task, void *baton, unsigned int type, vm_range_t *range, 37 | unsigned int size); 38 | 39 | // We only care about the pointer to enumerator, which is the first pointer in the struct. 40 | // Full definition of malloc_introspection_t available in libmalloc/blob/main/include/malloc/malloc.h 41 | typedef struct malloc_introspection_t { 42 | // Enumerates all the malloc pointers in use 43 | int (*enumerator)(unsigned int task, void *, unsigned int type_mask, uintptr_t zone_address, memory_reader_t reader, 44 | vm_range_recorder_t recorder); 45 | } malloc_introspection_t; 46 | 47 | // We only care about the pointer to malloc_introspection_t which is the 13th pointer in the struct. 48 | // Full definition of malloc_zone_t available in libmalloc/blob/main/include/malloc/malloc.h 49 | typedef struct malloc_zone_t { 50 | void *reserved1[12]; 51 | struct malloc_introspection_t *introspect; 52 | } malloc_zone_t; 53 | 54 | // Information about memory regions to be returned to LLEF. 55 | struct malloc_region { 56 | uintptr_t lo_addr; 57 | uintptr_t hi_addr; 58 | }; 59 | 60 | typedef struct callback_baton_t { 61 | range_callback_t callback; 62 | unsigned int num_matches; 63 | malloc_region matches[MAX_MATCHES + 1]; // Null terminate 64 | } callback_baton_t; 65 | 66 | // Memory read callback function. 67 | memory_reader_t task_peek = [](unsigned int task, uintptr_t remote_address, uintptr_t size, 68 | void **local_memory) -> int { 69 | *local_memory = (void *)remote_address; 70 | return KERN_SUCCESS; 71 | }; 72 | 73 | // Callback to populate structure with low, high malloc addresses. 74 | range_callback_t range_callback = [](unsigned int task, void *baton, unsigned int type, uintptr_t ptr_addr, 75 | uintptr_t ptr_size) -> void { 76 | callback_baton_t *lldb_info = (callback_baton_t *)baton; 77 | // Upper limit for our array 78 | if (lldb_info->num_matches < MAX_MATCHES) { 79 | uintptr_t lo = ptr_addr; 80 | uintptr_t hi = lo + ptr_size; 81 | lldb_info->matches[lldb_info->num_matches].lo_addr = lo; 82 | lldb_info->matches[lldb_info->num_matches].hi_addr = hi; 83 | lldb_info->num_matches++; 84 | } 85 | }; 86 | 87 | // Callback function from introspect enumerator function. 88 | vm_range_recorder_t range_recorder = [](unsigned int task, void *baton, unsigned int type, vm_range_t *ranges, 89 | unsigned int size) -> void { 90 | range_callback_t callback = ((callback_baton_t *)baton)->callback; 91 | for (unsigned int i = 0; i < size; ++i) { 92 | // Call range_callback to record each allocation in baton. 93 | callback(task, baton, type, ranges[i].address, ranges[i].size); 94 | } 95 | }; 96 | 97 | uintptr_t *zones = 0; 98 | unsigned int num_zones = 0; 99 | unsigned int task = 0; 100 | 101 | // Populate zones with pointer to a malloc_zone_t array representing heap zones. 102 | int err = (int)malloc_get_all_zones(task, task_peek, &zones, &num_zones); 103 | 104 | // baton struct used to store data on heap regions between callbacks. 105 | callback_baton_t baton = {range_callback, 0, {0}}; 106 | 107 | if (KERN_SUCCESS == err) { 108 | // Enumerate over all heap zones. 109 | for (unsigned int i = 0; i < num_zones; ++i) { 110 | const malloc_zone_t *zone = (const malloc_zone_t *)zones[i]; 111 | /* Introspection API will call our callback for each heap region (rather than each allocation as in 112 | * malloc_info) */ 113 | if (zone && zone->introspect) 114 | zone->introspect->enumerator(task, &baton, MALLOC_PTR_REGION_RANGE_TYPE, (uintptr_t)zone, task_peek, 115 | range_recorder); 116 | } 117 | } 118 | /* return the value */ 119 | baton.matches -------------------------------------------------------------------------------- /commands/xinfo.py: -------------------------------------------------------------------------------- 1 | """Xinfo command class.""" 2 | 3 | import argparse 4 | import os 5 | import shlex 6 | from typing import Any, Union 7 | 8 | from lldb import ( 9 | SBCommandReturnObject, 10 | SBDebugger, 11 | SBExecutionContext, 12 | SBMemoryRegionInfo, 13 | SBProcess, 14 | SBStream, 15 | SBTarget, 16 | ) 17 | 18 | from arch import get_arch 19 | from commands.base_command import BaseCommand 20 | from common.constants import MSG_TYPE, XINFO 21 | from common.context_handler import ContextHandler 22 | from common.output_util import print_message 23 | from common.state import LLEFState 24 | from common.util import check_process, hex_int 25 | 26 | 27 | class XinfoCommand(BaseCommand): 28 | """Implements the xinfo command""" 29 | 30 | program: str = "xinfo" 31 | container = None 32 | context_handler = None 33 | 34 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 35 | super().__init__() 36 | self.parser = self.get_command_parser() 37 | self.context_handler = ContextHandler(debugger) 38 | self.state = LLEFState() 39 | 40 | @classmethod 41 | def get_command_parser(cls) -> argparse.ArgumentParser: 42 | """Get the command parser.""" 43 | parser = argparse.ArgumentParser() 44 | parser.add_argument( 45 | "address", 46 | type=hex_int, 47 | help="A value/address/symbol used as the location to print the xinfo from", 48 | ) 49 | return parser 50 | 51 | @staticmethod 52 | def get_short_help() -> str: 53 | """Return a short help message""" 54 | return "Usage: xinfo [address]" 55 | 56 | @staticmethod 57 | def get_long_help() -> str: 58 | """Return a longer help message""" 59 | return XinfoCommand.get_command_parser().format_help() 60 | 61 | def get_xinfo(self, process: SBProcess, target: SBTarget, address: int) -> Union[dict[XINFO, Any], None]: 62 | """ 63 | Gets memory region information for a given `address`, including: 64 | - `region_start` address 65 | - `region_end` address 66 | - `region_size` 67 | - `region_offset` (offset of address from start of region) 68 | - file `path` corrosponding to the address 69 | - `inode` of corrosponding file 70 | 71 | :param state: The LLEF state containing platform variable. 72 | :param process: The running process of the target to extract memory regions. 73 | :param target: The target executable. 74 | :param address: The address get information about. 75 | :return: A dictionary containing the information about the address. 76 | The function will return `None` if the address isn't mapped. 77 | """ 78 | memory_region = SBMemoryRegionInfo() 79 | error = process.GetMemoryRegionInfo(address, memory_region) 80 | 81 | if error.Fail() or not memory_region.IsMapped(): 82 | return None 83 | 84 | xinfo: dict[XINFO, Any] = { 85 | XINFO.REGION_START: None, 86 | XINFO.REGION_END: None, 87 | XINFO.REGION_SIZE: None, 88 | XINFO.REGION_OFFSET: None, 89 | XINFO.PERMISSIONS: None, 90 | XINFO.PATH: None, 91 | XINFO.INODE: None, 92 | } 93 | 94 | xinfo[XINFO.REGION_START] = memory_region.GetRegionBase() 95 | xinfo[XINFO.REGION_END] = memory_region.GetRegionEnd() 96 | xinfo[XINFO.REGION_SIZE] = xinfo[XINFO.REGION_END] - xinfo[XINFO.REGION_START] 97 | xinfo[XINFO.REGION_OFFSET] = address - xinfo[XINFO.REGION_START] 98 | 99 | permissions = "" 100 | permissions += "r" if memory_region.IsReadable() else "" 101 | permissions += "w" if memory_region.IsWritable() else "" 102 | permissions += "x" if memory_region.IsExecutable() else "" 103 | xinfo[XINFO.PERMISSIONS] = permissions 104 | 105 | if self.state.platform == "Darwin": 106 | sb_address = target.ResolveLoadAddress(address) 107 | module = sb_address.GetModule() 108 | filespec = module.GetFileSpec() 109 | description = SBStream() 110 | filespec.GetDescription(description) 111 | xinfo[XINFO.PATH] = description.GetData() 112 | else: 113 | xinfo[XINFO.PATH] = memory_region.GetName() 114 | 115 | if xinfo[XINFO.PATH] is not None and os.path.exists(xinfo[XINFO.PATH]): 116 | xinfo[XINFO.INODE] = os.stat(xinfo[XINFO.PATH]).st_ino 117 | else: 118 | xinfo[XINFO.INODE] = None 119 | 120 | return xinfo 121 | 122 | @check_process 123 | def __call__( 124 | self, 125 | debugger: SBDebugger, 126 | command: str, 127 | exe_ctx: SBExecutionContext, 128 | result: SBCommandReturnObject, 129 | ) -> None: 130 | """Handles the invocation of the xinfo command""" 131 | 132 | args = self.parser.parse_args(shlex.split(command)) 133 | address = args.address 134 | 135 | if address < 0 or address > 2 ** get_arch(exe_ctx.target)().bits: 136 | print_message(MSG_TYPE.ERROR, "Invalid address.") 137 | return 138 | 139 | xinfo = self.get_xinfo(exe_ctx.process, exe_ctx.target, address) 140 | 141 | if xinfo is not None: 142 | print_message(MSG_TYPE.SUCCESS, f"Found: {hex(address)}") 143 | print_message( 144 | MSG_TYPE.INFO, 145 | ( 146 | f"Page/Region: {hex(xinfo[XINFO.REGION_START])}->{hex(xinfo[XINFO.REGION_END])}" 147 | f" (size={hex(xinfo[XINFO.REGION_SIZE])})" 148 | ), 149 | ) 150 | print_message(MSG_TYPE.INFO, f"Permissions: {xinfo[XINFO.PERMISSIONS]}") 151 | print_message(MSG_TYPE.INFO, f"Pathname: {xinfo[XINFO.PATH]}") 152 | print_message(MSG_TYPE.INFO, f"Offset (from page/region): +{hex(xinfo[XINFO.REGION_OFFSET])}") 153 | 154 | if xinfo[XINFO.INODE] is not None: 155 | print_message(MSG_TYPE.INFO, f"Inode: {xinfo[XINFO.INODE]}") 156 | else: 157 | print_message(MSG_TYPE.ERROR, "No inode found: Path cannot be found locally.") 158 | else: 159 | print_message(MSG_TYPE.ERROR, f"Not Found: {hex(address)}") 160 | -------------------------------------------------------------------------------- /common/color_settings.py: -------------------------------------------------------------------------------- 1 | """Color settings module""" 2 | 3 | import os 4 | 5 | from common.base_settings import BaseLLEFSettings 6 | from common.constants import TERM_COLORS 7 | from common.output_util import output_line 8 | from common.singleton import Singleton 9 | 10 | 11 | class LLEFColorSettings(BaseLLEFSettings, metaclass=Singleton): 12 | """ 13 | Color settings class - loaded from file defined in `LLEF_CONFIG_PATH` 14 | """ 15 | 16 | LLEF_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".llef_colors") 17 | GLOBAL_SECTION = "LLEF" 18 | 19 | supported_colors: list[str] = [] 20 | 21 | @property 22 | def register_color(self) -> str: 23 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "register_color", fallback="BLUE").upper() 24 | 25 | @property 26 | def modified_register_color(self) -> str: 27 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "modified_register_color", fallback="RED").upper() 28 | 29 | @property 30 | def code_color(self) -> str: 31 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "code_color", fallback="RED").upper() 32 | 33 | @property 34 | def heap_color(self) -> str: 35 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "heap_color", fallback="GREEN").upper() 36 | 37 | @property 38 | def stack_color(self) -> str: 39 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "stack_color", fallback="PINK").upper() 40 | 41 | @property 42 | def string_color(self) -> str: 43 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "string_color", fallback="YELLOW").upper() 44 | 45 | @property 46 | def stack_address_color(self) -> str: 47 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "stack_address_color", fallback="CYAN").upper() 48 | 49 | @property 50 | def function_name_color(self) -> str: 51 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "function_name_color", fallback="GREEN").upper() 52 | 53 | @property 54 | def instruction_color(self) -> str: 55 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "instruction_color", fallback="GREY").upper() 56 | 57 | @property 58 | def highlighted_instruction_color(self) -> str: 59 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "highlighted_instruction_color", fallback="GREEN").upper() 60 | 61 | @property 62 | def line_color(self) -> str: 63 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "line_color", fallback="GREY").upper() 64 | 65 | @property 66 | def rebased_address_color(self) -> str: 67 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "rebased_address_color", fallback="GREY").upper() 68 | 69 | @property 70 | def section_header_color(self) -> str: 71 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "section_header_color", fallback="BLUE").upper() 72 | 73 | @property 74 | def highlighted_index_color(self) -> str: 75 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "highlighted_index_color", fallback="GREEN").upper() 76 | 77 | @property 78 | def index_color(self) -> str: 79 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "index_color", fallback="PINK").upper() 80 | 81 | @property 82 | def dereferenced_value_color(self) -> str: 83 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "dereferenced_value_color", fallback="GREY").upper() 84 | 85 | @property 86 | def dereferenced_register_color(self) -> str: 87 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "dereferenced_register_color", fallback="BLUE").upper() 88 | 89 | @property 90 | def frame_argument_name_color(self) -> str: 91 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "frame_argument_name_color", fallback="YELLOW").upper() 92 | 93 | @property 94 | def read_memory_address_color(self) -> str: 95 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "read_memory_address_color", fallback="CYAN").upper() 96 | 97 | @property 98 | def address_operand_color(self) -> str: 99 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "address_operand_color", fallback="RED").upper() 100 | 101 | @property 102 | def go_type_color(self) -> str: 103 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "go_type_color", fallback="CYAN").upper() 104 | 105 | def __init__(self) -> None: 106 | self.supported_colors = [color.name for color in TERM_COLORS] 107 | self.supported_colors.remove(TERM_COLORS.ENDC.name) 108 | super().__init__() 109 | 110 | def validate_settings(self, setting: str = "") -> bool: 111 | """ 112 | Validate settings by attempting to retrieve all properties thus executing any ConfigParser coverters 113 | Check all colors are valid options 114 | """ 115 | settings_names = LLEFColorSettings._get_setting_names() 116 | 117 | if setting: 118 | if setting not in settings_names: 119 | output_line(f"Invalid LLEF setting {setting}") 120 | return False 121 | settings_names = [setting] 122 | 123 | valid = True 124 | for setting_name in settings_names: 125 | try: 126 | value = getattr(self, setting_name) 127 | if value not in self.supported_colors: 128 | raise ValueError 129 | except ValueError: 130 | valid = False 131 | raw_value = self._RAW_CONFIG.get(self.GLOBAL_SECTION, setting_name) 132 | output_line(f"Error parsing setting {setting_name}. Invalid value '{raw_value}'") 133 | return valid 134 | 135 | def list_settings(self) -> None: 136 | """ 137 | List all color settings and their current values, colored appropriately 138 | """ 139 | supported_colours_strings = [] 140 | for color in self.supported_colors: 141 | supported_colours_strings.append(f"{TERM_COLORS[color].value}{color}{TERM_COLORS.ENDC.value}") 142 | output_line(f"Supported Colors: {', '.join(supported_colours_strings)}\n") 143 | 144 | settings_names = self._get_setting_names() 145 | for setting_name in settings_names: 146 | color = getattr(self, setting_name) 147 | output_line(f"{setting_name}={TERM_COLORS[color].value}{color}{TERM_COLORS.ENDC.value}") 148 | -------------------------------------------------------------------------------- /common/output_util.py: -------------------------------------------------------------------------------- 1 | """Utility functions related to terminal output.""" 2 | 3 | import os 4 | import re 5 | import shutil 6 | from textwrap import TextWrapper 7 | from typing import Any, Union 8 | 9 | from lldb import SBAddress 10 | 11 | from common.constants import ALIGN, DEFAULT_TERMINAL_COLUMNS, DEFAULT_TERMINAL_LINES, GLYPHS, MSG_TYPE, TERM_COLORS 12 | from common.state import LLEFState 13 | 14 | 15 | def generate_rebased_address_string(address: SBAddress, rebase_addresses: bool, rebase_offset: int, col: str) -> str: 16 | module = address.GetModule() 17 | 18 | if module is not None and rebase_addresses is True: 19 | file_name = os.path.basename(str(module.file)) 20 | rebased_address = address.GetFileAddress() + rebase_offset 21 | return color_string(f"({file_name} {rebased_address:#x})", col) 22 | 23 | return "" 24 | 25 | 26 | def color_string(string: str, color_setting: Union[str, None], lwrap: str = "", rwrap: str = "") -> str: 27 | """ 28 | Colors a @string based on the @color_setting. 29 | Optional: Wrap the string with uncolored strings @lwrap and @rwrap. 30 | 31 | :param string: The string to color. 32 | :param color_setting: The color that will be fetched from TERM_COLORS (i.e., TERM_COLORS[color_setting]). 33 | :param lwrap: Uncolored string prepended to the colored @string. 34 | :param rwrap: Uncolored string appended to the colored @string. 35 | :return: The resulting string. 36 | """ 37 | if color_setting is None: 38 | result = f"{lwrap}{string}{rwrap}" 39 | else: 40 | result = f"{lwrap}{TERM_COLORS[color_setting].value}{string}{TERM_COLORS.ENDC.value}{rwrap}" 41 | 42 | return result 43 | 44 | 45 | def terminal_columns() -> int: 46 | """ 47 | Returns the column width of the terminal. If this is not availble in the 48 | terminal environment variables then DEFAULT_TERMINAL_COLUMNS we be returned. 49 | """ 50 | try: 51 | columns = shutil.get_terminal_size().columns or DEFAULT_TERMINAL_COLUMNS 52 | except OSError: 53 | columns = DEFAULT_TERMINAL_COLUMNS 54 | 55 | return columns 56 | 57 | 58 | def remove_color(string: str) -> str: 59 | """Removes all ANSI color character sequences from string.""" 60 | ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") 61 | return ansi_escape.sub("", string) 62 | 63 | 64 | def truncate_line(line: str) -> str: 65 | """Truncates a line to fix within terminal width.""" 66 | truncation_step = 10 67 | color_character_count = len(line) - len(remove_color(line)) 68 | 69 | w = TextWrapper( 70 | width=terminal_columns() + color_character_count, 71 | max_lines=1, 72 | placeholder=f"{TERM_COLORS.ENDC.value}...", 73 | ) 74 | 75 | while len(remove_color(line)) > terminal_columns(): 76 | w.width -= truncation_step 77 | line = w.fill(line) 78 | 79 | return line 80 | 81 | 82 | def output_line(line: Any, never_truncate: bool = False) -> None: 83 | """ 84 | Format a line of output for printing. Print should not be used elsewhere. 85 | Exception - clear_page would not function without terminal characters 86 | """ 87 | 88 | line = str(line) 89 | if LLEFState().use_color is False: 90 | line = remove_color(line) 91 | 92 | if LLEFState().truncate_output and not never_truncate: 93 | for s_line in line.split("\n"): 94 | print(truncate_line(s_line)) 95 | else: 96 | print(line) 97 | 98 | 99 | def clear_page() -> None: 100 | """ 101 | Used to clear the previously printed breakpoint information before 102 | printing the next information. 103 | """ 104 | try: 105 | num_lines = shutil.get_terminal_size().lines 106 | except OSError: 107 | num_lines = DEFAULT_TERMINAL_LINES 108 | 109 | for _ in range(num_lines): 110 | print() 111 | print("\033[0;0H") # Ansi escape code: Set cursor to 0,0 position 112 | print("\033[J") # Ansi escape code: Clear contents from cursor to end of screen 113 | 114 | 115 | def print_line_with_string( 116 | string: str, 117 | char: GLYPHS = GLYPHS.HORIZONTAL_LINE, 118 | line_color: str = TERM_COLORS.GREY.name, 119 | string_color: str = TERM_COLORS.BLUE.name, 120 | align: ALIGN = ALIGN.RIGHT, 121 | ) -> None: 122 | """ 123 | Print a line with the provided @string padded with @char. 124 | 125 | :param string: The string to be embedded in the line. 126 | :param char: The character that the line consist of. 127 | :param line_color: The color setting to define the color of the line. 128 | :param string_color: The color setting to define the color of the embedded string. 129 | :align: Defines where the string will be embedded in the line. 130 | """ 131 | width = terminal_columns() 132 | if align == ALIGN.RIGHT: 133 | l_pad = (width - len(string) - 6) * char.value 134 | r_pad = 4 * char.value 135 | 136 | elif align == ALIGN.CENTRE: 137 | l_pad = (width - len(string)) * char.value 138 | r_pad = 4 * char.value 139 | 140 | else: # align == ALIGN.LEFT: 141 | l_pad = 4 * char.value 142 | r_pad = (width - len(string) - 6) * char.value 143 | 144 | line = color_string(l_pad, line_color) 145 | line += color_string(string, string_color, " ", " ") 146 | line += color_string(r_pad, line_color) 147 | 148 | output_line(line, never_truncate=True) 149 | 150 | 151 | def print_line(char: GLYPHS = GLYPHS.HORIZONTAL_LINE, color: str = TERM_COLORS.GREY.name) -> None: 152 | """Print a line of @char""" 153 | line = color_string(terminal_columns() * char.value, color) 154 | output_line(line, never_truncate=True) 155 | 156 | 157 | def print_message(msg_type: MSG_TYPE, message: str) -> None: 158 | """Format, color and print a @message based on its @msg_type.""" 159 | info_color = TERM_COLORS.BLUE.name 160 | success_color = TERM_COLORS.GREEN.name 161 | error_color = TERM_COLORS.RED.name 162 | 163 | if msg_type == MSG_TYPE.INFO: 164 | message = color_string("[i] ", info_color, rwrap=message) 165 | elif msg_type == MSG_TYPE.SUCCESS: 166 | message = color_string("[+] ", success_color, rwrap=message) 167 | elif msg_type == MSG_TYPE.ERROR: 168 | message = color_string("[-] ", error_color, rwrap=message) 169 | else: 170 | raise KeyError(f"{msg_type} is an invalid MSG_TYPE.") 171 | 172 | output_line(message, never_truncate=True) 173 | -------------------------------------------------------------------------------- /common/golang/data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | from common.golang.constants import GO_STR_TRUNCATE_LEN 5 | 6 | 7 | class Confidence(Enum): 8 | JUNK = 1 9 | LOW = 2 10 | MEDIUM = 3 11 | HIGH = 4 12 | CERTAIN = 5 13 | 14 | def to_float(self) -> float: 15 | if self is Confidence.JUNK: 16 | return 0.0 17 | elif self is Confidence.LOW: 18 | return 0.2 19 | elif self is Confidence.MEDIUM: 20 | return 0.4 21 | elif self is Confidence.HIGH: 22 | return 0.7 23 | else: 24 | # CERTAIN (only to provide a 1.0, not for comparing against!) 25 | return 1.0 26 | 27 | 28 | @dataclass(frozen=True) 29 | class GoData: 30 | """ 31 | Base class for Python representations of Go objects. 32 | """ 33 | 34 | heuristic: float # Internal only: a measure from 0.0-1.0 of how successful the parsing was. 35 | 36 | def confidence(self) -> Confidence: 37 | if self.heuristic < Confidence.LOW.to_float(): 38 | return Confidence.JUNK 39 | elif self.heuristic < Confidence.MEDIUM.to_float(): 40 | return Confidence.LOW 41 | elif self.heuristic < Confidence.HIGH.to_float(): 42 | return Confidence.MEDIUM 43 | else: 44 | return Confidence.HIGH 45 | 46 | 47 | @dataclass(frozen=True) 48 | class GoDataBad(GoData): 49 | """ 50 | The underlying memory region doesn't exist or the values obtained cannot constitute a legal Go object. 51 | """ 52 | 53 | def __str__(self) -> str: 54 | return "?" 55 | 56 | 57 | @dataclass(frozen=True) 58 | class GoDataUnparsed(GoData): 59 | """ 60 | There was more to parse, but we ran out of depth. 61 | The heuristic represents how likely we think the data would be valid. 62 | """ 63 | 64 | address: int 65 | 66 | def __str__(self) -> str: 67 | return hex(self.address) + ".." 68 | 69 | 70 | @dataclass(frozen=True) 71 | class GoDataBool(GoData): 72 | """ 73 | A boolean. 74 | """ 75 | 76 | value: bool 77 | 78 | def __str__(self) -> str: 79 | if self.value: 80 | return "true" 81 | else: 82 | return "false" 83 | 84 | 85 | @dataclass(frozen=True) 86 | class GoDataInteger(GoData): 87 | """ 88 | A uint or int. 89 | """ 90 | 91 | value: int 92 | 93 | def __str__(self) -> str: 94 | return str(self.value) 95 | 96 | 97 | @dataclass(frozen=True) 98 | class GoDataFloat(GoData): 99 | """ 100 | A floating point number. 101 | """ 102 | 103 | value: float 104 | 105 | def __str__(self) -> str: 106 | return str(self.value) 107 | 108 | 109 | @dataclass(frozen=True) 110 | class GoDataComplex(GoData): 111 | """ 112 | A complex number (two floats). 113 | """ 114 | 115 | real: float 116 | imag: float 117 | 118 | def __str__(self) -> str: 119 | return f"({self.real}+{self.imag}i)" 120 | 121 | 122 | @dataclass(frozen=True) 123 | class GoDataArray(GoData): 124 | """ 125 | An array. 126 | """ 127 | 128 | contents: list[GoData] 129 | 130 | def __str__(self) -> str: 131 | build = "[" 132 | for elem in self.contents: 133 | if isinstance(elem, GoDataString): 134 | build += f'"{str(elem)}", ' 135 | else: 136 | build += f"{str(elem)}, " 137 | return build.removesuffix(", ") + "]" 138 | 139 | 140 | @dataclass(frozen=True) 141 | class GoDataSlice(GoData): 142 | """ 143 | A slice. 144 | """ 145 | 146 | base: int 147 | length: int 148 | capacity: int 149 | 150 | # The len(self.contents) may be less than self.length, in the case of a memory read error or truncation. 151 | contents: list[GoData] 152 | 153 | def __str__(self) -> str: 154 | if len(self.contents) == 0: 155 | return f"" 156 | 157 | else: 158 | build = "[" 159 | for elem in self.contents: 160 | build += str(elem) + ", " 161 | build = build.removesuffix(", ") 162 | if len(self.contents) < self.length: 163 | build += f"...{self.length - len(self.contents)} more" 164 | return build + "]" 165 | 166 | 167 | @dataclass(frozen=True) 168 | class GoDataString(GoData): 169 | """ 170 | A string. 171 | """ 172 | 173 | base: int 174 | length: int 175 | 176 | # The len(self.contents) may be less than self.length, in the case of a memory read error. 177 | contents: bytes 178 | 179 | def __str__(self) -> str: 180 | if len(self.contents) == self.length: 181 | full = self.contents.decode("utf-8", "replace") 182 | rep = repr(full) 183 | # Switch single quotes from repr() to double quotes. 184 | if len(rep) >= 2: 185 | rep = rep[1:-1] 186 | if len(rep) > GO_STR_TRUNCATE_LEN: 187 | return rep[: GO_STR_TRUNCATE_LEN - 1] + ".." 188 | else: 189 | return rep 190 | else: 191 | return f"" 192 | 193 | 194 | @dataclass(frozen=True) 195 | class GoDataStruct(GoData): 196 | """ 197 | A struct. 198 | """ 199 | 200 | fields: list[tuple[str, GoData]] 201 | 202 | def __str__(self) -> str: 203 | build = "{" 204 | for f_name, f_val in self.fields: 205 | if isinstance(f_val, GoDataString): 206 | build += f'{f_name}: "{str(f_val)}", ' 207 | else: 208 | build += f"{f_name}: {str(f_val)}, " 209 | build = build.removesuffix(", ") + "}" 210 | return build 211 | 212 | 213 | @dataclass(frozen=True) 214 | class GoDataMap(GoData): 215 | """ 216 | A map. 217 | """ 218 | 219 | entries: list[tuple[GoData, GoData]] 220 | 221 | def __str__(self) -> str: 222 | build = "[" 223 | for key, val in self.entries: 224 | if isinstance(val, GoDataString): 225 | build += f'{key}: "{str(val)}", ' 226 | else: 227 | build += f"{key}: {str(val)}, " 228 | build = build.removesuffix(", ") + "]" 229 | return build 230 | 231 | 232 | @dataclass(frozen=True) 233 | class GoDataPointer(GoData): 234 | """ 235 | A pointer. 236 | """ 237 | 238 | address: int 239 | 240 | def __str__(self) -> str: 241 | return hex(self.address) 242 | -------------------------------------------------------------------------------- /commands/pattern.py: -------------------------------------------------------------------------------- 1 | """Pattern command class.""" 2 | 3 | import argparse 4 | import binascii 5 | import os 6 | import shlex 7 | from typing import Any 8 | 9 | from lldb import SBCommandReturnObject, SBDebugger, SBExecutionContext 10 | 11 | from commands.base_command import BaseCommand 12 | from commands.base_container import BaseContainer 13 | from common.constants import MSG_TYPE, TERM_COLORS 14 | from common.de_bruijn import generate_cyclic_pattern 15 | from common.output_util import output_line, print_message 16 | from common.state import LLEFState 17 | 18 | 19 | class PatternContainer(BaseContainer): 20 | """Creates a container for the Pattern command. Sub commands are implemented in inner classes""" 21 | 22 | container_verb: str = "pattern" 23 | 24 | @staticmethod 25 | def get_short_help() -> str: 26 | return "pattern (create|search)" 27 | 28 | @staticmethod 29 | def get_long_help() -> str: 30 | return """ 31 | Generate or Search a De Bruijn Sequence of unique substrings of length N 32 | and a total length of LENGTH. The default value of N is set to match the 33 | currently loaded architecture. 34 | """ 35 | 36 | 37 | class PatternCreateCommand(BaseCommand): 38 | """Implements the 'create' subcommand""" 39 | 40 | program: str = "create" 41 | container: type[BaseContainer] = PatternContainer 42 | state: LLEFState 43 | 44 | @classmethod 45 | def get_command_parser(cls) -> argparse.ArgumentParser: 46 | """Get the command parser.""" 47 | parser = argparse.ArgumentParser() 48 | parser.add_argument("length", type=int, help="Length of desired output") 49 | parser.add_argument( 50 | "-n", 51 | "--cycle-length", 52 | type=int, 53 | help="The length of the De Bruijn Cycle", 54 | ) 55 | return parser 56 | 57 | @staticmethod 58 | def get_short_help() -> str: 59 | return "Usage: pattern create L [-n]" 60 | 61 | @staticmethod 62 | def get_long_help() -> str: 63 | return ( 64 | "Generate a De Bruijn Sequence of unique substrings of length N and a total length of LENGTH." 65 | + os.linesep 66 | + PatternCreateCommand.get_command_parser().format_help() 67 | ) 68 | 69 | def __init__(self, _: SBDebugger, __: dict[Any, Any]) -> None: 70 | """Class initializer.""" 71 | self.parser = self.get_command_parser() 72 | self.state = LLEFState() 73 | 74 | def __call__( 75 | self, 76 | debugger: SBDebugger, 77 | command: str, 78 | exe_ctx: SBExecutionContext, 79 | result: SBCommandReturnObject, 80 | ) -> None: 81 | """Handles the invocation of 'pattern create' command""" 82 | args = self.parser.parse_args(shlex.split(command)) 83 | length = args.length 84 | num_chars = args.cycle_length or 4 # Hardcoded default value. 85 | print_message(MSG_TYPE.INFO, f"Generating a pattern of {length} bytes (n={num_chars})") 86 | pattern = generate_cyclic_pattern(length, num_chars) 87 | output_line(pattern.decode("utf-8")) 88 | 89 | if exe_ctx.GetProcess().GetState() == 0: 90 | print_message( 91 | MSG_TYPE.ERROR, 92 | "Created pattern cannot be stored in a convenience variable as there is no running process", 93 | ) 94 | else: 95 | value = exe_ctx.GetTarget().EvaluateExpression(f'"{pattern.decode("utf-8")}"') 96 | print_message( 97 | MSG_TYPE.INFO, 98 | f"Pattern saved in variable: {TERM_COLORS.RED.value}{value.GetName()}{TERM_COLORS.ENDC.value}", 99 | ) 100 | self.state.created_patterns.append( 101 | { 102 | "name": value.GetName(), 103 | "pattern_bytes": pattern, 104 | "pattern_string": pattern.decode("utf-8"), 105 | "length": length, 106 | "num_chars": num_chars, 107 | } 108 | ) 109 | 110 | 111 | class PatternSearchCommand(BaseCommand): 112 | """Implements the 'search' subcommand.""" 113 | 114 | program = "search" 115 | container: type[BaseContainer] = PatternContainer 116 | state: LLEFState 117 | 118 | @classmethod 119 | def get_command_parser(cls) -> argparse.ArgumentParser: 120 | """Get the command parser.""" 121 | parser = argparse.ArgumentParser() 122 | parser.add_argument("pattern", help="The pattern of bytes to search for") 123 | return parser 124 | 125 | @staticmethod 126 | def get_short_help() -> str: 127 | return "Usage: pattern search " 128 | 129 | @staticmethod 130 | def get_long_help() -> str: 131 | return ( 132 | "Search a pattern (e.g. a De Bruijn Sequence) of unique substring." 133 | + os.linesep 134 | + PatternCreateCommand.get_command_parser().format_help() 135 | ) 136 | 137 | def __init__(self, _: SBDebugger, __: dict[Any, Any]) -> None: 138 | """Class initializer.""" 139 | self.parser = self.get_command_parser() 140 | self.state = LLEFState() 141 | 142 | def __call__( 143 | self, 144 | debugger: SBDebugger, 145 | command: str, 146 | exe_ctx: SBExecutionContext, 147 | result: SBCommandReturnObject, 148 | ) -> None: 149 | """Handles the invocation of 'pattern create' command.""" 150 | args = self.parser.parse_args(shlex.split(command)) 151 | 152 | pattern = args.pattern 153 | if pattern.startswith("$"): 154 | pattern_value = exe_ctx.GetTarget().EvaluateExpression(pattern) 155 | pattern_array = pattern_value.GetData().uint8 156 | pattern = "".join([chr(x) for x in pattern_array]) 157 | pattern = pattern.rstrip("\x00") 158 | elif pattern.startswith("0x"): 159 | pattern = binascii.unhexlify(pattern[2:]).decode() 160 | else: 161 | pass 162 | if pattern: 163 | for created_pattern in self.state.created_patterns: 164 | pattern_string = created_pattern.get("pattern_string") 165 | if pattern_string and pattern in pattern_string: 166 | print_message( 167 | MSG_TYPE.INFO, 168 | f"Found in {created_pattern.get('name')} at index" 169 | f" {pattern_string.index(pattern)} (little endian)", 170 | ) 171 | reverse_pattern = pattern[::-1] 172 | if pattern_string and reverse_pattern in pattern_string: 173 | print_message( 174 | MSG_TYPE.INFO, 175 | f"Found in {created_pattern.get('name')} at index" 176 | f" {pattern_string.index(reverse_pattern)} (big endian)", 177 | ) 178 | -------------------------------------------------------------------------------- /common/settings.py: -------------------------------------------------------------------------------- 1 | """Global settings module""" 2 | 3 | import os 4 | from typing import Union 5 | 6 | from lldb import SBDebugger 7 | 8 | from arch import supported_arch 9 | from common.base_settings import BaseLLEFSettings 10 | from common.constants import MSG_TYPE 11 | from common.output_util import output_line, print_message 12 | from common.singleton import Singleton 13 | 14 | 15 | class LLEFSettings(BaseLLEFSettings, metaclass=Singleton): 16 | """ 17 | Global general settings class - loaded from file defined in `LLEF_CONFIG_PATH` 18 | """ 19 | 20 | LLEF_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".llef") 21 | GLOBAL_SECTION = "LLEF" 22 | DEFAUL_OUTPUT_ORDER = "registers,stack,code,threads,trace" 23 | debugger: Union[SBDebugger, None] = None 24 | 25 | @property 26 | def color_output(self) -> bool: 27 | default = False 28 | if self.debugger is not None: 29 | default = self.debugger.GetUseColor() 30 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "color_output", fallback=default) 31 | 32 | @property 33 | def register_coloring(self) -> bool: 34 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "register_coloring", fallback=True) 35 | 36 | @property 37 | def show_legend(self) -> bool: 38 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_legend", fallback=True) 39 | 40 | @property 41 | def show_registers(self) -> bool: 42 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_registers", fallback=True) 43 | 44 | @property 45 | def show_stack(self) -> bool: 46 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_stack", fallback=True) 47 | 48 | @property 49 | def show_code(self) -> bool: 50 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_code", fallback=True) 51 | 52 | @property 53 | def show_threads(self) -> bool: 54 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_threads", fallback=True) 55 | 56 | @property 57 | def show_trace(self) -> bool: 58 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_trace", fallback=True) 59 | 60 | @property 61 | def force_arch(self) -> Union[str, None]: 62 | arch = self._RAW_CONFIG.get(self.GLOBAL_SECTION, "force_arch", fallback=None) 63 | return None if arch not in supported_arch else arch 64 | 65 | @property 66 | def rebase_addresses(self) -> bool: 67 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "rebase_addresses", fallback=True) 68 | 69 | @property 70 | def rebase_offset(self) -> int: 71 | return self._RAW_CONFIG.getint(self.GLOBAL_SECTION, "rebase_offset", fallback=0x100000) 72 | 73 | @property 74 | def show_all_registers(self) -> bool: 75 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "show_all_registers", fallback=False) 76 | 77 | @property 78 | def output_order(self) -> str: 79 | return self._RAW_CONFIG.get(self.GLOBAL_SECTION, "output_order", fallback=self.DEFAUL_OUTPUT_ORDER) 80 | 81 | @property 82 | def truncate_output(self) -> bool: 83 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "truncate_output", fallback=True) 84 | 85 | @property 86 | def enable_darwin_heap_scan(self) -> bool: 87 | return self._RAW_CONFIG.getboolean(self.GLOBAL_SECTION, "enable_darwin_heap_scan", fallback=False) 88 | 89 | @property 90 | def go_support_level(self) -> str: 91 | support_level = self._RAW_CONFIG.get(self.GLOBAL_SECTION, "go_support_level", fallback="auto").lower() 92 | return "auto" if support_level not in ("disable", "auto", "force") else support_level 93 | 94 | @property 95 | def max_trace_length(self) -> int: 96 | return self._RAW_CONFIG.getint(self.GLOBAL_SECTION, "max_trace_length", fallback=10) 97 | 98 | @property 99 | def stack_view_size(self) -> int: 100 | return self._RAW_CONFIG.getint(self.GLOBAL_SECTION, "stack_view_size", fallback=12) 101 | 102 | @property 103 | def max_disassembly_length(self) -> int: 104 | return self._RAW_CONFIG.getint(self.GLOBAL_SECTION, "max_disassembly_length", fallback=9) 105 | 106 | @property 107 | def go_confidence_threshold(self) -> str: 108 | threshold = self._RAW_CONFIG.get(self.GLOBAL_SECTION, "go_confidence_threshold", fallback="low").lower() 109 | return "low" if threshold not in ("low", "medium", "high") else threshold 110 | 111 | def validate_output_order(self, value: str) -> None: 112 | default_sections = self.DEFAUL_OUTPUT_ORDER.split(",") 113 | sections = value.split(",") 114 | if len(sections) != len(default_sections): 115 | raise ValueError(f"Requires {len(default_sections)} elements: '{','.join(default_sections)}'") 116 | 117 | missing_sections = [] 118 | for section in default_sections: 119 | if section not in sections: 120 | missing_sections.append(section) 121 | 122 | if len(missing_sections) > 0: 123 | raise ValueError(f"Missing '{','.join(missing_sections)}' from output order.") 124 | 125 | def validate_settings(self, setting: str = "") -> bool: 126 | """ 127 | Validate settings by attempting to retrieve all properties thus executing any ConfigParser coverters 128 | """ 129 | settings_names = LLEFSettings._get_setting_names() 130 | 131 | if setting: 132 | if setting not in settings_names: 133 | output_line(f"Invalid LLEF setting {setting}") 134 | return False 135 | settings_names = [setting] 136 | 137 | valid = True 138 | for setting_name in settings_names: 139 | try: 140 | value = getattr(self, setting_name) 141 | if ( 142 | setting_name == "color_output" 143 | and value is True 144 | and self.debugger is not None 145 | and self.debugger.GetUseColor() is False 146 | ): 147 | raise ValueError("Colour is not supported by your terminal") 148 | 149 | elif setting_name == "output_order": 150 | self.validate_output_order(value) 151 | except ValueError as e: 152 | valid = False 153 | print_message(MSG_TYPE.ERROR, f"Invalid value for {setting_name}. {e}") 154 | return valid 155 | 156 | def __init__(self, debugger: SBDebugger): 157 | super().__init__() 158 | self.debugger = debugger 159 | 160 | def set(self, setting: str, value: str) -> None: 161 | super().set(setting, value) 162 | 163 | if setting == "color_output": 164 | self.state.change_use_color(self.color_output) 165 | elif setting == "truncate_output": 166 | self.state.change_truncate_output(self.truncate_output) 167 | 168 | def load(self, reset: bool = False) -> None: 169 | super().load(reset) 170 | self.state.change_use_color(self.color_output) 171 | self.state.change_truncate_output(self.truncate_output) 172 | -------------------------------------------------------------------------------- /commands/dereference.py: -------------------------------------------------------------------------------- 1 | """Dereference command class.""" 2 | 3 | import argparse 4 | import shlex 5 | from typing import Any, Union 6 | 7 | from lldb import ( 8 | SBAddress, 9 | SBCommandReturnObject, 10 | SBDebugger, 11 | SBError, 12 | SBExecutionContext, 13 | SBInstruction, 14 | SBMemoryRegionInfoList, 15 | SBProcess, 16 | SBTarget, 17 | ) 18 | 19 | from commands.base_command import BaseCommand 20 | from common.color_settings import LLEFColorSettings 21 | from common.constants import GLYPHS, TERM_COLORS 22 | from common.context_handler import ContextHandler 23 | from common.output_util import color_string, output_line 24 | from common.state import LLEFState 25 | from common.util import attempt_to_read_string_from_memory, check_process, hex_int, hex_or_str, is_code, positive_int 26 | 27 | 28 | class DereferenceCommand(BaseCommand): 29 | """Implements the dereference command""" 30 | 31 | program: str = "dereference" 32 | container = None 33 | context_handler: Union[ContextHandler, None] = None 34 | 35 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 36 | super().__init__() 37 | self.parser = self.get_command_parser() 38 | self.context_handler = ContextHandler(debugger) 39 | self.color_settings = LLEFColorSettings() 40 | self.state = LLEFState() 41 | 42 | @classmethod 43 | def get_command_parser(cls) -> argparse.ArgumentParser: 44 | """Get the command parser.""" 45 | parser = argparse.ArgumentParser() 46 | parser.add_argument( 47 | "-l", 48 | "--lines", 49 | type=positive_int, 50 | default=10, 51 | help="The number of consecutive addresses to dereference", 52 | ) 53 | parser.add_argument( 54 | "-b", 55 | "--base", 56 | type=positive_int, 57 | default=0, 58 | help="An address to calculate offsets from. By default this is the stack pointer ($rsp)", 59 | ) 60 | parser.add_argument( 61 | "address", 62 | type=hex_int, 63 | help="A value/address/symbol used as the location to print the dereference from", 64 | ) 65 | return parser 66 | 67 | @staticmethod 68 | def get_short_help() -> str: 69 | """Return a short help message""" 70 | return "Usage: dereference [-h] [-l LINES] [-b OFFSET-BASE] [address]" 71 | 72 | @staticmethod 73 | def get_long_help() -> str: 74 | """Return a longer help message""" 75 | return DereferenceCommand.get_command_parser().format_help() 76 | 77 | def read_instruction(self, target: SBTarget, address: int) -> SBInstruction: 78 | """ 79 | We disassemble an instruction at the given memory @address. 80 | 81 | :param target: The target object file. 82 | :param address: The memory address of the instruction. 83 | :return: An object of the disassembled instruction. 84 | """ 85 | instruction_address = SBAddress(address, target) 86 | instruction_list = target.ReadInstructions(instruction_address, 1, self.state.disassembly_syntax) 87 | return instruction_list.GetInstructionAtIndex(0) 88 | 89 | def dereference_last_address( 90 | self, 91 | data: list[Union[int, str]], 92 | target: SBTarget, 93 | process: SBProcess, 94 | regions: Union[SBMemoryRegionInfoList, None], 95 | ) -> None: 96 | """ 97 | Memory data at the last address (second to last in @data list) is 98 | either disassembled to an instruction or converted to a string or neither. 99 | 100 | :param data: List of memory addresses/data. 101 | :param target: The target object file. 102 | :param process: The running process of the target. 103 | :param regions: List of memory regions of the process. 104 | """ 105 | last_address = data[-2] 106 | if isinstance(last_address, str): 107 | return 108 | 109 | if is_code(last_address, process, target, regions): 110 | instruction = self.read_instruction(target, last_address) 111 | if instruction.IsValid(): 112 | data[-1] = color_string( 113 | f"{instruction.GetMnemonic(target)}{instruction.GetOperands(target)}", 114 | self.color_settings.instruction_color, 115 | ) 116 | else: 117 | string = attempt_to_read_string_from_memory(process, last_address) 118 | if string != "": 119 | data[-1] = color_string(string, self.color_settings.string_color) 120 | 121 | def dereference( 122 | self, address: int, target: SBTarget, process: SBProcess, regions: Union[SBMemoryRegionInfoList, None] 123 | ) -> list[Union[int, str]]: 124 | """ 125 | Dereference a memory @address until it reaches data that cannot be resolved to an address. 126 | Memory data at the last address is either disassembled to an instruction or converted to a string or neither. 127 | The chain of dereferencing is output. 128 | 129 | :param address: The address to dereference 130 | :param offset: The offset of address from a choosen base. 131 | :param target: The target object file. 132 | :param process: The running process of the target. 133 | :param regions: List of memory regions of the process. 134 | """ 135 | 136 | data: list[Union[int, str]] = [] 137 | 138 | error = SBError() 139 | while error.Success(): 140 | data.append(address) 141 | address = process.ReadPointerFromMemory(address, error) 142 | if len(data) > 1 and data[-1] in data[:-2]: 143 | data.append(color_string("[LOOPING]", TERM_COLORS.GREY.name)) 144 | break 145 | 146 | if len(data) < 2: 147 | data.append(color_string("NOT ACCESSIBLE", TERM_COLORS.RED.name)) 148 | else: 149 | self.dereference_last_address(data, target, process, regions) 150 | 151 | return data 152 | 153 | def print_dereference_result(self, result: list[Union[int, str]], offset: int) -> None: 154 | """Format and output the results of dereferencing an address.""" 155 | output = color_string(hex_or_str(result[0]), TERM_COLORS.CYAN.name, rwrap=GLYPHS.VERTICAL_LINE.value) 156 | if offset >= 0: 157 | output += f"+0x{offset:04x}: " 158 | else: 159 | output += f"-0x{-offset:04x}: " 160 | output += " -> ".join(map(hex_or_str, result[1:])) 161 | output_line(output) 162 | 163 | @check_process 164 | def __call__( 165 | self, 166 | debugger: SBDebugger, 167 | command: str, 168 | exe_ctx: SBExecutionContext, 169 | result: SBCommandReturnObject, 170 | ) -> None: 171 | """Handles the invocation of the dereference command""" 172 | 173 | args = self.parser.parse_args(shlex.split(command)) 174 | 175 | start_address = args.address 176 | lines = args.lines 177 | if args.base: 178 | base = args.base 179 | else: 180 | base = start_address 181 | 182 | if self.context_handler is None: 183 | raise AttributeError("Class not properly initialised: self.context_handler is None") 184 | 185 | self.context_handler.refresh(exe_ctx) 186 | 187 | address_size = exe_ctx.target.GetAddressByteSize() 188 | 189 | end_address = start_address + address_size * lines 190 | for address in range(start_address, end_address, address_size): 191 | offset = address - base 192 | deref_result = self.dereference(address, exe_ctx.target, exe_ctx.process, self.context_handler.regions) 193 | self.print_dereference_result(deref_result, offset) 194 | -------------------------------------------------------------------------------- /commands/scan.py: -------------------------------------------------------------------------------- 1 | """Scan command class.""" 2 | 3 | import argparse 4 | import shlex 5 | from typing import Any, Union 6 | 7 | from lldb import ( 8 | SBCommandReturnObject, 9 | SBDebugger, 10 | SBError, 11 | SBExecutionContext, 12 | SBMemoryRegionInfo, 13 | SBProcess, 14 | SBTarget, 15 | SBValue, 16 | ) 17 | 18 | from commands.base_command import BaseCommand 19 | from common.constants import MSG_TYPE 20 | from common.context_handler import ContextHandler 21 | from common.output_util import print_message 22 | from common.state import LLEFState 23 | from common.util import check_process 24 | 25 | 26 | class ScanCommand(BaseCommand): 27 | """Implements the scan command""" 28 | 29 | program: str = "scan" 30 | container = None 31 | context_handler: Union[ContextHandler, None] = None 32 | 33 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 34 | super().__init__() 35 | self.parser = self.get_command_parser() 36 | self.context_handler = ContextHandler(debugger) 37 | self.state = LLEFState() 38 | 39 | @classmethod 40 | def get_command_parser(cls) -> argparse.ArgumentParser: 41 | """Get the command parser.""" 42 | parser = argparse.ArgumentParser() 43 | parser.add_argument( 44 | "search_region", 45 | type=str, 46 | help="Memory region to search through.", 47 | ) 48 | parser.add_argument( 49 | "target_region", 50 | type=str, 51 | help="Memory address range to search for.", 52 | ) 53 | return parser 54 | 55 | @staticmethod 56 | def get_short_help() -> str: 57 | """Return a short help message""" 58 | return "Usage: scan [search_region] [target_region]" 59 | 60 | @staticmethod 61 | def get_long_help() -> str: 62 | """Return a longer help message""" 63 | return ScanCommand.get_command_parser().format_help() 64 | 65 | def parse_address_ranges(self, process: SBProcess, region_name: str) -> list[tuple[int, int]]: 66 | """ 67 | Parse a custom address range (e.g., 0x7fffffffe208-0x7fffffffe240) 68 | or extract address ranges from memory regions with a given name (e.g., libc). 69 | 70 | :param process: Running process of target executable. 71 | :param region_name: A name that can be found in the pathname of memory regions or a custom address range. 72 | :return: A list of address ranges. 73 | """ 74 | address_ranges = [] 75 | 76 | if "-" in region_name: 77 | region_start_end = region_name.split("-") 78 | if len(region_start_end) == 2: 79 | try: 80 | region_start = int(region_start_end[0], 16) 81 | region_end = int(region_start_end[1], 16) 82 | address_ranges.append((region_start, region_end)) 83 | except ValueError: 84 | print_message(MSG_TYPE.ERROR, "Invalid address range.") 85 | else: 86 | address_ranges = self.find_address_ranges(process, region_name) 87 | 88 | return address_ranges 89 | 90 | def find_address_ranges(self, process: SBProcess, region_name: str) -> list[tuple[int, int]]: 91 | """ 92 | Extract address ranges from memory regions with @region_name. 93 | 94 | :param process: Running process of target executable. 95 | :param region_name: A name that can be found in the pathname of memory regions. 96 | :return: A list of address ranges. 97 | """ 98 | 99 | address_ranges = [] 100 | 101 | memory_regions = process.GetMemoryRegions() 102 | memory_region_count = memory_regions.GetSize() 103 | for i in range(memory_region_count): 104 | memory_region = SBMemoryRegionInfo() 105 | if ( 106 | memory_regions.GetMemoryRegionAtIndex(i, memory_region) 107 | and memory_region.IsMapped() 108 | and memory_region.GetName() is not None 109 | and region_name in memory_region.GetName() 110 | ): 111 | region_start = memory_region.GetRegionBase() 112 | region_end = memory_region.GetRegionEnd() 113 | address_ranges.append((region_start, region_end)) 114 | 115 | return address_ranges 116 | 117 | def scan( 118 | self, 119 | search_address_ranges: list[tuple[int, int]], 120 | target_address_ranges: list[tuple[int, int]], 121 | address_size: int, 122 | process: SBProcess, 123 | target: SBTarget, 124 | ) -> list[tuple[SBValue, int]]: 125 | """ 126 | Scan through a given search space in memory for addresses that point towards a target memory space. 127 | 128 | :param search_address_ranges: A list of start and end addresses of memory regions to search. 129 | :param target_address_ranges: A list of start and end addresses defining the range of addresses to search for. 130 | :param address_size: The expected address size for the architecture. 131 | :param process: The running process of the target. 132 | :param target: The target executable. 133 | :return: A list of addresses (with their offsets) in the search space that point towards the target address 134 | space. 135 | """ 136 | results = [] 137 | error = SBError() 138 | for search_start, search_end in search_address_ranges: 139 | for search_address in range(search_start, search_end, address_size): 140 | target_address = process.ReadUnsignedFromMemory(search_address, address_size, error) 141 | if error.Success(): 142 | for target_start, target_end in target_address_ranges: 143 | if target_address >= target_start and target_address < target_end: 144 | offset = search_address - search_start 145 | search_address_value = target.EvaluateExpression(f"{search_address}") 146 | results.append((search_address_value, offset)) 147 | else: 148 | print_message(MSG_TYPE.ERROR, f"Memory at {search_address} couldn't be read.") 149 | return results 150 | 151 | @check_process 152 | def __call__( 153 | self, 154 | debugger: SBDebugger, 155 | command: str, 156 | exe_ctx: SBExecutionContext, 157 | result: SBCommandReturnObject, 158 | ) -> None: 159 | """Handles the invocation of the scan command""" 160 | 161 | args = self.parser.parse_args(shlex.split(command)) 162 | search_region = args.search_region 163 | target_region = args.target_region 164 | 165 | if self.context_handler is None: 166 | raise AttributeError("Class not properly initialised: self.context_handler is None") 167 | 168 | self.context_handler.refresh(exe_ctx) 169 | 170 | search_address_ranges = self.parse_address_ranges(exe_ctx.process, search_region) 171 | target_address_ranges = self.parse_address_ranges(exe_ctx.process, target_region) 172 | 173 | if self.state.platform == "Darwin" and (search_address_ranges == [] or target_address_ranges == []): 174 | print_message( 175 | MSG_TYPE.ERROR, 176 | "Memory region names cannot be resolved on macOS. Use memory address ranges instead.", 177 | ) 178 | return 179 | 180 | print_message(MSG_TYPE.INFO, f"Searching for addresses in '{search_region}' that point to '{target_region}'") 181 | 182 | address_size = exe_ctx.target.GetAddressByteSize() 183 | 184 | results = self.scan(search_address_ranges, target_address_ranges, address_size, exe_ctx.process, exe_ctx.target) 185 | for address, offset in results: 186 | self.context_handler.print_stack_addr(address.GetValueAsUnsigned(), offset) 187 | -------------------------------------------------------------------------------- /common/golang/util_stateless.py: -------------------------------------------------------------------------------- 1 | """Various utility functions used for Go analysis that don't require importing LLEFState. 2 | Mainly used for avoiding circular imports.""" 3 | 4 | import itertools 5 | import math 6 | from typing import Any, Iterator, Union 7 | 8 | from lldb import UINT32_MAX, SBTarget 9 | 10 | from common.constants import pointer 11 | 12 | 13 | def file_to_load_address(target: SBTarget, static_address: int) -> pointer: 14 | """ 15 | Converts an in-file address into an in-memory address. 16 | 17 | :param SBTarget target: The target associated with the current process. 18 | :param int static_address: The address as described in the binary. 19 | :return int: The corresponding address as it has been mapped in memory. 20 | """ 21 | return target.ResolveFileAddress(static_address).GetLoadAddress(target) 22 | 23 | 24 | def read_varint(bytebuf: Any, offset: int) -> tuple[int, int]: 25 | """ 26 | Reads a variable-length unsigned integer (varint) as per Go's encoding. 27 | 28 | :param Any bytebuf: An array-like object supporting the extraction of bytes by indexing. 29 | :param int offset: The offset to start reading the variable-length integer at. 30 | :return tuple[int, int]: A pair of the decoded value and the first unread offset (for chaining calls). 31 | """ 32 | value = 0 33 | shift = 0 34 | 35 | # The number is split into 7-bit groups and encoded with the least significant group first. 36 | # The 8th bit tells us whether another group is coming. 37 | # The number is never more than 32 bits long, so 5 iterations is enough. 38 | # https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub 39 | for _ in range(5): 40 | b = bytebuf[offset] 41 | offset += 1 42 | 43 | # Mask off the continuation-indicating bit. 44 | value |= (b & 0b01111111) << shift 45 | 46 | if b & 0b10000000 == 0: 47 | break 48 | shift += 7 49 | return value & UINT32_MAX, offset 50 | 51 | 52 | def entropy(bitstring: str) -> float: 53 | """ 54 | Calculates the entropy of a short string consisting only of 0s and 1s. The string should be no more than 55 | 1000 characters long, otherwise overly-large numbers will be generated during the calculation. 56 | The method examines the number of bit flips while reading left to right. 57 | 58 | :param str bitstring: A string over '0' and '1' of length up to 1000. 59 | :return float: The probability of seeing the number of bit flips (or a more unlikely result) 60 | if the input were drawn from a unifom random distribution. 61 | """ 62 | n = len(bitstring) - 1 63 | bit_changes = 0 64 | for i in range(1, n + 1): 65 | if bitstring[i - 1] != bitstring[i]: 66 | bit_changes += 1 67 | 68 | if bit_changes > n // 2: 69 | bit_changes = n - bit_changes 70 | coeffs = 0.0 71 | power = 0.5 ** (n - 1) 72 | for x in range(bit_changes + 1): 73 | coeffs += math.comb(n, x) 74 | return coeffs * power 75 | 76 | 77 | def rate_candidate_length(length: int, threshold: float, softness: float) -> float: 78 | """ 79 | Dynamic datatypes that encode their length as a field are normally not too long. If the decoded length is extremely 80 | large, then we probably mistook this memory for a type other than the one it actually is. This function grades the 81 | length of a slice or string, with an ultimately inversely proportional relationship between length and return value. 82 | 83 | :param int length: The candidate length of this datatype. 84 | :param float threshold: The maximum length that can be awarded a score of 1.0. 85 | :param float softness: The resistance of the returned score to decrease - higher means a longer tail in the curve. 86 | :return float: A float between 0.0 and 1.0. 87 | """ 88 | 89 | k = threshold * softness 90 | return min(1.0, k / (length + k - threshold)) 91 | 92 | 93 | class LeastRecentlyAddedDictionary: 94 | """ 95 | A form of least-recently-added mapping datastructure. 96 | The first entry to be evicted is the one that was added/modified last. 97 | The keys are integers in this implementation. 98 | """ 99 | 100 | length: int 101 | capacity: int 102 | addition_uid: Iterator[int] 103 | 104 | # (key, addition_id, value). None means empty slot. 105 | store: list[Union[tuple[int, int, Any], None]] 106 | 107 | def __init__(self, capacity: int = 128): 108 | self.capacity = capacity 109 | self.store = [] 110 | for _ in range(capacity): 111 | self.store.append(None) 112 | self.length = 0 113 | self.addition_uid = itertools.count() 114 | 115 | def __get_idx(self, key: int) -> Union[int, None]: 116 | """ 117 | Internal linear search for a record with matching key. 118 | 119 | :param int key: The search key. 120 | :return Union[int, None]: If found, the index in the store. Otherwise None. 121 | """ 122 | for i in range(self.capacity): 123 | record = self.store[i] 124 | if record is not None and record[0] == key: 125 | return i 126 | return None 127 | 128 | def __get_lra(self) -> int: 129 | """ 130 | Precondition: self.length > 0. 131 | Finds the least-recently-added entry in the store. 132 | 133 | :return int: The index of the least-recently-added entry. 134 | """ 135 | # Precondition: self.length > 0 136 | lowest_uid: Union[int, None] = None 137 | lowest_uid_index = None 138 | for i in range(self.capacity): 139 | record = self.store[i] 140 | if record is not None and (lowest_uid is None or record[1] < lowest_uid): 141 | lowest_uid = record[1] 142 | lowest_uid_index = i 143 | 144 | # lowest_uid_index is always not None by precondition. 145 | if lowest_uid_index is not None: 146 | return lowest_uid_index 147 | else: 148 | return 0 149 | 150 | def add(self, key: int, val: Any) -> None: 151 | """ 152 | Equivalent to Python's dict[key] = val. Will silently overwrite a LRA entry if out of room. 153 | 154 | :param int key: The key to add the value against. 155 | :param Any val: The value to add against the key. 156 | """ 157 | uid = next(self.addition_uid) 158 | record = (key, uid, val) 159 | 160 | if self.length == 0: 161 | # Empty dict 162 | self.store[0] = record 163 | self.length += 1 164 | else: 165 | # Other entries in dict 166 | index = self.__get_idx(key) 167 | if index is not None: 168 | # Already present: overwrite but bump the added time 169 | self.store[index] = record 170 | else: 171 | # Not already present. 172 | if self.length < self.capacity: 173 | # There's a free space somewhere 174 | spot = self.store.index(None) 175 | self.store[spot] = record 176 | self.length += 1 177 | else: 178 | # At capacity: evict the LRA. 179 | lra_index = self.__get_lra() 180 | self.store[lra_index] = record 181 | 182 | def delete(self, key: int) -> None: 183 | """ 184 | Equivalent to Python's del dict[key]. Fails silently if key not in dict. 185 | 186 | :param int key: The key to delete. 187 | """ 188 | position = self.__get_idx(key) 189 | if position is not None: 190 | self.store[position] = None 191 | self.length -= 1 192 | 193 | def search(self, key: int, default: Any = None) -> Any: 194 | """ 195 | Equivalent to Python's dict.get(key, default=...). 196 | Will try to lookup the key, falling back to default if not present. 197 | 198 | :param int key: The key to search for. 199 | :param Any default: The value to return if the key could not be found, defaults to None 200 | :return Any: The value next to the given key, otherwise the default value if it could not be found. 201 | """ 202 | position = self.__get_idx(key) 203 | if position is not None: 204 | # Next line is well-typed because self.__get_idx(key) ensures self.store[position] is not None. 205 | return self.store[position][2] # type: ignore[index] 206 | else: 207 | return default 208 | -------------------------------------------------------------------------------- /common/golang/util.py: -------------------------------------------------------------------------------- 1 | """Various utility functions used for Go analysis""" 2 | 3 | from functools import lru_cache 4 | from typing import Any, Union 5 | 6 | from lldb import SBError, SBFrame, SBProcess 7 | 8 | from arch.aarch64 import Aarch64 9 | from arch.arm import Arm 10 | from arch.base_arch import BaseArch 11 | from arch.i386 import I386 12 | from arch.ppc import PPC 13 | from arch.x86_64 import X86_64 14 | from common.constants import pointer 15 | from common.golang.interfaces import GoFunc 16 | from common.settings import LLEFSettings 17 | from common.state import LLEFState 18 | 19 | 20 | def go_context_analysis(settings: LLEFSettings) -> bool: 21 | """ 22 | Check preconditions for running Go context analysis functions on the current binary: 23 | 1. The current binary is a Go binary. 24 | 2. The one-shot static Go analysis has been performed successfully. 25 | 3. The user-configurable setting for Go analysis is set to either "auto" or "force". 26 | 27 | :param LLEFSettings settings: The LLEFSettings object for accessing the user-configured Go support level. 28 | :return bool: Returns True if all preconditions for Go context analysis are met, otherwise False. 29 | """ 30 | 31 | return ( 32 | LLEFState.go_state.is_go_binary 33 | and LLEFState.go_state.analysed 34 | and settings.go_support_level in ("auto", "force") 35 | ) 36 | 37 | 38 | def is_address_go_frame_pointer(settings: LLEFSettings, addr: int, frame: SBFrame) -> bool: 39 | """ 40 | Checks whether the given address is the current Go frame pointer. 41 | 42 | :param LLEFSettings settings: The LLEFSettings for checking Go support level. 43 | :param int addr: A stack address. 44 | :param SBFrame frame: The current frame containing PC and SP information. 45 | :return bool: Returns true if the given address is the current Go frame pointer. 46 | """ 47 | if go_context_analysis(settings): 48 | # Need to calculate base pointer as it may not be stored in dedicated bp register depending on arch. 49 | bp = go_calculate_base_pointer(frame.GetPC(), frame.GetSP()) 50 | return addr == bp 51 | 52 | return False 53 | 54 | 55 | def bytes_for_saved_pc(arch: type[BaseArch]) -> int: 56 | """ 57 | Calculates how many bytes are taken up by a saved return address. 58 | 59 | :param Type[BaseArch] arch: The class describing our current target architecture 60 | :return int: The size of a saved return pointer. Returns 0 for unsupported architectures. 61 | """ 62 | if arch in (I386, X86_64): 63 | return arch().bits // 8 64 | return 0 65 | 66 | 67 | def go_stackwalk(proc: SBProcess, pc: int, sp: int, bytes_for_pc: int, length: int) -> list[tuple[int, int]]: 68 | """ 69 | Walks back through stack frames from the current frame. Uses metadata intended for Go's panic traceback. 70 | 71 | :param SBProcess proc: The process object currently being debugged. 72 | :param int pc: The current program counter to begin walking at. 73 | :param int sp: The current stack pointer to begin walking at. 74 | :param int bytes_for_pc: How many bytes are taken up by a saved return address. 75 | :return list[tuple[int, int]]: A list of PC, frame pointer pairs tracing through the call stack. 76 | """ 77 | if bytes_for_pc == 0: 78 | # Unsupported architecture for stack walking. 79 | return [] 80 | 81 | out = [] 82 | # Hard-cap the number of iterations, as we only display so many on-screen. 83 | for _ in range(length): 84 | bp = go_calculate_base_pointer(pc, sp) 85 | out.append((pc, bp or 0)) 86 | if bp is None: 87 | break 88 | ra_loc = bp 89 | sp = bp + bytes_for_pc 90 | 91 | err = SBError() 92 | max_pointer_size = 1 << (LLEFState.go_state.pclntab_info.ptr_size * 8) 93 | if ra_loc >= 0 and ra_loc + bytes_for_pc <= max_pointer_size: 94 | pc = proc.ReadUnsignedFromMemory(ra_loc, bytes_for_pc, err) 95 | if err.Fail(): 96 | break 97 | else: 98 | break 99 | return out 100 | 101 | 102 | def go_find_func_name_offset(pc: int) -> tuple[str, int]: 103 | """ 104 | Retrieves the name of the function containing the supplied program counter, and the offset from its entry address. 105 | 106 | :param int pc: Program counter, a pointer to code. 107 | :return tuple[str, int]: Returns the Go function name and offset into it corresponding to the supplied address. 108 | """ 109 | record = go_find_func(pc) 110 | if record is not None: 111 | (entry, gofunc) = record 112 | return (gofunc.name, pc - entry) 113 | 114 | # otherwise, gracefully fail for display purposes 115 | return ("", pc) 116 | 117 | 118 | def pc_binsearch(search_pc: pointer, data: list[tuple[pointer, Any]]) -> Union[tuple[pointer, Any], None]: 119 | """ 120 | Implements a generic binary search to find a record (of any type) 121 | paired to the highest PC that is less than or equal to the search_pc. 122 | 123 | :param pointer search_pc: Program counter, the code address. 124 | :return Union[tuple[pointer, Any], None]: The record associated with the greatest PC still less than search_pc, 125 | otherwise None. 126 | """ 127 | n = len(data) 128 | 129 | # let pcs = map(data, lambda x: x[0]) 130 | # Precondition: pcs is sorted (a safe assumption since the Go runtime relies on it) 131 | # Postcondition: finds i s.t. pcs[i] <= search_pc && pcs[i+1] > search_pc 132 | left = 0 133 | right = n 134 | # Invariant: pcs[0..left) <= search_pc && pcs[right..n) > search_pc. 135 | # The invariant is true at the beginning and end of every loop cycle. 136 | # Liveness variant: (right - left) is strictly decreasing 137 | while right - left > 0: 138 | middle = (left + right) // 2 139 | if data[middle][0] <= search_pc: 140 | left = middle + 1 141 | else: 142 | right = middle 143 | # Inv & !cond => left = right 144 | # => pcs[0..left) <= search_pc && pcs[left..n) > search_pc 145 | 146 | if left == 0: 147 | # all pcs > search_pc 148 | return None 149 | 150 | # func_entries[0..left-1] <= search_pc, so left-1 is our man. 151 | return data[left - 1] 152 | 153 | 154 | # Caching is safe - we clear whenever internal state changes. 155 | @lru_cache(maxsize=128) 156 | def go_find_func(pc: pointer) -> Union[tuple[pointer, GoFunc], None]: 157 | """ 158 | Performs a binary search to find the function record corresponding to a code address. 159 | 160 | :param pointer pc: Program counter, the code address. 161 | :return Union[tuple[pointer, str], None]: Returns the function record containing the program counter, 162 | otherwise None. 163 | """ 164 | result = None 165 | if pc <= LLEFState.go_state.pclntab_info.max_pc_runtime: 166 | func_mapping = LLEFState.go_state.pclntab_info.func_mapping 167 | result = pc_binsearch(pc, func_mapping) 168 | return result 169 | 170 | 171 | # Caching is safe - we clear whenever internal state changes. 172 | @lru_cache(maxsize=128) 173 | def go_calculate_base_pointer(pc: pointer, sp: pointer) -> Union[pointer, None]: 174 | """ 175 | Performs two binary searches to first identify the function, then the stack pointer delta, corresponding to a PC. 176 | 177 | :param pointer pc: The current program counter. 178 | :param pointer sp: The current stack pointer. 179 | :return Union[pointer, None]: Returns the offset from the stack pointer to the Go frame pointer, 180 | else None if unknown. 181 | """ 182 | if pc <= LLEFState.go_state.pclntab_info.max_pc_runtime: 183 | func_mapping = LLEFState.go_state.pclntab_info.func_mapping 184 | 185 | result = pc_binsearch(pc, func_mapping) 186 | if result is not None: 187 | stack_deltas = result[1].stack_deltas 188 | 189 | result2 = pc_binsearch(pc, stack_deltas) 190 | if result2 is not None: 191 | stack_delta: int = result2[1] 192 | return sp + stack_delta 193 | return None 194 | 195 | 196 | def get_arg_registers(arch: BaseArch) -> list[str]: 197 | """ 198 | Get a sequence of register names in which Go will pass arguments before going to the stack. 199 | See https://go.dev/s/regabi. 200 | 201 | :param BaseArch arch: The object describing our current target architecture 202 | :return list[str]: The ordered list of register names that Go passes function arguments in. 203 | """ 204 | if isinstance(arch, I386): 205 | return ["eax", "ebx", "ecx", "edi", "esi"] 206 | elif isinstance(arch, X86_64): 207 | return ["rax", "rbx", "rcx", "rdi", "rsi", "r8", "r9", "r10", "r11"] 208 | elif isinstance(arch, Arm): 209 | return ["r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7"] 210 | elif isinstance(arch, Aarch64): 211 | return ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13", "x14", "x15"] 212 | elif isinstance(arch, PPC): 213 | return ["r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10"] 214 | else: 215 | return [] 216 | -------------------------------------------------------------------------------- /commands/checksec.py: -------------------------------------------------------------------------------- 1 | """Checksec command class.""" 2 | 3 | import argparse 4 | from typing import Any, Union 5 | 6 | from lldb import SBCommandReturnObject, SBDebugger, SBError, SBExecutionContext, SBTarget 7 | 8 | from arch import get_arch 9 | from commands.base_command import BaseCommand 10 | from common.constants import ( 11 | ARCH_BITS, 12 | DYNAMIC_ENTRY_TYPE, 13 | DYNAMIC_ENTRY_VALUE, 14 | EXECUTABLE_TYPE, 15 | MSG_TYPE, 16 | PERMISSION_SET, 17 | PROGRAM_HEADER_TYPE, 18 | SECURITY_CHECK, 19 | SECURITY_FEATURE, 20 | TERM_COLORS, 21 | ) 22 | from common.context_handler import ContextHandler 23 | from common.output_util import color_string, output_line, print_message 24 | from common.util import check_elf, check_target, read_program_int 25 | 26 | PROGRAM_HEADER_OFFSET_32BIT_OFFSET = 0x1C 27 | PROGRAM_HEADER_SIZE_32BIT_OFFSET = 0x2A 28 | PROGRAM_HEADER_COUNT_32BIT_OFFSET = 0x2C 29 | PROGRAM_HEADER_PERMISSION_OFFSET_32BIT_OFFSET = 0x18 30 | 31 | PROGRAM_HEADER_OFFSET_64BIT_OFFSET = 0x20 32 | PROGRAM_HEADER_SIZE_64BIT_OFFSET = 0x36 33 | PROGRAM_HEADER_COUNT_64BIT_OFFSET = 0x38 34 | PROGRAM_HEADER_PERMISSION_OFFSET_64BIT_OFFSET = 0x04 35 | 36 | 37 | class ChecksecCommand(BaseCommand): 38 | """Implements the checksec command""" 39 | 40 | program: str = "checksec" 41 | container = None 42 | context_handler: Union[ContextHandler, None] = None 43 | 44 | def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None: 45 | super().__init__() 46 | self.parser = self.get_command_parser() 47 | self.context_handler = ContextHandler(debugger) 48 | 49 | @classmethod 50 | def get_command_parser(cls) -> argparse.ArgumentParser: 51 | """Get the command parser.""" 52 | parser = argparse.ArgumentParser() 53 | return parser 54 | 55 | @staticmethod 56 | def get_short_help() -> str: 57 | """Return a short help message""" 58 | return "Usage: checksec" 59 | 60 | @staticmethod 61 | def get_long_help() -> str: 62 | """Return a longer help message""" 63 | return ChecksecCommand.get_command_parser().format_help() 64 | 65 | def get_executable_type(self, target: SBTarget) -> int: 66 | """ 67 | Get executable type for a given @target ELF file. 68 | 69 | :param target: The target object file. 70 | :return: An integer representing the executable type. 71 | """ 72 | return read_program_int(target, 0x10, 2) 73 | 74 | def get_program_header_permission(self, target: SBTarget, target_header_type: int) -> Union[int, None]: 75 | """ 76 | Get value of the permission field from a program header entry. 77 | 78 | :param target: The target object file. 79 | :param target_header_type: The type of the program header entry. 80 | :return: An integer between 0 and 7 representing the permission. Returns 'None' if program header is not found. 81 | """ 82 | arch = get_arch(target)().bits 83 | 84 | if arch == ARCH_BITS.BITS_32: 85 | program_header_offset = read_program_int(target, PROGRAM_HEADER_OFFSET_32BIT_OFFSET, 4) 86 | program_header_entry_size = read_program_int(target, PROGRAM_HEADER_SIZE_32BIT_OFFSET, 2) 87 | program_header_count = read_program_int(target, PROGRAM_HEADER_COUNT_32BIT_OFFSET, 2) 88 | program_header_permission_offset = PROGRAM_HEADER_PERMISSION_OFFSET_32BIT_OFFSET 89 | else: 90 | program_header_offset = read_program_int(target, PROGRAM_HEADER_OFFSET_64BIT_OFFSET, 8) 91 | program_header_entry_size = read_program_int(target, PROGRAM_HEADER_SIZE_64BIT_OFFSET, 2) 92 | program_header_count = read_program_int(target, PROGRAM_HEADER_COUNT_64BIT_OFFSET, 2) 93 | program_header_permission_offset = PROGRAM_HEADER_PERMISSION_OFFSET_64BIT_OFFSET 94 | 95 | permission = None 96 | for i in range(program_header_count): 97 | program_header_type = read_program_int(target, program_header_offset + program_header_entry_size * i, 4) 98 | if program_header_type == target_header_type: 99 | permission = read_program_int( 100 | target, program_header_offset + program_header_entry_size * i + program_header_permission_offset, 4 101 | ) 102 | break 103 | 104 | return permission 105 | 106 | def get_dynamic_entry(self, target: SBTarget, target_entry_type: int) -> Union[int, None]: 107 | """ 108 | Get value for a given entry type in the .dynamic section table. 109 | 110 | :param target: The target object file. 111 | :param target_entry_type: The type of the entry in the .dynamic table. 112 | :return: Value of the entry. Returns 'None' if entry type not found. 113 | """ 114 | target_entry_value = None 115 | # Executable has always been observed at module 0, but isn't specifically stated in docs. 116 | module = target.GetModuleAtIndex(0) 117 | section = module.FindSection(".dynamic") 118 | entry_count = int(section.GetByteSize() / 16) 119 | for i in range(entry_count): 120 | entry_type = section.GetSectionData(i * 16, 8).GetUnsignedInt64(SBError(), 0) 121 | entry_value = section.GetSectionData(i * 16 + 8, 8).GetUnsignedInt64(SBError(), 0) 122 | 123 | if target_entry_type == entry_type: 124 | target_entry_value = entry_value 125 | break 126 | 127 | return target_entry_value 128 | 129 | def check_security(self, target: SBTarget) -> dict[SECURITY_FEATURE, SECURITY_CHECK]: 130 | """ 131 | Checks the following security features on the target executable: 132 | - Stack Canary 133 | - NX Support 134 | - PIE Support 135 | - RPath 136 | - RunPath 137 | - Full/Partial RelRO 138 | 139 | :param target: The target executable. 140 | :return: A dictionary showing whether each security feature is enabled or disabled. 141 | """ 142 | checks = { 143 | SECURITY_FEATURE.STACK_CANARY: SECURITY_CHECK.NO, 144 | SECURITY_FEATURE.NX_SUPPORT: SECURITY_CHECK.UNKNOWN, 145 | SECURITY_FEATURE.PIE_SUPPORT: SECURITY_CHECK.UNKNOWN, 146 | SECURITY_FEATURE.NO_RPATH: SECURITY_CHECK.UNKNOWN, 147 | SECURITY_FEATURE.NO_RUNPATH: SECURITY_CHECK.UNKNOWN, 148 | SECURITY_FEATURE.PARTIAL_RELRO: SECURITY_CHECK.UNKNOWN, 149 | SECURITY_FEATURE.FULL_RELRO: SECURITY_CHECK.UNKNOWN, 150 | } 151 | 152 | # Check for Stack Canary 153 | for symbol in target.GetModuleAtIndex(0): 154 | if symbol.GetName() in ["__stack_chk_fail", "__stack_chk_guard", "__intel_security_cookie"]: 155 | checks[SECURITY_FEATURE.STACK_CANARY] = SECURITY_CHECK.YES 156 | break 157 | 158 | # Check for NX Support 159 | try: 160 | if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_STACK) in PERMISSION_SET.NOT_EXEC: 161 | checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.YES 162 | else: 163 | checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.NO 164 | except MemoryError as error: 165 | print_message(MSG_TYPE.ERROR, str(error)) 166 | checks[SECURITY_FEATURE.NX_SUPPORT] = SECURITY_CHECK.UNKNOWN 167 | 168 | # Check for PIE Support 169 | try: 170 | if self.get_executable_type(target) == EXECUTABLE_TYPE.DYN: 171 | checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.YES 172 | else: 173 | checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.NO 174 | except MemoryError as error: 175 | print_message(MSG_TYPE.ERROR, str(error)) 176 | checks[SECURITY_FEATURE.PIE_SUPPORT] = SECURITY_CHECK.UNKNOWN 177 | 178 | # Check for Partial RelRO 179 | try: 180 | if self.get_program_header_permission(target, PROGRAM_HEADER_TYPE.GNU_RELRO) is not None: 181 | checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.YES 182 | else: 183 | checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.NO 184 | except MemoryError as error: 185 | print_message(MSG_TYPE.ERROR, str(error)) 186 | checks[SECURITY_FEATURE.PARTIAL_RELRO] = SECURITY_CHECK.UNKNOWN 187 | 188 | # Check for Full RelRO 189 | if checks[SECURITY_FEATURE.PARTIAL_RELRO] == SECURITY_CHECK.UNKNOWN: 190 | checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.UNKNOWN 191 | elif ( 192 | self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.FLAGS) == DYNAMIC_ENTRY_VALUE.BIND_NOW 193 | and checks[SECURITY_FEATURE.PARTIAL_RELRO] == SECURITY_CHECK.YES 194 | ): 195 | checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.YES 196 | else: 197 | checks[SECURITY_FEATURE.FULL_RELRO] = SECURITY_CHECK.NO 198 | 199 | # Check for No RPath 200 | if self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RPATH) is None: 201 | checks[SECURITY_FEATURE.NO_RPATH] = SECURITY_CHECK.YES 202 | else: 203 | checks[SECURITY_FEATURE.NO_RPATH] = SECURITY_CHECK.NO 204 | 205 | # Check for No RunPath 206 | if self.get_dynamic_entry(target, DYNAMIC_ENTRY_TYPE.RUNPATH) is None: 207 | checks[SECURITY_FEATURE.NO_RUNPATH] = SECURITY_CHECK.YES 208 | else: 209 | checks[SECURITY_FEATURE.NO_RUNPATH] = SECURITY_CHECK.NO 210 | 211 | return checks 212 | 213 | @check_target 214 | @check_elf 215 | def __call__( 216 | self, 217 | debugger: SBDebugger, 218 | command: str, 219 | exe_ctx: SBExecutionContext, 220 | result: SBCommandReturnObject, 221 | ) -> None: 222 | """Handles the invocation of the checksec command""" 223 | 224 | if self.context_handler is None: 225 | raise AttributeError("Class not properly initialised: self.context_handler is None") 226 | 227 | self.context_handler.refresh(exe_ctx) 228 | 229 | target = exe_ctx.GetTarget() 230 | checks = self.check_security(target) 231 | 232 | for check, status in checks.items(): 233 | if status == SECURITY_CHECK.YES: 234 | color = TERM_COLORS.GREEN.name 235 | elif status == SECURITY_CHECK.NO: 236 | color = TERM_COLORS.RED.name 237 | else: 238 | color = TERM_COLORS.GREY.name 239 | check_value_string = check.value + ": " 240 | line = color_string(status.value, color, lwrap=f"{check_value_string:<20}") 241 | output_line(line) 242 | -------------------------------------------------------------------------------- /common/golang/static.py: -------------------------------------------------------------------------------- 1 | """Functions that do (short) one-shot static analysis of a loaded Go binary.""" 2 | 3 | import struct 4 | from dataclasses import dataclass 5 | from typing import Iterator, Union 6 | 7 | from lldb import SBData, SBError, SBModule, SBProcess, SBTarget, eByteOrderLittle 8 | 9 | from common.constants import MSG_TYPE, pointer 10 | from common.golang.constants import GO_MAGICS, GO_NOPTRDATA_NAMES, GO_PCLNTAB_NAMES 11 | from common.golang.interfaces import ModuleDataInfo 12 | from common.golang.moduledata_parser import ModuleDataParser 13 | from common.golang.pclntab_parser import PCLnTabParser 14 | from common.output_util import print_message 15 | from common.settings import LLEFSettings 16 | from common.state import LLEFState 17 | 18 | 19 | def parse_pclntab(proc: SBProcess, target: SBTarget, buf: SBData, file_addr: int) -> bool: 20 | """ 21 | Attempts to parse the PCLNTAB from a Go binary. 22 | 23 | :param SBProcess proc: The process object associated with the target. 24 | :param SBTarget target: The target program running under the debugger, for resolving file->load addresses. 25 | :param SBData buf: Binary buffer containing gopclntab. 26 | :param int file_addr: The file address at which the PCLNTAB's magic bytes are found. 27 | :return bool: Returns True if parsing succeeded (high confidence of actual Go binary). 28 | """ 29 | 30 | err = SBError() 31 | first8bytes = buf.ReadRawData(err, 0, 8) 32 | if err.Success() and first8bytes is not None: 33 | (magic, pad, min_instr_size, ptr_size) = struct.unpack(" Union[ModuleDataInfo, None]: 48 | """ 49 | Attempts to parse the ModuleData structure from a Go binary. This analysis must not be run before parse_pclntab. 50 | 51 | :param SBProcess proc: The process object associated with the target. 52 | :param SBModule module: The debugger module associated with the target. Used for finding sections. 53 | :param SBTarget target: The target program running under the debugger, for resolving file->load addresses. 54 | :param int pclntab_base: The address of the PCLNTAB, to aid searching for ModuleData. 55 | 56 | :return Union[ModuleDataInfo, None]: If parsing was successful, returns a completed ModuleDataInfo. Else None. 57 | """ 58 | 59 | # The search value (pclntab_address) is the address of the PCLNTAB section encoded at the same 60 | # width as a pointer, and assumed Little-Endian since LLEF only currently supports LE. 61 | if LLEFState.go_state.pclntab_info.ptr_size == 4: 62 | pclntab_address = struct.pack(" Iterator[CandidatePCLnTab]: 110 | """ 111 | An iterator through possible PCLNTAB locations. Tries specific section names at first and falls back to a byte scan 112 | for the magic value. 113 | This is an iterator, rather than returning a list, because the suggestions are ordered in progressive order of 114 | computational intensity. Iterators are lazy so we don't have to do the expensive ones if a cheap one succeeds. 115 | 116 | :param SBModule module: The process object associated with the target. 117 | :param SBTarget target: The target program running under the debugger. 118 | :return Iterator[Candidate]: An iterator over results, each consisting of the containing buffer and location info. 119 | """ 120 | 121 | # ELF and Mach-O formats 122 | for pclntab_name in GO_PCLNTAB_NAMES: 123 | section = module.FindSection(pclntab_name) 124 | if section is not None and section.IsValid(): 125 | section_data = section.GetSectionData() 126 | if section_data is not None and section_data.IsValid(): 127 | yield CandidatePCLnTab( 128 | buffer=section_data, 129 | file_address=section.GetFileAddress(), 130 | load_address=section.GetLoadAddress(target), 131 | ) 132 | 133 | # Check if Windows 134 | windows = False 135 | header = module.GetSectionAtIndex(0) 136 | if header is not None and header.IsValid(): 137 | header_data = header.GetSectionData() 138 | if header_data is not None and header_data.IsValid(): 139 | first_two_bytes = header_data.uint8[0:2] 140 | # 'MZ' or 'ZM' magic number. 141 | if first_two_bytes in ([0x4D, 0x5A], [0x5A, 0x4D]): 142 | windows = True 143 | 144 | read_only_data = None 145 | rdata_sect = None 146 | if windows: 147 | # *********************************************************************************** 148 | # Upon reaching this point, we're about to do some heavy static scanning of the binary. 149 | # This is okay if the user has explicitly forced Go mode, but otherwise (auto) we should 150 | # quit and wait for the user to do that later on. 151 | # *********************************************************************************** 152 | if settings.go_support_level == "auto": 153 | settings.set("go_support_level", "disable") 154 | LLEFState.go_state.analysed = False 155 | return 156 | 157 | # Heavy scanning permitted from here. 158 | # Obtain read-only data as Python bytes 159 | rdata_sect = module.FindSection(".rdata") 160 | if rdata_sect is not None and rdata_sect.IsValid(): 161 | rdata = rdata_sect.GetSectionData() 162 | if rdata is not None and rdata.IsValid(): 163 | err = SBError() 164 | rdata_bytes = rdata.ReadRawData(err, 0, rdata.GetByteSize()) 165 | if err.Success() and rdata_bytes is not None: 166 | read_only_data = rdata_bytes 167 | 168 | # read_only_data not None implies rdata_sect not None, but the type checker doesn't know this. 169 | if read_only_data is not None and rdata_sect is not None: 170 | # If successful, initiate a manual search for PCLNTAB over each value it could start with. 171 | print_message(MSG_TYPE.INFO, "PE binary detected. Scanning for Golang...") 172 | ptr_size = module.GetAddressByteSize() 173 | # struct.iter_unpack requires that read_only_data be a multiple of 4 bytes. We just ensure our local copy is 174 | # a multiple of the pointer size (which will be 4 or 8) for easier alignment. 175 | while len(read_only_data) % ptr_size != 0: 176 | read_only_data += b"\x00" 177 | 178 | for magic in GO_MAGICS: 179 | search_pattern = struct.pack(" None: 211 | """ 212 | Called once for a newly-loaded binary. Sets up go_state. 213 | settings.go_support_level is either auto or force, and go_state.analysed is False. 214 | 215 | :param SBProcess proc: The process object associated with the target. 216 | :param SBTarget target: The target program running under the debugger. 217 | """ 218 | LLEFState.go_state.analysed = True 219 | 220 | # The executable has always been observed at module 0. 221 | module = target.GetModuleAtIndex(0) 222 | 223 | if module.IsValid(): 224 | for candidate in pclntab_candidates(module, target, settings): 225 | LLEFState.go_state.is_go_binary = parse_pclntab(proc, target, candidate.buffer, candidate.load_address) 226 | if LLEFState.go_state.is_go_binary: 227 | print_message(MSG_TYPE.SUCCESS, "Golang detected. Parsing type information...") 228 | LLEFState.go_state.moduledata_info = parse_moduledata(proc, module, target, candidate.file_address) 229 | if LLEFState.go_state.moduledata_info is not None: 230 | print_message(MSG_TYPE.SUCCESS, "Type information found.") 231 | 232 | else: 233 | print_message(MSG_TYPE.ERROR, "No type information available.") 234 | break 235 | -------------------------------------------------------------------------------- /common/golang/moduledata_parser.py: -------------------------------------------------------------------------------- 1 | """Class for extracting information from a Go moduledata structure.""" 2 | 3 | import struct 4 | from typing import Union 5 | 6 | from lldb import UINT32_MAX, SBData, SBError, SBProcess, SBTarget 7 | 8 | from common.constants import pointer 9 | from common.golang.constants import GO_MD_7_ONLY, GO_MD_8_TO_15, GO_MD_16_TO_17, GO_MD_18_TO_19, GO_MD_20_TO_24 10 | from common.golang.interfaces import ModuleDataInfo 11 | from common.golang.types import GoType, PopulateInfo, TypeHeader 12 | from common.golang.util_stateless import file_to_load_address, read_varint 13 | from common.state import LLEFState 14 | 15 | 16 | class ModuleDataParser: 17 | """ 18 | Stores information about the ModuleData context and parses type information from it. 19 | 20 | Latest ModuleData struct information found at: https://github.com/golang/go/blob/master/src/runtime/symtab.go. 21 | """ 22 | 23 | section_offset: int 24 | 25 | # Start of the 'types' section pointed to by the Go moduledata struct. Name aligns with Go source. 26 | types: pointer 27 | 28 | # End of the 'types' section pointed to by the Go moduledata struct. Name aligns with Go source. 29 | etypes: pointer 30 | 31 | # typelinks is an array of offsets to these type information structures. The length is typelinks_len. 32 | # Name aligns with Go source. 33 | typelinks: int 34 | typelinks_len: int 35 | 36 | # A map holding successfully parsed GoType Python structures with their associated addresses. 37 | # Name aligns with Go source. 38 | __type_structs: dict[pointer, GoType] 39 | 40 | def __init__(self, section_offset: int) -> None: 41 | self.section_offset = section_offset 42 | self.__type_structs = {} 43 | 44 | def get_name(self, type_section: bytes, name_offset: int, header: TypeHeader) -> Union[str, None]: 45 | """ 46 | Run the version-specific procedure for decoding the name of a type from memory. 47 | 48 | :param bytes type_section: Slice of program memory from self.types to self.etypes. 49 | :param int name_offset: The offset within the section to begin reading at. 50 | :param TypeHeader header: Information about this type, such as the tflag. 51 | :return Union[str, None]: If name_offset is valid, returns the decoded name. Otherwise, None. 52 | """ 53 | 54 | name = None 55 | # Check that pointer + offset doesn't exceed the end pointer for the types section. 56 | if self.types + name_offset < self.etypes: 57 | # Module data layout depends on the Go version. 58 | (go_min_version, go_max_version) = LLEFState.go_state.pclntab_info.version_bounds 59 | if go_min_version >= 17: 60 | length, name_offset = read_varint(type_section, name_offset) 61 | if self.types + name_offset + length <= self.etypes: 62 | name = type_section[name_offset : name_offset + length].decode("utf-8", "replace") 63 | # Sometimes names start with an extraneous asterisk (*) - tflag tells us when. 64 | if header.tflag & 2: 65 | name = name[1:] 66 | 67 | elif go_max_version <= 16: 68 | (length,) = struct.unpack_from(">H", type_section, name_offset) 69 | name_offset += 2 70 | if self.types + name_offset + length <= self.etypes: 71 | name = type_section[name_offset : name_offset + length].decode("utf-8", "replace") 72 | if header.tflag & 2: 73 | name = name[1:] 74 | 75 | return name 76 | 77 | def parse_type(self, type_section: bytes, type_offset: int) -> bool: 78 | """ 79 | Decodes and adds to internal state an individual type information structure. 80 | 81 | :param bytes type_section: Slice of program memory from self.types to self.etypes. 82 | :param int offset: The offset within the section to begin parsing at. 83 | :return bool: If parsing the type (and all children/parent types) succeeds, returns True. Otherwise False. 84 | """ 85 | type_address = self.types + type_offset 86 | if type_address in self.__type_structs: 87 | # Type already parsed. 88 | return True 89 | 90 | ptr_size = LLEFState.go_state.pclntab_info.ptr_size 91 | if ptr_size == 4: 92 | ptr_specifier = "I" 93 | else: 94 | # ptr_size == 8 here. 95 | ptr_specifier = "Q" 96 | 97 | # Send some useful information to populate(). 98 | info = PopulateInfo(types=self.types, etypes=self.etypes, ptr_size=ptr_size, ptr_specifier=ptr_specifier) 99 | 100 | type_entry_width = ptr_size * 4 + 16 101 | # Check that struct.unpack_from() won't read outside bounds. 102 | if type_address + type_entry_width <= self.etypes: 103 | header = TypeHeader() 104 | 105 | # Luckily, this format has remained the same for all Go versions since inception. 106 | unpacker = "<" + ptr_specifier * 2 + "IBBBB" + ptr_specifier * 2 + "II" 107 | tup = struct.unpack_from(unpacker, type_section, type_offset) 108 | ( 109 | header.size, # usize 110 | header.ptrbytes, # usize 111 | header.t_hash, # uint32 112 | header.tflag, # uint8 113 | header.align, # uint8 114 | header.fieldalign, # uint8 115 | header.kind, # uint8 116 | _, # (equal) usize 117 | _, # (gcdata) usize 118 | name_offset, # uint32 119 | ptr_to_this_offset, # uint32 120 | ) = tup 121 | 122 | name_offset += 1 123 | name = self.get_name(type_section, name_offset, header) 124 | if name is not None: 125 | header.name = name 126 | 127 | go_type = GoType.make_from(header, LLEFState.go_state.pclntab_info.version_bounds) 128 | 129 | if go_type is not None: 130 | # Each type has a corresponding populate() function. 131 | type_struct_pointers = go_type.populate(type_section, type_offset + type_entry_width, info) 132 | 133 | # If an error occurred during parsing: type_struct_pointers is None 134 | # Otherwise, if simple data type: type_struct_pointers is [] 135 | # Otherwise, if complex data type: it's a list of pointers go walk over next (recursively). 136 | if type_struct_pointers is not None: 137 | self.__type_structs[type_address] = go_type 138 | 139 | processing_valid = True 140 | for type_addr in type_struct_pointers: 141 | if self.types <= type_addr < self.etypes: 142 | if not self.parse_type(type_section, type_addr - self.types): 143 | processing_valid = False 144 | break 145 | else: 146 | processing_valid = False 147 | break 148 | 149 | if processing_valid and ptr_to_this_offset not in (0, UINT32_MAX): 150 | if not self.parse_type(type_section, ptr_to_this_offset): 151 | processing_valid = False 152 | return processing_valid 153 | 154 | return False 155 | 156 | def parse(self, proc: SBProcess, data: SBData, target: SBTarget) -> Union[ModuleDataInfo, None]: 157 | """ 158 | Attempts to parse a candidate ModuleData, as located by self.section_offset. 159 | 160 | :param SBProcess proc: The process currently being debugged. 161 | :param SBData data: The buffer holding the candidate ModuleData structure. 162 | :param SBTarget target: The target associated with the process. Used for resolving file->load addresses. 163 | :return Union[ModuleDataInfo, None]: If run on a real ModuleData, and a supported Go version, then returns 164 | the parsed information as a data structure. Otherwise None. 165 | """ 166 | 167 | offsets = None 168 | 169 | (min_go, max_go) = LLEFState.go_state.pclntab_info.version_bounds 170 | if min_go == 7 and max_go == 7: 171 | offsets = GO_MD_7_ONLY 172 | if min_go >= 8 and max_go <= 15: 173 | offsets = GO_MD_8_TO_15 174 | elif min_go >= 16 and max_go <= 17: 175 | offsets = GO_MD_16_TO_17 176 | elif min_go >= 18 and max_go <= 19: 177 | offsets = GO_MD_18_TO_19 178 | elif min_go >= 20 and max_go <= 24: 179 | offsets = GO_MD_20_TO_24 180 | 181 | module_data_info = None 182 | 183 | if offsets is not None: 184 | if LLEFState.go_state.pclntab_info.ptr_size == 4: 185 | reader = data.uint32[self.section_offset // 4 :] 186 | else: 187 | # ptr_size == 8 here. 188 | reader = data.uint64[self.section_offset // 8 :] 189 | 190 | # Use these fields as a sanity check, to ensure we really did find ModuleData. 191 | min_program_counter = reader[offsets.minpc] 192 | max_program_counter = reader[offsets.maxpc] 193 | first_function_address = LLEFState.go_state.pclntab_info.func_mapping[0][1].file_addr 194 | if ( 195 | min_program_counter == first_function_address 196 | and max_program_counter == LLEFState.go_state.pclntab_info.max_pc_file 197 | ): 198 | self.types = file_to_load_address(target, reader[offsets.types]) 199 | # -1 +1 so that we don't miss the end of the section by 1, and the file->load resolution then fails. 200 | self.etypes = file_to_load_address(target, reader[offsets.etypes] - 1) + 1 201 | self.typelinks = file_to_load_address(target, reader[offsets.typelinks]) 202 | self.typelinks_len = reader[offsets.typelinks_len] 203 | 204 | err = SBError() 205 | type_section = proc.ReadMemory(self.types, self.etypes - self.types, err) 206 | if err.Success() and type_section is not None: 207 | read_success = True 208 | for i in range(self.typelinks_len): 209 | err = SBError() 210 | offset = proc.ReadUnsignedFromMemory(self.typelinks + i * 4, 4, err) 211 | if err.Fail(): 212 | read_success = False 213 | if not self.parse_type(type_section, offset): 214 | read_success = False 215 | 216 | # Now we have discovered everything, go and fill in links from type to type. 217 | if read_success and len(self.__type_structs) > 0: 218 | for go_type in self.__type_structs.values(): 219 | go_type.fixup_types(self.__type_structs) 220 | module_data_info = ModuleDataInfo(type_structs=self.__type_structs) 221 | 222 | return module_data_info 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | llef logo 3 |

4 | 5 | # LLEF 6 | 7 | LLEF (pronounced ɬɛf - "hlyeff") is an LLDB plugin to make it more usable for low-level RE and VR. Similar to [GEF](https://github.com/hugsy/gef), but for LLDB. 8 | 9 | It uses LLDB's Python API to add extra status output and a few new commands, so that security researchers can more easily use LLDB to analyse software as it's running. 10 | 11 | ![llef demo](https://foundryzero.co.uk/assets/img/llef-small.gif) 12 | 13 | ## 💻 Supported Architectures 14 | * x86_64 15 | * arm 16 | * aarch64 / arm64 17 | * i386 18 | * PowerPC 19 | 20 | ## 📓 Requirements 21 | * LLDB 15+ (https://apt.llvm.org/) _On macOS this is bundled with Xcode 14.3+_ 22 | 23 | ## ⚙ Installation 24 | The instructions below will install LLEF so that it is used by LLDB by default. 25 | 26 | 1. Clone the repository. 27 | 2. `cd ` 28 | 3. Run `./install.sh` 29 | 4. Select automatic (overwrites `~/.lldbinit`) or manual installation. 30 | 31 | _LLDB uses AT&T disassembly syntax for x86 binaries by default. The installer provides an option to override this._ 32 | 33 | ## ▶ Usage 34 | 35 | ### Launch LLDB 36 | 37 | ```bash 38 | lldb-15 39 | ``` 40 | 41 | ### Use commands: 42 | 43 | #### llefsettings 44 | Various commands for setting, saving, loading and listing LLEF specific commands: 45 | ``` 46 | (lldb) llefsettings --help 47 | list list all settings 48 | save Save settings to config file 49 | reload Reload settings from config file (retain session values) 50 | reset Reload settings from config file (purge session values) 51 | set Set LLEF settings 52 | ``` 53 | 54 | Settings are stored in a file `.llef` located in your home directory formatted as following: 55 | ``` 56 | [LLEF] 57 | = 58 | ``` 59 | 60 | ##### Available Settings 61 | 62 | | Setting | Type | Description | 63 | | ----------------------- | ------- | -------------------------------------------------- | 64 | | color_output | Boolean | Enable/disable color terminal output | 65 | | register_coloring | Boolean | Enable/disable register coloring | 66 | | show_legend | Boolean | Enable/disable legend output | 67 | | show_registers | Boolean | Enable/disable registers output | 68 | | show_stack | Boolean | Enable/disable stack output | 69 | | show_code | Boolean | Enable/disable code output | 70 | | show_threads | Boolean | Enable/disable threads output | 71 | | show_trace | Boolean | Enable/disable trace output | 72 | | force_arch | String | Force register display architecture (experimental) | 73 | | rebase_addresses | Boolean | Enable/disable address rebase output | 74 | | rebase_offset | Int | Set the rebase offset (default 0x100000) | 75 | | show_all_registers | Boolean | Enable/disable extended register output | 76 | | 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 | 77 | | max_trace_length | Int | Set the maximum length of the call stack backtrace to display | 78 | | stack_view_size | Int | Set the number of entries in the stack read to display | 79 | | max_disassembly_length | Int | Set the maximum number of instructions to disassemble and display around the current PC | 80 | | go_support_level | String | Control Golang-specific analysis. `disable` / `auto` (default) / `force`. For performance reasons, Go support in Windows binaries requires `force`. | 81 | | go_confidence_threshold | String | Set the confidence threshold (`low` / `medium` / `high`) for Go objects to be shown in the context view | 82 | 83 | #### llefcolorsettings 84 | Allows setting LLEF GUI colors: 85 | ``` 86 | (lldb) llefcolorsettings --help 87 | list list all color settings 88 | save Save settings to config file 89 | reload Reload settings from config file (retain session values) 90 | reset Reload settings from config file (purge session values) 91 | set Set LLEF color settings 92 | ``` 93 | 94 | ##### Available Color Settings 95 | 96 | Supported colors: BLUE, GREEN, YELLOW, RED, PINK, CYAN, GREY 97 | 98 | | Color | 99 | | ----------------------------- | 100 | | register_color | 101 | | modified_register_color | 102 | | code_color | 103 | | heap_color | 104 | | stack_color | 105 | | string_color | 106 | | stack_address_color | 107 | | function_name_color | 108 | | instruction_color | 109 | | highlighted_instruction_color | 110 | | line_color | 111 | | rebased_address_color | 112 | | section_header_color | 113 | | highlighted_index_color | 114 | | index_color | 115 | | dereferenced_value_color | 116 | | dereferenced_register_color | 117 | | frame_argument_name_color | 118 | | read_memory_address_color | 119 | | go_type_color | 120 | 121 | #### Hexdump 122 | View memory contents with: 123 | ``` 124 | (lldb) hexdump type address [--size SIZE] [--reverse] 125 | ``` 126 | e.g. 127 | ``` 128 | (lldb) hexdump byte 0x7fffffffecc8 --size 0x38 129 | 0x7fffffffecc8 3d 2f 75 73 72 2f 6c 6f 63 61 6c 2f 73 62 69 6e =/usr/local/sbin 130 | 0x7fffffffecd8 3a 2f 75 73 72 2f 6c 6f 63 61 6c 2f 62 69 6e 3a :/usr/local/bin: 131 | 0x7fffffffece8 2f 75 73 72 2f 73 62 69 6e 3a 2f 75 73 72 2f 62 /usr/sbin:/usr/b 132 | 0x7fffffffecf8 69 6e 3a 2f 73 62 69 6e in:/sbin 133 | (lldb) hexdump word 0x7fffffffecc8 --reverse 134 | 0x7fffffffece6│+001e: 0x4654 135 | 0x7fffffffece4│+001c: 0x4361 136 | 0x7fffffffece2│+001a: 0x746f 137 | 0x7fffffffece0│+0018: 0x4e23 138 | 0x7fffffffecde│+0016: 0x3f73 139 | 0x7fffffffecdc│+0014: 0x6968 140 | 0x7fffffffecda│+0012: 0x742d 141 | 0x7fffffffecd8│+0010: 0x6564 142 | 0x7fffffffecd6│+000e: 0x6f63 143 | 0x7fffffffecd4│+000c: 0x6564 144 | 0x7fffffffecd2│+000a: 0x2d75 145 | 0x7fffffffecd0│+0008: 0x6f79 146 | 0x7fffffffecce│+0006: 0x2d64 147 | 0x7fffffffeccc│+0004: 0x6964 148 | 0x7fffffffecca│+0002: 0x2d79 149 | 0x7fffffffecc8│+0000: 0x6857 150 | ``` 151 | 152 | #### Context 153 | 154 | Refresh the LLEF GUI with: 155 | ``` 156 | (lldb) context 157 | ``` 158 | Refresh components of the LLEF GUI with: 159 | ``` 160 | (lldb) context [{registers,stack,code,threads,trace,all} ...] 161 | ``` 162 | 163 | #### Pattern Create 164 | ``` 165 | (lldb) pattern create 10 166 | [+] Generating a pattern of 10 bytes (n=4) 167 | aaaabaaaca 168 | [+] Pattern saved in variable: $8 169 | (lldb) pattern create 100 -n 2 170 | [+] Generating a pattern of 100 bytes (n=2) 171 | aabacadaea 172 | [+] Pattern saved in variable: $9 173 | ``` 174 | 175 | #### Pattern Search 176 | 177 | ``` 178 | (lldb) pattern search $rdx 179 | [+] Found in $10 at index 45 (big endian) 180 | (lldb) pattern search $8 181 | [+] Found in $10 at index 0 (little endian) 182 | (lldb) pattern search aaaabaaac 183 | [+] Found in $8 at index 0 (little endian) 184 | (lldb) pattern search 0x61616161626161616361 185 | [+] Found in $8 at index 0 (little endian) 186 | ``` 187 | 188 | #### (Go) Unpack Type 189 | 190 | ``` 191 | (lldb) go unpack-type 0xc000130000 []main.Country 192 | [{Name:'Japan' Capital:'Tokyo' Continent:'Asia'} {Name:'Germany' Capital:'Berlin' Continent:'Europe'}] 193 | (lldb) go unpack-type 0xc000130000 []main.Country --depth 1 194 | [0xc000142000.. 0xc000142030..] 195 | (lldb) go unpack-type 0xc000142000 main.Country 196 | {Name:'Japan' Capital:'Tokyo' Continent:'Asia'} 197 | (lldb) go unpack-type 0xc000142000 [6]uintptr 198 | [0xc000114140 0x5 0xc000114145 0x5 0xc00011414c 0x4] 199 | ``` 200 | 201 | #### (Go) Find Function 202 | ``` 203 | (lldb) go find-func main.main 204 | 0x55c6894c0280 - main.main (file address = 0x4c0280) 205 | (lldb) go find-func 0x55c689454a3a 206 | 0x55c689454a20 - runtime.(*moduledata).textAddr (file address = 0x454a20) 207 | ``` 208 | 209 | #### (Go) Get Type 210 | ``` 211 | (lldb) go get-type json.mapEncoder --depth 3 212 | json.mapEncoder = struct { elemEnc func(*json.encodeState, struct { typ_ *abi.Type; ptr unsafe.Pointer; flag uintptr }, struct { quoted bool; escapeHTML bool }) } 213 | Size in bytes: 0x8 214 | (lldb) go get-type json.encodeState --depth 1 215 | json.encodeState = struct { Buffer bytes.Buffer; ptrLevel uint; ptrSeen map[interface {}]struct {} } 216 | Size in bytes: 0x38 217 | ``` 218 | 219 | ### Breakpoint hook 220 | This is automatic and prints all the currently implemented information at a break point. 221 | 222 | #### Address Rebasing 223 | Configurable with the `rebase_addresses` setting the address rebasing feature performs a lookup for each code address presented in the output to display the associated binary and relative address. This relative address is offset by the value defined in setting `rebase_offset` which defaults to the Ghidra base address of `0x100000`. The result is an address output that can be easily copied and pasted into an IDE "Go To Address" feature without having to do the maths to convert from the runtime address. 224 | 225 | Rebased addresses are shown in brackets after the runtime address: 226 | ![rebase address feature](assets/rebase-feature.png) 227 | 228 | ## 👷‍♂️ Troubleshooting LLDB Python support 229 | LLDB comes bundled with python modules that are required for LLEF to run. If on launching LLDB with LLEF you encounter `ModuleNotFoundError` messages it is likely you will need to manually add the LLDB python modules on your python path. 230 | 231 | To do this run the following to establish your site-packages location: 232 | 233 | ```bash 234 | python3 -m site --user-site 235 | ``` 236 | 237 | Then locate the LLDB python modules location. This is typically at a location such as `/usr/lib/llvm-15/lib/python3.10/dist-packages` but depends on your python version. 238 | 239 | Finally, modify and execute the following to add the above LLDB module path into a new file `lldb.pth` in the site-packages location discovered above. 240 | 241 | ```bash 242 | echo "/usr/lib/llvm-15/lib/python3.10/dist-packages" > ~/.local/lib/python3.10/site-packages/lldb.pth 243 | ``` 244 | 245 | ## Performance Optimisations 246 | 247 | Rendering LLEF output at each breakpoint has been observed to be slow on some platforms. The root cause of this has been traced to the underlying `GetMemoryRegions` LLDB API call. Fortunately, this is only used to identify to whether register values point to code, stack or heap addresses. 248 | 249 | To disable register coloring, and potentially significantly improve LLEF performance, disable the `register_coloring` feature using the following `llefsettings` command. 250 | 251 | ``` 252 | llefsettings set register_coloring False 253 | ``` 254 | 255 | 256 | ## 👏 Thanks 257 | We’re obviously standing on the shoulders of giants here - we’d like to credit [hugsy](https://twitter.com/_hugsy_) for [GEF](https://github.com/hugsy/gef) in particular, from which this tool draws *heavy* inspiration! Please consider this imitation as flattery 🙂 258 | 259 | If you'd like to read a bit more about LLEF you could visit our [launch blog post](https://foundryzero.co.uk/2023/07/13/llef.html). 260 | -------------------------------------------------------------------------------- /common/golang/type_getter.py: -------------------------------------------------------------------------------- 1 | """A utility class that parses type names into corresponding structures.""" 2 | 3 | import math 4 | from dataclasses import dataclass 5 | from typing import Union 6 | 7 | from common.golang.types import ( 8 | GoType, 9 | GoTypeArray, 10 | GoTypeBool, 11 | GoTypeComplex64, 12 | GoTypeComplex128, 13 | GoTypeFloat32, 14 | GoTypeFloat64, 15 | GoTypeInt, 16 | GoTypeInt8, 17 | GoTypeInt16, 18 | GoTypeInt32, 19 | GoTypeInt64, 20 | GoTypeMap, 21 | GoTypePointer, 22 | GoTypeSlice, 23 | GoTypeString, 24 | GoTypeStruct, 25 | GoTypeStructField, 26 | GoTypeUint, 27 | GoTypeUint8, 28 | GoTypeUint16, 29 | GoTypeUint32, 30 | GoTypeUint64, 31 | GoTypeUintptr, 32 | GoTypeUnsafePointer, 33 | TypeHeader, 34 | ) 35 | from common.state import LLEFState 36 | 37 | 38 | @dataclass(frozen=True) 39 | class SimpleType: 40 | """ 41 | SimpleType represents information about a unit type, such as int/bool/unsafe.Pointer. 42 | """ 43 | 44 | go_type: type[GoType] 45 | size: int 46 | alignment: int 47 | 48 | 49 | class TypeGetter: 50 | """ 51 | TypeGetter is a parser that turns type names into the corresponding structures. It's used when a user-provided type 52 | does not exactly match any of those present in the runtime, so we attempt to construct it from scratch. 53 | """ 54 | 55 | __version: tuple[int, int] 56 | __ptr_size: int 57 | __name_to_type: dict[str, GoType] 58 | 59 | __simple_map: dict[str, SimpleType] 60 | 61 | def __slice_to_type(self, slice_repr: str) -> Union[GoTypeSlice, None]: 62 | """ 63 | Parses the string representation of a Go slice type. The string must start with "[]". 64 | 65 | :param slice_repr str: The string representation of a Go slice type. 66 | :return Union[GoTypeSlice, None]: Returns a GoTypeSlice if the provided string is valid, otherwise None. 67 | """ 68 | resolved: Union[GoTypeSlice, None] = None 69 | elem_type = self.string_to_type(slice_repr[2:]) 70 | if elem_type is not None: 71 | header = TypeHeader() 72 | header.align = self.__ptr_size 73 | 74 | # Slices store three ints: base address, length, capacity. 75 | header.size = 3 * self.__ptr_size 76 | resolved = GoTypeSlice(header=header, version=self.__version) 77 | resolved.child_type = elem_type 78 | 79 | return resolved 80 | 81 | def __array_to_type(self, array_repr: str) -> Union[GoTypeArray, None]: 82 | """ 83 | Parses the string representation of a Go array type. The string must start with "[N]", where N is a number. 84 | 85 | :param array_repr str: The string representation of a Go array type. 86 | :return Union[GoTypeArray, None]: Returns a GoTypeArray if the provided string is valid, otherwise None. 87 | """ 88 | resolved: Union[GoTypeArray, None] = None 89 | partitioned = array_repr[1:].split("]", maxsplit=1) 90 | if len(partitioned) == 2: 91 | [length_string, elem_string] = partitioned 92 | 93 | valid_length = True 94 | length = 0 95 | try: 96 | length = int(length_string, base=0) 97 | except ValueError: 98 | valid_length = False 99 | 100 | if valid_length: 101 | elem_type = self.string_to_type(elem_string) 102 | if elem_type is not None: 103 | header = TypeHeader() 104 | header.align = elem_type.header.align 105 | rounded_size = ((elem_type.header.size + header.align - 1) // header.align) * header.align 106 | header.size = length * rounded_size 107 | resolved = GoTypeArray(header=header, version=self.__version) 108 | resolved.length = length 109 | resolved.child_type = elem_type 110 | return resolved 111 | 112 | def __pointer_to_type(self, pointer_repr: str) -> Union[GoTypePointer, None]: 113 | """ 114 | Parses the string representation of a Go pointer type. The string must start with "*". 115 | 116 | :param pointer_repr str: The string representation of a Go pointer type. 117 | :return Union[GoTypePointer, None]: Returns a GoTypePointer if the provided string is valid, otherwise None. 118 | """ 119 | resolved: Union[GoTypePointer, None] = None 120 | deref_type = self.string_to_type(pointer_repr[1:]) 121 | if deref_type is not None: 122 | header = TypeHeader() 123 | header.align = self.__ptr_size 124 | header.size = self.__ptr_size 125 | resolved = GoTypePointer(header=header, version=self.__version) 126 | resolved.child_type = deref_type 127 | return resolved 128 | 129 | def __struct_to_type(self, struct_repr: str) -> Union[GoTypeStruct, None]: 130 | """ 131 | Parses the string representation of a Go struct type. The string must start with "struct". 132 | 133 | :param struct_repr str: The string representation of a Go struct type. 134 | :return Union[GoTypeStruct, None]: Returns a GoTypeStruct if the provided string is valid, otherwise None. 135 | """ 136 | resolved: Union[GoTypeStruct, None] = None 137 | body = struct_repr[6:].strip() 138 | if body.startswith("{") and body.endswith("}"): 139 | body = body[1:-1].strip() 140 | 141 | valid = True 142 | field_list: list[GoTypeStructField] = [] 143 | 144 | # Track level of {} nestedness. 145 | level = 0 146 | field_string = "" 147 | fields = [] 148 | for char in body: 149 | if level == 0 and char == ";": 150 | fields.append(field_string) 151 | field_string = "" 152 | else: 153 | field_string += char 154 | if char == "{": 155 | level += 1 156 | elif char == "}": 157 | level -= 1 158 | if len(field_string) > 0: 159 | fields.append(field_string) 160 | 161 | offset = 0 162 | alignment = 1 163 | for field in fields: 164 | partitioned = field.strip().split(" ", maxsplit=1) 165 | if len(partitioned) == 2: 166 | [name, field_string] = partitioned 167 | field_type = self.string_to_type(field_string) 168 | if field_type is not None: 169 | alignment = math.lcm(alignment, field_type.header.align) 170 | # pad until the field can live in the struct 171 | while offset % field_type.header.align != 0: 172 | offset += 1 173 | 174 | struct_field = GoTypeStructField(offset=offset, name=name, type_addr=0) 175 | offset += field_type.header.size 176 | struct_field.type = field_type 177 | field_list.append(struct_field) 178 | 179 | else: 180 | valid = False 181 | break 182 | else: 183 | valid = False 184 | break 185 | 186 | if valid: 187 | header = TypeHeader() 188 | header.align = alignment 189 | header.size = offset 190 | resolved = GoTypeStruct(header=header, version=self.__version) 191 | resolved.fields = field_list 192 | return resolved 193 | 194 | def __map_to_type(self, map_repr: str) -> Union[GoTypeMap, None]: 195 | """ 196 | Parses the string representation of a Go map type. The string must start with "map[". 197 | 198 | :param map_repr str: The string representation of a Go map type. 199 | :return Union[GoTypeMap, None]: Returns a GoTypeMap if the provided string is valid, otherwise None. 200 | """ 201 | resolved: Union[GoTypeMap, None] = None 202 | body = map_repr[4:] 203 | # Track level of [] nestedness. 204 | level = 1 205 | 206 | i = 0 207 | while i < len(body): 208 | if body[i] == "[": 209 | level += 1 210 | elif body[i] == "]": 211 | level -= 1 212 | if level == 0: 213 | break 214 | i += 1 215 | 216 | if i < len(body) - 1: 217 | key_string = body[:i] 218 | val_string = body[i + 1 :] 219 | key_type = self.string_to_type(key_string) 220 | val_type = self.string_to_type(val_string) 221 | if key_type is not None and val_type is not None: 222 | header = TypeHeader() 223 | header.align = self.__ptr_size 224 | header.size = self.__ptr_size 225 | resolved = GoTypeMap(header=header, version=self.__version) 226 | resolved.key_type = key_type 227 | resolved.child_type = val_type 228 | 229 | (go_min_version, _) = self.__version 230 | if go_min_version < 24: 231 | # Old map type. 232 | bucket_str = ( 233 | f"struct {{ topbits [8]uint8; keys [8]{key_string}; " 234 | f"elems [8]{val_string}; overflow uintptr }}" 235 | ) 236 | else: 237 | # New (Swiss) map type. 238 | bucket_str = f"struct {{ ctrl uint64; slots [8]struct {{ key {key_string}; elem {val_string} }} }}" 239 | resolved.bucket_type = self.string_to_type(bucket_str) 240 | return resolved 241 | 242 | def __construct_from_simple(self, simple_type: SimpleType) -> GoType: 243 | """ 244 | Converts an entry in the simple map into an actual GoType. 245 | 246 | :param SimpleType simple_triple: The type, size and alignment. 247 | :return GoType: A GoType with the provided characteristics. 248 | """ 249 | header = TypeHeader() 250 | header.align = simple_type.alignment 251 | header.size = simple_type.size 252 | return simple_type.go_type(header=header, version=self.__version) 253 | 254 | def string_to_type(self, type_repr: str) -> Union[GoType, None]: 255 | """ 256 | Parses the string representation of any Go type. 257 | This is not a fully-compliant parser: for example, do not use characters such as "{}[];" in struct field names. 258 | 259 | :param type_repr str: The string representation of a Go type. 260 | :return Union[GoType, None]: Returns a GoType object if the provided string is valid, otherwise None. 261 | """ 262 | 263 | resolved: Union[GoType, None] = None 264 | 265 | if LLEFState.go_state.moduledata_info is not None: 266 | # First check if easily available from the binary: 267 | resolved = self.__name_to_type.get(type_repr) 268 | if resolved is None: 269 | # If not, parse it ourselves. 270 | 271 | simple_triple = self.__simple_map.get(type_repr) 272 | if simple_triple is not None: 273 | # Simple data types. 274 | resolved = self.__construct_from_simple(simple_triple) 275 | 276 | else: 277 | # Complex data types. 278 | if type_repr.startswith("[]"): 279 | resolved = self.__slice_to_type(type_repr) 280 | 281 | elif type_repr.startswith("["): 282 | resolved = self.__array_to_type(type_repr) 283 | 284 | elif type_repr.startswith("*"): 285 | resolved = self.__pointer_to_type(type_repr) 286 | 287 | elif type_repr.startswith("struct"): 288 | resolved = self.__struct_to_type(type_repr) 289 | 290 | elif type_repr.startswith("map["): 291 | resolved = self.__map_to_type(type_repr) 292 | 293 | elif type_repr.startswith("func") or type_repr.startswith("chan"): 294 | # We don't unpack these types, so just leave them as raw pointers. 295 | resolved = self.__construct_from_simple(self.__simple_map["uintptr"]) 296 | 297 | elif type_repr.startswith("interface"): 298 | pass 299 | 300 | return resolved 301 | 302 | def __init__(self, type_structs: dict[int, GoType]) -> None: 303 | """ 304 | Set up internal state, such as the map from type names to types. 305 | 306 | :param dict[int, GoType] type_structs: The type_structs object from moduledata_info. 307 | """ 308 | self.__version = LLEFState.go_state.pclntab_info.version_bounds 309 | self.__ptr_size = LLEFState.go_state.pclntab_info.ptr_size 310 | self.__name_to_type: dict[str, GoType] = {} 311 | for go_type in type_structs.values(): 312 | self.__name_to_type[go_type.header.name] = go_type 313 | 314 | self.__simple_map = { 315 | "bool": SimpleType(go_type=GoTypeBool, size=1, alignment=1), 316 | "complex64": SimpleType(go_type=GoTypeComplex64, size=8, alignment=4), 317 | "complex128": SimpleType(go_type=GoTypeComplex128, size=16, alignment=8), 318 | "float32": SimpleType(go_type=GoTypeFloat32, size=4, alignment=4), 319 | "float64": SimpleType(go_type=GoTypeFloat64, size=8, alignment=8), 320 | "int": SimpleType(go_type=GoTypeInt, size=self.__ptr_size, alignment=self.__ptr_size), 321 | "int8": SimpleType(go_type=GoTypeInt8, size=1, alignment=1), 322 | "int16": SimpleType(go_type=GoTypeInt16, size=2, alignment=2), 323 | "int32": SimpleType(go_type=GoTypeInt32, size=4, alignment=4), 324 | "int64": SimpleType(go_type=GoTypeInt64, size=8, alignment=8), 325 | "uint": SimpleType(go_type=GoTypeUint, size=self.__ptr_size, alignment=self.__ptr_size), 326 | "uint8": SimpleType(go_type=GoTypeUint8, size=1, alignment=1), 327 | "uint16": SimpleType(go_type=GoTypeUint16, size=2, alignment=2), 328 | "uint32": SimpleType(go_type=GoTypeUint32, size=4, alignment=4), 329 | "uint64": SimpleType(go_type=GoTypeUint64, size=8, alignment=8), 330 | "string": SimpleType(go_type=GoTypeString, size=self.__ptr_size * 2, alignment=self.__ptr_size), 331 | "uintptr": SimpleType(go_type=GoTypeUintptr, size=self.__ptr_size, alignment=self.__ptr_size), 332 | "unsafe.Pointer": SimpleType(go_type=GoTypeUnsafePointer, size=self.__ptr_size, alignment=self.__ptr_size), 333 | } 334 | --------------------------------------------------------------------------------