Skip to content

Commit

Permalink
Client: Add ObjectiveC symbol.
Browse files Browse the repository at this point in the history
  • Loading branch information
matan1008 committed Feb 15, 2022
1 parent f191809 commit ad7121e
Show file tree
Hide file tree
Showing 10 changed files with 773 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/rpcclient/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ IPython
traitlets
cached-property
dataclasses; python_version<"3.7"
pygments
objc_types_decoder
17 changes: 13 additions & 4 deletions src/rpcclient/rpcclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from rpcclient.fs import Fs
from rpcclient.network import Network
from rpcclient.processes import Processes
from rpcclient.protocol import protocol_message_t, cmd_type_t, exec_chunk_t, exec_chunk_type_t, UNAME_VERSION_LEN
from rpcclient.protocol import protocol_message_t, cmd_type_t, exec_chunk_t, exec_chunk_type_t, UNAME_VERSION_LEN, \
reply_protocol_message_t
from rpcclient.darwin.structs import pid_t, exitcode_t
from rpcclient.symbol import Symbol
from rpcclient.symbols_jar import SymbolsJar
Expand Down Expand Up @@ -164,6 +165,9 @@ def peek(self, address: int, size: int) -> bytes:
'data': {'address': address, 'size': size},
})
self._sock.sendall(message)
reply = protocol_message_t.parse(self._recvall(reply_protocol_message_t.sizeof()))
if reply.cmd_type == cmd_type_t.CMD_REPLY_ERROR:
raise ArgumentError()
return self._recvall(size)

def poke(self, address: int, data: bytes):
Expand Down Expand Up @@ -258,11 +262,16 @@ def safe_calloc(self, size: int):

@contextlib.contextmanager
def safe_malloc(self, size: int):
x = self.symbols.malloc(size)
try:
with self.freeing(self.symbols.malloc(size)) as x:
yield x

@contextlib.contextmanager
def freeing(self, symbol):
try:
yield symbol
finally:
self.symbols.free(x)
if symbol:
self.symbols.free(symbol)

def interactive(self):
""" Start an interactive shell """
Expand Down
59 changes: 59 additions & 0 deletions src/rpcclient/rpcclient/darwin/client.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
from collections import namedtuple
from functools import lru_cache
import struct
import typing

from cached_property import cached_property

from rpcclient.client import Client
from rpcclient.darwin import objective_c_class
from rpcclient.darwin.fs import DarwinFs
from rpcclient.darwin.media import DarwinMedia
from rpcclient.darwin.network import DarwinNetwork
from rpcclient.darwin.objective_c_symbol import ObjectiveCSymbol
from rpcclient.darwin.processes import DarwinProcesses
from rpcclient.darwin.preferences import Preferences
from rpcclient.exceptions import RpcClientException
from rpcclient.darwin.structs import utsname
from rpcclient.darwin.consts import kCFNumberSInt64Type, kCFNumberDoubleType
from rpcclient.darwin.symbol import DarwinSymbol

IsaMagic = namedtuple('IsaMagic', 'mask value')
ISA_MAGICS = [
# ARM64
IsaMagic(mask=0x000003f000000001, value=0x000001a000000001),
# X86_64
IsaMagic(mask=0x001f800000000001, value=0x001d800000000001),
]
# Mask for tagged pointer, from objc-internal.h
OBJC_TAG_MASK = (1 << 63)


class DarwinClient(Client):

Expand Down Expand Up @@ -103,3 +117,48 @@ def cf(self, o: object):
return self.symbols.CFDictionaryCreate(0, keys_buf, values_buf, len(cfvalues), 0, 0, 0)
else:
raise NotImplementedError()

def objc_symbol(self, address) -> ObjectiveCSymbol:
"""
Get objc symbol wrapper for given address
:param address:
:return: ObjectiveC symbol object
"""
return ObjectiveCSymbol.create(int(address), self)

@lru_cache(maxsize=None)
def objc_get_class(self, name: str):
"""
Get ObjC class object
:param name:
:return:
"""
return objective_c_class.Class.from_class_name(self, name)

@staticmethod
def is_objc_type(symbol: DarwinSymbol) -> bool:
"""
Test if a given symbol represents an objc object
:param symbol:
:return:
"""
# Tagged pointers are ObjC objects
if symbol & OBJC_TAG_MASK == OBJC_TAG_MASK:
return True

# Class are not ObjC objects
for mask, value in ISA_MAGICS:
if symbol & mask == value:
return False

try:
with symbol.change_item_size(8):
isa = symbol[0]
except RpcClientException:
return False

for mask, value in ISA_MAGICS:
if isa & mask == value:
return True

return False
209 changes: 209 additions & 0 deletions src/rpcclient/rpcclient/darwin/objc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
from collections import namedtuple
from typing import Mapping
from dataclasses import dataclass, field
from functools import lru_cache

from objc_types_decoder.decode import decode as decode_type, decode_with_tail

Property = namedtuple('Property', 'name attributes')
PropertyAttributes = namedtuple('PropertyAttributes', 'synthesize type_ list')


def convert_encoded_property_attributes(encoded):
conversions = {
'R': lambda x: 'readonly',
'C': lambda x: 'copy',
'&': lambda x: 'strong',
'N': lambda x: 'nonatomic',
'G': lambda x: 'getter=' + x[1:],
'S': lambda x: 'setter=' + x[1:],
'd': lambda x: 'dynamic',
'W': lambda x: 'weak',
'P': lambda x: '<garbage-collected>',
't': lambda x: 'encoding=' + x[1:],
}

type_, tail = decode_with_tail(encoded[1:])
attributes = []
synthesize = None
for attr in filter(None, tail.lstrip(',').split(',')):
if attr[0] in conversions:
attributes.append(conversions[attr[0]](attr))
elif attr[0] == 'V':
synthesize = attr[1:]

