CapIoT is a command-line tool to automate IoT experiments on Android & iOS apps and capture their network traffic. It’s built for security engineers and researchers who want repeatable, scripted interactions on LAN and WAN setups and complete traffic traces—with optional TLS interception and frida hooks.
- What Can I Use It For?
- Key Features
- Getting Started
- Configuration
- Commands
- Platform Notes
- Extending CapIoT
Example scenario — App ↔ IoT device analysis (same LAN):
You want to understand how the IoT mobile app communicates with your smart plug on your home network.
With CapIoT you can:
- automatically launch the app, tap through actions like “Power On/Off,”
- capture traffic on the server (
tcpdumpon wifi interface) and on the phone (PCAPdroid/iOS tcpdump), - Run experiments where the app and device are on the same network (LAN) and the device and phone are on different networks (WAN)
- proxy traffic via mitmproxy and export SSL keys,
- bypass certificate pinning via frida,
- repeat the same steps multiple times (no-frida / frida phases) to compare behavior,
- validate UI changes with screenshots and similarity checks.
- Setup (optional): record a brief setup phase (e.g., device onboarding) with captures running.
- Record coordinates.
- No-Frida phase:
- start server capture (tcpdump)
- start phone capture (PCAPdroid / tcpdump on iOS)
- launch app → run scripted
tap X Yinteractions - take screenshots and compare vs. baselines (SSIM)
- keep capturing for a post-interaction window to include all related traffic
- Frida phase:
- run
mitmdump(transparent mode) and SSL key log - start
fridahooks (e.g., TLS unpinning) - repeat interactions & captures
- run
- Tear-down: stop captures, copy artifacts, summarize iterations.
- 🚀 One command to run full experiments (per-run artifact folders)
- 🌐 LAN & WAN profiles: capture on the same network or different networks.
- 🔍 TLS introspection – MITM, SSL key log export, frida hooks.
- 📸 UI automation (
tap X Y) + screenshot similarity (SSIM) - 🧰 Utilities:
check-configdump-apprecord-coordinates
- 🧩 Extensible: add your own runner via
@register(priority=N) - 🗂️ Clear folder structure per run (
frida/,no_frida/,mitm/,sslkeys/,logs/)
# Clone repository
git clone https://github.com/SecPriv/CapIoT.git
cd capiot
# Create & activate virtual environment (choose one or both)
python3 -m venv .android # or: python3 -m venv .ios
source .android/bin/activate # or. source .ios/bin/activate
# Install dependencies for your platform
pip install -U pip
pip install '.[android]' # or: pip install '.[ios]'Follow SETUP.md for server and device prerequisites.
Get device identifiers:
adb devices # Android: ADB device id
idevice_id -l # iOS: UDIDCapIoT reads a YAML file describing platform, profile, and paths.
A template is provided in config/config.yaml. Check out also some example configuration files in config/examples.
To reduce false positives from dynamic UI elements (e.g., the clock, notifications, rotating banners), we crop the screenshot to a region of interest and compute similarity only within that region.
Example:
{
"iot_device_name": [
{"x": 258, "y": 2142, "width": 123, "height": 114},
{"x": 258, "y": 2142, "width": 123, "height": 114}
]
}
You can provide custom delays between certain actions. Simply add the path to your config file (see config.yaml).
The file is hot-reloaded, so any tweaks to those sleep times take effect immediately while experiments are running.
Example:
start_app: 15 # wait after launching the app
stop_app: 10 # wait after stopping the app
after_tap: 2 # delay after each tap before screenshot
after_similarity: 9 # delay after screenshot comparison
between_iterations: 300 # keep capturing after UI action to include all related traffic| Command | Purpose |
|---|---|
capiot check-config |
Validate a YAML config |
capiot record-coordinates |
Capture baseline screenshots & tap points |
capiot run |
Execute the full experiment pipeline |
capiot dump-app |
Pull APK/IPA & permissions for static analysis |
capiot check-config --config /path/to/config.yamlcapiot record-coordinates --platform <android or ios> --phone-id <ADB_ID or UDID> --package-name com.example.app --device-name iot_device_name --output /data/coordsArtefacts are written to /data/coords/iot_device_name:
- Text file:
<data/coords/iot_device_name>/<iot_device_name>.txt.
Each actionable line must be exactly:tap X Y - Baseline screenshots
On iOS, the coordinate recorder cannot automatically capture taps due to iOS limitations. Instead:
- Run
record-coordinatesto capture screenshots. - Open each screenshot in GIMP.
- Click the target and note pixel coordinates.
- Add
tap x ylines to coordinates file (i.e.,<data/coords/iot_device_name>/<iot_device_name>.txt).
capiot run -p com.example.app -i <ADB_ID or UDID> -d iot_device_name --config /path/to/config.yamlArtifacts are written to:
<output_path>/<device_name>/<YYYY-MM-DD_HH-MM>/
frida/
no_frida/
mitm/
sslkeys/
logs/
# Android
capiot dump-app -f android -p com.example.app -i <ADB_ID> -o ./apk
# iOS
capiot dump-app -f ios -p com.example.app -i <UDID> --ssh-host 192.168.1.13 --ssh-port 22 --ssh-user mobile -o ./ipaResults:
- Android →
./apk/com.example.app/(*.apk, optionallypermissions.txt) - iOS →
./ipa/com.example.app_dump.ipa+permissions/com.example.app_Info.plist
- No Bluetooth log collection due to iOS limitations.
- iOS has no PCAPdroid-like tool for per-app captures, so CapIoT relies on the system’s App Privacy Report to list the domains each app contacts; to preserve that report, the app is left installed after the experiment.
- No device reboot after full experiment (breaks
palera1njailbreak). - Coordinates must be recorded manually (see the GIMP workflow above).
You can define your own experiment workflow by creating your own runner. At launch, the framework evaluates every registered runner and picks the one with the highest priority whose can_handle() method returns True for the loaded configuration.
- Subclass
BaseRunner. - Decorate with
@register(priority=N). A higher priority value wins when multiple runners match. - Implement two methods:
@classmethod can_handle(cls, cfg)– returnTrueif this runner should execute for the given config.def run(self, ctx)– perform all experiment steps.
- Add custom keys to your
config.yamlif needed — they are passed through unchanged.
Example
from runners import BaseRunner, register
@register(priority=0)
class CustomRunner(BaseRunner):
@classmethod
def can_handle(cls, config) -> bool:
return getattr(config, "custom_key", False) is True
def run(self, ctx):
# your workflow here
...Happy capturing! 🚀