- eapol.rs: key_info/replay_counter/key_data_len liam little-endian; são big-endian (802.1X segue ordem de rede), então handshakes reais nunca fechavam mesmo com deauth funcionando. testutil.rs e os geradores de quadro de teste corrigidos junto, mais um teste contra bytes de wire reais (não só contra o próprio encoder). - inject.rs: deauth/disassoc agora grava Sequence Control crescente por quadro — reenviar o mesmo quadro era descartado como duplicata pelo driver/AP depois do primeiro envio. - dot11.rs/scan.rs: parsing de AP/cliente bem mais completo (PMF obrigatório, WPS detalhado, fabricante por OUI, país, largura de banda, padrão PHY, privacidade) e histórico de sinal/eventos por cliente. - handshake.rs/ccmp.rs: deriva a Temporal Key (CCMP) quando a senha é quebrada, o suficiente pra decifrar o tráfego em claro do par (BSSID, estação) cujo handshake foi capturado.
180 lines
7.0 KiB
Rust
180 lines
7.0 KiB
Rust
//! 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.
|
|
|
|
mod common;
|
|
|
|
use sabia_core::scan::ScanTable;
|
|
|
|
#[test]
|
|
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.
|
|
let beacon = common::beacon_frame(common::ap(), "RedeTeste", 6, &[2]);
|
|
table.ingest(&common::radiotap_wrap(&beacon, 2437, -35));
|
|
|
|
let probe = common::probe_request(common::sta(), "RedeTeste");
|
|
table.ingest(&common::radiotap_wrap(&probe, 2437, -50));
|
|
|
|
let (f1, f2) = common::handshake_radiotap_frames("RedeTeste", "senha-correta", 2, false);
|
|
let ev1 = table.ingest_at(&f1, 10);
|
|
assert!(ev1.eapol, "quadro msg1 deveria ser reconhecido como EAPOL");
|
|
let ev2 = table.ingest_at(&f2, 11);
|
|
assert!(ev2.eapol, "quadro msg2 deveria ser reconhecido como EAPOL");
|
|
assert!(
|
|
ev2.handshake_updated,
|
|
"msg2 deveria completar/atualizar o handshake"
|
|
);
|
|
|
|
let aps = table.access_points();
|
|
assert_eq!(aps.len(), 1);
|
|
let ap = &aps[0];
|
|
assert_eq!(ap.bssid, sabia_core::dot11::mac_to_string(&common::ap()));
|
|
assert_eq!(ap.ssid.as_deref(), Some("RedeTeste"));
|
|
assert_eq!(ap.channel, Some(6));
|
|
assert_eq!(ap.security.as_deref(), Some("WPA2"));
|
|
|
|
let clients = table.clients();
|
|
assert!(clients
|
|
.iter()
|
|
.any(|c| c.mac == sabia_core::dot11::mac_to_string(&common::sta())));
|
|
|
|
let handshakes = table.handshakes();
|
|
assert_eq!(handshakes.len(), 1);
|
|
let hs = &handshakes[0];
|
|
assert_eq!(hs.ssid.as_deref(), Some("RedeTeste"));
|
|
assert!(hs.complete, "handshake deveria estar completo (msg1+msg2)");
|
|
assert!(hs.crackable, "handshake deveria ser quebrável");
|
|
|
|
let stats = table.stats();
|
|
assert_eq!(stats.aps, 1);
|
|
assert_eq!(stats.clients, 1);
|
|
assert_eq!(stats.handshakes, 1);
|
|
assert!(stats.eapol_frames >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn hidden_ssid_beacon_is_marked_hidden() {
|
|
let mut table = ScanTable::new();
|
|
|
|
// SSID vazio = rede oculta.
|
|
let beacon = common::beacon_frame(common::ap(), "", 11, &[2]);
|
|
table.ingest(&common::radiotap_wrap(&beacon, 2462, -60));
|
|
|
|
let aps = table.access_points();
|
|
assert_eq!(aps.len(), 1);
|
|
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.
|
|
#[test]
|
|
fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
|
use sabia_core::crack::{compute_ptk, derive_pmk};
|
|
|
|
let mut table = ScanTable::new();
|
|
let ssid = "RedeCifrada";
|
|
let password = "senha-correta";
|
|
|
|
let beacon = common::beacon_frame(common::ap(), ssid, 6, &[2]);
|
|
table.ingest(&common::radiotap_wrap(&beacon, 2437, -35));
|
|
|
|
let (f1, f2) = common::handshake_radiotap_frames(ssid, password, 2, false);
|
|
table.ingest(&f1);
|
|
table.ingest(&f2);
|
|
|
|
let bssid_str = sabia_core::dot11::mac_to_string(&common::ap());
|
|
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.
|
|
let anonce = [0xaa; 32];
|
|
let snonce = [0xbb; 32];
|
|
let pmk = derive_pmk(password, ssid);
|
|
let ptk = compute_ptk(&pmk, &common::ap(), &common::sta(), &anonce, &snonce);
|
|
let expected_tk: [u8; 16] = ptk[32..48].try_into().unwrap();
|
|
let hs = table
|
|
.handshake(&bssid_str, &station_str)
|
|
.expect("handshake presente");
|
|
assert_eq!(
|
|
hs.tk,
|
|
Some(expected_tk),
|
|
"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.
|
|
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
|
|
v.extend_from_slice(&ip);
|
|
v
|
|
};
|
|
let ccmp_header = [0x01u8, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00]; // PN=1, ExtIV
|
|
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
|
|
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
|
|
|
|
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(&[0u8; 2]); // sequence control
|
|
frame.extend_from_slice(&[0u8; 2]); // QoS control, TID 0
|
|
frame.extend_from_slice(&body);
|
|
|
|
let ev = table.ingest_at(&common::radiotap_wrap(&frame, 2437, -45), 30);
|
|
assert!(!ev.eapol, "não é um quadro EAPOL");
|
|
|
|
let clients = table.clients();
|
|
let client = clients
|
|
.iter()
|
|
.find(|c| c.mac == station_str)
|
|
.expect("cliente deveria ter sido visto");
|
|
assert_eq!(
|
|
client.ip.as_deref(),
|
|
Some("192.168.1.50"),
|
|
"o IP só aparece se o CCMP foi decifrado corretamente"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
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.
|
|
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).
|
|
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);
|
|
assert!(ev.eapol);
|
|
|
|
let handshakes = table.handshakes();
|
|
assert_eq!(handshakes.len(), 1);
|
|
assert!(handshakes[0].crackable, "PMKID isolado já é quebrável");
|
|
}
|