iotsploit-exploits 0.0.7__tar.gz → 0.0.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. iotsploit_exploits-0.0.9/PKG-INFO +85 -0
  2. iotsploit_exploits-0.0.9/README.md +59 -0
  3. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/pyproject.toml +19 -4
  4. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/adb_check/adb_check.py +3 -8
  5. iotsploit_exploits-0.0.9/src/iotsploit_exploits/canbus/__init__.py +8 -0
  6. iotsploit_exploits-0.0.9/src/iotsploit_exploits/canbus/frame_composer.py +414 -0
  7. iotsploit_exploits-0.0.9/src/iotsploit_exploits/canbus/live_capture.py +1261 -0
  8. iotsploit_exploits-0.0.9/src/iotsploit_exploits/demo/async_run.py +142 -0
  9. iotsploit_exploits-0.0.9/src/iotsploit_exploits/demo/interactive.py +164 -0
  10. iotsploit_exploits-0.0.9/src/iotsploit_exploits/demo/observations.py +269 -0
  11. iotsploit_exploits-0.0.9/src/iotsploit_exploits/demo/privileged.py +144 -0
  12. iotsploit_exploits-0.0.9/src/iotsploit_exploits/demo/stream.py +230 -0
  13. iotsploit_exploits-0.0.9/src/iotsploit_exploits/doip/__init__.py +1 -0
  14. iotsploit_exploits-0.0.9/src/iotsploit_exploits/doip/authentication.py +194 -0
  15. iotsploit_exploits-0.0.9/src/iotsploit_exploits/doip/configuration_did_catalog.py +21 -0
  16. iotsploit_exploits-0.0.9/src/iotsploit_exploits/doip/configuration_dids.py +262 -0
  17. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/flood_attack/flood_attack.py +21 -20
  18. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/flood_attack/syn_flood_attack.py +4 -9
  19. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/greatfet_echo.py +2 -8
  20. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/greatfet_rubber_duck.py +3 -5
  21. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/hydra_ssh_attack.py +2 -2
  22. iotsploit_exploits-0.0.9/src/iotsploit_exploits/ip_scan/ip_scan.py +344 -0
  23. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/nmap_scan/nmap_scan.py +154 -5
  24. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/plugin_ssh.py +64 -18
  25. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/serial/picocom_serial_reader.py +22 -16
  26. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/simple_rubber_duck.py +6 -8
  27. iotsploit_exploits-0.0.9/src/iotsploit_exploits/someip/__init__.py +1 -0
  28. iotsploit_exploits-0.0.9/src/iotsploit_exploits/someip/someip_call.py +200 -0
  29. iotsploit_exploits-0.0.9/src/iotsploit_exploits/someip/someip_discover.py +263 -0
  30. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/__init__.py +7 -0
  31. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/catalog.py +139 -0
  32. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/data/__init__.py +1 -0
  33. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/dictionary.py +70 -0
  34. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/did_registry.py +220 -0
  35. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/exchange.py +215 -0
  36. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/interactive.py +411 -0
  37. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/local_profile.py +54 -0
  38. iotsploit_exploits-0.0.9/src/iotsploit_exploits/uds/probe.py +532 -0
  39. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/wifi_scan/wifi_scan.py +2 -2
  40. iotsploit_exploits-0.0.7/PKG-INFO +0 -66
  41. iotsploit_exploits-0.0.7/README.md +0 -42
  42. iotsploit_exploits-0.0.7/src/iotsploit_exploits/demo/async_sleep_attack.py +0 -106
  43. iotsploit_exploits-0.0.7/src/iotsploit_exploits/demo/stream_data_attack.py +0 -184
  44. iotsploit_exploits-0.0.7/src/iotsploit_exploits/ip_scan/ip_scan.py +0 -196
  45. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/__init__.py +0 -0
  46. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/adb_check/__init__.py +0 -0
  47. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/demo/__init__.py +0 -0
  48. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/flood_attack/__init__.py +0 -0
  49. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/hydra_cracker/weak_pass.txt +0 -0
  50. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/hydra_cracker/weak_pass_simple.txt +0 -0
  51. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/ip_scan/__init__.py +0 -0
  52. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/nmap_scan/__init__.py +0 -0
  53. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/rubber_duck_scripts/linux_infogather.txt +0 -0
  54. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/rubber_duck_scripts/windows_payload.txt +0 -0
  55. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/serial/__init__.py +0 -0
  56. {iotsploit_exploits-0.0.7 → iotsploit_exploits-0.0.9}/src/iotsploit_exploits/wifi_scan/__init__.py +0 -0
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: iotsploit-exploits
3
+ Version: 0.0.9
4
+ Summary: Official IoTSploit exploit plugin package
5
+ License: GPL-3.0-or-later
6
+ Author: IoTSploit Team
7
+ Author-email: support@iotsploit.org
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Requires-Dist: facedancer (>=3.0.6,<4.0)
18
+ Requires-Dist: iotsploit-core
19
+ Requires-Dist: iotsploit-django
20
+ Requires-Dist: iotsploit-protocols
21
+ Requires-Dist: paramiko (>=3.0)
22
+ Requires-Dist: pyserial (>=3.5)
23
+ Requires-Dist: scapy (>=2.5)
24
+ Description-Content-Type: text/markdown
25
+
26
+ # iotsploit-exploits
27
+
28
+ Official IoTSploit exploit plugin package. Contains all built-in exploit/security testing plugins.
29
+
30
+ ## Included Plugins
31
+
32
+ Every plugin is registered as an `iotsploit.exploit_plugins` entry point in
33
+ `pyproject.toml`; a plugin that is not registered cannot be run.
34
+
35
+ The `demo_*` plugins are the reference gallery — one per framework capability,
36
+ small enough to read, safe to run anywhere. Start there when writing a plugin,
37
+ and see `docs/writing-plugins.md` for which file teaches what.
38
+
39
+ | Plugin | Module | Description |
40
+ |--------|--------|-------------|
41
+ | Observations Demo | `iotsploit_exploits.demo.observations` | Reference: parameters, target reading, recording observations |
42
+ | Interactive Demo | `iotsploit_exploits.demo.interactive` | Reference: operator prompts |
43
+ | Async Demo | `iotsploit_exploits.demo.async_run` | Reference: long-running work, progress, cooperative stop |
44
+ | Stream Demo | `iotsploit_exploits.demo.stream` | Reference: streaming live data to the UI |
45
+ | Privileged Helper Check | `iotsploit_exploits.demo.privileged` | Reference: the bounded privileged helper |
46
+ | UDS Probe | `iotsploit_exploits.uds.probe` | Generic UDS probing over DoIP |
47
+ | DoIP Configuration DIDs | `iotsploit_exploits.doip.configuration_dids` | Read ZXD configuration data identifiers |
48
+ | DoIP UDS 0x27 Authentication | `iotsploit_exploits.doip.authentication` | SecurityAccess with the local FastSA profile |
49
+ | SOME/IP Discovery | `iotsploit_exploits.someip.someip_discover` | Record the service instances a segment announces |
50
+ | SOME/IP Call | `iotsploit_exploits.someip.someip_call` | Call one SOME/IP method and decode the reply |
51
+ | CAN Frame Composer | `iotsploit_exploits.canbus.frame_composer` | Compose and send CAN frames |
52
+ | CAN Live Capture | `iotsploit_exploits.canbus.live_capture` | Bounded capture decoded against the target's definitions |
53
+ | IP Scan | `iotsploit_exploits.ip_scan.ip_scan` | IP network scanner |
54
+ | Nmap Scan | `iotsploit_exploits.nmap_scan.nmap_scan` | Nmap-based port scanner |
55
+ | WiFi Scan | `iotsploit_exploits.wifi_scan.wifi_scan` | WiFi network scanner |
56
+ | Flood Attack | `iotsploit_exploits.flood_attack.flood_attack` | ICMP/UDP/TCP/MAC flood against an ECU |
57
+ | SYN Flood | `iotsploit_exploits.flood_attack.syn_flood_attack` | TCP SYN flood |
58
+ | ADB Check | `iotsploit_exploits.adb_check.adb_check` | Android Debug Bridge security check |
59
+ | Hydra SSH | `iotsploit_exploits.hydra_ssh_attack` | Hydra SSH credential attack |
60
+ | SSH Plugin | `iotsploit_exploits.plugin_ssh` | SSH connection plugin |
61
+ | Serial Reader | `iotsploit_exploits.serial.picocom_serial_reader` | Picocom serial reader |
62
+ | GreatFET Echo | `iotsploit_exploits.greatfet_echo` | FTDI echo emulation |
63
+ | Rubber Duck | `iotsploit_exploits.greatfet_rubber_duck` | USB rubber duck (GreatFET) |
64
+ | Simple Rubber Duck | `iotsploit_exploits.simple_rubber_duck` | Simplified USB rubber duck |
65
+
66
+ ## System Tool Dependencies
67
+
68
+ Some plugins require system-level tools (not installable via pip):
69
+
70
+ - **nmap_scan**: Requires `nmap` (`apt install nmap`)
71
+ - **hydra_ssh_attack**: Requires `hydra` (`apt install hydra`)
72
+ - **adb_check**: Requires `adb` (`apt install android-tools-adb`)
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install iotsploit-exploits
78
+ ```
79
+
80
+ For development:
81
+
82
+ ```bash
83
+ pip install -e ./iotsploit-exploits
84
+ ```
85
+
@@ -0,0 +1,59 @@
1
+ # iotsploit-exploits
2
+
3
+ Official IoTSploit exploit plugin package. Contains all built-in exploit/security testing plugins.
4
+
5
+ ## Included Plugins
6
+
7
+ Every plugin is registered as an `iotsploit.exploit_plugins` entry point in
8
+ `pyproject.toml`; a plugin that is not registered cannot be run.
9
+
10
+ The `demo_*` plugins are the reference gallery — one per framework capability,
11
+ small enough to read, safe to run anywhere. Start there when writing a plugin,
12
+ and see `docs/writing-plugins.md` for which file teaches what.
13
+
14
+ | Plugin | Module | Description |
15
+ |--------|--------|-------------|
16
+ | Observations Demo | `iotsploit_exploits.demo.observations` | Reference: parameters, target reading, recording observations |
17
+ | Interactive Demo | `iotsploit_exploits.demo.interactive` | Reference: operator prompts |
18
+ | Async Demo | `iotsploit_exploits.demo.async_run` | Reference: long-running work, progress, cooperative stop |
19
+ | Stream Demo | `iotsploit_exploits.demo.stream` | Reference: streaming live data to the UI |
20
+ | Privileged Helper Check | `iotsploit_exploits.demo.privileged` | Reference: the bounded privileged helper |
21
+ | UDS Probe | `iotsploit_exploits.uds.probe` | Generic UDS probing over DoIP |
22
+ | DoIP Configuration DIDs | `iotsploit_exploits.doip.configuration_dids` | Read ZXD configuration data identifiers |
23
+ | DoIP UDS 0x27 Authentication | `iotsploit_exploits.doip.authentication` | SecurityAccess with the local FastSA profile |
24
+ | SOME/IP Discovery | `iotsploit_exploits.someip.someip_discover` | Record the service instances a segment announces |
25
+ | SOME/IP Call | `iotsploit_exploits.someip.someip_call` | Call one SOME/IP method and decode the reply |
26
+ | CAN Frame Composer | `iotsploit_exploits.canbus.frame_composer` | Compose and send CAN frames |
27
+ | CAN Live Capture | `iotsploit_exploits.canbus.live_capture` | Bounded capture decoded against the target's definitions |
28
+ | IP Scan | `iotsploit_exploits.ip_scan.ip_scan` | IP network scanner |
29
+ | Nmap Scan | `iotsploit_exploits.nmap_scan.nmap_scan` | Nmap-based port scanner |
30
+ | WiFi Scan | `iotsploit_exploits.wifi_scan.wifi_scan` | WiFi network scanner |
31
+ | Flood Attack | `iotsploit_exploits.flood_attack.flood_attack` | ICMP/UDP/TCP/MAC flood against an ECU |
32
+ | SYN Flood | `iotsploit_exploits.flood_attack.syn_flood_attack` | TCP SYN flood |
33
+ | ADB Check | `iotsploit_exploits.adb_check.adb_check` | Android Debug Bridge security check |
34
+ | Hydra SSH | `iotsploit_exploits.hydra_ssh_attack` | Hydra SSH credential attack |
35
+ | SSH Plugin | `iotsploit_exploits.plugin_ssh` | SSH connection plugin |
36
+ | Serial Reader | `iotsploit_exploits.serial.picocom_serial_reader` | Picocom serial reader |
37
+ | GreatFET Echo | `iotsploit_exploits.greatfet_echo` | FTDI echo emulation |
38
+ | Rubber Duck | `iotsploit_exploits.greatfet_rubber_duck` | USB rubber duck (GreatFET) |
39
+ | Simple Rubber Duck | `iotsploit_exploits.simple_rubber_duck` | Simplified USB rubber duck |
40
+
41
+ ## System Tool Dependencies
42
+
43
+ Some plugins require system-level tools (not installable via pip):
44
+
45
+ - **nmap_scan**: Requires `nmap` (`apt install nmap`)
46
+ - **hydra_ssh_attack**: Requires `hydra` (`apt install hydra`)
47
+ - **adb_check**: Requires `adb` (`apt install android-tools-adb`)
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install iotsploit-exploits
53
+ ```
54
+
55
+ For development:
56
+
57
+ ```bash
58
+ pip install -e ./iotsploit-exploits
59
+ ```
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "iotsploit-exploits"
3
- version = "0.0.7"
3
+ version = "0.0.9"
4
4
  description = "Official IoTSploit exploit plugin package"
