You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pwndbg/pwndbg/commands/cymbol.py

388 lines
13 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
Add, load, show, edit, or delete symbols for custom structures.
For the generation of the symbols g++/gcc is being used under the hood.
In case of remote debugging a binary which is not native to your architecture it
is advised to configure the 'gcc-config-path' config parameter to your own cross-platform
gnu gcc compiled toolchain for your target architecture.
You are advised to configure the 'cymbol-editor' config parameter to the path of your
favorite text editor. Otherwise cymbol expands $EDITOR and $VISUAL environment variables
to find the path to the default text editor.
"""
from __future__ import annotations
import argparse
import functools
import os
import subprocess
import sys
import tempfile
from typing import Dict
from typing import TypeVar
import gdb
from typing_extensions import ParamSpec
from typing_extensions import Protocol
import pwndbg
import pwndbg.aglib.arch
import pwndbg.commands
import pwndbg.lib.config
import pwndbg.lib.tempfile
import pwndbg.lib.zig
from pwndbg.color import message
from pwndbg.commands import CommandCategory
P = ParamSpec("P")
T = TypeVar("T")
gcc_compiler_path = pwndbg.config.add_param(
"gcc-compiler-path",
"",
"path to the gcc/g++ toolchain for generating imported symbols",
param_class=pwndbg.lib.config.PARAM_OPTIONAL_FILENAME,
)
cymbol_editor = pwndbg.config.add_param(
"cymbol-editor",
"",
"path to the editor for editing custom structures",
param_class=pwndbg.lib.config.PARAM_OPTIONAL_FILENAME,
)
# Remeber loaded symbols. This would be useful for 'remove-symbol-file'.
loaded_symbols: Dict[str, str] = {}
# Where generated symbol source files are saved.
pwndbg_cachedir = pwndbg.lib.tempfile.cachedir("custom-symbols")
def create_temp_header_file(content: str) -> str:
"""Create a temporary header file with the given content."""
with tempfile.NamedTemporaryFile(delete=False, suffix=".h") as tmp_file:
tmp_file.write(content.encode())
return tmp_file.name
def unload_loaded_symbol(custom_structure_name: str) -> None:
custom_structure_symbols_file = loaded_symbols.get(custom_structure_name)
if custom_structure_symbols_file is not None:
gdb.execute(f"remove-symbol-file {custom_structure_symbols_file}")
loaded_symbols.pop(custom_structure_name)
class _OnlyWhenStructFileExists(Protocol):
def __call__(self, custom_structure_name: str, custom_structure_path: str = "") -> T | None: ...
def OnlyWhenStructFileExists(func: _OnlyWhenStructFileExists) -> _OnlyWhenStructFileExists:
@functools.wraps(func)
def wrapper(custom_structure_name: str, custom_structure_path: str = "") -> T | None:
pwndbg_custom_structure_path = (
custom_structure_path or os.path.join(pwndbg_cachedir, custom_structure_name) + ".c"
)
if not os.path.exists(pwndbg_custom_structure_path):
print(message.error("No custom structure was found with the given name!"))
return None
return func(custom_structure_name, pwndbg_custom_structure_path)
return wrapper
def compile_with_flags(gcc_extra_flags):
if gcc_compiler_path != "":
compiler_flags = [gcc_compiler_path]
else:
try:
compiler_flags = pwndbg.lib.zig.flags(pwndbg.aglib.arch)
except ValueError as exception:
print(message.error(exception))
return False
gcc_cmd = compiler_flags + gcc_extra_flags
try:
subprocess.run(gcc_cmd, check=True, text=True)
return True
except subprocess.CalledProcessError as exception:
print(message.error(exception))
print(
message.error(
f"Failed to compile {gcc_extra_flags[0]}. Please fix any compilation errors there may be."
)
)
except Exception as exception:
print(message.error(exception))
print(message.error("An error occured while generating the debug symbols."))
return False
def generate_debug_symbols(
custom_structure_path: str, pwndbg_debug_symbols_output_file: str | None = None
) -> str | None:
if not pwndbg_debug_symbols_output_file:
_, pwndbg_debug_symbols_output_file = tempfile.mkstemp(prefix="custom-", suffix=".dbg")
# -fno-eliminate-unused-debug-types is a handy gcc flag that lets us extract debug symbols from non-used defined structures.
gcc_extra_flags = [
custom_structure_path,
"-c",
"-g",
"-fno-eliminate-unused-debug-types",
"-o",
pwndbg_debug_symbols_output_file,
]
if not compile_with_flags(gcc_extra_flags):
return None
return pwndbg_debug_symbols_output_file
def create_blank_elf():
try:
import lief
except ImportError:
print(
message.error(
"lief python package is not installed (this will be auto-installed with next version of Pwndbg; for now, you can install it manually in the Pwndbg venv)."
)
)
return None
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".S")
tmp.write(b".global _start\n_start:\nnop")
tmp.flush()
_, output_path = tempfile.mkstemp(prefix="blank-", suffix=".dbg")
gcc_extra_flags = [
tmp.name,
"-nostdlib",
"--static",
"-o",
output_path,
]
if not compile_with_flags(gcc_extra_flags):
return None
blank_elf = lief.ELF.parse(output_path)
for s in blank_elf.symbols:
blank_elf.remove_symtab_symbol(s)
blank_elf.write(output_path)
return output_path
def add_custom_structure(custom_structure_name: str, force=False):
pwndbg_custom_structure_path = os.path.join(pwndbg_cachedir, custom_structure_name) + ".c"
if os.path.exists(pwndbg_custom_structure_path) and not force:
option = input(
message.notice(
"A custom structure was found with the given name, would you like to overwrite it? [y/N] "
)
)
if option != "y":
return
print(
message.notice("Enter your custom structure in a C header style, press Ctrl+D to save:\n")
)
custom_structures_source = sys.stdin.read().strip()
if custom_structures_source == "":
print(message.notice("An empty structure is entered, skipping ..."))
return
with open(pwndbg_custom_structure_path, "w") as f:
f.write(custom_structures_source)
# Avoid checking for file existance. Call the decorator wrapper directly.
load_custom_structure.__wrapped__(custom_structure_name, pwndbg_custom_structure_path)
def add_structure_from_header(
header_file: str, custom_structure_name: str = None, force: bool = False
) -> None:
custom_structure_name = (
custom_structure_name.strip()
if custom_structure_name
else os.path.splitext(os.path.basename(header_file))[0]
)
if not custom_structure_name:
print(message.error("Invalid structure name provided or generated."))
return
pwndbg_custom_structure_path = os.path.join(pwndbg_cachedir, custom_structure_name) + ".c"
if os.path.exists(pwndbg_custom_structure_path):
if not force:
option = input(
message.notice(
f"Structure '{custom_structure_name}' already exists. Overwrite? [y/N] "
)
)
if option.lower() != "y":
print(message.notice("Aborted by user."))
return
try:
with open(header_file, "r") as src, open(pwndbg_custom_structure_path, "w") as f:
content = src.read().strip()
if not content:
print(message.notice("Header file is empty, skipping..."))
return
f.write(content)
except (IOError, OSError) as e:
print(message.error(f"Failed to process header file: {e}"))
return
load_custom_structure.__wrapped__(custom_structure_name, pwndbg_custom_structure_path)
@OnlyWhenStructFileExists
def edit_custom_structure(custom_structure_name: str, custom_structure_path: str = "") -> None:
# Lookup an editor to use for editing the custom structure.
editor_preference = os.getenv("EDITOR")
if not editor_preference:
editor_preference = os.getenv("VISUAL")
if not editor_preference:
editor_preference = "vi"
if cymbol_editor != "":
editor_preference = cymbol_editor
try:
subprocess.run(
[editor_preference, custom_structure_path],
check=True,
)
except Exception:
print(message.error("An error occured during opening the source file."))
print(message.error(f"Path to the custom structure: {custom_structure_path}"))
print(message.error("Please try to manually edit the structure."))
print(
message.error(
'\nTry to set a path to an editor with:\n\tset "cymbol-editor" /usr/bin/nano'
)
)
return
input(message.notice("Press enter when finished editing."))
load_custom_structure(custom_structure_name)
@OnlyWhenStructFileExists
def remove_custom_structure(custom_structure_name: str, custom_structure_path: str = "") -> None:
unload_loaded_symbol(custom_structure_name)
os.remove(custom_structure_path)
print(message.success("Symbols are removed!"))
@OnlyWhenStructFileExists
def load_custom_structure(custom_structure_name: str, custom_structure_path: str = "") -> None:
unload_loaded_symbol(custom_structure_name)
pwndbg_debug_symbols_output_file = generate_debug_symbols(custom_structure_path)
if not pwndbg_debug_symbols_output_file:
return # generate_debug_symbols prints on failures
pwndbg.dbg.selected_inferior().add_symbol_file(pwndbg_debug_symbols_output_file)
loaded_symbols[custom_structure_name] = pwndbg_debug_symbols_output_file
print(message.success(f"Loaded custom symbols! (from {custom_structure_path})"))
@OnlyWhenStructFileExists
def show_custom_structure(custom_structure_name: str, custom_structure_path: str = "") -> None:
# Call non-caching version of the function (thus .__wrapped__)
highlighted_source = pwndbg.pwndbg.commands.context.get_highlight_source.__wrapped__(
custom_structure_path
)
print("\n".join(highlighted_source))
parser = argparse.ArgumentParser(
description="Manage custom C structures in pwndbg. Supports project-specific auto-loading from .gdbinit."
)
subparsers = parser.add_subparsers(dest="subcommand", help="Available subcommands")
add_parser = subparsers.add_parser("add", help="Add a custom structure")
add_parser.add_argument("name", help="Name of custom structure", type=str)
add_parser.add_argument(
"--force", action="store_true", help="Overwrite if structure already exists"
)
remove_parser = subparsers.add_parser("remove", help="Remove a custom structure")
remove_parser.add_argument("name", help="Name of custom structure")
edit_parser = subparsers.add_parser("edit", help="Edit a custom structure")
edit_parser.add_argument("name", help="Name of custom structure", type=str)
load_parser = subparsers.add_parser("load", help="Load a custom structure")
load_parser.add_argument("name", help="Name of custom structure", type=str)
show_parser = subparsers.add_parser("show", help="Show a custom structure")
show_parser.add_argument("name", help="Name of custom structure", type=str)
file_parser = subparsers.add_parser("file", help="Add a structure from a header file")
file_parser.add_argument("path", help="Path to header file", type=str)
file_parser.add_argument("--name", help="Optional structure name", type=str)
file_parser.add_argument("--force", action="store_true", help="Overwrite if exists")
show_all_parser = subparsers.add_parser("show-all", help="Show all stored structure")
@pwndbg.commands.Command(
parser,
category=CommandCategory.MISC,
notes="""
The `cymbol` command loads custom C structs and symbols into the debugger using GCC under the hood.
Usage Example:
`cymbol file --force ./structs.h`
--force:
Use this flag to force symbol reloading, even if symbols with the same name already exist.
Warning:
If a loaded structure defines a symbol that already exists, the debugger may prefer the original
symbol or behave unexpectedly. Its recommended to use unique struct names to avoid
symbol conflicts.
Tip:
You can add this command to your `.gdbinit` file for automatic loading:
`cymbol file --force ./path/to/structs.h`
""",
)
def cymbol(
subcommand: str = None,
name: str = None,
path: str = None,
force=False,
):
match subcommand:
case "add":
add_custom_structure(name, force=force)
case "remove":
remove_custom_structure(name)
case "edit":
edit_custom_structure(name)
case "load":
load_custom_structure(name)
case "file":
add_structure_from_header(path, name, force=force)
case "show":
show_custom_structure(name)
case "show-all":
print(message.notice("Available custom structure names:\n"))
for file in os.listdir(pwndbg_cachedir):
if file.endswith(".c"):
name = os.path.splitext(file)[0]
print(f" - {name}")
case _:
parser.print_help()