mirror of https://github.com/pwndbg/pwndbg.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
420 lines
11 KiB
Python
420 lines
11 KiB
Python
"""
|
|
Reading, writing, and describing memory.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Dict
|
|
from typing import Union
|
|
|
|
import gdb
|
|
|
|
import pwndbg.gdblib.arch
|
|
import pwndbg.gdblib.events
|
|
import pwndbg.gdblib.qemu
|
|
import pwndbg.gdblib.typeinfo
|
|
import pwndbg.lib.cache
|
|
import pwndbg.lib.memory
|
|
from pwndbg.lib.memory import PAGE_MASK
|
|
from pwndbg.lib.memory import PAGE_SIZE
|
|
|
|
GdbDict = Dict[str, Union["GdbDict", int]]
|
|
|
|
|
|
MMAP_MIN_ADDR = 0x8000
|
|
|
|
|
|
def read(addr: int, count: int, partial: bool = False) -> bytearray:
|
|
"""read(addr, count, partial=False) -> bytearray
|
|
|
|
Read memory from the program being debugged.
|
|
|
|
Arguments:
|
|
addr(int): Address to read
|
|
count(int): Number of bytes to read
|
|
partial(bool): Whether less than ``count`` bytes can be returned
|
|
|
|
Returns:
|
|
:class:`bytearray`: The memory at the specified address,
|
|
or ``None``.
|
|
"""
|
|
result = b""
|
|
count = max(int(count), 0)
|
|
|
|
try:
|
|
result = gdb.selected_inferior().read_memory(addr, count)
|
|
except gdb.error as e:
|
|
if not partial:
|
|
raise
|
|
|
|
message = str(e)
|
|
|
|
stop_addr = addr
|
|
match = re.search(r"Memory at address (\w+) unavailable\.", message)
|
|
if match:
|
|
stop_addr = int(match.group(1), 0)
|
|
else:
|
|
stop_addr = int(message.split()[-1], 0)
|
|
|
|
if stop_addr != addr:
|
|
return read(addr, stop_addr - addr)
|
|
|
|
# QEMU will return the start address as the failed
|
|
# read address. Try moving back a few pages at a time.
|
|
stop_addr = addr + count
|
|
|
|
# Move the stop address down to the previous page boundary
|
|
stop_addr &= PAGE_MASK
|
|
while stop_addr > addr:
|
|
result = read(addr, stop_addr - addr)
|
|
|
|
if result:
|
|
return result
|
|
|
|
# Move down by another page
|
|
stop_addr -= PAGE_SIZE
|
|
|
|
return bytearray(result)
|
|
|
|
|
|
def readtype(gdb_type: gdb.Type, addr: int) -> int:
|
|
"""readtype(gdb_type, addr) -> int
|
|
|
|
Reads an integer-type (e.g. ``uint64``) and returns a Python
|
|
native integer representation of the same.
|
|
|
|
Arguments:
|
|
gdb_type(gdb.Type): GDB type to read
|
|
addr(int): Address at which the value to be read resides
|
|
|
|
Returns:
|
|
:class:`int`
|
|
"""
|
|
return int(gdb.Value(addr).cast(gdb_type.pointer()).dereference())
|
|
|
|
|
|
def write(addr: int, data: str | bytes | bytearray) -> None:
|
|
"""write(addr, data)
|
|
|
|
Writes data into the memory of the process being debugged.
|
|
|
|
Arguments:
|
|
addr(int): Address to write
|
|
data(str,bytes,bytearray): Data to write
|
|
"""
|
|
if isinstance(data, str):
|
|
data = bytes(data, "utf8")
|
|
|
|
# Throws an exception if can't access memory
|
|
gdb.selected_inferior().write_memory(addr, data)
|
|
|
|
|
|
def peek(address: int) -> str | None:
|
|
"""peek(address) -> str
|
|
|
|
Read one byte from the specified address.
|
|
|
|
Arguments:
|
|
address(int): Address to read
|
|
|
|
Returns:
|
|
:class:`str`: A single byte of data, or ``None`` if the
|
|
address cannot be read.
|
|
"""
|
|
try:
|
|
return chr(read(address, 1)[0])
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
@pwndbg.lib.cache.cache_until("stop")
|
|
def is_readable_address(address: int) -> bool:
|
|
"""is_readable_address(address) -> bool
|
|
|
|
Check if the address can be read by GDB.
|
|
|
|
Arguments:
|
|
address(int): Address to read
|
|
|
|
Returns:
|
|
:class:`bool`: Whether the address is readable.
|
|
"""
|
|
# We use vmmap to check before `peek()` because accessing memory for embedded targets might be slow and expensive.
|
|
return pwndbg.gdblib.vmmap.find(address) is not None and peek(address) is not None
|
|
|
|
|
|
def poke(address: int) -> bool:
|
|
"""poke(address)
|
|
|
|
Checks whether an address is writable.
|
|
|
|
Arguments:
|
|
address(int): Address to check
|
|
|
|
Returns:
|
|
:class:`bool`: Whether the address is writable.
|
|
"""
|
|
c = peek(address)
|
|
if c is None:
|
|
return False
|
|
try:
|
|
write(address, c)
|
|
except Exception:
|
|
return False
|
|
return True
|
|
|
|
|
|
def string(addr: int, max: int = 4096) -> bytearray:
|
|
"""Reads a null-terminated string from memory.
|
|
|
|
Arguments:
|
|
addr(int): Address to read from
|
|
max(int): Maximum string length (default 4096)
|
|
|
|
Returns:
|
|
An empty bytearray, or a NULL-terminated bytearray.
|
|
"""
|
|
if peek(addr):
|
|
data = read(addr, max, partial=True)
|
|
|
|
try:
|
|
return data[: data.index(b"\x00")]
|
|
except ValueError:
|
|
pass
|
|
|
|
return bytearray()
|
|
|
|
|
|
def byte(addr: int) -> int:
|
|
"""byte(addr) -> int
|
|
|
|
Read one byte at the specified address
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uchar, addr)
|
|
|
|
|
|
def uchar(addr: int) -> int:
|
|
"""uchar(addr) -> int
|
|
|
|
Read one ``unsigned char`` at the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uchar, addr)
|
|
|
|
|
|
def ushort(addr: int) -> int:
|
|
"""ushort(addr) -> int
|
|
|
|
Read one ``unisgned short`` at the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.ushort, addr)
|
|
|
|
|
|
def uint(addr: int) -> int:
|
|
"""uint(addr) -> int
|
|
|
|
Read one ``unsigned int`` at the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uint, addr)
|
|
|
|
|
|
def pvoid(addr: int) -> int:
|
|
"""pvoid(addr) -> int
|
|
|
|
Read one pointer from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.pvoid, addr)
|
|
|
|
|
|
def u8(addr: int) -> int:
|
|
"""u8(addr) -> int
|
|
|
|
Read one ``uint8_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uint8, addr)
|
|
|
|
|
|
def u16(addr: int) -> int:
|
|
"""u16(addr) -> int
|
|
|
|
Read one ``uint16_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uint16, addr)
|
|
|
|
|
|
def u32(addr: int) -> int:
|
|
"""u32(addr) -> int
|
|
|
|
Read one ``uint32_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uint32, addr)
|
|
|
|
|
|
def u64(addr: int) -> int:
|
|
"""u64(addr) -> int
|
|
|
|
Read one ``uint64_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.uint64, addr)
|
|
|
|
|
|
def u(addr: int, size: int | None = None) -> int:
|
|
"""u(addr, size=None) -> int
|
|
|
|
Read one ``unsigned`` integer from the specified address,
|
|
with the bit-width specified by ``size``, which defaults
|
|
to the pointer width.
|
|
"""
|
|
if size is None:
|
|
size = pwndbg.gdblib.arch.ptrsize * 8
|
|
return {8: u8, 16: u16, 32: u32, 64: u64}[size](addr)
|
|
|
|
|
|
def s8(addr: int) -> int:
|
|
"""s8(addr) -> int
|
|
|
|
Read one ``int8_t`` from the specified address
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.int8, addr)
|
|
|
|
|
|
def s16(addr: int) -> int:
|
|
"""s16(addr) -> int
|
|
|
|
Read one ``int16_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.int16, addr)
|
|
|
|
|
|
def s32(addr: int) -> int:
|
|
"""s32(addr) -> int
|
|
|
|
Read one ``int32_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.int32, addr)
|
|
|
|
|
|
def s64(addr: int) -> int:
|
|
"""s64(addr) -> int
|
|
|
|
Read one ``int64_t`` from the specified address.
|
|
"""
|
|
return readtype(pwndbg.gdblib.typeinfo.int64, addr)
|
|
|
|
|
|
# TODO: `readtype` is just `int(poi(type, addr))`
|
|
def poi(type: gdb.Type, addr: int | gdb.Value) -> gdb.Value:
|
|
"""poi(addr) -> gdb.Value
|
|
|
|
Read one ``gdb.Type`` object at the specified address.
|
|
"""
|
|
return gdb.Value(addr).cast(type.pointer()).dereference()
|
|
|
|
|
|
@pwndbg.lib.cache.cache_until("stop")
|
|
def find_upper_boundary(addr: int, max_pages: int = 1024) -> int:
|
|
"""find_upper_boundary(addr, max_pages=1024) -> int
|
|
|
|
Brute-force search the upper boundary of a memory mapping,
|
|
by reading the first byte of each page, until an unmapped
|
|
page is found.
|
|
"""
|
|
addr = pwndbg.lib.memory.page_align(int(addr))
|
|
try:
|
|
for i in range(max_pages):
|
|
pwndbg.gdblib.memory.read(addr, 1)
|
|
# import sys
|
|
# sys.stdout.write(hex(addr) + '\n')
|
|
addr += PAGE_SIZE
|
|
|
|
# Sanity check in case a custom GDB server/stub
|
|
# incorrectly returns a result from read
|
|
# (this is most likely redundant, but its ok to keep it?)
|
|
if addr > pwndbg.gdblib.arch.ptrmask:
|
|
return pwndbg.gdblib.arch.ptrmask
|
|
except gdb.MemoryError:
|
|
pass
|
|
return addr
|
|
|
|
|
|
@pwndbg.lib.cache.cache_until("stop")
|
|
def find_lower_boundary(addr: int, max_pages: int = 1024) -> int:
|
|
"""find_lower_boundary(addr, max_pages=1024) -> int
|
|
|
|
Brute-force search the lower boundary of a memory mapping,
|
|
by reading the first byte of each page, until an unmapped
|
|
page is found.
|
|
"""
|
|
addr = pwndbg.lib.memory.page_align(int(addr))
|
|
try:
|
|
for _ in range(max_pages):
|
|
pwndbg.gdblib.memory.read(addr, 1)
|
|
addr -= PAGE_SIZE
|
|
|
|
# Sanity check (see comment in find_upper_boundary)
|
|
if addr < 0:
|
|
return 0
|
|
|
|
except gdb.MemoryError:
|
|
addr += PAGE_SIZE
|
|
return addr
|
|
|
|
|
|
def update_min_addr() -> None:
|
|
global MMAP_MIN_ADDR
|
|
if pwndbg.gdblib.qemu.is_qemu_kernel():
|
|
MMAP_MIN_ADDR = 0
|
|
|
|
|
|
def fetch_struct_as_dictionary(
|
|
struct_name: str,
|
|
struct_address: int,
|
|
include_only_fields: set[str] = set(),
|
|
exclude_fields: set[str] = set(),
|
|
) -> GdbDict:
|
|
struct_type = gdb.lookup_type("struct " + struct_name)
|
|
fetched_struct = poi(struct_type, struct_address)
|
|
|
|
return pack_struct_into_dictionary(fetched_struct, include_only_fields, exclude_fields)
|
|
|
|
|
|
def pack_struct_into_dictionary(
|
|
fetched_struct: gdb.Value,
|
|
include_only_fields: set[str] = set(),
|
|
exclude_fields: set[str] = set(),
|
|
) -> GdbDict:
|
|
struct_as_dictionary = {}
|
|
|
|
if len(include_only_fields) != 0:
|
|
for field_name in include_only_fields:
|
|
key = field_name
|
|
value = convert_gdb_value_to_python_value(fetched_struct[field_name])
|
|
struct_as_dictionary[key] = value
|
|
else:
|
|
for field in fetched_struct.type.fields():
|
|
if field.name is None:
|
|
# Flatten anonymous structs/unions
|
|
anon_type = convert_gdb_value_to_python_value(fetched_struct[field])
|
|
assert isinstance(anon_type, dict)
|
|
struct_as_dictionary.update(anon_type)
|
|
elif field.name not in exclude_fields:
|
|
key = field.name
|
|
value = convert_gdb_value_to_python_value(fetched_struct[field])
|
|
struct_as_dictionary[key] = value
|
|
|
|
return struct_as_dictionary
|
|
|
|
|
|
def convert_gdb_value_to_python_value(gdb_value: gdb.Value) -> int | GdbDict:
|
|
gdb_type = gdb_value.type.strip_typedefs()
|
|
|
|
if gdb_type.code == gdb.TYPE_CODE_PTR:
|
|
return int(gdb_value)
|
|
elif gdb_type.code == gdb.TYPE_CODE_INT:
|
|
return int(gdb_value)
|
|
elif gdb_type.code == gdb.TYPE_CODE_STRUCT:
|
|
return pack_struct_into_dictionary(gdb_value)
|
|
|
|
raise NotImplementedError
|