moriarty-project 0.1.20__py3-none-any.whl → 0.1.23__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.
- moriarty/__init__.py +1 -1
- moriarty/cli/app.py +5 -6
- moriarty/modules/port_scanner.py +1 -5
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/METADATA +36 -49
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/RECORD +7 -22
- moriarty/cli/wifippler.py +0 -124
- moriarty/modules/wifippler/__init__.py +0 -65
- moriarty/modules/wifippler/core/__init__.py +0 -80
- moriarty/modules/wifippler/core/attacks/__init__.py +0 -19
- moriarty/modules/wifippler/core/attacks/deauth.py +0 -340
- moriarty/modules/wifippler/core/attacks/handshake.py +0 -402
- moriarty/modules/wifippler/core/attacks/pmkid.py +0 -424
- moriarty/modules/wifippler/core/attacks/wep.py +0 -467
- moriarty/modules/wifippler/core/attacks/wpa.py +0 -446
- moriarty/modules/wifippler/core/attacks/wps.py +0 -474
- moriarty/modules/wifippler/core/models/__init__.py +0 -10
- moriarty/modules/wifippler/core/models/network.py +0 -216
- moriarty/modules/wifippler/core/scanner.py +0 -901
- moriarty/modules/wifippler/core/utils/__init__.py +0 -622
- moriarty/modules/wifippler/core/utils.py +0 -851
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/WHEEL +0 -0
- {moriarty_project-0.1.20.dist-info → moriarty_project-0.1.23.dist-info}/entry_points.txt +0 -0
@@ -1,901 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Módulo de escaneamento WiFi para o WifiPPLER.
|
3
|
-
|
4
|
-
Este módulo fornece funcionalidades avançadas para descoberta e análise de redes WiFi,
|
5
|
-
incluindo detecção de clientes, análise de sinal e suporte a diferentes modos de varredura.
|
6
|
-
"""
|
7
|
-
import asyncio
|
8
|
-
import json
|
9
|
-
import time
|
10
|
-
import signal
|
11
|
-
import tempfile
|
12
|
-
import shutil
|
13
|
-
from pathlib import Path
|
14
|
-
from typing import Dict, List, Optional, Set, Tuple, Any, Callable
|
15
|
-
from dataclasses import dataclass, field, asdict
|
16
|
-
from datetime import datetime, timedelta
|
17
|
-
import subprocess
|
18
|
-
import re
|
19
|
-
import logging
|
20
|
-
from enum import Enum, auto
|
21
|
-
from concurrent.futures import ThreadPoolExecutor
|
22
|
-
|
23
|
-
from rich.console import Console
|
24
|
-
from rich.table import Table, Column
|
25
|
-
from rich.progress import (
|
26
|
-
Progress, SpinnerColumn, TextColumn, BarColumn,
|
27
|
-
TimeElapsedColumn, TaskID
|
28
|
-
)
|
29
|
-
|
30
|
-
from moriarty.modules.wifippler.core.utils import (
|
31
|
-
run_command_async, is_root, get_interface_mac,
|
32
|
-
set_monitor_mode, restore_network_interface, command_exists,
|
33
|
-
randomize_mac, get_wireless_interfaces
|
34
|
-
)
|
35
|
-
|
36
|
-
# Configuração de logging
|
37
|
-
logging.basicConfig(
|
38
|
-
level=logging.INFO,
|
39
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
40
|
-
)
|
41
|
-
logger = logging.getLogger(__name__)
|
42
|
-
|
43
|
-
class ScanMode(Enum):
|
44
|
-
"""Modos de varredura disponíveis."""
|
45
|
-
ACTIVE = auto() # Varredura ativa (envia pacotes)
|
46
|
-
PASSIVE = auto() # Varredura passiva (apenas escuta)
|
47
|
-
FAST = auto() # Varredura rápida (canais mais comuns)
|
48
|
-
DEEP = auto() # Varredura profunda (todos os canais)
|
49
|
-
|
50
|
-
class WiFiSecurityType(Enum):
|
51
|
-
"""Tipos de segurança de rede WiFi."""
|
52
|
-
NONE = "Aberta"
|
53
|
-
WEP = "WEP"
|
54
|
-
WPA = "WPA"
|
55
|
-
WPA2 = "WPA2"
|
56
|
-
WPA3 = "WPA3"
|
57
|
-
WPA_WPA2 = "WPA/WPA2"
|
58
|
-
WPA2_WPA3 = "WPA2/WPA3"
|
59
|
-
UNKNOWN = "Desconhecido"
|
60
|
-
|
61
|
-
@dataclass
|
62
|
-
class WiFiClient:
|
63
|
-
"""Representa um dispositivo cliente conectado a uma rede WiFi."""
|
64
|
-
mac: str
|
65
|
-
bssid: str
|
66
|
-
signal: int
|
67
|
-
packets: int = 0
|
68
|
-
first_seen: datetime = field(default_factory=datetime.utcnow)
|
69
|
-
last_seen: datetime = field(default_factory=datetime.utcnow)
|
70
|
-
vendor: Optional[str] = None
|
71
|
-
is_associated: bool = True
|
72
|
-
|
73
|
-
def to_dict(self) -> Dict:
|
74
|
-
"""Converte para dicionário."""
|
75
|
-
return {
|
76
|
-
'mac': self.mac,
|
77
|
-
'bssid': self.bssid,
|
78
|
-
'signal': self.signal,
|
79
|
-
'packets': self.packets,
|
80
|
-
'first_seen': self.first_seen.isoformat(),
|
81
|
-
'last_seen': self.last_seen.isoformat(),
|
82
|
-
'vendor': self.vendor,
|
83
|
-
'is_associated': self.is_associated
|
84
|
-
}
|
85
|
-
|
86
|
-
def update_seen(self):
|
87
|
-
"""Atualiza o timestamp da última vez que foi visto."""
|
88
|
-
self.last_seen = datetime.utcnow()
|
89
|
-
|
90
|
-
@dataclass
|
91
|
-
class WiFiNetwork:
|
92
|
-
"""Represents a discovered WiFi network."""
|
93
|
-
bssid: str
|
94
|
-
ssid: str
|
95
|
-
channel: int
|
96
|
-
signal: int
|
97
|
-
encryption: str
|
98
|
-
cipher: str
|
99
|
-
authentication: str
|
100
|
-
wps: bool = False
|
101
|
-
wps_locked: bool = False
|
102
|
-
clients: List[Dict[str, str]] = field(default_factory=list)
|
103
|
-
last_seen: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
104
|
-
|
105
|
-
def to_dict(self) -> Dict:
|
106
|
-
"""Convert to dictionary."""
|
107
|
-
return asdict(self)
|
108
|
-
|
109
|
-
def to_json(self) -> str:
|
110
|
-
"""Convert to JSON string."""
|
111
|
-
return json.dumps(self.to_dict(), indent=2)
|
112
|
-
|
113
|
-
class WiFiScanner:
|
114
|
-
"""Classe principal para escaneamento de redes WiFi.
|
115
|
-
|
116
|
-
Esta classe fornece funcionalidades avançadas para descoberta e análise
|
117
|
-
de redes WiFi, incluindo detecção de clientes, análise de sinal e suporte
|
118
|
-
a diferentes modos de varredura.
|
119
|
-
"""
|
120
|
-
|
121
|
-
def __init__(
|
122
|
-
self,
|
123
|
-
interface: str = None,
|
124
|
-
scan_time: int = 10,
|
125
|
-
scan_mode: ScanMode = ScanMode.ACTIVE,
|
126
|
-
output_dir: str = None,
|
127
|
-
random_mac: bool = False,
|
128
|
-
verbose: bool = False
|
129
|
-
):
|
130
|
-
"""Inicializa o scanner WiFi.
|
131
|
-
|
132
|
-
Args:
|
133
|
-
interface: Interface de rede a ser usada para escaneamento
|
134
|
-
scan_time: Tempo de escaneamento em segundos
|
135
|
-
scan_mode: Modo de varredura (ACTIVE, PASSIVE, FAST, DEEP)
|
136
|
-
output_dir: Diretório para salvar os resultados
|
137
|
-
random_mac: Se deve usar um endereço MAC aleatório
|
138
|
-
verbose: Habilita saída detalhada
|
139
|
-
"""
|
140
|
-
if not is_root():
|
141
|
-
raise PermissionError("Este aplicativo requer privilégios de root para executar.")
|
142
|
-
|
143
|
-
# Configuração básica
|
144
|
-
self.scan_time = max(5, min(scan_time, 300)) # Limita entre 5 e 300 segundos
|
145
|
-
self.scan_mode = scan_mode
|
146
|
-
self.verbose = verbose
|
147
|
-
self.running = False
|
148
|
-
self.original_mac = None
|
149
|
-
self.original_mode = None
|
150
|
-
self.temp_dir = None
|
151
|
-
|
152
|
-
# Configuração de saída
|
153
|
-
self.output_dir = Path(output_dir) if output_dir else Path.cwd() / 'wifippler_scan'
|
154
|
-
self.output_file = self.output_dir / 'scan_results.json'
|
155
|
-
|
156
|
-
# Configuração da interface
|
157
|
-
self.interface = self._setup_interface(interface, random_mac)
|
158
|
-
|
159
|
-
# Dados
|
160
|
-
self.networks: Dict[str, WiFiNetwork] = {}
|
161
|
-
self.clients: Dict[str, WiFiClient] = {}
|
162
|
-
self.scan_start_time = None
|
163
|
-
self.scan_end_time = None
|
164
|
-
|
165
|
-
# Interface de usuário
|
166
|
-
self.console = Console()
|
167
|
-
self.progress = Progress(
|
168
|
-
SpinnerColumn(),
|
169
|
-
TextColumn("[progress.description]{task.description}"),
|
170
|
-
BarColumn(bar_width=40),
|
171
|
-
"[progress.percentage]{task.percentage:>3.0f}%",
|
172
|
-
TimeElapsedColumn(),
|
173
|
-
console=self.console,
|
174
|
-
transient=True,
|
175
|
-
)
|
176
|
-
|
177
|
-
# Thread pool para operações em segundo plano
|
178
|
-
self.executor = ThreadPoolExecutor(max_workers=4)
|
179
|
-
|
180
|
-
# Configuração de sinal para encerramento gracioso
|
181
|
-
self.should_stop = False
|
182
|
-
signal.signal(signal.SIGINT, self._signal_handler)
|
183
|
-
signal.signal(signal.SIGTERM, self._signal_handler)
|
184
|
-
|
185
|
-
def _signal_handler(self, signum, frame):
|
186
|
-
"""Manipula sinais de encerramento."""
|
187
|
-
self.console.print("\n[bold yellow]Recebido sinal de encerramento. Finalizando...[/]")
|
188
|
-
self.should_stop = True
|
189
|
-
|
190
|
-
def _setup_interface(self, interface: str = None, random_mac: bool = False) -> str:
|
191
|
-
"""Configura a interface de rede para escaneamento.
|
192
|
-
|
193
|
-
Args:
|
194
|
-
interface: Nome da interface
|
195
|
-
random_mac: Se deve usar um endereço MAC aleatório
|
196
|
-
|
197
|
-
Returns:
|
198
|
-
str: Nome da interface configurada
|
199
|
-
"""
|
200
|
-
# Obtém interfaces disponíveis
|
201
|
-
interfaces = get_wireless_interfaces()
|
202
|
-
if not interfaces:
|
203
|
-
raise RuntimeError("Nenhuma interface WiFi encontrada.")
|
204
|
-
|
205
|
-
# Seleciona a interface
|
206
|
-
if interface:
|
207
|
-
if interface not in [iface['name'] for iface in interfaces]:
|
208
|
-
raise ValueError(f"Interface {interface} não encontrada ou não é sem fio.")
|
209
|
-
selected_iface = interface
|
210
|
-
else:
|
211
|
-
# Usa a primeira interface sem fio disponível
|
212
|
-
selected_iface = interfaces[0]['name']
|
213
|
-
|
214
|
-
# Salva o estado original
|
215
|
-
self.original_mac = get_interface_mac(selected_iface)
|
216
|
-
self.original_mode = 'managed' # Assumindo modo gerenciado por padrão
|
217
|
-
|
218
|
-
# Configura o endereço MAC aleatório, se solicitado
|
219
|
-
if random_mac:
|
220
|
-
if not randomize_mac(selected_iface):
|
221
|
-
logger.warning("Falha ao definir endereço MAC aleatório. Continuando com o endereço original.")
|
222
|
-
|
223
|
-
# Configura o modo monitor
|
224
|
-
if not set_monitor_mode(selected_iface):
|
225
|
-
raise RuntimeError(f"Falha ao configurar o modo monitor na interface {selected_iface}.")
|
226
|
-
|
227
|
-
return selected_iface
|
228
|
-
|
229
|
-
def _restore_interface(self) -> bool:
|
230
|
-
"""Restaura a interface para o estado original.
|
231
|
-
|
232
|
-
Returns:
|
233
|
-
bool: True se bem-sucedido, False caso contrário
|
234
|
-
"""
|
235
|
-
try:
|
236
|
-
# Restaura o modo original
|
237
|
-
if self.original_mode == 'managed':
|
238
|
-
if not restore_network_interface(self.interface):
|
239
|
-
logger.error(f"Falha ao restaurar o modo gerenciado na interface {self.interface}.")
|
240
|
-
return False
|
241
|
-
|
242
|
-
# Restaura o endereço MAC original, se necessário
|
243
|
-
if self.original_mac and get_interface_mac(self.interface) != self.original_mac:
|
244
|
-
# Implementar lógica para restaurar o MAC original
|
245
|
-
pass
|
246
|
-
|
247
|
-
return True
|
248
|
-
|
249
|
-
except Exception as e:
|
250
|
-
logger.error(f"Erro ao restaurar a interface: {e}")
|
251
|
-
return False
|
252
|
-
|
253
|
-
def _create_output_dir(self) -> bool:
|
254
|
-
"""Cria o diretório de saída, se necessário.
|
255
|
-
|
256
|
-
Returns:
|
257
|
-
bool: True se bem-sucedido, False caso contrário
|
258
|
-
"""
|
259
|
-
try:
|
260
|
-
self.output_dir.mkdir(parents=True, exist_ok=True)
|
261
|
-
return True
|
262
|
-
except Exception as e:
|
263
|
-
logger.error(f"Falha ao criar diretório de saída {self.output_dir}: {e}")
|
264
|
-
return False
|
265
|
-
|
266
|
-
def _parse_airodump_line(self, line: str) -> Optional[WiFiNetwork]:
|
267
|
-
"""Analisa uma linha de saída do airodump-ng.
|
268
|
-
|
269
|
-
Args:
|
270
|
-
line: Linha de saída do airodump-ng
|
271
|
-
|
272
|
-
Returns:
|
273
|
-
Optional[WiFiNetwork]: Objeto WiFiNetwork ou None se a linha for inválida
|
274
|
-
"""
|
275
|
-
if not line.strip() or line.startswith('BSSID') or line.startswith('Station'):
|
276
|
-
return None
|
277
|
-
|
278
|
-
parts = [p.strip() for p in line.split(',') if p.strip()]
|
279
|
-
if len(parts) < 10: # Não há dados suficientes para uma rede
|
280
|
-
return None
|
281
|
-
|
282
|
-
try:
|
283
|
-
# Extrai informações básicas
|
284
|
-
bssid = parts[0].upper()
|
285
|
-
first_seen = datetime.utcnow()
|
286
|
-
channel = int(parts[3]) if parts[3] else 1
|
287
|
-
signal = int(parts[8].split()[0]) # Remove 'dBm' se presente
|
288
|
-
|
289
|
-
# Determina o tipo de segurança
|
290
|
-
encryption = parts[5].strip() or 'NONE'
|
291
|
-
security = self._determine_security_type(encryption)
|
292
|
-
|
293
|
-
# Cria o objeto de rede
|
294
|
-
network = WiFiNetwork(
|
295
|
-
bssid=bssid,
|
296
|
-
ssid=parts[13] if len(parts) > 13 else "<hidden>",
|
297
|
-
channel=channel,
|
298
|
-
frequency=self._channel_to_frequency(channel),
|
299
|
-
signal=signal,
|
300
|
-
security=security,
|
301
|
-
encryption=encryption,
|
302
|
-
cipher=parts[6].strip() if len(parts) > 6 else '',
|
303
|
-
authentication=parts[7].strip() if len(parts) > 7 else '',
|
304
|
-
first_seen=first_seen,
|
305
|
-
last_seen=first_seen,
|
306
|
-
essid_hidden=len(parts) <= 13 or not parts[13].strip()
|
307
|
-
)
|
308
|
-
|
309
|
-
# Verifica recursos avançados
|
310
|
-
self._detect_network_features(network, parts)
|
311
|
-
|
312
|
-
return network
|
313
|
-
|
314
|
-
except (IndexError, ValueError) as e:
|
315
|
-
logger.debug(f"Não foi possível analisar a linha da rede: {line}. Erro: {e}")
|
316
|
-
return None
|
317
|
-
|
318
|
-
def _determine_security_type(self, encryption: str) -> WiFiSecurityType:
|
319
|
-
"""Determina o tipo de segurança com base na string de criptografia.
|
320
|
-
|
321
|
-
Args:
|
322
|
-
encryption: String de criptografia do airodump-ng
|
323
|
-
|
324
|
-
Returns:
|
325
|
-
WiFiSecurityType: Tipo de segurança detectado
|
326
|
-
"""
|
327
|
-
if not encryption or encryption.upper() == 'NONE':
|
328
|
-
return WiFiSecurityType.NONE
|
329
|
-
|
330
|
-
encryption = encryption.upper()
|
331
|
-
|
332
|
-
if 'WPA3' in encryption and 'WPA2' in encryption:
|
333
|
-
return WiFiSecurityType.WPA2_WPA3
|
334
|
-
elif 'WPA2' in encryption and 'WPA' in encryption:
|
335
|
-
return WiFiSecurityType.WPA_WPA2
|
336
|
-
elif 'WPA3' in encryption:
|
337
|
-
return WiFiSecurityType.WPA3
|
338
|
-
elif 'WPA2' in encryption:
|
339
|
-
return WiFiSecurityType.WPA2
|
340
|
-
elif 'WPA' in encryption:
|
341
|
-
return WiFiSecurityType.WPA
|
342
|
-
elif 'WEP' in encryption:
|
343
|
-
return WiFiSecurityType.WEP
|
344
|
-
else:
|
345
|
-
return WiFiSecurityType.UNKNOWN
|
346
|
-
|
347
|
-
def _channel_to_frequency(self, channel: int) -> int:
|
348
|
-
"""Converte um número de canal para frequência em MHz.
|
349
|
-
|
350
|
-
Args:
|
351
|
-
channel: Número do canal (1-14 para 2.4GHz, 36-165 para 5GHz)
|
352
|
-
|
353
|
-
Returns:
|
354
|
-
int: Frequência em MHz
|
355
|
-
"""
|
356
|
-
if 1 <= channel <= 13: # 2.4 GHz
|
357
|
-
return 2412 + (channel - 1) * 5
|
358
|
-
elif channel == 14: # 2.4 GHz (apenas Japão)
|
359
|
-
return 2484
|
360
|
-
elif 36 <= channel <= 165: # 5 GHz
|
361
|
-
return 5180 + (channel - 36) * 5
|
362
|
-
else:
|
363
|
-
return 0 # Canal desconhecido
|
364
|
-
|
365
|
-
def _detect_network_features(self, network: WiFiNetwork, parts: list) -> None:
|
366
|
-
"""Detecta recursos avançados da rede.
|
367
|
-
|
368
|
-
Args:
|
369
|
-
network: Objeto WiFiNetwork a ser atualizado
|
370
|
-
parts: Partes da linha do airodump-ng
|
371
|
-
"""
|
372
|
-
# Verifica WPS
|
373
|
-
if len(parts) > 9 and 'WPS' in parts[9]:
|
374
|
-
network.wps = True
|
375
|
-
# Detalhes adicionais do WPS podem ser extraídos aqui
|
376
|
-
|
377
|
-
# Verifica recursos avançados (HT/VHT/HE)
|
378
|
-
if len(parts) > 10:
|
379
|
-
flags = parts[10].upper()
|
380
|
-
network.ht = 'HT' in flags
|
381
|
-
network.vht = 'VHT' in flags
|
382
|
-
network.he = 'HE' in flags
|
383
|
-
|
384
|
-
# Verifica se é uma rede de alto rendimento
|
385
|
-
network.high_throughput = network.ht or network.vht or network.he
|
386
|
-
network.very_high_throughput = network.vht or network.he
|
387
|
-
|
388
|
-
async def scan_networks(self) -> List[WiFiNetwork]:
|
389
|
-
"""Realiza a varredura de redes WiFi próximas.
|
390
|
-
|
391
|
-
Returns:
|
392
|
-
List[WiFiNetwork]: Lista de redes WiFi descobertas
|
393
|
-
"""
|
394
|
-
if not self._create_output_dir():
|
395
|
-
raise RuntimeError("Falha ao criar diretório de saída")
|
396
|
-
|
397
|
-
self.scan_start_time = datetime.utcnow()
|
398
|
-
self.running = True
|
399
|
-
self.should_stop = False
|
400
|
-
|
401
|
-
# Configura o prefixo do arquivo temporário
|
402
|
-
temp_prefix = f"wifippler_scan_{int(time.time())}"
|
403
|
-
csv_file = self.output_dir / f"{temp_prefix}-01.csv"
|
404
|
-
|
405
|
-
# Configura os parâmetros do airodump-ng
|
406
|
-
cmd = [
|
407
|
-
"airodump-ng",
|
408
|
-
"--write-interval", "1",
|
409
|
-
"--output-format", "csv",
|
410
|
-
"--write", str(self.output_dir / temp_prefix),
|
411
|
-
]
|
412
|
-
|
413
|
-
# Adiciona parâmetros específicos do modo de varredura
|
414
|
-
if self.scan_mode == ScanMode.FAST:
|
415
|
-
cmd.extend(["--band", "bg"]) # Apenas 2.4GHz
|
416
|
-
elif self.scan_mode == ScanMode.DEEP:
|
417
|
-
cmd.extend(["--band", "abg"]) # 2.4GHz e 5GHz
|
418
|
-
|
419
|
-
# Adiciona a interface no final
|
420
|
-
cmd.append(self.interface)
|
421
|
-
|
422
|
-
self.console.print(f"[bold blue]\n🔍 Iniciando varredura WiFi no modo {self.scan_mode.name}...\n")
|
423
|
-
|
424
|
-
try:
|
425
|
-
# Inicia o airodump-ng em segundo plano
|
426
|
-
process = await asyncio.create_subprocess_exec(
|
427
|
-
*cmd,
|
428
|
-
stdout=asyncio.subprocess.PIPE,
|
429
|
-
stderr=asyncio.subprocess.PIPE
|
430
|
-
)
|
431
|
-
|
432
|
-
# Inicia a barra de progresso
|
433
|
-
with self.progress:
|
434
|
-
task = self.progress.add_task(
|
435
|
-
"[cyan]Escaneando redes...",
|
436
|
-
total=self.scan_time
|
437
|
-
)
|
438
|
-
|
439
|
-
# Aguarda o arquivo de saída ser criado
|
440
|
-
start_time = time.time()
|
441
|
-
while not csv_file.exists() and (time.time() - start_time) < 5:
|
442
|
-
await asyncio.sleep(0.5)
|
443
|
-
|
444
|
-
# Monitora o progresso
|
445
|
-
while not self.should_stop and (time.time() - start_time) < self.scan_time:
|
446
|
-
await asyncio.sleep(0.5)
|
447
|
-
self.progress.update(
|
448
|
-
task,
|
449
|
-
advance=0.5,
|
450
|
-
description=f"[cyan]Escaneando redes (restam {max(0, int(self.scan_time - (time.time() - start_time)))}s)..."
|
451
|
-
)
|
452
|
-
|
453
|
-
# Atualiza a lista de redes periodicamente
|
454
|
-
if int(time.time() - start_time) % 2 == 0:
|
455
|
-
await self._update_networks_from_file(csv_file)
|
456
|
-
|
457
|
-
# Finaliza o processo
|
458
|
-
process.terminate()
|
459
|
-
await process.wait()
|
460
|
-
|
461
|
-
# Atualiza a lista de redes uma última vez
|
462
|
-
if csv_file.exists():
|
463
|
-
await self._update_networks_from_file(csv_file)
|
464
|
-
|
465
|
-
return list(self.networks.values())
|
466
|
-
|
467
|
-
except Exception as e:
|
468
|
-
logger.error(f"Erro durante a varredura: {e}", exc_info=self.verbose)
|
469
|
-
self.console.print(f"[red]Erro durante a varredura: {e}[/]")
|
470
|
-
return []
|
471
|
-
|
472
|
-
finally:
|
473
|
-
self.running = False
|
474
|
-
self.scan_end_time = datetime.utcnow()
|
475
|
-
await self._cleanup()
|
476
|
-
|
477
|
-
async def _update_networks_from_file(self, csv_file: Path) -> None:
|
478
|
-
"""Atualiza a lista de redes a partir do arquivo CSV do airodump-ng.
|
479
|
-
|
480
|
-
Args:
|
481
|
-
csv_file: Caminho para o arquivo CSV
|
482
|
-
"""
|
483
|
-
try:
|
484
|
-
if not csv_file.exists():
|
485
|
-
return
|
486
|
-
|
487
|
-
with open(csv_file, 'r', errors='ignore') as f:
|
488
|
-
content = f.read()
|
489
|
-
|
490
|
-
# Divide o conteúdo em seções de redes e clientes
|
491
|
-
sections = re.split(r'\n\n', content.strip())
|
492
|
-
|
493
|
-
if not sections:
|
494
|
-
return
|
495
|
-
|
496
|
-
# Processa a seção de redes
|
497
|
-
network_lines = sections[0].split('\n')[1:] # Ignora o cabeçalho
|
498
|
-
for line in network_lines:
|
499
|
-
if not line.strip() or line.startswith('Station'):
|
500
|
-
continue
|
501
|
-
|
502
|
-
network = self._parse_airodump_line(line)
|
503
|
-
if network:
|
504
|
-
self._update_network(network)
|
505
|
-
|
506
|
-
# Processa a seção de clientes, se existir
|
507
|
-
if len(sections) > 1:
|
508
|
-
client_lines = sections[1].split('\n')[1:] # Ignora o cabeçalho
|
509
|
-
for line in client_lines:
|
510
|
-
if not line.strip():
|
511
|
-
continue
|
512
|
-
|
513
|
-
self._process_client_line(line)
|
514
|
-
|
515
|
-
except Exception as e:
|
516
|
-
logger.error(f"Erro ao processar arquivo de saída: {e}", exc_info=self.verbose)
|
517
|
-
|
518
|
-
def _update_network(self, network: WiFiNetwork) -> None:
|
519
|
-
"""Atualiza ou adiciona uma rede à lista de redes conhecidas.
|
520
|
-
|
521
|
-
Args:
|
522
|
-
network: Rede a ser atualizada/adicionada
|
523
|
-
"""
|
524
|
-
if network.bssid in self.networks:
|
525
|
-
# Atualiza a rede existente
|
526
|
-
existing = self.networks[network.bssid]
|
527
|
-
existing.signal = network.signal
|
528
|
-
existing.last_seen = datetime.utcnow()
|
529
|
-
|
530
|
-
# Atualiza outros campos, se necessário
|
531
|
-
if network.ssid and network.ssid != "<hidden>":
|
532
|
-
existing.ssid = network.ssid
|
533
|
-
|
534
|
-
if network.channel:
|
535
|
-
existing.channel = network.channel
|
536
|
-
existing.frequency = self._channel_to_frequency(network.channel)
|
537
|
-
|
538
|
-
# Atualiza a segurança, se disponível
|
539
|
-
if network.security != WiFiSecurityType.UNKNOWN:
|
540
|
-
existing.security = network.security
|
541
|
-
existing.encryption = network.encryption
|
542
|
-
existing.cipher = network.cipher
|
543
|
-
existing.authentication = network.authentication
|
544
|
-
|
545
|
-
else:
|
546
|
-
# Adiciona uma nova rede
|
547
|
-
self.networks[network.bssid] = network
|
548
|
-
|
549
|
-
def _process_client_line(self, line: str) -> None:
|
550
|
-
"""Processa uma linha de cliente do airodump-ng.
|
551
|
-
|
552
|
-
Args:
|
553
|
-
line: Linha de saída do airodump-ng contendo informações do cliente
|
554
|
-
"""
|
555
|
-
parts = [p.strip() for p in line.split(',') if p.strip()]
|
556
|
-
if len(parts) < 6: # Não há dados suficientes para um cliente
|
557
|
-
return
|
558
|
-
|
559
|
-
try:
|
560
|
-
client_mac = parts[0].upper()
|
561
|
-
bssid = parts[5].upper()
|
562
|
-
|
563
|
-
# Verifica se a rede do cliente está na nossa lista
|
564
|
-
if bssid not in self.networks:
|
565
|
-
return
|
566
|
-
|
567
|
-
# Cria ou atualiza o cliente
|
568
|
-
client = WiFiClient(
|
569
|
-
mac=client_mac,
|
570
|
-
bssid=bssid,
|
571
|
-
signal=int(parts[3]) if parts[3] else 0,
|
572
|
-
packets=int(parts[2]) if parts[2] else 0,
|
573
|
-
is_associated=parts[4].strip() == '0', # 0 = associado, 1 = não associado
|
574
|
-
)
|
575
|
-
|
576
|
-
# Atualiza a lista de clientes da rede
|
577
|
-
self.networks[bssid].add_client(client)
|
578
|
-
|
579
|
-
except (IndexError, ValueError) as e:
|
580
|
-
logger.debug(f"Não foi possível analisar a linha do cliente: {line}. Erro: {e}")
|
581
|
-
|
582
|
-
async def _cleanup(self):
|
583
|
-
"""Limpa arquivos temporários e restaura o estado original."""
|
584
|
-
try:
|
585
|
-
# Encerra processos em segundo plano
|
586
|
-
await run_command_async(["pkill", "-f", "airodump-ng"])
|
587
|
-
|
588
|
-
# Limpa arquivos temporários
|
589
|
-
temp_files = list(self.output_dir.glob("wifippler_scan_*"))
|
590
|
-
for file in temp_files:
|
591
|
-
try:
|
592
|
-
if file.is_file():
|
593
|
-
file.unlink()
|
594
|
-
elif file.is_dir():
|
595
|
-
shutil.rmtree(file)
|
596
|
-
except Exception as e:
|
597
|
-
logger.debug(f"Falha ao remover arquivo temporário {file}: {e}")
|
598
|
-
|
599
|
-
# Restaura a interface de rede
|
600
|
-
self._restore_interface()
|
601
|
-
|
602
|
-
except Exception as e:
|
603
|
-
logger.error(f"Erro durante a limpeza: {e}", exc_info=self.verbose)
|
604
|
-
|
605
|
-
def display_networks(self, networks: List[WiFiNetwork] = None, sort_by: str = 'signal') -> None:
|
606
|
-
"""Exibe as redes descobertas em uma tabela formatada.
|
607
|
-
|
608
|
-
Args:
|
609
|
-
networks: Lista de redes a serem exibidas. Se None, usa as redes escaneadas.
|
610
|
-
sort_by: Campo para ordenação (signal, channel, ssid, bssid)
|
611
|
-
"""
|
612
|
-
if networks is None:
|
613
|
-
networks = list(self.networks.values())
|
614
|
-
|
615
|
-
if not networks:
|
616
|
-
self.console.print("[yellow]Nenhuma rede encontrada.[/yellow]")
|
617
|
-
return
|
618
|
-
|
619
|
-
# Ordena as redes
|
620
|
-
reverse_sort = sort_by == 'signal' # Ordem decrescente para sinal
|
621
|
-
networks_sorted = sorted(
|
622
|
-
networks,
|
623
|
-
key=lambda x: (
|
624
|
-
getattr(x, sort_by, 0) if hasattr(x, sort_by) else 0,
|
625
|
-
x.ssid or ""
|
626
|
-
),
|
627
|
-
reverse=reverse_sort
|
628
|
-
)
|
629
|
-
|
630
|
-
# Cria a tabela
|
631
|
-
table = Table(
|
632
|
-
title=f"📶 Redes WiFi Encontradas ({len(networks_sorted)})",
|
633
|
-
show_header=True,
|
634
|
-
header_style="bold magenta",
|
635
|
-
expand=True
|
636
|
-
)
|
637
|
-
|
638
|
-
# Adiciona colunas
|
639
|
-
table.add_column("#", style="dim", width=4)
|
640
|
-
table.add_column("SSID", min_width=20)
|
641
|
-
table.add_column("BSSID", width=18)
|
642
|
-
table.add_column("Canal", width=8, justify="center")
|
643
|
-
table.add_column("Sinal", width=12, justify="right")
|
644
|
-
table.add_column("Segurança", width=15)
|
645
|
-
table.add_column("Clientes", width=10, justify="center")
|
646
|
-
table.add_column("WPS", width=6, justify="center")
|
647
|
-
|
648
|
-
# Adiciona linhas
|
649
|
-
for i, network in enumerate(networks_sorted, 1):
|
650
|
-
# Formata o SSID (mostra "<hidden>" se estiver oculto)
|
651
|
-
ssid = network.ssid if network.ssid and network.ssid != "<hidden>" else "[dim]<oculto>[/]"
|
652
|
-
|
653
|
-
# Formata o sinal com cores
|
654
|
-
if network.signal >= -50:
|
655
|
-
signal_str = f"[green]{network.signal} dBm[/]"
|
656
|
-
elif network.signal >= -70:
|
657
|
-
signal_str = f"[yellow]{network.signal} dBm[/]"
|
658
|
-
else:
|
659
|
-
signal_str = f"[red]{network.signal} dBm[/]"
|
660
|
-
|
661
|
-
# Formata a segurança
|
662
|
-
security = network.security.value if network.security else network.encryption
|
663
|
-
|
664
|
-
# Ícone WPS
|
665
|
-
wps_icon = "✅" if network.wps else "❌"
|
666
|
-
if network.wps_locked:
|
667
|
-
wps_icon = "🔒"
|
668
|
-
|
669
|
-
# Adiciona a linha à tabela
|
670
|
-
table.add_row(
|
671
|
-
str(i),
|
672
|
-
ssid,
|
673
|
-
network.bssid,
|
674
|
-
str(network.channel),
|
675
|
-
signal_str,
|
676
|
-
security,
|
677
|
-
str(len(network.clients)),
|
678
|
-
wps_icon
|
679
|
-
)
|
680
|
-
|
681
|
-
# Exibe a tabela
|
682
|
-
self.console.print()
|
683
|
-
self.console.print(table)
|
684
|
-
|
685
|
-
# Adiciona um resumo
|
686
|
-
self.console.print(f"[bold]Total de redes:[/] {len(networks_sorted)}")
|
687
|
-
|
688
|
-
# Contagem por tipo de segurança
|
689
|
-
security_counts = {}
|
690
|
-
for net in networks_sorted:
|
691
|
-
sec = net.security.value if net.security else "Desconhecido"
|
692
|
-
security_counts[sec] = security_counts.get(sec, 0) + 1
|
693
|
-
|
694
|
-
if security_counts:
|
695
|
-
self.console.print("\n[bold]Distribuição de segurança:[/]")
|
696
|
-
for sec, count in sorted(security_counts.items()):
|
697
|
-
self.console.print(f" • {sec}: {count}")
|
698
|
-
|
699
|
-
# Tempo de escaneamento
|
700
|
-
if self.scan_start_time and self.scan_end_time:
|
701
|
-
duration = (self.scan_end_time - self.scan_start_time).total_seconds()
|
702
|
-
self.console.print(f"\n[dim]Escaneamento concluído em {duration:.1f} segundos.[/]")
|
703
|
-
|
704
|
-
def get_network_by_bssid(self, bssid: str) -> Optional[WiFiNetwork]:
|
705
|
-
"""Obtém uma rede pelo seu BSSID.
|
706
|
-
|
707
|
-
Args:
|
708
|
-
bssid: Endereço BSSID da rede
|
709
|
-
|
710
|
-
Returns:
|
711
|
-
Optional[WiFiNetwork]: A rede encontrada ou None
|
712
|
-
"""
|
713
|
-
return self.networks.get(bssid.upper())
|
714
|
-
|
715
|
-
def get_networks_by_ssid(self, ssid: str) -> List[WiFiNetwork]:
|
716
|
-
"""Obtém todas as redes com um determinado SSID.
|
717
|
-
|
718
|
-
Args:
|
719
|
-
ssid: Nome da rede (SSID) a ser procurado
|
720
|
-
|
721
|
-
Returns:
|
722
|
-
List[WiFiNetwork]: Lista de redes com o SSID especificado
|
723
|
-
"""
|
724
|
-
return [net for net in self.networks.values()
|
725
|
-
if net.ssid and net.ssid.lower() == ssid.lower()]
|
726
|
-
|
727
|
-
def get_networks_by_security(self, security_type: WiFiSecurityType) -> List[WiFiNetwork]:
|
728
|
-
"""Obtém todas as redes com um determinado tipo de segurança.
|
729
|
-
|
730
|
-
Args:
|
731
|
-
security_type: Tipo de segurança a ser filtrado
|
732
|
-
|
733
|
-
Returns:
|
734
|
-
List[WiFiNetwork]: Lista de redes com o tipo de segurança especificado
|
735
|
-
"""
|
736
|
-
return [net for net in self.networks.values()
|
737
|
-
if net.security == security_type]
|
738
|
-
|
739
|
-
def get_networks_with_clients(self) -> List[WiFiNetwork]:
|
740
|
-
"""Obtém todas as redes que possuem clientes conectados.
|
741
|
-
|
742
|
-
Returns:
|
743
|
-
List[WiFiNetwork]: Lista de redes com clientes
|
744
|
-
"""
|
745
|
-
return [net for net in self.networks.values() if net.clients]
|
746
|
-
|
747
|
-
def get_wps_networks(self) -> List[WiFiNetwork]:
|
748
|
-
"""Obtém todas as redes com WPS ativado.
|
749
|
-
|
750
|
-
Returns:
|
751
|
-
List[WiFiNetwork]: Lista de redes com WPS ativado
|
752
|
-
"""
|
753
|
-
return [net for net in self.networks.values() if net.wps]
|
754
|
-
|
755
|
-
def to_json(self, file_path: str = None) -> Optional[str]:
|
756
|
-
"""Converte as redes descobertas para JSON.
|
757
|
-
|
758
|
-
Args:
|
759
|
-
file_path: Caminho para salvar o arquivo JSON. Se None, retorna a string JSON.
|
760
|
-
|
761
|
-
Returns:
|
762
|
-
Optional[str]: String JSON se file_path for None, caso contrário None
|
763
|
-
"""
|
764
|
-
data = {
|
765
|
-
'scan_start': self.scan_start_time.isoformat() if self.scan_start_time else None,
|
766
|
-
'scan_end': self.scan_end_time.isoformat() if self.scan_end_time else None,
|
767
|
-
'interface': self.interface,
|
768
|
-
'networks': [net.to_dict() for net in self.networks.values()]
|
769
|
-
}
|
770
|
-
|
771
|
-
json_str = json.dumps(data, indent=2, default=str)
|
772
|
-
|
773
|
-
if file_path:
|
774
|
-
try:
|
775
|
-
with open(file_path, 'w') as f:
|
776
|
-
f.write(json_str)
|
777
|
-
return None
|
778
|
-
except Exception as e:
|
779
|
-
logger.error(f"Falha ao salvar arquivo JSON: {e}")
|
780
|
-
return None
|
781
|
-
|
782
|
-
return json_str
|
783
|
-
|
784
|
-
async def run() -> None:
|
785
|
-
"""Função principal para executar o scanner a partir da linha de comando."""
|
786
|
-
import argparse
|
787
|
-
|
788
|
-
# Configura o parser de argumentos
|
789
|
-
parser = argparse.ArgumentParser(description='WiFiPPLER - Ferramenta avançada de análise de redes WiFi')
|
790
|
-
parser.add_argument('-i', '--interface', help='Interface de rede para escaneamento')
|
791
|
-
parser.add_argument('-t', '--time', type=int, default=10,
|
792
|
-
help='Tempo de escaneamento em segundos (padrão: 10)')
|
793
|
-
parser.add_argument('-m', '--mode', choices=['active', 'passive', 'fast', 'deep'],
|
794
|
-
default='active', help='Modo de varredura (padrão: active)')
|
795
|
-
parser.add_argument('-o', '--output', help='Arquivo de saída para salvar os resultados em JSON')
|
796
|
-
parser.add_argument('--random-mac', action='store_true',
|
797
|
-
help='Usar endereço MAC aleatório')
|
798
|
-
parser.add_argument('-v', '--verbose', action='store_true',
|
799
|
-
help='Modo verboso (mais detalhes de depuração)')
|
800
|
-
|
801
|
-
# Parse dos argumentos
|
802
|
-
args = parser.parse_args()
|
803
|
-
|
804
|
-
# Mapeia o modo de varredura
|
805
|
-
scan_mode_map = {
|
806
|
-
'active': ScanMode.ACTIVE,
|
807
|
-
'passive': ScanMode.PASSIVE,
|
808
|
-
'fast': ScanMode.FAST,
|
809
|
-
'deep': ScanMode.DEEP
|
810
|
-
}
|
811
|
-
|
812
|
-
try:
|
813
|
-
# Cria e configura o scanner
|
814
|
-
scanner = WiFiScanner(
|
815
|
-
interface=args.interface,
|
816
|
-
scan_time=args.time,
|
817
|
-
scan_mode=scan_mode_map[args.mode],
|
818
|
-
random_mac=args.random_mac,
|
819
|
-
verbose=args.verbose
|
820
|
-
)
|
821
|
-
|
822
|
-
# Executa a varredura
|
823
|
-
console = Console()
|
824
|
-
console.print("[bold green]🚀 Iniciando WiFiPPLER - Ferramenta de Análise WiFi[/]\n")
|
825
|
-
|
826
|
-
# Mostra informações iniciais
|
827
|
-
console.print(f"[bold]Interface:[/] {scanner.interface}")
|
828
|
-
console.print(f"[bold]Modo de varredura:[/] {args.mode.capitalize()}")
|
829
|
-
console.print(f"[bold]Tempo de escaneamento:[/] {args.time} segundos")
|
830
|
-
console.print(f"[bold]Endereço MAC aleatório:[/] {'Sim' if args.random_mac else 'Não'}\n")
|
831
|
-
|
832
|
-
# Realiza a varredura
|
833
|
-
networks = await scanner.scan_networks()
|
834
|
-
|
835
|
-
# Exibe os resultados
|
836
|
-
scanner.display_networks(networks)
|
837
|
-
|
838
|
-
# Salva os resultados em um arquivo, se solicitado
|
839
|
-
if args.output:
|
840
|
-
result = scanner.to_json(args.output)
|
841
|
-
if result is None:
|
842
|
-
console.print(f"[green]✅ Resultados salvos em {args.output}[/]")
|
843
|
-
else:
|
844
|
-
console.print(f"[yellow]⚠️ Resultados não foram salvos em {args.output}[/]")
|
845
|
-
|
846
|
-
console.print("\n[bold green]✅ Análise concluída com sucesso![/]")
|
847
|
-
|
848
|
-
except KeyboardInterrupt:
|
849
|
-
console.print("\n[yellow]⚠️ Escaneamento interrompido pelo usuário.[/]")
|
850
|
-
except Exception as e:
|
851
|
-
console.print(f"[red]❌ Erro durante a execução: {e}[/]")
|
852
|
-
if args.verbose:
|
853
|
-
import traceback
|
854
|
-
traceback.print_exc()
|
855
|
-
finally:
|
856
|
-
# Garante que a interface seja restaurada
|
857
|
-
if 'scanner' in locals():
|
858
|
-
await scanner._cleanup()
|
859
|
-
|
860
|
-
async def main():
|
861
|
-
"""Função de entrada para testes rápidos."""
|
862
|
-
try:
|
863
|
-
console = Console()
|
864
|
-
console.print("[bold blue]🔍 Iniciando teste do WiFiPPLER...[/]\n")
|
865
|
-
|
866
|
-
# Cria o scanner com configurações padrão
|
867
|
-
scanner = WiFiScanner(scan_time=15, verbose=True)
|
868
|
-
|
869
|
-
# Executa a varredura
|
870
|
-
console.print("[cyan]Escaneando redes próximas... (pressione Ctrl+C para interromper)[/]")
|
871
|
-
networks = await scanner.scan_networks()
|
872
|
-
|
873
|
-
# Exibe os resultados
|
874
|
-
scanner.display_networks(networks)
|
875
|
-
|
876
|
-
# Mostra um resumo
|
877
|
-
console.print("\n[bold]Resumo:[/]")
|
878
|
-
console.print(f"Total de redes encontradas: {len(networks)}")
|
879
|
-
wps_count = sum(1 for net in networks if net.wps)
|
880
|
-
console.print(f"Redes com WPS ativado: {wps_count}")
|
881
|
-
|
882
|
-
# Se houver redes com WPS, destaca-as
|
883
|
-
wps_nets = [net for net in networks if net.wps]
|
884
|
-
if wps_nets:
|
885
|
-
console.print("\n[bold yellow]⚠️ Redes com WPS ativado:[/]")
|
886
|
-
for net in wps_nets:
|
887
|
-
locked = "(travado) " if net.wps_locked else ""
|
888
|
-
console.print(f"- {net.ssid or '<oculto>'} {locked}({net.bssid}) - Canal {net.channel}")
|
889
|
-
|
890
|
-
console.print("\n[green]✅ Teste concluído com sucesso![/]")
|
891
|
-
|
892
|
-
except KeyboardInterrupt:
|
893
|
-
console.print("\n[yellow]⚠️ Teste interrompido pelo usuário.[/]")
|
894
|
-
except Exception as e:
|
895
|
-
console.print(f"[red]❌ Erro durante o teste: {e}[/]")
|
896
|
-
finally:
|
897
|
-
if 'scanner' in locals():
|
898
|
-
await scanner._cleanup()
|
899
|
-
|
900
|
-
if __name__ == "__main__":
|
901
|
-
asyncio.run(run())
|