Skip to content

Commit

Permalink
Merge pull request #3 from doronz88/feature/forward
Browse files Browse the repository at this point in the history
cli: add lockdown/forward command
  • Loading branch information
matan1008 authored Apr 18, 2021
2 parents 404a72a + 8b9b299 commit 4622fbd
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 41 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ To understand the bits and bytes of the communication with `lockdownd` you are a

https://jon-gabilondo-angulo-7635.medium.com/understanding-usbmux-and-the-ios-lockdown-service-7f2a1dfd07ae

# Features

* TCP portwarding
* `pymobiledevice3 lockdown forward src_port dst_port`)
* Screenshots
* `pymobiledevice3 screenshot screen.png`
* Live and past syslogs
* `pymobiledevice3 syslog live`
* `pymobiledevice3 syslog archive syslogs.pax`
* Profile installation
* `pymobiledevice3 profile install/remove/list`
* Application management
* `pymobiledevice3 apps`
* File system management (AFC)
* `pymobiledevice3 afc`
* Crash reports management
* `pymobiledevice3 crash`
* Network sniffing
* `pymobiledevice3 pcap [out.pcap]`
* Raw shell for experimenting:
* `pymobiledevice3 lockdown service service_name`
* Mounting images
* `pymobiledevice3 mounter`
* DeveloperDiskImage features:
* Process management
* `pymobiledevice3 developer kill/launch/....`
* **Non-chrooted** directory listing
* `pymobiledevice3 developer ls /`
* Raw shell for experimenting:
* `pymobiledevice3 developer shell`

* And some more 😁

# Installation

```shell
Expand All @@ -29,16 +62,16 @@ Options:
Commands:
afc FileSystem utils
apps application options
config configuration options
crash crash utils
developer developer options
diagnostics diagnostics options
lockdown lockdown options
mounter mounter options
notification API for notify_post() & notify_register_dispatch().
pcap sniff device traffic
profile profile options
ps show process list
screenshot take a screenshot into a PNG format
screenshot take a screenshot in PNG format
syslog syslog options
```

Expand Down
36 changes: 28 additions & 8 deletions pymobiledevice3/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
#!/usr/bin/env python3
from functools import partial
from pprint import pprint
import logging
import tempfile
import json
import os

from pygments import highlight, lexers, formatters
from daemonize import Daemonize
from termcolor import colored
import coloredlogs
import click

from pymobiledevice3.afc import AFCShell, AFCClient
from pymobiledevice3.tcp_forwarder import TcpForwarder
from pymobiledevice3.services.diagnostics_service import DiagnosticsService
from pymobiledevice3.services.house_arrest_service import HouseArrestService
from pymobiledevice3.services.installation_proxy_service import InstallationProxyService
Expand Down Expand Up @@ -159,27 +163,27 @@ def apps_afc(lockdown, bundle_id):


@cli.group()
def config():
""" configuration options """
def profile():
""" profile options """
pass


@config.command('list', cls=Command)
def config_list(lockdown):
@profile.command('list', cls=Command)
def profile_list(lockdown):
""" list installed profiles """
pprint(MobileConfigService(lockdown=lockdown).get_profile_list())


@config.command('install', cls=Command)
@profile.command('install', cls=Command)
@click.argument('profile', type=click.File('rb'))
def config_install(lockdown, profile):
def profile_install(lockdown, profile):
""" install given profile file """
pprint(MobileConfigService(lockdown=lockdown).install_profile(profile.read()))


@config.command('remove', cls=Command)
@profile.command('remove', cls=Command)
@click.argument('name')
def config_remove(lockdown, name):
def profile_remove(lockdown, name):
""" remove profile by name """
pprint(MobileConfigService(lockdown=lockdown).remove_profile(name))

Expand All @@ -190,6 +194,22 @@ def lockdown():
pass


@lockdown.command('forward', cls=Command)
@click.argument('src_port', type=click.IntRange(1, 0xffff))
@click.argument('dst_port', type=click.IntRange(1, 0xffff))
@click.option('-d', '--daemonize', is_flag=True)
def lockdown_forward(lockdown, src_port, dst_port, daemonize):
""" forward tcp port """
forwarder = TcpForwarder(lockdown, src_port, dst_port)

