├── dbgtools ├── commands │ ├── __init__.py │ ├── asanviz.py │ ├── asanok.py │ ├── breaknew.py │ ├── commands.py │ ├── dofree.py │ ├── gadgetsearch.py │ ├── domalloc.py │ ├── libcbase.py │ ├── tlsptrmangle.py │ ├── utils.py │ ├── heapbase.py │ ├── breakpie.py │ ├── v8heap.py │ ├── pwndump.py │ ├── getoffsets.py │ ├── traceheap.py │ ├── tracer.py │ └── heaplookup.py ├── utils.py ├── __init__.py ├── bits.py ├── v8.py ├── asan.py ├── random.py ├── tls.py ├── gdbapi.py ├── logger.py ├── breakpoints.py ├── memory.py ├── functions.py ├── main.py ├── regs.py └── types.py ├── requirements.txt ├── gdbinit.py ├── examples ├── programs │ ├── asan.c │ ├── basic.c │ ├── traceheap.c │ ├── Makefile │ └── types.c ├── simple_trace.py ├── basic_test.py ├── asan.py ├── traceheap.py └── types.py ├── .gitignore ├── README.md └── LICENSE /dbgtools/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from dbgtools.commands.commands import * 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pwntools 2 | pwndbg @ git+https://github.com/pwndbg/pwndbg.git@dev 3 | -------------------------------------------------------------------------------- /gdbinit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # append the folder of this file to the search path 3 | sys.path.append(__file__.rsplit('/', 1)[0]) 4 | 5 | import dbgtools 6 | -------------------------------------------------------------------------------- /examples/programs/asan.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | void vuln(void) { 5 | char buf[0xff] = { 0 }; 6 | buf[0x10] = 0x41; 7 | 8 | buf[0xff + 0x10] = 0x69; 9 | } 10 | 11 | int main(void) { 12 | vuln(); 13 | } 14 | -------------------------------------------------------------------------------- /examples/programs/basic.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | int a = 42; 6 | int b = 0; 7 | b += a; 8 | printf("%d\n", b); 9 | char v; 10 | read(0, &v, 1); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # created by virtualenv automatically 2 | bin/ 3 | lib/ 4 | pwntools-doc/ 5 | pyvenv.cfg 6 | examples/gdb_script.log 7 | examples/trace_example.py 8 | **/.gdb_history 9 | **/gdb_script.log 10 | .gdb_history 11 | **/__pycache__ 12 | -------------------------------------------------------------------------------- /dbgtools/utils.py: -------------------------------------------------------------------------------- 1 | def singleton(class_): 2 | instances = {} 3 | def getinstance(*args, **kwargs): 4 | if class_ not in instances: 5 | instances[class_] = class_(*args, **kwargs) 6 | return instances[class_] 7 | return getinstance 8 | -------------------------------------------------------------------------------- /examples/simple_trace.py: -------------------------------------------------------------------------------- 1 | import dbgtools 2 | from dbgtools.logger import Logger 3 | 4 | 5 | def log() -> bytes: 6 | return b'hit log breakpoint' 7 | 8 | 9 | dbgtools.LogBreakpoint('*(main+12)', log) 10 | logger = Logger() 11 | logger.print_log() 12 | dbgtools.gdbapi.run() 13 | -------------------------------------------------------------------------------- /examples/basic_test.py: -------------------------------------------------------------------------------- 1 | import pwn 2 | import os 3 | 4 | 5 | # folder this file is in 6 | folder = '/'.join(__file__.split('/')[:-1]) + '/' 7 | p = pwn.gdb.debug(os.path.join(folder, 'bin/basic'), api=True, env=dict()) 8 | api = p.gdb 9 | print(api.sys.modules['dbgtools']) 10 | p.sendline(b'A') 11 | -------------------------------------------------------------------------------- /examples/programs/traceheap.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | 5 | int main(void) { 6 | void *p1 = malloc(0x80); 7 | void *p2 = malloc(0x20); 8 | 9 | free(p1); 10 | 11 | void *p3 = malloc(0x1000); 12 | 13 | free(p2); 14 | free(p3); 15 | 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /examples/asan.py: -------------------------------------------------------------------------------- 1 | import dbgtools 2 | from dbgtools.regs import * 3 | 4 | 5 | dbgtools.CustomBreakpoint('*(vuln+349)', explicit_stop=True) 6 | dbgtools.gdbapi.run() 7 | 8 | assert dbgtools.asan.access_ok(registers.rax) 9 | assert not dbgtools.asan.access_ok(registers.rax + 0x100) 10 | 11 | dbgtools.asan.visualize_region(registers.rax + 0xd0) 12 | -------------------------------------------------------------------------------- /examples/programs/Makefile: -------------------------------------------------------------------------------- 1 | # written compiler specific to match offsets 2 | # do not reexecute this as different versions might 3 | # change offsets 4 | 5 | 6 | all: basic types asan traceheap 7 | 8 | basic: 9 | gcc -o ../bin/basic basic.c 10 | 11 | types: 12 | gcc -o ../bin/types types.c 13 | 14 | asan: 15 | clang -o ../bin/asan -fsanitize=address -Wno-array-bounds asan.c 16 | 17 | traceheap: 18 | gcc -o ../bin/traceheap traceheap.c 19 | -------------------------------------------------------------------------------- /dbgtools/commands/asanviz.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pwndbg 3 | import pwndbg.commands 4 | from dbgtools.asan import visualize_region 5 | 6 | 7 | parser = argparse.ArgumentParser(description="Visualize asan redzones") 8 | parser.add_argument("ptr", type=int, help="ptr to check surronding region") 9 | 10 | @pwndbg.gdblib.proc.OnlyWhenRunning 11 | @pwndbg.commands.ArgparsedCommand(parser) 12 | def asanviz(ptr: int): 13 | visualize_region(ptr) 14 | -------------------------------------------------------------------------------- /dbgtools/__init__.py: -------------------------------------------------------------------------------- 1 | from dbgtools.main import * 2 | from dbgtools.breakpoints import * 3 | from dbgtools import logger 4 | from dbgtools import gdbapi 5 | from dbgtools.commands import * 6 | from dbgtools import v8 7 | from dbgtools import asan 8 | from dbgtools import types 9 | from dbgtools import memory 10 | from dbgtools import regs 11 | from dbgtools import functions 12 | from dbgtools import utils 13 | from dbgtools import bits 14 | from dbgtools import tls 15 | -------------------------------------------------------------------------------- /dbgtools/bits.py: -------------------------------------------------------------------------------- 1 | def rol(val: int, r_bits: int, max_bits: int) -> int: 2 | return (val << r_bits % max_bits) & (2**max_bits - 1) | \ 3 | ((val & (2**max_bits - 1)) >> (max_bits - (r_bits % max_bits))) 4 | 5 | def ror(val: int, r_bits: int, max_bits: int) -> int: \ 6 | return ((val & (2**max_bits - 1)) >> r_bits % max_bits) | \ 7 | (val << (max_bits - (r_bits % max_bits)) & (2**max_bits - 1)) 8 | 9 | def rol64(val: int, r_bits: int): 10 | return rol(val, r_bits, 64) 11 | 12 | def ror64(val: int, r_bits: int): 13 | return ror(val, r_bits, 64) 14 | -------------------------------------------------------------------------------- /dbgtools/commands/asanok.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from dbgtools.asan import access_ok 5 | 6 | 7 | parser = argparse.ArgumentParser(description="Check if ptr is in ASAN redzone") 8 | parser.add_argument("ptr", type=int, help="ptr to check") 9 | 10 | @pwndbg.gdblib.proc.OnlyWhenRunning 11 | @pwndbg.commands.ArgparsedCommand(parser) 12 | def asanok(ptr: int): 13 | aok = access_ok(ptr) 14 | if aok: 15 | print(f"Writing to {hex(ptr)} allowed") 16 | else: 17 | print(f"Writing to {hex(ptr)} forbidden") 18 | -------------------------------------------------------------------------------- /dbgtools/commands/breaknew.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from dbgtools.gdbapi import execute_command, delete_all_breakpoints 5 | 6 | 7 | parser = argparse.ArgumentParser(description="Creates a breakpoint and delete all previous ones") 8 | parser.add_argument("bp", type=str, help="breakpoint string") 9 | 10 | @pwndbg.commands.ArgparsedCommand(parser) 11 | def bnew(bp: str): 12 | delete_all_breakpoints() 13 | # TODO(ju256): refactor for general bp interface that 14 | # can handle *bp as well 15 | execute_command(f"b {bp}") 16 | -------------------------------------------------------------------------------- /dbgtools/commands/commands.py: -------------------------------------------------------------------------------- 1 | import dbgtools.commands.domalloc 2 | import dbgtools.commands.dofree 3 | import dbgtools.commands.heapbase 4 | import dbgtools.commands.breaknew 5 | import dbgtools.commands.breakpie 6 | import dbgtools.commands.asanok 7 | import dbgtools.commands.asanviz 8 | import dbgtools.commands.v8heap 9 | import dbgtools.commands.gadgetsearch 10 | import dbgtools.commands.libcbase 11 | import dbgtools.commands.traceheap 12 | import dbgtools.commands.tracer 13 | import dbgtools.commands.heaplookup 14 | import dbgtools.commands.pwndump 15 | import dbgtools.commands.getoffsets 16 | import dbgtools.commands.tlsptrmangle 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dbgtools 2 | 3 | dbgtools is a GDB and pwndbg extension. It aims to remove friction and repetition from binary exploitation and reverse engineering. 4 | 5 | Main use cases include: 6 | * Scripting gdb from pwntools 7 | * Flexible tracing of a whole binary execution with gdb 8 | * Various commands that make common exploitation steps quicker 9 | 10 | See `examples/` for how this and more might look like 11 | 12 | ## Installation 13 | Right now, just: 14 | * Clone the repo somewhere 15 | * Install `requirements.txt` 16 | * Add the line `source //gdbinit.py` after loading pwndbg 17 | 18 | ## Contributing 19 | 20 | We are welcoming pull requests and issues! 21 | -------------------------------------------------------------------------------- /dbgtools/commands/dofree.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pwndbg 3 | import pwndbg.commands 4 | from dbgtools.commands.utils import SupressedOutput, parse_tint 5 | from dbgtools.functions import free 6 | from dbgtools.gdbapi import execute_command 7 | 8 | 9 | parser = argparse.ArgumentParser(description="Performs free(ptr)") 10 | parser.add_argument("ptr", type=int, help="ptr to free") 11 | 12 | @pwndbg.gdblib.proc.OnlyWhenRunning 13 | @pwndbg.commands.ArgparsedCommand(parser) 14 | def dofree(): 15 | try: 16 | with SupressedOutput(): 17 | free(ptr) 18 | execute_command(f"x/4gx {hex(ptr)}") 19 | except ValueError: 20 | print("Address of free could not be found.") 21 | -------------------------------------------------------------------------------- /dbgtools/v8.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | from dbgtools.main import vmmap 3 | from dbgtools.memory import read_u64 4 | 5 | 6 | HEAP_BASE_TO_SELF_OFF = 0x20 7 | 8 | 9 | # TODO(ju256): only works in d8. expand this to properly find the v8 heap base in 10 | # renderer processes of chrome 11 | def v8heap_page(): 12 | if not gdb.current_progspace().filename.endswith("d8"): 13 | print("Failed to detect d8 binary. Output may be wrong") 14 | 15 | for page in vmmap(): 16 | if page.rw and read_u64(page.start+HEAP_BASE_TO_SELF_OFF) & (~0xffff) == page.start: 17 | return page.start 18 | 19 | return None 20 | 21 | def v8heap_start_addr(): 22 | if (page := v8heap_page()) is not None: 23 | return page.start 24 | else: 25 | return -1 26 | -------------------------------------------------------------------------------- /dbgtools/commands/gadgetsearch.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from dbgtools.memory import read_bytes 5 | from dbgtools.main import get_executable_pages, find_gadget 6 | import pwn 7 | 8 | 9 | parser = argparse.ArgumentParser(description="In memory search for (ROP) gadgets") 10 | parser.add_argument("gadget", type=str, help="Gadget string") 11 | 12 | 13 | @pwndbg.gdblib.proc.OnlyWhenRunning 14 | @pwndbg.commands.ArgparsedCommand(parser) 15 | def gadgetsearch(gadget: str): 16 | b = pwn.asm(gadget, arch="amd64", os="linux") 17 | print(f"Searching for {str(b).lstrip('b')}") 18 | for (page, offset) in find_gadget(b): 19 | ptr = page.start + offset 20 | print(f"{gadget} @ {page.objfile}+{hex(offset)} ({hex(ptr)})") 21 | -------------------------------------------------------------------------------- /dbgtools/commands/domalloc.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pwndbg 3 | import pwndbg.commands 4 | from dbgtools.commands.utils import SupressedOutput 5 | from dbgtools.functions import malloc 6 | from dbgtools.gdbapi import execute_command 7 | 8 | 9 | parser = argparse.ArgumentParser(description="Performs malloc(size)") 10 | parser.add_argument("size", type=int, help="size") 11 | 12 | @pwndbg.gdblib.proc.OnlyWhenRunning 13 | @pwndbg.commands.ArgparsedCommand(parser) 14 | def domalloc(size): 15 | try: 16 | with SupressedOutput(): 17 | malloc_chunk = malloc(size) 18 | execute_command(f"x/{(size//8)+1}gx {hex(malloc_chunk)}") 19 | print(f"malloc({hex(size)}) -> {hex(malloc_chunk)}") 20 | except ValueError: 21 | print("Address of malloc could not be found.") 22 | -------------------------------------------------------------------------------- /dbgtools/commands/libcbase.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from dbgtools.main import get_libc_base 5 | from dbgtools.commands.utils import parse_tint 6 | 7 | # TODO(liam) I belief pwndbg by now support this 8 | # if this is true and better, depricate this command 9 | 10 | parser = argparse.ArgumentParser(description="Looks up the libc base address") 11 | parser.add_argument("ptr", type=int, nargs='?', default=0, help="get offset to this pointer from libcbase") 12 | 13 | @pwndbg.gdblib.proc.OnlyWhenRunning 14 | @pwndbg.commands.ArgparsedCommand(parser) 15 | def libcbase(ptr: int = 0): 16 | libc_base = get_libc_base() 17 | if libc_base is None: 18 | print("libc base not found") 19 | elif ptr == 0: 20 | print(f"libc @ {hex(libc_base)}") 21 | else: 22 | print(f"libc @ {hex(libc_base)} | off: {hex(ptr - libc_base)}") 23 | -------------------------------------------------------------------------------- /dbgtools/commands/tlsptrmangle.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from typing import Optional 5 | from dbgtools.tls import ptr_mangling_cookie, ptr_mangle, ptr_demangle 6 | 7 | 8 | parser = argparse.ArgumentParser(description="Print tls ptr mangling cookie") 9 | parser.add_argument("ptr", type=int, nargs='?', help="ptr to mangle/demangle") 10 | parser.add_argument("--mangle", action="store_true", help="mangle ptr instead of demangling") 11 | 12 | 13 | @pwndbg.gdblib.proc.OnlyWhenRunning 14 | @pwndbg.commands.ArgparsedCommand(parser) 15 | def tlsptrmangle(ptr: Optional[int], mangle: bool = False): 16 | print(f"tls PTR_MANGLE cookie: {hex(ptr_mangling_cookie())}") 17 | if ptr is not None: 18 | if mangle: 19 | print(f"mangled ptr: {hex(ptr_mangle(ptr))}") 20 | else: 21 | print(f"demangled ptr: {hex(ptr_demangle(ptr))}") 22 | -------------------------------------------------------------------------------- /dbgtools/asan.py: -------------------------------------------------------------------------------- 1 | from dbgtools.memory import read_byte, read_u64 2 | 3 | 4 | NORMAL = "\x1b[0m" 5 | RED = "\x1b[31m" 6 | GREEN = "\x1b[32m" 7 | BLUE = "\x1b[34m" 8 | 9 | 10 | ASAN_REGION_RANGE = 0x50 11 | 12 | 13 | def access_ok(ptr): 14 | return read_byte((ptr >> 3) + 0x7fff8000) == 0 15 | 16 | def visualize_region(optr): 17 | def pad_mem(ptr): 18 | return "0x" + (hex(read_u64(p1))[2:]).rjust(16, "0") 19 | 20 | def pad_color_asan(ptr): 21 | v = pad_mem(ptr) 22 | color = GREEN if access_ok(ptr) else RED 23 | return color+v 24 | 25 | for ptr in range(optr - ASAN_REGION_RANGE, optr + ASAN_REGION_RANGE, 16): 26 | p1 = ptr 27 | p2 = ptr + 8 28 | v1 = pad_color_asan(p1) 29 | v2 = pad_color_asan(p2) 30 | 31 | if p1 == optr: 32 | pp = f"{BLUE}{hex(p1)}{NORMAL}" 33 | else: 34 | pp = hex(p1) 35 | 36 | print(f"{NORMAL}{pp}: {v1} {v2}{NORMAL}") 37 | -------------------------------------------------------------------------------- /dbgtools/random.py: -------------------------------------------------------------------------------- 1 | def get_vm_log_breakpoint_template(addresses, handler_ids = None, pie=True, print_handlers=False): 2 | if handler_ids is None: 3 | handler_ids = [] 4 | else: 5 | assert len(handler_ids) == len(addresses) 6 | 7 | def get_func_name(i): 8 | func_id = handler_ids[i] if len(handler_ids) != 0 else i 9 | return f"vm_handler_{func_id}" 10 | 11 | for i, addr in enumerate(addresses): 12 | func_id = handler_ids[i] if len(handler_ids) != 0 else i 13 | func_name_template = f'def {get_func_name(i)}\n return "OP{func_id}"\n' 14 | print(func_name_template) 15 | 16 | print() 17 | 18 | for i, addr in enumerate(addresses): 19 | if pie: 20 | bp_template = "LogBreakpoint.create_pie_bp" 21 | else: 22 | bp_template = "LogBreakpoint.create_pt_bp" 23 | 24 | print(f"{bp_template}({hex(addr)}, {get_func_name(i)})") 25 | -------------------------------------------------------------------------------- /examples/programs/types.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | int prod(int a, int b) { 8 | return a*b; 9 | } 10 | 11 | 12 | typedef struct A { 13 | unsigned int x; 14 | unsigned int y; 15 | } A; 16 | 17 | typedef struct Test { 18 | char *data; 19 | double flt; 20 | bool b; 21 | A* a; 22 | int (*func_ptr)(int, int); 23 | } Test; 24 | 25 | 26 | 27 | 28 | int main() { 29 | char buf[0x10] = "AAAAAAABBBBBBBB"; 30 | Test test; 31 | test.data = (char*) malloc(0x10); 32 | test.flt = 1.1; 33 | test.b = true; 34 | test.a = (A*)malloc(sizeof(A)); 35 | test.a->x = 0x1337; 36 | test.a->y = 0x420; 37 | 38 | test.func_ptr = prod; 39 | 40 | memcpy(test.data, buf, 0x10); 41 | 42 | printf("test @ %p\n", &test); 43 | 44 | puts(test.data); 45 | printf("%lf\n", test.flt); 46 | 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /dbgtools/commands/utils.py: -------------------------------------------------------------------------------- 1 | from dbgtools.gdbapi import execute_commands 2 | import gdb 3 | 4 | 5 | def supress_output(): 6 | execute_commands(["set logging file /dev/null", 7 | "set logging redirect on", 8 | "set logging enabled on", 9 | "set context-output /dev/null"]) 10 | 11 | 12 | def reenable_output(): 13 | execute_commands(["set logging redirect off", 14 | "set logging enabled off", 15 | "set context-output stdout"]) 16 | 17 | class SupressedOutput(): 18 | def __enter__(self): 19 | supress_output() 20 | 21 | def __exit__(self, exc_type, exc_value, exc_traceback): 22 | reenable_output() 23 | 24 | 25 | # TODO(ju256): remove when everything is argparsed 26 | def parse_tint(s): 27 | try: 28 | return int(s, 0) 29 | except ValueError: 30 | return int(gdb.parse_and_eval(s)) 31 | 32 | -------------------------------------------------------------------------------- /dbgtools/tls.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | from dbgtools.memory import read_u64 3 | from dbgtools.bits import ror64, rol64 4 | from dbgtools.types import Struct, StructField, U64Type 5 | 6 | 7 | # TODO(ju256): get to consts somewhere 8 | TLS_TO_CANARY_OFFSET = 0x28 9 | 10 | 11 | # TODO(ju256): expand on this 12 | class TLS(Struct): 13 | canary: StructField[TLS_TO_CANARY_OFFSET, U64Type] 14 | ptr_mangle_cookie: U64Type 15 | 16 | 17 | def base() -> int: 18 | return pwndbg.gdblib.tls.find_address_with_register() 19 | 20 | def tls() -> TLS: 21 | return TLS(base()) 22 | 23 | def ptr_mangling_cookie() -> int: 24 | return tls().ptr_mangling_cookie 25 | 26 | def ptr_demangle(mangled_ptr: int) -> int: 27 | cookie = ptr_mangling_cookie() 28 | return ror64(mangled_ptr, 0x11) ^ cookie 29 | 30 | def ptr_mangle(ptr: int) -> int: 31 | cookie = ptr_mangling_cookie() 32 | return rol64(ptr ^ cookie, 0x11) 33 | 34 | def canary() -> int: 35 | return tls().canary 36 | -------------------------------------------------------------------------------- /dbgtools/commands/heapbase.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from dbgtools.main import get_heap_base 5 | from typing import Optional 6 | 7 | 8 | parser = argparse.ArgumentParser(description="Looks up the base address of all existing heaps") 9 | parser.add_argument("--ptr", type=int, help="ptr to calculate difference to heap base") 10 | 11 | @pwndbg.gdblib.proc.OnlyWhenRunning 12 | @pwndbg.commands.ArgparsedCommand(parser) 13 | def heapbase(ptr: Optional[int] = None): 14 | heap_addresses = get_heap_base() 15 | if heap_addresses is None: 16 | print("no heap found") 17 | elif len(heap_addresses) == 1 and heap_addresses[0] != -1: 18 | heap_ptr = heap_addresses[0] 19 | if ptr is not None: 20 | print(f"heap @ {hex(heap_ptr)} | off: {hex(ptr - heap_ptr)}") 21 | else: 22 | print(f"heap @ {hex(heap_ptr)}") 23 | elif len(heap_addresses) >= 2: 24 | print("Found multiple heaps") 25 | for hp in heap_addresses: 26 | print(f"heap @ {hex(hp)}") 27 | -------------------------------------------------------------------------------- /examples/traceheap.py: -------------------------------------------------------------------------------- 1 | from dbgtools.logger import Logger 2 | from dbgtools.gdbapi import execute_commands 3 | from dbgtools.gdbapi import run, cont 4 | import re 5 | 6 | logger = Logger() 7 | execute_commands(["tb *main", "tb *main+92"]) 8 | 9 | run() 10 | 11 | execute_commands(["traceheap on"]) 12 | 13 | cont() 14 | 15 | execute_commands(["traceheap off"]) 16 | 17 | heap_trace_log = list(filter(lambda l: l.startswith(b"[TraceHeap] "), Logger().content.splitlines())) 18 | heap_trace_log = [l.decode().lstrip("[TraceHeap] ") for l in heap_trace_log] 19 | 20 | mallocs = [] 21 | frees = [] 22 | 23 | for l in heap_trace_log: 24 | if (m := re.match(r"malloc\((0x[a-f0-9]+)\) => (0x[a-f0-9]+)", l)): 25 | mallocs.append(m.groups()) 26 | if (m := re.match(r"free\((0x[a-f0-9]+)\)", l)): 27 | frees.append(m.groups()) 28 | 29 | malloc_sizes = [int(m[0], 16) for m in mallocs] 30 | malloc_ptrs = [int(m[1], 16) for m in mallocs] 31 | free_ptrs = [int(m[0], 16) for m in frees] 32 | 33 | assert malloc_sizes == [0x80, 0x20, 0x1000] 34 | assert set(malloc_ptrs) == set(free_ptrs) 35 | 36 | -------------------------------------------------------------------------------- /examples/types.py: -------------------------------------------------------------------------------- 1 | import dbgtools 2 | from dbgtools.types import * 3 | from dbgtools.regs import * 4 | 5 | 6 | class A(Struct): 7 | x: U32Type 8 | y: U32Type 9 | 10 | 11 | class Test(Struct): 12 | data: MutPointerType[StringType[None]] 13 | flt: DoubleType 14 | b: BoolType 15 | _: Padding[4] 16 | a: PointerType[A] 17 | func_ptr: FunctionPtr[2] 18 | 19 | 20 | # TODO: 21 | # - add array + union tests 22 | # - pointer arithmetic test 23 | # - explicit offset test 24 | 25 | 26 | 27 | def parse_struct(): 28 | t = Test(registers.rsi) 29 | print(t) 30 | 31 | assert t.data.data == b'AAAAAAABBBBBBBB\x00' 32 | assert t.data.ptr != 0 33 | assert t.flt == 1.1 34 | assert t.b == True 35 | 36 | a = t.a.data 37 | assert a.x == 0x1337 38 | assert a.y == 0x420 39 | 40 | assert t.func_ptr(0x13, 0x37) == 0x13 * 0x37 41 | 42 | t.data.data = b"XXXXYYYY\x00" 43 | t.flt = 1337.1337 44 | 45 | a.x = 1234 46 | assert a.x == 1234 47 | assert t.a.data.x == 1234 48 | 49 | 50 | 51 | dbgtools.CustomBreakpoint('*(main+169)', explicit_stop=True) 52 | dbgtools.gdbapi.run() 53 | 54 | 55 | parse_struct() 56 | -------------------------------------------------------------------------------- /dbgtools/commands/breakpie.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from dbgtools.main import force_load_pie_base, get_pie_base 5 | from dbgtools.gdbapi import set_breakpoint 6 | from dbgtools.commands.utils import parse_tint 7 | 8 | 9 | # Works before program start which is not supported by pwndbg 10 | parser = argparse.ArgumentParser(description="Creates a breakpoint relative to the current PIE base.") 11 | parser.add_argument("offset", type=int, help="offset to pie base") 12 | 13 | @pwndbg.commands.ArgparsedCommand(parser) 14 | def bpie(offset: int): 15 | piebase = get_pie_base() 16 | if piebase is None: 17 | # program not running probably 18 | print("Current PIE base could not be found.\n" + 19 | "Do you want to try and force PIE base loading (program will be executed!)") 20 | choice = input("[y/n] > ") 21 | if len(choice) >= 1 and choice[0].lower() == "y": 22 | piebase = force_load_pie_base() 23 | if piebase is None: 24 | print("Could not force load PIE base") 25 | return 26 | else: 27 | return 28 | 29 | set_breakpoint(piebase + offset) 30 | -------------------------------------------------------------------------------- /dbgtools/commands/v8heap.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pwndbg 3 | import pwndbg.commands 4 | from typing import Optional 5 | from dbgtools.v8 import v8heap_page, v8heap_start_addr 6 | from dbgtools.commands.utils import parse_tint 7 | 8 | 9 | parser = argparse.ArgumentParser(description="Show V8 heap address") 10 | parser.add_argument("--ptr", type=int, help="Calculate offset from pointer to v8 heap") 11 | parser.add_argument("--offset", type=int, help="Offset from v8 heap") 12 | 13 | 14 | @pwndbg.gdblib.proc.OnlyWhenRunning 15 | @pwndbg.commands.ArgparsedCommand(parser) 16 | def v8heap(ptr: Optional[int] = None, offset: Optional[int] = None): 17 | v8_heap_page_obj = v8heap_page() 18 | v8_heap_addr = v8heap_start_addr() 19 | if v8_heap_addr == -1: 20 | print("V8 heap not found") 21 | return 22 | else: 23 | if ptr is None and offset is None: 24 | print(f"V8 heap @ {hex(v8_heap_addr)}") 25 | elif offset is not None: 26 | print(f"V8 heap+{hex(offset)} @ {hex(v8_heap_addr+offset)}") 27 | else: 28 | if ptr >= v8_heap_page_obj.end: 29 | print("ptr does not seem to be on V8 heap") 30 | else: 31 | print(f"V8 heap offset @ {hex(ptr - v8_heap_addr)}") 32 | -------------------------------------------------------------------------------- /dbgtools/gdbapi.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | import gdb 3 | import re 4 | import pwndbg 5 | 6 | def execute_commands(cmds: list[str]) -> Sequence[str]: 7 | for cmd in cmds: 8 | yield execute_command(cmd) 9 | 10 | def execute_command(cmd: str) -> str: 11 | return gdb.execute(cmd, to_string=True) 12 | 13 | def set_breakpoint(addr: int): 14 | execute_command(f"b *{hex(addr)}") 15 | 16 | def set_watchpoint(addr: int): 17 | execute_command(f"watch *{hex(addr)}") 18 | 19 | def delete_all_breakpoints(): 20 | execute_command("del") 21 | 22 | def run(args: Optional[list[str]] = None): 23 | if args is not None: 24 | execute_command(f"r {' '.join(args)}") 25 | else: 26 | execute_command("r") 27 | 28 | # TODO(ju256): dont like 29 | def cont(): 30 | execute_command("c") 31 | 32 | def si(): 33 | execute_command("si") 34 | 35 | def parse_and_eval(s: str): 36 | return gdb.parse_and_eval(s) 37 | 38 | def finish(): 39 | execute_command("finish") 40 | 41 | def examine(ptr: int, repeat: int = 8, size: int = 8, fmt: str = "x"): 42 | size_fmt_map = {1: 'b', 2: 'h', 4: 'w', 8: 'g'} 43 | if size not in size_fmt_map.keys(): 44 | raise ValueError(f"size has to be in {list(size_fmt.keys())}") 45 | size_fmt = size_fmt_map[size] 46 | data = execute_command(f"x/{repeat}{size_fmt}{fmt} {hex(ptr)}") 47 | if size == 8: 48 | # color potential pointers 49 | matches = re.findall(r"(0x[a-z0-9]{8,16})", data) 50 | for q in matches: 51 | colored = pwndbg.color.memory.get(int(q, 16), q) 52 | data = data.replace(q, colored) 53 | return data 54 | -------------------------------------------------------------------------------- /dbgtools/commands/pwndump.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | from typing import Optional 5 | from dbgtools.main import get_current_libc_path, wrap_readelf_s, \ 6 | get_main_arena_off, get_libc_bin_sh 7 | 8 | 9 | parser = argparse.ArgumentParser(description="Dump useful offsets") 10 | parser.add_argument("--libc", type=str, help="path to libc") 11 | parser.add_argument("--code", action="store_true", help="dump offsets in python syntax") 12 | 13 | 14 | @pwndbg.gdblib.proc.OnlyWhenRunning 15 | @pwndbg.commands.ArgparsedCommand(parser) 16 | def pwnd(libc: Optional[str] = None, code: bool = False): 17 | if libc is None: 18 | libc = get_current_libc_path() 19 | 20 | if libc is None: 21 | raise ValueError("Couldn't find libc. Specify the path manually!") 22 | else: 23 | symbols = ["system", "__free_hook", "__malloc_hook", "malloc", 24 | "free", "printf", "dup2", "puts"] 25 | for sym_name in symbols: 26 | try: 27 | sym_addresses = wrap_readelf_s(libc, sym_name) 28 | if len(sym_addresses) != 0: 29 | sym_addr = sym_addresses[0][1] 30 | if not code: 31 | print(f"{sym_name}: {hex(sym_addr)}") 32 | else: 33 | print(f"{sym_name}_off = {hex(sym_addr)}") 34 | except: 35 | pass 36 | print() 37 | main_arena_off = get_main_arena_off(libc) 38 | if main_arena_off != -1: 39 | if not code: 40 | print(f"main_arena_off: {hex(main_arena_off)}") 41 | else: 42 | print(f"main_arena_off = {hex(main_arena_off)}") 43 | else: 44 | if not code: 45 | print(f"main_arena_off: Not found") 46 | else: 47 | print(f"# main_arena_off: Not found") 48 | print() 49 | bin_sh_off = get_libc_bin_sh(libc) 50 | if not code: 51 | print(f"/bin/sh: {hex(bin_sh_off)}") 52 | else: 53 | print(f"bin_sh_off = {hex(bin_sh_off)}") 54 | -------------------------------------------------------------------------------- /dbgtools/commands/getoffsets.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import argparse 3 | import pwndbg.commands 4 | import os.path 5 | from typing import Optional 6 | from dbgtools.main import get_current_libc_path, resolve_symbol_address 7 | 8 | 9 | 10 | def build_python_offsets_code(local_libc_symbols_with_address, binary_symbols_with_address): 11 | data = "" 12 | for sym_name, address in binary_symbols_with_address: 13 | addr_str = hex(address) if address != -1 else -1 14 | sym_str = sym_name.replace("@got", "_got").replace("@plt", "_plt") 15 | data += f"{sym_str}_off = {addr_str}\n" 16 | data += "\n" 17 | 18 | for sym_name, address in local_libc_symbols_with_address: 19 | addr_str = hex(address) if address != -1 else -1 20 | data += f"{sym_name}_off = {addr_str}\n" 21 | 22 | return data 23 | 24 | 25 | parser = argparse.ArgumentParser(description="Retrieves offsets for list of (libc|binary) symbols") 26 | parser.add_argument("symbols", nargs='+', type=str, default=[], help="symbols to resolve") 27 | parser.add_argument("--libc", type=str, help="path to libc") 28 | # TODO(ju256): --code flag? probably useless but might consider 29 | 30 | 31 | @pwndbg.gdblib.proc.OnlyWhenRunning 32 | @pwndbg.commands.ArgparsedCommand(parser) 33 | def get_offsets(symbols: list[str], libc: Optional[str] = None): 34 | if libc is not None: 35 | if not os.path.exists(libc_path): 36 | raise ValueError(f"Libc not found at given path {libc_path}!") 37 | else: 38 | libc = get_current_libc_path() 39 | 40 | libc_symbols_with_address = [] 41 | # TODO(ju256): kinda shit. we might want libc@(got|plt) support as well + non got plt binary offsets? 42 | libc_symbols = list(filter(lambda s: not (s.endswith("@plt") or s.endswith("@got")), symbols)) 43 | binary_symbols = list(filter(lambda s: s not in libc_symbols, symbols)) 44 | 45 | for sym_name in libc_symbols: 46 | sym_addr = resolve_symbol_address(sym_name, libc) 47 | libc_symbols_with_address.append((sym_name, sym_addr)) 48 | 49 | binary_symbols_with_address = [(sym, resolve_symbol_address(sym)) for sym in binary_symbols] 50 | print(build_python_offsets_code(libc_symbols_with_address, binary_symbols_with_address)) 51 | -------------------------------------------------------------------------------- /dbgtools/logger.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Union 3 | import os 4 | from dbgtools.utils import singleton 5 | 6 | 7 | # TODO(ju256): refactor this garbage 8 | @singleton 9 | class Logger: 10 | # TODO(liam) why did we choose bytes here and is this the best choice? 11 | content: bytes 12 | _log_count: int 13 | _config: dict 14 | used_log: bool 15 | _start_time: float 16 | 17 | def __init__(self): 18 | self.content = b"" 19 | self._log_count = 0 20 | self._config = {"enable_file_logging": True, 21 | "enable_log_write_buffering": False, 22 | "file_logging_mode": "append", 23 | "log_file": "gdb_script.log", 24 | "log_file_update_threshold": 5000} 25 | self.used_log = False 26 | self.clear_log_file() 27 | self._start_time = time.time() 28 | 29 | def clear_log_file(self) -> None: 30 | if (self._config["enable_file_logging"] 31 | and self._config["file_logging_mode"] == "append" 32 | and os.path.exists(self._config["log_file"])): 33 | with open(self._config["log_file"], "w") as f: 34 | f.write("") 35 | 36 | def log(self, message: bytes) -> None: 37 | self.used_log = True 38 | self.content += message 39 | self._log_count += 1 40 | self._file_logging() 41 | 42 | def set_enable_file_logging(self, enabled: bool): 43 | self._config["enable_file_logging"] = enabled 44 | 45 | def _file_logging(self): 46 | if self._config["enable_file_logging"]: 47 | update_th: int = self._config["log_file_update_threshold"] 48 | write_buf = self._config["enable_log_write_buffering"] 49 | if self._log_count % update_th == 0 or not write_buf: 50 | self.write_log_to_log_file() 51 | print(f"Written {self._log_count} logs to log file after" 52 | + f" {time.time() - self._start_time} seconds") 53 | 54 | def write_log_to_log_file(self): 55 | # TODO(liam) Keep the file open while the logger is active 56 | if self._config["file_logging_mode"] == "append": 57 | with open(self._config["log_file"], "ab") as f: 58 | f.write(self.content) 59 | else: 60 | with open(self._config["log_file"], "wb") as f: 61 | f.write(self.content) 62 | 63 | def print_log(self) -> None: 64 | if self._config["file_logging_mode"] == "append": 65 | # we need to read the log file here since content 66 | # was cleared after the last append to file 67 | with open(self._config["log_file"], "rb") as f: 68 | print(f.read()) 69 | else: 70 | print(self.content) 71 | 72 | def log_line(self, message: bytes) -> None: 73 | self.log(message + b"\n") 74 | 75 | def update_config(self, config: dict[str, Union[bool, str, int]]) -> None: 76 | self._config = config 77 | 78 | def clear(self) -> None: 79 | self.content = b"" 80 | self._log_count = 0 81 | -------------------------------------------------------------------------------- /dbgtools/commands/traceheap.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | import pwndbg 4 | import argparse 5 | import pwndbg.commands 6 | from dbgtools.breakpoints import LogBreakpoint 7 | from dbgtools.functions import get_malloc_addr, get_free_addr 8 | from dbgtools.regs import * 9 | from dbgtools.memory import read_stack 10 | from dbgtools.logger import Logger 11 | from dbgtools.utils import singleton 12 | 13 | 14 | @singleton 15 | class TraceHeapWrapper: 16 | def __init__(self): 17 | self._tracing_active: bool = False 18 | self._trace_bps: list[LogBreakpoint] = [] 19 | self._last_malloc_size: int = -1 20 | self._cur_tmp_malloc_finish_bp: Optional[LogBreakpoint] = None 21 | 22 | def enable(self): 23 | if not self._tracing_active: 24 | self._tracing_active = True 25 | self._make_bps() 26 | print("TraceHeap on") 27 | 28 | def disable(self): 29 | if self._tracing_active: 30 | for bp in self._trace_bps: 31 | bp.delete() 32 | self._trace_bps.remove(bp) 33 | self._tracing_active = False 34 | print("TraceHeap off") 35 | 36 | def _make_bps(self): 37 | for bp in self._trace_bps: 38 | bp.delete() 39 | self._trace_bps.remove(bp) 40 | malloc_addr = get_malloc_addr() 41 | free_addr = get_free_addr() 42 | 43 | if malloc_addr is None: 44 | raise ValueError("Address of malloc could not be determined") 45 | if free_addr is None: 46 | raise ValueError("Address of free could not be determined") 47 | 48 | free_bp = LogBreakpoint.create_pt_bp(free_addr, self._free_log_func) 49 | malloc_bp = LogBreakpoint.create_pt_bp(malloc_addr, lambda: "", action_funcs=[self._set_tmp_malloc_bp]) 50 | self._trace_bps = [malloc_bp, free_bp] 51 | 52 | def _set_tmp_malloc_bp(self): 53 | self._last_malloc_size = registers.rdi 54 | ret_addr = read_stack() 55 | self._cur_tmp_malloc_finish_bp = LogBreakpoint.create_pt_bp(ret_addr, self._malloc_log_func, temporary=True) 56 | 57 | def _malloc_log_func(self): 58 | if self._last_malloc_size == -1 \ 59 | or self._cur_tmp_malloc_finish_bp is None: 60 | raise ValueError("HeapTracing failed") 61 | 62 | log = f"[TraceHeap] malloc({hex(self._last_malloc_size)}) => {hex(registers.rax)}".encode() 63 | self._last_malloc_size = -1 64 | self._cur_tmp_malloc_finish_bp = None 65 | return log 66 | 67 | def _free_log_func(self) -> str: 68 | return f"[TraceHeap] free({hex(registers.rdi)})".encode() 69 | 70 | 71 | 72 | class TraceHeapState(Enum): 73 | ON = "on" 74 | OFF = "off" 75 | SHOW_TRACE = "" 76 | 77 | def __str__(self): 78 | return self.value 79 | 80 | 81 | parser = argparse.ArgumentParser(description="Traces malloc() and free() calls") 82 | parser.add_argument("state", type=TraceHeapState, nargs='?', choices=list(TraceHeapState), help="on/off state") 83 | 84 | @pwndbg.gdblib.proc.OnlyWhenRunning 85 | @pwndbg.commands.ArgparsedCommand(parser) 86 | def traceheap(state: Optional[str] = None): 87 | if state is None: 88 | state = TraceHeapState.SHOW_TRACE 89 | 90 | tracewrapper = TraceHeapWrapper() 91 | if state == TraceHeapState.ON: 92 | tracewrapper.enable() 93 | elif state == TraceHeapState.OFF: 94 | tracewrapper.disable() 95 | else: 96 | heap_trace_log = list(filter(lambda l: l.startswith(b"[TraceHeap] "), Logger().content.splitlines())) 97 | for l in heap_trace_log: 98 | print(l.replace(b"[TraceHeap] ", b"").decode()) 99 | -------------------------------------------------------------------------------- /dbgtools/commands/tracer.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | import time 3 | import pwndbg 4 | import argparse 5 | import pwndbg.commands 6 | from dbgtools import is_program_running 7 | from dbgtools.gdbapi import run, cont, set_breakpoint, set_watchpoint,\ 8 | delete_all_breakpoints, si, execute_command 9 | from dbgtools.regs import * 10 | from dbgtools.logger import Logger 11 | from typing import Optional 12 | 13 | 14 | # TODO(ju256): pretty bad refactor 15 | class Tracer: 16 | def __init__(self, trace_end_addr=None, start_bp_addr=None, start_wp_addr=None, force_rerun=True, start_args=None, timeout=60): 17 | self._trace_end_addr = trace_end_addr 18 | self._start_bp_addr = start_bp_addr 19 | self._start_wp_addr = start_wp_addr 20 | self._force_rerun = force_rerun 21 | self._start_args = start_args 22 | self._timeout = timeout 23 | self._start_time = None 24 | 25 | if self._start_bp_addr is None and self._start_wp_addr is None: 26 | raise ValueError("Tracer needs atleast start breakpoint or start watchpoint") 27 | 28 | @classmethod 29 | def get_from_watchpoint(cls, wp_addr, trace_end_addr=None, force_rerun=True, start_args=None, timeout=60): 30 | return Tracer(start_wp_addr=wp_addr, trace_end_addr=trace_end_addr, force_rerun=force_rerun, start_args=start_args, timeout=timeout) 31 | 32 | @classmethod 33 | def get_from_breakpoint(cls, bp_addr, trace_end_addr=None, force_rerun=True, start_args=None, timeout=60): 34 | return Tracer(start_bp_addr=bp_addr, trace_end_addr=trace_end_addr, force_rerun=force_rerun, start_args=start_args, timeout=timeout) 35 | 36 | def start(self): 37 | if self._start_bp_addr is not None: 38 | set_breakpoint(self._start_bp_addr) 39 | elif self._start_wp_addr is not None: 40 | set_watchpoint(self._start_wp_addr) 41 | 42 | self._start_time = time.time() 43 | if self._force_rerun: 44 | run(self._start_args) 45 | else: 46 | print("jo") 47 | print(registers.rdi) 48 | print(is_program_running()) 49 | if is_program_running(): 50 | cont() 51 | else: 52 | run(self._start_args) 53 | 54 | # wait for breakpoint or watchpoint hit 55 | logger = Logger() 56 | logger.clear_log_file() 57 | while True: 58 | try: 59 | time_ran = (time.time() - self._start_time) 60 | if time_ran >= self._timeout: 61 | print("[Tracer] Hit timeout") 62 | break 63 | # TODO(liam) check if may be null 64 | log = gdb.execute("x/i $rip", to_string=True) 65 | if log is None: 66 | log = "" 67 | log = log.encode().lstrip(b"=> ").rstrip() 68 | if len(log) != 0: 69 | logger.log_line(b"[Tracer] " + log) 70 | if self._trace_end_addr is not None and registers.rip == self._trace_end_addr: 71 | break 72 | si() 73 | # TODO(ju256): use this again if possible 74 | # dont remember when this broke 75 | # execute_command("s") 76 | except gdb.error: 77 | # TODO(ju256): shit way to stop here. refactor 78 | break 79 | 80 | logger.print_log() 81 | logger.write_log_to_log_file() 82 | 83 | 84 | parser = argparse.ArgumentParser(description="Traces instructions") 85 | parser.add_argument("--watchpoint", action='store_true', help="start tracing from a watchpoint") 86 | parser.add_argument("--breakpoint", action='store_true', help="start tracing from a breakpoint") 87 | parser.add_argument("ptr", type=int, help="watch/breakpoint string") 88 | parser.add_argument("--end", type=int, help="stop tracing when this address is hit") 89 | parser.add_argument("--force-rerun", action='store_true', help="force a rerun the program") 90 | 91 | 92 | 93 | @pwndbg.gdblib.proc.OnlyWhenRunning 94 | @pwndbg.commands.ArgparsedCommand(parser) 95 | def tracer(watchpoint: bool, breakpoint: bool, ptr: int, end: Optional[int] = None, force_rerun: bool = False): 96 | if (not watchpoint and not breakpoint) or (watchpoint and breakpoint): 97 | raise ValueError("Either --watchpoint or --breakpoint need to be specified") 98 | else: 99 | delete_all_breakpoints() 100 | if watchpoint: 101 | t = Tracer.get_from_watchpoint(ptr, trace_end_addr=end, force_rerun=force_rerun) 102 | else: 103 | t = Tracer.get_from_breakpoint(ptr, trace_end_addr=end, force_rerun=force_rerun) 104 | t.start() 105 | -------------------------------------------------------------------------------- /dbgtools/commands/heaplookup.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | import pwndbg 3 | import argparse 4 | import pwndbg.commands 5 | from typing import Optional 6 | from dbgtools.main import get_first_heap_address, get_first_heap_end_address, get_libc_base, get_binary_base, ptr_to_symbol 7 | from dbgtools.memory import read_pointer 8 | 9 | 10 | def print_interesting(heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable): 11 | fstr = f"[{hex(heap_addr)}|heap+{hex(heap_off)}]:\t {hex(ptr)}" 12 | if base_img != "": 13 | fstr += f" | {base_img}+{hex(base_img_off)}" 14 | if sym_name != "": 15 | fstr += f" | {sym_name}" 16 | if points_to_executable: 17 | fstr += f" (Points to executable memory | Possible function pointer)" 18 | print(fstr) 19 | 20 | def print_summary(ptr_tpls): 21 | # heap_addr, heap_off, ptr, base_img, base_img_off, sym_name = ptr_tpls[0] 22 | print("="*100) 23 | print("Libc pointers") 24 | for heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable in ptr_tpls: 25 | if base_img == "libc": 26 | print_interesting(heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable) 27 | print() 28 | print("Binary pointers") 29 | for heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable in ptr_tpls: 30 | if base_img == "binary": 31 | print_interesting(heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable) 32 | print() 33 | print("Stack pointers") 34 | for heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable in ptr_tpls: 35 | if base_img == "stack": 36 | print_interesting(heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable) 37 | print() 38 | print("Possible function pointers") 39 | for heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable in ptr_tpls: 40 | if points_to_executable: 41 | print_interesting(heap_addr, heap_off, ptr, base_img, base_img_off, sym_name, points_to_executable) 42 | print("="*100) 43 | 44 | 45 | parser = argparse.ArgumentParser(description="Tries to find interesting pointers on the heap") 46 | parser.add_argument("--start", type=int, help="pointer to start scanning from") 47 | parser.add_argument("--end", type=int, help="pointer to stop scanning on") 48 | 49 | 50 | @pwndbg.gdblib.proc.OnlyWhenRunning 51 | @pwndbg.commands.ArgparsedCommand(parser) 52 | def heaplookup(start: Optional[int] = None, end: Optional[int] = None): 53 | heap_start_addr = get_first_heap_address() 54 | heap_end_addr = get_first_heap_end_address() 55 | if heap_start_addr is None or heap_end_addr is None: 56 | raise ValueError("Heap start or end address could not be found") 57 | 58 | if start is None: 59 | start = heap_start_addr 60 | if end is None: 61 | end = heap_end_addr 62 | 63 | if start < heap_start_addr or start > heap_end_addr or end < heap_start_addr or end > heap_end_addr: 64 | print("Start or end address out of range") 65 | all_ptrs = [] 66 | for heap_addr in range(start, end, 8): 67 | ptr = read_pointer(heap_addr) 68 | page_of_ptr = pwndbg.gdblib.vmmap.find(ptr) 69 | if page_of_ptr is not None: 70 | # TODO(liam) this switching is almost certainly not correct 71 | # Therefore: check and refecotor it 72 | is_libc_ptr = "libc" in page_of_ptr.objfile 73 | is_heap_ptr = ptr in range(heap_start_addr, heap_end_addr) 74 | is_stack_ptr = "stack" in page_of_ptr.objfile 75 | base_img = "" 76 | base_img_off = -1 77 | sym_name = ptr_to_symbol(ptr) 78 | 79 | progspace = gdb.current_progspace() 80 | is_binimg_ptr = False 81 | if progspace is not None: 82 | is_binimg_ptr = progspace.filename in page_of_ptr.objfile 83 | if is_libc_ptr: 84 | libc_base = get_libc_base() 85 | base_img = "libc" 86 | base_img_off = ptr - libc_base 87 | elif is_heap_ptr: 88 | base_img = "heap" 89 | base_img_off = ptr - heap_start_addr 90 | elif is_stack_ptr: 91 | base_img = "stack" 92 | base_img_off = ptr - page_of_ptr.start 93 | elif is_binimg_ptr: 94 | base_img = "binary" 95 | base_img_off = ptr - get_binary_base() 96 | print_interesting(heap_addr, heap_addr - heap_start_addr, ptr, base_img, base_img_off, sym_name, page_of_ptr.execute) 97 | 98 | all_ptrs.append((heap_addr, heap_addr - heap_start_addr, ptr, base_img, base_img_off, sym_name, page_of_ptr.execute)) 99 | print_summary(all_ptrs) 100 | -------------------------------------------------------------------------------- /dbgtools/breakpoints.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | from typing import Sequence, Callable, Optional 3 | from dbgtools.main import get_pie_base 4 | from dbgtools.logger import Logger 5 | 6 | 7 | # TODO(ju256): move to gdb api 8 | def get_all_breakpoints() -> Sequence[gdb.Breakpoint]: 9 | return gdb.breakpoints() 10 | 11 | 12 | def get_breakpoints_with_location(location): 13 | return list(filter(lambda b: b.location == location, get_all_breakpoints())) 14 | 15 | 16 | def convert_to_gdb_bp_str(ptr: Optional[int] = None, 17 | func_name: Optional[str] = None, 18 | offset: Optional[int] = None) -> str: 19 | if ptr is not None: 20 | assert func_name is None and offset is None 21 | return f"*{hex(ptr)}" 22 | elif func_name is not None and offset is not None: 23 | return f"*({func_name}+{hex(offset)})" 24 | else: 25 | msg = "Breakpoint string has to consist of either or " \ 26 | + "" 27 | raise ValueError(msg) 28 | 29 | 30 | class CustomBreakpoint(gdb.Breakpoint): 31 | _action_funcs: list[Callable[[], None]] 32 | _explicit_stop: bool 33 | 34 | def __init__(self, 35 | bp_str: str, 36 | action_funcs: Optional[list[Callable[[], None]]] = None, 37 | enabled_default: bool = True, 38 | explicit_stop: bool = False, 39 | temporary: bool = False): 40 | super().__init__(bp_str, temporary=temporary) 41 | if action_funcs is None: 42 | action_funcs = [] 43 | self._make_unique() 44 | self._bp_str = bp_str 45 | self.enabled = enabled_default 46 | self._explicit_stop = explicit_stop 47 | self._action_funcs = action_funcs 48 | self._cond_func = None 49 | 50 | @classmethod 51 | def create_pt_bp(cls, ptr, *args, **kwargs): 52 | return cls(convert_to_gdb_bp_str(ptr=ptr), *args, **kwargs) 53 | 54 | @classmethod 55 | def create_pie_bp(cls, ptr, *args, **kwargs): 56 | return cls(convert_to_gdb_bp_str(ptr=get_pie_base() + ptr), 57 | *args, **kwargs) 58 | 59 | @classmethod 60 | def create_func_off_bp(cls, func_name, offset, *args, **kwargs): 61 | return cls(convert_to_gdb_bp_str(func_name=func_name, offset=offset), 62 | *args, **kwargs) 63 | 64 | def _make_unique(self): 65 | for bp in get_breakpoints_with_location(self.location): 66 | if bp.number != self.number: 67 | bp.delete() 68 | 69 | def set_condition_func(self, cond_func): 70 | self._cond_func = cond_func 71 | 72 | def reset_condition_func(self): 73 | self._cond_func = None 74 | 75 | def stop(self): 76 | for bp_stop_func in self._action_funcs: 77 | bp_stop_func() 78 | if self._explicit_stop: 79 | return True 80 | if self._cond_func is not None: 81 | return self._cond_func() 82 | return False 83 | 84 | 85 | class LogBreakpoint(CustomBreakpoint): 86 | logger_func: Callable[[], bytes] 87 | 88 | def __init__(self, 89 | bp_str: str, 90 | logger_func: Callable[[], bytes], 91 | action_funcs: Optional[list[Callable[[], None]]] =None, 92 | enabled_default: bool = True, 93 | explicit_stop: bool = False, 94 | temporary: bool = False): 95 | super().__init__(bp_str, 96 | action_funcs, 97 | enabled_default, 98 | explicit_stop, 99 | temporary) 100 | self._logger_func = logger_func 101 | 102 | def stop(self): 103 | logger = Logger() 104 | log = self._logger_func() 105 | if len(log) != 0: 106 | logger.log_line(log) 107 | return super().stop() 108 | 109 | 110 | class ActionBreakpoint(CustomBreakpoint): 111 | def __init__(self, 112 | bp_str: str, 113 | action_funcs: Optional[list[Callable[[], None]]] , 114 | explicit_stop: bool = False): 115 | super().__init__(bp_str, 116 | action_funcs, 117 | enabled_default=True, 118 | explicit_stop=explicit_stop, 119 | temporary=False) 120 | 121 | 122 | # TODO: cleanup 123 | class TraceBreakpoint(CustomBreakpoint): 124 | def __init__(self, bp_str: str, trace_function, explicit_stop=False): 125 | super().__init__(bp_str, [self._do_trace], enabled_default=True, explicit_stop=explicit_stop, temporary=False) 126 | self._trace_func = trace_function 127 | self._last_traces = [] 128 | self._traces = [] 129 | 130 | def get_traces(self): 131 | return self._traces 132 | 133 | def get_last_traces(self): 134 | return self._last_traces 135 | 136 | def reset_last_traces(self): 137 | self._last_traces = [] 138 | 139 | def _do_trace(self): 140 | trace = self._trace_func() 141 | self._last_traces.append(trace) 142 | self._traces.append(trace) 143 | 144 | def reset(self): 145 | self._traces = [] 146 | self._last_traces = [] 147 | -------------------------------------------------------------------------------- /dbgtools/memory.py: -------------------------------------------------------------------------------- 1 | import pwndbg 2 | import struct 3 | from typing import Optional, Sequence 4 | from dbgtools.regs import registers 5 | 6 | 7 | def read_bytes(addr: int, count: int) -> bytes: 8 | return bytes(pwndbg.gdblib.memory.read(addr, count)) 9 | 10 | 11 | def write_bytes(addr: int, data: str | bytes | bytearray) -> None: 12 | pwndbg.gdblib.memory.write(addr, data) 13 | 14 | 15 | def read_byte(addr: int) -> int: 16 | return read_bytes(addr, 1)[0] 17 | 18 | 19 | read_u8 = read_byte 20 | read_char = read_byte 21 | 22 | 23 | def write_byte(addr: int, b: int) -> None: 24 | write_bytes(addr, bytes([b])) 25 | 26 | 27 | write_u8 = write_byte 28 | 29 | 30 | def read_u64(addr: int) -> int: 31 | m = read_bytes(addr, 8) 32 | return struct.unpack(" None: 36 | write_bytes(addr, struct.pack(" int: 40 | m = read_bytes(addr, 2) 41 | return struct.unpack(" None: 45 | write_bytes(addr, struct.pack(" int: 49 | m = read_bytes(addr, 4) 50 | return struct.unpack(" None: 54 | write_bytes(addr, struct.pack(" None: 58 | m = read_bytes(addr, 8) 59 | return struct.unpack(" None: 63 | write_bytes(addr, struct.pack(" float: 67 | m = read_bytes(addr, 8) 68 | return struct.unpack(" None: 72 | write_bytes(addr, struct.pack(" None: 76 | write_bytes(addr, struct.pack(" float: 80 | m = read_bytes(addr, 4) 81 | return struct.unpack(" int: 85 | m = read_bytes(addr, 4) 86 | return struct.unpack(" None: 90 | write_bytes(addr, struct.pack(" int: 94 | if deref_count >= 1: 95 | return read_pointer(read_pointer(addr), deref_count=deref_count-1) 96 | else: 97 | return read_u64(addr) 98 | 99 | def read_stack(off: int = 0): 100 | rsp_val = registers.rsp 101 | return read_u64(rsp_val + off * 8) 102 | 103 | def write_pointer(addr: int, ptr: int) -> None: 104 | write_u64(addr, ptr) 105 | 106 | def read_bool(addr: int) -> bool: 107 | v = read_u32(addr) 108 | return True if v != 0 else False 109 | 110 | def write_bool(addr: int, v: bool) -> None: 111 | write_u32(addr, 1 if v else 0) 112 | 113 | 114 | def read_bytestring(addr: int, length: Optional[int] = None) -> bytes: 115 | s = b"" 116 | b = read_byte(addr) 117 | i = 1 118 | s += bytes([b]) 119 | while b != 0x0 and (length is None or i < length): 120 | b = read_byte(addr + i) 121 | s += bytes([b]) 122 | i += 1 123 | return s 124 | 125 | read_string = read_bytestring 126 | 127 | def write_string(addr: int, s: bytes, length: Optional[int] = None, 128 | append_zero: bool = False): 129 | 130 | # extend string to length with null bytes or crop it 131 | if length is not None: 132 | s = s[:length] 133 | if len(s) < length: 134 | s += bytes([0]) * (length - len(s)) 135 | 136 | if append_zero and s[-1] != 0: 137 | s += bytes([0]) 138 | 139 | write_bytes(addr, s) 140 | 141 | 142 | def read_array(addr: int, count: int, element_size: int) -> list[int]: 143 | element_readers = {1: read_byte, 2: read_u16, 4: read_u32, 8: read_u64} 144 | if element_size not in [1, 2, 4, 8]: 145 | raise ValueError("element_size has to be in [1, 2, 4, 8]") 146 | 147 | reader = element_readers[element_size] 148 | arr = [] 149 | for i in range(count): 150 | arr.append(reader(addr + i * element_size)) 151 | return arr 152 | 153 | 154 | def read_u8_array(addr: int, count: int) -> Sequence[int]: 155 | return read_array(addr, count, 1) 156 | 157 | read_char_array = read_u8_array 158 | 159 | def read_u16_array(addr: int, count: int) -> Sequence[int]: 160 | return read_array(addr, count, 2) 161 | 162 | 163 | def read_u32_array(addr: int, count: int) -> Sequence[int]: 164 | return read_array(addr, count, 4) 165 | 166 | 167 | def read_u64_array(addr: int, count: int) -> Sequence[int]: 168 | return read_array(addr, count, 8) 169 | 170 | 171 | def write_array(addr: int, data_array: Sequence[int], element_size: int): 172 | element_writers = {1: write_byte, 173 | 2: write_u16, 174 | 4: write_u32, 175 | 8: write_u64} 176 | if element_size not in [1, 2, 4, 8]: 177 | raise ValueError("element_size has to be in [1, 2, 4, 8]") 178 | 179 | writer = element_writers[element_size] 180 | for i, v in enumerate(data_array): 181 | writer(addr + i * element_size, v) 182 | 183 | 184 | def write_u8_array(addr: int, data_array: Sequence[int]): 185 | write_array(addr, data_array, 1) 186 | 187 | write_char_array = write_u8_array 188 | 189 | def write_u16_array(addr: int, data_array: Sequence[int]): 190 | write_array(addr, data_array, 2) 191 | 192 | 193 | def write_u32_array(addr: int, data_array: Sequence[int]): 194 | write_array(addr, data_array, 4) 195 | 196 | def write_u64_array(addr: int, data_array: Sequence[int]): 197 | write_array(addr, data_array, 8) 198 | 199 | 200 | def read_string_from_reg_ptr(reg_name): 201 | str_ptr = read_reg(reg_name) 202 | s = read_bytestring(str_ptr) 203 | return str_ptr, s 204 | 205 | def write_string_to_reg_ptr(reg_name, s): 206 | str_ptr = read_reg(reg_name) 207 | write_string(str_ptr, s, len(s)) 208 | return str_ptr, s 209 | -------------------------------------------------------------------------------- /dbgtools/functions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Type 2 | from abc import ABC, abstractmethod 3 | from dbgtools.regs import * 4 | from dbgtools.gdbapi import execute_command, finish, si 5 | from dbgtools.main import get_function_symbol_addr, find_gadget 6 | from dbgtools.regs import RegisterDescriptor, registers 7 | from dbgtools.memory import write_bytes 8 | import pwn 9 | 10 | 11 | # TODO(ju256): move/get constants to/from somewhere 12 | PROT_READ = 0x1 13 | PROT_WRITE = 0x2 14 | PROT_EXEC = 0x4 15 | 16 | MAP_ANON = 0x20 17 | MAP_PRIVATE = 0x2 18 | 19 | SYS_READ = 0x0 20 | SYS_WRITE = 0x1 21 | SYS_EXIT = 0x3c 22 | 23 | 24 | class RegTransparentScope: 25 | def __init__(self): 26 | self._reg_state: Optional[dict[str, RegisterDescriptor]] = None 27 | 28 | def __enter__(self): 29 | self._reg_state = registers.dump() 30 | 31 | def __exit__(self, exc_type, exc_val, exc_tb): 32 | registers.restore(self._reg_state) 33 | 34 | 35 | class CallingConvention(ABC): 36 | def __init__(self, *args): 37 | self._arglist = args 38 | 39 | @abstractmethod 40 | def call(self, ptr: int): 41 | ... 42 | 43 | def callsym(self, sym: str): 44 | ptr = get_function_symbol_addr(sym) 45 | return self.call(ptr) 46 | 47 | def arg(self, idx: int) -> int: 48 | return self._arglist[idx] if len(self._arglist) > idx else None 49 | 50 | def argcount(self) -> int: 51 | return len(self._arglist) 52 | 53 | 54 | 55 | class SystemVAMD64(CallingConvention): 56 | def call(self, func_ptr: int): 57 | # TODO(ju256): allow stack arguments 58 | if self.argcount() > 6: 59 | raise ValueError("Currently only <= 6 arguments are supported") 60 | 61 | rdi = self.arg(0) 62 | rsi = self.arg(1) 63 | rdx = self.arg(2) 64 | rcx = self.arg(3) 65 | r8 = self.arg(4) 66 | r9 = self.arg(5) 67 | 68 | with RegTransparentScope(): 69 | if rdi is not None: 70 | registers.rdi = rdi 71 | if rsi is not None: 72 | registers.rsi = rsi 73 | if rdx is not None: 74 | registers.rdx = rdx 75 | if rcx is not None: 76 | registers.rcx = rcx 77 | if r8 is not None: 78 | registers.r8 = r8 79 | if r9 is not None: 80 | registers.r9 = r9 81 | 82 | rip_val = registers.rip 83 | registers.rip = func_ptr 84 | sim_call(rip_val) 85 | finish() 86 | return registers.rax 87 | 88 | 89 | class Syscall64(CallingConvention): 90 | def call(self, syscall_gadget_ptr: int): 91 | if self.argcount() == 0: 92 | raise ValueError("Syscall number has to be specified as first argument") 93 | 94 | rax = self.arg(0) 95 | assert rax is not None 96 | 97 | rdi = self.arg(1) 98 | rsi = self.arg(2) 99 | rdx = self.arg(3) 100 | r10 = self.arg(4) 101 | r8 = self.arg(5) 102 | r9 = self.arg(6) 103 | 104 | with RegTransparentScope(): 105 | registers.rax = rax 106 | if rdi is not None: 107 | registers.rdi = rdi 108 | if rsi is not None: 109 | registers.rsi = rsi 110 | if rdx is not None: 111 | registers.rdx = rdx 112 | if r10 is not None: 113 | registers.r10 = r10 114 | if r8 is not None: 115 | registers.r8 = r8 116 | if r9 is not None: 117 | registers.r9 = r9 118 | 119 | rip_val = registers.rip 120 | registers.rip = syscall_gadget_ptr 121 | si() # execute syscall 122 | return registers.rax 123 | 124 | 125 | def get_malloc_addr(): 126 | return get_function_symbol_addr("__libc_malloc") 127 | 128 | def get_free_addr(): 129 | return get_function_symbol_addr("__libc_free") 130 | 131 | def get_mmap_addr(): 132 | return get_function_symbol_addr("mmap") 133 | 134 | def get_mprotect_addr(): 135 | return get_function_symbol_addr("mprotect") 136 | 137 | def get_puts_addr(): 138 | return get_function_symbol_addr("puts") 139 | 140 | def sim_call(ret_address: int): 141 | registers.rsp -= 8 142 | execute_command("set {long*}$rsp="+f"{hex(ret_address)}") 143 | 144 | 145 | def mprotect(address: int = 0, len: int = 0x1000, prot: int = PROT_READ|PROT_WRITE|PROT_EXEC, calling_convention: Type[CallingConvention] = SystemVAMD64): 146 | return calling_convention(address, len, prot).callsym("mprotect") 147 | 148 | def mmap(address: int = 0, size: int = 0x1000, protect: int = PROT_READ|PROT_WRITE|PROT_EXEC, 149 | flags: int = MAP_PRIVATE|MAP_ANON, filedes: int = 0, offset: int = 0, calling_convention: Type[CallingConvention] = SystemVAMD64): 150 | return calling_convention(address, size, protect, flags, filedes, offset).callsym("mmap") 151 | 152 | def malloc(size: int, calling_convention: Type[CallingConvention] = SystemVAMD64): 153 | return calling_convention(size).callsym("malloc") 154 | 155 | def free(ptr: int, calling_convention: Type[CallingConvention] = SystemVAMD64): 156 | return calling_convention(ptr).callsym("free") 157 | 158 | def puts(ptr: int, calling_convention: Type[CallingConvention] = SystemVAMD64): 159 | return calling_convention(ptr).callsym("puts") 160 | 161 | def munmap(addr: int, length: int, calling_convention: Type[CallingConvention] = SystemVAMD64): 162 | return calling_convention(addr, length).callsym("munmap") 163 | 164 | def syscall(num: int, *args): 165 | syscall_gadget_bytes = pwn.asm("syscall", arch="amd64", os="linux") 166 | tmp_page = None 167 | try: 168 | page, offset = next(find_gadget(syscall_gadget_bytes)) 169 | syscall_gadget_ptr = page.start + offset 170 | except StopIteration: 171 | tmp_page = mmap(size=0x1000) 172 | write_bytes(tmp_page, syscall_gadget_bytes) 173 | syscall_gadget_ptr = tmp_page 174 | 175 | ret_value = Syscall64(num, *args).call(syscall_gadget_ptr) 176 | if tmp_page is not None: 177 | munmap(tmp_page, 0x1000) 178 | return ret_value 179 | 180 | def sys_read(fd: int, buf: int, n: int): 181 | return syscall(SYS_READ, fd, buf, n) 182 | 183 | def sys_write(fd: int, buf: int, n: int): 184 | return syscall(SYS_WRITE, fd, buf, n) 185 | 186 | def sys_exit(error_code: int): 187 | return syscall(SYS_EXIT, error_code) 188 | -------------------------------------------------------------------------------- /dbgtools/main.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | import pwndbg 3 | import ctypes 4 | import struct 5 | import time 6 | import os 7 | import re 8 | from typing import Optional, Sequence 9 | from dataclasses import dataclass 10 | from subprocess import check_output 11 | from dbgtools.logger import Logger 12 | from dbgtools.gdbapi import execute_command 13 | from dbgtools.regs import * 14 | from dbgtools.memory import write_pointer, read_bytes 15 | 16 | 17 | def is_program_running(): 18 | # very hacky 19 | try: 20 | registers.ax 21 | return True 22 | except gdb.error: 23 | return False 24 | 25 | 26 | def patch_string_gdb(addr, string): 27 | cmd = f"set "+" {char["+str(len(string)+1)+"]}"+ f'{hex(addr)} = "{string}"' 28 | gdb.execute(cmd) 29 | 30 | 31 | def get_function_symbol_addr(sym_name): 32 | return pwndbg.gdblib.symbol.address(sym_name) 33 | 34 | def ptr_to_symbol(ptr): 35 | return pwndbg.gdblib.symbol.get(ptr) 36 | 37 | 38 | # FIXME(ju256): do better here 39 | def wrap_readelf_s(libc_path, sym_name): 40 | data = check_output(f"readelf -s {libc_path}", shell=True).splitlines() 41 | data = list(filter(lambda l: sym_name in l.decode(), data)) 42 | p = re.compile(f"\\s{sym_name}@@GLIBC") 43 | parsed_syms = [] 44 | for d in data: 45 | off = int(d.split(b": ")[1].split()[0], 16) 46 | sym_name = d.split(b"@@GLIBC_")[0].split()[-1] 47 | parsed_syms.append((sym_name, off)) 48 | # print(parsed_syms) 49 | if len(parsed_syms) == 1: 50 | return parsed_syms 51 | for ps_name, ps_addr in parsed_syms: 52 | if ps_name == sym_name: 53 | return [(ps_name, ps_addr)] 54 | return parsed_syms 55 | 56 | 57 | def exit_handler(event): 58 | logger = Logger() 59 | if logger.used_log: 60 | logger.write_log_to_log_file() 61 | logger.print_log() 62 | # print(f"Finished in {duration}s") 63 | 64 | def get_libc_bin_sh(libc_path): 65 | data = check_output(f'strings -t x {libc_path} | grep "/bin/sh"', shell=True) 66 | return int(data.split(b"/bin/sh")[0].strip(), 16) 67 | 68 | 69 | # TODO(ju256): just using current libc in case of failure is confusing. fix 70 | def get_main_arena_off(libc_path): 71 | # https://github.com/bash-c/main_arena_offset/blob/master/main_arena 72 | try: 73 | __free_hook_off = wrap_readelf_s(libc_path, "__free_hook")[0][1] 74 | __malloc_hook_off = wrap_readelf_s(libc_path, "__malloc_hook")[0][1] 75 | __realloc_hook_off = wrap_readelf_s(libc_path, "__realloc_hook")[0][1] 76 | except IndexError: 77 | print("Couldn't find in libc. Trying with current libc") 78 | libc_base = get_libc_base() 79 | free_hook_sym = get_function_symbol_addr("__free_hook") 80 | malloc_hook_sym = get_function_symbol_addr("__malloc_hook") 81 | realloc_hook_sym = get_function_symbol_addr("__realloc_hook") 82 | if any(map(lambda s: s is None, [libc_base, free_hook_sym, malloc_hook_sym, realloc_hook_sym])): 83 | return -1 84 | else: 85 | __free_hook_off = free_hook_sym - libc_base 86 | __malloc_hook_off = malloc_hook_sym - libc_base 87 | __realloc_hook_off = realloc_hook_sym - libc_base 88 | 89 | main_arena_off = (__malloc_hook_off - __realloc_hook_off) * 2 + __malloc_hook_off 90 | return main_arena_off 91 | 92 | # FIXME(ju256): fully bricked 93 | def wrap_get_got_addr(symbol_name): 94 | jmpslots = list(pwndbg.wrappers.readelf.get_jmpslots()) 95 | for line in jmpslots: 96 | address, info, rtype, value, name = line.split()[:5] 97 | 98 | if symbol_name not in name: 99 | continue 100 | return int(address, 16) 101 | return -1 102 | 103 | def wrap_get_plt_addr(symbol_name): 104 | # FIXME: implement 105 | raise NotImplementedError("parsing .plt(.sec) seems to be to hard for tools???? to lazy to implement the parsing myself now") 106 | 107 | def resolve_symbol_address(symbol_name, libc_path=None): 108 | is_binary_sym = symbol_name.endswith("@got") or symbol_name.endswith("@plt") 109 | if is_binary_sym: 110 | if symbol_name.endswith("@got"): 111 | return wrap_get_got_addr(symbol_name.replace("@got", "")) 112 | else: 113 | return wrap_get_plt_addr(symbol_name.replace("@plt", "")) 114 | 115 | else: 116 | if libc_path is None: 117 | raise ValueError("No libc path provided for libc symbol!") 118 | 119 | try: 120 | return wrap_readelf_s(libc_path, symbol_name)[0][1] 121 | except IndexError: 122 | return -1 123 | 124 | 125 | def vmmap(): 126 | return pwndbg.gdblib.vmmap.get() 127 | 128 | def get_executable_pages(): 129 | return [p for p in vmmap() if p.execute] 130 | 131 | def get_heap_base(): 132 | if pwndbg.heap.current is not None: 133 | heap_ptrs = [] 134 | for arena in pwndbg.heap.current.arenas: 135 | for heap in arena.heaps: 136 | heap_ptrs.append(heap.start) 137 | if len(heap_ptrs) == 1: 138 | return heap_ptrs[:1] 139 | else: 140 | return heap_ptrs 141 | else: 142 | for page in vmmap(): 143 | if "heap" in page.objfile: 144 | return [page.start] 145 | return [-1] 146 | 147 | 148 | def get_first_heap_end_address(): 149 | return pwndbg.gdblib.vmmap.find(get_first_heap_address()).end 150 | 151 | def get_first_heap_address(): 152 | heap_addresses = get_heap_base() 153 | return heap_addresses[0] 154 | 155 | def set_library_path(path): 156 | gdb.execute(f"set env LD_LIBRARY_PATH {path}") 157 | 158 | 159 | # TODO(liam) change the return types of the following functions to Optional[...] 160 | # and where already did make sure they are used as optionals 161 | 162 | # TODO(liam) I belief pwndbg by now support something more stable 163 | # if this is true, use pwndbg implementation instead. 164 | def get_libc_base() -> Optional[int]: 165 | for page in pwndbg.gdblib.vmmap.get(): 166 | if "libc." in page.objfile: 167 | return page.start 168 | 169 | def get_binary_base() -> Optional[int]: 170 | for page in pwndbg.gdblib.vmmap.get(): 171 | progspace = gdb.current_progspace() 172 | if progspace is not None and progspace.filename in page.objfile: 173 | return page.start 174 | 175 | def get_pie_base() -> Optional[int]: 176 | return get_binary_base() 177 | 178 | def force_load_pie_base() -> Optional[int]: 179 | execute_command("entry") 180 | return get_pie_base() 181 | 182 | def get_current_libc_path() -> Optional[str]: 183 | for page in pwndbg.gdblib.vmmap.get(): 184 | if "libc" in page.objfile: 185 | return page.objfile 186 | 187 | 188 | def find_gadget(gadget_bytes: bytes): 189 | for page in sorted(get_executable_pages(), key=lambda p: p.start): 190 | if not page.read: 191 | # vsyscall 192 | continue 193 | for ptr in range(page.start, page.end + 1 - 8): 194 | if read_bytes(ptr, len(gadget_bytes)) == gadget_bytes: 195 | offset = ptr - page.start 196 | yield (page, offset) 197 | 198 | 199 | gdb.events.exited.connect(exit_handler) 200 | -------------------------------------------------------------------------------- /dbgtools/regs.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from dataclasses import dataclass 3 | from dbgtools.gdbapi import execute_command, parse_and_eval 4 | 5 | 6 | def is_bit_set(val, bit): 7 | return val & (1 << bit) != 0 8 | 9 | 10 | def read_reg_ctype_convert(reg_name, conv_func): 11 | return conv_func(parse_and_eval(f"${reg_name}")).value 12 | 13 | def set_reg(reg_name, val): 14 | execute_command(f"set ${reg_name} = {hex(val)}") 15 | 16 | 17 | def read_reg8(reg_name): 18 | return read_reg_ctype_convert(reg_name, ctypes.c_ulong) 19 | 20 | 21 | def read_reg4(reg_name): 22 | return read_reg_ctype_convert(reg_name, ctypes.c_uint) 23 | 24 | 25 | def read_reg2(reg_name): 26 | return read_reg_ctype_convert(reg_name, ctypes.c_ushort) 27 | 28 | 29 | def read_reg1(reg_name): 30 | return read_reg_ctype_convert(reg_name, ctypes.c_ubyte) 31 | 32 | 33 | def write_reg_ctype_convert(reg_name, v, conv_func): 34 | v = conv_func(v).value 35 | set_reg(reg_name, v) 36 | 37 | 38 | def write_reg8(reg_name, v): 39 | write_reg_ctype_convert(reg_name, v, ctypes.c_ulong) 40 | 41 | 42 | def write_reg4(reg_name, v): 43 | write_reg_ctype_convert(reg_name, v, ctypes.c_uint) 44 | 45 | 46 | def write_reg2(reg_name, v): 47 | write_reg_ctype_convert(reg_name, v, ctypes.c_ushort) 48 | 49 | 50 | def write_reg1(reg_name, v): 51 | write_reg_ctype_convert(reg_name, v, ctypes.c_ubyte) 52 | 53 | 54 | reg_readers = { 55 | "rax": lambda: read_reg8("rax"), 56 | "eax": lambda: read_reg4("eax"), 57 | "rbx": lambda: read_reg8("rbx"), 58 | "ebx": lambda: read_reg4("ebx"), 59 | "rcx": lambda: read_reg8("rcx"), 60 | "ecx": lambda: read_reg4("ecx"), 61 | "rdx": lambda: read_reg8("rdx"), 62 | "edx": lambda: read_reg4("edx"), 63 | "rsi": lambda: read_reg8("rsi"), 64 | "esi": lambda: read_reg4("esi"), 65 | "rdi": lambda: read_reg8("rdi"), 66 | "edi": lambda: read_reg4("edi"), 67 | "rbp": lambda: read_reg8("rbp"), 68 | "ebp": lambda: read_reg4("ebp"), 69 | "rsp": lambda: read_reg8("rsp"), 70 | "esp": lambda: read_reg4("esp"), 71 | "r8": lambda: read_reg8("r8"), 72 | "r9": lambda: read_reg8("r9"), 73 | "r10": lambda: read_reg8("r10"), 74 | "r11": lambda: read_reg8("r11"), 75 | "r12": lambda: read_reg8("r12"), 76 | "r13": lambda: read_reg8("r13"), 77 | "r14": lambda: read_reg8("r14"), 78 | "r15": lambda: read_reg8("r15"), 79 | "r8d": lambda: read_reg4("r8d"), 80 | "r9d": lambda: read_reg4("r9d"), 81 | "r10d": lambda: read_reg4("r10d"), 82 | "r11d": lambda: read_reg4("r11d"), 83 | "r12d": lambda: read_reg4("r12d"), 84 | "r13d": lambda: read_reg4("r13d"), 85 | "r14d": lambda: read_reg4("r14d"), 86 | "r15d": lambda: read_reg4("r15d"), 87 | "rip": lambda: read_reg8("rip"), 88 | "eip": lambda: read_reg4("eip"), 89 | "al": lambda: read_reg1("al"), 90 | "bl": lambda: read_reg1("bl"), 91 | "cl": lambda: read_reg1("cl"), 92 | "dl": lambda: read_reg1("dl"), 93 | "ax": lambda: read_reg2("ax"), 94 | "bx": lambda: read_reg2("bx"), 95 | "cx": lambda: read_reg2("cx"), 96 | "dx": lambda: read_reg2("dx"), 97 | "eflags": lambda: read_reg8("eflags"), 98 | } 99 | 100 | reg_writers = { 101 | "rax": lambda v: write_reg8("rax", v), 102 | "eax": lambda v: write_reg4("eax", v), 103 | "rbx": lambda v: write_reg8("rbx", v), 104 | "ebx": lambda v: write_reg4("ebx", v), 105 | "rcx": lambda v: write_reg8("rcx", v), 106 | "ecx": lambda v: write_reg4("ecx", v), 107 | "rdx": lambda v: write_reg8("rdx", v), 108 | "edx": lambda v: write_reg4("edx", v), 109 | "rsi": lambda v: write_reg8("rsi", v), 110 | "esi": lambda v: write_reg4("esi", v), 111 | "rdi": lambda v: write_reg8("rdi", v), 112 | "edi": lambda v: write_reg4("edi", v), 113 | "rbp": lambda v: write_reg8("rbp", v), 114 | "ebp": lambda v: write_reg4("ebp", v), 115 | "rsp": lambda v: write_reg8("rsp", v), 116 | "esp": lambda v: write_reg4("esp", v), 117 | "r8": lambda v: write_reg8("r8", v), 118 | "r9": lambda v: write_reg8("r9", v), 119 | "r10": lambda v: write_reg8("r10", v), 120 | "r11": lambda v: write_reg8("r11", v), 121 | "r12": lambda v: write_reg8("r12", v), 122 | "r13": lambda v: write_reg8("r13", v), 123 | "r14": lambda v: write_reg8("r14", v), 124 | "r15": lambda v: write_reg8("r15", v), 125 | "r8d": lambda v: write_reg4("r8d", v), 126 | "r9d": lambda v: write_reg4("r9d", v), 127 | "r10d": lambda v: write_reg4("r10d", v), 128 | "r11d": lambda v: write_reg4("r11d", v), 129 | "r12d": lambda v: write_reg4("r12d", v), 130 | "r13d": lambda v: write_reg4("r13d", v), 131 | "r14d": lambda v: write_reg4("r14d", v), 132 | "r15d": lambda v: write_reg4("r15d", v), 133 | "rip": lambda v: write_reg8("rip", v), 134 | "eip": lambda v: write_reg4("eip", v), 135 | "al": lambda v: write_reg1("al", v), 136 | "bl": lambda v: write_reg1("bl", v), 137 | "cl": lambda v: write_reg1("cl", v), 138 | "dl": lambda v: write_reg1("dl", v), 139 | "ax": lambda v: write_reg2("ax", v), 140 | "bx": lambda v: write_reg2("bx", v), 141 | "cx": lambda v: write_reg2("cx", v), 142 | "dx": lambda v: write_reg2("dx", v), 143 | "eflags": lambda v: write_reg8("eflags", v), 144 | } 145 | 146 | 147 | def read_reg(reg_name): 148 | return reg_readers[reg_name]() 149 | 150 | 151 | def write_reg(reg_name, v): 152 | return reg_writers[reg_name](v) 153 | 154 | 155 | class RegisterDescriptor: 156 | def __init__(self, reg_name: str): 157 | self._reg_name = reg_name 158 | 159 | def __get__(self, obj, objtype=None): 160 | return read_reg(self._reg_name) 161 | 162 | def __set__(self, obj, value): 163 | return write_reg(self._reg_name, value) 164 | 165 | def info(self): 166 | return "" 167 | 168 | 169 | def get_eflags_info(): 170 | return EFLAGSRegisterDescriptor("eflags").info() 171 | 172 | 173 | class EFLAGSRegisterDescriptor(RegisterDescriptor): 174 | def __init__(self, reg_name: str): 175 | super().__init__(reg_name) 176 | 177 | def _check_flag_bit(self, bit): 178 | return is_bit_set(self.__get__(None), bit) 179 | 180 | def info(self): 181 | val = self.__get__(None) 182 | infol = [] 183 | 184 | CF = self._check_flag_bit(0) 185 | PF = self._check_flag_bit(2) 186 | AF = self._check_flag_bit(4) 187 | ZF = self._check_flag_bit(6) 188 | SF = self._check_flag_bit(7) 189 | TF = self._check_flag_bit(8) 190 | IF = self._check_flag_bit(9) 191 | DF = self._check_flag_bit(10) 192 | OF = self._check_flag_bit(11) 193 | IOPL = (val >> 12) & 0b11 194 | NT = self._check_flag_bit(14) 195 | RF = self._check_flag_bit(16) 196 | VM = self._check_flag_bit(17) 197 | AC = self._check_flag_bit(18) 198 | VIF = self._check_flag_bit(19) 199 | VIP = self._check_flag_bit(20) 200 | ID = self._check_flag_bit(21) 201 | 202 | if CF: 203 | infol.append('CF') 204 | if PF: 205 | infol.append('PF') 206 | if AF: 207 | infol.append('AF') 208 | if ZF: 209 | infol.append('ZF') 210 | if SF: 211 | infol.append('SF') 212 | if TF: 213 | infol.append('TF') 214 | if IF: 215 | infol.append('IF') 216 | if DF: 217 | infol.append('DF') 218 | if OF: 219 | infol.append('OF') 220 | infol.append(f'IOPL={IOPL}') 221 | if NT: 222 | infol.append('NT') 223 | if RF: 224 | infol.append('RF') 225 | if VM: 226 | infol.append('VM') 227 | if AC: 228 | infol.append('AC') 229 | if VIF: 230 | infol.append('VIF') 231 | if VIP: 232 | infol.append('VIP') 233 | if ID: 234 | infol.append('ID') 235 | 236 | return "[" + " ".join(infol) + "]" 237 | 238 | 239 | 240 | @dataclass 241 | class Registers: 242 | al: RegisterDescriptor 243 | ax: RegisterDescriptor 244 | eax: RegisterDescriptor 245 | rax: RegisterDescriptor 246 | bl: RegisterDescriptor 247 | bx: RegisterDescriptor 248 | ebx: RegisterDescriptor 249 | rbx: RegisterDescriptor 250 | cl: RegisterDescriptor 251 | cx: RegisterDescriptor 252 | ecx: RegisterDescriptor 253 | rcx: RegisterDescriptor 254 | dl: RegisterDescriptor 255 | dx: RegisterDescriptor 256 | edx: RegisterDescriptor 257 | rdx: RegisterDescriptor 258 | 259 | esi: RegisterDescriptor 260 | rsi: RegisterDescriptor 261 | edi: RegisterDescriptor 262 | rdi: RegisterDescriptor 263 | rbp: RegisterDescriptor 264 | ebp: RegisterDescriptor 265 | 266 | esp: RegisterDescriptor 267 | rsp: RegisterDescriptor 268 | r8: RegisterDescriptor 269 | r9: RegisterDescriptor 270 | r10: RegisterDescriptor 271 | r11: RegisterDescriptor 272 | r12: RegisterDescriptor 273 | r13: RegisterDescriptor 274 | r14: RegisterDescriptor 275 | r15: RegisterDescriptor 276 | 277 | r8d: RegisterDescriptor 278 | r9d: RegisterDescriptor 279 | r10d: RegisterDescriptor 280 | r11d: RegisterDescriptor 281 | r12d: RegisterDescriptor 282 | r13d: RegisterDescriptor 283 | r14d: RegisterDescriptor 284 | r15d: RegisterDescriptor 285 | 286 | eflags: EFLAGSRegisterDescriptor 287 | rip: RegisterDescriptor 288 | 289 | @classmethod 290 | def create(cls): 291 | reg_dict = {} 292 | for reg_name in Registers.__dataclass_fields__.keys(): 293 | reg_dict[reg_name] = Registers.__dataclass_fields__[reg_name].type(reg_name) 294 | x = Registers(**reg_dict) 295 | return x 296 | 297 | def dump(self): 298 | return {reg_name: self.__dict__[reg_name].__get__(self) for reg_name in Registers.__dataclass_fields__.keys()} 299 | 300 | def hexdump(self): 301 | reg_dict = self.dump() 302 | for reg in reg_dict.keys(): 303 | print(reg, hex(reg_dict[reg]), self.__dict__[reg].info()) 304 | 305 | def restore(self, reg_dump): 306 | for reg_name in reg_dump.keys(): 307 | set_reg(reg_name, reg_dump[reg_name]) 308 | 309 | def __getattribute__(self, attr): 310 | obj = object.__getattribute__(self, attr) 311 | if hasattr(obj, '__get__'): 312 | return obj.__get__(self, type(self)) 313 | return obj 314 | 315 | # TODO(ju256): refactor 316 | def __setattr__(self, attr, value): 317 | # hacky way to check whether we setup the Registers object here with attr iE RegisterDescriptor 318 | # or if this is actually user invoked e.g. registers.rax = 1337 319 | if isinstance(value, RegisterDescriptor): 320 | object.__setattr__(self, attr, value) 321 | else: 322 | if not isinstance(value, int): 323 | raise ValueError(f"Invalid value in register setter: {value}") 324 | obj = object.__getattribute__(self, attr) 325 | if hasattr(obj, '__set__'): 326 | return obj.__set__(self, value) 327 | else: 328 | raise ValueError(f"__set__ not found on {obj}") 329 | 330 | 331 | registers = Registers.create() 332 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /dbgtools/types.py: -------------------------------------------------------------------------------- 1 | ### Code is shit and cursed but works 2 | ### TODO: refactor 3 | 4 | 5 | from dbgtools.memory import read_u8, read_u16, read_bool, read_s32, read_s64, read_u32, read_u64, read_double, read_float, read_string, read_pointer,\ 6 | write_u8, write_u16, write_bool, write_s32, write_s64, write_u32, write_u64, write_double, write_float, write_string, write_pointer 7 | from dbgtools.main import ptr_to_symbol 8 | from dbgtools.functions import SystemVAMD64 9 | 10 | from abc import ABC, abstractmethod 11 | from typing import Optional, Union 12 | import types 13 | 14 | 15 | class FieldPointerType: 16 | def can_be_derefed(self): 17 | return False 18 | 19 | def is_pointer_type(self): 20 | return False 21 | 22 | 23 | class FieldUserDefinedType: 24 | @classmethod 25 | def is_user_defined_type(cls): 26 | return False 27 | 28 | 29 | class FieldCallableType: 30 | @classmethod 31 | def is_callable(cls): 32 | return False 33 | 34 | 35 | 36 | class FieldType(ABC, FieldPointerType, FieldUserDefinedType, FieldCallableType): 37 | FIELD = True 38 | def __init__(self, size: Optional[int] = None): 39 | self._size = size 40 | self._skip_print = False 41 | 42 | def set_skip_print(self): 43 | self._skip_print = True 44 | 45 | def skip_print(self): 46 | return self._skip_print 47 | 48 | def size(self) -> int: 49 | return self._size 50 | 51 | @abstractmethod 52 | def read(self, ptr: int): 53 | ... 54 | 55 | @abstractmethod 56 | def write(self, ptr: int, value): 57 | ... 58 | 59 | def str(self, ptr: int): 60 | return str(self.read(ptr)) 61 | 62 | def is_initialized(self): 63 | return True 64 | 65 | 66 | 67 | class U8Type(FieldType): 68 | def __init__(self): 69 | super().__init__(1) 70 | 71 | def read(self, ptr: int): 72 | return read_u8(ptr) 73 | 74 | def write(self, ptr: int, value): 75 | return write_u8(ptr, value) 76 | 77 | def str(self, ptr: int): 78 | return hex(self.read(ptr)) 79 | 80 | 81 | 82 | class U16Type(FieldType): 83 | def __init__(self): 84 | super().__init__(2) 85 | 86 | def read(self, ptr: int): 87 | return read_u16(ptr) 88 | 89 | def write(self, ptr: int, value): 90 | return write_u16(ptr, value) 91 | 92 | def str(self, ptr: int): 93 | return hex(self.read(ptr)) 94 | 95 | 96 | class BoolType(FieldType): 97 | def __init__(self): 98 | super().__init__(4) 99 | 100 | def read(self, ptr: int): 101 | return read_bool(ptr) 102 | 103 | def write(self, ptr: int, value): 104 | return write_bool(ptr, value) 105 | 106 | 107 | class S32Type(FieldType): 108 | def __init__(self): 109 | super().__init__(4) 110 | 111 | def read(self, ptr: int): 112 | return read_s32(ptr) 113 | 114 | def write(self, ptr: int, value): 115 | return write_s32(ptr, value) 116 | 117 | 118 | class S64Type(FieldType): 119 | def __init__(self): 120 | super().__init__(8) 121 | 122 | def read(self, ptr: int): 123 | return read_s64(ptr) 124 | 125 | def write(self, ptr: int, value): 126 | return write_s64(ptr, value) 127 | 128 | 129 | class U32Type(FieldType): 130 | def __init__(self): 131 | super().__init__(4) 132 | 133 | def read(self, ptr: int): 134 | return read_u32(ptr) 135 | 136 | def write(self, ptr: int, value): 137 | return write_u32(ptr, value) 138 | 139 | def str(self, ptr: int): 140 | return hex(self.read(ptr)) 141 | 142 | 143 | class U64Type(FieldType): 144 | def __init__(self): 145 | super().__init__(8) 146 | 147 | def read(self, ptr: int): 148 | return read_u64(ptr) 149 | 150 | def write(self, ptr: int, value): 151 | return write_u64(ptr, value) 152 | 153 | def str(self, ptr: int): 154 | return hex(self.read(ptr)) 155 | 156 | 157 | class DoubleType(FieldType): 158 | def __init__(self): 159 | super().__init__(8) 160 | 161 | def read(self, ptr: int): 162 | return read_double(ptr) 163 | 164 | def write(self, ptr: int, value): 165 | return write_double(ptr, value) 166 | 167 | 168 | class FloatType(FieldType): 169 | def __init__(self): 170 | super().__init__(4) 171 | 172 | def read(self, ptr: int): 173 | return read_float(ptr) 174 | 175 | def write(self, ptr: int, value): 176 | return write_float(ptr, value) 177 | 178 | 179 | class StringFieldType(FieldType): 180 | def __init__(self, length: Optional[int] = None): 181 | self._length = length 182 | super().__init__(length) 183 | 184 | def read(self, ptr): 185 | return read_string(ptr, self._length) 186 | 187 | def write(self, ptr: int, value): 188 | return write_string(ptr, value, self._length) 189 | 190 | 191 | class StringTypeMeta(type): 192 | def __getitem__(cls, args): 193 | length = args 194 | return lambda: StringFieldType(length) 195 | 196 | 197 | class UserDefinedType: 198 | FIELD = True 199 | @classmethod 200 | def is_user_defined_type(cls): 201 | return True 202 | 203 | def __init__(self, ptr: Optional[int] = None, referenced_user_defined_type_fields: Optional[list["StructFieldInstance"]] = None): 204 | self._ptr = ptr 205 | self._skip_print = False 206 | 207 | if referenced_user_defined_type_fields is None: 208 | referenced_user_defined_type_fields = [] 209 | self._referenced_user_defined_type_fields: list["StructFieldInstance"] = referenced_user_defined_type_fields 210 | 211 | def is_pointer_type(self): 212 | return False 213 | 214 | def can_be_derefed(self): 215 | return False 216 | 217 | @classmethod 218 | def is_callable(cls): 219 | return False 220 | 221 | def initialize_referenced_ud_type(self): 222 | for field in self._referenced_user_defined_type_fields: 223 | field.set_ptr(self.ptr() + field.offset()) 224 | field.initialize_referenced_ud_type() 225 | 226 | def ptr(self): 227 | if self._ptr is not None: 228 | return self._ptr 229 | else: 230 | raise ValueError("ptr needs to be set") 231 | 232 | def set_ptr(self, ptr: Optional[int]): 233 | self._ptr = ptr 234 | 235 | def set_skip_print(self): 236 | self._skip_print = True 237 | 238 | def skip_print(self): 239 | return self._skip_print 240 | 241 | def is_initialized(self): 242 | return self._ptr is not None 243 | 244 | 245 | class StringType(metaclass=StringTypeMeta): 246 | ... 247 | 248 | 249 | class IPointer: 250 | def __init__(self, ref_ptr: int, type: Union[FieldType, UserDefinedType], is_mutable: bool): 251 | self._ref_ptr = ref_ptr 252 | self._type = type 253 | assert self._type.is_pointer_type() 254 | self._is_mutable = is_mutable 255 | 256 | def __call__(self, *args): 257 | # allow call directly on pointer for convenience 258 | if self._type.can_be_derefed(): 259 | sub_type = self._type.deref() 260 | if sub_type.is_callable(): 261 | icallable = sub_type.gen_ifc() 262 | return icallable(*args) 263 | 264 | raise TypeError("Only pointers holding callables can be called") 265 | 266 | def __str__(self): 267 | return f"Pointer[{hex(self._ptr())}]" 268 | 269 | def _mutable(self): 270 | return self._is_mutable 271 | 272 | def __getattribute__(self, attr): 273 | if attr == "data": 274 | return self._data() 275 | elif attr == "ptr": 276 | return self._ptr() 277 | else: 278 | return object.__getattribute__(self, attr) 279 | 280 | def __setattr__(self, attr, value): 281 | if attr == "data": 282 | if not self._mutable(): 283 | raise ValueError("Pointer is not mutable") 284 | 285 | if self._type.can_be_derefed(): 286 | sub_type = self._type.deref() 287 | if sub_type.is_pointer_type(): 288 | raise ValueError("Pointer data cannot be written directly") 289 | elif sub_type.is_user_defined_type(): 290 | raise ValueError("User defined types cannot be written directly behind a pointer") 291 | elif sub_type.is_callable(): 292 | raise ValueError("Callables cannot be written directly behind a pointer") 293 | else: 294 | sub_type.write(self._ptr(), value) 295 | else: 296 | raise ValueError("Can't deref") 297 | elif attr == "ptr": 298 | if not self._mutable(): 299 | raise ValueError("Pointer is not mutable") 300 | 301 | write_pointer(self._ref_ptr, value) 302 | if self._type.can_be_derefed(): 303 | sub_type = self._type.deref() 304 | if sub_type.is_pointer_type() or sub_type.is_user_defined_type() or sub_type.is_callable(): 305 | sub_type.set_ptr(value) 306 | else: 307 | return object.__setattr__(self, attr, value) 308 | 309 | def _data(self): 310 | if self._type.can_be_derefed(): 311 | sub_type = self._type.deref() 312 | if sub_type.is_pointer_type(): 313 | return IPointer(self._ptr(), sub_type, sub_type.mutable()) 314 | elif sub_type.is_user_defined_type(): 315 | return sub_type 316 | elif sub_type.is_callable(): 317 | return sub_type.gen_ifc() 318 | else: 319 | return sub_type.read(self._ptr()) 320 | else: 321 | raise ValueError("Can't deref") 322 | 323 | def _ptr(self): 324 | return read_pointer(self._ref_ptr) 325 | 326 | 327 | class PointerFieldType(FieldType): 328 | def __init__(self, type: Union[FieldType, UserDefinedType], mutable: bool = False): 329 | self._type = type 330 | assert self._type is None or hasattr(self._type, "FIELD") 331 | super().__init__(8) 332 | self._ptr = None 333 | self._mutable = mutable 334 | 335 | def mutable(self): 336 | return self._mutable 337 | 338 | def set_ptr(self, ptr): 339 | d = read_pointer(ptr) 340 | self._ptr = ptr 341 | if self.can_be_derefed() and (self.is_user_defined_type() or self.is_callable()): 342 | self._type.set_ptr(d) 343 | 344 | def is_callable(self): 345 | return self.can_be_derefed() and self._type.is_callable() 346 | 347 | def ptr(self): 348 | assert self._ptr is not None 349 | return self._ptr 350 | 351 | def initialize_referenced_ud_type(self): 352 | if self.is_user_defined_type(): 353 | self._type.initialize_referenced_ud_type() 354 | 355 | def is_pointer_type(self): 356 | return True 357 | 358 | def can_be_derefed(self): 359 | return self._type is not None 360 | 361 | def is_user_defined_type(self): 362 | return self.can_be_derefed() and self._type.is_user_defined_type() 363 | 364 | def read(self): 365 | raise NotImplementedError() 366 | 367 | def deref(self): 368 | assert self.can_be_derefed() 369 | if self._type.is_user_defined_type(): 370 | return self._type 371 | else: 372 | return self._type 373 | 374 | def write(self, value): 375 | raise NotImplementedError() 376 | 377 | def str(self, ptr: int): 378 | if self.can_be_derefed(): 379 | p = read_pointer(ptr) 380 | if not self.is_user_defined_type(): 381 | return f"{hex(p)} -> {self._type.str(p)}" 382 | else: 383 | sstr = self._type.str(p) 384 | out = f"{hex(p)} -> \n" 385 | for l in sstr.splitlines(): 386 | out += " " * 2 + l + "\n" 387 | return out 388 | else: 389 | return hex(read_pointer(self.ptr())) 390 | 391 | def __str__(self): 392 | return f"Pointer[{hex(self.ptr())}]" 393 | 394 | 395 | class PointerTypeMeta(type): 396 | def __getitem__(cls, args): 397 | if args is not None: 398 | type = args() 399 | else: 400 | type = args 401 | return lambda: PointerFieldType(type, False) 402 | 403 | 404 | class PointerType(metaclass=PointerTypeMeta): 405 | ... 406 | 407 | 408 | class MutPointerTypeMeta(type): 409 | def __getitem__(cls, args): 410 | if args is not None: 411 | type = args() 412 | else: 413 | type = args 414 | return lambda: PointerFieldType(type, True) 415 | 416 | 417 | class MutPointerType(metaclass=MutPointerTypeMeta): 418 | ... 419 | 420 | 421 | class ArrayFieldType(FieldType): 422 | def __init__(self, length: int, element_type: FieldType): 423 | self._length = length 424 | self._element_type = element_type 425 | self._element_size = self._element_type.size() 426 | super().__init__(self._length * self._element_size) 427 | 428 | def read(self, ptr): 429 | array = [] 430 | for i in range(self._length): 431 | array.append(self._element_type.read(ptr + (i * self._element_size))) 432 | return array 433 | 434 | def write(self, ptr: int, value): 435 | for i in range(self._length): 436 | self._element_type.write(ptr + (i * self._element_size), value[i]) 437 | 438 | 439 | class ArrayTypeMeta(type): 440 | def __getitem__(cls, args): 441 | if len(args) != 2: 442 | raise ValueError("Expecting element type and length") 443 | else: 444 | element_type = args[0]() 445 | length = args[1] 446 | return lambda: ArrayFieldType(length, element_type) 447 | 448 | 449 | class ArrayType(metaclass=ArrayTypeMeta): 450 | ... 451 | 452 | 453 | VoidPointerType = PointerType[None] 454 | MutVoidPointerType = MutPointerType[None] 455 | 456 | 457 | 458 | class PaddingFieldType(FieldType): 459 | def __init__(self, size: int): 460 | super().__init__(size) 461 | 462 | def skip_print(self): 463 | return True 464 | 465 | def read(self, ptr: int): 466 | raise NotImplementedError() 467 | 468 | def write(self, ptr: int, value): 469 | raise NotImplementedError() 470 | 471 | class PaddingTypeMeta(type): 472 | def __getitem__(cls, size): 473 | return lambda: PaddingFieldType(size) 474 | 475 | 476 | class Padding(metaclass=PaddingTypeMeta): 477 | ... 478 | 479 | Gap = Padding 480 | 481 | 482 | ###################### 483 | 484 | 485 | 486 | class ArrayMeta(type): 487 | def __new__(meta, name, bases, attrs): 488 | if attrs["__qualname__"] != "Array": 489 | assert "__annotations__" in attrs.keys() 490 | annotations = attrs["__annotations__"] 491 | if "length" in annotations.keys(): 492 | length = annotations["length"] 493 | del annotations["length"] 494 | else: 495 | length = None 496 | 497 | assert len(annotations.keys()) == 1 498 | element_type = list(annotations.values())[0] 499 | if element_type.is_callable(): 500 | raise ValueError("Callables cannot be the element type of an array. Wrap the callable in a pointer type") 501 | 502 | attrs["element_type"] = element_type() 503 | attrs["_hidden_length"] = length 504 | del attrs["__annotations__"] 505 | 506 | return type.__new__(meta, name, bases, attrs) 507 | 508 | 509 | # FIXME: something still of with array not initializing their element if its a struct 510 | class Array(UserDefinedType, metaclass=ArrayMeta): 511 | def __init__(self, ptr: Optional[int] = None, length: Optional[int] = None): 512 | ref_type = None 513 | pointer_type = None 514 | callable_type = None 515 | if self._element_type().is_pointer_type(): 516 | pointer_type = self._element_type() 517 | elif self._element_type().is_user_defined_type(): 518 | ref_type = self._element_type() 519 | elif self._element_type().is_callable(): 520 | raise ValueError("Callables cannot be the element type of an array. Wrap the callable in a pointer type") 521 | 522 | ref_types = [ref_type] if ref_type is not None else [] 523 | super().__init__(ptr, ref_types) 524 | self._length = length 525 | 526 | if self.is_initialized(): 527 | if ref_type is not None: 528 | ref_type.set_ptr(self.ptr()) 529 | if pointer_type is not None: 530 | pointer_type.set_ptr(self.ptr()) 531 | if callable_type: 532 | callable_type.set_ptr(self.ptr()) 533 | 534 | def set_length(self, length: int): 535 | self._length = length 536 | 537 | def _get_hidden_length(self): 538 | return object.__getattribute__(self, "_hidden_length") 539 | 540 | def length(self): 541 | if self._length is not None: 542 | return self._length 543 | elif self._get_hidden_length() is not None: 544 | return self._get_hidden_length() 545 | else: 546 | raise ValueError("size not specified") 547 | 548 | def _element_type(self): 549 | return object.__getattribute__(self, "element_type") 550 | 551 | def element_size(self): 552 | return self._element_type().size() 553 | 554 | def size(self): 555 | return self.element_size() * self.length() 556 | 557 | def get(self, idx: int): 558 | element_ptr = self.ptr() + (idx * self.element_size()) 559 | if self._element_type().is_pointer_type(): 560 | field = self._element_type() 561 | return IPointer(element_ptr, field, field.mutable()) 562 | elif self._element_type().is_user_defined_type(): 563 | field = self._element_type() 564 | field.set_ptr(element_ptr) 565 | return field 566 | elif self._element_type().is_callable(): 567 | assert 0 568 | else: 569 | return self._element_type().read(element_ptr) 570 | 571 | def get_str(self, idx: int): 572 | element_ptr = self.ptr() + (idx * self.element_size()) 573 | if self._element_type().is_pointer_type(): 574 | field = self._element_type() 575 | return str(IPointer(element_ptr, field, field.mutable())) 576 | elif self._element_type().is_user_defined_type(): 577 | field = self._element_type() 578 | field.set_ptr(element_ptr) 579 | return field.str(element_ptr) 580 | elif self._element_type().is_callable(): 581 | assert 0 582 | else: 583 | return self._element_type().str(element_ptr) 584 | 585 | 586 | def set(self, idx: int, value): 587 | assert not self._element_type().is_user_defined_type() and not self._element_type().is_pointer_type() and not self._element_type().is_callable() 588 | return self._element_type().write(self.ptr() + (idx * self.element_size()), value) 589 | 590 | def items(self) -> list: 591 | return list(map(self.get, list(range(self.length())))) 592 | 593 | def str(self, ptr): 594 | self.set_ptr(ptr) 595 | if self._element_type().is_user_defined_type(): 596 | field = self._element_type() 597 | field.set_ptr(ptr) 598 | return str(self) 599 | 600 | def __str__(self): 601 | l = list(map(self.get_str, list(range(0, min(self.length(), 20))))) 602 | if self.length() > 20: 603 | l.append("...") 604 | return str(l) 605 | 606 | def __getitem__(self, idx): 607 | return self.get(idx) 608 | 609 | def __setitem__(self, idx, value): 610 | return self.set(idx, value) 611 | 612 | 613 | 614 | class ICallable: 615 | def __init__(self, param_count: int, func_ptr: int): 616 | self._param_count = param_count 617 | self._func_ptr = func_ptr 618 | 619 | def str(self, ptr: int): 620 | return str(self) 621 | 622 | def __str__(self): 623 | symbol = ptr_to_symbol(self._func_ptr) 624 | if symbol == "": 625 | target_str = hex(self._func_ptr) 626 | else: 627 | target_str = f"{hex(self._func_ptr)} <{symbol}>" 628 | return f"Callable({target_str})({', '.join(['_']*self._param_count)})" 629 | 630 | def __call__(self, *args): 631 | if len(args) != self._param_count: 632 | raise ValueError(f"Invalid amount of parameters: Expected {self._param_count} got {len(args)}") 633 | 634 | if len(args) > 6: 635 | raise ValueError("Only supporting registers call arguments for now") 636 | 637 | # TODO(ju256): support more calling conventions 638 | return SystemVAMD64(*args).call(self._func_ptr) 639 | 640 | 641 | class CallableFieldType(FieldType): 642 | def __init__(self, param_count: int, func_ptr: Optional[int] = None): 643 | self._param_count = param_count 644 | 645 | self._func_ptr = func_ptr 646 | 647 | def set_ptr(self, func_ptr: int): 648 | self._func_ptr = func_ptr 649 | 650 | def ptr(self): 651 | assert self._func_ptr is not None 652 | return self._func_ptr 653 | 654 | def gen_ifc(self): 655 | return ICallable(self._param_count, self.ptr()) 656 | 657 | def __str__(self): 658 | return str(self.gen_ifc()) 659 | 660 | def str(self, ptr): 661 | return str(self) 662 | 663 | def size(self): 664 | return 0 665 | 666 | def read(self, ptr: int): 667 | raise NotImplementedError("Callables cannot be read from or written to") 668 | 669 | def write(self, ptr: int): 670 | raise NotImplementedError("Callables cannot be read from or written to") 671 | 672 | @classmethod 673 | def is_callable(cls): 674 | return True 675 | 676 | 677 | # TODO: implement different calling conventions (or atleast easy interface) 678 | class CallableMeta(type): 679 | def __getitem__(cls, args): 680 | param_count = args 681 | return lambda: CallableFieldType(param_count) 682 | 683 | 684 | 685 | class Callable(metaclass=CallableMeta): 686 | ... 687 | 688 | 689 | 690 | 691 | ###################### 692 | 693 | 694 | class StructFieldInstance: 695 | def __init__(self, offset: int, field: Union[FieldType, UserDefinedType]): 696 | assert hasattr(field, "FIELD") 697 | self._offset = offset 698 | self._field = field 699 | 700 | def mutable(self): 701 | return self._field.mutable() 702 | 703 | def skip_print(self): 704 | return self._field.skip_print() 705 | 706 | def set_skip_print(self): 707 | self._field.set_skip_print() 708 | 709 | def offset(self): 710 | return self._offset 711 | 712 | def set_offset(self, offset: int): 713 | self._offset = offset 714 | 715 | def read(self, ptr: int): 716 | return self._field.read(ptr + self._offset) 717 | 718 | def write(self, ptr: int, value): 719 | return self._field.write(ptr + self._offset, value) 720 | 721 | def size(self) -> int: 722 | return self._field.size() 723 | 724 | def str(self, ptr: int) -> str: 725 | return self._field.str(ptr + self._offset) 726 | 727 | def is_user_defined_type(self) -> bool: 728 | return self._field.is_user_defined_type() 729 | 730 | def ptr(self) -> int: 731 | assert self.is_user_defined_type() 732 | return self._field.ptr() 733 | 734 | def set_ptr(self, ptr: int) -> int: 735 | assert self.is_user_defined_type() or self.is_pointer_type() 736 | self._field.set_ptr(ptr) 737 | if self.is_user_defined_type() or self.is_pointer_type(): 738 | self.initialize_referenced_ud_type() 739 | 740 | def initialize_referenced_ud_type(self): 741 | assert self.is_user_defined_type() or self.is_pointer_type() 742 | self._field.initialize_referenced_ud_type() 743 | 744 | def field(self): 745 | return self._field 746 | 747 | def is_initialized(self): 748 | return self._field.is_initialized() 749 | 750 | def can_be_derefed(self): 751 | return self._field.can_be_derefed() 752 | 753 | def is_callable(self): 754 | raise NotImplementedError("Struct fields should never contain callables directly") 755 | 756 | def deref(self, ptr: int): 757 | assert self.can_be_derefed() 758 | return self._field.deref() 759 | 760 | def is_pointer_type(self): 761 | return self._field.is_pointer_type() 762 | 763 | 764 | 765 | class StructFieldMeta(type): 766 | def __getitem__(cls, args): 767 | if type(args) == tuple and len(args) == 2: 768 | offset = args[0] 769 | field_type = args[1] 770 | elif type(args) != tuple: 771 | offset = None 772 | field_type = args 773 | else: 774 | raise ValueError("Expecting (offset and) field_type") 775 | 776 | field_type = field_type() 777 | assert hasattr(field_type, "FIELD") 778 | field = field_type 779 | return StructFieldInstance(offset, field) 780 | 781 | 782 | class StructField(metaclass=StructFieldMeta): 783 | pass 784 | 785 | 786 | 787 | class StructMeta(type): 788 | def __new__(meta, name, bases, attrs): 789 | field_names = [] 790 | if attrs["__qualname__"] != "Struct": 791 | assert "__annotations__" in attrs.keys() 792 | annotations = attrs["__annotations__"] 793 | user_defined_types = [] 794 | cur_position = 0 795 | 796 | if "_size" in annotations.keys(): 797 | struct_type_size = annotations["_size"] 798 | del annotations["_size"] 799 | attrs["_hidden_type_size"] = struct_type_size 800 | 801 | for field_name in annotations.keys(): 802 | field = annotations[field_name] 803 | # if field.is_user_defined_type(): 804 | # we need to setup the pointer somehow 805 | # raise NotImplementedError() 806 | if not hasattr(field, "_offset"): 807 | field = StructFieldInstance(cur_position, field()) 808 | cur_position += field.size() 809 | else: 810 | assert field.offset() >= cur_position 811 | cur_position = field.offset() + field.size() 812 | if field_name.startswith("_"): 813 | field.set_skip_print() 814 | 815 | attrs[field_name] = field 816 | field_names.append(field_name) 817 | 818 | attrs["_field_names"] = field_names 819 | del attrs["__annotations__"] 820 | 821 | return type.__new__(meta, name, bases, attrs) 822 | 823 | 824 | class Struct(UserDefinedType, metaclass=StructMeta): 825 | def __init__(self, ptr: Optional[int] = None): 826 | ref_types = [] 827 | pointer_types = [] 828 | for field_name in self._field_names: 829 | field = self._get_raw_field(field_name) 830 | if field.is_user_defined_type(): 831 | ref_types.append(field) 832 | elif field.is_pointer_type(): 833 | pointer_types.append(field) 834 | 835 | super().__init__(ptr, ref_types) 836 | 837 | if self.is_initialized(): 838 | for field in ref_types: 839 | field.set_ptr(self.ptr() + field.offset()) 840 | 841 | for field in pointer_types: 842 | field.set_ptr(self.ptr() + field.offset()) 843 | 844 | def str(self, ptr: int): 845 | return str(self) 846 | 847 | def __str__(self): 848 | out = "" 849 | for field_name in self._field_names: 850 | field = self._get_raw_field(field_name) 851 | prefix = f"({field_name}) {hex(field.offset())}|" 852 | if not field.skip_print() and not field.is_user_defined_type(): 853 | out += f"{prefix} {field.str(self.ptr())}\n" 854 | elif field.is_user_defined_type(): 855 | out += f"{prefix}\n" 856 | sstr = field.str(self.ptr()) 857 | for l in sstr.splitlines(): 858 | out += (" " * 2) + l + "\n" 859 | 860 | return out 861 | 862 | def _get_raw_field(self, field_name: str): 863 | return object.__getattribute__(self, field_name) 864 | 865 | def __getattribute__(self, attr): 866 | if attr == "_field_names": 867 | return object.__getattribute__(self, attr) 868 | else: 869 | if attr in self._field_names: 870 | field = self._get_raw_field(attr) 871 | if field.is_pointer_type(): 872 | return IPointer(self.ptr() + field.offset(), field.field(), field.mutable()) 873 | elif field.is_user_defined_type(): 874 | return field.field() 875 | else: 876 | return field.read(self.ptr()) 877 | else: 878 | return object.__getattribute__(self, attr) 879 | 880 | def __setattr__(self, attr, value): 881 | if attr in self._field_names: 882 | field = self._get_raw_field(attr) 883 | assert not field.is_user_defined_type() and not field.is_pointer_type() 884 | return field.write(self.ptr(), value) 885 | else: 886 | return object.__setattr__(self, attr, value) 887 | 888 | def _get_hidden_struct_size(self) -> Optional[int]: 889 | if hasattr(self, "_hidden_type_size"): 890 | return object.__getattribute__(self, "_hidden_type_size") 891 | else: 892 | return None 893 | 894 | def size(self): 895 | sz = 0 896 | 897 | hidden_struct_size = self._get_hidden_struct_size() 898 | if hidden_struct_size is not None: 899 | sz = hidden_struct_size 900 | else: 901 | for field_name in self._field_names: 902 | sz += self._get_raw_field(field_name).size() 903 | return sz 904 | 905 | 906 | 907 | class UnionMeta(type): 908 | def __new__(meta, name, bases, attrs): 909 | field_names = [] 910 | if attrs["__qualname__"] != "Union": 911 | assert "__annotations__" in attrs.keys() 912 | annotations = attrs["__annotations__"] 913 | user_defined_types = [] 914 | 915 | for field_name in annotations.keys(): 916 | field = annotations[field_name] 917 | field = field() 918 | attrs[field_name] = field 919 | field_names.append(field_name) 920 | 921 | attrs["_field_names"] = field_names 922 | del attrs["__annotations__"] 923 | 924 | return type.__new__(meta, name, bases, attrs) 925 | 926 | 927 | class Union(UserDefinedType, metaclass=UnionMeta): 928 | def __init__(self, ptr: Optional[int] = None): 929 | ref_types = [] 930 | pointer_types = [] 931 | for field_name in self._field_names: 932 | field = self._get_raw_field(field_name) 933 | if field.is_user_defined_type(): 934 | ref_types.append(field) 935 | elif field.is_pointer_type(): 936 | pointer_types.append(field) 937 | elif field.is_callable(): 938 | raise ValueError("Callables cannot be fields in a struct. Wrap the callable in a pointer type") 939 | 940 | super().__init__(ptr, ref_types) 941 | 942 | if self.is_initialized(): 943 | for field in ref_types + pointer_types: 944 | field.set_ptr(self.ptr()) 945 | 946 | def str(self, ptr: int): 947 | return str(self) 948 | 949 | def __str__(self): 950 | out = "" 951 | for field_name in self._field_names: 952 | field = self._get_raw_field(field_name) 953 | prefix = f"{field_name}|" 954 | assert not field.skip_print() 955 | if not field.is_user_defined_type(): 956 | out += f"{prefix} {field.str(self.ptr())}\n" 957 | else: 958 | assert field.is_user_defined_type() 959 | out += f"{prefix}\n" 960 | sstr = field.str(self.ptr()) 961 | for l in sstr.splitlines(): 962 | out += (" " * 2) + l + "\n" 963 | 964 | return out 965 | 966 | def _get_raw_field(self, field_name: str): 967 | return object.__getattribute__(self, field_name) 968 | 969 | def __getattribute__(self, attr): 970 | if attr == "_field_names": 971 | return object.__getattribute__(self, attr) 972 | else: 973 | if attr in self._field_names: 974 | field = self._get_raw_field(attr) 975 | if field.is_pointer_type(): 976 | return IPointer(self.ptr(), field, field.mutable()) 977 | elif field.is_user_defined_type(): 978 | return field 979 | elif field.is_callable(): 980 | assert 0 981 | else: 982 | return field.read(self.ptr()) 983 | else: 984 | return object.__getattribute__(self, attr) 985 | 986 | def __setattr__(self, attr, value): 987 | if attr in self._field_names: 988 | field = self._get_raw_field(attr) 989 | assert not field.is_user_defined_type() and not field.is_pointer_type() 990 | return field.write(self.ptr(), value) 991 | else: 992 | return object.__setattr__(self, attr, value) 993 | 994 | def size(self): 995 | szs = [] 996 | 997 | for field_name in self._field_names: 998 | szs.append(self._get_raw_field(field_name).size()) 999 | return max(szs) 1000 | 1001 | 1002 | 1003 | 1004 | class FunctionPtrMeta(type): 1005 | def __getitem__(cls, args): 1006 | return PointerType[Callable[args]] 1007 | 1008 | 1009 | class FunctionPtr(metaclass=FunctionPtrMeta): 1010 | pass 1011 | --------------------------------------------------------------------------------