Skip to content

Commit

Permalink
Merge pull request #118 from doronz88/feature/vmu
Browse files Browse the repository at this point in the history
darwin: processes: add vmu api
  • Loading branch information
doronz88 authored Mar 29, 2022
2 parents 2a2bce9 + 7380991 commit ac3938f
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 10 deletions.
4 changes: 2 additions & 2 deletions src/rpcclient/rpcclient/darwin/objective_c_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ def show(self):
"""
print(highlight(str(self), ObjectiveCLexer(), TerminalTrueColorFormatter(style='native')))

def objc_call(self, sel: str, *args):
def objc_call(self, sel: str, *args, **kwargs):
"""
Invoke a selector on the given class object.
:param sel: Selector name.
:return: whatever the selector returned as a symbol.
"""
return self._class_object.objc_call(sel, *args)
return self._class_object.objc_call(sel, *args, **kwargs)

def get_method(self, name: str):
"""
Expand Down
4 changes: 2 additions & 2 deletions src/rpcclient/rpcclient/darwin/objective_c_symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ def show(self, recursive: bool = False):
"""
print(highlight(self._to_str(recursive), ObjectiveCLexer(), TerminalTrueColorFormatter(style='native')))

def objc_call(self, selector: str, *params):
def objc_call(self, selector: str, *params, **kwargs):
"""
Make objc_call() from self return ObjectiveCSymbol when it's an objc symbol.
:param selector: Selector to execute.
:param params: Additional parameters.
:return: ObjectiveCSymbol when return type is an objc symbol.
"""
symbol = super(ObjectiveCSymbol, self).objc_call(selector, *params)
symbol = super(ObjectiveCSymbol, self).objc_call(selector, *params, **kwargs)
return symbol.objc_symbol if self._client.is_objc_type(symbol) else symbol

def _set_ivar(self, name, value):
Expand Down
153 changes: 147 additions & 6 deletions src/rpcclient/rpcclient/darwin/processes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dataclasses
import errno
from collections import namedtuple
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Mapping

Expand All @@ -14,9 +15,14 @@
PROC_PIDFDSOCKETINFO, socket_fdinfo, so_kind_t, so_family_t, PROX_FDTYPE_PIPE, PROC_PIDFDPIPEINFO, pipe_info, \
task_dyld_info_data_t, TASK_DYLD_INFO_COUNT, all_image_infos_t, dyld_image_info_t, x86_thread_state64_t, \
arm_thread_state64_t
from rpcclient.exceptions import BadReturnValueError, ArgumentError
from rpcclient.darwin.symbol import DarwinSymbol
from rpcclient.exceptions import BadReturnValueError, ArgumentError, SymbolAbsentError, MissingLibraryError
from rpcclient.processes import Processes
from rpcclient.structs.consts import SIGTERM
from rpcclient.protocol import arch_t
from rpcclient.structs.consts import SIGTERM, RTLD_NOW

_CF_STRING_ARRAY_PREFIX_LEN = len(' "')
_CF_STRING_ARRAY_SUFFIX_LEN = len('",')

FdStruct = namedtuple('FdStruct', 'fd struct')

Expand Down Expand Up @@ -149,16 +155,35 @@ def set_state(self, state: Mapping):
raise BadReturnValueError('thread_set_state() failed')


@dataclasses.dataclass
class Region:
region_type: str
start: int
end: int
vsize: str
protection: str
protection_max: str
region_detail: str


@dataclasses.dataclass
class LoadedClass:
name: str
type_name: str
binary_path: str


class Process:
PEEK_STR_CHUNK_SIZE = 0x100

def __init__(self, client, pid: int):
self._client = client
self._pid = pid

self._thread_class = IntelThread64
if self._client.uname.machine != 'x86_64':
if self._client.arch == arch_t.ARCH_ARM64:
self._thread_class = ArmThread64
else:
self._thread_class = IntelThread64

def kill(self, sig: int = SIGTERM):
""" kill(pid, sig) at remote. read man for more details. """
Expand Down Expand Up @@ -195,6 +220,28 @@ def poke(self, address: int, buf: bytes):
if self._client.symbols.vm_write(self.task, address, buf, len(buf)):
raise BadReturnValueError('vm_write() failed')

def get_symbol_name(self, address: int) -> str:
if self._client.arch != arch_t.ARCH_ARM64:
raise NotImplementedError('implemented only on ARCH_ARM64')
x = self.vmu_object_identifier.objc_call('symbolForAddress:', address, return_raw=True).x
if x[0] == 0 and x[1] == 0:
raise SymbolAbsentError()
return self._client.symbols.CSSymbolGetName(x[0], x[1]).peek_str()

def get_symbol_address(self, name: str, lib: str) -> int:
return self.vmu_object_identifier.objc_call('addressOfSymbol:inLibrary:', name, lib).c_uint64

@property
def loaded_classes(self):
realized_classes = self.vmu_object_identifier.objc_call('realizedClasses')

for i in range(1, realized_classes.objc_call('count') + 1):
class_info = realized_classes.objc_call('classInfoForIndex:', i)
name = class_info.objc_call('className').py
type_name = class_info.objc_call('typeName').py
binary_path = class_info.objc_call('binaryPath').py
yield LoadedClass(name=name, type_name=type_name, binary_path=binary_path)

@property
def images(self) -> List[Image]:
""" get loaded image list """
Expand Down Expand Up @@ -335,6 +382,28 @@ def task_all_info(self):
raise BadReturnValueError('proc_pidinfo(PROC_PIDTASKALLINFO) failed')
return proc_taskallinfo.parse_stream(pti)

@property
def callstacks(self) -> str:
result = ''
for bt in self._client.symbols.objc_getClass('VMUSampler').objc_call('sampleAllThreadsOfTask:', self.task).py:
result += bt.objc_call('description').py + '\n'
return result

@cached_property
def vmu_proc_info(self) -> DarwinSymbol:
return self._client.symbols.objc_getClass('VMUProcInfo').objc_call('alloc').objc_call('initWithTask:',
self.task)

@cached_property
def vmu_region_identifier(self) -> DarwinSymbol:
return self._client.symbols.objc_getClass('VMUVMRegionIdentifier').objc_call('alloc').objc_call('initWithTask:',
self.task)

@cached_property
def vmu_object_identifier(self) -> DarwinSymbol:
return self._client.symbols.objc_getClass('VMUObjectIdentifier').objc_call('alloc').objc_call('initWithTask:',
self.task)

@cached_property
def task(self) -> int:
with self._client.safe_malloc(8) as p_task:
Expand Down Expand Up @@ -382,13 +451,85 @@ def ruid(self) -> int:
def rgid(self) -> int:
return self.task_all_info.pbsd.pbi_rgid

@cached_property
def start_time(self) -> datetime:
if self._client.arch != arch_t.ARCH_ARM64:
raise NotImplementedError('implemented only on ARCH_ARM64')
val = self.vmu_proc_info.objc_call('startTime', return_raw=True)
tv_sec = val.x[0]
tv_nsec = val.x[1]
return datetime.fromtimestamp(tv_sec + (tv_nsec / (10 ** 9)))

@property
def environ(self) -> List[str]:
return self.vmu_proc_info.objc_call('envVars').py

@property
def arguments(self) -> List[str]:
return self.vmu_proc_info.objc_call('arguments').py

@property
def regions(self) -> List[Region]:
result = []

# remove the '()' wrapping the list:
# (
# "item1",
# "item2",
# )
buf = self.vmu_region_identifier.objc_call('regions').cfdesc.split('\n')[1:-1]

for line in buf:
# remove line prefix and suffix and split into words
line = line[_CF_STRING_ARRAY_PREFIX_LEN:-_CF_STRING_ARRAY_SUFFIX_LEN].split()
i = 0

region_type = line[i]
i += 1

while '-' not in line[i]:
# skip till address range
i += 1

address_range = line[i]
i += 1
start, end = address_range.split('-')
start = int(start, 0)
end = int(end, 0)
vsize = line[i].split('V=', 1)[1].split(']', 1)[0]
i += 1
protection = line[i].split('/')
i += 1

region_detail = None
if len(line) >= i + 1:
region_detail = line[i]

result.append(Region(region_type=region_type, start=start, end=end, vsize=vsize, protection=protection[0],
protection_max=protection[1], region_detail=region_detail))

return result

def __repr__(self):
return f'<{self.__class__.__name__} PID:{self.pid} PATH:{self.path}>'


class DarwinProcesses(Processes):
""" manage processes """

def __init__(self, client):
super().__init__(client)
self._load_symbolication_library()

def _load_symbolication_library(self):
options = [
'/System/Library/PrivateFrameworks/Symbolication.framework/Symbolication'
]
for option in options:
if self._client.dlopen(option, RTLD_NOW):
return
raise MissingLibraryError('Symbolication library isn\'t available')

def get_by_pid(self, pid: int) -> Process:
""" get process object by pid """
proc_list = self.list()
Expand All @@ -398,10 +539,10 @@ def get_by_pid(self, pid: int) -> Process:
raise ArgumentError(f'failed to locate process with pid: {pid}')

def get_by_basename(self, name: str) -> Process:
""" get process object by name """
""" get process object by basename """
proc_list = self.list()
for p in proc_list:
if p.name == name:
if p.basename == name:
return p
raise ArgumentError(f'failed to locate process with name: {name}')

Expand Down
5 changes: 5 additions & 0 deletions src/rpcclient/rpcclient/symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def c_int64(self) -> int:
""" cast to c_int64 """
return ctypes.c_int64(self).value

@property
def c_uint64(self) -> int:
""" cast to c_uint64 """
return ctypes.c_uint64(self).value

@property
def c_int32(self) -> int:
""" cast to c_int32 """
Expand Down

0 comments on commit ac3938f

Please sign in to comment.