5
5
  authors = ["IoTSploit Team <support@iotsploit.org>"]
6
6
  license = "GPL-3.0-or-later"
@@ -10,13 +10,15 @@ include = [
10
10
  { path = "src/iotsploit_exploits/rubber_duck_scripts/*.txt", format = ["sdist", "wheel"] },
11
11
  { path = "src/iotsploit_exploits/hydra_cracker/*.txt", format = ["sdist", "wheel"] },
12
12
  ]
13
+ exclude = ["src/iotsploit_exploits/oem/**"]
13
14
 
14
15
  [tool.poetry.dependencies]
15
16
  python = ">=3.10,<4.0"
16
17
  iotsploit-core = "*"
17
18
  iotsploit-django = "*"
19
+ iotsploit-protocols = "*"
18
20
  scapy = ">=2.5"
19
- pwntools = ">=4.12"
21
+ paramiko = ">=3.0"
20
22
  pyserial = ">=3.5"
21
23
  # Bare ">=3.0" also matches legacy FaceDancer 2019.x releases under PEP 440.
22
24
  # Keep this on the 3.x line so the exploit imports resolve to the expected API.
@@ -28,15 +30,28 @@ syn_flood_attack = "iotsploit_exploits.flood_attack.syn_flood_attack:SynFloodAtt
28
30
  wifi_scan = "iotsploit_exploits.wifi_scan.wifi_scan:WifiScanPlugin"
