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**

@ -12,6 +12,8 @@ from __future__ import annotations
from typing import Callable
from typing import Generator
from typing import Generic
from typing import List
from typing import Tuple
from typing import TypeVar
from typing_extensions import override
@ -22,6 +24,8 @@ import pwndbg.aglib.macho
import pwndbg.aglib.memory
import pwndbg.aglib.symbol
import pwndbg.aglib.typeinfo
from pwndbg.aglib.disasm.instruction import PwndbgInstruction
from pwndbg.dbg import Type
T = TypeVar("T")
@ -904,3 +908,251 @@ class _ClassPropertyList(_EntList[ClassProperty]):
@override
def _from_ptr(self, ptr: int) -> ClassProperty:
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
from pwndbg.aglib.disasm.instruction import PwndbgInstruction
from pwndbg.aglib.nearpc import c as N
from pwndbg.lib.arch import Platform
from pwndbg.lib.functions import format_flags_argument
@ -83,7 +84,19 @@ def get(instruction: PwndbgInstruction) -> List[Tuple[pwndbg.lib.functions.Argum
name = name.replace("_chk", "")
name = name.strip().lstrip("_") # _malloc
func = pwndbg.lib.functions.functions.get(name, None)
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)
# Try to grab the data out of IDA
if not func and target:

Loading…
Cancel
Save