chore(i18n): finish translating code comments and scripts to English
CI / Format and clippy (push) Successful in 1m13s
CI / Dependency audit (push) Successful in 13s
CI / Tests (Linux) (push) Successful in 1m35s
Release / Create release (push) Successful in 7s
CI / Check desktop app (Linux) (push) Successful in 3m19s
Release / CLI (linux-aarch64) (push) Successful in 1m3s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-aarch64) (push) Successful in 7m16s
Release / Desktop app (linux-x86_64) (push) Successful in 10m17s
Release / Desktop app (windows) (push) Successful in 4m54s
CI / Format and clippy (push) Successful in 1m13s
CI / Dependency audit (push) Successful in 13s
CI / Tests (Linux) (push) Successful in 1m35s
Release / Create release (push) Successful in 7s
CI / Check desktop app (Linux) (push) Successful in 3m19s
Release / CLI (linux-aarch64) (push) Successful in 1m3s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-aarch64) (push) Successful in 7m16s
Release / Desktop app (linux-x86_64) (push) Successful in 10m17s
Release / Desktop app (windows) (push) Successful in 4m54s
Completes the English-only standardization for code, comments, scripts and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en, untouched). Covers the remaining crates/, apps/gui/ui/ and apps/gui/src-tauri/ comments, .gitea/ workflow and template comments, .githooks/ hooks, and the scripts/ source comments for the files already renamed in a prior commit (check.sh, generate-logo.py, install-hooks.sh). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
1 parent
bf059fd0bc
commit
55e19a8e5b
59 files changed
+935
-879
No files matched your search
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sabia-core"
|
||||
description = "Sabiá: núcleo de captura, análise e auditoria de redes Wi-Fi"
|
||||
description = "Sabiá: Wi-Fi network capture, analysis, and auditing core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -25,5 +25,5 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"]
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# captura crua (AF_PACKET) só existe no Linux; outras plataformas compilam
|
||||
# a crate mas os módulos de captura ficam indisponíveis em tempo de execução.
|
||||
# Raw capture (AF_PACKET) is Linux-only; other platforms compile the crate
|
||||
# while live capture modules remain unavailable at runtime.
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Análise de canais, detecção de anomalias (WIDS) e leitura de WPA3.
|
||||
//! Channel utilization analysis, wireless anomaly detection (WIDS), and WPA3 indicators.
|
||||
//!
|
||||
//! Everything here is derived passively from the captured scan table without injecting frames.
|
||||
//! Both the live UI and the standalone audit report consume this model.
|
||||
//!
|
||||
//! Tudo aqui é derivado da tabela já capturada — nada injeta quadros. A
|
||||
//! interface usa o resultado para desenhar o gráfico de canais e os alertas;
|
||||
//! o relatório usa o mesmo modelo para não ter duas verdades.
|
||||
|
||||
use crate::radiotap::{self, Band};
|
||||
use crate::scan::AccessPoint;
|
||||
@@ -280,8 +280,8 @@ pub fn analyze(aps: &[AccessPoint], deauth_60s: u32, now: u64) -> Analysis {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// SSID anunciado com seguranças diferentes é o sinal clássico de evil
|
||||
// twin — legítimo costuma manter o mesmo conjunto em todos os rádios.
|
||||
// Same SSID advertised with mixed security suites (e.g. WPA2 alongside Open)
|
||||
// is a classic Evil Twin indicator.
|
||||
let mut by_ssid: HashMap<String, Vec<&AccessPoint>> = HashMap::new();
|
||||
for ap in aps {
|
||||
if let Some(ssid) = ap.ssid.as_deref().filter(|s| !s.is_empty()) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Trilha de auditoria encadeada por hash de toda ação sensível (ligar
|
||||
//! modo monitor, iniciar captura, trocar canal, etc.) — mesmo princípio do
|
||||
//! módulo `audit` do Guará: cada evento guarda o hash do anterior, então
|
||||
//! qualquer edição retroativa do log quebra a cadeia e é detectável.
|
||||
//! Hash-chained audit trail of every sensitive operation (enabling
|
||||
//! monitor mode, starting capture, changing channels, active attacks):
|
||||
//! each entry commits to the SHA-256 of the previous entry so any
|
||||
//! retroactive tampering breaks the chain.
|
||||
//!
|
||||
//! Provides verifiable engagement evidence of what was executed, when,
|
||||
//! and against which target/interface.
|
||||
//!
|
||||
//!
|
||||
//! Isso existe para que o uso do Sabiá em um engajamento de pentest tenha
|
||||
//! evidência de o quê foi feito, quando e contra qual rede/interface —
|
||||
//! não para decidir sozinho se uma ação é autorizada (isso é sempre do
|
||||
//! operador).
|
||||
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -85,7 +85,7 @@ impl AuditLog {
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
/// Lê as entradas da trilha, opcionalmente só as `limit` mais recentes.
|
||||
/// Reads entries from the audit log, optionally returning only the last `limit` entries.
|
||||
pub fn read_entries(path: impl AsRef<Path>, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
|
||||
let file = OpenOptions::new().read(true).open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
@@ -105,8 +105,8 @@ impl AuditLog {
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Verifica a integridade de toda a cadeia; retorna o índice da
|
||||
/// primeira entrada inválida, se houver.
|
||||
/// Verifies the integrity of the entire hash chain; returns the index of
|
||||
/// the first invalid entry if tampered.
|
||||
pub fn verify(path: impl AsRef<Path>) -> Result<Option<usize>> {
|
||||
let file = OpenOptions::new().read(true).open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Captura crua de quadros 802.11 via `AF_PACKET`, sem depender de libpcap.
|
||||
//! Raw 802.11 frame capture via Linux `AF_PACKET` sockets without requiring `libpcap`.
|
||||
//!
|
||||
//! The interface must already be in monitor mode (see [`crate::netlink`]).
|
||||
//! Uses the same kernel packet socket interface that `libpcap` uses on Linux
|
||||
//! without adding an external system library dependency.
|
||||
//!
|
||||
//! A interface já precisa estar em modo monitor (ver [`crate::netlink`]).
|
||||
//! Isso só funciona em Linux — é exatamente o que o libpcap faz por baixo
|
||||
//! quando usado em modo monitor, então reimplementar aqui é pouco código e
|
||||
//! remove uma dependência de sistema.
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
@@ -21,8 +21,8 @@ pub struct RawCapture {
|
||||
}
|
||||
|
||||
impl RawCapture {
|
||||
/// Abre um socket raw ligado à interface `iface` (já em modo monitor).
|
||||
/// Exige `CAP_NET_RAW` (tipicamente root).
|
||||
/// Opens a raw `AF_PACKET` socket bound to `iface` (must already be in monitor mode).
|
||||
/// Requires `CAP_NET_RAW` (typically root).
|
||||
pub fn open(iface: &str) -> Result<Self> {
|
||||
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, ETH_P_ALL as i32) };
|
||||
if fd < 0 {
|
||||
@@ -57,8 +57,8 @@ impl RawCapture {
|
||||
Ok(Self { fd })
|
||||
}
|
||||
|
||||
/// Define um timeout de recepção, para o loop de captura poder checar
|
||||
/// periodicamente um sinal de parada sem travar para sempre em `recv`.
|
||||
/// Sets `SO_RCVTIMEO` so the capture loop can periodically check its stop flag
|
||||
/// without blocking indefinitely in `recv`.
|
||||
pub fn set_read_timeout(&self, timeout: Duration) -> Result<()> {
|
||||
let tv = libc::timeval {
|
||||
tv_sec: timeout.as_secs() as libc::time_t,
|
||||
@@ -79,7 +79,7 @@ impl RawCapture {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lê um quadro. Retorna `Ok(None)` em timeout (ver `set_read_timeout`).
|
||||
/// Reads a single frame. Returns `Ok(None)` on receive timeout (see `set_read_timeout`).
|
||||
pub fn recv(&self, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let n = unsafe { libc::recv(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
|
||||
if n < 0 {
|
||||
@@ -114,8 +114,8 @@ fn if_index(name: &str) -> Result<i32> {
|
||||
Ok(idx as i32)
|
||||
}
|
||||
|
||||
/// Garante, de forma best-effort, que o processo tem privilégio para abrir
|
||||
/// sockets raw — útil para dar um erro claro antes de tentar o resto.
|
||||
/// Best-effort check that the process has privileges to open raw packet sockets.
|
||||
///
|
||||
pub fn check_capabilities() -> bool {
|
||||
let euid = unsafe { libc::geteuid() };
|
||||
euid == 0
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
//! Decriptação CCMP (WPA2-AES) de quadros de dados, usando a TK já derivada
|
||||
//! de um handshake quebrado.
|
||||
//! WPA2 CCMP (AES-CTR) data frame decryption using the Temporal Key (TK) derived
|
||||
//! from a cracked 4-way handshake.
|
||||
//!
|
||||
//! Used passively to inspect cleartext upper-layer protocols (DHCP, mDNS, ARP, HTTP)
|
||||
//! of a station whose own 4-way handshake was captured and cracked, allowing
|
||||
//! hostname/device identification. Because each association derives an ephemeral
|
||||
//! PTK, the TK is scoped to a specific `(BSSID, station)` pair.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! Implements AES-CTR payload decryption (skipping 8-byte CBC-MAC MIC verification
|
||||
//! since we only extract passive identity hints from known-key traffic).
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! Isso não serve pra forjar nem reinjetar nada — só pra enxergar o conteúdo
|
||||
//! em claro (DHCP, mDNS) de um cliente cuja senha da rede já foi recuperada
|
||||
//! por dicionário, e assim extrair hostname/fabricante de verdade em vez de
|
||||
//! só o MAC. A chave é par (BSSID, estação): só decifra tráfego do cliente
|
||||
//! cujo próprio handshake foi capturado e quebrado, nunca de terceiros —
|
||||
//! cada associação tem sua PTK efêmera, então a senha da rede sozinha não
|
||||
//! abre o tráfego de quem a gente não viu entrar.
|
||||
//!
|
||||
//! Implementa só o modo CTR (a parte que decifra o conteúdo). O MIC
|
||||
//! (CBC-MAC) não é conferido: a intenção aqui é mostrar o payload de um
|
||||
//! tráfego que a gente já pode decifrar porque a senha é conhecida, não
|
||||
//! provar integridade de um quadro adulterado — e implementar o AAD/CBC-MAC
|
||||
//! certinho exigiria replicar o mascaramento do cabeçalho 802.11 inteiro
|
||||
//! sem ganho prático pro que isso serve aqui.
|
||||
|
||||
use aes::cipher::{BlockEncrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
|
||||
/// Decifra o corpo de um quadro de dados CCMP, a partir do cabeçalho CCMP
|
||||
/// (8 bytes) em diante — ou seja, `body` é `frame.body[frame.header_len..]`.
|
||||
/// `transmitter` é o endereço 2 (A2) do quadro — o rádio que o enviou, não o
|
||||
/// BSSID/estação lógicos. `qos_tid` é o TID do QoS Control (0 se não-QoS).
|
||||
/// Decrypts a CCMP-protected data frame payload starting at the 8-byte CCMP header
|
||||
/// (`frame.body[frame.header_len..]`).
|
||||
/// `transmitter` is Address 2 (`A2`), and `qos_tid` is the QoS TID (`0` for non-QoS).
|
||||
///
|
||||
///
|
||||
/// Returns the decrypted LLC/SNAP + payload (excluding the trailing 8-byte MIC).
|
||||
///
|
||||
/// Retorna o LLC/SNAP + payload em claro (o mesmo formato que um quadro não
|
||||
/// protegido teria), sem os 8 bytes de MIC cifrado no final.
|
||||
pub fn decrypt_data(
|
||||
tk: &[u8; 16],
|
||||
transmitter: &[u8; 6],
|
||||
@@ -40,8 +40,8 @@ pub fn decrypt_data(
|
||||
let pn0 = body[0];
|
||||
let pn1 = body[1];
|
||||
let key_id_octet = body[3];
|
||||
// ExtIV (bit 5) precisa estar ligado — senão não é um cabeçalho CCMP de
|
||||
// verdade (podia ser WEP de 4 bytes por coincidência de layout).
|
||||
// ExtIV (bit 5) must be set for a valid 8-byte CCMP IV header.
|
||||
//
|
||||
if key_id_octet & 0x20 == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -53,8 +53,8 @@ pub fn decrypt_data(
|
||||
let ciphertext_and_mic = &body[HEADER_LEN..];
|
||||
let ciphertext = &ciphertext_and_mic[..ciphertext_and_mic.len() - MIC_LEN];
|
||||
|
||||
// Nonce CCMP (13 bytes): flags/prioridade (1) || A2 (6) || PN, do byte
|
||||
// mais significativo (PN5) ao menos significativo (PN0).
|
||||
// 13-byte CCMP nonce: `flags/priority (1) || A2 (6) || PN5..PN0 (6)`.
|
||||
//
|
||||
let mut nonce = [0u8; 13];
|
||||
nonce[0] = qos_tid & 0x0f;
|
||||
nonce[1..7].copy_from_slice(transmitter);
|
||||
@@ -67,10 +67,10 @@ pub fn decrypt_data(
|
||||
|
||||
let cipher = Aes128::new(tk.into());
|
||||
let mut out = Vec::with_capacity(ciphertext.len());
|
||||
let mut counter: u16 = 1; // o bloco de contador 0 cifra só o MIC, não dado.
|
||||
let mut counter: u16 = 1; // counter block 0 encrypts the MIC; payload blocks start at counter 1.
|
||||
for chunk in ciphertext.chunks(16) {
|
||||
let mut block = [0u8; 16];
|
||||
block[0] = 0x01; // flag octet do CCMP counter mode (L=1, sem AAD aqui)
|
||||
block[0] = 0x01; // CCMP counter mode flags octet (`L = 1`)
|
||||
block[1..14].copy_from_slice(&nonce);
|
||||
block[14..16].copy_from_slice(&counter.to_be_bytes());
|
||||
let mut keystream = block.into();
|
||||
@@ -87,9 +87,9 @@ pub fn decrypt_data(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// AES-128 do FIPS-197 (vetor de referência oficial do NIST): confirma
|
||||
/// que a cifra de bloco usada embaixo está correta antes de confiar no
|
||||
/// contador CTR por cima dela.
|
||||
/// NIST FIPS-197 Appendix B AES-128 test vector.
|
||||
///
|
||||
///
|
||||
#[test]
|
||||
fn aes128_matches_fips197_vector() {
|
||||
let key: [u8; 16] = [
|
||||
@@ -110,19 +110,19 @@ mod tests {
|
||||
assert_eq!(block.as_slice(), &expected);
|
||||
}
|
||||
|
||||
/// Round-trip de auto-consistência: decifrar um texto que a própria
|
||||
/// função "cifrou" (mesmo algoritmo, usado nos dois sentidos — CTR é
|
||||
/// simétrico) tem que devolver o original. Isso pega bug de indexação
|
||||
/// no nonce/contador mesmo sem um vetor de teste oficial do CCMP à mão.
|
||||
/// CTR symmetry round-trip: encrypting and decrypting with the same CCMP
|
||||
/// keystream recovers the original LLC/SNAP payload.
|
||||
///
|
||||
///
|
||||
#[test]
|
||||
fn ctr_round_trip_recovers_plaintext() {
|
||||
let tk = [0x42u8; 16];
|
||||
let transmitter = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
||||
let plaintext = b"AA AA 03 00 00 00 08 00 payload de teste bem maior que um bloco de 16 bytes para cobrir mais de uma rodada do contador";
|
||||
|
||||
// Monta um "quadro" CCMP cifrando manualmente com a mesma lógica de
|
||||
// keystream (ela é simétrica: XOR com o keystream cifra e decifra).
|
||||
let header = [0x11u8, 0x22, 0x00, 0x20, 0x33, 0x44, 0x55, 0x66]; // ExtIV ligado
|
||||
// Build a synthetic CCMP payload using the symmetric CTR keystream.
|
||||
//
|
||||
let header = [0x11u8, 0x22, 0x00, 0x20, 0x33, 0x44, 0x55, 0x66]; // ExtIV bit set
|
||||
let mut nonce = [0u8; 13];
|
||||
nonce[0] = 3; // TID
|
||||
nonce[1..7].copy_from_slice(&transmitter);
|
||||
@@ -147,7 +147,7 @@ mod tests {
|
||||
}
|
||||
counter = counter.wrapping_add(1);
|
||||
}
|
||||
ciphertext.extend_from_slice(&[0u8; 8]); // MIC fictício, ignorado na decriptação
|
||||
ciphertext.extend_from_slice(&[0u8; 8]); // dummy 8-byte MIC (stripped during decryption)
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(&header);
|
||||
@@ -160,7 +160,7 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_body_without_extiv_bit() {
|
||||
let tk = [0u8; 16];
|
||||
let body = [0u8; 20]; // key_id_octet (índice 3) sem o bit ExtIV
|
||||
let body = [0u8; 20]; // key_id_octet (index 3) without the ExtIV bit set
|
||||
assert!(decrypt_data(&tk, &[0; 6], 0, &body).is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//! Quebra de WPA/WPA2-PSK por dicionário, em Rust puro.
|
||||
//! Pure-Rust WPA/WPA2-PSK dictionary and mask cracker.
|
||||
//!
|
||||
//! Duas provas são aceitas, como no aircrack-ng/hashcat:
|
||||
//! Supports both verification modes used by `aircrack-ng` and `hashcat`:
|
||||
//!
|
||||
//! * **EAPOL (4-way handshake):** deriva a PMK com PBKDF2-HMAC-SHA1
|
||||
//! (4096 iterações), expande a PTK com o PRF do 802.11i e recalcula o MIC da
|
||||
//! mensagem 2.
|
||||
//! * **PMKID:** recalcula `HMAC-SHA1(PMK, "PMK Name" || AP || STA)` direto da
|
||||
//! mensagem 1 — não precisa do handshake completo.
|
||||
//! * **EAPOL (4-way handshake):** derives the PMK via PBKDF2-HMAC-SHA1
|
||||
//! (4096 iterations), expands the PTK via IEEE 802.11i PRF, and verifies the
|
||||
//! message 2 MIC.
|
||||
//! * **PMKID:** computes `HMAC-SHA1(PMK, "PMK Name" || AP || STA)` directly from
|
||||
//! message 1 without requiring a full 4-way handshake.
|
||||
//!
|
||||
//! É CPU puro, sem GPU: suficiente para a v0.1.0 e para dicionários comuns,
|
||||
//! mas não substitui o hashcat em ataques de força bruta grandes.
|
||||
//! Runs multi-threaded on the CPU; for large GPU-accelerated attacks, export
|
||||
//! `.hc22000` via [`crate::hashcat`].
|
||||
|
||||
use crate::dot11::parse_mac;
|
||||
use crate::error::{Result, SabiaError};
|
||||
@@ -33,9 +33,9 @@ type HmacMd5 = Hmac<Md5>;
|
||||
|
||||
const WPA_ITERATIONS: u32 = 4096;
|
||||
|
||||
/// `.gz` do dicionário embutido — descomprimido só na primeira vez que é
|
||||
/// usado (ver [`default_wordlist`]). Cru são ~101MB; comprimido, ~39MB no
|
||||
/// binário em vez de ~101MB.
|
||||
/// Gzip-compressed built-in wordlist (`~39 MB` compressed -> `~101 MB` raw),
|
||||
/// decompressed lazily on first use (see [`default_wordlist`]).
|
||||
///
|
||||
const DEFAULT_WORDLIST_GZ: &[u8] = include_bytes!("../assets/wordlists/wpa-full.txt.gz");
|
||||
|
||||
pub const DEFAULT_WORDLIST_SOURCE: &str =
|
||||
@@ -49,17 +49,17 @@ static DEFAULT_WORDLIST_CACHE: LazyLock<String> = LazyLock::new(|| {
|
||||
out
|
||||
});
|
||||
|
||||
/// Lista embutida de senhas prováveis de Wi-Fi, usada quando o usuário não
|
||||
/// escolhe um dicionário próprio. Não depende de arquivo externo: é a lista
|
||||
/// mais completa que o Sabiá consegue embutir sem exigir download —
|
||||
/// 9.703.831 senhas (203.806 do Probable-Wordlists, ranqueadas por
|
||||
/// probabilidade, seguidas pelo rockyou.txt inteiro) filtradas pro
|
||||
/// comprimento válido de WPA (8–63) e pra ASCII puro (WPA-PSK exige um
|
||||
/// octeto válido; a imensa maioria dos roteadores só aceita ASCII mesmo, e
|
||||
/// isso evita o pouco de lixo binário que o rockyou.txt original carrega),
|
||||
/// sem duplicatas e na ordem de prioridade: as mais prováveis primeiro,
|
||||
/// então a quebra tende a achar a senha cedo, não só no fim da lista. Ver
|
||||
/// `assets/wordlists/LICENSE.wpa-full` pra atribuição completa.
|
||||
/// Built-in WPA wordlist (9,703,831 deduplicated 8–63 char ASCII passwords
|
||||
/// combining Probable-Wordlists and `rockyou.txt` ordered by probability).
|
||||
/// See `crates/sabia-core/assets/wordlists/LICENSE.wpa-full` for attribution.
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn default_wordlist() -> &'static str {
|
||||
&DEFAULT_WORDLIST_CACHE
|
||||
}
|
||||
@@ -142,7 +142,7 @@ impl CrackTarget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deriva a PMK (256 bits) de uma frase-senha WPA e do SSID.
|
||||
/// Derives the 256-bit WPA PMK from an ASCII passphrase and SSID (`PBKDF2-HMAC-SHA1`, 4096 rounds).
|
||||
pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] {
|
||||
let dk = pbkdf2_sha1(passphrase.as_bytes(), ssid.as_bytes(), WPA_ITERATIONS, 32);
|
||||
let mut pmk = [0u8; 32];
|
||||
@@ -150,10 +150,10 @@ pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] {
|
||||
pmk
|
||||
}
|
||||
|
||||
/// Aceita uma frase-senha (8–63 caracteres) ou uma PMK crua em hexadecimal
|
||||
/// (64 caracteres, como no hashcat/aircrack). `pub(crate)` porque
|
||||
/// `handshake.rs` reusa exatamente essa regra ao derivar a TK de uma senha
|
||||
/// já quebrada (o campo `password` aceita as duas formas, igual aqui).
|
||||
/// Resolves either an 8–63 character WPA passphrase or a raw 64-hex-character PMK.
|
||||
/// Shared with `handshake.rs` when deriving the CCMP Temporal Key (TK) from a known password.
|
||||
///
|
||||
///
|
||||
pub(crate) fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> {
|
||||
if candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return hex_to_32(candidate.as_bytes());
|
||||
@@ -170,10 +170,10 @@ pub(crate) fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32
|
||||
pub fn pbkdf2_sha1(password: &[u8], salt: &[u8], iterations: u32, out_len: usize) -> Vec<u8> {
|
||||
let iterations = iterations.max(1);
|
||||
let mut out = Vec::with_capacity(out_len);
|
||||
// O par ipad/opad depende só da senha — monta uma vez e clona a cada
|
||||
// rodada (clone é só copiar os dois blocos já prontos) em vez de montar
|
||||
// o HMAC do zero em cada uma das milhares de iterações. Em 4096
|
||||
// iterações por bloco isso é a maior parte do custo fora do SHA1 em si.
|
||||
// Precompute the HMAC inner/outer SHA-1 states once per passphrase and clone
|
||||
// across all 4096 PBKDF2 rounds to avoid re-hashing the key block each iteration.
|
||||
//
|
||||
//
|
||||
let base = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave");
|
||||
let mut block: u32 = 1;
|
||||
while out.len() < out_len {
|
||||
@@ -213,7 +213,7 @@ fn prf_sha1(key: &[u8], label: &[u8], data: &[u8], out_len: usize) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
/// PTK de 64 bytes: KCK (16) + KEK (16) + TK (16) + 16 de reserva.
|
||||
/// Derives the 64-byte PTK: `KCK (16) || KEK (16) || TK (16) || MIC keys (16)`.
|
||||
pub fn compute_ptk(
|
||||
pmk: &[u8; 32],
|
||||
bssid: &[u8; 6],
|
||||
@@ -314,14 +314,14 @@ fn valid_candidate(candidate: &str) -> bool {
|
||||
|| (candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
/// Tenta cada linha do dicionário contra o alvo, dividindo o trabalho entre
|
||||
/// as threads disponíveis (PBKDF2-HMAC-SHA1 de 4096 iterações é CPU puro e
|
||||
/// cada candidato é independente — ganho quase linear com o número de
|
||||
/// núcleos). `progress` recebe o número de candidatos já processados, ~8x
|
||||
/// por segundo enquanto roda. Com `apply_rules`, cada linha vira até
|
||||
/// [`rules::VARIANTS_PER_WORD`] candidatos (maiúscula, sufixos comuns, leet
|
||||
/// simples) em vez de só a palavra crua — multiplica o tempo de verdade,
|
||||
/// por isso é opt-in.
|
||||
/// Tests wordlist candidates against `target` across all available CPU threads.
|
||||
/// Invokes `progress` (~8x/s) with the number of tested candidates. When
|
||||
/// `apply_rules` is enabled, expands each base word via [`rules::expand`].
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
||||
target: &CrackTarget,
|
||||
mut wordlist: R,
|
||||
@@ -330,9 +330,9 @@ pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
||||
) -> CrackReport {
|
||||
let start = Instant::now();
|
||||
|
||||
// Carrega a lista inteira antes de começar: dá pra repartir em pedaços
|
||||
// contíguos por thread (melhor localidade de cache que round-robin) e o
|
||||
// próprio dicionário embutido cabe de sobra em memória.
|
||||
// Load candidates upfront and partition into contiguous per-thread slices.
|
||||
//
|
||||
//
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
@@ -384,8 +384,8 @@ pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
||||
});
|
||||
}
|
||||
|
||||
// Thread "repórter": roda dentro do mesmo scope só pra poder
|
||||
// emprestar `progress` (FnMut, chamado de um único lugar).
|
||||
// Progress reporter thread scoped to borrow `progress` (`FnMut`) safely.
|
||||
//
|
||||
scope.spawn(|| loop {
|
||||
std::thread::sleep(std::time::Duration::from_millis(120));
|
||||
let n = tried.load(Ordering::Relaxed);
|
||||
@@ -426,7 +426,7 @@ pub fn crack_handshake<F: FnMut(u64) + Send>(
|
||||
))
|
||||
}
|
||||
|
||||
/// Quebra usando a lista embutida.
|
||||
/// Runs a dictionary crack using the built-in wordlist.
|
||||
pub fn crack_handshake_default<F: FnMut(u64) + Send>(
|
||||
hs: &Handshake,
|
||||
apply_rules: bool,
|
||||
@@ -449,7 +449,7 @@ fn target_for(hs: &Handshake) -> Result<CrackTarget> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Lê um `.pcap`, monta os handshakes e tenta quebrar o primeiro alvo útil.
|
||||
/// Parses a `.pcap` file, assembles handshakes, and cracks the first crackable target.
|
||||
pub fn crack_pcap<F: FnMut(u64) + Send>(
|
||||
pcap: impl AsRef<Path>,
|
||||
wordlist: impl AsRef<Path>,
|
||||
@@ -474,11 +474,11 @@ pub fn crack_pcap<F: FnMut(u64) + Send>(
|
||||
crack_handshake(hs, wordlist, apply_rules, progress)
|
||||
}
|
||||
|
||||
/// Força bruta por máscara (estilo hashcat `-a 3`): gera e testa cada
|
||||
/// combinação possível, sem depender de dicionário. Mesma paralelização por
|
||||
/// threads do `crack_reader`, mas gerando candidatos por índice (base
|
||||
/// mista) em vez de carregar tudo em memória — é o que permite máscaras com
|
||||
/// milhões/bilhões de combinações sem estourar RAM.
|
||||
/// Mask brute-force attack (hashcat `-a 3` style): generates candidates on the fly
|
||||
/// by mixed-radix index across all CPU threads without materializing the keyspace in RAM.
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn crack_mask<F: FnMut(u64) + Send>(
|
||||
target: &CrackTarget,
|
||||
mask: &Mask,
|
||||
@@ -546,7 +546,7 @@ pub fn crack_mask<F: FnMut(u64) + Send>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Quebra por máscara a partir de um handshake já montado.
|
||||
/// Runs a mask brute-force attack against an assembled `Handshake`.
|
||||
pub fn crack_handshake_mask<F: FnMut(u64) + Send>(
|
||||
hs: &Handshake,
|
||||
mask: &Mask,
|
||||
@@ -562,8 +562,8 @@ mod tests {
|
||||
use crate::testutil::{handshake_frames, synthetic_handshake};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Mede só o tempo de descomprimir o dicionário embutido (gzip, ~39MB -> ~101MB),
|
||||
/// que acontece uma vez, na primeira chamada a `default_wordlist()`.
|
||||
/// Benchmarks one-time decompression of the built-in gzip wordlist (`~39 MB -> ~101 MB`).
|
||||
///
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_default_wordlist_decompress() {
|
||||
@@ -577,17 +577,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Não é teste de verdade — mede candidatos/segundo pra guiar otimização
|
||||
/// de performance. `cargo test --release -p sabia-core -- --ignored
|
||||
/// --nocapture bench_crack_throughput`.
|
||||
/// Throughput benchmark (`cargo test --release -p sabia-core -- --ignored --nocapture bench_crack_throughput`).
|
||||
///
|
||||
///
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_crack_throughput() {
|
||||
let (m1, m2) = handshake_frames("IEEE", "senha-que-nao-esta-na-lista", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
// Só uma fatia: a lista inteira (9,7 milhões) levaria minutos — o
|
||||
// objetivo aqui é medir candidatos/s, não o tempo total.
|
||||
// Benchmark a 20,000-password slice to measure candidates/sec quickly.
|
||||
//
|
||||
let sample: String = default_wordlist()
|
||||
.lines()
|
||||
.take(500_000)
|
||||
@@ -677,8 +677,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rules_find_a_mutated_variant_not_present_verbatim() {
|
||||
// A senha de verdade é "wifi12345" — não está na lista, mas é um
|
||||
// sufixo numérico de "wifi" que `rules::expand` gera.
|
||||
// True password `"wifi12345"` is derived from `"wifi"` via `rules::expand`.
|
||||
//
|
||||
let (m1, m2) = handshake_frames("IEEE", "wifi12345", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
@@ -692,10 +692,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mask_crack_finds_a_numeric_password() {
|
||||
// Máscara minúscula de propósito (só 10 combinações): cada candidato
|
||||
// é um PBKDF2 de 4096 iterações, pesado até em paralelo — um brute
|
||||
// force de verdade (ex.: ?d×8, 100 milhões) é coisa pra uso real,
|
||||
// não pra um teste unitário que roda em `cargo test` toda hora.
|
||||
// Small 10-candidate mask (`wifi123?d`) to keep unit tests fast.
|
||||
//
|
||||
//
|
||||
//
|
||||
let (m1, m2) = handshake_frames("IEEE", "wifiabc7", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Diagnóstico do ambiente: privilégios, ferramentas externas, interfaces,
|
||||
//! suporte a modo monitor e integridade da trilha de auditoria.
|
||||
//! Environment diagnostics: privileges, external helper tools, wireless interfaces,
|
||||
//! monitor mode support, and audit trail integrity.
|
||||
|
||||
use crate::audit::AuditLog;
|
||||
use crate::iface::{self, Interface};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Parser de quadros 802.11 — gerenciamento (beacon, probe, associação,
|
||||
//! desautenticação) e dados — com leitura dos *information elements* que
|
||||
//! interessam para auditoria: SSID, canal, segurança (RSN/WPA), WPS, PMF e
|
||||
//! a localização do payload EAPOL dos handshakes WPA/WPA2.
|
||||
//! IEEE 802.11 frame parser — management (beacon, probe, association,
|
||||
//! deauthentication) and data frames — extracting security-relevant Information
|
||||
//! Elements (SSID, channel, RSN/WPA, WPS, PMF) and locating EAPOL handshake
|
||||
//! payloads and cleartext client identity hints.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FrameKind {
|
||||
@@ -94,9 +94,9 @@ pub struct ManagementInfo {
|
||||
pub wps_serial: Option<String>,
|
||||
}
|
||||
|
||||
/// Pistas de identidade tiradas de payload em claro (DHCP, mDNS/DNS, SSDP,
|
||||
/// HTTP e ARP). Redes criptografadas não expõem nada disso; nesse caso os
|
||||
/// campos ficam vazios e a UI mostra só MAC, OUI e probes.
|
||||
/// Client identity hints extracted from cleartext upper-layer payloads (DHCP, mDNS/DNS,
|
||||
/// SSDP, HTTP, ARP). Empty on encrypted networks unless the station's CCMP
|
||||
/// session key has been derived from a cracked handshake.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PayloadInfo {
|
||||
pub ip: Option<String>,
|
||||
@@ -172,7 +172,7 @@ pub fn is_broadcast(mac: &[u8; 6]) -> bool {
|
||||
*mac == [0xff; 6]
|
||||
}
|
||||
|
||||
/// Quadro 802.11 já sem o cabeçalho radiotap.
|
||||
/// Parsed 802.11 frame (after stripping the leading Radiotap header).
|
||||
pub struct Frame<'a> {
|
||||
pub kind: FrameKind,
|
||||
pub fc: u16,
|
||||
@@ -327,7 +327,7 @@ impl<'a> Frame<'a> {
|
||||
self.dst.map(|m| mac_to_string(&m))
|
||||
}
|
||||
|
||||
/// Slice dos information elements, quando o quadro tem um.
|
||||
/// Returns the Information Elements (IE) byte slice for management frames that carry one.
|
||||
pub fn ies(&self) -> Option<&'a [u8]> {
|
||||
if self.ie_offset == 0 || self.ie_offset >= self.body.len() {
|
||||
None
|
||||
@@ -336,8 +336,8 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Faz parsing dos information elements (TLV), extraindo SSID, canal,
|
||||
/// cifra/AKM e indicadores de WPS e PMF.
|
||||
/// Parses TLV Information Elements, extracting SSID, channel, cipher/AKM suites,
|
||||
/// WPS state, and PMF (802.11w) flags.
|
||||
pub fn management_info(&self) -> ManagementInfo {
|
||||
let mut info = ManagementInfo::default();
|
||||
let Some(ies) = self.ies() else {
|
||||
@@ -449,8 +449,8 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload EAPOL (handshake WPA) de um quadro de dados não protegido.
|
||||
/// O slice começa no cabeçalho EAPOL (versão, tipo, tamanho).
|
||||
/// Extracts the EAPOL payload slice (`0x888E`) from an unprotected data frame.
|
||||
/// The returned slice begins at the 4-byte EAPOL header.
|
||||
pub fn eapol(&self) -> Option<&'a [u8]> {
|
||||
if self.protected || !self.kind.is_data() {
|
||||
return None;
|
||||
@@ -469,10 +469,10 @@ impl<'a> Frame<'a> {
|
||||
self.body.get(start..end)
|
||||
}
|
||||
|
||||
/// Tenta identificar a estação por payload não criptografado. Só olha
|
||||
/// dados em claro; em WPA2/WPA3 o conteúdo é opaco e retorna vazio — a
|
||||
/// menos que a senha já tenha sido quebrada (ver [`Self::ccmp_payload`]
|
||||
/// + [`payload_info_from_llc`]).
|
||||
/// Extracts passive client identity hints from an unprotected data frame.
|
||||
/// Returns empty `PayloadInfo` for protected frames (use [`Self::ccmp_payload`]
|
||||
/// + [`payload_info_from_llc`] when the station's TK is known).
|
||||
///
|
||||
pub fn payload_info(&self) -> PayloadInfo {
|
||||
if self.protected || !self.kind.is_data() {
|
||||
return PayloadInfo::default();
|
||||
@@ -483,8 +483,8 @@ impl<'a> Frame<'a> {
|
||||
payload_info_from_llc(payload)
|
||||
}
|
||||
|
||||
/// TID do campo QoS Control, quando o quadro é QoS Data. `None` pra
|
||||
/// dados não-QoS (o nonce do CCMP usa TID 0 nesse caso).
|
||||
/// Extracts the QoS Control TID (`0..15`) for QoS Data frames, or `None` for non-QoS.
|
||||
///
|
||||
pub fn qos_tid(&self) -> Option<u8> {
|
||||
if !self.kind.is_data() {
|
||||
return None;
|
||||
@@ -500,10 +500,10 @@ impl<'a> Frame<'a> {
|
||||
self.body.get(offset).map(|b| b & 0x0f)
|
||||
}
|
||||
|
||||
/// Decifra um quadro de dados CCMP com a TK já derivada (handshake
|
||||
/// quebrado) e devolve o LLC/SNAP + payload em claro, no mesmo formato
|
||||
/// que um quadro não protegido teria. `None` se o quadro não for CCMP
|
||||
/// protegido ou os bytes não baterem com um cabeçalho CCMP válido.
|
||||
/// Decrypts a CCMP-protected data frame using the station's 16-byte Temporal Key (TK)
|
||||
/// and returns the cleartext LLC/SNAP + upper-layer payload.
|
||||
///
|
||||
///
|
||||
pub fn ccmp_payload(&self, tk: &[u8; 16]) -> Option<Vec<u8>> {
|
||||
if !self.protected || !self.kind.is_data() {
|
||||
return None;
|
||||
@@ -515,9 +515,9 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// LLC/SNAP (AA AA 03 00 00 00 + ethertype) seguido do payload em claro —
|
||||
/// compartilhado entre quadros nunca cifrados e quadros CCMP já decifrados
|
||||
/// com a TK conhecida.
|
||||
/// Parses cleartext LLC/SNAP (`AA AA 03 00 00 00 + EtherType`) payloads for client
|
||||
/// identity hints (shared by unencrypted frames and CCMP-decrypted frames).
|
||||
///
|
||||
pub fn payload_info_from_llc(payload: &[u8]) -> PayloadInfo {
|
||||
let mut info = PayloadInfo::default();
|
||||
let Some(llc) = payload.get(..8) else {
|
||||
@@ -1062,7 +1062,7 @@ fn manufacturer_from_text(text: &str) -> Option<String> {
|
||||
Some(name.to_string())
|
||||
}
|
||||
|
||||
/// Mantido para chamadores antigos: há um EAPOL neste quadro?
|
||||
/// Returns `true` if this frame carries an EAPOL payload.
|
||||
pub fn is_eapol(body: &[u8]) -> bool {
|
||||
parse(body).and_then(|f| f.eapol().map(|_| ())).is_some()
|
||||
}
|
||||
@@ -1102,8 +1102,8 @@ fn add_phy(phy: &mut Vec<String>, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Information Element de WPS (vendor 00:50:f2 tipo 4). O conteúdo começa
|
||||
/// com a versão do protocolo e segue em atributos TLV de 16 bits.
|
||||
/// Parses a Microsoft WPS vendor-specific IE (`00:50:f2:04`, big-endian 16-bit TLVs).
|
||||
///
|
||||
fn parse_wps(val: &[u8], info: &mut ManagementInfo) {
|
||||
if val.len() < 6 {
|
||||
return;
|
||||
@@ -1366,7 +1366,7 @@ mod tests {
|
||||
f.extend_from_slice(&0x0108u16.to_le_bytes()); // data, ToDS
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
f.extend_from_slice(&mac(0x11)); // addr1 = AP
|
||||
f.extend_from_slice(&mac(0x22)); // addr2 = cliente
|
||||
f.extend_from_slice(&mac(0x22)); // addr2 = client
|
||||
f.extend_from_slice(&mac(0x11)); // addr3 = AP
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
f.extend_from_slice(&[0xAA, 0xAA, 0x03, 0, 0, 0]);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Parser do handshake WPA/WPA2 (EAPOL-Key, IEEE 802.1X).
|
||||
//! WPA/WPA2 EAPOL-Key (IEEE 802.1X) 4-way handshake parser.
|
||||
//!
|
||||
//! Identifies messages 1–4 of the 4-way handshake and extracts nonces, MIC,
|
||||
//! key descriptor version, and PMKID for [`crate::crack`].
|
||||
//!
|
||||
//! Lê as quatro mensagens do 4-way handshake, identifica o número de cada
|
||||
//! uma, extrai nonces, MIC, versão de chave e PMKID — matéria-prima para
|
||||
//! montar um alvo de quebra por dicionário em [`crate::crack`].
|
||||
|
||||
pub const EAPOL_TYPE_KEY: u8 = 3;
|
||||
|
||||
@@ -12,8 +12,8 @@ const KEY_INFO_ACK: u16 = 0x0080;
|
||||
const KEY_INFO_MIC: u16 = 0x0100;
|
||||
const KEY_INFO_SECURE: u16 = 0x0200;
|
||||
|
||||
/// Deslocamento absoluto do campo MIC dentro do quadro EAPOL (já incluindo
|
||||
/// o cabeçalho de 4 bytes).
|
||||
/// Byte offset of the 16-byte MIC field within the full EAPOL frame (including
|
||||
/// the 4-byte 802.1X header).
|
||||
pub const MIC_OFFSET: usize = 81;
|
||||
pub const KEY_DATA_OFFSET: usize = 99;
|
||||
pub const MIN_KEY_FRAME_LEN: usize = 99;
|
||||
@@ -22,10 +22,10 @@ pub const MIN_KEY_FRAME_LEN: usize = 99;
|
||||
pub struct EapolKey {
|
||||
pub version: u8,
|
||||
pub descriptor_type: u8,
|
||||
/// 1, 2, 3 ou 4; 0 quando não é uma das mensagens do 4-way handshake.
|
||||
/// Handshake message number (`1..=4`, or `0` if not a 4-way handshake frame).
|
||||
pub message: u8,
|
||||
pub key_info: u16,
|
||||
/// Versão do descritor: 1 = TKIP, 2 = CCMP, 3 = AES-CMAC.
|
||||
/// Key descriptor version: `1` = HMAC-MD5/RC4 (TKIP), `2` = HMAC-SHA1/AES (CCMP), `3` = AES-128-CMAC.
|
||||
pub key_version: u8,
|
||||
pub pairwise: bool,
|
||||
pub replay_counter: u64,
|
||||
@@ -33,8 +33,8 @@ pub struct EapolKey {
|
||||
pub mic: [u8; 16],
|
||||
pub key_data: Vec<u8>,
|
||||
pub pmkid: Option<[u8; 16]>,
|
||||
/// Quadro EAPOL completo (cabeçalho + descritor + dados), com o MIC no
|
||||
/// lugar — usado para recalcular o MIC durante a quebra.
|
||||
/// Full raw EAPOL frame (header + key descriptor + key data), used to
|
||||
/// zero the MIC field and recompute it during password verification.
|
||||
pub frame: Vec<u8>,
|
||||
}
|
||||
|
||||
@@ -51,17 +51,17 @@ pub fn parse(buf: &[u8]) -> Option<EapolKey> {
|
||||
let frame = buf[..total].to_vec();
|
||||
|
||||
let descriptor_type = frame[4];
|
||||
// 2 = RSN (WPA2/WPA3), 254 = WPA1. Outros descritores não interessam.
|
||||
// 2 = RSN (WPA2/WPA3), 254 = legacy WPA1.
|
||||
if descriptor_type != 2 && descriptor_type != 254 {
|
||||
return None;
|
||||
}
|
||||
// Todo campo multi-byte do descritor EAPOL-Key (Key Information, Key
|
||||
// Length, Replay Counter, Key Data Length) é big-endian — é 802.1X,
|
||||
// segue a mesma ordem de rede do cabeçalho EAPOL em si. Ler como
|
||||
// little-endian (bug anterior) embaralhava os bits de ack/mic/secure e
|
||||
// fazia praticamente todo handshake real cair em message=0, descartado
|
||||
// silenciosamente — só não aparecia nos testes porque o gerador de
|
||||
// quadro sintético cometia o mesmo engano, simetricamente.
|
||||
// All multi-byte fields in the EAPOL-Key descriptor (Key Information, Key
|
||||
// Length, Replay Counter, Key Data Length) are network byte order (big-endian).
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
let key_info = u16::from_be_bytes([frame[5], frame[6]]);
|
||||
let key_version = (key_info & 0x0007) as u8;
|
||||
let pairwise = key_info & KEY_INFO_PAIRWISE != 0;
|
||||
@@ -111,8 +111,8 @@ pub fn parse(buf: &[u8]) -> Option<EapolKey> {
|
||||
})
|
||||
}
|
||||
|
||||
/// PMKID KDE: `dd 14 00 0f ac 04` + 16 bytes. Pode vir em qualquer posição
|
||||
/// dos dados da chave (normalmente na mensagem 1).
|
||||
/// Extracts the 16-byte PMKID from an RSN PMKID KDE (`dd 14 00 0f ac 04 + 16 bytes`)
|
||||
/// inside EAPOL-Key data (typically present in message 1).
|
||||
pub fn parse_pmkid_kde(data: &[u8]) -> Option<[u8; 16]> {
|
||||
let mut i = 0usize;
|
||||
while i + 2 <= data.len() {
|
||||
@@ -183,24 +183,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte a byte, independente do helper `key_frame` (que usa a mesma
|
||||
/// codificação do parser): confere que um quadro Mensagem 1 real — key
|
||||
/// info 0x008a (Ack=1, versão CCMP=2, pairwise), big-endian como é no
|
||||
/// fio de verdade — é lido como message=1/key_version=2, não message=0.
|
||||
/// É exatamente o valor citado pela documentação de ferramentas de
|
||||
/// quebra de WPA pra mensagem 1 com CCMP.
|
||||
/// Verifies against a raw wire-format EAPOL Message 1 frame (`key_info = 0x008a`
|
||||
/// big-endian: Ack=1, Pairwise=1, Version=2 CCMP).
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
#[test]
|
||||
fn parses_real_wire_message1_key_info_as_big_endian() {
|
||||
let mut frame = vec![0u8; MIN_KEY_FRAME_LEN];
|
||||
frame[0] = 2; // versão EAPOL
|
||||
frame[0] = 2; // EAPOL version
|
||||
frame[1] = EAPOL_TYPE_KEY;
|
||||
frame[2..4].copy_from_slice(&((MIN_KEY_FRAME_LEN - 4) as u16).to_be_bytes());
|
||||
frame[4] = 2; // descritor RSN
|
||||
frame[5..7].copy_from_slice(&0x008au16.to_be_bytes()); // Ack=1, versão=2(CCMP), pairwise=1
|
||||
frame[4] = 2; // RSN key descriptor
|
||||
frame[5..7].copy_from_slice(&0x008au16.to_be_bytes()); // Ack=1, version=2 (CCMP), pairwise=1
|
||||
frame[7..9].copy_from_slice(&16u16.to_be_bytes()); // key length
|
||||
frame[9..17].copy_from_slice(&1u64.to_be_bytes()); // replay counter
|
||||
frame[17..49].fill(0xaa); // nonce
|
||||
frame[97..99].copy_from_slice(&0u16.to_be_bytes()); // sem key data
|
||||
frame[97..99].copy_from_slice(&0u16.to_be_bytes()); // key data length = 0
|
||||
|
||||
let parsed = parse(&frame).expect("mensagem 1 real deveria parsear");
|
||||
assert_eq!(parsed.message, 1);
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
//! Evil Twin pra redes WPA2/WPA3-Enterprise (802.1X — usuário+senha via
|
||||
//! RADIUS, não uma senha única como o PSK comum): clona o AP numa segunda
|
||||
//! interface anunciando o mesmo SSID e aceita o handshake EAP (PEAP/EAP-TTLS
|
||||
//! com MSCHAPv2) de qualquer cliente que tente autenticar nele. Diferente do
|
||||
//! Evil Twin de PSK (`eviltwin.rs`), aqui o dispositivo manda a credencial
|
||||
//! **automaticamente**, como parte do próprio protocolo 802.1X — não precisa
|
||||
//! de portal cativo nem de nenhuma ação humana, porque o cliente já tenta
|
||||
//! autenticar sozinho em qualquer AP que anuncie o SSID que ele reconhece.
|
||||
//! WPA2/WPA3-Enterprise (802.1X) Evil Twin: clones an Enterprise AP on a
|
||||
//! secondary interface using `hostapd-wpe` to accept EAP (PEAP/EAP-TTLS with
|
||||
//! inner MSCHAPv2) authentication attempts and log NetNTLMv1 challenge/response
|
||||
//! hashes for offline cracking with `john` or `hashcat -m 5500`.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! Note: because the server certificate is self-signed, clients configured to
|
||||
//! strictly validate the RADIUS CA certificate chain will reject the connection;
|
||||
//! clients without strict CA pinning will complete the inner MSCHAPv2 exchange.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! Isso só existe pra Enterprise porque só nela a senha (ou algo derivado
|
||||
//! dela) de fato viaja pela rede como parte da autenticação — no PSK a
|
||||
//! senha nunca é transmitida (só confere um MIC), por isso o Evil Twin de
|
||||
//! PSK precisa enganar um humano a digitá-la de novo.
|
||||
//!
|
||||
//! Orquestra o `hostapd-wpe` (não vem com o Sabiá — precisa estar instalado
|
||||
//! separadamente: não é um binário pequeno vendorizável como o reaver, é um
|
||||
//! patch sobre a árvore de build do próprio hostapd). O Sabiá só gera a
|
||||
//! config, o `eap_user` genérico e o certificado autoassinado, e depois lê o
|
||||
//! log de credenciais capturadas.
|
||||
//!
|
||||
//! Limitação honesta: o certificado é autoassinado. Clientes que validam a
|
||||
//! cadeia de certificado da rede (comum em dispositivos atualizados,
|
||||
//! principalmente com perfil MDM corporativo) vão recusar a conexão ou
|
||||
//! avisar o usuário — exatamente a defesa que deveria existir contra esse
|
||||
//! ataque. Funciona de verdade contra dispositivos configurados sem essa
|
||||
//! validação (ainda comum em redes legadas/mal configuradas), que é o caso
|
||||
//! que esse ataque sempre existiu para expor.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Config do hostapd-wpe pro AP clone: mesmo SSID/canal do AP real,
|
||||
/// anunciando WPA2-Enterprise e aceitando qualquer EAP (PEAP/TTLS) — é o
|
||||
/// `eap_user_file` genérico que faz ele aceitar qualquer identidade.
|
||||
/// Generates the `hostapd-wpe` configuration for the Enterprise clone AP.
|
||||
///
|
||||
///
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn hostapd_wpe_conf(
|
||||
iface: &str,
|
||||
@@ -64,20 +64,20 @@ pub fn hostapd_wpe_conf(
|
||||
out
|
||||
}
|
||||
|
||||
/// `eap_user` genérico: aceita qualquer identidade externa pra PEAP/TTLS e
|
||||
/// força MSCHAPv2 por dentro — é o que faz o hostapd-wpe logar usuário +
|
||||
/// challenge/response de qualquer cliente que tentar autenticar, sem
|
||||
/// precisar saber credenciais reais de antemão.
|
||||
/// Wildcard `eap_user` table: accepts any outer identity for PEAP/TTLS and
|
||||
/// negotiates inner MSCHAPv2 so `hostapd-wpe` logs the challenge/response pair.
|
||||
///
|
||||
///
|
||||
pub const DEFAULT_EAP_USER_FILE: &str = concat!(
|
||||
"# gerado pelo Sabiá — aceita qualquer identidade, qualquer EAP externo\n",
|
||||
"* PEAP,TTLS\n",
|
||||
"\"anonymous\" TTLS-MSCHAPV2,MSCHAPV2 \"senha\" [2]\n",
|
||||
);
|
||||
|
||||
/// Argumentos do `openssl req` pra gerar o par certificado+chave
|
||||
/// autoassinado que o hostapd-wpe precisa pra abrir TLS (PEAP/TTLS sempre
|
||||
/// envolve um túnel TLS, mesmo sendo um AP falso). Efêmero: 1 dia de validade
|
||||
/// é de sobra pra uma sessão de teste.
|
||||
/// Builds `openssl req` arguments to generate a 1-day ephemeral self-signed
|
||||
/// RSA certificate and private key for the outer EAP-TLS/PEAP/TTLS tunnel.
|
||||
///
|
||||
///
|
||||
pub fn openssl_selfsigned_cert_args(key_path: &str, cert_path: &str) -> Vec<String> {
|
||||
[
|
||||
"req",
|
||||
@@ -103,23 +103,23 @@ fn sanitize_conf_value(value: &str) -> String {
|
||||
value.replace(['\n', '\r'], "")
|
||||
}
|
||||
|
||||
/// Uma credencial capturada: identidade + a linha pronta pra crackear
|
||||
/// (formato `netntlm` do John the Ripper / compatível com `hashcat -m 5500`),
|
||||
/// se o hostapd-wpe conseguiu capturar os dois.
|
||||
/// A captured Enterprise credential: username plus NetNTLMv1 hash string
|
||||
/// (compatible with `john --format=netntlm` and `hashcat -m 5500`).
|
||||
///
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CapturedCredential {
|
||||
pub username: String,
|
||||
pub netntlm: Option<String>,
|
||||
}
|
||||
|
||||
/// Lê o `wpe_logfile` e extrai as credenciais capturadas. Parser
|
||||
/// best-effort: o formato do hostapd-wpe é documentado (blocos
|
||||
/// `username:`/`challenge:`/`response:`/`jtr NETNTLM:`), mas não dá pra
|
||||
/// validar contra uma instalação de verdade nesta máquina (não é um
|
||||
/// binário que o Sabiá vendoriza). Pega a linha `jtr NETNTLM:` quando
|
||||
/// presente — já vem pronta pra `john --format=netntlm` ou pra reformatar
|
||||
/// pro `hashcat -m 5500` — e nunca falha "silenciosamente demais": o job
|
||||
/// sempre expõe o arquivo de log cru também, pra conferência manual.
|
||||
/// Parses `hostapd-wpe` log output and extracts captured MSCHAPv2 / NetNTLMv1 credentials.
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn captured_identities(log: &str) -> Vec<CapturedCredential> {
|
||||
let mut out: Vec<CapturedCredential> = Vec::new();
|
||||
let mut current_username: Option<String> = None;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Montagem dos handshakes WPA/WPA2 a partir dos quadros EAPOL vistos no ar.
|
||||
//! Assembles WPA/WPA2 4-way handshakes and PMKIDs from captured EAPOL frames.
|
||||
//!
|
||||
//! Keyed by `(BSSID, station)`. Tracks messages 1–4, PMKID from message 1,
|
||||
//! and the raw radiotap+802.11 frames required to export `.pcap` or `.hc22000`.
|
||||
//!
|
||||
//!
|
||||
//! Um handshake é identificado pelo par (BSSID, estação). Guardamos as quatro
|
||||
//! mensagens quando aparecem, o PMKID da mensagem 1 e os quadros crus
|
||||
//! necessários para exportar um `.pcap` que o `aircrack-ng`/hashcat também
|
||||
//! entendam.
|
||||
|
||||
use crate::dot11;
|
||||
use crate::eapol::{self, EapolKey};
|
||||
@@ -24,20 +24,20 @@ pub struct Handshake {
|
||||
pub snonce: Option<[u8; 32]>,
|
||||
pub pmkid: Option<[u8; 16]>,
|
||||
pub key_version: u8,
|
||||
/// Bitmask das mensagens vistas (bit 0 = mensagem 1).
|
||||
/// Bitmask of observed handshake messages (bit 0 = message 1).
|
||||
pub messages: u8,
|
||||
/// Quadro EAPOL da mensagem 2, com o MIC — necessário para conferir a
|
||||
/// senha candidata.
|
||||
/// Full EAPOL frame from message 2 (containing the MIC) used to verify
|
||||
/// candidate passphrases.
|
||||
pub eapol_msg2: Option<Vec<u8>>,
|
||||
pub raw: Vec<RawFrame>,
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
pub password: Option<String>,
|
||||
/// Temporal Key (CCMP/AES) derivada quando `password` é preenchido.
|
||||
/// Só pra TKIP (key_version 1) fica None: a decriptação aqui só cobre
|
||||
/// CCMP. Serve pra enxergar o tráfego em claro desse par (BSSID,
|
||||
/// estação) específico — não abre o tráfego de outros clientes, já que
|
||||
/// a PTK é efêmera por associação.
|
||||
/// 16-byte CCMP Temporal Key (TK) derived once `password` is known.
|
||||
/// `None` for TKIP (`key_version == 1`) or PMKID-only handshakes. Scoped
|
||||
/// to this specific `(BSSID, station)` association.
|
||||
///
|
||||
///
|
||||
pub tk: Option<[u8; 16]>,
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ impl Handshake {
|
||||
}
|
||||
}
|
||||
|
||||
/// Quadros crus (radiotap + 802.11) das mensagens vistas, em ordem.
|
||||
/// Raw radiotap + 802.11 frames of observed messages, in arrival order.
|
||||
pub fn raw_frames(&self) -> Vec<Vec<u8>> {
|
||||
let mut v = self.raw.clone();
|
||||
v.sort_by_key(|f| f.message);
|
||||
@@ -140,8 +140,8 @@ impl HandshakeTracker {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Processa um quadro 802.11. Retorna `true` quando o quadro era EAPOL e
|
||||
/// atualizou (ou criou) um handshake.
|
||||
/// Processes a parsed 802.11 frame. Returns `true` if it contained an EAPOL-Key
|
||||
/// message that created or updated a handshake entry.
|
||||
pub fn ingest(
|
||||
&mut self,
|
||||
frame: &dot11::Frame<'_>,
|
||||
@@ -159,8 +159,8 @@ impl HandshakeTracker {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Em data frames com DS, o BSSID está em `frame.bssid`; a estação é
|
||||
// quem recebe (1/3, AP -> STA) ou quem transmite (2/4, STA -> AP).
|
||||
// In DS data frames, `frame.bssid` holds the AP MAC; the station is
|
||||
// `destination` on downlink (M1/M3) or `transmitter` on uplink (M2/M4).
|
||||
let bssid = frame.bssid_str();
|
||||
let station = match key.message {
|
||||
1 | 3 => frame.addr1.map(|m| dot11::mac_to_string(&m)),
|
||||
@@ -198,8 +198,8 @@ impl HandshakeTracker {
|
||||
true
|
||||
}
|
||||
|
||||
/// Atualiza o SSID de handshakes já vistos (quando o beacon chega depois
|
||||
/// do EAPOL, o que acontece em capturas começando no meio do handshake).
|
||||
/// Backfills the SSID on existing handshakes when a beacon arrives after
|
||||
/// the EAPOL exchange.
|
||||
pub fn update_ssid(&mut self, bssid: &str, ssid: &str) {
|
||||
if ssid.is_empty() {
|
||||
return;
|
||||
@@ -233,8 +233,8 @@ impl HandshakeTracker {
|
||||
self.all().into_iter().map(Handshake::info).collect()
|
||||
}
|
||||
|
||||
/// Primeiro handshake que pode ser quebrado; filtros opcionais por BSSID
|
||||
/// e estação.
|
||||
/// Returns the first crackable handshake matching the optional `bssid` and
|
||||
/// `station` filters.
|
||||
pub fn first_crackable(
|
||||
&self,
|
||||
bssid: Option<&str>,
|
||||
@@ -254,9 +254,9 @@ impl HandshakeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// TK já derivada pra esse par (BSSID, estação), se a senha da rede já
|
||||
/// foi quebrada e o handshake tinha os dois nonces (EAPOL completo — um
|
||||
/// handshake só de PMKID não dá pra derivar PTK/TK, só confirma a PSK).
|
||||
/// Returns the derived 16-byte CCMP Temporal Key (TK) for `(bssid, station)`
|
||||
/// if the network password has been cracked and both nonces were captured.
|
||||
///
|
||||
pub fn tk_for(&self, bssid: &str, station: &str) -> Option<[u8; 16]> {
|
||||
self.get(bssid, station).and_then(|hs| hs.tk)
|
||||
}
|
||||
@@ -270,10 +270,10 @@ impl HandshakeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deriva a TK (bytes 32..48 da PTK) a partir da senha quebrada, pra poder
|
||||
/// decifrar CCMP do tráfego desse cliente. Só funciona com EAPOL/CCMP
|
||||
/// completo (key_version 2); PMKID sozinho não tem os dois nonces pra
|
||||
/// derivar PTK, e TKIP (key_version 1) usa RC4 + Michael, não implementado.
|
||||
/// Derives the 16-byte CCMP Temporal Key (`PTK[32..48]`) from a cracked passphrase.
|
||||
/// Requires a full EAPOL CCMP handshake (`key_version == 2`) with both nonces.
|
||||
///
|
||||
///
|
||||
fn derive_tk(hs: &Handshake, password: &str) -> Option<[u8; 16]> {
|
||||
if hs.key_version != 2 {
|
||||
return None;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Exportação no formato `.hc22000` do hashcat.
|
||||
//! Exports captured WPA/WPA2 material in hashcat `.hc22000` format (`-m 22000`).
|
||||
//!
|
||||
//! Duas linhas possíveis, como no hcxtools:
|
||||
//! Emits both record types supported by `hcxtools` / `hashcat`:
|
||||
//!
|
||||
//! * `WPA*01*PMKID*AP*STA*SSID***`
|
||||
//! * `WPA*02*MIC*AP*STA*SSID*ANONCE*EAPOL*MESSAGEPAIR`
|
||||
//!
|
||||
//! O EAPOL exportado é o quadro da mensagem 2 completo (cabeçalho 802.1X +
|
||||
//! descritor), que é o que o hashcat espera no campo `EAPOL_CLIENT`.
|
||||
//! The exported EAPOL field contains the full message 2 frame (802.1X header +
|
||||
//! key descriptor) expected by hashcat's `EAPOL_CLIENT` field.
|
||||
|
||||
use crate::eapol::MIC_OFFSET;
|
||||
use crate::handshake::Handshake;
|
||||
@@ -55,7 +55,7 @@ pub fn hashcat_line(hs: &Handshake) -> Option<String> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Linhas únicas, uma por handshake que tenha material suficiente.
|
||||
/// Formats deduplicated `.hc22000` lines for all crackable handshakes.
|
||||
pub fn export<'a>(handshakes: impl IntoIterator<Item = &'a Handshake>) -> Vec<String> {
|
||||
let mut lines: Vec<String> = handshakes.into_iter().filter_map(hashcat_line).collect();
|
||||
lines.sort();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Descoberta de interfaces de rede e identificação das que são Wi-Fi.
|
||||
//! Network interface enumeration and 802.11 wireless phy identification.
|
||||
//!
|
||||
//! Não depende de `iw`: lê direto de `/sys/class/net`, do mesmo jeito que o
|
||||
//! próprio `iw`/`ip` fazem por baixo.
|
||||
//! Reads directly from `/sys/class/net` on Linux, falling back to `iw` for
|
||||
//! detailed interface mode inspection when available.
|
||||
|
||||
use crate::error::{Result, SabiaError};
|
||||
use std::fs;
|
||||
@@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
|
||||
pub struct Interface {
|
||||
pub name: String,
|
||||
pub mac: String,
|
||||
/// Caminho do phy (ex.: `phy0`), presente só em interfaces 802.11.
|
||||
/// Wireless `wiphy` name (e.g. `"phy0"`), present only on 802.11 interfaces.
|
||||
pub phy: Option<String>,
|
||||
pub mode: Mode,
|
||||
pub up: bool,
|
||||
@@ -28,7 +28,7 @@ fn net_root() -> &'static Path {
|
||||
Path::new("/sys/class/net")
|
||||
}
|
||||
|
||||
/// Lista todas as interfaces de rede, Wi-Fi ou não (chamador filtra por `phy`).
|
||||
/// Lists all network interfaces on the system (wireless interfaces have `phy.is_some()`).
|
||||
pub fn list_all() -> Result<Vec<Interface>> {
|
||||
let mut out = Vec::new();
|
||||
for entry in fs::read_dir(net_root())? {
|
||||
@@ -43,7 +43,7 @@ pub fn list_all() -> Result<Vec<Interface>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Lista apenas interfaces com um `phy80211` (ou seja, rádios Wi-Fi).
|
||||
/// Lists only 802.11 wireless interfaces (`phy80211` present).
|
||||
pub fn list_wireless() -> Result<Vec<Interface>> {
|
||||
Ok(list_all()?
|
||||
.into_iter()
|
||||
@@ -79,8 +79,8 @@ fn read_interface(name: &str) -> Result<Interface> {
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|f| f.to_string_lossy().into_owned()))
|
||||
.or_else(|| {
|
||||
// em alguns kernels phy80211 é um diretório real, não symlink;
|
||||
// o nome do phy então vem do subdiretório "phyNN" dentro dele.
|
||||
// On some kernels `phy80211` is a directory rather than a symlink;
|
||||
// read the `"phyNN"` subdirectory inside it.
|
||||
fs::read_dir(&phy80211)
|
||||
.ok()
|
||||
.and_then(|mut rd| rd.next())
|
||||
@@ -106,10 +106,10 @@ fn read_interface(name: &str) -> Result<Interface> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Lê o modo (`managed`/`monitor`) via `/sys/class/net/<if>/type` + `uevent`
|
||||
/// não é suficiente (type é sempre ARPHRD_ETHER para Wi-Fi); a forma estável
|
||||
/// sem libnl é delegar a leitura real ao `iw dev <if> info` quando disponível,
|
||||
/// mantendo aqui um melhor-esforço local para não depender de root.
|
||||
/// Reads the interface operating mode (`managed`/`monitor`) via `/sys/class/net/<if>/type`
|
||||
/// and `iw dev <if> info` when available.
|
||||
///
|
||||
///
|
||||
fn read_wireless_mode(name: &str) -> Mode {
|
||||
match crate::netlink::iw_dev_type(name) {
|
||||
Some(Mode::Monitor) => Mode::Monitor,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//! Construção e envio de quadros de gerenciamento (desautenticação e
|
||||
//! desassociação) para testes autorizados de robustez de redes Wi-Fi.
|
||||
//! Construction and raw socket injection of 802.11 management frames
|
||||
//! (deauthentication, disassociation, open auth, association) for authorized testing.
|
||||
//!
|
||||
//! Frame builders are platform-independent; raw transmission uses Linux `AF_PACKET`.
|
||||
//! Rate limits, counts, and audit logging are enforced by [`crate::engine::Engine`].
|
||||
//!
|
||||
//! A construção do quadro é independente de plataforma e testável; o envio
|
||||
//! usa `AF_PACKET` e só existe no Linux. O chamador (engine) impõe limites de
|
||||
//! quantidade e intervalo e registra a ação na trilha de auditoria.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::error::{Result, SabiaError};
|
||||
|
||||
pub const BROADCAST: [u8; 6] = [0xff; 6];
|
||||
|
||||
/// Cabeçalho radiotap mínimo (8 bytes, sem campos). É aceito por mac80211 e
|
||||
/// deixa o driver escolher a taxa; suficiente para quadros de gerenciamento.
|
||||
/// Minimal 8-byte Radiotap header (no present fields), accepted by `mac80211`
|
||||
/// for management frame injection.
|
||||
fn radiotap_min() -> [u8; 8] {
|
||||
[0, 0, 8, 0, 0, 0, 0, 0]
|
||||
}
|
||||
@@ -20,9 +20,9 @@ fn mgmt_frame(subtype: u8, bssid: [u8; 6], destination: [u8; 6], reason: u16) ->
|
||||
mgmt_frame_from(subtype, bssid, bssid, destination, reason)
|
||||
}
|
||||
|
||||
/// Quadro de gerenciamento com transmissor explícito. Permite forjar tanto
|
||||
/// AP→STA (`transmitter = BSSID`) quanto STA→AP (`transmitter = estação`),
|
||||
/// usado no teste bidirecional de PMF.
|
||||
/// Builds a deauth/disassoc management frame with an explicit transmitter MAC,
|
||||
/// supporting both AP->STA (`transmitter = BSSID`) and STA->AP (`transmitter = client`).
|
||||
///
|
||||
fn mgmt_frame_from(
|
||||
subtype: u8,
|
||||
transmitter: [u8; 6],
|
||||
@@ -35,21 +35,21 @@ fn mgmt_frame_from(
|
||||
f.extend_from_slice(&radiotap_min());
|
||||
f.extend_from_slice(&fc.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
f.extend_from_slice(&destination); // addr1: destino
|
||||
f.extend_from_slice(&transmitter); // addr2: transmissor
|
||||
f.extend_from_slice(&destination); // addr1: destination
|
||||
f.extend_from_slice(&transmitter); // addr2: transmitter
|
||||
f.extend_from_slice(&bssid); // addr3: BSSID
|
||||
f.extend_from_slice(&[0u8; 2]); // número de sequência
|
||||
f.extend_from_slice(&[0u8; 2]); // sequence control
|
||||
f.extend_from_slice(&reason.to_le_bytes());
|
||||
f
|
||||
}
|
||||
|
||||
/// Quadro de desautenticação (subtipo 12). `destination` pode ser uma
|
||||
/// estação específica ou [`BROADCAST`].
|
||||
/// Builds an AP->STA deauthentication frame (subtype 12). `destination` may be
|
||||
/// a unicast client MAC or [`BROADCAST`].
|
||||
pub fn deauth_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
||||
mgmt_frame(12, bssid, destination, reason)
|
||||
}
|
||||
|
||||
/// Desautenticação forjada com transmissor arbitrário (STA→AP).
|
||||
/// Builds a deauthentication frame with an explicit transmitter MAC.
|
||||
pub fn deauth_frame_from(
|
||||
transmitter: [u8; 6],
|
||||
bssid: [u8; 6],
|
||||
@@ -59,7 +59,7 @@ pub fn deauth_frame_from(
|
||||
mgmt_frame_from(12, transmitter, bssid, destination, reason)
|
||||
}
|
||||
|
||||
/// Desassociação forjada com transmissor arbitrário (STA→AP).
|
||||
/// Builds a disassociation frame with an explicit transmitter MAC.
|
||||
pub fn disassoc_frame_from(
|
||||
transmitter: [u8; 6],
|
||||
bssid: [u8; 6],
|
||||
@@ -69,19 +69,19 @@ pub fn disassoc_frame_from(
|
||||
mgmt_frame_from(10, transmitter, bssid, destination, reason)
|
||||
}
|
||||
|
||||
/// Quadro de desassociação (subtipo 10).
|
||||
/// Builds an AP->STA disassociation frame (subtype 10).
|
||||
pub fn disassoc_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
||||
mgmt_frame(10, bssid, destination, reason)
|
||||
}
|
||||
|
||||
/// Offset do campo "Sequence Control" dentro de um quadro de gerenciamento
|
||||
/// construído por [`mgmt_frame_from`] (8 de radiotap + 22 até o campo).
|
||||
/// Byte offset of the 802.11 Sequence Control field inside a frame built by
|
||||
/// [`mgmt_frame_from`] (8-byte Radiotap + 22-byte MAC header offset).
|
||||
const SEQ_CTRL_OFFSET: usize = 30;
|
||||
|
||||
/// Grava um número de sequência (0–4095) no quadro, deixando o número de
|
||||
/// fragmento (4 bits baixos) zerado. Sem isso, todo quadro injetado sai com
|
||||
/// sequência 0 e o driver do alvo descarta as repetições como duplicata,
|
||||
/// fazendo a desautenticação não ter efeito nenhum apesar de "enviada".
|
||||
/// Writes a 12-bit 802.11 sequence number (`0..4096`) into `frame` (with fragment
|
||||
/// number `0`) so target drivers do not drop repeated frames as duplicates.
|
||||
///
|
||||
///
|
||||
pub fn set_seq(frame: &mut [u8], seq: u16) {
|
||||
if frame.len() < SEQ_CTRL_OFFSET + 2 {
|
||||
return;
|
||||
@@ -99,8 +99,8 @@ fn ie(tag: u8, value: &[u8]) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Autenticação aberta (algoritmo 0, sequência 1). Usada antes do association
|
||||
/// request no teste de PMKID.
|
||||
/// Builds an Open System Authentication request frame (algorithm 0, seq 1) used
|
||||
/// prior to Association Request when probing for clientless PMKID.
|
||||
pub fn auth_frame(bssid: [u8; 6], station: [u8; 6]) -> Vec<u8> {
|
||||
let mut f = Vec::with_capacity(8 + 24 + 6);
|
||||
f.extend_from_slice(&radiotap_min());
|
||||
@@ -110,8 +110,8 @@ pub fn auth_frame(bssid: [u8; 6], station: [u8; 6]) -> Vec<u8> {
|
||||
f.extend_from_slice(&station);
|
||||
f.extend_from_slice(&bssid);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
f.extend_from_slice(&0u16.to_le_bytes()); // algoritmo aberto
|
||||
f.extend_from_slice(&1u16.to_le_bytes()); // sequência
|
||||
f.extend_from_slice(&0u16.to_le_bytes()); // Open System algorithm (0)
|
||||
f.extend_from_slice(&1u16.to_le_bytes()); // auth sequence (1)
|
||||
f.extend_from_slice(&0u16.to_le_bytes()); // status
|
||||
f
|
||||
}
|
||||
@@ -119,7 +119,7 @@ pub fn auth_frame(bssid: [u8; 6], station: [u8; 6]) -> Vec<u8> {
|
||||
fn rsn_ie(akms: &[u8], pmf: bool, pmf_required: bool, pmkid: &[u8; 16]) -> Vec<u8> {
|
||||
let mut val = Vec::new();
|
||||
val.extend_from_slice(&1u16.to_le_bytes());
|
||||
val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); // grupo CCMP
|
||||
val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); // group cipher suite: CCMP
|
||||
val.extend_from_slice(&1u16.to_le_bytes());
|
||||
val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); // pairwise CCMP
|
||||
val.extend_from_slice(&(akms.len() as u16).to_le_bytes());
|
||||
@@ -139,8 +139,8 @@ fn rsn_ie(akms: &[u8], pmf: bool, pmf_required: bool, pmkid: &[u8; 16]) -> Vec<u
|
||||
ie(48, &val)
|
||||
}
|
||||
|
||||
/// Association request com RSN/PMKID. O AP pode responder com a mensagem 1
|
||||
/// do 4-way handshake contendo o PMKID — é isso que o teste tenta provocar.
|
||||
/// Builds an Association Request carrying an RSN IE to elicit EAPOL Message 1
|
||||
/// (and its optional PMKID) from the target AP.
|
||||
pub fn assoc_request_frame(
|
||||
bssid: [u8; 6],
|
||||
station: [u8; 6],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! `sabia-core`: tudo que fala com o rádio Wi-Fi, interpreta quadros 802.11,
|
||||
//! mantém a trilha de auditoria e quebra handshakes. A CLI e a interface
|
||||
//! gráfica são só consumidoras disto — a parte complexa (radiotap, 802.11,
|
||||
//! EAPOL, captura e injeção) fica inteira aqui dentro.
|
||||
//! `sabia-core`: core 802.11 wireless auditing library (Radiotap/802.11/EAPOL
|
||||
//! parsers, raw capture and injection, WIDS channel analysis, WPA2/PMKID cracking,
|
||||
//! WPS, Evil Twin, reporting, and hash-chained audit logging).
|
||||
//!
|
||||
|
||||
pub mod analysis;
|
||||
pub mod audit;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Controle de modo (managed/monitor), canal e leitura das capacidades do
|
||||
//! rádio via `iw`.
|
||||
//! Wireless interface mode control (`managed`/`monitor`), channel switching,
|
||||
//! and supported channel enumeration via `iw` and `ip`.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! É uma ponte deliberada: o protocolo real é nl80211 por netlink, e trocar
|
||||
//! este módulo por uma implementação nativa (sem depender dos binários `iw` e
|
||||
//! `ip`) é o próximo passo natural. Por ora `iw`/`ip` são universais em
|
||||
//! Linux, testados, e evitam reimplementar genl/nl80211 antes de validar o
|
||||
//! pipeline com hardware real.
|
||||
|
||||
use crate::error::{Result, SabiaError};
|
||||
use crate::iface::Mode;
|
||||
@@ -98,7 +98,7 @@ pub fn phy_for_iface(iface: &str) -> Result<String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Lista canais suportados pelo phy da interface.
|
||||
/// Lists all 802.11 channels supported by the interface's `wiphy`.
|
||||
pub fn channels(iface: &str) -> Result<Vec<ChannelInfo>> {
|
||||
let phy = phy_for_iface(iface)?;
|
||||
let info = run("iw", &["phy", &phy, "info"])?;
|
||||
@@ -113,7 +113,7 @@ pub fn supported_channels(iface: &str) -> Result<Vec<u32>> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Canais a percorrer. `band` aceita `"2.4"`, `"5"`, `"6"`, `"all"`/`None`.
|
||||
/// Filters supported channels by band (`"2.4"`, `"5"`, `"6"`, or `"all"`/`None`).
|
||||
pub fn channel_plan(iface: &str, band: Option<&str>) -> Result<Vec<u32>> {
|
||||
let want = match band.map(|b| b.trim().to_ascii_lowercase()) {
|
||||
None => None,
|
||||
@@ -159,7 +159,7 @@ pub fn driver_name(iface: &str) -> Option<String> {
|
||||
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
|
||||
}
|
||||
|
||||
/// Põe a interface em modo monitor. Requer `CAP_NET_ADMIN` (normalmente root).
|
||||
/// Toggles monitor or managed mode on `iface` (requires `CAP_NET_ADMIN` / root).
|
||||
pub fn set_monitor_mode(iface: &str, enable: bool) -> Result<()> {
|
||||
run("ip", &["link", "set", iface, "down"])?;
|
||||
let target = if enable { "monitor" } else { "managed" };
|
||||
@@ -168,7 +168,7 @@ pub fn set_monitor_mode(iface: &str, enable: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Troca o canal atual da interface (precisa já estar em modo monitor).
|
||||
/// Switches `iface` to `channel` (the interface must already be in monitor mode).
|
||||
pub fn set_channel(iface: &str, channel: u32) -> Result<()> {
|
||||
run(
|
||||
"iw",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Identificação de fabricante pelo prefixo OUI do MAC.
|
||||
//! IEEE OUI vendor lookup by 3-byte MAC prefix.
|
||||
|
||||
const OUIS: &[(&str, &str)] = &[
|
||||
("00000c", "Cisco"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Leitor do formato `.pcap` clássico, usado para analisar capturas feitas
|
||||
//! pelo próprio Sabiá (link-type 127, radiotap) e pelo aircrack-ng/tcpdump
|
||||
//! (link-type 105, 802.11 cru).
|
||||
//! Classic libpcap (`.pcap`) reader supporting both `LINKTYPE_IEEE802_11_RADIOTAP` (127)
|
||||
//! and raw `LINKTYPE_IEEE802_11` (105) captures.
|
||||
//!
|
||||
|
||||
use crate::error::{Result, SabiaError};
|
||||
use std::fs::File;
|
||||
@@ -75,7 +75,7 @@ impl PcapReader {
|
||||
self.linktype
|
||||
}
|
||||
|
||||
/// Próximo quadro; `None` no fim do arquivo.
|
||||
/// Reads the next frame, returning `Ok(None)` at EOF.
|
||||
pub fn next_frame(&mut self) -> Result<Option<PcapFrame>> {
|
||||
let mut rec = [0u8; 16];
|
||||
match self.input.read_exact(&mut rec) {
|
||||
@@ -102,8 +102,8 @@ impl PcapReader {
|
||||
.read_exact(&mut data)
|
||||
.map_err(|e| SabiaError::InvalidPcap(format!("quadro truncado: {e}")))?;
|
||||
|
||||
// Capturas sem radiotap (link-type 105) recebem um cabeçalho mínimo
|
||||
// para o mesmo pipeline de parsing funcionar.
|
||||
// Prepend a minimal 8-byte Radiotap header to link-type 105 (raw 802.11)
|
||||
// frames so the downstream parser receives a uniform format.
|
||||
if self.linktype == LINKTYPE_IEEE802_11 {
|
||||
let mut with_radiotap = vec![0u8, 0, 8, 0, 0, 0, 0, 0];
|
||||
with_radiotap.extend_from_slice(&data);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Escritor de arquivos `.pcap` (formato clássico, link-type 127 =
|
||||
//! IEEE802_11_RADIOTAP) para exportar capturas — compatível com
|
||||
//! Wireshark, aircrack-ng e qualquer outra ferramenta do ecossistema.
|
||||
//! Classic libpcap (`.pcap`) writer (`LINKTYPE_IEEE802_11_RADIOTAP` = 127),
|
||||
//! compatible with Wireshark, `aircrack-ng`, and `hcxpcapngtool`.
|
||||
//!
|
||||
|
||||
use crate::error::Result;
|
||||
use std::fs::File;
|
||||
@@ -18,7 +18,7 @@ impl PcapWriter {
|
||||
pub fn create(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let file = File::create(path)?;
|
||||
let mut out = BufWriter::new(file);
|
||||
// Cabeçalho global do pcap clássico.
|
||||
// 24-byte classic libpcap global header.
|
||||
out.write_all(&0xa1b2c3d4u32.to_le_bytes())?; // magic
|
||||
out.write_all(&2u16.to_le_bytes())?; // version major
|
||||
out.write_all(&4u16.to_le_bytes())?; // version minor
|
||||
@@ -29,7 +29,7 @@ impl PcapWriter {
|
||||
Ok(Self { out })
|
||||
}
|
||||
|
||||
/// Grava um quadro crú (radiotap + 802.11), com timestamp de agora.
|
||||
/// Writes a raw Radiotap + 802.11 frame stamped with the current system time.
|
||||
pub fn write_frame(&mut self, raw: &[u8]) -> Result<()> {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -37,7 +37,7 @@ impl PcapWriter {
|
||||
self.out.write_all(&(now.as_secs() as u32).to_le_bytes())?;
|
||||
self.out.write_all(&(now.subsec_micros()).to_le_bytes())?;
|
||||
self.out.write_all(&(raw.len() as u32).to_le_bytes())?; // caplen
|
||||
self.out.write_all(&(raw.len() as u32).to_le_bytes())?; // len original
|
||||
self.out.write_all(&(raw.len() as u32).to_le_bytes())?; // original wire length
|
||||
self.out.write_all(raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Parser de cabeçalho Radiotap.
|
||||
//! IEEE 802.11 Radiotap header parser (<https://www.radiotap.org/>).
|
||||
//!
|
||||
//! Walks present flags in specification order with proper field alignment to
|
||||
//! extract channel/frequency and antenna signal strength (dBm).
|
||||
//!
|
||||
//!
|
||||
//! Só extraímos o que o restante do núcleo usa — frequência/canal e força do
|
||||
//! sinal — mas avançamos pelos campos na ordem da especificação, respeitando
|
||||
//! alinhamento, para não depender do formato exato que cada driver emite.
|
||||
//! Especificação: <https://www.radiotap.org/>
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum Band {
|
||||
@@ -36,7 +36,7 @@ pub struct RadiotapInfo {
|
||||
pub signal_dbm: Option<i8>,
|
||||
pub noise_dbm: Option<i8>,
|
||||
pub header_len: usize,
|
||||
/// Bitmap "present" da primeira palavra (campos padrão).
|
||||
/// Standard Radiotap `it_present` bit positions.
|
||||
pub present: u32,
|
||||
}
|
||||
|
||||
@@ -48,20 +48,20 @@ const BIT_FHSS: u32 = 1 << 4;
|
||||
const BIT_ANTSIGNAL: u32 = 1 << 5;
|
||||
const BIT_ANTNOISE: u32 = 1 << 6;
|
||||
|
||||
/// Retorna as infos extraídas e quantos bytes o cabeçalho ocupa (para o
|
||||
/// chamador saber onde o frame 802.11 propriamente começa).
|
||||
/// Parses the Radiotap header at the start of `buf`, returning `(RadiotapInfo, header_len)`.
|
||||
///
|
||||
pub fn parse(buf: &[u8]) -> Option<RadiotapInfo> {
|
||||
if buf.len() < 8 || buf[0] != 0 {
|
||||
return None; // version deve ser 0
|
||||
return None; // Radiotap `it_version` must be 0
|
||||
}
|
||||
let header_len = u16::from_le_bytes([buf[2], buf[3]]) as usize;
|
||||
if header_len < 8 || header_len > buf.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Pode haver múltiplas palavras de "present" (bit 31 = continua). Só a
|
||||
// primeira descreve os campos padrão; as seguintes são namespaces de
|
||||
// extensão que ficam depois dos campos que usamos.
|
||||
// Advance past any chained `it_present` extension words (bit 31 = more present words).
|
||||
//
|
||||
//
|
||||
let mut present = 0u32;
|
||||
let mut offset = 4usize;
|
||||
let mut first = true;
|
||||
@@ -139,7 +139,7 @@ pub fn freq_to_channel(freq_mhz: u16) -> Option<u32> {
|
||||
match freq_mhz {
|
||||
2412..=2472 => Some(((freq_mhz as u32 - 2412) / 5) + 1),
|
||||
2484 => Some(14),
|
||||
// 4.9 GHz / segurança pública (canais 182–196).
|
||||
// 4.9 GHz public safety band (channels 182–196).
|
||||
4910 => Some(182),
|
||||
4920 => Some(184),
|
||||
4940 => Some(188),
|
||||
@@ -193,13 +193,13 @@ mod tests {
|
||||
let present: u32 = BIT_TSFT | BIT_FLAGS | BIT_RATE | BIT_CHANNEL | BIT_ANTSIGNAL;
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&[0, 0]); // version, pad
|
||||
buf.extend_from_slice(&0u16.to_le_bytes()); // len (preenchido depois)
|
||||
buf.extend_from_slice(&0u16.to_le_bytes()); // header length (backfilled below)
|
||||
buf.extend_from_slice(&present.to_le_bytes());
|
||||
// present termina em 8; TSFT alinhado em 8
|
||||
// `it_present` ends at offset 8; TSFT is 8-byte aligned
|
||||
buf.extend_from_slice(&0x1122_3344_5566_7788u64.to_le_bytes());
|
||||
buf.push(0x10); // flags
|
||||
buf.push(0x02); // rate 1 Mb/s
|
||||
// channel alinhado em 2 (cursor 18 já é par)
|
||||
// Channel field is 2-byte aligned (offset 18)
|
||||
buf.extend_from_slice(&freq.to_le_bytes());
|
||||
buf.extend_from_slice(&0xa0u16.to_le_bytes());
|
||||
buf.push(signal as u8);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Relatório de auditoria em HTML paginado com Paged.js.
|
||||
//! Paginated HTML/PDF wireless security audit report generator (powered by Paged.js).
|
||||
//!
|
||||
//! Shared between the live in-app report preview and standalone offline HTML export.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! O mesmo modelo alimenta a pré-visualização na interface e o arquivo
|
||||
//! standalone. A paginação é feita no navegador (A4, margens, cabeçalho e
|
||||
//! rodapé), então o PDF sai pelo diálogo de impressão sem depender de
|
||||
//! Chromium no servidor.
|
||||
|
||||
use crate::analysis::Analysis;
|
||||
use crate::audit::AuditEntry;
|
||||
@@ -28,8 +28,8 @@ pub struct ReportModel {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ReportOptions {
|
||||
/// Troca o `<script src>` por um marcador para o servidor embutir o
|
||||
/// polyfill e gerar um HTML que funciona offline.
|
||||
/// Placeholder replaced with the inline `paged.polyfill.min.js` script in
|
||||
/// standalone offline HTML exports.
|
||||
pub inline_pagedjs: bool,
|
||||
}
|
||||
|
||||
@@ -37,11 +37,11 @@ const PAGED_MARKER: &str = "<!--SABIA_PAGEDJS-->";
|
||||
const LOGO_PNG: &[u8] = include_bytes!("../../../logo.png");
|
||||
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/// Embute o logo como `data:` URI — sem isso o relatório standalone (modo
|
||||
/// offline, pensado pra abrir sem servidor nenhum) teria um `<img
|
||||
/// src="/logo.png">` que nunca resolve fora do `sabia ui`. Pequeno o
|
||||
/// bastante (poucos KB) pra não valer a pena puxar uma crate de base64 só
|
||||
/// por isso.
|
||||
/// Encodes the Sabiá logo as a `data:image/png;base64,...` URI so standalone
|
||||
/// exported reports render offline without an HTTP server.
|
||||
///
|
||||
///
|
||||
///
|
||||
fn base64(bytes: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
|
||||
for chunk in bytes.chunks(3) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Regras de mutação sobre uma palavra do dicionário (estilo hashcat `-r`,
|
||||
//! bem mais simples) e força bruta por máscara (estilo hashcat `-a 3`).
|
||||
//! Wordlist mutation rules (lightweight hashcat `-r` style) and mask brute-force
|
||||
//! generator (hashcat `-a 3` style).
|
||||
//!
|
||||
//! Because each WPA-PSK candidate requires 4096 rounds of PBKDF2-HMAC-SHA1,
|
||||
//! mutation rules focus on high-yield patterns (capitalization, common numeric
|
||||
//! suffixes, single-character leetspeak) to keep the multiplier bounded.
|
||||
//!
|
||||
//!
|
||||
//! Em quebra de hash normal, regras são "de graça" — um hash SHA256 custa
|
||||
//! nanossegundos. Aqui não: cada candidato é um PBKDF2-HMAC-SHA1 de 4096
|
||||
//! iterações, então cada variante multiplica o tempo de verdade. As regras
|
||||
//! abaixo cobrem os padrões mais comuns (maiúscula inicial, sufixo numérico,
|
||||
//! leet simples) sem explodir pra centenas de variantes por palavra.
|
||||
|
||||
/// Quantas variantes [`expand`] gera por palavra base (pra calcular o
|
||||
/// multiplicador de tempo antes de rodar).
|
||||
/// Upper bound on variants produced by [`expand`] per base word.
|
||||
///
|
||||
pub const VARIANTS_PER_WORD: usize = 1 + 1 + 1 + SUFFIXES.len() + LEET_PAIRS.len();
|
||||
|
||||
const SUFFIXES: &[&str] = &[
|
||||
@@ -16,12 +16,12 @@ const SUFFIXES: &[&str] = &[
|
||||
"@", "#",
|
||||
];
|
||||
|
||||
/// (char original, substituto) — leet simples, uma troca por vez, não
|
||||
/// combinada, senão a combinatória explode rápido demais.
|
||||
/// `(source_char, leet_replacement)` single-substitution pairs.
|
||||
///
|
||||
const LEET_PAIRS: &[(char, char)] = &[('a', '@'), ('e', '3'), ('i', '1'), ('o', '0'), ('s', '$')];
|
||||
|
||||
/// Gera as variantes de `word` (incluindo a palavra original) que respeitam
|
||||
/// o comprimento válido de WPA (8–63), sem duplicatas.
|
||||
/// Generates deduplicated WPA-valid (8–63 char) mutations of `word` (including `word` itself).
|
||||
///
|
||||
pub fn expand(word: &str) -> Vec<String> {
|
||||
let mut out = Vec::with_capacity(VARIANTS_PER_WORD);
|
||||
let mut push = |s: String| {
|
||||
@@ -52,9 +52,9 @@ pub fn expand(word: &str) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Classe de caractere de um token `?x` de máscara (sintaxe hashcat: `?d`
|
||||
/// dígito, `?l` minúscula, `?u` maiúscula, `?s` símbolo comum, `?a` os
|
||||
/// quatro juntos). `??` escapa um `?` literal.
|
||||
/// Character class for a mask token (`?d` digits, `?l` lowercase, `?u` uppercase,
|
||||
/// `?s` symbols, `?a` all printable ASCII, `??` literal `?`).
|
||||
///
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CharClass {
|
||||
Digit,
|
||||
@@ -83,17 +83,17 @@ impl CharClass {
|
||||
}
|
||||
}
|
||||
|
||||
/// Máscara compilada: uma lista de alfabetos, um por posição. Gera
|
||||
/// candidatos por índice (contagem em base mista) sem materializar a lista
|
||||
/// inteira — essencial, já que `?d?d?d?d?d?d?d?d` sozinho são 100 milhões.
|
||||
/// Compiled brute-force mask (one alphabet per character position), supporting
|
||||
/// O(1) mixed-radix candidate generation by index.
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mask {
|
||||
alphabets: Vec<Vec<char>>,
|
||||
}
|
||||
|
||||
impl Mask {
|
||||
/// Compila uma máscara tipo `wifi?d?d?d?d` (literal "wifi" + 4 dígitos).
|
||||
/// `??` é um `?` literal; `?` seguido de letra desconhecida é erro.
|
||||
/// Compiles a hashcat-style mask pattern such as `"wifi?d?d?d?d"`.
|
||||
/// `??` escapes a literal `?`; unknown `?x` tokens return an error.
|
||||
pub fn parse(pattern: &str) -> Result<Self, String> {
|
||||
let mut alphabets = Vec::new();
|
||||
let mut chars = pattern.chars().peekable();
|
||||
@@ -123,8 +123,8 @@ impl Mask {
|
||||
Ok(Self { alphabets })
|
||||
}
|
||||
|
||||
/// Total de candidatos. `None` se não couber num `u64` (máscara longa
|
||||
/// demais pra ser levada a sério de qualquer forma).
|
||||
/// Total keyspace size, or `None` if it overflows `u64`.
|
||||
///
|
||||
pub fn total(&self) -> Option<u64> {
|
||||
self.alphabets
|
||||
.iter()
|
||||
@@ -139,9 +139,9 @@ impl Mask {
|
||||
self.alphabets.is_empty()
|
||||
}
|
||||
|
||||
/// Candidato de índice `index` (contagem em base mista, como um
|
||||
/// odômetro: a última posição gira mais rápido). `None` se `index` >=
|
||||
/// `total()`.
|
||||
/// Materializes the candidate at `index` (mixed-radix odometer order), or
|
||||
/// `None` if `index >= total()`.
|
||||
///
|
||||
pub fn candidate(&self, mut index: u64) -> Option<String> {
|
||||
let total = self.total()?;
|
||||
if index >= total {
|
||||
@@ -165,8 +165,8 @@ mod tests {
|
||||
#[test]
|
||||
fn expand_respects_min_length() {
|
||||
let variants = expand("senha");
|
||||
// "senha" tem 5 chars; sozinha não é candidata WPA válida (precisa
|
||||
// de 8+), mas com sufixo (ex.: "senha123", 8) sim.
|
||||
// `"senha"` is 5 chars (too short for WPA alone), but `"senha123"` (8 chars) is kept.
|
||||
//
|
||||
assert!(!variants.iter().any(|v| v == "senha"));
|
||||
assert!(variants.contains(&"senha123".to_string()));
|
||||
}
|
||||
@@ -183,7 +183,7 @@ mod tests {
|
||||
assert!(variants.contains(&"Abacaxi1".to_string()));
|
||||
assert!(variants.contains(&"ABACAXI1".to_string()));
|
||||
assert!(variants.contains(&"abacaxi1123".to_string()));
|
||||
// leet troca TODAS as ocorrências de 'a': as três em "abacaxi1".
|
||||
// Leetspeak substitution replaces all occurrences of `'a'` in `"abacaxi1"`.
|
||||
assert!(variants.contains(&"@b@c@xi1".to_string()));
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mask_literal_question_mark_and_unknown_class() {
|
||||
// "??" vira um '?' literal; os dois primeiros chars são consumidos
|
||||
// juntos, então "???d" é: '?' literal + "?d" (classe dígito).
|
||||
// `"??"` escapes a literal `'?'`, so `"???d"` parses as literal `'?'` + `?d`.
|
||||
//
|
||||
assert_eq!(
|
||||
Mask::parse("???d").unwrap().candidate(0).as_deref(),
|
||||
Some("?0")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Agregação de quadros capturados em tabelas de redes, clientes e
|
||||
//! handshakes — o coração do que a CLI e a interface mostram.
|
||||
//! Aggregates captured 802.11 frames into access point, client station, and
|
||||
//! handshake tables.
|
||||
|
||||
use crate::dot11::{self, FrameKind, Security};
|
||||
use crate::handshake::{Handshake, HandshakeInfo, HandshakeTracker};
|
||||
@@ -59,7 +59,7 @@ pub struct ClientEvent {
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Client {
|
||||
pub mac: String,
|
||||
/// Nome curto para exibir: hostname, nome WPS, modelo ou fabricante.
|
||||
/// Best human-readable label for a station: DHCP/mDNS hostname, WPS device name, model, or OUI vendor.
|
||||
pub display_name: Option<String>,
|
||||
pub vendor: Option<String>,
|
||||
pub manufacturer: Option<String>,
|
||||
@@ -78,10 +78,10 @@ pub struct Client {
|
||||
pub associated: bool,
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
/// Amostras de sinal (mais antiga primeiro), limitadas para não crescer
|
||||
/// sem fim numa captura longa.
|
||||
/// Recent RSSI history (oldest first, bounded ring) for sparkline rendering.
|
||||
///
|
||||
pub signal_history: Vec<SignalSample>,
|
||||
/// Linha do tempo curta: primeiro visto, probe, associação, desautenticação.
|
||||
/// Per-client event timeline (first seen, probe, association, deauthentication, etc.).
|
||||
pub events: Vec<ClientEvent>,
|
||||
}
|
||||
|
||||
@@ -102,15 +102,15 @@ pub struct ScanSnapshot {
|
||||
pub clients: Vec<Client>,
|
||||
pub handshakes: Vec<HandshakeInfo>,
|
||||
pub analysis: crate::analysis::Analysis,
|
||||
/// Preenchido quando a captura morreu sozinha (erro de I/O, adaptador
|
||||
/// caiu da USB, etc.) — `running` já vem `false` nesse caso, mas sem
|
||||
/// isso a UI não tinha como dizer *por quê* parou.
|
||||
/// Populated when the capture loop terminated due to an I/O error (e.g. unplugged
|
||||
/// USB adapter).
|
||||
///
|
||||
pub error: Option<String>,
|
||||
/// Preenchido quando a captura está rodando mas não recebeu nenhum
|
||||
/// quadro depois de um tempo razoável saltando canais — sintoma de uma
|
||||
/// placa que aceita ativar o modo monitor (o netlink não recusa) mas não
|
||||
/// entrega quadro nenhum de verdade. Comum em placas Wi-Fi internas de
|
||||
/// notebook; diferente de `error`, a sessão continua `running`.
|
||||
/// Populated when capture has been running for >12s across channels with zero
|
||||
/// received frames (typical of drivers/firmware that silently ignore monitor mode).
|
||||
///
|
||||
///
|
||||
///
|
||||
pub warning: Option<String>,
|
||||
}
|
||||
|
||||
@@ -140,8 +140,8 @@ impl ScanTable {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Processa um quadro radiotap+802.11 crú. Retorna `true` se o quadro
|
||||
/// continha EAPOL (mantido para chamadores antigos).
|
||||
/// Ingests a raw Radiotap + 802.11 frame. Returns `true` if the frame carried
|
||||
/// an EAPOL handshake message.
|
||||
pub fn ingest(&mut self, raw: &[u8]) -> bool {
|
||||
let now = unix_now();
|
||||
self.ingest_at(raw, now).eapol
|
||||
@@ -317,9 +317,9 @@ impl ScanTable {
|
||||
};
|
||||
if let (Some(client), Some(bssid)) = (client.clone(), bssid.clone()) {
|
||||
if client != bssid {
|
||||
// Calculado antes do touch_client: a TK vem de self.handshakes, e
|
||||
// o borrow checker não deixa consultar isso com `c` (borrow de
|
||||
// self.clients) vivo ao mesmo tempo.
|
||||
// Look up any derived CCMP Temporal Key (TK) before borrowing `self.clients`.
|
||||
//
|
||||
//
|
||||
let tk = (frame.protected && frame.to_ds && !frame.from_ds)
|
||||
.then(|| self.handshakes.tk_for(&bssid, &client))
|
||||
.flatten();
|
||||
@@ -327,9 +327,9 @@ impl ScanTable {
|
||||
c.packets += 1;
|
||||
if frame.to_ds && !frame.from_ds {
|
||||
if frame.protected {
|
||||
// Só decifra se já quebramos a senha e vimos o handshake
|
||||
// DESSE cliente — a PTK é por associação, então a senha da
|
||||
// rede sozinha não abre tráfego de quem não foi capturado.
|
||||
// Decrypt CCMP data frames when we have derived this station's association TK.
|
||||
//
|
||||
//
|
||||
if let Some(plain) = tk.and_then(|tk| frame.ccmp_payload(&tk)) {
|
||||
let payload = dot11::payload_info_from_llc(&plain);
|
||||
apply_payload_identity(c, &payload);
|
||||
@@ -339,9 +339,9 @@ impl ScanTable {
|
||||
apply_payload_identity(c, &payload);
|
||||
}
|
||||
}
|
||||
// Sempre recalcula o nome de exibição, mesmo em quadros AP→estação
|
||||
// (downlink): sem isso, um cliente só visto recebendo dados nunca
|
||||
// ganhava o fallback para o fabricante (OUI), ficando só com o MAC.
|
||||
// Refresh `display_name` on both uplink and downlink frames so OUI vendor
|
||||
// fallback is populated even for receive-only stations.
|
||||
//
|
||||
refresh_identity(c);
|
||||
let was = c.associated;
|
||||
c.bssid = Some(bssid.clone());
|
||||
@@ -462,9 +462,9 @@ impl ScanTable {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Já dá o fallback pro fabricante (OUI) desde o primeiro avistamento,
|
||||
// antes de qualquer quadro de gerenciamento/dados chegar a chamar
|
||||
// refresh_identity de novo.
|
||||
// Populate `display_name` from the OUI vendor immediately on first sighting.
|
||||
//
|
||||
//
|
||||
if c.display_name.is_none() {
|
||||
refresh_identity(c);
|
||||
}
|
||||
@@ -507,7 +507,7 @@ impl ScanTable {
|
||||
self.handshakes.get(bssid, station)
|
||||
}
|
||||
|
||||
/// Handshakes completos (com quadros crus) para exportação.
|
||||
/// Returns all tracked handshakes (including raw frames for `.pcap`/`.hc22000` export).
|
||||
pub fn handshake_entries(&self) -> Vec<Handshake> {
|
||||
self.handshakes.all().into_iter().cloned().collect()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Helpers de teste: quadros 802.11/EAPOL sintéticos, radiotap e handshakes
|
||||
//! válidos gerados a partir de uma senha conhecida. Só existe em `cfg(test)`.
|
||||
//! Unit-test helpers: synthetic Radiotap/802.11/EAPOL frame builders and
|
||||
//! valid 4-way handshake generators for a known passphrase (`#[cfg(test)]`).
|
||||
|
||||
use crate::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk};
|
||||
use crate::dot11;
|
||||
@@ -46,11 +46,11 @@ pub fn frame_with_eapol(
|
||||
f.extend_from_slice(&fc.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
if from_ap {
|
||||
f.extend_from_slice(&sta); // addr1 = destino
|
||||
f.extend_from_slice(&sta); // addr1 = destination
|
||||
f.extend_from_slice(&ap); // addr2 = BSSID
|
||||
} else {
|
||||
f.extend_from_slice(&ap); // addr1 = BSSID
|
||||
f.extend_from_slice(&sta); // addr2 = origem
|
||||
f.extend_from_slice(&sta); // addr2 = source
|
||||
}
|
||||
f.extend_from_slice(&da);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
@@ -148,7 +148,7 @@ pub fn data_frame(
|
||||
f
|
||||
}
|
||||
|
||||
/// Gera (msg1, msg2) válidas para a senha informada, com MIC correto.
|
||||
/// Generates a valid `(msg1, msg2)` EAPOL-Key pair with a verified MIC for `passphrase`.
|
||||
pub fn handshake_frames(
|
||||
ssid: &str,
|
||||
password: &str,
|
||||
@@ -172,8 +172,8 @@ pub fn handshake_frames(
|
||||
(m1, m2)
|
||||
}
|
||||
|
||||
/// Monta um `Handshake` diretamente dos quadros EAPOL, para testes de quebra
|
||||
/// sem depender do pipeline de captura.
|
||||
/// Assembles a `Handshake` directly from synthetic EAPOL frames for cracking tests.
|
||||
///
|
||||
pub fn synthetic_handshake(
|
||||
ssid: &str,
|
||||
bssid: &str,
|
||||
@@ -207,7 +207,7 @@ pub fn synthetic_handshake(
|
||||
password: None,
|
||||
tk: None,
|
||||
};
|
||||
// Exercita o parser de MAC do alvo.
|
||||
// Exercises MAC address parsing.
|
||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||
assert!(dot11::parse_mac(&hs.station).is_some());
|
||||
hs
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Auto-atualização simples: verifica a última release no Gitea, baixa o
|
||||
//! binário certo para a plataforma atual, confere o checksum e troca o
|
||||
//! executável em uso — sem o usuário precisar baixar nada manualmente.
|
||||
//! CLI self-updater: queries the latest Gitea Release, downloads the matching
|
||||
//! platform archive, verifies its SHA-256 checksum, and atomically replaces the
|
||||
//! running `sabia` binary.
|
||||
//!
|
||||
//! Usa `curl` (quase universal em Linux/macOS) em vez de trazer uma stack
|
||||
//! HTTP/TLS inteira como dependência só para isso.
|
||||
//! Uses the system `curl` and `tar` binaries to avoid pulling a full TLS stack
|
||||
//! into `sabia-core`.
|
||||
|
||||
use crate::error::{Result, SabiaError};
|
||||
use serde::Deserialize;
|
||||
@@ -12,12 +12,12 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// A API do Gitea devolve o mesmo formato de `releases/latest` que o GitHub devolvia
|
||||
/// (`tag_name`, `assets[].name`, `assets[].browser_download_url`).
|
||||
/// Gitea `releases/latest` API endpoint (`tag_name`, `assets[].name`, `assets[].browser_download_url`).
|
||||
///
|
||||
const LATEST_RELEASE_URL: &str =
|
||||
"https://git.lucasrguerra.dev.br/api/v1/repos/lucasrguerra/sabia/releases/latest";
|
||||
|
||||
/// Página de downloads, para quem usa o app desktop.
|
||||
/// Human-facing Gitea Releases page for desktop GUI users.
|
||||
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia/releases";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -75,9 +75,9 @@ fn curl_download(url: &str, dest: &PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sufixo que identifica o pacote certo entre os assets da release — o
|
||||
/// nome completo inclui a tag (ex.: `sabia-v0.1.0-linux-x86_64.tar.gz`),
|
||||
/// então casamos pelo fim em vez do nome inteiro.
|
||||
/// Returns the expected release asset suffix for the current OS/architecture
|
||||
/// (e.g. `"-linux-x86_64.tar.gz"`).
|
||||
///
|
||||
fn asset_suffix_for_platform() -> String {
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
@@ -95,15 +95,15 @@ fn asset_suffix_for_platform() -> String {
|
||||
format!("-{os}-{arch}.{ext}")
|
||||
}
|
||||
|
||||
/// Consulta a última release publicada e compara com a versão atual
|
||||
/// (`CARGO_PKG_VERSION` do binário chamador).
|
||||
/// Queries the latest published release and compares it against the running
|
||||
/// `CARGO_PKG_VERSION`.
|
||||
pub fn check(current_version: &str) -> Result<UpdateStatus> {
|
||||
let url = LATEST_RELEASE_URL;
|
||||
let body = match curl_text(url) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
// Sem rede, ou repositório ainda sem releases publicadas: não é
|
||||
// um erro fatal, só significa "sem atualização disponível".
|
||||
// Offline or no published releases yet: treat as no update available.
|
||||
//
|
||||
return Ok(UpdateStatus {
|
||||
current_version: current_version.to_string(),
|
||||
latest_version: None,
|
||||
@@ -121,12 +121,12 @@ pub fn check(current_version: &str) -> Result<UpdateStatus> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Baixa e aplica a atualização mais recente, substituindo o binário atual.
|
||||
/// Retorna a versão instalada.
|
||||
/// Downloads, verifies, and installs the latest CLI release in place.
|
||||
/// Returns the newly installed version string.
|
||||
pub fn apply(current_exe: &PathBuf) -> Result<String> {
|
||||
// O pacote da release contém o CLI (`sabia`). Trocar por ele um executável que
|
||||
// não seja o CLI, como o `sabia-gui`, o deixaria como um CLI e quebraria o app
|
||||
// na próxima abertura. O app desktop se atualiza baixando o instalador.
|
||||
// Guard against replacing `sabia-gui` with the CLI binary (`sabia`);
|
||||
// desktop GUI updates use the Tauri updater / installer instead.
|
||||
//
|
||||
let is_cli = current_exe
|
||||
.file_stem()
|
||||
.map(|stem| stem == "sabia")
|
||||
@@ -183,10 +183,10 @@ pub fn apply(current_exe: &PathBuf) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Extrai o pacote (tar.gz no Linux/macOS, zip no Windows — `tar xf` sem
|
||||
// flag de compressão detecta os dois formatos nos `tar` modernos,
|
||||
// incluindo o bsdtar que o Windows já traz por padrão) e substitui o
|
||||
// binário atual atomicamente.
|
||||
// Extract the archive (`tar xf` auto-detects `.tar.gz` and `.zip` on modern
|
||||
// libarchive/bsdtar) and atomically replace the current executable.
|
||||
//
|
||||
//
|
||||
let extract_dir = tmp_dir.join("extracted");
|
||||
let _ = fs::remove_dir_all(&extract_dir);
|
||||
fs::create_dir_all(&extract_dir)?;
|
||||
@@ -219,9 +219,9 @@ pub fn apply(current_exe: &PathBuf) -> Result<String> {
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
/// O binário vem dentro de uma subpasta (o CI empacota
|
||||
/// `sabia-vX.Y.Z-<plataforma>/sabia`), então procura até um nível de
|
||||
/// profundidade abaixo do diretório extraído.
|
||||
/// Locates `name` at the archive root or one directory level below
|
||||
/// (`sabia-vX.Y.Z-<platform>/sabia`).
|
||||
///
|
||||
fn find_binary(dir: &PathBuf) -> Result<PathBuf> {
|
||||
fn is_binary(name: &std::ffi::OsStr) -> bool {
|
||||
name == "sabia" || name == "sabia.exe"
|
||||
@@ -252,7 +252,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn apply_refuses_to_overwrite_the_desktop_app() {
|
||||
// Falha antes de qualquer acesso à rede: o nome do executável já não é o do CLI.
|
||||
// Rejects non-CLI binary names before making any network requests.
|
||||
let err = apply(&PathBuf::from("/opt/sabia/sabia-gui")).unwrap_err();
|
||||
assert!(err.to_string().contains("só para o CLI"), "{err}");
|
||||
let err = apply(&PathBuf::from("C:/Program Files/Sabia/sabia-gui.exe")).unwrap_err();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Ataque de PIN WPS com o `reaver` embutido.
|
||||
//! WPS PIN / Pixie Dust auditing via bundled `reaver` (or system `bully`).
|
||||
//!
|
||||
//! Extracts the bundled `reaver` binary at runtime (see
|
||||
//! `crates/sabia-core/assets/tools/LICENSE.reaver`) or uses a system
|
||||
//! `reaver`/`bully` installation when present.
|
||||
//!
|
||||
//!
|
||||
//! O protocolo EAP-WSC é grande demais para reimplementar aqui; o Sabiá
|
||||
//! carrega uma cópia do reaver (GPL-2, veja `crates/sabia-core/assets/tools/LICENSE.reaver`),
|
||||
//! extrai em tempo de execução e mostra a saída ao vivo. Assim o WPS já
|
||||
//! funciona sem instalar nada. Se o usuário tiver um `bully` no PATH, ele
|
||||
//! também aparece como alternativa.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
@@ -19,9 +19,9 @@ pub struct WpsTool {
|
||||
#[cfg(target_os = "linux")]
|
||||
const BUNDLED_REAVER: &[u8] = include_bytes!("../assets/tools/reaver");
|
||||
|
||||
/// Procura um binário no `PATH` — usado pelo WPS (reaver/bully) e, fora
|
||||
/// deste módulo, pelo Evil Twin (hostapd/dnsmasq), que precisa da mesma
|
||||
/// checagem sem puxar uma dependência nova só pra isso.
|
||||
/// Searches `PATH` (plus standard sbin directories) for an executable `name`.
|
||||
///
|
||||
///
|
||||
pub(crate) fn find_in_path(binary: &str) -> Option<PathBuf> {
|
||||
let path = std::env::var_os("PATH")?;
|
||||
for dir in std::env::split_paths(&path) {
|
||||
@@ -72,8 +72,8 @@ fn bundled_reaver() -> Option<PathBuf> {
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// `temp_dir` é compartilhado; usa um diretório por UID e confere que ele
|
||||
// não é um symlink antes de gravar, para não seguir link de outro usuário.
|
||||
// Extract into a per-UID private directory and verify it is not a symlink.
|
||||
//
|
||||
let uid = unsafe { libc::geteuid() };
|
||||
let dir = std::env::temp_dir().join(format!("sabia-tools-{uid}"));
|
||||
if let Ok(meta) = fs::symlink_metadata(&dir) {
|
||||
@@ -104,8 +104,8 @@ fn bundled_reaver() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Argumentos para uma execução. Sem PIN, usa Pixie Dust (`-K 1`) no reaver
|
||||
/// e o ataque padrão no bully.
|
||||
/// Builds CLI arguments for `reaver` or `bully` (defaults to Pixie Dust `-K 1` when `pin` is `None`).
|
||||
///
|
||||
pub fn args(tool: &str, iface: &str, bssid: &str, channel: u32, pin: Option<&str>) -> Vec<String> {
|
||||
let channel = channel.to_string();
|
||||
match tool {
|
||||
@@ -151,7 +151,7 @@ pub fn args(tool: &str, iface: &str, bssid: &str, channel: u32, pin: Option<&str
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrai PIN/PSK de uma linha de saída do reaver/bully.
|
||||
/// Parses a recovered WPS PIN or WPA PSK from a `reaver`/`bully` output line.
|
||||
pub fn parse_line(line: &str) -> (Option<String>, Option<String>) {
|
||||
let lower = line.to_ascii_lowercase();
|
||||
let grab = |needle: &str| -> Option<String> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Integração: trilha de auditoria encadeada por hash — grava entradas,
|
||||
//! confirma que a cadeia valida, depois adultera um registro no disco e
|
||||
//! confirma que a verificação detecta a falha.
|
||||
//! Integration test: hash-chained audit trail — appends entries, verifies
|
||||
//! chain integrity, then tampers with a middle record on disk and confirms
|
||||
//! verification pinpoints the corrupted entry.
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -26,7 +26,7 @@ fn chain_validates_after_recording_entries() {
|
||||
assert_eq!(entries.len(), 3);
|
||||
assert_eq!(entries[0].action, "monitor-on");
|
||||
assert_eq!(entries[2].action, "deauth");
|
||||
// cadeia encadeada corretamente
|
||||
// each entry's `prev_hash` matches the preceding entry's `hash`
|
||||
assert_eq!(entries[1].prev_hash, entries[0].hash);
|
||||
assert_eq!(entries[2].prev_hash, entries[1].hash);
|
||||
|
||||
@@ -52,11 +52,11 @@ fn tampering_with_an_entry_breaks_verification() {
|
||||
.expect("record 3");
|
||||
}
|
||||
|
||||
// Confirma que, intacta, a cadeia passa.
|
||||
// Intact chain verifies cleanly.
|
||||
assert_eq!(AuditLog::verify(&path).expect("verify ok"), None);
|
||||
|
||||
// Adultera o "detail" da segunda entrada sem recalcular o hash,
|
||||
// simulando uma edição retroativa do log.
|
||||
// Tamper with the second entry's `detail` field without recomputing its hash.
|
||||
//
|
||||
let contents = std::fs::read_to_string(&path).expect("read");
|
||||
let mut lines: Vec<String> = contents.lines().map(|l| l.to_string()).collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#![allow(dead_code)]
|
||||
//! Helpers de teste de integração: quadros 802.11/EAPOL sintéticos,
|
||||
//! radiotap e handshakes válidos gerados a partir de uma senha conhecida.
|
||||
//! Integration-test helpers: synthetic Radiotap/802.11/EAPOL frames and
|
||||
//! valid 4-way handshakes generated from a known passphrase.
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! Porta a lógica de `crates/sabia-core/src/testutil.rs` (que só existe em
|
||||
//! `#[cfg(test)]` e por isso não fica visível aqui), usando só a API
|
||||
//! pública de `sabia_core`.
|
||||
|
||||
use sabia_core::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk};
|
||||
use sabia_core::dot11;
|
||||
@@ -58,11 +58,11 @@ pub fn frame_with_eapol(
|
||||
f.extend_from_slice(&fc.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
if from_ap {
|
||||
f.extend_from_slice(&sta); // addr1 = destino
|
||||
f.extend_from_slice(&sta); // addr1 = destination
|
||||
f.extend_from_slice(&ap); // addr2 = BSSID
|
||||
} else {
|
||||
f.extend_from_slice(&ap); // addr1 = BSSID
|
||||
f.extend_from_slice(&sta); // addr2 = origem
|
||||
f.extend_from_slice(&sta); // addr2 = source
|
||||
}
|
||||
f.extend_from_slice(&da);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
@@ -160,7 +160,7 @@ pub fn data_frame(
|
||||
f
|
||||
}
|
||||
|
||||
/// Gera (msg1, msg2) válidas para a senha informada, com MIC correto.
|
||||
/// Generates a valid `(msg1, msg2)` EAPOL-Key pair with a verified MIC for `passphrase`.
|
||||
pub fn handshake_frames(
|
||||
ssid: &str,
|
||||
password: &str,
|
||||
@@ -184,8 +184,8 @@ pub fn handshake_frames(
|
||||
(m1, m2)
|
||||
}
|
||||
|
||||
/// Monta quadros radiotap+802.11 completos (prontos para `ScanTable::ingest`)
|
||||
/// com o 4-way handshake entre `AP` e `STA` para o SSID/senha dados.
|
||||
/// Builds complete Radiotap + 802.11 frames (ready for `ScanTable::ingest`)
|
||||
/// carrying the 4-way handshake between `AP` and `STA` for `ssid`/`passphrase`.
|
||||
pub fn handshake_radiotap_frames(
|
||||
ssid: &str,
|
||||
password: &str,
|
||||
@@ -198,8 +198,8 @@ pub fn handshake_radiotap_frames(
|
||||
(radiotap_wrap(&f1, 2437, -40), radiotap_wrap(&f2, 2437, -42))
|
||||
}
|
||||
|
||||
/// Monta um `Handshake` diretamente dos quadros EAPOL, para testes de quebra
|
||||
/// sem depender do pipeline de captura.
|
||||
/// Assembles a `Handshake` directly from synthetic EAPOL frames for cracking tests.
|
||||
///
|
||||
pub fn synthetic_handshake(
|
||||
ssid: &str,
|
||||
bssid: &str,
|
||||
@@ -233,7 +233,7 @@ pub fn synthetic_handshake(
|
||||
password: None,
|
||||
tk: None,
|
||||
};
|
||||
// Exercita o parser de MAC do alvo.
|
||||
// Exercises MAC address parsing.
|
||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||
assert!(dot11::parse_mac(&hs.station).is_some());
|
||||
hs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Integração: handshake sintético (gerado com senha conhecida) -> montagem
|
||||
//! de `CrackTarget` -> `verify()`, para os dois métodos (EAPOL 4-way e
|
||||
//! PMKID), cobrindo senha certa e errada.
|
||||
//! Integration test: synthetic handshake -> `CrackTarget` -> `verify()` and
|
||||
//! dictionary cracking across both EAPOL 4-way and PMKID modes.
|
||||
//!
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -45,7 +45,7 @@ fn tkip_keyversion_roundtrip_verifies_password() {
|
||||
#[test]
|
||||
fn incomplete_handshake_without_pmkid_or_msg2_has_no_target() {
|
||||
let (m1, _m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, false);
|
||||
// Sem msg2 e sem PMKID: não há prova suficiente para quebrar.
|
||||
// Without message 2 or a PMKID, the handshake is not yet crackable.
|
||||
let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &[]);
|
||||
assert!(CrackTarget::from_handshake(&hs).is_none());
|
||||
}
|
||||
@@ -60,17 +60,17 @@ fn derive_pmk_compute_ptk_and_mic_are_consistent() {
|
||||
let pmk = derive_pmk(password, ssid);
|
||||
let ptk = compute_ptk(&pmk, &common::ap(), &common::sta(), &anonce, &snonce);
|
||||
|
||||
// MIC calculado sobre um quadro msg2 qualquer, zerando o campo MIC antes.
|
||||
// Compute the MIC over a message 2 frame with its MIC field zeroed.
|
||||
let mut m2 = common::key_frame(2, 2, snonce, &[]);
|
||||
let mic = compute_mic(2, &ptk[..16], &m2, sabia_core::eapol::MIC_OFFSET).expect("mic");
|
||||
m2[sabia_core::eapol::MIC_OFFSET..sabia_core::eapol::MIC_OFFSET + 16].copy_from_slice(&mic);
|
||||
|
||||
// Recalculando o MIC sobre o quadro já preenchido deve bater de novo
|
||||
// (compute_mic zera o campo internamente antes de recalcular).
|
||||
// Recomputing the MIC over the already-signed frame must yield the same digest
|
||||
// (`compute_mic` zeroes the MIC slice before hashing).
|
||||
let mic_again = compute_mic(2, &ptk[..16], &m2, sabia_core::eapol::MIC_OFFSET).expect("mic");
|
||||
assert_eq!(mic, mic_again);
|
||||
|
||||
// PMKID direto também deve ser determinístico e usável no verify().
|
||||
// Direct PMKID computation is deterministic and verifies via `CrackTarget::Pmkid`.
|
||||
let pmkid = compute_pmkid(&pmk, &common::ap(), &common::sta());
|
||||
assert_eq!(
|
||||
pmkid,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Integração: grava quadros com `PcapWriter`, lê de volta com
|
||||
//! `PcapReader` e confere que batem byte a byte; depois confirma que
|
||||
//! `Engine::analyze_pcap` encontra redes e handshakes no arquivo recém-escrito.
|
||||
//! Integration test: writes frames with `PcapWriter`, reads them back with
|
||||
//! `PcapReader`, and verifies `Engine::analyze_pcap` reconstructs the AP and handshake.
|
||||
//!
|
||||
|
||||
mod common;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Integração: beacon + probe + handshake completo passando pelo caminho
|
||||
//! público de agregação (`ScanTable`), verificando que AP, cliente e
|
||||
//! handshake são detectados de ponta a ponta.
|
||||
//! End-to-end integration test: ingests beacon, probe, and 4-way handshake
|
||||
//! frames into `ScanTable` and verifies AP, client, handshake, and live CCMP
|
||||
//! data frame decryption.
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -10,7 +10,7 @@ use sabia_core::scan::ScanTable;
|
||||
fn detects_ap_client_and_handshake_end_to_end() {
|
||||
let mut table = ScanTable::new();
|
||||
|
||||
// RSN com AKM PSK (2) para que a rede apareça como WPA2-PSK.
|
||||
// Include an RSN IE with PSK AKM (2) so the AP is classified as WPA2-PSK.
|
||||
let beacon = common::beacon_frame(common::ap(), "RedeTeste", 6, &[2]);
|
||||
table.ingest(&common::radiotap_wrap(&beacon, 2437, -35));
|
||||
|
||||
@@ -58,7 +58,7 @@ fn detects_ap_client_and_handshake_end_to_end() {
|
||||
fn hidden_ssid_beacon_is_marked_hidden() {
|
||||
let mut table = ScanTable::new();
|
||||
|
||||
// SSID vazio = rede oculta.
|
||||
// Empty 0-length SSID IE = hidden network.
|
||||
let beacon = common::beacon_frame(common::ap(), "", 11, &[2]);
|
||||
table.ingest(&common::radiotap_wrap(&beacon, 2462, -60));
|
||||
|
||||
@@ -67,11 +67,11 @@ fn hidden_ssid_beacon_is_marked_hidden() {
|
||||
assert!(aps[0].hidden);
|
||||
}
|
||||
|
||||
/// Depois que a senha de uma rede WPA2/CCMP é conhecida (quebrada), o
|
||||
/// tráfego de dados do cliente cujo handshake foi capturado passa a ser
|
||||
/// decifrado em tempo real pelo pipeline de ingestão — e a identidade
|
||||
/// (aqui, o IP de origem, que exercita o parser de IPv4 por trás do LLC)
|
||||
/// aparece mesmo com o bit "Protected" ligado.
|
||||
/// Once a WPA2/CCMP handshake has been cracked, subsequent protected QoS Data
|
||||
/// frames from that station are decrypted on the fly using its derived TK to
|
||||
/// populate client identity fields (such as its IPv4 address).
|
||||
///
|
||||
///
|
||||
#[test]
|
||||
fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
||||
use sabia_core::crack::{compute_ptk, derive_pmk};
|
||||
@@ -91,8 +91,8 @@ fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
||||
let station_str = sabia_core::dot11::mac_to_string(&common::sta());
|
||||
table.set_password(&bssid_str, &station_str, password);
|
||||
|
||||
// A TK derivada pelo pipeline tem que bater com a calculada à parte, com
|
||||
// os mesmos nonces fixos (0xaa/0xbb) que handshake_radiotap_frames usa.
|
||||
// The pipeline-derived TK must match the TK computed directly from the same nonces.
|
||||
//
|
||||
let anonce = [0xaa; 32];
|
||||
let snonce = [0xbb; 32];
|
||||
let pmk = derive_pmk(password, ssid);
|
||||
@@ -107,16 +107,16 @@ fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
||||
"TK deveria ser derivada ao quebrar a senha"
|
||||
);
|
||||
|
||||
// Monta um quadro QoS Data, STA->AP, protegido, com CCMP header (PN=1,
|
||||
// ExtIV ligado) e cifra um payload LLC/SNAP+IPv4 "na mão" — o CTR do
|
||||
// CCMP é simétrico, então usar decrypt_data pra cifrar é válido aqui.
|
||||
// Build a protected STA->AP QoS Data frame (CCMP PN=1, ExtIV=1) carrying an
|
||||
// encrypted LLC/SNAP + IPv4 packet.
|
||||
//
|
||||
let llc_ipv4 = {
|
||||
let mut v = vec![0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00];
|
||||
let mut ip = vec![
|
||||
0x45, 0x00, 0x00, 20, 0x00, 0x00, 0x00, 0x00, 64, 17, 0x00, 0x00,
|
||||
];
|
||||
ip.extend_from_slice(&[192, 168, 1, 50]); // origem privada
|
||||
ip.extend_from_slice(&[192, 168, 1, 1]); // destino
|
||||
ip.extend_from_slice(&[192, 168, 1, 50]); // private source IPv4
|
||||
ip.extend_from_slice(&[192, 168, 1, 1]); // destination IPv4
|
||||
v.extend_from_slice(&ip);
|
||||
v
|
||||
};
|
||||
@@ -124,22 +124,22 @@ fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
||||
let mut to_cipher = Vec::new();
|
||||
to_cipher.extend_from_slice(&ccmp_header);
|
||||
to_cipher.extend_from_slice(&llc_ipv4);
|
||||
to_cipher.extend_from_slice(&[0u8; 8]); // placeholder pro MIC, ignorado
|
||||
to_cipher.extend_from_slice(&[0u8; 8]); // 8-byte MIC placeholder
|
||||
let ciphertext = sabia_core::ccmp::decrypt_data(&expected_tk, &common::sta(), 0, &to_cipher)
|
||||
.expect("cifra via CTR simétrico");
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(&ccmp_header);
|
||||
body.extend_from_slice(&ciphertext);
|
||||
body.extend_from_slice(&[0u8; 8]); // MIC cifrado — não conferido na decriptação
|
||||
body.extend_from_slice(&[0u8; 8]); // trailing 8-byte MIC
|
||||
|
||||
let mut frame = Vec::new();
|
||||
let fc: u16 = 0x0088 | 0x0100 | 0x4000; // QoS Data, to_ds, protected
|
||||
frame.extend_from_slice(&fc.to_le_bytes());
|
||||
frame.extend_from_slice(&0u16.to_le_bytes()); // duration
|
||||
frame.extend_from_slice(&common::ap()); // addr1 = BSSID
|
||||
frame.extend_from_slice(&common::sta()); // addr2 = transmissor (estação)
|
||||
frame.extend_from_slice(&common::ap()); // addr3 = destino (irrelevante aqui)
|
||||
frame.extend_from_slice(&common::sta()); // addr2 = transmitter (station)
|
||||
frame.extend_from_slice(&common::ap()); // addr3 = destination
|
||||
frame.extend_from_slice(&[0u8; 2]); // sequence control
|
||||
frame.extend_from_slice(&[0u8; 2]); // QoS control, TID 0
|
||||
frame.extend_from_slice(&body);
|
||||
@@ -163,11 +163,11 @@ fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
||||
fn pmkid_only_handshake_is_detected_and_crackable() {
|
||||
let mut table = ScanTable::new();
|
||||
|
||||
// Beacon com SSID visível, para que o handshake herde o SSID via hint.
|
||||
// Beacon with visible SSID so the PMKID handshake inherits the SSID hint.
|
||||
let beacon = common::beacon_frame(common::ap(), "RedePmkid", 11, &[2]);
|
||||
table.ingest_at(&common::radiotap_wrap(&beacon, 2462, -60), 1);
|
||||
|
||||
// Handshake via PMKID (só msg1, sem msg2/snonce).
|
||||
// PMKID-only handshake (message 1 with PMKID KDE, no message 2).
|
||||
let (m1, _m2) = common::handshake_frames("RedePmkid", "outra-senha", 2, true);
|
||||
let f1 = common::frame_with_eapol(true, common::ap(), common::sta(), common::sta(), &m1);
|
||||
let ev = table.ingest_at(&common::radiotap_wrap(&f1, 2462, -55), 20);
|
||||
|
||||
Reference in new issue
Block a user