29
31
  ip_scan = "iotsploit_exploits.ip_scan.ip_scan:IPScanPlugin"
30
32
  nmap_scan = "iotsploit_exploits.nmap_scan.nmap_scan:NmapScanPlugin"
33
+ can_frame_composer = "iotsploit_exploits.canbus.frame_composer:CanFrameComposerPlugin"
34
+ can_live_capture = "iotsploit_exploits.canbus.live_capture:CanLiveCapturePlugin"
35
+ someip_discover = "iotsploit_exploits.someip.someip_discover:SomeIpDiscoverPlugin"
36
+ someip_call = "iotsploit_exploits.someip.someip_call:SomeIpCallPlugin"
31
37
  adb_check = "iotsploit_exploits.adb_check.adb_check:AdbSecurityCheckPlugin"
32
38
  hydra_ssh_attack = "iotsploit_exploits.hydra_ssh_attack:HydraSSHAttackPlugin"
33
39
  plugin_ssh = "iotsploit_exploits.plugin_ssh:SSHPlugin"
40
+ uds_probe = "iotsploit_exploits.uds.probe:UdsProbePlugin"
41
+ doip_configuration_dids = "iotsploit_exploits.doip.configuration_dids:DoipConfigurationDidsPlugin"
42
+ doip_27_authentication = "iotsploit_exploits.doip.authentication:Doip27AuthenticationPlugin"
34
43
  picocom_serial_reader = "iotsploit_exploits.serial.picocom_serial_reader:PicocomSerialReaderPlugin"