if daemonize:
with tempfile.NamedTemporaryFile('wt') as pid_file:
daemon = Daemonize(app=f'forwarder {src_port}->{dst_port}', pid=pid_file.name, action=forwarder.start)
daemon.start()
else:
forwarder.start()


@lockdown.command('recovery', cls=Command)
def lockdown_recovery(lockdown):
""" enter recovery """
Expand Down
30 changes: 20 additions & 10 deletions pymobiledevice3/lockdown.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#!/usr/bin/env python3
import plistlib
import platform
import logging
import uuid
import sys
import os
import platform
import plistlib
import re
import sys
import uuid

from pymobiledevice3.service_connection import ServiceConnection
from pymobiledevice3.ca import ca_do_everything
from pymobiledevice3 import usbmux
from pymobiledevice3.ca import ca_do_everything
from pymobiledevice3.service_connection import ServiceConnection


class NotTrustedError(Exception):
Expand All @@ -36,6 +36,10 @@ class FatalPairingError(Exception):
pass


class NoDeviceConnected(Exception):
pass


# we store pairing records and ssl keys in ~/.pymobiledevice3
HOMEFOLDER = ".pymobiledevice3"
MAXTRIES = 20
Expand Down Expand Up @@ -78,7 +82,7 @@ def write_home_file(folder_name, filename, data):

def list_devices():
mux = usbmux.USBMux()
mux.process(1)
mux.process(0.1)
return [d.serial for d in mux.devices]


Expand All @@ -87,10 +91,16 @@ class LockdownClient(object):
SERVICE_PORT = 62078

def __init__(self, udid=None, client_name=DEFAULT_CLIENT_NAME):
if udid is None:
available_udids = list_devices()
if len(available_udids) == 0:
raise NoDeviceConnected()
udid = available_udids[0]

self.logger = logging.getLogger(__name__)
self.paired = False
self.SessionID = None
self.service = ServiceConnection(self.SERVICE_PORT, udid)
self.service = ServiceConnection.create(udid, self.SERVICE_PORT)
self.host_id = self.generate_host_id()
self.system_buid = self.generate_host_id()
self.paired = False
Expand All @@ -112,7 +122,7 @@ def __init__(self, udid=None, client_name=DEFAULT_CLIENT_NAME):

if not self.validate_pairing():
self.pair()
self.service = ServiceConnection(self.SERVICE_PORT, udid)
self.service = ServiceConnection.create(udid, self.SERVICE_PORT)
if not self.validate_pairing():
raise FatalPairingError
self.paired = True
Expand Down Expand Up @@ -313,7 +323,7 @@ def start_service(self, name, ssl=False, escrow_bag=None) -> ServiceConnection:
raise StartServiceError(
'your device is protected with password, please enter password in device and try again')
raise StartServiceError(response.get("Error"))
service_connection = ServiceConnection(response.get('Port'), self.udid)
service_connection = ServiceConnection.create(self.udid, response.get('Port'))
if ssl_enabled:
service_connection.ssl_start(self.ssl_file, self.ssl_file)
return service_connection
Expand Down
43 changes: 22 additions & 21 deletions pymobiledevice3/service_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,41 @@ class ConnectionFailedException(Exception):


class ServiceConnection(object):
def __init__(self, port, udid=None, logger=None):
self.logger = logger or logging.getLogger(__name__)
self.port = port
self.connect(udid)
def __init__(self, socket):
self.logger = logging.getLogger(__name__)
self.socket = socket

def connect(self, udid=None):
@staticmethod
def create(udid, port):
mux = usbmux.USBMux()
mux.process(1.0)
dev = None
target_device = None

