Minimalistic server (written in C) and a python3 client to allow calling native functions on a remote host for automation purposes
Simple RPC service providing an API for controlling every aspect of the target machine. Thie swiss army knife can be used for:
This project includes two components:
The python client utilizes the ability to call native functions in order to provide APIs for different aspects:
p.spawn()
)p.shell()
)p.fs.*
)p.network.*
)p.sysctl.*
)p.media.*
)p.preferences.*
)p.processes.*
)p.location.*
)p.hid.*
)
p.ioregistry.*
)p.reports.*
)p.time.*
)p.bluetooth.*
)p.location.*
)p.xpc.*
)p.mobile_gestalt.*
)p.backlight.*
)p.processes.get_by_basename(process_name).dump_app('/path/to/output')
)and much more…
# install the package
python3 -m pip install -U rpcclient
# enter shell
rpclocal
Download and execute the latest server artifact, according to your platform and arch from the latest GitHub action execution.
If your specific platform and arch isn’t listed, you can also build it yourself.
Install the latest client from PyPi:
python3 -m pip install -U rpcclient
Note: Cross-platform support is currently not available.
For macOS/iOS (Ensure that Xcode is installed):
brew install protobuf protobuf-c
python3 -m pip install mypy-protobuf protobuf grpcio-tools
git clone [email protected]:doronz88/rpc-project.git
cd rpc-project
make -C src/protos/ all
cd src/rpcserver
mkdir build
cd build
cmake .. -DTARGET=OSX
make
cmake .. -DTARGET=IOS
make
sudo apt-get install -y protobuf-compiler libprotobuf-dev libprotoc-dev protobuf-c-compiler
python3 -m pip mypy-protobuf protobuf grpcio-tools
git clone [email protected]:doronz88/rpc-project.git --recurse-submodules
cd rpc-project
make -C src/protos/ all
cd src/rpcserver
mkdir build
cd build
cmake .. -DTARGET=LINUX
make
To execute the server:
Usage: ./rpcserver [-p port] [-o (stdout|syslog|file:filename)]
-h show this help message
-o output. can be all of the following: stdout, syslog and file:filename. can be passed multiple times
Example usage:
./rpcserver -p 5910 -o syslog -o stdout -o file:/tmp/log.txt
Connecting via:
python3 -m rpcclient <HOST>
Full usage:
Usage: python -m rpcclient [OPTIONS] HOSTNAME
Options:
-p, --port INTEGER
-r, --rebind-symbols reload all symbols upon connection
-l, --load-all-libraries load all libraries
--help Show this message and exit.
NOTE: If you are attempting to connect to a jailbroken iOS device, you will be required to also create a TCP tunnel to your device. For example, using:
pymobiledevice3
:python3 -m pymobiledevice3 usbmux forward 5910 5910 -vvv
You should now get a nice iPython shell looking like this:
➜ rpc-project git:(master) python3 -m rpcclient 127.0.0.1
2022-03-29 23:42:31 Cyber root[24947] INFO connection uname.sysname: Darwin
Welcome to the rpcclient interactive shell! You interactive shell for controlling the remote rpcserver.
Feel free to use the following globals:
🌍 p - the injected process
🌍 symbols - process global symbols
Have a nice flight ✈️!
Starting an IPython shell... 🐍
Python 3.9.10 (main, Jan 15 2022, 11:48:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.25.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
And… Congrats! You are now ready to go! 😎
Try accessing the different features using the global p
variable.
For example (Just a tiny sample of the many things you can now do. Feel free to explore much more!):
In [2]: p.spawn(['sleep', '1'])
2022-03-29 23:45:51 Cyber root[24947] INFO shell process started as pid: 25047
Out[2]: SpawnResult(error=0, pid=25047, stdout=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>)
In [3]: p.fs.listdir('.')
Out[3]:
['common.c',
'.pytest_cache',
'Makefile',
'rpcserver_iphoneos_arm64',
'rpcserver.c',
'common.h',
'rpcserver_macosx_x86_64',
'ents.plist',
'build_darwin.sh']
In [4]: p.processes.get_by_pid(p.pid).fds
Out[4]:
[FileFd(fd=0, path='/dev/ttys000'),
FileFd(fd=1, path='/dev/ttys000'),
FileFd(fd=2, path='/dev/ttys000'),
Ipv6TcpFd(fd=3, local_address='0.0.0.0', local_port=5910, remote_address='0.0.0.0', remote_port=0),
Ipv6TcpFd(fd=4, local_address='127.0.0.1', local_port=5910, remote_address='127.0.0.1', remote_port=53217),
Ipv6TcpFd(fd=5, local_address='127.0.0.1', local_port=5910, remote_address='127.0.0.1', remote_port=53229),
Ipv6TcpFd(fd=6, local_address='127.0.0.1', local_port=5910, remote_address='127.0.0.1', remote_port=53530)]
In [5]: p.processes.get_by_pid(p.pid).regions[:3]
Out[5]:
[Region(region_type='__TEXT', start=4501995520, end=4502028288, vsize='32K', protection='r-x', protection_max='r-x', region_detail='/Users/USER/*/rpcserver_macosx_x86_64'),
Region(region_type='__DATA_CONST', start=4502028288, end=4502044672, vsize='16K', protection='r--', protection_max='rw-', region_detail='/Users/USER/*/rpcserver_macosx_x86_64'),
Region(region_type='__DATA', start=4502044672, end=4502061056, vsize='16K', protection='rw-', protection_max='rw-', region_detail='/Users/USER/*/rpcserver_macosx_x86_64')]
In rpc-project
, almost everything is wrapped using the Symbol
Object. Symbol is just a nicer way for referring to addresses
encapsulated with an object allowing to deref the memory inside, or use these addresses as functions.
In order to create a symbol from a given address, please use:
s = p.symbol(0x12345678)
# the Symbol object extends `int`
True == isinstance(s, int)
# write into this memory
s.poke(b'abc')
# peek(/read) 20 bytes of memory
print(s.peek(20))
# jump to `s` as a function, passing (1, "string") as its args
s(1, "string")
# change the size of each item_size inside `s` for derefs
s.item_size = 1
# *(char *)s = 1
s[0] = 1
# *(((char *)s)+1) = 1
s[1] = 1
# symbol inherits from int, so all int operations apply
s += 4
# change s item size back to 8 to store pointers
s.item_size = 8
# *(intptr_t *)s = 1
s[0] = 1
# storing the return value of the function executed at `0x11223344`
# into `*s`
s[0] = p.symbol(0x11223344)() # calling symbols also returns symbols
# query in which file 0x11223344 is loaded from
print(p.symbol(0x11223344).filename)
Usually you would want/need to use the symbols already mapped into the currently running process. To do so, you can
access them using symbols.<symbol-name>
. The symbols
global object is of type SymbolsJar
, which is a wrapper
to dict
for accessing all exported symbols. For example, the following will generate a call to the exported
malloc
function with 20
as its only argument:
x = symbols.malloc(20)
You can also just write their name as if they already were in the global scope. The client will check if no name collision
exists, and if so, will perform the following lazily for you:
x = malloc(20)
# is equivalent to:
malloc = symbols.malloc
x = malloc(20)
When working on Darwin based systems, it can be sometimes easier to use the builtin ObjC support.
# creating CF/NSObjets using the builtin cf() method
some_cf_string = p.cf('some string')
# create a new NSMutableDictionary
a = NSMutableDictionary.new()
# tell where this class is loaded from
print(NSMutableDictionary.bundle_path)
# which is a short-hand for objc_get_class(class_name)
a = p.objc_get_class('NSMutableDictionary').new()
# each darwin object is a "DarwinSymbol", instead of a "simple Symbol"
# that mean it has the special method: objc_call("selector", ...)
a.objc_call('setObject:forKey:', p.cf('value'), p.cf('key'))
# we can look at the CFDescription of every DarwinSymbol using the "cfdesc" property
a.cfdesc
# it can be easier to use further ObjC capabilities by converting the current DarwinSymbol into an ObjectiveCSymbol instead
a = a.objc_symbol
# We can now examine the class/objects properties
a.show()
# now we'll have a auto-complete for all of its selectors, ivars, etc.
a.setObject_forKey_(p.cf('value2'), p.cf('key2'))
# and we can easily convert this object to python native using the py() method. please note that this is done behind-the-scene using plistlib, meaning only plist-serializable objects (and None) can be coverted this way.
a = a.py()
# attempt to load all frameworks for auto-completions of all ObjC classes
# (equivalent to running the client with -l -r)
p.load_all_libraries()