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.
907 lines
27 KiB
Python
907 lines
27 KiB
Python
"""
|
|
Apple Objective-C Runtime Support
|
|
|
|
This module implements support for analyzing the Apple Objective-C runtime. As
|
|
expected, Apple provides no oficial specification for the internal ABI of ObjC
|
|
and no guarantees of its stability, and so this module is not guaranteed to
|
|
work on all versions of Darwin.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
from typing import Generator
|
|
from typing import Generic
|
|
from typing import TypeVar
|
|
|
|
from typing_extensions import override
|
|
|
|
import pwndbg
|
|
import pwndbg.aglib.arch
|
|
import pwndbg.aglib.macho
|
|
import pwndbg.aglib.memory
|
|
import pwndbg.aglib.symbol
|
|
import pwndbg.aglib.typeinfo
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class _IdRaw:
|
|
"""
|
|
Pointer to an Objective-C object in the heap.
|
|
"""
|
|
|
|
def __init__(self, ptr: int):
|
|
self.addr = ptr
|
|
|
|
|
|
class _IdTagged:
|
|
"""
|
|
Tagged pointer to an Objective-C object.
|
|
|
|
This is a bit of a misnomer, as tagged pointers may not be pointers at all,
|
|
and the data for the entire object may be contained in the payload, with no
|
|
backing allocation in the heap. It is up to the class to determine how to
|
|
decode the payload properly.
|
|
"""
|
|
|
|
def __init__(self, tag: int, payload: int, extended: bool):
|
|
self.tag = tag
|
|
self.payload = payload
|
|
self.extended = extended
|
|
|
|
def lookup_class(self) -> Class:
|
|
"""
|
|
Looks up the class object matching the tag in this pointer.
|
|
"""
|
|
classes = _tagged_pointer_classes()
|
|
|
|
if self.extended:
|
|
classes += self.tag - 256
|
|
else:
|
|
classes += self.tag
|
|
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(int(classes.address))
|
|
ptr = _ptrauth_strip(ptr)
|
|
|
|
return Class(ptr)
|
|
|
|
|
|
class _IsaPtr:
|
|
"""
|
|
Pointer to an `isa_t` structure.
|
|
"""
|
|
|
|
ISA_MASK = 0x0000000FFFFFFFF8
|
|
"Mask of bits containing just the authenticated class pointer."
|
|
|
|
def __init__(self, addr: int):
|
|
self._addr = addr
|
|
|
|
def _read(self) -> int:
|
|
"""
|
|
Read the bits of the `isa_t` structure into an integer.
|
|
"""
|
|
return pwndbg.aglib.memory.read_pointer_width(self._addr)
|
|
|
|
def get_class(self) -> Class:
|
|
ptr = self._read() & _IsaPtr.ISA_MASK
|
|
ptr = _ptrauth_strip(ptr)
|
|
|
|
return Class(ptr)
|
|
|
|
|
|
def _isa_class_mask() -> int:
|
|
return pwndbg.aglib.memory.read_pointer_width(
|
|
pwndbg.aglib.symbol.lookup_symbol_addr("objc_debug_isa_class_mask")
|
|
)
|
|
|
|
|
|
class _ClassRoPtr:
|
|
RO_META = 0x1
|
|
RO_ROOT = 0x2
|
|
RO_HAS_CXX_STRUCTORS = 0x4
|
|
RO_HIDDEN = 0x10
|
|
RO_EXCEPTION = 0x20
|
|
RO_HAS_SWIFT_INITIALIZER = 0x40
|
|
RO_IS_ARC = 0x80
|
|
RO_HAS_CXX_DTOR_ONLY = 0x100
|
|
RO_HAS_WEAK_WITHOUT_ARC = 0x200
|
|
RO_FORBIDS_ASSOCIATED_OBJECTS = 0x400
|
|
RO_FROM_BUNDLE = 0x20000000
|
|
RO_FUTURE = 0x40000000
|
|
RO_REALIZED = 0x80000000
|
|
|
|
def __init__(self, addr: int):
|
|
self._ptr = addr
|
|
|
|
def name(self) -> bytes:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr + 24)
|
|
return pwndbg.aglib.memory.string(ptr)
|
|
|
|
def flags(self) -> int:
|
|
return pwndbg.aglib.memory.u32(self._ptr)
|
|
|
|
def methods(self) -> Generator[Method]:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr + 32)
|
|
|
|
if ptr & 1 == 0:
|
|
if ptr == 0:
|
|
return
|
|
yield from _MethodList(ptr).entries()
|
|
else:
|
|
if ptr & ~1 == 0:
|
|
# Not expected to happen, but better safe than sorry.
|
|
return
|
|
|
|
list_of_lists = _RelativeListOfLists(_MethodList, ptr & ~1)
|
|
for lst in list_of_lists.entries():
|
|
if lst is None:
|
|
continue
|
|
yield from lst.get_list().entries()
|
|
|
|
def ivars(self) -> Generator[InstanceVariable]:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr + 0x30)
|
|
if ptr != 0:
|
|
yield from _IVarList(ptr).entries()
|
|
|
|
def properties(self) -> Generator[ClassProperty]:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr + 0x40)
|
|
if ptr & 1 == 0:
|
|
if ptr == 0:
|
|
return
|
|
|
|
yield from _ClassPropertyList(ptr).entries()
|
|
else:
|
|
if ptr & ~1 == 0:
|
|
# Not expected to happen, but better safe than sorry.
|
|
return
|
|
|
|
list_of_lists = _RelativeListOfLists(_ClassPropertyList, ptr & ~1)
|
|
for lst in list_of_lists.entries():
|
|
if lst is None:
|
|
continue
|
|
yield from lst.get_list().entries()
|
|
|
|
|
|
class _ClassRwExtPtr:
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
def ro(self) -> _ClassRoPtr:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr)
|
|
ptr = _ptrauth_strip(ptr)
|
|
return _ClassRoPtr(ptr)
|
|
|
|
def methods(self) -> _ListArray[Method]:
|
|
return _ListArray(_MethodList, self._ptr + pwndbg.aglib.typeinfo.ptrsize)
|
|
|
|
def properties(self) -> _ListArray[ClassProperty]:
|
|
return _ListArray(_ClassPropertyList, self._ptr + 2 * pwndbg.aglib.typeinfo.ptrsize)
|
|
|
|
def demangled_name(self) -> bytes | None:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr + 4 * pwndbg.aglib.typeinfo.ptrsize)
|
|
if ptr == 0:
|
|
return None
|
|
return pwndbg.aglib.memory.string(ptr)
|
|
|
|
def version(self) -> int:
|
|
return pwndbg.aglib.memory.u32(self._ptr + 5 * pwndbg.aglib.typeinfo.ptrsize)
|
|
|
|
|
|
class _ClassRwPtr:
|
|
RW_REALIZED = 1 << 31
|
|
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
def ro_or_rw_ext(self) -> _ClassRoPtr | _ClassRwExtPtr:
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr + 8)
|
|
if ptr & 1 == 1:
|
|
return _ClassRwExtPtr(ptr & ~1)
|
|
else:
|
|
return _ClassRoPtr(ptr)
|
|
|
|
|
|
class _ClassDataBitsPtr:
|
|
"""
|
|
Pointer to a `class_data_bits_t` structure.
|
|
"""
|
|
|
|
FAST_IS_RW_POINTER = 0x8000000000000000
|
|
FAST_IS_SWIFT_LEGACY = 0x1
|
|
FAST_IS_SWIFT_STABLE = 0x2
|
|
FAST_HAS_DEFAULT_RR = 0x4
|
|
|
|
FAST_DATA_MASK = 0x0F007FFFFFFFFFF8
|
|
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
def data(self) -> _ClassRoPtr | _ClassRwPtr:
|
|
if self._is_rw():
|
|
return _ClassRwPtr(self._data_addr())
|
|
|
|
return _ClassRoPtr(self._data_addr())
|
|
|
|
def _is_rw(self) -> bool:
|
|
return ((self._ptr & _ClassDataBitsPtr.FAST_IS_RW_POINTER) != 0) or (
|
|
(self._flags() & _ClassRwPtr.RW_REALIZED) != 0
|
|
)
|
|
|
|
def _data_addr(self) -> int:
|
|
return _ptrauth_strip(self._ptr) & _ClassDataBitsPtr.FAST_DATA_MASK
|
|
|
|
def _flags(self) -> int:
|
|
return pwndbg.aglib.memory.u32(self._data_addr())
|
|
|
|
|
|
class _EntList(Generic[T]):
|
|
"""
|
|
Entity list.
|
|
"""
|
|
|
|
_flags_mask: int = 0
|
|
"Mask for the flag bits of `entsizeAndFlags`"
|
|
|
|
def __init__(self, ptr: int):
|
|
self._addr = self._addr_from_ptr(ptr)
|
|
self._ptr = ptr
|
|
|
|
def _entsize_and_flags(self) -> int:
|
|
return pwndbg.aglib.memory.u32(self._addr)
|
|
|
|
def _entries(self) -> int:
|
|
return pwndbg.aglib.memory.u32(self._addr + 4)
|
|
|
|
def flags(self) -> int:
|
|
return self._entsize_and_flags() & self._flags_mask
|
|
|
|
def entsize(self) -> int:
|
|
return self._entsize_and_flags() & ~self._flags_mask
|
|
|
|
def _modify_pointer(self, ptr: int) -> int:
|
|
return ptr
|
|
|
|
def _from_ptr(self, ptr: int) -> T:
|
|
"""
|
|
Build the type of this list from a pointer.
|
|
|
|
Must be implemented by the specialized class.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _addr_from_ptr(self, ptr: int) -> int:
|
|
"""
|
|
Strip any metadata from the pointer to this list.
|
|
|
|
Must be implemented by the specialized class.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def __len__(self) -> int:
|
|
return self._entries()
|
|
|
|
def get(self, i: int) -> T:
|
|
if i >= len(self):
|
|
raise IndexError(f"Index {i} is out-of-range for entlist with {len(self)} entries")
|
|
|
|
return self._from_ptr(self._modify_pointer(self._addr + 8 + i * self.entsize()))
|
|
|
|
def entries(self) -> Generator[T]:
|
|
for i in range(len(self)):
|
|
yield self.get(i)
|
|
|
|
|
|
class _RelativeListOfListsEntry(Generic[T]):
|
|
def __init__(self, ty: Callable[[int], _EntList[T]], ptr: int):
|
|
self._ptr = ptr
|
|
self._ty = ty
|
|
|
|
def image_index(self) -> int:
|
|
return pwndbg.aglib.memory.u64(self._ptr) & 0xFFFF
|
|
|
|
def _list_offset(self) -> int:
|
|
return pwndbg.aglib.memory.s64(self._ptr) >> 16
|
|
|
|
def get_list(self) -> _EntList[T]:
|
|
return self._ty(self._ptr + self._list_offset())
|
|
|
|
|
|
class _RelativeListOfLists(
|
|
_EntList[_RelativeListOfListsEntry[T] | None],
|
|
Generic[T],
|
|
):
|
|
"""
|
|
An array of relative pointers to lists.
|
|
|
|
This corresponds to the `relative_list_list_t` type in libobjc.
|
|
"""
|
|
|
|
def __init__(self, ty: Callable[[int], _EntList[T]], ptr: int):
|
|
super().__init__(ptr)
|
|
self._ty = ty
|
|
|
|
@override
|
|
def _addr_from_ptr(self, ptr: int) -> int:
|
|
# Top-Byte-Ignore is assumed for method lists, but method list pointers
|
|
# may have metadata attached to them.
|
|
return ptr & ~0xFF00000000000000
|
|
|
|
@override
|
|
def _from_ptr(self, ptr: int) -> _RelativeListOfListsEntry[T] | None:
|
|
entry = _RelativeListOfListsEntry(self._ty, ptr)
|
|
if not _header_info_rw_is_image_loaded(entry.image_index()):
|
|
# The entry is only valid if its corresponding image has been marked
|
|
# as loaded in `objc_debug_headerInfoRWs`.
|
|
return None
|
|
|
|
return entry
|
|
|
|
|
|
class _ListArray(Generic[T]):
|
|
"""
|
|
A runtime-polymorphic array type for lists. May be a pointer to a list type,
|
|
an array of pointers, or a _RelativeListOfLists, distinguished by a tag in
|
|
a pointer.
|
|
|
|
Strangely for Apple, the tagged pointer to the final list is contained
|
|
inside the list array structure, rather than having the whole structure be
|
|
inlined into a pointer value. Suspiciously sane.
|
|
|
|
This corresponds to the `list_array_tt` type in libobjc.
|
|
"""
|
|
|
|
def __init__(self, ty: Callable[[int], _EntList[T]], ptr: int):
|
|
self._ptr = ptr
|
|
self._ty = ty
|
|
|
|
def entries(self) -> Generator[T]:
|
|
raw_ptr = pwndbg.aglib.memory.read_pointer_width(self._ptr)
|
|
|
|
tag = raw_ptr & 3
|
|
ptr = raw_ptr & ~3
|
|
|
|
if ptr == 0:
|
|
return
|
|
|
|
if tag == 0:
|
|
# This is just a pointer to the list.
|
|
yield from self._ty(ptr).entries()
|
|
elif tag == 1:
|
|
# This is an array of lists.
|
|
count = pwndbg.aglib.memory.u32(ptr)
|
|
for i in range(count):
|
|
yield from self._ty(
|
|
pwndbg.aglib.memory.read_pointer_width(
|
|
ptr + 8 + i * pwndbg.aglib.typeinfo.ptrsize
|
|
)
|
|
).entries()
|
|
elif tag == 2:
|
|
# This is a relative list of lists.
|
|
for ll in _RelativeListOfLists(self._ty, ptr).entries():
|
|
yield from ll.get_list().entries()
|
|
|
|
|
|
def _header_info_rw_is_image_loaded(index: int) -> bool:
|
|
"""
|
|
Queries `objc_debug_headerInfoRWs` and checks whether the image with the
|
|
given index is loaded.
|
|
"""
|
|
addr = pwndbg.aglib.memory.read_pointer_width(
|
|
pwndbg.aglib.symbol.lookup_symbol_addr("objc_debug_headerInfoRWs")
|
|
)
|
|
|
|
count = pwndbg.aglib.memory.u32(addr)
|
|
entsize = pwndbg.aglib.memory.u32(addr + 4)
|
|
|
|
if index >= count:
|
|
raise IndexError(
|
|
f"Image index {index} is out-of-bounds for headerInfoRWs structure with {count} entries"
|
|
)
|
|
|
|
return pwndbg.aglib.memory.read_pointer_width(addr + 8 + entsize * index) & 1 == 1
|
|
|
|
|
|
def _tagged_pointer_classes() -> pwndbg.dbg_mod.Value:
|
|
"""
|
|
The Objective-C runtime tagged pointer classs list.
|
|
|
|
The classes to which the tag values in a tagged pointer corresponds are not
|
|
fixed, and are instead stored in a runtime-global array that gets looked up
|
|
when a message is sent.
|
|
"""
|
|
return pwndbg.aglib.symbol.lookup_symbol("objc_debug_taggedpointer_classes").cast(
|
|
pwndbg.aglib.typeinfo.void.pointer().pointer()
|
|
)
|
|
|
|
|
|
def _ptr_obfuscation_value() -> int:
|
|
"""
|
|
The Objective-C runtime obfuscates tagged pointer values.
|
|
"""
|
|
return pwndbg.aglib.memory.read_pointer_width(
|
|
pwndbg.aglib.symbol.lookup_symbol_addr("objc_debug_taggedpointer_obfuscator")
|
|
)
|
|
|
|
|
|
def _try_decode_tagged_split(ptr: int) -> _IdTagged | None:
|
|
"""
|
|
Decodes a tagged pointer encoded in the split-tag scheme, if it is tagged.
|
|
|
|
This is the encoding scheme used in modern - iOS 14 and newer - ARM64
|
|
platforms.
|
|
|
|
If the pointer is not tagged, returns `None`.
|
|
"""
|
|
if ptr & 0x8000000000000000 == 0:
|
|
# Not a tagged pointer.
|
|
return None
|
|
|
|
if ptr & 7 == 7:
|
|
# This is an extended tag with a 52-bit payload.
|
|
tag = (ptr >> 55) & 0xFF
|
|
payload = (ptr >> 3) & 0xFFFFFFFFFFFFF
|
|
extended = True
|
|
else:
|
|
# This is a short tag with a 60-bit payload.
|
|
tag = ptr & 7
|
|
payload = (ptr >> 3) & 0xFFFFFFFFFFFFFFF
|
|
extended = False
|
|
|
|
return _IdTagged(tag, payload, extended)
|
|
|
|
|
|
def _try_decode_tagged_lsb(ptr: int) -> _IdTagged:
|
|
"""
|
|
Decodes a tagged pointer encoded in the LSB-tag scheme, if it is tagged.
|
|
|
|
This is the encoding scheme used in all x86-64 versions of Darwin.
|
|
|
|
If the pointer is not tagged, returns `None`.
|
|
"""
|
|
if ptr & 1 == 0:
|
|
# Not a tagged pointer.
|
|
return None
|
|
|
|
if ptr & 14 == 14:
|
|
# This is an extended tag with a 52-bit payload.
|
|
tag = (ptr >> 4) & 0xFF
|
|
payload = ptr >> 12
|
|
extended = True
|
|
else:
|
|
# This is a short tage with a 60-bit payload.
|
|
tag = (ptr >> 1) & 7
|
|
payload = ptr >> 4
|
|
extended = False
|
|
|
|
return _IdTagged(tag, payload, extended)
|
|
|
|
|
|
def _decode_prog_id(ptr: int) -> _IdRaw | _IdTagged:
|
|
"""
|
|
Given an Objective-C program, decode it.
|
|
"""
|
|
|
|
# First, check for tagged pointers.
|
|
tagged = None
|
|
match pwndbg.aglib.arch.name:
|
|
case "aarch64":
|
|
tagged = _try_decode_tagged_split(ptr)
|
|
case "x86-64":
|
|
tagged = _try_decode_tagged_lsb(ptr)
|
|
case other:
|
|
raise AssertionError(f"Unexpected Objective-C architecture: {other}")
|
|
if tagged is not None:
|
|
# Successfuly decoded the tagged pointer.
|
|
return tagged
|
|
|
|
# This is a direct pointer.
|
|
return _IdRaw(ptr)
|
|
|
|
|
|
def _ptrauth_strip(ptr: int) -> int:
|
|
"""
|
|
Strip pointer signing information from a given signed pointer.
|
|
"""
|
|
return ptr & 0xFFFFFFFFFFFF
|
|
|
|
|
|
class Object:
|
|
_addr: int
|
|
"Object pointer value, as seen in the program. May be tagged, obfuscated, authenticated."
|
|
|
|
_id: _IdRaw | _IdTagged
|
|
"Decoded object pointer value. May be tagged."
|
|
|
|
def __init__(self, addr: int):
|
|
self._addr = addr
|
|
self._id = _decode_prog_id(addr)
|
|
|
|
@property
|
|
def cls(self) -> Class | None:
|
|
if isinstance(self._id, _IdRaw):
|
|
isa = _IsaPtr(self._id.addr)
|
|
return isa.get_class()
|
|
elif isinstance(self._id, _IdTagged):
|
|
return self._id.lookup_class()
|
|
|
|
|
|
class Class(Object):
|
|
def __init__(self, addr: int):
|
|
super().__init__(addr)
|
|
assert isinstance(self._id, _IdRaw), "Class pointers are never tagged"
|
|
|
|
def _data_bits(self) -> _ClassDataBitsPtr:
|
|
# MyPy fails if we don't check this a second time.
|
|
assert isinstance(self._id, _IdRaw), "Class pointers are never tagged"
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(self._id.addr + 32)
|
|
ptr = _ptrauth_strip(ptr)
|
|
return _ClassDataBitsPtr(ptr)
|
|
|
|
def _ro(self) -> _ClassRoPtr:
|
|
data = self._data_bits().data()
|
|
if isinstance(data, _ClassRoPtr):
|
|
return data
|
|
elif isinstance(data, _ClassRwPtr):
|
|
ro_or_rw_ext = data.ro_or_rw_ext()
|
|
if isinstance(ro_or_rw_ext, _ClassRwExtPtr):
|
|
return ro_or_rw_ext.ro()
|
|
elif isinstance(ro_or_rw_ext, _ClassRoPtr):
|
|
return ro_or_rw_ext
|
|
else:
|
|
# FIXME: Should be `typing.assert_never`, needs Python 3.11
|
|
assert False
|
|
else:
|
|
# FIXME: Should be `typing.assert_never`, needs Python 3.11
|
|
assert False
|
|
|
|
def _rw_ext(self) -> _ClassRwExtPtr | None:
|
|
data = self._data_bits().data()
|
|
if isinstance(data, _ClassRoPtr):
|
|
return None
|
|
elif isinstance(data, _ClassRwPtr):
|
|
ro_or_rw_ext = data.ro_or_rw_ext()
|
|
if isinstance(ro_or_rw_ext, _ClassRwExtPtr):
|
|
return ro_or_rw_ext
|
|
elif isinstance(ro_or_rw_ext, _ClassRoPtr):
|
|
return None
|
|
else:
|
|
# FIXME: Should be `typing.assert_never`, needs Python 3.11
|
|
assert False
|
|
else:
|
|
# FIXME: Should be `typing.assert_never`, needs Python 3.11
|
|
assert False
|
|
|
|
@property
|
|
def superclass(self) -> Class | None:
|
|
# MyPy fails if we don't check this a second time.
|
|
assert isinstance(self._id, _IdRaw), "Class pointers are never tagged"
|
|
|
|
if self._ro().flags() & _ClassRoPtr.RO_ROOT != 0:
|
|
# This is a root class, and thus has no superclass.
|
|
return None
|
|
|
|
ptr_addr = self._id.addr + pwndbg.aglib.typeinfo.ptrsize
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(ptr_addr)
|
|
ptr = _ptrauth_strip(ptr)
|
|
|
|
return Class(ptr)
|
|
|
|
@property
|
|
def name(self) -> bytes:
|
|
return self._ro().name()
|
|
|
|
@property
|
|
def methods(self) -> Generator[Method]:
|
|
if (rw_ext := self._rw_ext()) is not None:
|
|
# Return the methods added to the class at runtime from the Class
|
|
# R/W structure, which also include the base methods.
|
|
yield from rw_ext.methods().entries()
|
|
else:
|
|
# Return the base methods.
|
|
yield from self._ro().methods()
|
|
|
|
@property
|
|
def ivars(self) -> Generator[InstanceVariable]:
|
|
yield from self._ro().ivars()
|
|
|
|
@property
|
|
def properties(self) -> Generator[ClassProperty]:
|
|
if (rw_ext := self._rw_ext()) is not None:
|
|
# Return the properties added to the class at runtime from the Class
|
|
# R/W structure, which also include the base properties.
|
|
yield from rw_ext.properties().entries()
|
|
else:
|
|
# Return the base properties.
|
|
yield from self._ro().properties()
|
|
|
|
@property
|
|
def is_metaclass(self) -> bool:
|
|
return (self._ro().flags() & _ClassRoPtr.RO_META) != 0
|
|
|
|
@override
|
|
@property
|
|
def cls(self) -> Class | None:
|
|
if self.is_metaclass:
|
|
# Following this pointer in metaclasses is weird. Users are better
|
|
# served following the superclass chain, instead.
|
|
return None
|
|
return super().cls
|
|
|
|
|
|
class InstanceVariable:
|
|
"""
|
|
An Objective-C Instance Variable.
|
|
|
|
Instance Variables are NOT objects!
|
|
"""
|
|
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
@property
|
|
def offset(self) -> int:
|
|
"""
|
|
The offset in bytes of this value from the start of the object instance.
|
|
"""
|
|
return pwndbg.aglib.memory.s32(pwndbg.aglib.memory.read_pointer_width(self._ptr))
|
|
|
|
@property
|
|
def name(self) -> bytes:
|
|
"""
|
|
The name of this instance variable.
|
|
"""
|
|
return pwndbg.aglib.memory.string(
|
|
pwndbg.aglib.memory.read_pointer_width(self._ptr + pwndbg.aglib.typeinfo.ptrsize)
|
|
)
|
|
|
|
@property
|
|
def typename(self) -> bytes:
|
|
"""
|
|
The name of the type of this instance variable.
|
|
"""
|
|
return pwndbg.aglib.memory.string(
|
|
pwndbg.aglib.memory.read_pointer_width(self._ptr + pwndbg.aglib.typeinfo.ptrsize * 2)
|
|
)
|
|
|
|
@property
|
|
def alignment(self) -> int:
|
|
"""
|
|
The alignment of this instance variable, in bytes.
|
|
"""
|
|
align_log2 = pwndbg.aglib.memory.u32(self._ptr + pwndbg.aglib.typeinfo.ptrsize * 3)
|
|
|
|
# All ones indicates the natural alignment of a pointer.
|
|
if align_log2 == 0xFFFFFFFF:
|
|
return pwndbg.aglib.typeinfo.ptrsize
|
|
|
|
return 1 << align_log2
|
|
|
|
@property
|
|
def size(self) -> int:
|
|
"""
|
|
The size of this instance variable, in bytes.
|
|
"""
|
|
return pwndbg.aglib.memory.u32(self._ptr + pwndbg.aglib.typeinfo.ptrsize * 3 + 4)
|
|
|
|
|
|
class ClassProperty:
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
@property
|
|
def name(self) -> bytes:
|
|
"""
|
|
The name of this class property.
|
|
"""
|
|
return pwndbg.aglib.memory.string(pwndbg.aglib.memory.read_pointer_width(self._ptr))
|
|
|
|
@property
|
|
def value(self) -> bytes:
|
|
"""
|
|
The value of this property.
|
|
"""
|
|
return pwndbg.aglib.memory.string(
|
|
pwndbg.aglib.memory.read_pointer_width(self._ptr + pwndbg.aglib.typeinfo.ptrsize)
|
|
)
|
|
|
|
|
|
class Selector:
|
|
"""
|
|
An Objective-C Selector.
|
|
|
|
Selectors are NOT objects!
|
|
"""
|
|
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
@property
|
|
def name(self) -> bytes:
|
|
"""
|
|
Retrieves the name of this selector.
|
|
"""
|
|
|
|
# In the Apple Objective-C runtime, selectors are human-readable strings
|
|
# with unique identities[1]. The identity is simply the pointer to the
|
|
# string itself, guaranteed by the tooling to be unique. To read the
|
|
# name of the selector, then, we can simply follow its identity pointer.
|
|
#
|
|
# [1]: https://web.archive.org/web/20161010081824/http://unixjunkie.blogspot.com/2006/02/nil-and-nil.html
|
|
return pwndbg.aglib.memory.string(self._ptr)
|
|
|
|
|
|
class Method:
|
|
"""
|
|
An Objective-C Method Pointer.
|
|
|
|
Methods are NOT objects!
|
|
|
|
A method pointer can be one of three types: Small, small direct, and big.
|
|
|
|
Pointer types are distinguished by the two least significant bits in the
|
|
integer representation of the pointer. A value of `1` is used for both small
|
|
pointer types, while all other values are used to distinguish between the
|
|
signing nuances of big pointers.
|
|
|
|
Small pointers 32-bit wide and relative to a given base value. Big pointers
|
|
contain the pointers themselves, and they may or may not be signed.
|
|
|
|
Small direct pointers are small pointers that reside in the shared cache, and
|
|
their selectors are relative to @selector(🤯), while the selectors of regular
|
|
small pointers are relative to the pointers themselves.
|
|
"""
|
|
|
|
def __init__(self, ptr: int):
|
|
self._ptr = ptr
|
|
|
|
@property
|
|
def sel(self) -> Selector:
|
|
"The selector this method responds to."
|
|
kind = self._ptr & 3
|
|
base = self._ptr & ~3
|
|
if kind == 1:
|
|
if pwndbg.aglib.macho.shared_cache().is_address_in_shared_cache(base):
|
|
# To resolve selectors of small method pointers in the shared cache,
|
|
# we have to look up the identity of @selector(🤯).
|
|
rel = (
|
|
pwndbg.aglib.macho.shared_cache()
|
|
.objc_builtin_selectors()
|
|
.lookup("🤯".encode("utf-8"))
|
|
)
|
|
ptr = rel + pwndbg.aglib.memory.s32(base)
|
|
else:
|
|
offset = pwndbg.aglib.memory.s32(base)
|
|
ref = base + offset
|
|
|
|
# Non-shared cache values are pointers to selectors.
|
|
ptr = pwndbg.aglib.memory.read_pointer_width(ref)
|
|
|
|
return Selector(ptr)
|
|
else:
|
|
return Selector(_ptrauth_strip(pwndbg.aglib.memory.read_pointer_width(base)))
|
|
|
|
@property
|
|
def types(self) -> bytes:
|
|
"The types of the arguments to this method."
|
|
kind = self._ptr & 3
|
|
base = self._ptr & ~3
|
|
if kind == 1:
|
|
ptr = base + 4
|
|
offset = pwndbg.aglib.memory.s32(ptr)
|
|
addr = ptr + offset
|
|
else:
|
|
ptr = base + 8
|
|
addr = _ptrauth_strip(
|
|
pwndbg.aglib.memory.read_pointer_width(base + pwndbg.aglib.typeinfo.ptrsize)
|
|
)
|
|
|
|
return pwndbg.aglib.memory.string(addr)
|
|
|
|
@property
|
|
def imp(self) -> int:
|
|
"The pointer to the function that implements this method."
|
|
kind = self._ptr & 3
|
|
base = self._ptr & ~3
|
|
if kind == 1:
|
|
# There's a bit of nuance here.
|
|
#
|
|
# Method swizzling for small pointers is implemented using a global
|
|
# hash map of method pointers to implementation pointers. When
|
|
# getting the IMP pointer for a small pointer, the runtime will
|
|
# first check the global hash map to see if the method has been
|
|
# swizzled, and return the swizzled method if it has. The runtime
|
|
# will do what we do here if method has not been swizzled.
|
|
#
|
|
# Currently, we have no good way to query this map, and no other way
|
|
# to detect that a method has been swizzled, so swizzles to small
|
|
# pointers are unfortunately compeltely invisible to us.
|
|
#
|
|
# TODO: Handle method swizzles for small-pointer-type Objective-C methods.
|
|
ptr = base + 8
|
|
offset = pwndbg.aglib.memory.s32(ptr)
|
|
return ptr + offset
|
|
else:
|
|
ptr = base + 16
|
|
return _ptrauth_strip(
|
|
pwndbg.aglib.memory.read_pointer_width(base + pwndbg.aglib.typeinfo.ptrsize)
|
|
)
|
|
|
|
|
|
class _MethodList(_EntList[Method]):
|
|
"""
|
|
Method entity list.
|
|
"""
|
|
|
|
_flags_mask = 0xFFFF0003
|
|
|
|
SMALL_METHOD_LIST_FLAG = 0x80000000
|
|
"Indicates that the pointers in this list are small method pointers."
|
|
|
|
BIG_SIGNED_METHOD_LIST_FLAG = 0x8000000000000000
|
|
"""
|
|
Indicates that the pointers in this list are big and signed.
|
|
|
|
Stored as part of the pointer to the method list, rather than in the flags
|
|
field, as is the case with other flags.
|
|
"""
|
|
|
|
@override
|
|
def _modify_pointer(self, ptr: int) -> int:
|
|
if self.flags() & self.SMALL_METHOD_LIST_FLAG != 0:
|
|
# This is a small pointer list.
|
|
return (ptr & ~3) | 1
|
|
elif self._ptr & self.BIG_SIGNED_METHOD_LIST_FLAG:
|
|
# This is a big signed poitner list.
|
|
return (ptr & ~3) | 2
|
|
else:
|
|
# No tag or flag. This is a big pointer list.
|
|
return ptr & ~3
|
|
|
|
@override
|
|
def _addr_from_ptr(self, ptr: int) -> int:
|
|
# Top-Byte-Ignore is assumed for method lists, but method list pointers
|
|
# may have metadata attached to them.
|
|
return ptr & ~0xFF00000000000000
|
|
|
|
@override
|
|
def _from_ptr(self, ptr: int) -> Method:
|
|
return Method(ptr)
|
|
|
|
|
|
class _IVarList(_EntList[InstanceVariable]):
|
|
"IVar entity list."
|
|
|
|
_flags_mask = 0
|
|
|
|
@override
|
|
def _modify_pointer(self, ptr: int) -> int:
|
|
return ptr
|
|
|
|
@override
|
|
def _addr_from_ptr(self, ptr: int) -> int:
|
|
return ptr
|
|
|
|
@override
|
|
def _from_ptr(self, ptr: int) -> InstanceVariable:
|
|
return InstanceVariable(ptr)
|
|
|
|
|
|
class _ClassPropertyList(_EntList[ClassProperty]):
|
|
"Class property entity list."
|
|
|
|
_flags_mask = 0
|
|
|
|
@override
|
|
def _modify_pointer(self, ptr: int) -> int:
|
|
return ptr
|
|
|
|
@override
|
|
def _addr_from_ptr(self, ptr: int) -> int:
|
|
return ptr
|
|
|
|
@override
|
|
def _from_ptr(self, ptr: int) -> ClassProperty:
|
|
return ClassProperty(ptr)
|