- 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.
174 lines
6.8 KiB
Rust
174 lines
6.8 KiB
Rust
//! 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());
|
|
}
|
|
}
|