#6 Black Hat Rust. Наша первая программа на Rust: взломщик хэшей SHA-1.
Здравствуйте, дорогие друзья.
Настал момент закатать рукава, и написать нашу первую программу на Rust.
$ cargo new sha1_cracker
Создадим новый проект в папке sha1_cracker .
SHA-1 — это хэш-функция, используемая многими старыми веб-сайтами для хранения паролей пользователей. Теоретически, хэшированный пароль не может быть восстановлен по его хэшу, и, таким образом, сохраняя хэш в своих базах данных, веб-сайт может утверждать, что данный пользователь знает его пароль, не сохраняя пароль в открытом виде. Таким образом, если база данных веб-сайта взломана, восстановить пароли и получить доступ к данным пользователей невозможно.
Реальность совсем иная. Давайте представим сценарий, в котором мы только что взломали такой веб-сайт и теперь хотим восстановить учетные данные пользователей, чтобы получить доступ к их учетным записям. Вот где полезен “взломщик хэшей”. Взломщик хэшей — это программа, которая перепробует множество различных хэшей, чтобы найти исходный пароль.
Эта простая программа поможет нам изучить основы Rust.
Как и почти во всех языках программирования, точкой входа в программу Rust является основная функция.
1 2 3 |
fn main { // ... } |
Чтение аргументов командной строки так же просто, как:
1 2 3 4 |
use std::env; fn main { let args: Vec<String> = env::args().collect(); } |
Где use std::env
импортирует модуль env
из стандартной библиотеки, а env::args()
вызывает метод args
из этого модуля и возвращает итератор, который может быть “собран” в Vec<String>, вектор объектов String.
Затем легко проверить количество аргументов и отобразить сообщение об ошибке, если оно не соответствует ожидаемому.
1 2 3 4 5 6 7 8 9 |
use std::env; fn main { let args: Vec<String> = env::args().collect(); if args.len() != 3 { println!("Usage:"); println!("sha1_cracker: <wordlist.txt> <sha1_hash>"); return; } } |
Как Вы, возможно, заметили, синтаксис println! с восклицательным знаком странный. Действительно, println! — это не классическая функция, а макрос.
println! это макрос, а не функция, потому что Rust не поддерживает (пока?) вариативные обобщения.
Обработка ошибок
Как должна вести себя наша программа при обнаружении ошибки? И как сообщить об этом пользователю? Это то, что мы называем обработкой ошибок.
Среди дюжины языков программирования, с которыми у меня есть опыт работы, Rust, без всяких сомнений, является моим любимым языком обработки ошибок из-за его ясности, безопасности и лаконичности. Поскольку это также хорошо документировано и не является темой данной статьи, вот, безусловно, один из самых актуальных ресурсов по этому поводу: https://nick.groenen.me/posts/rust-error-handling
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use std::{ env, error::Error, }; const SHA1_HEX_STRING_LENGTH: usize = 40; fn main() -> Result<(), Box<dyn Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 3 { println!("Usage:"); println!("sha1_cracker: <wordlist.txt> <sha1_hash>"); return Ok(()); } let hash_to_crack = args[2].trim(); if hash_to_crack.len() != SHA1_HEX_STRING_LENGTH { return Err("sha1 hash is not valid".into()); } Ok(()) } |
Чтение файлов
Поскольку проверка всех возможных комбинаций букв, цифр и специальных символов может занять слишком много времени, нам нужно уменьшить количество SHA-1, которые мы будем генерировать. Мы будем использовать специальный вид словаря, известный как список слов, который содержит наиболее распространенный пароль, найденный на взломанных веб-сайтах. Чтение файла в Rust может быть достигнуто с помощью стандартной библиотеки, подобной этой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
use std::{ env, error::Error, fs::File, io::{BufRead, BufReader}, }; const SHA1_HEX_STRING_LENGTH: usize = 40; fn main() -> Result<(), Box<dyn Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 3 { println!("Usage:"); println!("sha1_cracker: <wordlist.txt> <sha1_hash>"); return Ok(()); } let hash_to_crack = args[2].trim(); if hash_to_crack.len() != SHA1_HEX_STRING_LENGTH { return Err("sha1 hash is not valid".into()); } let wordlist_file = File::open(&args[1])?; let reader = BufReader::new(&wordlist_file); for line in reader.lines() { let line = line?.trim().to_string(); println!("{}", line); } Ok(()) } |
Crates
Теперь, когда базовая структура нашей программы готова, нам действительно нужно вычислить хэши SHA-1. К счастью для нас, несколько талантливых разработчиков уже разработали этот сложный фрагмент кода и выложили его в сеть, готовым к использованию в виде внешней библиотеки. В rust мы называем эти библиотеки, или пакеты, crates. Их можно просмотреть онлайн по адресу https://crates.io.
Они управляются с помощью Cargo: менеджер пакетов Rust. Перед использованием crate в нашей программе, нам необходимо объявить его версию в файле манифеста Cargo: ‘Cargo.toml“.
1 2 3 4 5 6 7 8 9 |
[package] name = "sha1_cracker" version = "0.1.0" authors = ["Sylvain Kerkour"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] sha-1 = "0.9" hex = "0.4" |
Затем мы можем импортировать его в наш SHA-1 cracker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
use sha1::Digest; use std::{ env, error::Error, fs::File, io::{BufRead, BufReader}, }; const SHA1_HEX_STRING_LENGTH: usize = 40; fn main() -> Result<(), Box<dyn Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 3 { println!("Usage:"); println!("sha1_cracker: <wordlist.txt> <sha1_hash>"); return Ok(()); } let hash_to_crack = args[2].trim(); if hash_to_crack.len() != SHA1_HEX_STRING_LENGTH { return Err("sha1 hash is not valid".into()); } let wordlist_file = File::open(&args[1])?; let reader = BufReader::new(&wordlist_file); for line in reader.lines() { let line = line?; let common_password = line.trim(); if hash_to_crack == &hex::encode(sha1::Sha1::digest(common_password.as_bytes())) { println!("Password found: {}", &common_password); return Ok(()); } } println!("password not found in wordlist :("); Ok(()) } |
Ужас! Наша первая программа завершена. Мы можем протестировать ее, запустив:
$ cargo run -- wordlist.txt 7c6a61c68ef8b9b6b061b28c348bc1ed7921cb53
Пожалуйста, обратите внимание, что в реальном контексте мы можем использовать оптимизированные хэш-крекеры, такие как hashcat или John the Ripper.
RAII
Деталь, возможно, привлекла внимание самых дотошных из Вас: мы открываем файл списка слов, но никогда его не закрываем!
Этот шаблон (или функция) называется RAII: Получение ресурсов — это инициализация: в Rust переменные не только представляют части памяти компьютера, они также могут владеть ресурсами. Всякий раз, когда объект выходит за пределы области видимости, вызывается его деструктор, и принадлежащие ресурсы освобождаются.
В нашем случае переменная wordlist_file владеет файлом и имеет функцию main в качестве области видимости. Всякий раз, когда функция main завершает работу, либо из-за ошибки или при досрочном возврате принадлежащий файл закрывается.
Волшебство, не так ли? Благодаря этому утечка ресурсов в Rust чрезвычайно затруднена.
Ok(())
Возможно, Вы также заметили, что последняя строка нашей основной функции не содержит ключевого слова return. Это потому, что Rust — язык, ориентированный на выражения. Выражения принимают значение, а их противоположности, операторы, — это инструкции, которые что-то делают и заканчиваются точкой с запятой ( ; ).
Таким образом, если наша программа достигнет последней строки функции main, функция main примет значение Ok(()), что означает успех: все прошло по плану. Эквивалентом было бы:
return Ok(());
но не:
Ok(());
потому что здесь Ok(()); является оператором из-за точки с запятой, и функция main больше не вычисляет ожидаемый тип возвращаемого значения: Result.
На этом все. Всем хорошего дня!