Building a Multithreaded Password Cracker in Rust

Bwiz
4 min readJul 6, 2024

--

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.

--

--