Files
peace-maker fb35a123b4 Fix some global type hint errors (#2736)
* Fix some global type hint errors

Errors in global scope should be fixed before we're enabling strict mode in mypy and tackle the local problems.

* Remove pwnlib.util.pairwise since it's in itertools now

pairwise was added to itertools itself in 3.10.
https://docs.python.org/3/library/itertools.html#itertools.pairwise
2026-05-28 17:16:27 +02:00

637 lines
19 KiB
Python

import ctypes
import functools
import string
from pwnlib.context import context
from pwnlib.log import getLogger
from pwnlib.util.packing import p8, pack, unpack
log = getLogger(__name__)
__all__ = ['MemLeak', 'RelativeMemLeak']
class MemLeak:
r"""MemLeak is a caching and heuristic tool for exploiting memory leaks.
It can be used as a decorator, around functions of the form:
def some_leaker(addr):
...
return data_as_string_or_None
It will cache leaked memory (which requires either non-randomized static
data or a continouous session). If required, dynamic or known data can be
set with the set-functions, but this is usually not required. If a byte
cannot be recovered, it will try to leak nearby bytes in the hope that the
byte is recovered as a side-effect.
Arguments:
f (function): The leaker function.
search_range (int): How many bytes to search backwards in case an address does not work.
reraise (bool): Whether to reraise call :func:`pwnlib.log.warning` in case the leaker function throws an exception.
Example:
.. doctest::
:options: +POSIX +TODO
>>> import pwnlib
>>> binsh = pwnlib.util.misc.read('/bin/sh')
>>> @pwnlib.memleak.MemLeak
... def leaker(addr):
... print("leaking 0x%x" % addr)
... return binsh[addr:addr+4]
>>> leaker.s(0)[:4]
leaking 0x0
leaking 0x4
b'\x7fELF'
>>> leaker[:4]
b'\x7fELF'
>>> hex(leaker.d(0))
'0x464c457f'
>>> hex(leaker.clearb(1))
'0x45'
>>> hex(leaker.d(0))
leaking 0x1
'0x464c457f'
>>> @pwnlib.memleak.MemLeak
... def leaker_nonulls(addr):
... print("leaking 0x%x" % addr)
... if addr & 0xff == 0:
... return None
... return binsh[addr:addr+4]
>>> leaker_nonulls.d(0) is None
leaking 0x0
True
>>> leaker_nonulls[0x100:0x104] == binsh[0x100:0x104]
leaking 0x100
leaking 0xff
leaking 0x103
True
>>> memory = {-4+i: c.encode() for i,c in enumerate('wxyzABCDE')}
>>> def relative_leak(index):
... return memory.get(index, None)
>>> leak = pwnlib.memleak.MemLeak(relative_leak, relative = True)
>>> leak[-1:2]
b'zAB'
"""
def __init__(self, f, search_range = 20, reraise = True, relative = False):
self.leak = f
self.search_range = search_range
self.reraise = reraise
self.relative = relative
# Map of address: byte for all bytes received
self.cache = {}
functools.update_wrapper(self, f)
def __repr__(self):
return "%s.%s(%r, search_range=%i, reraise=%s)" % (
self.__class__.__module__,
self.__class__.__name__,
self.leak,
self.search_range,
self.reraise
)
def __call__(self, *a, **kw):
return self.leak(*a, **kw)
def struct(self, address, struct):
"""struct(address, struct) => structure object
Leak an entire structure.
Arguments:
address(int): Addess of structure in memory
struct(class): A ctypes structure to be instantiated with leaked data
Return Value:
An instance of the provided struct class, with the leaked data decoded
Examples:
>>> @pwnlib.memleak.MemLeak
... def leaker(addr):
... return b"A"
>>> e = leaker.struct(0, pwnlib.elf.Elf32_Phdr)
>>> hex(e.p_paddr)
'0x41414141'
"""
size = ctypes.sizeof(struct)
data = self.n(address, size)
obj = struct.from_buffer_copy(data)
return obj
def field(self, address, obj):
"""field(address, field) => a structure field.
Leak a field from a structure.
Arguments:
address(int): Base address to calculate offsets from
field(obj): Instance of a ctypes field
Return Value:
The type of the return value will be dictated by
the type of ``field``.
"""
size = obj.size
offset = obj.offset
data = self.n(address + offset, size)
if not data:
return None
return unpack(data, size*8)
def field_compare(self, address, obj, expected):
"""field_compare(address, field, expected) ==> bool
Leak a field from a structure, with an expected value.
As soon as any mismatch is found, stop leaking the structure.
Arguments:
address(int): Base address to calculate offsets from
field(obj): Instance of a ctypes field
expected(int,bytes): Expected value
Return Value:
The type of the return value will be dictated by
the type of ``field``.
"""
if isinstance(expected, int):
expected = pack(expected, bytes=obj.size)
elif not isinstance(expected, bytes):
raise TypeError("Expected value must be an int or bytes")
assert obj.size == len(expected)
return self.compare(address + obj.offset, expected)
def _leak(self, addr, n, recurse=True):
"""_leak(addr, n) => str
Leak ``n`` consecutive bytes starting at ``addr``.
Returns:
A string of length ``n``, or :const:`None`.
"""
if not self.relative and addr < 0:
return None
addresses = [addr+i for i in range(n)]
for address in addresses:
# Cache hit
if address in self.cache:
continue
# Cache miss, get the data from the leaker
data = None
try:
data = self.leak(address)
except Exception as e:
if self.reraise:
raise
if data:
for i,byte in enumerate(bytearray(data)):
self.cache[address+i] = p8(byte, endian='little', signed=False)
# We could not leak this particular byte, search backwards
# to see if another request will satisfy it
elif recurse:
for i in range(1, self.search_range):
data = self._leak(address-i, i+1, False)
if address in self.cache:
break
else:
return None
# Ensure everything is in the cache
if not all(a in self.cache for a in addresses):
return None
# Cache is filled, satisfy the request
return b''.join(self.cache[addr+i] for i in range(n))
def raw(self, addr, numb):
"""raw(addr, numb) -> list
Leak `numb` bytes at `addr`"""
return [self._leak(a, 1) for a in range(addr, addr+numb)]
def _b(self, addr, ndx, size):
addr += ndx * size
data = self._leak(addr, size)
if not data:
return None
return unpack(data, 8*size)
def b(self, addr, ndx = 0):
"""b(addr, ndx = 0) -> int
Leak byte at ``((uint8_t*) addr)[ndx]``
Examples:
>>> import string
>>> data = string.ascii_lowercase.encode()
>>> l = MemLeak(lambda a: data[a:a+2], reraise=False)
>>> l.b(0) == ord('a')
True
>>> l.b(25) == ord('z')
True
>>> l.b(26) is None
True
"""
return self._b(addr, ndx, 1)
def w(self, addr, ndx = 0):
"""w(addr, ndx = 0) -> int
Leak word at ``((uint16_t*) addr)[ndx]``
Examples:
>>> import string
>>> data = string.ascii_lowercase.encode()
>>> l = MemLeak(lambda a: data[a:a+4], reraise=False)
>>> l.w(0) == unpack(b'ab', 16)
True
>>> l.w(24) == unpack(b'yz', 16)
True
>>> l.w(25) is None
True
"""
return self._b(addr, ndx, 2)
def d(self, addr, ndx = 0):
"""d(addr, ndx = 0) -> int
Leak dword at ``((uint32_t*) addr)[ndx]``
Examples:
>>> import string
>>> data = string.ascii_lowercase.encode()
>>> l = MemLeak(lambda a: data[a:a+8], reraise=False)
>>> l.d(0) == unpack(b'abcd', 32)
True
>>> l.d(22) == unpack(b'wxyz', 32)
True
>>> l.d(23) is None
True
"""
return self._b(addr, ndx, 4)
def q(self, addr, ndx = 0):
"""q(addr, ndx = 0) -> int
Leak qword at ``((uint64_t*) addr)[ndx]``
Examples:
>>> import string
>>> data = string.ascii_lowercase.encode()
>>> l = MemLeak(lambda a: data[a:a+16], reraise=False)
>>> l.q(0) == unpack(b'abcdefgh', 64)
True
>>> l.q(18) == unpack(b'stuvwxyz', 64)
True
>>> l.q(19) is None
True
"""
return self._b(addr, ndx, 8)
def p(self, addr, ndx = 0):
"""p(addr, ndx = 0) -> int
Leak a pointer-width value at ``((void**) addr)[ndx]``
"""
return self._b(addr, ndx, context.bytes)
def s(self, addr):
r"""s(addr) -> str
Leak bytes at `addr` until failure or a nullbyte is found
Return:
A string, without a NULL terminator.
The returned string will be empty if the first byte is
a NULL terminator, or if the first byte could not be
retrieved.
Examples:
>>> data = b"Hello\x00World"
>>> l = MemLeak(lambda a: data[a:a+4], reraise=False)
>>> l.s(0) == b"Hello"
True
>>> l.s(5) == b""
True
>>> l.s(6) == b"World"
True
>>> l.s(999) == b""
True
"""
# This relies on the behavior of _leak to fill the cache
orig = addr
while self.b(addr):
addr += 1
return self._leak(orig, addr-orig)
def n(self, addr, numb):
"""n(addr, ndx = 0) -> str
Leak `numb` bytes at `addr`.
Returns:
A string with the leaked bytes, will return `None` if any are missing
Examples:
>>> import string
>>> data = string.ascii_lowercase.encode()
>>> l = MemLeak(lambda a: data[a:a+4], reraise=False)
>>> l.n(0,1) == b'a'
True
>>> l.n(0,26) == data
True
>>> len(l.n(0,26)) == 26
True
>>> l.n(0,27) is None
True
"""
return self._leak(addr, numb) or None
def _clear(self, addr, ndx, size):
addr += ndx * size
data = [self.cache.pop(x, None) for x in range(addr, addr+size)]
if not all(data):
return None
return unpack(b''.join(data), size*8)
def clearb(self, addr, ndx = 0):
"""clearb(addr, ndx = 0) -> int
Clears byte at ``((uint8_t*)addr)[ndx]`` from the cache and
returns the removed value or `None` if the address was not completely set.
Examples:
>>> l = MemLeak(lambda a: None)
>>> l.cache = {0:b'a'}
>>> l.n(0,1) == b'a'
True
>>> l.clearb(0) == unpack(b'a', 8)
True
>>> l.cache
{}
>>> l.clearb(0) is None
True
"""
return self._clear(addr, ndx, 1)
def clearw(self, addr, ndx = 0):
"""clearw(addr, ndx = 0) -> int
Clears word at ``((uint16_t*)addr)[ndx]`` from the cache and
returns the removed value or `None` if the address was not completely set.
Examples:
>>> l = MemLeak(lambda a: None)
>>> l.cache = {0: b'a', 1: b'b'}
>>> l.n(0, 2) == b'ab'
True
>>> l.clearw(0) == unpack(b'ab', 16)
True
>>> l.cache
{}
"""
return self._clear(addr, ndx, 2)
def cleard(self, addr, ndx = 0):
"""cleard(addr, ndx = 0) -> int
Clears dword at ``((uint32_t*)addr)[ndx]`` from the cache and
returns the removed value or `None` if the address was not completely set.
Examples:
>>> l = MemLeak(lambda a: None)
>>> l.cache = {0: b'a', 1: b'b', 2: b'c', 3: b'd'}
>>> l.n(0, 4) == b'abcd'
True
>>> l.cleard(0) == unpack(b'abcd', 32)
True
>>> l.cache
{}
"""
return self._clear(addr, ndx, 4)
def clearq(self, addr, ndx = 0):
"""clearq(addr, ndx = 0) -> int
Clears qword at ``((uint64_t*)addr)[ndx]`` from the cache and
returns the removed value or `None` if the address was not completely set.
Examples:
>>> c = MemLeak(lambda addr: b'')
>>> c.cache = {x:b'x' for x in range(0x100, 0x108)}
>>> c.clearq(0x100) == unpack(b'xxxxxxxx', 64)
True
>>> c.cache == {}
True
"""
return self._clear(addr, ndx, 8)
def _set(self, addr, val, ndx, size):
addr += ndx * size
for i,b in enumerate(bytearray(pack(val, size*8))):
self.cache[addr+i] = p8(b, endian='little', signed=False)
def setb(self, addr, val, ndx = 0):
"""Sets byte at ``((uint8_t*)addr)[ndx]`` to `val` in the cache.
Examples:
>>> l = MemLeak(lambda x: b'')
>>> l.cache == {}
True
>>> l.setb(33, 0x41)
>>> l.cache == {33: b'A'}
True
"""
return self._set(addr, val, ndx, 1)
def setw(self, addr, val, ndx = 0):
r"""Sets word at ``((uint16_t*)addr)[ndx]`` to `val` in the cache.
Examples:
>>> l = MemLeak(lambda x: b'')
>>> l.cache == {}
True
>>> l.setw(33, 0x41)
>>> l.cache == {33: b'A', 34: b'\x00'}
True
"""
return self._set(addr, val, ndx, 2)
def setd(self, addr, val, ndx = 0):
"""Sets dword at ``((uint32_t*)addr)[ndx]`` to `val` in the cache.
Examples:
See :meth:`setw`.
"""
return self._set(addr, val, ndx, 4)
def setq(self, addr, val, ndx = 0):
"""Sets qword at ``((uint64_t*)addr)[ndx]`` to `val` in the cache.
Examples:
See :meth:`setw`.
"""
return self._set(addr, val, ndx, 8)
def sets(self, addr, val, null_terminate = True):
r"""Set known string at `addr`, which will be optionally be null-terminated
Note that this method is a bit dumb about how it handles the data.
It will null-terminate the data, but it will not stop at the first null.
Examples:
>>> l = MemLeak(lambda x: b'')
>>> l.cache == {}
True
>>> l.sets(0, b'H\x00ello')
>>> l.cache == {0: b'H', 1: b'\x00', 2: b'e', 3: b'l', 4: b'l', 5: b'o', 6: b'\x00'}
True
"""
if null_terminate:
val += b'\x00'
for i,b in enumerate(bytearray(val)):
self.cache[addr+i] = p8(b, endian='little', signed=False)
def __getitem__(self, item):
if isinstance(item, slice):
start = item.start or 0
stop = item.stop
step = item.step
else:
start, stop, step = (item, item+1, 1)
if None in (stop, start):
log.error("Cannot perform unbounded leaks")
return self.n(start, stop-start)[::step]
def compare(self, address, bts):
for i, byte in enumerate(bytearray(bts)):
if self.n(address + i, 1) != p8(byte, endian='little', signed=False):
return False
return True
@staticmethod
def NoNulls(function):
"""Wrapper for leak functions such that addresses which contain NULL
bytes are not leaked.
This is useful if the address which is used for the leak is read in via
a string-reading function like ``scanf("%s")`` or smilar.
"""
@functools.wraps(function, updated=[])
def null_wrapper(address, *a, **kw):
if b'\x00' in pack(address):
log.info('Ignoring leak request for %#x: Contains NULL bytes' % address)
return None
return function(address, *a, **kw)
return MemLeak(null_wrapper)
@staticmethod
def NoWhitespace(function):
"""Wrapper for leak functions such that addresses which contain whitespace
bytes are not leaked.
This is useful if the address which is used for the leak is read in via
e.g. ``scanf()``.
"""
@functools.wraps(function, updated=[])
def whitespace_wrapper(address, *a, **kw):
if set(pack(address)) & set(string.whitespace.encode()):
log.info('Ignoring leak request for %#x: Contains whitespace' % address)
return None
return function(address, *a, **kw)
return MemLeak(whitespace_wrapper)
@staticmethod
def NoNewlines(function):
"""Wrapper for leak functions such that addresses which contain newline
bytes are not leaked.
This is useful if the address which is used for the leak is provided by
e.g. ``fgets()``.
"""
@functools.wraps(function, updated=[])
def whitespace_wrapper(address, *a, **kw):
if b'\n' in pack(address):
log.info('Ignoring leak request for %#x: Contains newlines' % address)
return None
return function(address, *a, **kw)
return MemLeak(whitespace_wrapper)
@staticmethod
def String(function):
"""Wrapper for leak functions which leak strings, such that a NULL
terminator is automaticall added.
This is useful if the data leaked is printed out as a NULL-terminated
string, via e.g. ``printf()``.
"""
@functools.wraps(function, updated=[])
def string_wrapper(address, *a, **kw):
result = function(address, *a, **kw)
if isinstance(result, str) and not isinstance(result, bytes):
result = result.encode('latin1')
if isinstance(result, bytes):
result += b'\x00'
return result
return MemLeak(string_wrapper)
# Aliases for convenience
u64 = q
u32 = d
u16 = w
u8 = b
p64 = setq
p32 = setd
p16 = setw
p8 = setb
class RelativeMemLeak(MemLeak):
def __init__(self, *a, **kw):
kw.setdefault('relative', True)
super(RelativeMemLeak, self).__init__(*a, **kw)