iotsploit-exploits 0.0.6__py3-none-any.whl
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.
- iotsploit_exploits/__init__.py +1 -0
- iotsploit_exploits/adb_check/__init__.py +0 -0
- iotsploit_exploits/adb_check/adb_check.py +493 -0
- iotsploit_exploits/demo/__init__.py +0 -0
- iotsploit_exploits/demo/async_sleep_attack.py +106 -0
- iotsploit_exploits/demo/stream_data_attack.py +184 -0
- iotsploit_exploits/flood_attack/__init__.py +0 -0
- iotsploit_exploits/flood_attack/flood_attack.py +129 -0
- iotsploit_exploits/flood_attack/syn_flood_attack.py +233 -0
- iotsploit_exploits/greatfet_echo.py +103 -0
- iotsploit_exploits/greatfet_rubber_duck.py +417 -0
- iotsploit_exploits/hydra_cracker/weak_pass.txt +471 -0
- iotsploit_exploits/hydra_cracker/weak_pass_simple.txt +5 -0
- iotsploit_exploits/hydra_ssh_attack.py +159 -0
- iotsploit_exploits/ip_scan/__init__.py +0 -0
- iotsploit_exploits/ip_scan/ip_scan.py +196 -0
- iotsploit_exploits/nmap_scan/__init__.py +0 -0
- iotsploit_exploits/nmap_scan/nmap_scan.py +207 -0
- iotsploit_exploits/plugin_ssh.py +146 -0
- iotsploit_exploits/rubber_duck_scripts/linux_infogather.txt +126 -0
- iotsploit_exploits/rubber_duck_scripts/windows_payload.txt +93 -0
- iotsploit_exploits/serial/__init__.py +0 -0
- iotsploit_exploits/serial/picocom_serial_reader.py +704 -0
- iotsploit_exploits/simple_rubber_duck.py +183 -0
- iotsploit_exploits/wifi_scan/__init__.py +0 -0
- iotsploit_exploits/wifi_scan/wifi_scan.py +242 -0
- iotsploit_exploits-0.0.6.dist-info/METADATA +65 -0
- iotsploit_exploits-0.0.6.dist-info/RECORD +30 -0
- iotsploit_exploits-0.0.6.dist-info/WHEEL +4 -0
- iotsploit_exploits-0.0.6.dist-info/entry_points.txt +16 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import pluggy
|
|
5
|
+
import asyncio
|
|
6
|
+
import serial
|
|
7
|
+
import serial.tools.list_ports
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from typing import Optional, Any, List, Dict
|
|
11
|
+
from threading import Thread, Event
|
|
12
|
+
from queue import Queue, Empty
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from iotsploit_core.core.exploit_spec import AsyncExploitResult
|
|
15
|
+
from iotsploit_core.core.base_plugin import BasePlugin
|
|
16
|
+
from iotsploit_core.utils import iots_logger
|
|
17
|
+
|
|
18
|
+
logger = iots_logger.get_logger("picocom_serial_reader")
|
|
19
|
+
hookimpl = pluggy.HookimplMarker("exploit_mgr")
|
|
20
|
+
|
|
21
|
+
# Add file logging for debugging
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
file_logger = logging.getLogger('picocom_serial_reader_file')
|
|
25
|
+
file_logger.setLevel(logging.DEBUG)
|
|
26
|
+
|
|
27
|
+
# Create logs directory if it doesn't exist
|
|
28
|
+
log_dir = "/tmp/sat_logs"
|
|
29
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# Add file handler
|
|
32
|
+
file_handler = logging.FileHandler(f"{log_dir}/picocom_serial_reader.log")
|
|
33
|
+
file_handler.setLevel(logging.DEBUG)
|
|
34
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
35
|
+
file_handler.setFormatter(formatter)
|
|
36
|
+
file_logger.addHandler(file_handler)
|
|
37
|
+
|
|
38
|
+
def log_both(level, message):
|
|
39
|
+
"""Log to both regular logger and file logger"""
|
|
40
|
+
getattr(logger, level)(message)
|
|
41
|
+
getattr(file_logger, level)(message)
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SerialAnalysis:
|
|
45
|
+
"""Data structure for serial port analysis results"""
|
|
46
|
+
device_type: str = "unknown"
|
|
47
|
+
confidence: float = 0.0
|
|
48
|
+
detected_patterns: List[str] = None
|
|
49
|
+
login_detected: bool = False
|
|
50
|
+
shell_type: str = "unknown"
|
|
51
|
+
firmware_info: Dict[str, str] = None
|
|
52
|
+
raw_output: List[str] = None
|
|
53
|
+
|
|
54
|
+
def __post_init__(self):
|
|
55
|
+
if self.detected_patterns is None:
|
|
56
|
+
self.detected_patterns = []
|
|
57
|
+
if self.firmware_info is None:
|
|
58
|
+
self.firmware_info = {}
|
|
59
|
+
if self.raw_output is None:
|
|
60
|
+
self.raw_output = []
|
|
61
|
+
|
|
62
|
+
class PicocomSerialReaderPlugin(BasePlugin):
|
|
63
|
+
def __init__(self):
|
|
64
|
+
super().__init__({
|
|
65
|
+
'Name': 'Picocom Serial Reader',
|
|
66
|
+
'Description': 'AI-powered serial port reader and analyzer similar to picocom',
|
|
67
|
+
'License': 'GPL',
|
|
68
|
+
'Author': ['iotsploit'],
|
|
69
|
+
'Parameters': {
|
|
70
|
+
'port': {
|
|
71
|
+
'type': 'str',
|
|
72
|
+
'required': True,
|
|
73
|
+
'description': 'Serial port path (e.g., /dev/ttyUSB0, COM1)',
|
|
74
|
+
'default': '/dev/ttyUSB0'
|
|
75
|
+
},
|
|
76
|
+
'baudrate': {
|
|
77
|
+
'type': 'int',
|
|
78
|
+
'required': False,
|
|
79
|
+
'description': 'Baud rate for serial communication',
|
|
80
|
+
'default': 115200,
|
|
81
|
+
'validation': {
|
|
82
|
+
'min': 300,
|
|
83
|
+
'max': 921600
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
'timeout': {
|
|
87
|
+
'type': 'int',
|
|
88
|
+
'required': False,
|
|
89
|
+
'description': 'Maximum time to wait for output (seconds)',
|
|
90
|
+
'default': 60,
|
|
91
|
+
'validation': {
|
|
92
|
+
'min': 5,
|
|
93
|
+
'max': 300
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
'auto_interact': {
|
|
97
|
+
'type': 'bool',
|
|
98
|
+
'required': False,
|
|
99
|
+
'description': 'Automatically send Enter and common inputs to trigger responses',
|
|
100
|
+
'default': True
|
|
101
|
+
},
|
|
102
|
+
'analyze_output': {
|
|
103
|
+
'type': 'bool',
|
|
104
|
+
'required': False,
|
|
105
|
+
'description': 'Enable AI analysis of serial output',
|
|
106
|
+
'default': True
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Plugin state
|
|
112
|
+
self._stop_reading = False
|
|
113
|
+
self._serial_conn = None
|
|
114
|
+
self._reader_thread = None
|
|
115
|
+
self._output_queue = Queue()
|
|
116
|
+
self._analysis = SerialAnalysis()
|
|
117
|
+
|
|
118
|
+
# Detection patterns
|
|
119
|
+
self._login_patterns = [
|
|
120
|
+
r'login\s*:',
|
|
121
|
+
r'username\s*:',
|
|
122
|
+
r'user\s*:',
|
|
123
|
+
r'password\s*:',
|
|
124
|
+
r'passwd\s*:',
|
|
125
|
+
r'>\s*$',
|
|
126
|
+
r'#\s*$',
|
|
127
|
+
r'\$\s*$',
|
|
128
|
+
r'.*@.*:.*\$',
|
|
129
|
+
r'.*@.*:.*#'
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
self._shell_patterns = {
|
|
133
|
+
'bash': [r'bash-\d+\.\d+', r'GNU bash'],
|
|
134
|
+
'sh': [r'\/bin\/sh', r'\$\s*$'],
|
|
135
|
+
'busybox': [r'BusyBox', r'built-in shell'],
|
|
136
|
+
'uboot': [r'U-Boot', r'=>\s*$', r'Hit any key to stop autoboot'],
|
|
137
|
+
'bootloader': [r'Bootloader', r'Boot>', r'bootloader>'],
|
|
138
|
+
'linux': [r'Linux version', r'GNU\/Linux', r'kernel'],
|
|
139
|
+
'embedded': [r'Embedded', r'firmware', r'version']
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
self._interaction_sequences = [
|
|
143
|
+
"\r\n", # Enter
|
|
144
|
+
"\r", # Carriage return
|
|
145
|
+
"\n", # Line feed
|
|
146
|
+
" \r\n", # Space + Enter
|
|
147
|
+
"help\r\n",
|
|
148
|
+
"?\r\n",
|
|
149
|
+
"ls\r\n",
|
|
150
|
+
"id\r\n",
|
|
151
|
+
"whoami\r\n"
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
@hookimpl
|
|
155
|
+
def initialize(self, device_plugin: Optional[Any] = None):
|
|
156
|
+
logger.info("Initializing PicocomSerialReaderPlugin")
|
|
157
|
+
self._stop_reading = False
|
|
158
|
+
self._analysis = SerialAnalysis()
|
|
159
|
+
|
|
160
|
+
@hookimpl
|
|
161
|
+
async def execute_async(self, target: Optional[Any] = None, parameters: Optional[dict] = None) -> AsyncExploitResult:
|
|
162
|
+
"""Asynchronous execution method for serial port reading and analysis"""
|
|
163
|
+
log_both('info', "๐ฏ Starting Picocom Serial Reader execution")
|
|
164
|
+
async_result = AsyncExploitResult()
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
# Extract parameters
|
|
168
|
+
params = parameters or {}
|
|
169
|
+
port = params.get('port', '/dev/ttyUSB0')
|
|
170
|
+
baudrate = params.get('baudrate', 115200)
|
|
171
|
+
timeout = params.get('timeout', 30)
|
|
172
|
+
auto_interact = params.get('auto_interact', True)
|
|
173
|
+
analyze_output = params.get('analyze_output', True)
|
|
174
|
+
|
|
175
|
+
log_both('info', f"๐ฏ Serial parameters: port={port}, baudrate={baudrate}, timeout={timeout}")
|
|
176
|
+
|
|
177
|
+
# Check if port exists
|
|
178
|
+
available_ports = self._list_serial_ports()
|
|
179
|
+
if port not in available_ports:
|
|
180
|
+
async_result.update(
|
|
181
|
+
status=False,
|
|
182
|
+
message=f"Port {port} not found. Available ports: {', '.join(available_ports)}",
|
|
183
|
+
progress=100
|
|
184
|
+
)
|
|
185
|
+
return async_result
|
|
186
|
+
|
|
187
|
+
# Initialize serial connection
|
|
188
|
+
async_result.update(
|
|
189
|
+
status=True,
|
|
190
|
+
progress=10,
|
|
191
|
+
message=f"Connecting to serial port {port}...",
|
|
192
|
+
data={"available_ports": available_ports}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
success = await self._connect_serial(port, baudrate)
|
|
196
|
+
if not success:
|
|
197
|
+
async_result.update(
|
|
198
|
+
status=False,
|
|
199
|
+
message=f"Failed to connect to {port}",
|
|
200
|
+
progress=100
|
|
201
|
+
)
|
|
202
|
+
return async_result
|
|
203
|
+
|
|
204
|
+
# Start reading thread
|
|
205
|
+
async_result.update(
|
|
206
|
+
status=True,
|
|
207
|
+
progress=20,
|
|
208
|
+
message="Starting serial communication...",
|
|
209
|
+
data={"port": port, "baudrate": baudrate}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self._start_reader_thread()
|
|
213
|
+
|
|
214
|
+
# Main reading and interaction loop
|
|
215
|
+
start_time = time.time()
|
|
216
|
+
interaction_step = 0
|
|
217
|
+
last_interaction_time = start_time
|
|
218
|
+
last_log_time = start_time
|
|
219
|
+
log_interval = 10.0 # Log every 10 seconds
|
|
220
|
+
|
|
221
|
+
# Simple early exit: exit when all interactions done + have some data
|
|
222
|
+
min_data_threshold = 5 # Need at least 5 lines of output for early exit
|
|
223
|
+
|
|
224
|
+
log_both('info', f"๐ Starting main reading loop - timeout: {timeout}s")
|
|
225
|
+
log_both('info', f"๐ Auto-interact: {auto_interact}, Available interactions: {len(self._interaction_sequences)}")
|
|
226
|
+
log_both('info', f"๐ Serial connection status: {self._serial_conn.is_open if self._serial_conn else 'No connection'}")
|
|
227
|
+
log_both('info', f"๐ Simple early exit: will exit when all {len(self._interaction_sequences)} interactions done + {min_data_threshold}+ lines captured")
|
|
228
|
+
|
|
229
|
+
while time.time() - start_time < timeout and not self._stop_reading:
|
|
230
|
+
# Check for stop condition
|
|
231
|
+
if self._stop_reading:
|
|
232
|
+
logger.info("๐ Stop reading flag set, breaking loop")
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
# Update progress
|
|
236
|
+
elapsed = time.time() - start_time
|
|
237
|
+
progress = 20 + (elapsed / timeout) * 70 # 20-90% for reading phase
|
|
238
|
+
|
|
239
|
+
# Periodic detailed logging
|
|
240
|
+
current_time = time.time()
|
|
241
|
+
if current_time - last_log_time >= log_interval:
|
|
242
|
+
log_both('info', f"๐ PROGRESS UPDATE: {elapsed:.1f}s/{timeout}s ({progress:.1f}%)")
|
|
243
|
+
log_both('info', f"๐ Serial port status: connected={self._serial_conn.is_open if self._serial_conn else False}")
|
|
244
|
+
log_both('info', f"๐ Lines captured so far: {len(self._analysis.raw_output)}")
|
|
245
|
+
log_both('info', f"๐ Interactions sent: {interaction_step}/{len(self._interaction_sequences)}")
|
|
246
|
+
log_both('info', f"๐ Device type detected: {self._analysis.device_type}")
|
|
247
|
+
log_both('info', f"๐ Login detected: {self._analysis.login_detected}")
|
|
248
|
+
|
|
249
|
+
# Check serial connection health
|
|
250
|
+
if self._serial_conn:
|
|
251
|
+
try:
|
|
252
|
+
waiting_bytes = self._serial_conn.in_waiting
|
|
253
|
+
log_both('info', f"๐ Bytes waiting in buffer: {waiting_bytes}")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
log_both('warning', f"๐ Error checking in_waiting: {e}")
|
|
256
|
+
|
|
257
|
+
last_log_time = current_time
|
|
258
|
+
|
|
259
|
+
# Collect any new output
|
|
260
|
+
new_output = self._collect_output()
|
|
261
|
+
if new_output:
|
|
262
|
+
self._analysis.raw_output.extend(new_output)
|
|
263
|
+
log_both('info', f"๐ฅ Received {len(new_output)} lines of output:")
|
|
264
|
+
for i, line in enumerate(new_output[:5]): # Show first 5 lines
|
|
265
|
+
log_both('info', f"๐ฅ Line {i+1}: {repr(line)}")
|
|
266
|
+
if len(new_output) > 5:
|
|
267
|
+
log_both('info', f"๐ฅ ... and {len(new_output) - 5} more lines")
|
|
268
|
+
|
|
269
|
+
# Analyze output in real-time
|
|
270
|
+
if analyze_output:
|
|
271
|
+
self._analyze_output_patterns(new_output)
|
|
272
|
+
log_both('info', f"๐ Analysis updated: device_type={self._analysis.device_type}, confidence={self._analysis.confidence:.2f}")
|
|
273
|
+
else:
|
|
274
|
+
log_both('debug', f"๐ญ No new output available (elapsed: {elapsed:.1f}s)")
|
|
275
|
+
|
|
276
|
+
# Simple early exit: all interactions done + have data = exit immediately
|
|
277
|
+
interactions_complete = interaction_step >= len(self._interaction_sequences)
|
|
278
|
+
sufficient_data = len(self._analysis.raw_output) >= min_data_threshold
|
|
279
|
+
|
|
280
|
+
if interactions_complete and sufficient_data:
|
|
281
|
+
log_both('info', f"๐ฏ SIMPLE EARLY EXIT: All done!")
|
|
282
|
+
log_both('info', f"๐ฏ - All interactions sent: {interaction_step}/{len(self._interaction_sequences)} โ
")
|
|
283
|
+
log_both('info', f"๐ฏ - Sufficient data captured: {len(self._analysis.raw_output)} lines โ
")
|
|
284
|
+
# Stop reader thread early to speed up cleanup
|
|
285
|
+
self._stop_reading = True
|
|
286
|
+
log_both('info', f"๐ฏ Exiting after {elapsed:.1f}s instead of waiting full {timeout}s")
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
# Auto-interact if enabled and no recent output
|
|
290
|
+
if (auto_interact and
|
|
291
|
+
current_time - last_interaction_time > 3 and # Wait 3 seconds between interactions
|
|
292
|
+
interaction_step < len(self._interaction_sequences)):
|
|
293
|
+
|
|
294
|
+
interaction = self._interaction_sequences[interaction_step]
|
|
295
|
+
log_both('info', f"๐ฌ Sending interaction #{interaction_step}: {repr(interaction)}")
|
|
296
|
+
|
|
297
|
+
if self._send_data(interaction):
|
|
298
|
+
last_interaction_time = current_time
|
|
299
|
+
interaction_step += 1
|
|
300
|
+
log_both('info', f"โ
Successfully sent interaction #{interaction_step-1}")
|
|
301
|
+
|
|
302
|
+
async_result.update(
|
|
303
|
+
status=True,
|
|
304
|
+
progress=progress,
|
|
305
|
+
message=f"Sent interaction: {repr(interaction)} | Lines captured: {len(self._analysis.raw_output)}",
|
|
306
|
+
data={
|
|
307
|
+
"interactions_sent": interaction_step,
|
|
308
|
+
"output_lines": len(self._analysis.raw_output),
|
|
309
|
+
"login_detected": self._analysis.login_detected,
|
|
310
|
+
"device_type": self._analysis.device_type
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
log_both('error', f"โ Failed to send interaction #{interaction_step}: {repr(interaction)}")
|
|
315
|
+
else:
|
|
316
|
+
log_both('debug', f"โณ Waiting... (elapsed: {elapsed:.1f}s, next_interaction: {interaction_step < len(self._interaction_sequences)})")
|
|
317
|
+
async_result.update(
|
|
318
|
+
status=True,
|
|
319
|
+
progress=progress,
|
|
320
|
+
message=f"Reading serial output... | Lines captured: {len(self._analysis.raw_output)}",
|
|
321
|
+
data={
|
|
322
|
+
"output_lines": len(self._analysis.raw_output),
|
|
323
|
+
"login_detected": self._analysis.login_detected,
|
|
324
|
+
"device_type": self._analysis.device_type
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Short sleep to prevent busy waiting
|
|
329
|
+
await asyncio.sleep(0.5)
|
|
330
|
+
|
|
331
|
+
logger.info(f"๐ Main loop completed: elapsed={time.time() - start_time:.1f}s, lines_captured={len(self._analysis.raw_output)}")
|
|
332
|
+
|
|
333
|
+
# Final analysis
|
|
334
|
+
async_result.update(
|
|
335
|
+
status=True,
|
|
336
|
+
progress=90,
|
|
337
|
+
message="Performing final analysis...",
|
|
338
|
+
data=self._get_analysis_data()
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if analyze_output:
|
|
342
|
+
final_analysis = self._perform_comprehensive_analysis()
|
|
343
|
+
self._analysis = final_analysis
|
|
344
|
+
|
|
345
|
+
# Generate final report
|
|
346
|
+
report = self._generate_analysis_report()
|
|
347
|
+
|
|
348
|
+
async_result.update(
|
|
349
|
+
status=True,
|
|
350
|
+
progress=100,
|
|
351
|
+
message="Serial reading and analysis completed",
|
|
352
|
+
data={
|
|
353
|
+
"analysis": self._get_analysis_data(),
|
|
354
|
+
"report": report,
|
|
355
|
+
"total_output_lines": len(self._analysis.raw_output),
|
|
356
|
+
"execution_time": time.time() - start_time
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
logger.info(f"Returning AsyncExploitResult with status={async_result.status}, message='{async_result.message}'")
|
|
361
|
+
return async_result
|
|
362
|
+
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Error during serial reading: {str(e)}")
|
|
365
|
+
async_result.update(
|
|
366
|
+
status=False,
|
|
367
|
+
message=f"Serial reading failed: {str(e)}",
|
|
368
|
+
progress=100,
|
|
369
|
+
data={"error": str(e)}
|
|
370
|
+
)
|
|
371
|
+
return async_result
|
|
372
|
+
finally:
|
|
373
|
+
# Cleanup
|
|
374
|
+
await self._cleanup()
|
|
375
|
+
|
|
376
|
+
@hookimpl
|
|
377
|
+
def stop(self):
|
|
378
|
+
"""Stop the running serial reading"""
|
|
379
|
+
logger.info("Stopping serial reading")
|
|
380
|
+
self._stop_reading = True
|
|
381
|
+
|
|
382
|
+
@hookimpl
|
|
383
|
+
def cleanup(self):
|
|
384
|
+
"""Cleanup after serial reading"""
|
|
385
|
+
logger.info("Cleaning up PicocomSerialReaderPlugin")
|
|
386
|
+
# Perform synchronous cleanup instead of creating async task
|
|
387
|
+
self._cleanup_sync()
|
|
388
|
+
|
|
389
|
+
def _cleanup_sync(self):
|
|
390
|
+
"""Synchronous cleanup for serial connection and threads"""
|
|
391
|
+
logger.info("Cleaning up serial connection (sync)...")
|
|
392
|
+
self._stop_reading = True
|
|
393
|
+
|
|
394
|
+
# Wait for reader thread to finish (non-blocking)
|
|
395
|
+
if self._reader_thread and self._reader_thread.is_alive():
|
|
396
|
+
self._reader_thread.join(timeout=2)
|
|
397
|
+
if self._reader_thread.is_alive():
|
|
398
|
+
logger.warning("Reader thread did not stop within 2s โ skipping join to avoid hang")
|
|
399
|
+
# Do NOT attempt to close the serial port while the reader thread is still active
|
|
400
|
+
# to avoid potential deadlocks. We will detach and let it die with the process.
|
|
401
|
+
self._reader_thread = None
|
|
402
|
+
return # Skip the rest of cleanup
|
|
403
|
+
|
|
404
|
+
# Close serial connection (safe to do only if reader thread has stopped)
|
|
405
|
+
if self._serial_conn and self._serial_conn.is_open:
|
|
406
|
+
try:
|
|
407
|
+
self._serial_conn.close()
|
|
408
|
+
logger.info("Serial connection closed")
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(f"Error closing serial connection: {e}")
|
|
411
|
+
|
|
412
|
+
self._serial_conn = None
|
|
413
|
+
self._reader_thread = None
|
|
414
|
+
|
|
415
|
+
async def _cleanup(self):
|
|
416
|
+
"""Async cleanup for use during normal execution"""
|
|
417
|
+
logger.info("Cleaning up serial connection...")
|
|
418
|
+
self._stop_reading = True
|
|
419
|
+
|
|
420
|
+
# Wait for reader thread to finish (non-blocking)
|
|
421
|
+
if self._reader_thread and self._reader_thread.is_alive():
|
|
422
|
+
self._reader_thread.join(timeout=2)
|
|
423
|
+
if self._reader_thread.is_alive():
|
|
424
|
+
logger.warning("Reader thread did not stop within 2s โ skipping join to avoid hang")
|
|
425
|
+
# Do NOT attempt to close the serial port while the reader thread is still active
|
|
426
|
+
# to avoid potential deadlocks. We will detach and let it die with the process.
|
|
427
|
+
self._reader_thread = None
|
|
428
|
+
return # Skip the rest of cleanup
|
|
429
|
+
|
|
430
|
+
# Close serial connection (safe to do only if reader thread has stopped)
|
|
431
|
+
if self._serial_conn and self._serial_conn.is_open:
|
|
432
|
+
try:
|
|
433
|
+
self._serial_conn.close()
|
|
434
|
+
logger.info("Serial connection closed")
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(f"Error closing serial connection: {e}")
|
|
437
|
+
|
|
438
|
+
self._serial_conn = None
|
|
439
|
+
self._reader_thread = None
|
|
440
|
+
|
|
441
|
+
def _list_serial_ports(self) -> List[str]:
|
|
442
|
+
"""List available serial ports"""
|
|
443
|
+
try:
|
|
444
|
+
ports = serial.tools.list_ports.comports()
|
|
445
|
+
return [port.device for port in ports]
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(f"Error listing serial ports: {e}")
|
|
448
|
+
return []
|
|
449
|
+
|
|
450
|
+
async def _connect_serial(self, port: str, baudrate: int) -> bool:
|
|
451
|
+
"""Connect to serial port"""
|
|
452
|
+
try:
|
|
453
|
+
log_both('info', f"๐ Attempting to connect to serial port {port} at {baudrate} baud...")
|
|
454
|
+
log_both('info', f"๐ Connection parameters: 8N1, timeout=1s, no flow control")
|
|
455
|
+
|
|
456
|
+
self._serial_conn = serial.Serial(
|
|
457
|
+
port=port,
|
|
458
|
+
baudrate=baudrate,
|
|
459
|
+
bytesize=serial.EIGHTBITS,
|
|
460
|
+
parity=serial.PARITY_NONE,
|
|
461
|
+
stopbits=serial.STOPBITS_ONE,
|
|
462
|
+
timeout=1,
|
|
463
|
+
xonxoff=False,
|
|
464
|
+
rtscts=False,
|
|
465
|
+
dsrdtr=False
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if self._serial_conn.is_open:
|
|
469
|
+
log_both('info', f"โ
Successfully connected to {port} at {baudrate} baud")
|
|
470
|
+
log_both('info', f"โ
Serial port details: name={self._serial_conn.name}, is_open={self._serial_conn.is_open}")
|
|
471
|
+
log_both('info', f"โ
Port settings: baudrate={self._serial_conn.baudrate}, bytesize={self._serial_conn.bytesize}")
|
|
472
|
+
|
|
473
|
+
# Try to read any initial data
|
|
474
|
+
initial_waiting = self._serial_conn.in_waiting
|
|
475
|
+
log_both('info', f"โ
Initial bytes waiting: {initial_waiting}")
|
|
476
|
+
|
|
477
|
+
return True
|
|
478
|
+
else:
|
|
479
|
+
log_both('error', f"โ Failed to open serial port {port}")
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
except Exception as e:
|
|
483
|
+
log_both('error', f"โ Error connecting to serial port {port}: {e}")
|
|
484
|
+
log_both('error', f"โ Exception type: {type(e).__name__}")
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
def _start_reader_thread(self):
|
|
488
|
+
"""Start the serial reading thread"""
|
|
489
|
+
if self._reader_thread and self._reader_thread.is_alive():
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
self._reader_thread = Thread(target=self._reader_loop, daemon=True)
|
|
493
|
+
self._reader_thread.start()
|
|
494
|
+
|
|
495
|
+
def _reader_loop(self):
|
|
496
|
+
"""Main reading loop running in separate thread"""
|
|
497
|
+
logger.info("๐งต Serial reader thread started")
|
|
498
|
+
buffer = b""
|
|
499
|
+
total_bytes_read = 0
|
|
500
|
+
total_lines_queued = 0
|
|
501
|
+
last_activity_log = time.time()
|
|
502
|
+
activity_log_interval = 30.0 # Log activity every 30 seconds
|
|
503
|
+
|
|
504
|
+
while not self._stop_reading and self._serial_conn and self._serial_conn.is_open:
|
|
505
|
+
try:
|
|
506
|
+
current_time = time.time()
|
|
507
|
+
|
|
508
|
+
# Periodic activity logging
|
|
509
|
+
if current_time - last_activity_log >= activity_log_interval:
|
|
510
|
+
logger.info(f"๐งต Reader thread status: bytes_read={total_bytes_read}, lines_queued={total_lines_queued}")
|
|
511
|
+
logger.info(f"๐งต Buffer size: {len(buffer)} bytes")
|
|
512
|
+
logger.info(f"๐งต Serial port connected: {self._serial_conn.is_open}")
|
|
513
|
+
last_activity_log = current_time
|
|
514
|
+
|
|
515
|
+
waiting = self._serial_conn.in_waiting
|
|
516
|
+
if waiting > 0:
|
|
517
|
+
logger.debug(f"๐งต Reading {waiting} bytes from serial port")
|
|
518
|
+
data = self._serial_conn.read(waiting)
|
|
519
|
+
buffer += data
|
|
520
|
+
total_bytes_read += len(data)
|
|
521
|
+
|
|
522
|
+
logger.debug(f"๐งต Read {len(data)} bytes, buffer now {len(buffer)} bytes")
|
|
523
|
+
|
|
524
|
+
# Process complete lines
|
|
525
|
+
lines_in_this_batch = 0
|
|
526
|
+
while b'\n' in buffer or b'\r' in buffer:
|
|
527
|
+
if b'\r\n' in buffer:
|
|
528
|
+
line, buffer = buffer.split(b'\r\n', 1)
|
|
529
|
+
elif b'\n' in buffer:
|
|
530
|
+
line, buffer = buffer.split(b'\n', 1)
|
|
531
|
+
elif b'\r' in buffer:
|
|
532
|
+
line, buffer = buffer.split(b'\r', 1)
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
decoded_line = line.decode('utf-8', errors='replace').strip()
|
|
536
|
+
if decoded_line: # Only queue non-empty lines
|
|
537
|
+
self._output_queue.put(decoded_line)
|
|
538
|
+
lines_in_this_batch += 1
|
|
539
|
+
total_lines_queued += 1
|
|
540
|
+
logger.debug(f"๐งต Queued line #{total_lines_queued}: {repr(decoded_line)}")
|
|
541
|
+
except Exception as e:
|
|
542
|
+
logger.warning(f"๐งต Error decoding line: {e}")
|
|
543
|
+
|
|
544
|
+
if lines_in_this_batch > 0:
|
|
545
|
+
logger.info(f"๐งต Processed {lines_in_this_batch} lines from {len(data)} bytes")
|
|
546
|
+
else:
|
|
547
|
+
logger.debug(f"๐งต No data waiting (checked at {current_time:.1f})")
|
|
548
|
+
|
|
549
|
+
time.sleep(0.01) # Small delay to prevent excessive CPU usage
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
logger.error(f"๐งต Error in reader loop: {e}")
|
|
553
|
+
logger.error(f"๐งต Exception type: {type(e).__name__}")
|
|
554
|
+
import traceback
|
|
555
|
+
logger.error(f"๐งต Traceback: {traceback.format_exc()}")
|
|
556
|
+
break
|
|
557
|
+
|
|
558
|
+
logger.info(f"๐งต Serial reader thread ended - Total: {total_bytes_read} bytes, {total_lines_queued} lines")
|
|
559
|
+
|
|
560
|
+
def _collect_output(self) -> List[str]:
|
|
561
|
+
"""Collect available output from the queue"""
|
|
562
|
+
output = []
|
|
563
|
+
try:
|
|
564
|
+
while True:
|
|
565
|
+
line = self._output_queue.get_nowait()
|
|
566
|
+
output.append(line)
|
|
567
|
+
except Empty:
|
|
568
|
+
pass
|
|
569
|
+
return output
|
|
570
|
+
|
|
571
|
+
def _send_data(self, data: str) -> bool:
|
|
572
|
+
"""Send data to serial port"""
|
|
573
|
+
try:
|
|
574
|
+
if self._serial_conn and self._serial_conn.is_open:
|
|
575
|
+
encoded_data = data.encode('utf-8')
|
|
576
|
+
log_both('info', f"๐ค Sending {len(encoded_data)} bytes: {repr(data)}")
|
|
577
|
+
self._serial_conn.write(encoded_data)
|
|
578
|
+
self._serial_conn.flush()
|
|
579
|
+
log_both('info', f"๐ค Data sent and flushed successfully")
|
|
580
|
+
return True
|
|
581
|
+
else:
|
|
582
|
+
log_both('error', f"๐ค Cannot send data - serial connection not available")
|
|
583
|
+
return False
|
|
584
|
+
except Exception as e:
|
|
585
|
+
log_both('error', f"๐ค Error sending data: {e}")
|
|
586
|
+
log_both('error', f"๐ค Exception type: {type(e).__name__}")
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
def _analyze_output_patterns(self, output_lines: List[str]):
|
|
590
|
+
"""Analyze output for patterns in real-time"""
|
|
591
|
+
for line in output_lines:
|
|
592
|
+
line_lower = line.lower()
|
|
593
|
+
|
|
594
|
+
# Check for login patterns
|
|
595
|
+
for pattern in self._login_patterns:
|
|
596
|
+
if re.search(pattern, line_lower):
|
|
597
|
+
self._analysis.login_detected = True
|
|
598
|
+
self._analysis.detected_patterns.append(f"login_pattern: {pattern}")
|
|
599
|
+
logger.info(f"Login pattern detected: {pattern}")
|
|
600
|
+
|
|
601
|
+
# Check for shell types
|
|
602
|
+
for shell_type, patterns in self._shell_patterns.items():
|
|
603
|
+
for pattern in patterns:
|
|
604
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
605
|
+
if self._analysis.shell_type == "unknown" or self._analysis.confidence < 0.8:
|
|
606
|
+
self._analysis.shell_type = shell_type
|
|
607
|
+
self._analysis.device_type = shell_type
|
|
608
|
+
self._analysis.confidence = 0.8
|
|
609
|
+
self._analysis.detected_patterns.append(f"shell_pattern: {shell_type}")
|
|
610
|
+
logger.info(f"Shell type detected: {shell_type}")
|
|
611
|
+
|
|
612
|
+
def _perform_comprehensive_analysis(self) -> SerialAnalysis:
|
|
613
|
+
"""Perform comprehensive analysis of all collected output"""
|
|
614
|
+
analysis = SerialAnalysis()
|
|
615
|
+
analysis.raw_output = self._analysis.raw_output.copy()
|
|
616
|
+
analysis.detected_patterns = self._analysis.detected_patterns.copy()
|
|
617
|
+
analysis.login_detected = self._analysis.login_detected
|
|
618
|
+
|
|
619
|
+
all_output = "\n".join(analysis.raw_output)
|
|
620
|
+
all_output_lower = all_output.lower()
|
|
621
|
+
|
|
622
|
+
# Advanced pattern matching
|
|
623
|
+
confidence_scores = {}
|
|
624
|
+
|
|
625
|
+
# Boot sequence analysis
|
|
626
|
+
boot_patterns = {
|
|
627
|
+
'uboot': [r'u-boot', r'hit any key to stop autoboot', r'=>'],
|
|
628
|
+
'linux': [r'linux version', r'kernel', r'init:', r'systemd'],
|
|
629
|
+
'busybox': [r'busybox', r'built-in shell'],
|
|
630
|
+
'embedded': [r'firmware', r'bootloader', r'flash'],
|
|
631
|
+
'network': [r'ip address', r'dhcp', r'network', r'ethernet'],
|
|
632
|
+
'iot': [r'wifi', r'sensor', r'temperature', r'gpio']
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for device_type, patterns in boot_patterns.items():
|
|
636
|
+
score = 0
|
|
637
|
+
for pattern in patterns:
|
|
638
|
+
matches = len(re.findall(pattern, all_output_lower))
|
|
639
|
+
score += matches * 0.2
|
|
640
|
+
|
|
641
|
+
if score > 0:
|
|
642
|
+
confidence_scores[device_type] = min(score, 1.0)
|
|
643
|
+
|
|
644
|
+
# Determine most likely device type
|
|
645
|
+
if confidence_scores:
|
|
646
|
+
best_match = max(confidence_scores.items(), key=lambda x: x[1])
|
|
647
|
+
analysis.device_type = best_match[0]
|
|
648
|
+
analysis.confidence = best_match[1]
|
|
649
|
+
|
|
650
|
+
# Extract firmware information
|
|
651
|
+
version_patterns = [
|
|
652
|
+
r'version\s+([0-9.]+)',
|
|
653
|
+
r'firmware\s+([0-9.]+)',
|
|
654
|
+
r'build\s+([0-9.]+)',
|
|
655
|
+
r'rev\s+([0-9.]+)'
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
for pattern in version_patterns:
|
|
659
|
+
matches = re.findall(pattern, all_output_lower)
|
|
660
|
+
if matches:
|
|
661
|
+
analysis.firmware_info['version'] = matches[0]
|
|
662
|
+
break
|
|
663
|
+
|
|
664
|
+
return analysis
|
|
665
|
+
|
|
666
|
+
def _generate_analysis_report(self) -> str:
|
|
667
|
+
"""Generate a human-readable analysis report"""
|
|
668
|
+
report = []
|
|
669
|
+
report.append("=== Serial Port Analysis Report ===")
|
|
670
|
+
report.append(f"Device Type: {self._analysis.device_type}")
|
|
671
|
+
report.append(f"Confidence: {self._analysis.confidence:.2f}")
|
|
672
|
+
report.append(f"Login Shell Detected: {'Yes' if self._analysis.login_detected else 'No'}")
|
|
673
|
+
report.append(f"Shell Type: {self._analysis.shell_type}")
|
|
674
|
+
report.append(f"Total Output Lines: {len(self._analysis.raw_output)}")
|
|
675
|
+
|
|
676
|
+
if self._analysis.firmware_info:
|
|
677
|
+
report.append("\nFirmware Information:")
|
|
678
|
+
for key, value in self._analysis.firmware_info.items():
|
|
679
|
+
report.append(f" {key}: {value}")
|
|
680
|
+
|
|
681
|
+
if self._analysis.detected_patterns:
|
|
682
|
+
report.append("\nDetected Patterns:")
|
|
683
|
+
for pattern in set(self._analysis.detected_patterns): # Remove duplicates
|
|
684
|
+
report.append(f" - {pattern}")
|
|
685
|
+
|
|
686
|
+
if self._analysis.raw_output:
|
|
687
|
+
report.append("\nSample Output (last 10 lines):")
|
|
688
|
+
for line in self._analysis.raw_output[-10:]:
|
|
689
|
+
report.append(f" > {line}")
|
|
690
|
+
|
|
691
|
+
return "\n".join(report)
|
|
692
|
+
|
|
693
|
+
def _get_analysis_data(self) -> Dict:
|
|
694
|
+
"""Get analysis data as dictionary"""
|
|
695
|
+
return {
|
|
696
|
+
"device_type": self._analysis.device_type,
|
|
697
|
+
"confidence": self._analysis.confidence,
|
|
698
|
+
"login_detected": self._analysis.login_detected,
|
|
699
|
+
"shell_type": self._analysis.shell_type,
|
|
700
|
+
"firmware_info": self._analysis.firmware_info,
|
|
701
|
+
"detected_patterns": self._analysis.detected_patterns,
|
|
702
|
+
"output_lines_count": len(self._analysis.raw_output),
|
|
703
|
+
"raw_output_sample": self._analysis.raw_output[-5:] if self._analysis.raw_output else []
|
|
704
|
+
}
|