Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,20 @@ Tested on Arch Linux with iOS 14.8.
`libimobiledevice` and `python3` must be installed. Ensure that the `usbmuxd` daemon is running.

### Windows
`python3` and iTunes must be installed. Ensure that the `AppleMobileDeviceService.exe` process is running.
`python3` and iTunes must be installed. Ensure that the `AppleMobileDeviceService.exe` process is running.
`libimobiledevice` will be downloaded as needed.

You will also need to install pywin32 from `requirements_windows.txt` for named pipe support.
```powershell
# Powershell:
# optional but suggested: set up and activate a venv
python3 -m venv .venv
.\.venv\Scripts\activate

# Install requirements
pip install -r requirements_windows.txt
```

## Usage

```
Expand All @@ -23,14 +34,22 @@ Tested on Arch Linux with iOS 14.8.
* pcapng: The default. Newer and allows for distinguishing between interfaces.
Wireshark 3.0+ supports streaming captures with this format.
* pcap: Older format for compatibility.
* `--udid`: device UDID
* `--udid`: device UDID
The specific device to target. If omitted, the first device found will be used.
* `outfile`: output file or FIFO, or `-` for standard output.
* On Windows, you can alternatively use: `--pipe arbitrary_pipe_name`

## Using with Wireshark
```
```sh
./rvi_capture.py - | wireshark -k -i -
```
On Windows:
```powershell
# first Powerhsell window
python rvi_capture.py --pipe rvi_capture
# second window:
& "$env:ProgramFiles\Wireshark\Wireshark.exe" -i"\\.\pipe\rvi_capture" -k
```

### Tips
- In Wireshark, you can filter for a particular network interface based on the
Expand Down
46 changes: 46 additions & 0 deletions output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import sys
import io

class OutputClosedError(Exception):
pass

class OutputFile:
def __init__(self, filename):
self._filename = filename
self._file = open(filename, 'wb', 0)

def write(self, data):
self._file.write(data)

def writelines(self, data):
self._file.writelines(data)

def close(self):
self._file.close()

def ready(self):
return True

def capture_instructions(self):
return f"Capturing to {self._filename} ..."

class OutputStdout:
def __init__(self):
self._stdout = sys.stdout.buffer
while isinstance(self._stdout, io.BufferedWriter):
self._stdout = self._stdout.detach()

def write(self, data):
self._stdout.write(data)

def writelines(self, data):
self._stdout.writelines(data)

def close(self):
self._stdout.close()

def ready(self):
return True

def capture_instructions(self):
return f"Capturing to stdout ..."
Binary file added requirements_windows.txt
Binary file not shown.
41 changes: 31 additions & 10 deletions rvi_capture.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3

import platform
import functools
import argparse
import ctypes
Expand All @@ -9,6 +10,9 @@
import sys
import time

from output import OutputFile, OutputStdout, OutputClosedError
if platform.system().lower().startswith("win"):
from windows_pipes import OutputPipe, add_windows_output_arguments

def load_cdll():
if sys.platform == 'linux':
Expand Down Expand Up @@ -345,31 +349,48 @@ def __init__(self, *args, **kwargs):
choices=('pcap', 'pcapng'), default='pcapng',
help='capture format')
parser.add_argument('--udid', help='device UDID (if more than 1 device)')
parser.add_argument('outfile', help='output file (- for stdout)')


if platform.system().lower().startswith("win"):
add_windows_output_arguments(parser)
else:
parser.add_argument('outfile', help='output file (- for stdout)')

args = parser.parse_args()
# open output file
if args.outfile == '-':
out_file = sys.stdout.buffer
while isinstance(out_file, io.BufferedWriter):
out_file = out_file.detach()
if args.outfile is None:
out_file = OutputPipe(args.pipe[0])
elif args.outfile == '-':
out_file = OutputStdout()
else:
out_file = open(args.outfile, 'wb', 0)
out_file = OutputFile(args.outfile)

# determine format to use
dumper_class = {
'pcap': PCAPPacketDumper,
'pcapng': NGPacketDumper,
}[args.format]
# start capture
stderr_print('capturing to {} ...'.format('<stdout>' if args.outfile == '-' else args.outfile))
stderr_print(out_file.capture_instructions())
num_packets = 0
def packet_callback(pkt):
nonlocal num_packets
num_packets += 1
stderr_print('\r{} packets captured.'.format(num_packets), end='', flush=True)


