Resolve arguments for Objective-C method calls (#3260)

* Resolve Objective-C method calls

* Check for proper environment before invoking ObjC function resolution
pull/3284/merge
Matt. 3 months ago committed by GitHub
parent 43de06be89
commit 8b0278be13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -858,6 +858,28 @@ Whether to show call arguments below instruction.
---------- ----------
## **objc-max-function-arguments**
Maximum number of arguments to resolve for an Objective-C method call.
**Default:** 32
----------
## **objc-max-function-types-depth**
Maximum allowed depth for a type in an Objective-C method call.
**Default:** 32
----------
## **safe-linking** ## **safe-linking**

@ -12,6 +12,8 @@ from __future__ import annotations
from typing import Callable from typing import Callable
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import List
from typing import Tuple
from typing import TypeVar from typing import TypeVar
from typing_extensions import override from typing_extensions import override
@ -22,6 +24,8 @@ import pwndbg.aglib.macho
import pwndbg.aglib.memory import pwndbg.aglib.memory
import pwndbg.aglib.symbol import pwndbg.aglib.symbol
import pwndbg.aglib.typeinfo import pwndbg.aglib.typeinfo
from pwndbg.aglib.disasm.instruction import PwndbgInstruction
from pwndbg.dbg import Type
T = TypeVar("T") T = TypeVar("T")
@ -904,3 +908,251 @@ class _ClassPropertyList(_EntList[ClassProperty]):
@override @override
def _from_ptr(self, ptr: int) -> ClassProperty: def _from_ptr(self, ptr: int) -> ClassProperty:
return ClassProperty(ptr) return ClassProperty(ptr)
def _parse_method_type_array(ty: bytes, depth: int) -> Tuple[Type, int] | None:
"""
Parses a typed array entry in an Objective-C method type string.
"""
if ty[0] != b"[":
return None
if (end := ty.find(b"]")) == -1:
return None
if (inner := _parse_method_type(ty[1:end], depth + 1)) is None:
return None
# Treat arrays as pointers.
return inner[0].pointer(), inner[1] + 2
def _parse_method_type_pointer(ty: bytes, depth: int) -> Tuple[Type, int] | None:
"""
Parses a typed pointer entry in an Objective-C method type string.
"""
if ty[0] != b"^":
return None
if (inner := _parse_method_type(ty[1:], depth + 1)) is None:
return None
return inner[0].pointer(), inner[1] + 1
def _parse_method_type_id_typed(ty: bytes, depth: int) -> Tuple[Type, int] | None:
"""
Parses a typed `id` entry in an Objective-C method type string.
"""
if ty[:2] != b'@"':
return None
if (end := ty.find(b'"', 2)) == -1:
return None
# Resolve to `id`, even if we could technically be more specific.
return pwndbg.aglib.typeinfo.lookup_types("id"), end + 1
def _parse_method_type(ty: bytes, depth: int) -> Tuple[Type, int] | None:
"""
Parses a single entry in an Objective-C method type string.
"""
if depth > max_method_type_depth.value:
# Too deep. Reject this type.
return None
while ty[0] >= 0x30 and ty[0] < 0x3A:
# Ignore integers that we don't recognize.
ty = ty[1:]
# Try to parse composite types.
if (res := _parse_method_type_array(ty, depth)) is not None:
return res
if (res := _parse_method_type_pointer(ty, depth)) is not None:
return res
if (res := _parse_method_type_id_typed(ty, depth)) is not None:
return res
# Try to parse atomic types.
match ty[0:1]:
case b"v":
return pwndbg.aglib.typeinfo.void, 1
case b"f":
return pwndbg.aglib.typeinfo.lookup_types("float"), 1
case b"d":
return pwndbg.aglib.typeinfo.lookup_types("double"), 1
case b"B":
return pwndbg.aglib.typeinfo.int32, 1
case b"*":
return pwndbg.aglib.typeinfo.char.pointer(), 1
case b"@":
return pwndbg.aglib.typeinfo.lookup_types("id"), 1
case b"#":
return pwndbg.aglib.typeinfo.lookup_types("Class"), 1
case b":":
return pwndbg.aglib.typeinfo.lookup_types("SEL"), 1
case b"c":
return pwndbg.aglib.typeinfo.char, 1
case b"s":
return pwndbg.aglib.typeinfo.int16, 1
case b"i":
return pwndbg.aglib.typeinfo.int32, 1
case b"l":
return pwndbg.aglib.typeinfo.int32, 1
case b"q":
return pwndbg.aglib.typeinfo.int64, 1
case b"C":
return pwndbg.aglib.typeinfo.uchar, 1
case b"S":
return pwndbg.aglib.typeinfo.uint16, 1
case b"I":
return pwndbg.aglib.typeinfo.uint32, 1
case b"L":
return pwndbg.aglib.typeinfo.uint32, 1
case b"Q":
return pwndbg.aglib.typeinfo.uint64, 1
return None
def _parse_method_type_string(ty: bytes) -> Generator[Type | None]:
"""
Return a generator that yields the type names of the arguments to a method,
if they can be resolved, yielding `None` for types that we fail to resolve.
"""
cursor = 0
yielded = 0
while yielded < max_method_argument_count.value + 1:
while cursor < len(ty) and ty[cursor] >= 0x30 and ty[cursor] < 0x3A:
# Ignore integers.
cursor += 1
if cursor >= len(ty):
break
res = _parse_method_type(ty[cursor:], 0)
if res is None:
yield None
break
yield res[0]
cursor += res[1]
yielded += 1
max_method_argument_count = pwndbg.config.add_param(
"objc-max-function-arguments",
32,
"maximum number of arguments to resolve for an Objective-C method call",
param_class=pwndbg.lib.config.PARAM_ZUINTEGER,
)
max_method_type_depth = pwndbg.config.add_param(
"objc-max-function-types-depth",
32,
"maximum allowed depth for a type in an Objective-C method call",
param_class=pwndbg.lib.config.PARAM_ZUINTEGER,
)
def try_resolve_call_at_current_pc(insn: PwndbgInstruction) -> pwndbg.lib.functions.Function | None:
"""
Tries to resolve a call to an Objective-C method for an instruction in the
current Program Counter.
"""
if not insn.call_like:
# No point in trying to resolve something that isn't a call.
return None
target = pwndbg.aglib.symbol.resolve_addr(insn.target)
if target == "objc_msgSend":
# Resolve msgSend.
#
# First, try to work out the method implementation from the selector in
# the current architecture.
if pwndbg.aglib.arch.name == "aarch64":
obj_reg = "x0"
sel_reg = "x1"
else:
# Not supported.
# TODO: Support resolution of Objective-C method calls in x86-64.
return None
obj_ptr = getattr(pwndbg.aglib.regs, obj_reg)
sel_ptr = getattr(pwndbg.aglib.regs, sel_reg)
obj = Object(obj_ptr)
sel = Selector(sel_ptr)
# Walk up the class chain and try to find the method value.
method = None
clss = obj.cls
while clss is not None:
try:
method = next((method for method in clss.methods if method.sel.name == sel.name))
break
except StopIteration:
pass
clss = clss.superclass
if method is None:
# Could not resolve the method, either an invalid call, or we don't
# know how to find out the method at runtime. Either way, not much
# can be done.
return None
# Resolve name and number of function arguments from the selector.
#
# In Objective-C, the calling convention and number of arguments is
# fixed at compile time, and no runtime reflection information is used,
# so there's nothing stopping a program from using a selector that does
# not at all reflect the real arguments to the method. Still, it is
# conventional for the selector to be representative.
#
# Because of that, we try to extract some information only on a best-
# effort basis, since we don't know whether it will be truly useful, or
# if the program is actively trying to confuse us.
sel_args: List[bytes] = [b"id", b"sel"]
sel_last_args_idx = 0
while len(sel_args) < max_method_argument_count.value:
index = sel.name.find(b":", sel_last_args_idx)
if index == -1:
break
sel_args.append(sel.name[sel_last_args_idx:index])
sel_last_args_idx = index + 1
# Resolve type and number of function arguments from the encoded type.
#
# Same caveats apply here as do with the selector.
types = list(_parse_method_type_string(method.types))
fn_rettype = types[0] if len(types) > 0 else None
# Build the function using the information we got.
fn_args: List[pwndbg.lib.functions.Argument] = []
fn_args_unk_count = 0
for arg_i in range(max(len(types) - 1, len(sel_args))):
if arg_i < len(sel_args):
name = sel_args[arg_i].decode("utf-8", errors="backslashreplace")
else:
# Name all arguments we don't have a name for "unknownX:"
name = f"unknown{fn_args_unk_count}"
fn_args_unk_count += 1
if arg_i < len(types) - 1:
ty = types[arg_i + 1].name_to_human_readable
else:
# Treat all arguments we don't know about as being `uintptr_t`s.
ty = "uintptr_t"
fn_args.append(pwndbg.lib.functions.Argument(type=ty, name=name, derefcnt=0))
return pwndbg.lib.functions.Function(
type=fn_rettype.name_to_human_readable if fn_rettype is not None else "void",
derefcnt=0,
name=sel.name.decode("utf-8", errors="backslashreplace"),
args=fn_args,
)
# Not a an Objective-C call or not a type we know about.
return None

@ -29,6 +29,7 @@ import pwndbg.lib.funcparser
import pwndbg.lib.functions import pwndbg.lib.functions
from pwndbg.aglib.disasm.instruction import PwndbgInstruction from pwndbg.aglib.disasm.instruction import PwndbgInstruction
from pwndbg.aglib.nearpc import c as N from pwndbg.aglib.nearpc import c as N
from pwndbg.lib.arch import Platform
from pwndbg.lib.functions import format_flags_argument from pwndbg.lib.functions import format_flags_argument
@ -83,6 +84,18 @@ def get(instruction: PwndbgInstruction) -> List[Tuple[pwndbg.lib.functions.Argum
name = name.replace("_chk", "") name = name.replace("_chk", "")
name = name.strip().lstrip("_") # _malloc name = name.strip().lstrip("_") # _malloc
func = None
if pwndbg.aglib.arch.platform == Platform.DARWIN:
# Try to resolve an Objective-C method call.
#
# Checking this first keeps us from resolving these as simple calls to
# `objc_msgSend` and functions like it, which have definitions that are
# rather barren of semantics in comparison.
func = pwndbg.aglib.objc.try_resolve_call_at_current_pc(instruction)
if func is None:
# If more specific call information can't be determined, use the regular
# function resolution flow.
func = pwndbg.lib.functions.functions.get(name, None) func = pwndbg.lib.functions.functions.get(name, None)
# Try to grab the data out of IDA # Try to grab the data out of IDA

Loading…
Cancel
Save