fix: captura de handshake real (endianness EAPOL) e enriquece o parsing 802.11
- 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.
This commit is contained in:
1 parent
dab86d1fa7
commit
5477b8fb03
10 files changed
+2523
-19
No files matched your search
@@ -37,11 +37,11 @@ pub fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> V
|
||||
f[1] = EAPOL_TYPE_KEY;
|
||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||
f[4] = 2;
|
||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
||||
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||
f[17..49].copy_from_slice(&nonce);
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes());
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_be_bytes());
|
||||
f[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||
f
|
||||
}
|
||||
@@ -231,6 +231,7 @@ pub fn synthetic_handshake(
|
||||
first_seen_unix: 1,
|
||||
last_seen_unix: 2,
|
||||
password: None,
|
||||
tk: None,
|
||||
};
|
||||
// Exercita o parser de MAC do alvo.
|
||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||
|
||||
@@ -67,6 +67,98 @@ 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.
|
||||
#[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();
|
||||
|
||||
Reference in new issue
Block a user