while not dev and mux.devices:
while target_device is None:
mux.process(1.0)
if udid:
for d in mux.devices:
if d.serial == udid:
dev = d
else:
dev = mux.devices[0]
self.logger.info(f'Connecting to device: {dev.serial}')
for connected_device in mux.devices:
if connected_device.serial == udid:
target_device = connected_device
break
try:
self.s = mux.connect(dev, self.port)
socket = mux.connect(target_device, port)
except:
raise ConnectionFailedException("Connection to device port %d failed" % self.port)
return dev.serial
raise ConnectionFailedException(f'Connection to device port {port} failed')

return ServiceConnection(socket)

def setblocking(self, blocking: bool):
self.socket.setblocking(blocking)

def close(self):
self.s.close()
self.socket.close()

def recv(self, length=4096):
data = self.s.recv(length)
data = self.socket.recv(length)
return data

def send(self, data):
self.s.sendall(data)
self.socket.sendall(data)

def send_request(self, data):
self.send_plist(data)
Expand Down Expand Up @@ -116,7 +117,7 @@ def send_plist(self, d):
return self.send(l + payload)

def ssl_start(self, keyfile, certfile):
self.s = ssl.wrap_socket(self.s, keyfile, certfile, ssl_version=ssl.PROTOCOL_TLSv1)
self.socket = ssl.wrap_socket(self.socket, keyfile, certfile, ssl_version=ssl.PROTOCOL_TLSv1)

def shell(self):
IPython.embed(
Expand Down
100 changes: 100 additions & 0 deletions pymobiledevice3/tcp_forwarder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import logging
import socket
import select

from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.service_connection import ServiceConnection, ConnectionFailedException


class TcpForwarder:
MAX_FORWARDED_CONNECTIONS = 200

def __init__(self, lockdown: LockdownClient, src_port: int, dst_port: int):
self.logger = logging.getLogger(__name__)
self.lockdown = lockdown
self.src_port = src_port
self.dst_port = dst_port
self.inputs = []

# dictionaries containing the required maps to transfer data between each local
# socket to its remote socket and vice versa
self.connections = {}

def start(self):
"""
forward each connection from given local machine port to remote device port
"""
# create local tcp server socket
self.server_socket = socket.socket()
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind(('0.0.0.0', self.src_port))
self.server_socket.listen(self.MAX_FORWARDED_CONNECTIONS)
self.server_socket.setblocking(False)

self.inputs = [self.server_socket]

local_connection = None
remote_connection = None

while self.inputs:
# will only perform the socket select on the inputs. the outputs will handled
# as synchronous blocking
readable, writable, exceptional = select.select(self.inputs, [], self.inputs)

for current_sock in readable:
if current_sock is self.server_socket:
self._handle_server_connection()
else:
self._handle_data(current_sock)

for current_sock in exceptional:
self._handle_close_or_error(current_sock)

def _handle_close_or_error(self, from_sock):
# if an error occurred its time to close the two sockets
other_sock = self.connections[current_sock]

other_sock.close()
current_sock.close()
inputs.remove(other_sock)
inputs.remove(current_sock)

self.logger.info(f'connection {other_sock} was closed')

def _handle_data(self, from_sock):
data = from_sock.recv(1024)

if data is None:
# no data means socket was closed
self._handle_close_or_error()
return

# when data is received from one end, just forward it to the other
other_sock = self.connections[from_sock]

# send the data in blocking manner
other_sock.setblocking(True)
other_sock.sendall(data)
other_sock.setblocking(False)

def _handle_server_connection(self):
# accept the connection from local machine and attempt to connect at remote
local_connection, client_address = self.server_socket.accept()
local_connection.setblocking(False)

try:
remote_connection = ServiceConnection.create(self.lockdown.udid, self.dst_port).socket
except ConnectionFailedException:
self.logger.error(f'failed to connect to port: {self.dst_port}')
local_connection.close()
remote_connection.setblocking(False)

# append the newly created sockets into input list
self.inputs.append(local_connection)
self.inputs.append(remote_connection)

# and store a map of which local connection is transferred to which remote one
self.connections[remote_connection] = local_connection
self.connections[local_connection] = remote_connection

self.logger.info(f'connection established from local to remote port {self.dst_port}')
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ bpylist2
pygments
hexdump
arrow
daemonize

0 comments on commit 4622fbd

Please sign in to comment.