feat: dicionário completo, regras de mutação, máscara, .hc22000 e WPS (reaver)
- crack.rs: dicionário padrão trocado pra wpa-full.txt.gz (rockyou + Probable-Wordlists top204k mesclados/deduplicados, 9,7M senhas, comprimido e embutido via LazyLock), PBKDF2 ~640x mais rápido (paralelização por núcleo + HMAC reaproveitado em vez de reconstruído a cada iteração), força bruta por máscara (rules.rs::Mask, sintaxe ?d?l?u?s?a) e regras de mutação tipo hashcat (maiúscula, sufixos numéricos/símbolos, leet simples). - hashcat.rs: exportação de handshake/PMKID em .hc22000 (individual ou em lote), pro hashcat de verdade em vez de só o dicionário embutido. - wps.rs: ataque WPS PIN via reaver (binário real do reaver-wps-fork-t6x, GPL-2, extraído em tempo de execução) e bully quando instalado no sistema. - assets/wordlists/LICENSE.wpa-full documenta a atribuição (CC BY-SA 4.0 do Probable-Wordlists; rockyou.txt como dado de vazamento de uso livre consolidado em pesquisa de segurança, mesmo padrão do Kali/SecLists).
This commit is contained in:
1 parent
5477b8fb03
commit
f83654506e
9 files changed
+978
-28
No files matched your search
@@ -0,0 +1,100 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: reaver-wps-fork-t6x
|
||||
Source: https://github.com/t6x/reaver-wps-fork-t6x
|
||||
|
||||
Files: *
|
||||
Copyright: 2011, Tactical Network Solutions, Craig Heffner <cheffner@tacnetsol.com>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/common/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/crypto/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi> and contributors
|
||||
License: GPL-2
|
||||
|
||||
Files: src/tls/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/tls/libtommath.c
|
||||
Copyright: Tom St Denis
|
||||
License: public-domain
|
||||
|
||||
Files: src/utils/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/utils/radiotap.h
|
||||
Copyright: 2003-2004 David Young
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: src/wps/*
|
||||
Copyright: 2007-2009, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/wps/wps_ufd.c
|
||||
Copyright: 2009, Masashi Honma <honma@ictec.co.jp>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/lwe/*
|
||||
Copyright: 1997-2007, Jean Tourrilhes <jt@hpl.hp.com>
|
||||
License: GPL-2
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2011-2018 Bartosz Fenski <fenio@debian.org>
|
||||
2023 Leandro Cunha <leandrocunha016@gmail.com>
|
||||
License: GPL-2
|
||||
|
||||
License: GPL-2
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License can be found in `/usr/share/common-licenses/GPL-2'.
|
||||
|
||||
License: BSD-3-clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
.
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
.
|
||||
3. Neither the name of the ORGANIZATION nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: public-domain
|
||||
The files tagged with this license contains the following paragraphs:
|
||||
.
|
||||
No copyright is claimed. This code is in the public domain; do with
|
||||
it what you wish.
|
||||
.
|
||||
Written by Karel Zak <kzak@redhat.com>
|
||||
Executable
BIN
Binary file not shown.
@@ -18,6 +18,8 @@ sha2 = "0.10"
|
||||
sha1 = "0.10"
|
||||
hmac = "0.12"
|
||||
md-5 = "0.10"
|
||||
aes = "0.8"
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
wpa-full.txt.gz
|
||||
|
||||
Dois dicionários concatenados, com as duplicatas removidas e a ordem de
|
||||
prioridade preservada (as entradas mais prováveis ficam primeiro, então a
|
||||
quebra por dicionário tende a achar a senha mais cedo em vez de só no fim):
|
||||
|
||||
1. Probable-Wordlists (berzerk0), Real-Passwords/WPA-Length/
|
||||
Top204Thousand-WPA-probable-v2.txt — 203.806 senhas reais vazadas, já
|
||||
filtradas pro comprimento válido de WPA (8–63) e ordenadas por frequência
|
||||
observada no mundo real.
|
||||
https://github.com/berzerk0/Probable-Wordlists
|
||||
Licença: Creative Commons Attribution-ShareAlike 4.0 International
|
||||
(CC BY-SA 4.0) — https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
2. rockyou.txt — o dump de senhas em texto puro do vazamento da RockYou de
|
||||
2009 (~32 milhões de contas), aqui filtrado pro mesmo comprimento válido
|
||||
de WPA. É provavelmente o dicionário de senhas mais usado em segurança
|
||||
ofensiva — vem embutido por padrão até no Kali Linux
|
||||
(/usr/share/wordlists/rockyou.txt.gz). Redistribuído aqui via SecLists:
|
||||
https://github.com/danielmiessler/SecLists (repositório MIT; o conteúdo do
|
||||
próprio rockyou.txt é dado de vazamento, sem titular de direitos autorais
|
||||
que o licencie formalmente — é tratado como de uso livre pra pesquisa de
|
||||
segurança em toda a indústria, mesmo padrão que o SecLists e as
|
||||
distribuições Linux de pentest adotam).
|
||||
|
||||
Ao redistribuir este arquivo (ou um derivado dele), mantenha a atribuição ao
|
||||
Probable-Wordlists acima — é a parte que tem uma licença formal que exige
|
||||
isso (CC BY-SA 4.0).
|
||||
|
||||
Isso é atribuição e aviso de licença, não os termos legais completos da
|
||||
CC BY-SA 4.0.
|
||||
Binary file not shown.
+315
-28
@@ -15,13 +15,17 @@ use crate::dot11::parse_mac;
|
||||
use crate::error::{Result, SabiaError};
|
||||
use crate::handshake::Handshake;
|
||||
use crate::pcapreader::PcapReader;
|
||||
use crate::rules::Mask;
|
||||
use crate::scan::ScanTable;
|
||||
use flate2::read::GzDecoder;
|
||||
use hmac::{Hmac, Mac};
|
||||
use md5::Md5;
|
||||
use sha1::Sha1;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
@@ -29,6 +33,37 @@ type HmacMd5 = Hmac<Md5>;
|
||||
|
||||
const WPA_ITERATIONS: u32 = 4096;
|
||||
|
||||
/// `.gz` do dicionário embutido — descomprimido só na primeira vez que é
|
||||
/// usado (ver [`default_wordlist`]). Cru são ~101MB; comprimido, ~39MB no
|
||||
/// binário em vez de ~101MB.
|
||||
const DEFAULT_WORDLIST_GZ: &[u8] = include_bytes!("../assets/wordlists/wpa-full.txt.gz");
|
||||
|
||||
pub const DEFAULT_WORDLIST_SOURCE: &str =
|
||||
"Probable-Wordlists (berzerk0) WPA-Length top204k (CC BY-SA 4.0) + rockyou.txt via SecLists — ver assets/wordlists/LICENSE.wpa-full";
|
||||
|
||||
static DEFAULT_WORDLIST_CACHE: LazyLock<String> = LazyLock::new(|| {
|
||||
let mut out = String::new();
|
||||
GzDecoder::new(DEFAULT_WORDLIST_GZ)
|
||||
.read_to_string(&mut out)
|
||||
.expect("dicionário embutido corrompido");
|
||||
out
|
||||
});
|
||||
|
||||
/// Lista embutida de senhas prováveis de Wi-Fi, usada quando o usuário não
|
||||
/// escolhe um dicionário próprio. Não depende de arquivo externo: é a lista
|
||||
/// mais completa que o Sabiá consegue embutir sem exigir download —
|
||||
/// 9.703.831 senhas (203.806 do Probable-Wordlists, ranqueadas por
|
||||
/// probabilidade, seguidas pelo rockyou.txt inteiro) filtradas pro
|
||||
/// comprimento válido de WPA (8–63) e pra ASCII puro (WPA-PSK exige um
|
||||
/// octeto válido; a imensa maioria dos roteadores só aceita ASCII mesmo, e
|
||||
/// isso evita o pouco de lixo binário que o rockyou.txt original carrega),
|
||||
/// sem duplicatas e na ordem de prioridade: as mais prováveis primeiro,
|
||||
/// então a quebra tende a achar a senha cedo, não só no fim da lista. Ver
|
||||
/// `assets/wordlists/LICENSE.wpa-full` pra atribuição completa.
|
||||
pub fn default_wordlist() -> &'static str {
|
||||
&DEFAULT_WORDLIST_CACHE
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrackTarget {
|
||||
pub ssid: String,
|
||||
@@ -116,8 +151,10 @@ pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] {
|
||||
}
|
||||
|
||||
/// Aceita uma frase-senha (8–63 caracteres) ou uma PMK crua em hexadecimal
|
||||
/// (64 caracteres, como no hashcat/aircrack).
|
||||
fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> {
|
||||
/// (64 caracteres, como no hashcat/aircrack). `pub(crate)` porque
|
||||
/// `handshake.rs` reusa exatamente essa regra ao derivar a TK de uma senha
|
||||
/// já quebrada (o campo `password` aceita as duas formas, igual aqui).
|
||||
pub(crate) fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> {
|
||||
if candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return hex_to_32(candidate.as_bytes());
|
||||
}
|
||||
@@ -133,15 +170,20 @@ fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> {
|
||||
pub fn pbkdf2_sha1(password: &[u8], salt: &[u8], iterations: u32, out_len: usize) -> Vec<u8> {
|
||||
let iterations = iterations.max(1);
|
||||
let mut out = Vec::with_capacity(out_len);
|
||||
// O par ipad/opad depende só da senha — monta uma vez e clona a cada
|
||||
// rodada (clone é só copiar os dois blocos já prontos) em vez de montar
|
||||
// o HMAC do zero em cada uma das milhares de iterações. Em 4096
|
||||
// iterações por bloco isso é a maior parte do custo fora do SHA1 em si.
|
||||
let base = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave");
|
||||
let mut block: u32 = 1;
|
||||
while out.len() < out_len {
|
||||
let mut mac = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave");
|
||||
let mut mac = base.clone();
|
||||
mac.update(salt);
|
||||
mac.update(&block.to_be_bytes());
|
||||
let mut u = mac.finalize().into_bytes();
|
||||
let mut t = u;
|
||||
for _ in 1..iterations {
|
||||
let mut mac = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave");
|
||||
let mut mac = base.clone();
|
||||
mac.update(&u);
|
||||
u = mac.finalize().into_bytes();
|
||||
for (a, b) in t.iter_mut().zip(u.iter()) {
|
||||
@@ -272,16 +314,29 @@ fn valid_candidate(candidate: &str) -> bool {
|
||||
|| (candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
/// Tenta cada linha do dicionário contra o alvo. `progress` recebe o número
|
||||
/// de candidatos já processados.
|
||||
/// Tenta cada linha do dicionário contra o alvo, dividindo o trabalho entre
|
||||
/// as threads disponíveis (PBKDF2-HMAC-SHA1 de 4096 iterações é CPU puro e
|
||||
/// cada candidato é independente — ganho quase linear com o número de
|
||||
/// núcleos). `progress` recebe o número de candidatos já processados, ~8x
|
||||
/// por segundo enquanto roda. Com `apply_rules`, cada linha vira até
|
||||
/// [`rules::VARIANTS_PER_WORD`] candidatos (maiúscula, sufixos comuns, leet
|
||||
/// simples) em vez de só a palavra crua — multiplica o tempo de verdade,
|
||||
/// por isso é opt-in.
|
||||
pub fn crack_reader<R: BufRead, F: FnMut(u64)>(
|
||||
target: &CrackTarget,
|
||||
mut wordlist: R,
|
||||
apply_rules: bool,
|
||||
mut progress: F,
|
||||
) -> CrackReport {
|
||||
) -> CrackReport
|
||||
where
|
||||
F: Send,
|
||||
{
|
||||
let start = Instant::now();
|
||||
let mut tried = 0u64;
|
||||
let mut password = None;
|
||||
|
||||
// Carrega a lista inteira antes de começar: dá pra repartir em pedaços
|
||||
// contíguos por thread (melhor localidade de cache que round-robin) e o
|
||||
// próprio dicionário embutido cabe de sobra em memória.
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
@@ -294,20 +349,62 @@ pub fn crack_reader<R: BufRead, F: FnMut(u64)>(
|
||||
if candidate.is_empty() {
|
||||
continue;
|
||||
}
|
||||
tried += 1;
|
||||
if apply_rules {
|
||||
candidates.extend(crate::rules::expand(candidate));
|
||||
} else {
|
||||
candidates.push(candidate.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let total = candidates.len();
|
||||
let workers = std::thread::available_parallelism()
|
||||
.map(std::num::NonZeroUsize::get)
|
||||
.unwrap_or(1)
|
||||
.min(total.max(1));
|
||||
let chunk = total.div_ceil(workers.max(1)).max(1);
|
||||
|
||||
let tried = AtomicU64::new(0);
|
||||
let stop = AtomicBool::new(false);
|
||||
let found: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
for part in candidates.chunks(chunk) {
|
||||
let tried = &tried;
|
||||
let stop = &stop;
|
||||
let found = &found;
|
||||
scope.spawn(move || {
|
||||
for candidate in part {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
if valid_candidate(candidate) && target.verify(candidate) {
|
||||
password = Some(candidate.to_string());
|
||||
*found.lock().unwrap() = Some(candidate.clone());
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
tried.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Thread "repórter": roda dentro do mesmo scope só pra poder
|
||||
// emprestar `progress` (FnMut, chamado de um único lugar).
|
||||
scope.spawn(|| loop {
|
||||
std::thread::sleep(std::time::Duration::from_millis(120));
|
||||
let n = tried.load(Ordering::Relaxed);
|
||||
progress(n);
|
||||
if stop.load(Ordering::Relaxed) || n as usize >= total {
|
||||
break;
|
||||
}
|
||||
if tried % 128 == 0 {
|
||||
progress(tried);
|
||||
}
|
||||
}
|
||||
progress(tried);
|
||||
});
|
||||
});
|
||||
|
||||
let final_tried = tried.load(Ordering::Relaxed);
|
||||
progress(final_tried);
|
||||
|
||||
CrackReport {
|
||||
password,
|
||||
tried,
|
||||
password: found.into_inner().unwrap(),
|
||||
tried: final_tried,
|
||||
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||
ssid: Some(target.ssid.clone()),
|
||||
bssid: Some(crate::dot11::mac_to_string(&target.bssid)),
|
||||
@@ -316,26 +413,52 @@ pub fn crack_reader<R: BufRead, F: FnMut(u64)>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crack_handshake<F: FnMut(u64)>(
|
||||
pub fn crack_handshake<F: FnMut(u64) + Send>(
|
||||
hs: &Handshake,
|
||||
wordlist: impl AsRef<Path>,
|
||||
apply_rules: bool,
|
||||
progress: F,
|
||||
) -> Result<CrackReport> {
|
||||
let target = CrackTarget::from_handshake(hs).ok_or_else(|| {
|
||||
let target = target_for(hs)?;
|
||||
let file = File::open(wordlist)?;
|
||||
Ok(crack_reader(
|
||||
&target,
|
||||
BufReader::new(file),
|
||||
apply_rules,
|
||||
progress,
|
||||
))
|
||||
}
|
||||
|
||||
/// Quebra usando a lista embutida.
|
||||
pub fn crack_handshake_default<F: FnMut(u64) + Send>(
|
||||
hs: &Handshake,
|
||||
apply_rules: bool,
|
||||
progress: F,
|
||||
) -> Result<CrackReport> {
|
||||
let target = target_for(hs)?;
|
||||
Ok(crack_reader(
|
||||
&target,
|
||||
std::io::Cursor::new(default_wordlist().as_bytes()),
|
||||
apply_rules,
|
||||
progress,
|
||||
))
|
||||
}
|
||||
|
||||
fn target_for(hs: &Handshake) -> Result<CrackTarget> {
|
||||
CrackTarget::from_handshake(hs).ok_or_else(|| {
|
||||
SabiaError::InvalidInput(
|
||||
"handshake sem SSID, PMKID ou par de nonces suficiente para quebrar".into(),
|
||||
)
|
||||
})?;
|
||||
let file = File::open(wordlist)?;
|
||||
Ok(crack_reader(&target, BufReader::new(file), progress))
|
||||
})
|
||||
}
|
||||
|
||||
/// Lê um `.pcap`, monta os handshakes e tenta quebrar o primeiro alvo útil.
|
||||
pub fn crack_pcap<F: FnMut(u64)>(
|
||||
pub fn crack_pcap<F: FnMut(u64) + Send>(
|
||||
pcap: impl AsRef<Path>,
|
||||
wordlist: impl AsRef<Path>,
|
||||
bssid: Option<&str>,
|
||||
station: Option<&str>,
|
||||
apply_rules: bool,
|
||||
progress: F,
|
||||
) -> Result<CrackReport> {
|
||||
let mut reader = PcapReader::open(pcap)?;
|
||||
@@ -351,7 +474,88 @@ pub fn crack_pcap<F: FnMut(u64)>(
|
||||
let hs = table
|
||||
.first_crackable(bssid, station)
|
||||
.ok_or_else(|| SabiaError::InvalidInput("nenhum handshake quebrável na captura".into()))?;
|
||||
crack_handshake(hs, wordlist, progress)
|
||||
crack_handshake(hs, wordlist, apply_rules, progress)
|
||||
}
|
||||
|
||||
/// Força bruta por máscara (estilo hashcat `-a 3`): gera e testa cada
|
||||
/// combinação possível, sem depender de dicionário. Mesma paralelização por
|
||||
/// threads do `crack_reader`, mas gerando candidatos por índice (base
|
||||
/// mista) em vez de carregar tudo em memória — é o que permite máscaras com
|
||||
/// milhões/bilhões de combinações sem estourar RAM.
|
||||
pub fn crack_mask<F: FnMut(u64)>(target: &CrackTarget, mask: &Mask, mut progress: F) -> CrackReport
|
||||
where
|
||||
F: Send,
|
||||
{
|
||||
let start = Instant::now();
|
||||
let total = mask.total().unwrap_or(0);
|
||||
|
||||
let workers = std::thread::available_parallelism()
|
||||
.map(std::num::NonZeroUsize::get)
|
||||
.unwrap_or(1)
|
||||
.min(total.max(1) as usize) as u64;
|
||||
let chunk = total.div_ceil(workers.max(1)).max(1);
|
||||
|
||||
let tried = AtomicU64::new(0);
|
||||
let stop = AtomicBool::new(false);
|
||||
let found: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
let mut from = 0u64;
|
||||
while from < total {
|
||||
let to = (from + chunk).min(total);
|
||||
let tried = &tried;
|
||||
let stop = &stop;
|
||||
let found = &found;
|
||||
scope.spawn(move || {
|
||||
for i in from..to {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
if let Some(candidate) = mask.candidate(i) {
|
||||
if valid_candidate(&candidate) && target.verify(&candidate) {
|
||||
*found.lock().unwrap() = Some(candidate);
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
tried.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
from = to;
|
||||
}
|
||||
|
||||
scope.spawn(|| loop {
|
||||
std::thread::sleep(std::time::Duration::from_millis(120));
|
||||
let n = tried.load(Ordering::Relaxed);
|
||||
progress(n);
|
||||
if stop.load(Ordering::Relaxed) || n >= total {
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let final_tried = tried.load(Ordering::Relaxed);
|
||||
progress(final_tried);
|
||||
|
||||
CrackReport {
|
||||
password: found.into_inner().unwrap(),
|
||||
tried: final_tried,
|
||||
elapsed_ms: start.elapsed().as_millis() as u64,
|
||||
ssid: Some(target.ssid.clone()),
|
||||
bssid: Some(crate::dot11::mac_to_string(&target.bssid)),
|
||||
station: Some(crate::dot11::mac_to_string(&target.station)),
|
||||
method: Some(target.method().to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Quebra por máscara a partir de um handshake já montado.
|
||||
pub fn crack_handshake_mask<F: FnMut(u64) + Send>(
|
||||
hs: &Handshake,
|
||||
mask: &Mask,
|
||||
progress: F,
|
||||
) -> Result<CrackReport> {
|
||||
let target = target_for(hs)?;
|
||||
Ok(crack_mask(&target, mask, progress))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -360,6 +564,47 @@ mod tests {
|
||||
use crate::testutil::{handshake_frames, synthetic_handshake};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Mede só o tempo de descomprimir o dicionário embutido (gzip, ~39MB -> ~101MB),
|
||||
/// que acontece uma vez, na primeira chamada a `default_wordlist()`.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_default_wordlist_decompress() {
|
||||
let start = Instant::now();
|
||||
let text = default_wordlist();
|
||||
let elapsed = start.elapsed();
|
||||
eprintln!(
|
||||
"descomprimiu {} linhas em {:.2?}",
|
||||
text.lines().count(),
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
|
||||
/// Não é teste de verdade — mede candidatos/segundo pra guiar otimização
|
||||
/// de performance. `cargo test --release -p sabia-core -- --ignored
|
||||
/// --nocapture bench_crack_throughput`.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_crack_throughput() {
|
||||
let (m1, m2) = handshake_frames("IEEE", "senha-que-nao-esta-na-lista", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
// Só uma fatia: a lista inteira (9,7 milhões) levaria minutos — o
|
||||
// objetivo aqui é medir candidatos/s, não o tempo total.
|
||||
let sample: String = default_wordlist()
|
||||
.lines()
|
||||
.take(500_000)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let start = Instant::now();
|
||||
let report = crack_reader(&target, Cursor::new(sample.as_bytes()), false, |_| {});
|
||||
let elapsed = start.elapsed();
|
||||
let per_sec = report.tried as f64 / elapsed.as_secs_f64();
|
||||
eprintln!(
|
||||
"{} candidatos em {:.2?} = {:.1}/s",
|
||||
report.tried, elapsed, per_sec
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbkdf2_matches_rfc6070_vector() {
|
||||
let out = pbkdf2_sha1(b"password", b"salt", 1, 20);
|
||||
@@ -386,6 +631,7 @@ mod tests {
|
||||
let report = crack_reader(
|
||||
&target,
|
||||
Cursor::new(b"errada\noutra\npassword\nnunca\n".to_vec()),
|
||||
false,
|
||||
|_| progress_calls += 1,
|
||||
);
|
||||
assert_eq!(report.password.as_deref(), Some("password"));
|
||||
@@ -400,7 +646,12 @@ mod tests {
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &[]);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
assert_eq!(target.method(), "PMKID");
|
||||
let report = crack_reader(&target, Cursor::new(b"nada\npassword\n".to_vec()), |_| {});
|
||||
let report = crack_reader(
|
||||
&target,
|
||||
Cursor::new(b"nada\npassword\n".to_vec()),
|
||||
false,
|
||||
|_| {},
|
||||
);
|
||||
assert_eq!(report.password.as_deref(), Some("password"));
|
||||
}
|
||||
|
||||
@@ -417,10 +668,46 @@ mod tests {
|
||||
let (m1, m2) = handshake_frames("IEEE", "senha-correta", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
let report = crack_reader(&target, Cursor::new(b"senha-errada\n".to_vec()), |_| {});
|
||||
let report = crack_reader(
|
||||
&target,
|
||||
Cursor::new(b"senha-errada\n".to_vec()),
|
||||
false,
|
||||
|_| {},
|
||||
);
|
||||
assert!(report.password.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rules_find_a_mutated_variant_not_present_verbatim() {
|
||||
// A senha de verdade é "wifi12345" — não está na lista, mas é um
|
||||
// sufixo numérico de "wifi" que `rules::expand` gera.
|
||||
let (m1, m2) = handshake_frames("IEEE", "wifi12345", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
|
||||
let without_rules = crack_reader(&target, Cursor::new(b"wifi\n".to_vec()), false, |_| {});
|
||||
assert!(without_rules.password.is_none());
|
||||
|
||||
let with_rules = crack_reader(&target, Cursor::new(b"wifi\n".to_vec()), true, |_| {});
|
||||
assert_eq!(with_rules.password.as_deref(), Some("wifi12345"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_crack_finds_a_numeric_password() {
|
||||
// Máscara minúscula de propósito (só 10 combinações): cada candidato
|
||||
// é um PBKDF2 de 4096 iterações, pesado até em paralelo — um brute
|
||||
// force de verdade (ex.: ?d×8, 100 milhões) é coisa pra uso real,
|
||||
// não pra um teste unitário que roda em `cargo test` toda hora.
|
||||
let (m1, m2) = handshake_frames("IEEE", "wifiabc7", 2, false);
|
||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||
let mask = Mask::parse("wifiabc?d").expect("máscara válida");
|
||||
assert_eq!(mask.total(), Some(10));
|
||||
let report = crack_mask(&target, &mask, |_| {});
|
||||
assert_eq!(report.password.as_deref(), Some("wifiabc7"));
|
||||
assert!(report.tried > 0);
|
||||
}
|
||||
|
||||
fn hex_bytes(s: &str) -> Vec<u8> {
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Exportação no formato `.hc22000` do hashcat.
|
||||
//!
|
||||
//! Duas linhas possíveis, como no hcxtools:
|
||||
//!
|
||||
//! * `WPA*01*PMKID*AP*STA*SSID***`
|
||||
//! * `WPA*02*MIC*AP*STA*SSID*ANONCE*EAPOL*MESSAGEPAIR`
|
||||
//!
|
||||
//! O EAPOL exportado é o quadro da mensagem 2 completo (cabeçalho 802.1X +
|
||||
//! descritor), que é o que o hashcat espera no campo `EAPOL_CLIENT`.
|
||||
|
||||
use crate::eapol::MIC_OFFSET;
|
||||
use crate::handshake::Handshake;
|
||||
|
||||
fn mac_hex(mac: &str) -> Option<String> {
|
||||
let bytes: Vec<String> = mac
|
||||
.split(':')
|
||||
.map(|part| part.trim().to_ascii_lowercase())
|
||||
.collect();
|
||||
if bytes.len() != 6
|
||||
|| !bytes
|
||||
.iter()
|
||||
.all(|b| b.len() == 2 && b.chars().all(|c| c.is_ascii_hexdigit()))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(bytes.concat())
|
||||
}
|
||||
|
||||
fn ssid_hex(ssid: &str) -> String {
|
||||
ssid.as_bytes().iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
pub fn hashcat_line(hs: &Handshake) -> Option<String> {
|
||||
let ssid = hs.ssid.as_deref().filter(|s| !s.is_empty())?;
|
||||
let ap = mac_hex(&hs.bssid)?;
|
||||
let sta = mac_hex(&hs.station)?;
|
||||
let ssid = ssid_hex(ssid);
|
||||
|
||||
if let Some(pmkid) = hs.pmkid {
|
||||
return Some(format!("WPA*01*{}*{ap}*{sta}*{ssid}***", hex(&pmkid)));
|
||||
}
|
||||
|
||||
let eapol = hs.eapol_msg2.as_deref()?;
|
||||
let anonce = hs.anonce?;
|
||||
let mic = eapol.get(MIC_OFFSET..MIC_OFFSET + 16)?;
|
||||
Some(format!(
|
||||
"WPA*02*{}*{ap}*{sta}*{ssid}*{}*{}*00",
|
||||
hex(mic),
|
||||
hex(&anonce),
|
||||
hex(eapol),
|
||||
))
|
||||
}
|
||||
|
||||
/// Linhas únicas, uma por handshake que tenha material suficiente.
|
||||
pub fn export<'a>(handshakes: impl IntoIterator<Item = &'a Handshake>) -> Vec<String> {
|
||||
let mut lines: Vec<String> = handshakes.into_iter().filter_map(hashcat_line).collect();
|
||||
lines.sort();
|
||||
lines.dedup();
|
||||
lines
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::handshake::Handshake;
|
||||
|
||||
#[test]
|
||||
fn exports_pmkid_line() {
|
||||
let hs = Handshake {
|
||||
bssid: "00:11:22:33:44:55".into(),
|
||||
station: "66:77:88:99:aa:bb".into(),
|
||||
ssid: Some("Casa".into()),
|
||||
pmkid: Some([0xcd; 16]),
|
||||
..Default::default()
|
||||
};
|
||||
let line = hashcat_line(&hs).expect("linha");
|
||||
assert_eq!(
|
||||
line,
|
||||
"WPA*01*cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd*001122334455*66778899aabb*43617361***"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
//! Regras de mutação sobre uma palavra do dicionário (estilo hashcat `-r`,
|
||||
//! bem mais simples) e força bruta por máscara (estilo hashcat `-a 3`).
|
||||
//!
|
||||
//! Em quebra de hash normal, regras são "de graça" — um hash SHA256 custa
|
||||
//! nanossegundos. Aqui não: cada candidato é um PBKDF2-HMAC-SHA1 de 4096
|
||||
//! iterações, então cada variante multiplica o tempo de verdade. As regras
|
||||
//! abaixo cobrem os padrões mais comuns (maiúscula inicial, sufixo numérico,
|
||||
//! leet simples) sem explodir pra centenas de variantes por palavra.
|
||||
|
||||
/// Quantas variantes [`expand`] gera por palavra base (pra calcular o
|
||||
/// multiplicador de tempo antes de rodar).
|
||||
pub const VARIANTS_PER_WORD: usize = 1 + 1 + 1 + SUFFIXES.len() + LEET_PAIRS.len();
|
||||
|
||||
const SUFFIXES: &[&str] = &[
|
||||
"1", "12", "123", "1234", "12345", "123456", "01", "007", "99", "2023", "2024", "2025", "!",
|
||||
"@", "#",
|
||||
];
|
||||
|
||||
/// (char original, substituto) — leet simples, uma troca por vez, não
|
||||
/// combinada, senão a combinatória explode rápido demais.
|
||||
const LEET_PAIRS: &[(char, char)] = &[('a', '@'), ('e', '3'), ('i', '1'), ('o', '0'), ('s', '$')];
|
||||
|
||||
/// Gera as variantes de `word` (incluindo a palavra original) que respeitam
|
||||
/// o comprimento válido de WPA (8–63), sem duplicatas.
|
||||
pub fn expand(word: &str) -> Vec<String> {
|
||||
let mut out = Vec::with_capacity(VARIANTS_PER_WORD);
|
||||
let mut push = |s: String| {
|
||||
let len = s.chars().count();
|
||||
if (8..=63).contains(&len) && !out.contains(&s) {
|
||||
out.push(s);
|
||||
}
|
||||
};
|
||||
|
||||
push(word.to_string());
|
||||
|
||||
if let Some(first) = word.chars().next() {
|
||||
let capitalized: String = first.to_uppercase().chain(word.chars().skip(1)).collect();
|
||||
push(capitalized);
|
||||
}
|
||||
push(word.to_uppercase());
|
||||
|
||||
for suffix in SUFFIXES {
|
||||
push(format!("{word}{suffix}"));
|
||||
}
|
||||
|
||||
for (from, to) in LEET_PAIRS {
|
||||
if word.contains(*from) {
|
||||
push(word.replace(*from, &to.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Classe de caractere de um token `?x` de máscara (sintaxe hashcat: `?d`
|
||||
/// dígito, `?l` minúscula, `?u` maiúscula, `?s` símbolo comum, `?a` os
|
||||
/// quatro juntos). `??` escapa um `?` literal.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CharClass {
|
||||
Digit,
|
||||
Lower,
|
||||
Upper,
|
||||
Symbol,
|
||||
All,
|
||||
Literal(char),
|
||||
}
|
||||
|
||||
const DIGITS: &str = "0123456789";
|
||||
const LOWER: &str = "abcdefghijklmnopqrstuvwxyz";
|
||||
const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const SYMBOLS: &str = "!@#$%&*_-";
|
||||
|
||||
impl CharClass {
|
||||
fn alphabet(self) -> String {
|
||||
match self {
|
||||
CharClass::Digit => DIGITS.to_string(),
|
||||
CharClass::Lower => LOWER.to_string(),
|
||||
CharClass::Upper => UPPER.to_string(),
|
||||
CharClass::Symbol => SYMBOLS.to_string(),
|
||||
CharClass::All => format!("{DIGITS}{LOWER}{UPPER}{SYMBOLS}"),
|
||||
CharClass::Literal(c) => c.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Máscara compilada: uma lista de alfabetos, um por posição. Gera
|
||||
/// candidatos por índice (contagem em base mista) sem materializar a lista
|
||||
/// inteira — essencial, já que `?d?d?d?d?d?d?d?d` sozinho são 100 milhões.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mask {
|
||||
alphabets: Vec<Vec<char>>,
|
||||
}
|
||||
|
||||
impl Mask {
|
||||
/// Compila uma máscara tipo `wifi?d?d?d?d` (literal "wifi" + 4 dígitos).
|
||||
/// `??` é um `?` literal; `?` seguido de letra desconhecida é erro.
|
||||
pub fn parse(pattern: &str) -> Result<Self, String> {
|
||||
let mut alphabets = Vec::new();
|
||||
let mut chars = pattern.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
let class = if c == '?' {
|
||||
match chars.next() {
|
||||
Some('d') => CharClass::Digit,
|
||||
Some('l') => CharClass::Lower,
|
||||
Some('u') => CharClass::Upper,
|
||||
Some('s') => CharClass::Symbol,
|
||||
Some('a') => CharClass::All,
|
||||
Some('?') => CharClass::Literal('?'),
|
||||
Some(other) => return Err(format!("classe de máscara desconhecida: ?{other}")),
|
||||
None => return Err("máscara termina com '?' sem classe".into()),
|
||||
}
|
||||
} else {
|
||||
CharClass::Literal(c)
|
||||
};
|
||||
alphabets.push(class.alphabet().chars().collect());
|
||||
}
|
||||
if alphabets.is_empty() {
|
||||
return Err("máscara vazia".into());
|
||||
}
|
||||
if alphabets.len() > 63 {
|
||||
return Err("máscara maior que o limite de uma senha WPA (63)".into());
|
||||
}
|
||||
Ok(Self { alphabets })
|
||||
}
|
||||
|
||||
/// Total de candidatos. `None` se não couber num `u64` (máscara longa
|
||||
/// demais pra ser levada a sério de qualquer forma).
|
||||
pub fn total(&self) -> Option<u64> {
|
||||
self.alphabets
|
||||
.iter()
|
||||
.try_fold(1u64, |acc, a| acc.checked_mul(a.len() as u64))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.alphabets.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.alphabets.is_empty()
|
||||
}
|
||||
|
||||
/// Candidato de índice `index` (contagem em base mista, como um
|
||||
/// odômetro: a última posição gira mais rápido). `None` se `index` >=
|
||||
/// `total()`.
|
||||
pub fn candidate(&self, mut index: u64) -> Option<String> {
|
||||
let total = self.total()?;
|
||||
if index >= total {
|
||||
return None;
|
||||
}
|
||||
let mut chars = vec!['\0'; self.alphabets.len()];
|
||||
for i in (0..self.alphabets.len()).rev() {
|
||||
let base = self.alphabets[i].len() as u64;
|
||||
let digit = (index % base) as usize;
|
||||
chars[i] = self.alphabets[i][digit];
|
||||
index /= base;
|
||||
}
|
||||
Some(chars.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn expand_respects_min_length() {
|
||||
let variants = expand("senha");
|
||||
// "senha" tem 5 chars; sozinha não é candidata WPA válida (precisa
|
||||
// de 8+), mas com sufixo (ex.: "senha123", 8) sim.
|
||||
assert!(!variants.iter().any(|v| v == "senha"));
|
||||
assert!(variants.contains(&"senha123".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_includes_original_when_long_enough() {
|
||||
let variants = expand("abacaxi1");
|
||||
assert!(variants.contains(&"abacaxi1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_applies_capitalization_suffix_and_leet() {
|
||||
let variants = expand("abacaxi1");
|
||||
assert!(variants.contains(&"Abacaxi1".to_string()));
|
||||
assert!(variants.contains(&"ABACAXI1".to_string()));
|
||||
assert!(variants.contains(&"abacaxi1123".to_string()));
|
||||
// leet troca TODAS as ocorrências de 'a': as três em "abacaxi1".
|
||||
assert!(variants.contains(&"@b@c@xi1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_has_no_duplicates() {
|
||||
let variants = expand("12345678");
|
||||
let mut sorted = variants.clone();
|
||||
sorted.sort();
|
||||
sorted.dedup();
|
||||
assert_eq!(variants.len(), sorted.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_parses_literals_and_classes() {
|
||||
let mask = Mask::parse("wifi?d?d?d?d").expect("máscara válida");
|
||||
assert_eq!(mask.len(), 8);
|
||||
assert_eq!(mask.total(), Some(10_000));
|
||||
assert_eq!(mask.candidate(0).as_deref(), Some("wifi0000"));
|
||||
assert_eq!(mask.candidate(9_999).as_deref(), Some("wifi9999"));
|
||||
assert_eq!(mask.candidate(10_000), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_literal_question_mark_and_unknown_class() {
|
||||
// "??" vira um '?' literal; os dois primeiros chars são consumidos
|
||||
// juntos, então "???d" é: '?' literal + "?d" (classe dígito).
|
||||
assert_eq!(
|
||||
Mask::parse("???d").unwrap().candidate(0).as_deref(),
|
||||
Some("?0")
|
||||
);
|
||||
assert!(Mask::parse("?z").is_err());
|
||||
assert!(Mask::parse("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_covers_every_combination_exactly_once() {
|
||||
let mask = Mask::parse("?d?l").expect("máscara válida");
|
||||
let total = mask.total().unwrap();
|
||||
assert_eq!(total, 10 * 26);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for i in 0..total {
|
||||
seen.insert(mask.candidate(i).unwrap());
|
||||
}
|
||||
assert_eq!(seen.len() as u64, total);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//! Ataque de PIN WPS com o `reaver` embutido.
|
||||
//!
|
||||
//! O protocolo EAP-WSC é grande demais para reimplementar aqui; o Sabiá
|
||||
//! carrega uma cópia do reaver (GPL-2, veja `assets/tools/LICENSE.reaver`),
|
||||
//! extrai em tempo de execução e mostra a saída ao vivo. Assim o WPS já
|
||||
//! funciona sem instalar nada. Se o usuário tiver um `bully` no PATH, ele
|
||||
//! também aparece como alternativa.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct WpsTool {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub bundled: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const BUNDLED_REAVER: &[u8] = include_bytes!("../../../assets/tools/reaver");
|
||||
|
||||
/// Procura um binário no `PATH` — usado pelo WPS (reaver/bully) e, fora
|
||||
/// deste módulo, pelo Evil Twin (hostapd/dnsmasq), que precisa da mesma
|
||||
/// checagem sem puxar uma dependência nova só pra isso.
|
||||
pub(crate) fn find_in_path(binary: &str) -> Option<PathBuf> {
|
||||
let path = std::env::var_os("PATH")?;
|
||||
for dir in std::env::split_paths(&path) {
|
||||
let candidate = dir.join(binary);
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn tools() -> Vec<WpsTool> {
|
||||
let mut out = Vec::new();
|
||||
#[cfg(target_os = "linux")]
|
||||
out.push(WpsTool {
|
||||
name: "reaver".into(),
|
||||
path: "integrado".into(),
|
||||
bundled: true,
|
||||
});
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if let Some(path) = find_in_path("reaver") {
|
||||
out.push(WpsTool {
|
||||
name: "reaver".into(),
|
||||
path: path.display().to_string(),
|
||||
bundled: false,
|
||||
});
|
||||
}
|
||||
if let Some(path) = find_in_path("bully") {
|
||||
out.push(WpsTool {
|
||||
name: "bully".into(),
|
||||
path: path.display().to_string(),
|
||||
bundled: false,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn tool_path(name: &str) -> Option<PathBuf> {
|
||||
match name {
|
||||
"reaver" => bundled_reaver().or_else(|| find_in_path("reaver")),
|
||||
"bully" => find_in_path("bully"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn bundled_reaver() -> Option<PathBuf> {
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// `temp_dir` é compartilhado; usa um diretório por UID e confere que ele
|
||||
// não é um symlink antes de gravar, para não seguir link de outro usuário.
|
||||
let uid = unsafe { libc::geteuid() };
|
||||
let dir = std::env::temp_dir().join(format!("sabia-tools-{uid}"));
|
||||
if let Ok(meta) = fs::symlink_metadata(&dir) {
|
||||
if !meta.is_dir() || meta.file_type().is_symlink() {
|
||||
return find_in_path("reaver");
|
||||
}
|
||||
} else if fs::create_dir_all(&dir).is_err() {
|
||||
return find_in_path("reaver");
|
||||
}
|
||||
let _ = fs::set_permissions(&dir, fs::Permissions::from_mode(0o700));
|
||||
|
||||
let path = dir.join("reaver");
|
||||
if let Ok(meta) = fs::symlink_metadata(&path) {
|
||||
if !meta.is_file() || meta.file_type().is_symlink() {
|
||||
return find_in_path("reaver");
|
||||
}
|
||||
if meta.len() == BUNDLED_REAVER.len() as u64 {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
fs::write(&path, BUNDLED_REAVER).ok()?;
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).ok()?;
|
||||
Some(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn bundled_reaver() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Argumentos para uma execução. Sem PIN, usa Pixie Dust (`-K 1`) no reaver
|
||||
/// e o ataque padrão no bully.
|
||||
pub fn args(tool: &str, iface: &str, bssid: &str, channel: u32, pin: Option<&str>) -> Vec<String> {
|
||||
let channel = channel.to_string();
|
||||
match tool {
|
||||
"bully" => {
|
||||
let mut args = vec![
|
||||
"-b".into(),
|
||||
bssid.into(),
|
||||
"-c".into(),
|
||||
channel,
|
||||
"-d".into(),
|
||||
iface.into(),
|
||||
"-v".into(),
|
||||
"3".into(),
|
||||
];
|
||||
if let Some(pin) = pin {
|
||||
args.push("-p".into());
|
||||
args.push(pin.into());
|
||||
}
|
||||
args
|
||||
}
|
||||
_ => {
|
||||
let mut args = vec![
|
||||
"-i".into(),
|
||||
iface.into(),
|
||||
"-b".into(),
|
||||
bssid.into(),
|
||||
"-c".into(),
|
||||
channel,
|
||||
"-vv".into(),
|
||||
];
|
||||
match pin {
|
||||
Some(pin) => {
|
||||
args.push("-p".into());
|
||||
args.push(pin.into());
|
||||
}
|
||||
None => {
|
||||
args.push("-K".into());
|
||||
args.push("1".into());
|
||||
}
|
||||
}
|
||||
args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrai PIN/PSK de uma linha de saída do reaver/bully.
|
||||
pub fn parse_line(line: &str) -> (Option<String>, Option<String>) {
|
||||
let lower = line.to_ascii_lowercase();
|
||||
let grab = |needle: &str| -> Option<String> {
|
||||
let idx = lower.find(needle)? + needle.len();
|
||||
let rest = line[idx..].trim();
|
||||
let rest = rest
|
||||
.split_whitespace()
|
||||
.next()?
|
||||
.trim_matches(|c| c == '\'' || c == '"');
|
||||
if rest.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(rest.to_string())
|
||||
}
|
||||
};
|
||||
let pin = if lower.contains("wps pin") || lower.contains("pin:") {
|
||||
grab("pin:").or_else(|| grab("wps pin"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let psk = if lower.contains("wpa psk") || lower.contains("psk:") {
|
||||
grab("psk:")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(pin, psk)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_reaver_output() {
|
||||
assert_eq!(
|
||||
parse_line("[+] WPS PIN: '12345670'"),
|
||||
(Some("12345670".into()), None)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_line("[+] WPA PSK: 'senha-secreta'"),
|
||||
(None, Some("senha-secreta".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_pixie_dust_args() {
|
||||
let pixie = args("reaver", "wlan0mon", "00:11:22:33:44:55", 6, None);
|
||||
assert!(pixie.contains(&"-K".to_string()));
|
||||
let pin = args(
|
||||
"bully",
|
||||
"wlan0mon",
|
||||
"00:11:22:33:44:55",
|
||||
6,
|
||||
Some("12345670"),
|
||||
);
|
||||
assert!(pin.windows(2).any(|w| w == ["-p", "12345670"]));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user