try:
packet_extractor = PacketExtractor(udid=args.udid)
packet_dumper = dumper_class(packet_extractor, out_file)
packet_dumper.run(packet_callback)
while True:
while not out_file.ready():
time.sleep(0.1)

try:
packet_extractor = PacketExtractor(udid=args.udid)
packet_dumper = dumper_class(packet_extractor, out_file)
packet_dumper.run(packet_callback)
except OutputClosedError:
stderr_print("\nOutput closed, waiting for reconnection ...")

except KeyboardInterrupt:
stderr_print()
stderr_print('closing capture ...')
Expand Down
114 changes: 114 additions & 0 deletions windows_pipes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import sys
import pywintypes
import win32pipe, win32file, win32event, winerror
from enum import Enum
from output import OutputClosedError

def add_windows_output_arguments(parser):
output_option = parser.add_mutually_exclusive_group(required=True)
output_option.add_argument('outfile', help='output file (- for stdout)', nargs="?")
output_option.add_argument('--pipe', help='output to named pipe', nargs=1)


class PipeState(Enum):
DISCONNECTED = 0
LISTEN_PENDING = 1
LISTENING = 2
CONNECTED = 3

class OutputPipe:
def __init__(self, pipe_name):
self._state = PipeState.DISCONNECTED
self._pipe_name = pipe_name
self._pipe = win32pipe.CreateNamedPipe(
self._pipe_path(),
win32pipe.PIPE_ACCESS_OUTBOUND,
win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_NOWAIT,
1, 65536, 65536,
0,
None)

self._connect_pipe()

def _pipe_path(self):
return f'\\\\.\\pipe\\{self._pipe_name}'

def _can_write(self):
if self._state != PipeState.CONNECTED:
self._connect_pipe()
return False
else:
return True

def _connect_pipe(self):
if self._state == PipeState.CONNECTED:
return

self._overlapped = pywintypes.OVERLAPPED()
result = 0
try:
result = win32pipe.ConnectNamedPipe(self._pipe, self._overlapped)
if result > 0:
self._state = PipeState.CONNECTED
except pywintypes.error as e:
self._handle_connect_results(result, e.winerror)

def _handle_connect_results(self, result, error):
# If result is nonzero, need to disconnect and reconnect
if result != 0:
self.reconnect()
else:
# Three options: pending listening, listening, or connected
if error == winerror.ERROR_PIPE_LISTENING:
self._state = PipeState.LISTENING
elif error == winerror.ERROR_IO_PENDING:
# Wait until we're listening
win32event.WaitForSingleObject(self._overlapped.hEvent, win32event.INFINITE)
try:
result = win32pipe.GetOverlappedResult(self._pipe, self._overlapped, False)
if result != 0:
print('Unexpected nonzero result opening pipe', file=sys.stderr, flush=True)
exit(1)
self._state = PipeState.LISTENING
except pywintypes.error as e:
self._handle_connect_results(result, e.winerror)
elif error == winerror.ERROR_PIPE_CONNECTED:
self._state = PipeState.CONNECTED
else:
print("UNKNOWN PIPE ERROR", error, flush=True, file=sys.stderr)

def reconnect(self):
win32pipe.DisconnectNamedPipe(self._pipe)
self._state = PipeState.DISCONNECTED
raise OutputClosedError()

def _handle_write_error(self, e):
if e.winerror == winerror.ERROR_BROKEN_PIPE:
self.reconnect()
elif e.winerror == winerror.ERROR_NO_DATA:
# Need to reconnect a formerly-used pipe
self.reconnect()
else:
raise e

def write(self, data):
if self._can_write():
try:
win32file.WriteFile(self._pipe, data)
except pywintypes.error as e:
self._handle_write_error(e)

def writelines(self, lines):
for line in lines:
self.write(line)

def close(self):
win32file.CloseHandle(self._pipe)

def ready(self):
return self._can_write()

def capture_instructions(self):
return (f"Waiting for connection {self._pipe_path()} ...\n"
f" Use Wireshark with:\n & \"$env:ProgramFiles\\Wireshark\\Wireshark.exe\" -i\"{self._pipe_path()}\" -k"
)