use std::io::Write;
use std::path::Path;
use std::sync::atomic::Ordering;
use std::time::Instant;
use chrono::Local;
use crate::debug::{dbg_enabled, dbg_hex, dbg_log};
use crate::parse::{address_offsets, data_body_offset};
use crate::types::*;
pub fn extract_replay_counter(eapol_payload: &[u8]) -> Option<u64> {
if eapol_payload.len() < 17 { return None; }
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&eapol_payload[9..17]);
Some(u64::from_be_bytes(bytes))
}
pub fn identify_eapol_message(eapol_payload: &[u8]) -> Option<u8> {
if eapol_payload.len() < 7 { return None; }
if eapol_payload[1] != 3 { return None; }
let key_info = u16::from_be_bytes([eapol_payload[5], eapol_payload[6]]);
let install = (key_info & 0x0040) != 0;
let key_ack = (key_info & 0x0080) != 0;
let key_mic = (key_info & 0x0100) != 0;
let secure = (key_info & 0x0200) != 0;
if key_ack && !key_mic { Some(1) }
else if !key_ack && key_mic && !install && !secure { Some(2) }
else if key_ack && key_mic && install && secure { Some(3) }
else if !key_ack && key_mic && !install && secure { Some(4) }
else { None }
}
pub fn extract_pmkid(eapol_payload: &[u8]) -> Option<Vec<u8>> {
if eapol_payload.len() < 101 { return None; }
let key_data_len = u16::from_be_bytes([eapol_payload[97], eapol_payload[98]]) as usize;
if key_data_len == 0 { return None; }
let key_data_start = 99;
if key_data_start + key_data_len > eapol_payload.len() { return None; }
let key_data = &eapol_payload[key_data_start..key_data_start + key_data_len];
let mut pos = 0;
while pos + 2 < key_data.len() {
let kde_type = key_data[pos];
let kde_len = key_data[pos + 1] as usize;
if pos + 2 + kde_len > key_data.len() { break; }
if kde_type == 0xdd && kde_len >= 20 {
let oui = &key_data[pos + 2..pos + 6];
if oui == [0x00, 0x0f, 0xac, 0x04] {
let pmkid = key_data[pos + 6..pos + 22].to_vec();
if pmkid.iter().any(|&b| b != 0) {
return Some(pmkid);
}
}
}
pos += 2 + kde_len;
}
None
}
pub fn find_eapol_in_data_body(body: &[u8]) -> Option<&[u8]> {
if body.len() < 12 { return None; }
if body[0] == 0xAA && body[1] == 0xAA && body[2] == 0x03
&& body[3] == 0x00 && body[4] == 0x00 && body[5] == 0x00
&& body[6] == 0x88 && body[7] == 0x8E
{
return Some(&body[8..]);
}
if body.len() >= 16
&& body[0] == 0xAA && body[1] == 0xAA && body[2] == 0x03
&& body[3] == 0x00 && body[4] == 0x00 && body[5] == 0x00
&& body[6] == 0x81 && body[7] == 0x00
&& body[10] == 0x88 && body[11] == 0x8E
{
return Some(&body[12..]);
}
None
}
#[allow(clippy::too_many_arguments)]
pub fn check_eapol(
raw: &[u8],
frame_bytes: &[u8],
bssid: &str,
src: &Option<String>,
dst: &str,
body_off: usize,
ap_map: &ApMap,
handshake_map: &HandshakeMap,
handshake_count: &HandshakeCount,
handshake_dir: &Path,
hs_timeout: u64,
) {
FRAMES_EAPOL_CHECKED.fetch_add(1, Ordering::Relaxed);
let body = &frame_bytes[body_off..];
let eapol_data = match find_eapol_in_data_body(body) {
Some(e) => e,
None => return,
};
FRAMES_EAPOL_FOUND.fetch_add(1, Ordering::Relaxed);
dbg_log(&format!("** EAPOL FOUND: bssid={} src={:?} dst={} body_off={} eapol_len={}",
bssid, src, dst, body_off, eapol_data.len()));
if dbg_enabled() {
dbg_hex(" LLC/SNAP + EAPOL body", body, 48);
dbg_hex(" EAPOL payload", eapol_data, 48);
}
let msg_num = match identify_eapol_message(eapol_data) {
Some(n) => n,
None => {
dbg_log(" EAPOL found but message type not identified");
if eapol_data.len() >= 7 {
let key_info = u16::from_be_bytes([eapol_data[5], eapol_data[6]]);
dbg_log(&format!(" version={} type={} key_info=0x{:04x}",
eapol_data[0], eapol_data[1], key_info));
}
return;
}
};
FRAMES_EAPOL_MSG[(msg_num - 1) as usize].fetch_add(1, Ordering::Relaxed);
let replay_counter = extract_replay_counter(eapol_data);
dbg_log(&format!("** EAPOL M{}: bssid={} src={:?} dst={} rc={:?}",
msg_num, bssid, src, dst, replay_counter));
let client = match msg_num {
1 | 3 => {
let c = dst.to_string();
if c == bssid { src.as_ref().unwrap_or(&String::new()).to_string() } else { c }
}
2 | 4 => {
let c = src.as_ref().unwrap_or(&String::new()).to_string();
if c.is_empty() || c == bssid { dst.to_string() } else { c }
}
_ => return,
};
if client.is_empty() || client == bssid {
dbg_log(&format!(" Skipping: client='{}' same as bssid", client));
return;
}
let key = HandshakeKey {
bssid: bssid.to_string(),
client: client.clone(),
};
let essid = {
let map = ap_map.lock().unwrap();
map.get(bssid).map(|ap| ap.essid.clone()).unwrap_or_default()
};
let mut hs_map = handshake_map.lock().unwrap();
let hs = hs_map
.entry(key.clone())
.or_insert_with(|| HandshakeState::new(key.clone(), essid.clone()));
if !essid.is_empty() && essid != "<hidden>" {
hs.essid = essid;
}
if msg_num == 1 {
dbg_log(&format!(" M1 resets handshake for {}<->{}", bssid, client));
hs.messages = [None, None, None, None];
hs.replay_counters = [None; 4];
hs.started = Instant::now();
hs.saved = false;
hs.pmkid = None;
if let Some(pmkid) = extract_pmkid(eapol_data) {
dbg_log(&format!(" PMKID found in M1: {:02x?}", &pmkid[..4]));
hs.pmkid = Some(pmkid);
let mut map = ap_map.lock().unwrap();
if let Some(ap) = map.get_mut(bssid) {
ap.pmkid_captured = true;
}
}
}
if hs.started.elapsed().as_secs() > hs_timeout && msg_num != 1 {
dbg_log(&format!(" Stale ({}s), resetting", hs.started.elapsed().as_secs()));
hs.messages = [None, None, None, None];
hs.replay_counters = [None; 4];
hs.started = Instant::now();
hs.saved = false;
hs.pmkid = None;
}
hs.messages[(msg_num - 1) as usize] = Some(raw.to_vec());
hs.replay_counters[(msg_num - 1) as usize] = replay_counter;
let state_str = format!("M1={} M2={} M3={} M4={} rc={:?} complete={} saved={}",
hs.messages[0].is_some(), hs.messages[1].is_some(),
hs.messages[2].is_some(), hs.messages[3].is_some(),
hs.replay_counters, hs.is_complete(), hs.saved);
dbg_log(&format!(" HS state: {}", state_str));
if !hs.saved && hs.is_complete() {
dbg_log(&format!("** HANDSHAKE COMPLETE: {} <-> {} ({})", bssid, client, hs.essid));
hs.saved = true;
let hs_clone = hs.clone();
let dir = handshake_dir.to_path_buf();
let bssid_str = bssid.to_string();
let ap_map_clone = ap_map.clone();
let hs_count = handshake_count.clone();
std::thread::spawn(move || {
match save_handshake(&hs_clone, &dir) {
Ok(path) => dbg_log(&format!(" Handshake pcap saved: {}", path)),
Err(e) => {
dbg_log(&format!(" ERROR saving handshake: {}", e));
eprintln!("[-] Failed to save handshake: {}", e);
}
}
let mut map = ap_map_clone.lock().unwrap();
if let Some(ap) = map.get_mut(&bssid_str) {
ap.handshakes += 1;
}
let mut count = hs_count.lock().unwrap();
*count += 1;
});
}
}
#[allow(clippy::too_many_arguments)]
pub fn handle_data_frame(
raw: &[u8],
frame_bytes: &[u8],
bssid_str: &str,
src: Option<String>,
dst: String,
now: Instant,
signal_dbm: i8,
ap_map: &ApMap,
handshake_map: &HandshakeMap,
handshake_count: &HandshakeCount,
handshake_dir: &Path,
capture_handshakes: bool,
hs_timeout: u64,
) {
{
let mut map = ap_map.lock().unwrap();
let ap = map.entry(bssid_str.to_string()).or_insert_with(|| {
let mac = MacAddr::from_str_hex(bssid_str).unwrap_or(MacAddr::ZERO);
let mut ap = AccessPoint::new(mac);
ap.vendor = crate::oui::lookup_vendor(bssid_str);
ap
});
ap.data_count += 1;
ap.last_seen = now;
add_client(ap, &src, &dst, signal_dbm);
}
if capture_handshakes {
if frame_bytes.len() >= 2 {
let fc0 = frame_bytes[0];
let fc1 = frame_bytes[1];
let subtype = (fc0 >> 4) & 0x0F;
let protected = (fc1 & 0x40) != 0;
if protected {
FRAMES_PROTECTED_SKIP.fetch_add(1, Ordering::Relaxed);
return;
}
if subtype == 4 || subtype == 5 || subtype == 12 || subtype == 13 {
FRAMES_NULL_SKIP.fetch_add(1, Ordering::Relaxed);
return;
}
}
let body_off = data_body_offset(frame_bytes);
if body_off < frame_bytes.len() {
check_eapol(
raw, frame_bytes, bssid_str, &src, &dst, body_off,
ap_map, handshake_map, handshake_count, handshake_dir,
hs_timeout,
);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn fallback_data_frame_eapol(
raw: &[u8],
frame_bytes: &[u8],
ap_map: &ApMap,
handshake_map: &HandshakeMap,
handshake_count: &HandshakeCount,
handshake_dir: &Path,
hs_timeout: u64,
) {
if frame_bytes.len() < 26 { return; }
let fc0 = frame_bytes[0];
let fc1 = frame_bytes[1];
let subtype = (fc0 >> 4) & 0x0F;
let protected = (fc1 & 0x40) != 0;
if protected { return; }
if subtype == 4 || subtype == 5 || subtype == 12 || subtype == 13 { return; }
let (bssid_off, src_off, dst_off) = match address_offsets(fc1) {
Some(o) => o,
None => return,
};
if frame_bytes.len() <= bssid_off + 5
|| frame_bytes.len() <= src_off + 5
|| frame_bytes.len() <= dst_off + 5
{ return; }
let fmt_mac = |off: usize| -> String {
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
frame_bytes[off], frame_bytes[off+1], frame_bytes[off+2],
frame_bytes[off+3], frame_bytes[off+4], frame_bytes[off+5])
};
let bssid = fmt_mac(bssid_off);
let src = fmt_mac(src_off);
let dst = fmt_mac(dst_off);
let body_off = data_body_offset(frame_bytes);
if body_off < frame_bytes.len() {
check_eapol(
raw, frame_bytes, &bssid, &Some(src), &dst, body_off,
ap_map, handshake_map, handshake_count, handshake_dir,
hs_timeout,
);
}
}
fn add_client(ap: &mut AccessPoint, src: &Option<String>, dst: &str, signal_dbm: i8) {
if let Some(ref client_mac) = src {
if *client_mac != ap.bssid && *client_mac != "ff:ff:ff:ff:ff:ff" {
let ci = ap.clients.entry(client_mac.clone()).or_insert_with(|| {
let mut c = ClientInfo::new(client_mac.clone());
c.vendor = crate::oui::lookup_vendor(client_mac);
c
});
ci.signal_dbm = signal_dbm;
ci.last_seen = Instant::now();
ci.data_count += 1;
}
}
if dst != ap.bssid && dst != "ff:ff:ff:ff:ff:ff" {
let ci = ap.clients.entry(dst.to_string()).or_insert_with(|| {
let mut c = ClientInfo::new(dst.to_string());
c.vendor = crate::oui::lookup_vendor(dst);
c
});
ci.last_seen = Instant::now();
}
}
pub fn save_handshake(hs: &HandshakeState, handshake_dir: &Path) -> Result<String, String> {
std::fs::create_dir_all(handshake_dir).map_err(|e| format!("mkdir: {}", e))?;
let safe_essid: String = hs.essid.chars()
.map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
.collect();
let safe_bssid = hs.key.bssid.replace(':', "");
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
let filename = format!("{}_{}_{}_{}.pcap",
safe_essid, safe_bssid, hs.key.client.replace(':', ""), timestamp);
let path = handshake_dir.join(&filename);
let mut file = std::fs::File::create(&path).map_err(|e| format!("create: {}", e))?;
let global_header: [u8; 24] = [
0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
];
file.write_all(&global_header).map_err(|e| format!("write header: {}", e))?;
let mut ts_sec: u32 = 0;
let write_pkt = |f: &mut std::fs::File, data: &[u8], ts: u32| -> Result<(), String> {
let len = data.len() as u32;
let mut hdr = [0u8; 16];
hdr[0..4].copy_from_slice(&ts.to_le_bytes());
hdr[4..8].copy_from_slice(&0u32.to_le_bytes());
hdr[8..12].copy_from_slice(&len.to_le_bytes());
hdr[12..16].copy_from_slice(&len.to_le_bytes());
f.write_all(&hdr).map_err(|e| format!("write pkt hdr: {}", e))?;
f.write_all(data).map_err(|e| format!("write pkt data: {}", e))?;
Ok(())
};
if let Some(ref beacon) = hs.beacon_raw {
write_pkt(&mut file, beacon, ts_sec)?;
ts_sec += 1;
}
for raw in hs.messages.iter().flatten() {
write_pkt(&mut file, raw, ts_sec)?;
ts_sec += 1;
}
Ok(path.display().to_string())
}