35
44
  greatfet_echo = "iotsploit_exploits.greatfet_echo:FTDIEchoPlugin"
36
45
  greatfet_rubber_duck = "iotsploit_exploits.greatfet_rubber_duck:RubberDuckPlugin"
37
46
  simple_rubber_duck = "iotsploit_exploits.simple_rubber_duck:SimpleRubberDuckPlugin"
38
- async_sleep_attack = "iotsploit_exploits.demo.async_sleep_attack:AsyncSleepAttackPlugin"
39
- stream_data_attack = "iotsploit_exploits.demo.stream_data_attack:StreamDataAttackPlugin"
47
+
48
+ # The reference gallery: one plugin per framework capability. Grouped by
49
+ # prefix so they sort together wherever plugins are listed.
50
+ demo_async = "iotsploit_exploits.demo.async_run:AsyncDemoPlugin"
51
+ demo_interactive = "iotsploit_exploits.demo.interactive:InteractiveDemoPlugin"
52
+ demo_observations = "iotsploit_exploits.demo.observations:ObservationDemoPlugin"
53
+ demo_privileged = "iotsploit_exploits.demo.privileged:PrivilegedDemoPlugin"
54
+ demo_stream = "iotsploit_exploits.demo.stream:StreamDemoPlugin"
40
55
 
41
56
  [build-system]
42
57
  requires = ["poetry-core>=1.8.0"]
@@ -1,9 +1,5 @@
1
1
  #!/usr/bin/python3
2
2
 
3
- import os
4
- # Disable pwntools terminal mode before importing to prevent fileno() errors in non-TTY environments
5
- os.environ.setdefault("PWNLIB_NOTERM", "1")
6
-
7
3
  import logging
8
4
  import traceback
9
5
  import pluggy
@@ -11,13 +7,13 @@ from iotsploit_django.tools.adb_mgr import ADB_Mgr
11
7
  from iotsploit_django.tools.doip_mgr import DoIP_Mgr
12
8
  from iotsploit_core.core.exploit_spec import ExploitResult
13
9
  from iotsploit_core.core.base_plugin import BasePlugin
14
- from iotsploit_core.utils import iots_logger
15
10
  from typing import Optional, List, Dict, Any
