In the field of cybersecurity, it’s important for professionals to be familiar with the techniques used by adversaries in password cracking. This article will explore how to create a multithreaded password cracker in Rust, leveraging its concurrency features and robust type system to build an efficient and reliable solution.
Project Overview
This password cracker supports three hashing algorithms: SHA-256, MD5, and SHA-1. The program takes three command-line arguments: the hash type, the target hash, and the path to a dictionary file containing possible passwords. The cracker attempts to find the original password by comparing the hash of each password in the dictionary with the target hash.
Project Setup
First, we set up our Rust project with the necessary dependencies. Here is the Cargo.toml
file:
[package]
name = "password_cracker"
version = "0.1.0"
edition = "2021"
[dependencies]
sha2 = "0.9"
md-5 = "0.10.1"
sha1 = "0.10"
num_cpus = "1.13"
hex = "0.4"
These dependencies include various hashing libraries (sha2
, md-5
, sha1
), a library for determining the number of CPU cores (num_cpus
), and a library for encoding and decoding hexadecimal (hex
).
Main Program Structure
The main logic of the password cracker is contained in the main.rs
file. Below is the complete source code:
use sha2::{Sha256, Digest as Sha2Digest};
use md5::Md5;
use sha1::{Sha1, Digest as Sha1Digest};
use std::fs::File;
use std::io::{BufRead, BufReader, Error};
use std::env;
use std::sync::{Arc, Mutex};
use std::thread;
#[derive(Clone)]
enum HashType {
Sha256,
Md5,
Sha1,
}
fn main() -> Result<(), Error> {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} <hash_type> <hash> <dictionary>", args[0]);
std::process::exit(1);
}
let hash_type = match args[1].as_str() {
"sha256" => HashType::Sha256,
"md5" => HashType::Md5,
"sha1" => HashType::Sha1,
_ => {
eprintln!("Unsupported hash type: {}", args[1]);
std::process::exit(1);
}
};
let target_hash = args[2].clone();
let dictionary_path = args[3].clone();
match crack_password(hash_type, &target_hash, &dictionary_path) {
Some(password) => println!("Password found: {}", password),
None => println!("Password not found"),
}
Ok(())
}
fn crack_password(hash_type: HashType, target_hash: &str, dictionary_path: &str) -> Option<String> {
let file = match File::open(dictionary_path) {
Ok(file) => file,
Err(e) => {
eprintln!("Could not open dictionary file: {}", e);
return None;
}
};
let reader = BufReader::new(file);
let passwords: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.collect();
let target_hash = Arc::new(target_hash.to_string());
let found_password = Arc::new(Mutex::new(None));
let num_threads = num_cpus::get();
let chunk_size = (passwords.len() / num_threads) + 1;
let mut threads = vec![];
for chunk in passwords.chunks(chunk_size) {
let target_hash = Arc::clone(&target_hash);
let found_password = Arc::clone(&found_password);
let chunk = chunk.to_vec();
let hash_type = hash_type.clone();
let handle = thread::spawn(move || {
for password in chunk {
let hash = hash_password(&hash_type, &password);
if hash == *target_hash {
let mut found = found_password.lock().unwrap();
*found = Some(password);
break;
}
if found_password.lock().unwrap().is_some() {
break;
}
}
});
threads.push(handle);
}
for handle in threads {
handle.join().expect("Thread failed to join");
}
let result = found_password.lock().unwrap().clone();
result
}
fn hash_password(hash_type: &HashType, password: &str) -> String {
match hash_type {
HashType::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(password);
let result = hasher.finalize();
hex::encode(result)
},
HashType::Md5 => {
let mut hasher = Md5::new();
hasher.update(password);
let result = hasher.finalize();
hex::encode(result)
},
HashType::Sha1 => {
let mut hasher = Sha1::new();
hasher.update(password);
let result = hasher.finalize();
hex::encode(result)
},
}
}
Detailed Explanation
Hash Type Enum
We define an enum HashType
to represent the different types of hashes our program supports:
#[derive(Clone)]
enum HashType {
Sha256,
Md5,
Sha1,
}
Main Function
The main
function parses command-line arguments and determines the hash type, target hash, and dictionary path. It then calls the crack_password
function:
fn main() -> Result<(), Error> {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} <hash_type> <hash> <dictionary>", args[0]);
std::process::exit(1);
}
let hash_type = match args[1].as_str() {
"sha256" => HashType::Sha256,
"md5" => HashType::Md5,
"sha1" => HashType::Sha1,
_ => {
eprintln!("Unsupported hash type: {}", args[1]);
std::process::exit(1);
}
};
let target_hash = args[2].clone();
let dictionary_path = args[3].clone();
match crack_password(hash_type, &target_hash, &dictionary_path) {
Some(password) => println!("Password found: {}", password),
None => println!("Password not found"),
}
Ok(())
}
Password Cracking Logic
The crack_password
function reads the dictionary file and splits the passwords into chunks for multithreading. It creates threads to process each chunk, hashing passwords and comparing them with the target hash:
fn crack_password(hash_type: HashType, target_hash: &str, dictionary_path: &str) -> Option<String> {
let file = match File::open(dictionary_path) {
Ok(file) => file,
Err(e) => {
eprintln!("Could not open dictionary file: {}", e);
return None;
}
};
let reader = BufReader::new(file);
let passwords: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.collect();
let target_hash = Arc::new(target_hash.to_string());
let found_password = Arc::new(Mutex::new(None));
let num_threads = num_cpus::get();
let chunk_size = (passwords.len() / num_threads) + 1;
let mut threads = vec![];
for chunk in passwords.chunks(chunk_size) {
let target_hash = Arc::clone(&target_hash);
let found_password = Arc::clone(&found_password);
let chunk = chunk.to_vec();
let hash_type = hash_type.clone();
let handle = thread::spawn(move || {
for password in chunk {
let hash = hash_password(&hash_type, &password);
if hash == *target_hash {
let mut found = found_password.lock().unwrap();
*found = Some(password);
break;
}
if found_password.lock().unwrap().is_some() {
break;
}
}
});
threads.push(handle);
}
for handle in threads {
handle.join().expect("Thread failed to join");
}
let result = found_password.lock().unwrap().clone();
result
}
Hashing Function
The hash_password
function generates the hash for a given password using the specified hash type:
fn hash_password(hash_type: &HashType, password: &str) -> String {
match hash_type {
HashType::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(password);
let result = hasher.finalize();
hex::encode(result)
},
HashType::Md5 => {
let mut hasher = Md5::new();
hasher.update(password);
let result = hasher.finalize();
hex::encode(result)
},
HashType::Sha1 => {
let mut hasher = Sha1::new();
hasher.update(password);
let result = hasher.finalize();
hex::encode(result)
},
}
}
Conclusion
The Rust password cracker we created demonstrates the power and efficiency of Rust’s concurrency model. By leveraging multithreading and the robust standard library, we built a tool capable of efficiently cracking hashed passwords using a dictionary attack. This project showcases how Rust’s features can be utilized to create high-performance and secure applications in the field of cybersecurity.