return PropertyAttributes(type_=type_, synthesize=synthesize, list=attributes)


@dataclass
class Method:
name: str
address: int = field(compare=False)
type_: str = field(compare=False)
return_type: str = field(compare=False)
is_class: bool = field(compare=False)
args_types: list = field(compare=False)

@staticmethod
def from_data(data: Mapping, client):
"""
Create Method object from raw data.
:param data: Data as loaded from get_objectivec_symbol_data.m.
:param rpcclient.darwin.client.DarwinClient client: Darwin client.
"""
return Method(
name=data['name'],
address=client.symbol(data['address']),
type_=data['type'],
return_type=decode_type(data['return_type']),
is_class=data['is_class'],
args_types=list(map(decode_type, data['args_types']))
)

def __str__(self):
if ':' in self.name:
args_names = self.name.split(':')
name = ' '.join(['{}:({})'.format(*arg) for arg in zip(args_names, self.args_types[2:])])
else:
name = self.name
prefix = '+' if self.is_class else '-'
return f'{prefix} {name}; // 0x{self.address:x} (returns: {self.return_type})\n'


def _class_data_list(client, class_, function):
with client.safe_malloc(4) as out_count:
out_count.item_size = 4
c_result = client.symbols.get_lazy(function)(class_, out_count)
count = out_count[0]
for i in range(count):
yield c_result[i]
if c_result:
client.symbols.free(c_result)


@lru_cache(maxsize=None)
def get_class_protocols(client, class_):
return [
client.symbols.protocol_getName(protocol).peek_str()
for protocol in _class_data_list(client, class_, 'class_copyProtocolList')
]


def _iter_class_ivars(client, class_):
for ivar in _class_data_list(client, class_, 'class_copyIvarList'):
yield {
'name': client.symbols.ivar_getName(ivar).peek_str(),
'type': decode_type(client.symbols.ivar_getTypeEncoding(ivar).peek_str()),
'offset': client.symbols.ivar_getOffset(ivar),
}


@lru_cache(maxsize=None)
def get_class_ivars(client, class_):
return sorted(_iter_class_ivars(client, class_), key=lambda ivar: ivar['offset'])


@lru_cache(maxsize=None)
def get_super(client, class_):
return client.symbols.class_getSuperclass(class_)


@lru_cache(maxsize=None)
def get_class_name(client, class_):
return client.symbols.class_getName(class_).peek_str()


def _iter_until_super(client, class_):
while True:
yield class_
class_ = client.symbols.class_getSuperclass(class_)
if not class_:
break


def _iter_object_ivars(client, class_, object_):
for objc_class in _iter_until_super(client, class_):
for ivar in _class_data_list(client, objc_class, 'class_copyIvarList'):
yield {
'name': client.symbols.ivar_getName(ivar).peek_str(),
'type': decode_type(client.symbols.ivar_getTypeEncoding(ivar).peek_str()),
'offset': client.symbols.ivar_getOffset(ivar),
'value': client.symbols.object_getIvar(object_, ivar)
}


def get_object_ivars(client, class_, object_):
ivars = sorted(_iter_object_ivars(client, class_, object_), key=lambda ivar: ivar['offset'])
for i, ivar in enumerate(ivars):
value = ivar['value']
if i < len(ivars) - 1:
# The .fm file returns a 64bit value, regardless of the real size.
size = ivars[i + 1]['offset'] - ivar['offset']
ivar['value'] = value & ((2 ** (size * 8)) - 1)
return ivars


def _iter_class_properties(client, class_):
for property_ in _class_data_list(client, class_, 'class_copyPropertyList'):
yield {
'name': client.symbols.property_getName(property_).peek_str(),
'attributes': client.symbols.property_getAttributes(property_).peek_str(),
}


@lru_cache(maxsize=None)
def get_class_properties(client, class_):
return [
Property(name=prop['name'], attributes=convert_encoded_property_attributes(prop['attributes']))
for prop in _iter_class_properties(client, class_)
]


def _iter_object_properties(client, class_):
fetched_properties = []
for objc_class in _iter_until_super(client, class_):
for property in _iter_class_properties(client, objc_class):
if property['name'] in fetched_properties:
continue
fetched_properties.append(property['name'])
yield property


@lru_cache(maxsize=None)
def get_object_properties(client, class_):
return [
Property(name=prop['name'], attributes=convert_encoded_property_attributes(prop['attributes']))
for prop in _iter_object_properties(client, class_)
]


def _iter_class_methods(client, class_):
for method in _class_data_list(client, class_, 'class_copyMethodList'):
args_types = []
for arg in range(client.symbols.method_getNumberOfArguments(method)):
with client.freeing(
client.symbols.method_copyArgumentType(method, arg)) as method_arguments_types:
args_types.append(method_arguments_types.peek_str())
with client.freeing(client.symbols.method_copyReturnType(method)) as method_return_type:
return_type = method_return_type.peek_str()
yield {
'name': client.symbols.sel_getName(client.symbols.method_getName(method)).peek_str(),
'address': client.symbols.method_getImplementation(method),
'type': client.symbols.method_getTypeEncoding(method).peek_str(),
'return_type': return_type,
'args_types': args_types,
}


def _iter_methods(client, class_):
for method in _iter_class_methods(client, client.symbols.object_getClass(class_)):
method['is_class'] = True
yield method
for method in _iter_class_methods(client, class_):
method['is_class'] = False
yield method


@lru_cache(maxsize=None)
def get_class_methods(client, class_):
return [Method.from_data(method, client) for method in _iter_methods(client, class_)]
Loading

0 comments on commit ad7121e

Please sign in to comment.