16
11
 
17
- logger = iots_logger.get_logger("adb_check")
12
+ logger = logging.getLogger(__name__)
18
13
  hookimpl = pluggy.HookimplMarker("exploit_mgr")
19
14
 
20
15
  class AdbSecurityCheckPlugin(BasePlugin):
16
+ REQUIRES = ("binary:adb",)
21
17
  def __init__(self):
22
18
  super().__init__({
23
19
  'Name': 'ADB Security Check',
@@ -88,7 +84,6 @@ class AdbSecurityCheckPlugin(BasePlugin):
88
84
  try_root = params.get('try_root', False)
89
85
  check_suid = params.get('check_suid', True)
90
86
  check_writable = params.get('check_writable', True)
91
- skip_interactive = params.get('skip_interactive', True)
92
87
 
93
88
  # If target is provided, try to get the device from it
94
89
  if target:
@@ -295,7 +290,7 @@ class AdbSecurityCheckPlugin(BasePlugin):
295
290
 
296
291
  # If root is required but we know we don't have it, don't even try
297
292
  if require_root and self._has_root is False:
298
- logger.debug(f"Root access required for command but device is not rooted")
293
+ logger.debug("Root access required for command but device is not rooted")
299
294
  return None
300
295
 
301
296
  # Execute the command
@@ -0,0 +1,8 @@
1
+ """Official CAN plugins: compose one frame, and watch a bus.
2
+
3
+ Both are thin. The catalogue, the codec, and the SocketCAN clients live in
4
+ ``iotsploit_protocols.canbus``; what belongs here is policy -- which requests
5
+ are allowed, what has to be confirmed first, and what a result may claim.
6
+ """
7
+
8
+ from __future__ import annotations
@@ -0,0 +1,414 @@
1
+ """Encode a target-defined CAN frame, and optionally put one on the wire.
2
+
3
+ The safety shape of this plugin is the whole design, so it is worth stating
4
+ plainly rather than leaving it to be inferred from the code:
5
+
6
+ *Preview is the default and touches no hardware.* It resolves, encodes, and
7
+ returns the exact bytes. It does not import ``python-can`` and does not open a
8
+ socket, so it works on a host with no CAN interface at all.
9
+
10
+ *Transmit requires a digest from the preview that produced those bytes.* The
11
+ digest covers the target, the frame definition, the normalized values, the
12
+ resulting bytes, and the physical channel. If the target was re-imported
13
+ between preview and transmit -- a signal rescaled, a DLC corrected -- the
14
+ recomputed digest differs and the send is refused rather than silently putting
15
+ different bytes on the wire than the operator approved.
16
+
17
+ The digest is a consistency guard and nothing more. It is not authorization, it
18
+ is not a one-time token, and repeating a confirmed API call sends another
19
+ frame. Authentication and rate limiting are platform-wide gaps that this
20
+ feature must not pretend to fill.
21
+
22
+ Sending on an interface that is already up needs no privileged helper verb;
23
+ bringing an interface up is explicitly not this plugin's job.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import json
30
+ import logging
31
+ from datetime import datetime, timezone
32
+ from typing import Any, Dict, Mapping, Optional, Tuple
33
+
34
+ import pluggy
35
+ from iotsploit_core.core.base_plugin import BasePlugin
36
+ from iotsploit_core.core.exploit_spec import ExploitResult
37
+ from iotsploit_protocols.canbus import TargetCanCatalog, decode_frame, encode_frame
38
+ from iotsploit_protocols.canbus.definitions import EncodedFrame, FrameDefinition
39
+ from iotsploit_protocols.canbus.errors import CanDefinitionError, CanValueError
40
+ from iotsploit_protocols.canbus.socketcan import (
41
+ CanTransportError,
42
+ SocketCanClient,
43
+ SocketCanConfig,
44
+ )
45
+ from iotsploit_protocols.errors import NotConfigured
46
+
47
+ logger = logging.getLogger(__name__)
48
+ hookimpl = pluggy.HookimplMarker("exploit_mgr")
49
+
50
+ SCHEMA_VERSION = 1
51
+ PREVIEW = "preview"
52
+ TRANSMIT = "transmit"
53
+ DEFAULT_TIMEOUT_MS = 1000
54
+
55
+
56
+ class RequestError(ValueError):
57
+ """The request itself is malformed, before any target is consulted."""
58
+
59
+ def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None) -> None:
60
+ self.field_errors = dict(field_errors or {})
61
+ super().__init__(message)
62
+
63
+
64
+ def normalize_request(raw: Any) -> Dict[str, Any]:
65
+ """Accept the request as an object or as JSON text, then validate its shape.
66
+
67
+ The string form is not a second contract: it is the same schema, and it
68
+ exists because the CLI renders an unknown parameter type as a text field.
69
+ Normalizing here means one validation path afterwards rather than two that
70
+ drift.
71
+ """
72
+ if isinstance(raw, str):
73
+ text = raw.strip()
74
+ if not text:
75
+ raise RequestError("no request supplied")
76
+ try:
77
+ raw = json.loads(text)
78
+ except (ValueError, RecursionError) as error:
79
+ # Neither of these is a JSONDecodeError, and both come out of
80
+ # json.loads on input a person can send: a number with more than
81
+ # 4300 digits fails inside int() as a plain ValueError, and a
82
+ # document nested a thousand deep exhausts the stack because the
83
+ # decoder recurses per level. Catching only the subclass let both
84
+ # escape a function whose contract is that nothing but
85
+ # RequestError does.
86
+ raise RequestError(f"request is not valid JSON: {error}") from error
87
+
88
+ if not isinstance(raw, Mapping):
89
+ raise RequestError(
90
+ f"request must be an object or a JSON object string, not {type(raw).__name__}"
91
+ )
92
+
93
+ version = raw.get("schema_version")
94
+ if version != SCHEMA_VERSION:
95
+ raise RequestError(
96
+ f"unsupported request schema_version {version!r}; this plugin speaks {SCHEMA_VERSION}"
97
+ )
98
+
99
+ operation = raw.get("operation", PREVIEW)
100
+ if operation not in (PREVIEW, TRANSMIT):
101
+ raise RequestError(f"operation must be {PREVIEW!r} or {TRANSMIT!r}, not {operation!r}")
102
+
103
+ frame = raw.get("frame")
104
+ if not isinstance(frame, Mapping):
105
+ raise RequestError("request.frame must be an object naming the bus and frame")
106
+
107
+ for key in ("bus_id", "frame_id"):
108
+ if frame.get(key) is None:
109
+ raise RequestError(f"request.frame.{key} is required")
110
+
111
+ frame_id = frame.get("frame_id")
112
+ if isinstance(frame_id, str):
113
+ try:
114
+ frame_id = int(frame_id, 16) if frame_id.lower().startswith("0x") else int(frame_id)
115
+ except ValueError:
116
+ raise RequestError(f"request.frame.frame_id {frame_id!r} is not a number") from None
117
+ if not isinstance(frame_id, int) or isinstance(frame_id, bool):
118
+ raise RequestError("request.frame.frame_id must be an integer")
119
+
120
+ signals = raw.get("signals", {})
121
+ if not isinstance(signals, Mapping):
122
+ raise RequestError("request.signals must be an object of signal name to value")
123
+
124
+ transport = raw.get("transport") or {}
125
+ if not isinstance(transport, Mapping):
126
+ raise RequestError("request.transport must be an object")
127
+
128
+ interface = transport.get("interface", "socketcan")
129
+ if interface != "socketcan":
130
+ raise RequestError(f"transport.interface {interface!r} is not supported; use 'socketcan'")
131
+
132
+ return {
133
+ "schema_version": SCHEMA_VERSION,
134
+ "operation": operation,
135
+ "bus_id": str(frame["bus_id"]),
136
+ "frame_id": frame_id,
137
+ "is_extended": bool(frame.get("is_extended", False)),
138
+ "expected_name": frame.get("name") or None,
139
+ "signals": dict(signals),
140
+ "interface": interface,
141
+ "channel": transport.get("channel"),
142
+ "timeout_ms": transport.get("timeout_ms", DEFAULT_TIMEOUT_MS),
143
+ "allow_draft_target": bool(raw.get("allow_draft_target", False)),
144
+ "preview_digest": raw.get("preview_digest"),
145
+ }
146
+
147
+
148
+ def _stable(value: Any) -> Any:
149
+ """Everything as text, so a digest cannot depend on int-versus-float typing.
150
+
151
+ The point of the digest is to answer "is this the same request over the same
152
+ definitions". Two runs that produce 3 and 3.0 for one signal describe the
153
+ same frame, and a guard that intermittently disagrees with itself is worse
154
+ than no guard: it fails closed at random and teaches the operator to
155
+ distrust it.
156
+ """
157
+ if isinstance(value, Mapping):
158
+ return {str(k): _stable(v) for k, v in sorted(value.items(), key=lambda kv: str(kv[0]))}
159
+ if isinstance(value, (list, tuple)):
160
+ return [_stable(v) for v in value]
161
+ if isinstance(value, bool):
162
+ return "true" if value else "false"
163
+ if value is None:
164
+ return None
165
+ return str(value)
166
+
167
+
168
+ def compute_digest(
169
+ *,
170
+ target_id: str,
171
+ target_status: str,
172
+ bus_id: str,
173
+ definition: FrameDefinition,
174
+ encoded: EncodedFrame,
175
+ channel: Optional[str],
176
+ ) -> str:
177
+ """A digest over everything that decides what reaches the wire.
178
+
179
+ Serialized with sorted keys, because a digest taken over incidental dict
180
+ ordering compares unequal for an unchanged request and turns the guard into
181
+ an intermittent refusal to transmit.
182
+ """
183
+ payload = _stable(
184
+ {
185
+ "schema_version": SCHEMA_VERSION,
186
+ "target": {"target_id": target_id, "status": target_status},
187
+ "bus_id": bus_id,
188
+ "definition": list(definition.encoding_key()),
189
+ "signals": encoded.signals,
190
+ "frame": {
191
+ "frame_id": encoded.frame_id,
192
+ "is_extended": encoded.is_extended,
193
+ "is_fd": encoded.is_fd,
194
+ "dlc": encoded.dlc,
195
+ "data_hex": encoded.data_hex,
196
+ },
197
+ "transport": {"interface": "socketcan", "channel": channel},
198
+ }
199
+ )
200
+ blob = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
201
+ return "sha256:" + hashlib.sha256(blob.encode("utf-8")).hexdigest()
202
+
203
+
204
+ def _target_field(target: Any, name: str, default: Any = None) -> Any:
205
+ if isinstance(target, Mapping):
206
+ return target.get(name, default)
207
+ return getattr(target, name, default)
208
+
209
+
210
+ def build_preview(
211
+ target: Any, request: Mapping[str, Any]
212
+ ) -> Tuple[Dict[str, Any], FrameDefinition, EncodedFrame]:
213
+ """Resolve, encode, and describe -- without touching any transport."""
214
+ target_id = _target_field(target, "target_id")
215
+ if not target_id:
216
+ raise RequestError("no target was supplied to the plugin")
217
+ target_status = str(_target_field(target, "status", "unknown") or "unknown")
218
+
219
+ catalog = TargetCanCatalog.from_target(target)
220
+ bus = catalog.bus(request["bus_id"])
221
+ definition = catalog.resolve(
222
+ request["bus_id"],
223
+ request["frame_id"],
224
+ request["is_extended"],
225
+ expected_name=request["expected_name"],
226
+ )
227
+ encoded = encode_frame(definition, request["signals"])
228
+
229
+ warnings = []
230
+ if target_status != "active":
231
+ warnings.append(
232
+ f"target {target_id!r} is {target_status!r}, not active; its CAN topology "
233
+ "may be an incomplete extract rather than the whole vehicle"
234
+ )
235
+ if bus.conflicts:
236
+ warnings.append(
237
+ f"{len(bus.conflicts)} other frames on this bus have conflicting definitions"
238
+ )
239
+
240
+ # Read the bytes back through the decoder. This is the one genuinely useful
241
+ # thing the inverse buys at preview time: it says what the frame states,
242
+ # rather than repeating what the operator typed.
243
+ read_back = decode_frame(definition, encoded.data)
244
+ if not read_back.ok:
245
+ warnings.append(f"encoded payload did not decode cleanly: {read_back.reason}")
246
+ elif read_back.reason:
247
+ warnings.append(read_back.reason)
248
+
249
+ digest = compute_digest(
250
+ target_id=str(target_id),
251
+ target_status=target_status,
252
+ bus_id=request["bus_id"],
253
+ definition=definition,
254
+ encoded=encoded,
255
+ channel=request["channel"],
256
+ )
257
+
258
+ data = {
259
+ "operation": request["operation"],
260
+ "target": {"target_id": str(target_id), "status": target_status},
261
+ "bus": {"bus_id": bus.bus_id, "name": bus.name},
262
+ "frame": {
263
+ "name": encoded.name,
264
+ "frame_id": encoded.frame_id,
265
+ "frame_id_hex": f"0x{encoded.frame_id:X}",
266
+ "is_extended": encoded.is_extended,
267
+ "is_fd": encoded.is_fd,
268
+ "dlc": encoded.dlc,
269
+ "data_hex": encoded.data_hex,
270
+ "owner_kind": definition.owner_kind,
271
+ "component_id": definition.component_id,
272
+ },
273
+ "signals": dict(encoded.signals),
274
+ "decoded": dict(read_back.signals) if read_back.ok else {},
275
+ "transport": {
276
+ "interface": "socketcan",
277
+ "channel": request["channel"],
278
+ "timeout_ms": request["timeout_ms"],
279
+ },
280
+ "preview_digest": digest,
281
+ "warnings": warnings,
282
+ }
283
+ return data, definition, encoded
284
+
285
+
286
+ class CanFrameComposerPlugin(BasePlugin):
287
+ REQUIRES = ("platform:linux", "module:can")
288
+ def __init__(self):
289
+ super().__init__(
290
+ {
291
+ "Name": "CAN Frame Composer",
292
+ "Description": (
293
+ "Encode target-defined signals and optionally send one SocketCAN "
294
+ "frame. Preview never touches hardware; transmitting requires a "
295
+ "matching preview digest and sends exactly one frame."
296
+ ),
297
+ "License": "GPL",
298
+ "Author": ["iotsploit"],
299
+ "Interactive": False,
300
+ "Parameters": {
301
+ "request": {
302
+ "type": "target_can_frame",
303
+ "schema_version": SCHEMA_VERSION,
304
+ "required": True,
305
+ "description": (
306
+ "Target CAN frame, signal values, transport, and operation."
307
+ ),
308
+ }
309
+ },
310
+ }
311
+ )
312
+
313
+ @hookimpl
314
+ def initialize(self, device_plugin: Any | None = None):
315
+ logger.debug("Initializing CanFrameComposerPlugin")
316
+
317
+ @hookimpl
318
+ def execute(
319
+ self, target: Any | None = None, parameters: dict | None = None
320
+ ) -> ExploitResult:
321
+ try:
322
+ request = normalize_request((parameters or {}).get("request"))
323
+ except RequestError as error:
324
+ return ExploitResult(False, str(error), {"field_errors": error.field_errors})
325
+
326
+ try:
327
+ data, definition, encoded = build_preview(target, request)
328
+ except CanValueError as error:
329
+ return ExploitResult(False, str(error), {"field_errors": error.field_errors})
330
+ except (CanDefinitionError, RequestError, NotConfigured) as error:
331
+ return ExploitResult(False, str(error), {})
332
+
333
+ if request["operation"] == PREVIEW:
334
+ return ExploitResult(
335
+ True, f"Encoded {data['frame']['name']} as {encoded.data_hex}", data
336
+ )
337
+
338
+ return self._transmit(request, data, encoded)
339
+
340
+ def _transmit(
341
+ self,
342
+ request: Mapping[str, Any],
343
+ data: Dict[str, Any],
344
+ encoded: EncodedFrame,
345
+ ) -> ExploitResult:
346
+ supplied = request.get("preview_digest")
347
+ if not supplied:
348
+ return ExploitResult(
349
+ False,
350
+ "transmitting requires the digest from a preview of this exact request",
351
+ data,
352
+ )
353
+ if supplied != data["preview_digest"]:
354
+ return ExploitResult(
355
+ False,
356
+ "the target or the request changed since this frame was previewed; "
357
+ "preview again and confirm the new bytes before sending",
358
+ data,
359
+ )
360
+
361
+ if data["target"]["status"] != "active" and not request["allow_draft_target"]:
362
+ return ExploitResult(
363
+ False,
364
+ f"target is {data['target']['status']!r}, not active; sending to a "
365
+ "draft target needs an explicit acknowledgement",
366
+ data,
367
+ )
368
+
369
+ channel = request.get("channel")
370
+ if not channel:
371
+ return ExploitResult(
372
+ False,
373
+ "no SocketCAN interface was chosen; pick one of the scanned CAN "
374
+ "devices, using its interface name such as 'can0'",
375
+ data,
376
+ )
377
+
378
+ try:
379
+ timeout = max(float(request["timeout_ms"]) / 1000.0, 0.001)
380
+ config = SocketCanConfig(
381
+ channel=str(channel), timeout=timeout, fd=encoded.is_fd
382
+ )
383
+ except (NotConfigured, TypeError, ValueError) as error:
384
+ return ExploitResult(False, str(error), data)
385
+
386
+ try:
387
+ with SocketCanClient(config) as client:
388
+ client.send(encoded)
389
+ except (CanTransportError, NotConfigured, OSError) as error:
390
+ return ExploitResult(False, str(error), data)
391
+
392
+ sent_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
393
+ data = {**data, "operation": TRANSMIT, "sent": True, "sent_at": sent_at}
394
+
395
+ # The audit record. Deliberately says the socket accepted the frame and
396
+ # stops there: nothing here knows whether an ECU saw it.
397
+ logger.info(
398
+ "CAN frame composer: target=%s bus=%s frame=0x%X ext=%s fd=%s "
399
+ "channel=%s payload=%s -> accepted by local socket",
400
+ data["target"]["target_id"],
401
+ data["bus"]["bus_id"],
402
+ encoded.frame_id,
403
+ encoded.is_extended,
404
+ encoded.is_fd,
405
+ channel,
406
+ encoded.data_hex,
407
+ )
408
+ return ExploitResult(
409
+ True,
410
+ f"Sent {encoded.name} (0x{encoded.frame_id:X}) on {channel}: "
411
+ f"{encoded.data_hex}. The local socket accepted the frame; this does "
412
+ "not confirm any ECU received or acted on it.",
413
+ data,
414
+ )