D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Всем привет! В этой статье (а может быть и цикле статей) хотелось бы рассказать о написания малвари на раст под виндой. Статья рассчитана на пользоваталей, умеющих работать с winapi, минимальными заниями какого-то либо другого системного языка программирования.
Rust представляет из себя компилируемый системный язык программирования, одной из главных фишек которого является безопасность. Некоторые воспринимают его как замену С++, но это не совсем так. Раст - этакий прокаченный С++, такой же blazing fast, но, в отличии от плюсов - memory safe, за счёт таких механизмов как Borrow Checker (который поначалу отпугивает новичков). О самом языке, его изучению воду лить не буду - читаем растбук - самую прекрасную доку в мире! Давайте преступим.
Начнём с самого нудного - с установки всего необходимого. Я опишу максимально кратко, будут вопросы - пишите в теме, помогу.
Я использую Windows 10 20H2 x64, вы можете выбрать любую версию (на ваш вкус).
Для комфортной разработки нам потребуется установить следующий стек:
1) Visual Studio C++ - нам понадобятся только линкер, дебаггер, но я установлю визуалку целиком
2) Тулчейн раста - компилятор (rustc), менеджер пакетов (cargo) и тд
3) Visual Studio Code или VSCodium (версия VSCode без телеметрии)
3.1) Расширение rust-analyzer - Language Server Provider раста - превратит из нашего текстового реадктора VSCode почти в полноценную IDE
3.2) Расширение Even Better TOML - для удобной работы с файлами конфигурации TOML
3.3) C/C++ - для удобной пошаговой отладки нашего кода
4) GNU Make - не обязательно, использую по привычке
Установка Visual Studio C++ Build Tools
Качаем установщик Community-версии отсюда: https://visualstudio.microsoft.com/downloads/
В установщике выбираем вот эти пункты и устанавливаем:
По окончанию установщик попросит перезагрузить компьютер, перезагружаем.
Установка тулчейна Rust
Запускаем rustup, выбираем следующие настройки:
По окончанию установки в консоли увидим следующее:
Давайте проверим всё ли корректно установилось:
Отлично!
Установка VSCode
Качаем установщик отсюда: https://code.visualstudio.com/download
Проблем с установкой, думаю, возникнуть не должно. Сразу же установим плагины Even Better TOML, rust-analyzer. Если используете VSCodium - расширения С/С++ нет в репозиториях, поэтому качаем по ссылке: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools и устанавливаем вручную.
Установка GNU Make
Качаем, компилируем: https://www.gnu.org/software/make/
Я сохранил бинарник в папку
После установки давайте создадим простой проект.
Я создал папку
Теперь создадим проект:
Cargo сгенерировал следующий проект:
Отлично, давайте запустим:
Видим ожидаемый результат.
Теперь давайте настроим проект для дальнейшей разработки. В настройках плагина rust-analyzer исправим:
1) Rust-analyzer > Check > Command -> clippy - будем использовать линтер clippy
2) Rust-analyzer > Debug > Engine -> ms-vscode.cpptools - для использования отладчика от визуалки
Теперь создадим пару файлов в папке %PROJECT_DIR%/.vscode launch.json, tasks.json
launch.json:
JSON: Скопировать в буфер обмена
tasks.json:
JSON: Скопировать в буфер обмена
Поставим бряк, запустим:
Отлично, всё работает.
Давайте глянем на получившийся бинарник в релизе:
Видим в импорте много api-ms-crt***.dll - не дело, нам не нужны лишние зависимости. Давайте исправим это. Слинкуем CRT статически: создаём файлик
Код: Скопировать в буфер обмена
Компилируем, смотрим импорты... Красота? Не очень. Лучше бы их вообще не было, да и вес бинарника многоват. Давайте исправим это. Раст хоть и позиционируется как язык с обилием zero cost abstractions, тем не менее, имеет свой небольшой
рантайм с зависимостью от сишного рантайма (CRT). Давайте всё это дело выпилим, чтоб на выходе у нас был миниатюрный бинарник. Нам для этого нужно написать свой небольшой рантайм, реализовав глобальный аллокатор, некоторые CRT-функции. Звучит сложновато, но это не совсем так. Первое, что нам понадобится - научиться как-либо взаимодействовать с winapi. В этом нам помогут официальные биндинги от microsoft: https://github.com/microsoft/windows-rs
Есть 2 крейта: windows и windows-sys, нам нужен первый, с удобными абстракциями, но он зависит от стандартной библиотеки раста - не проблема. Биндинги генерируются на основе метаданных, поставляемых самими майками, но править уже сгенерированные биндинги чуть более чем бессмысленно, ведь у нас есть исходники самого биндгена.
Качаем исходники отсюда https://github.com/microsoft/windows-rs/tree/0.50.0 и давайте разбираться.
Я сохранил их в папку
Код: Скопировать в буфер обмена
Ну и вызовем дефолтный месседж:
Код: Скопировать в буфер обмена
Отлично, работает! Но в нашем коде каждый вызов winapi-функции нужно оборачивать в блок unsafe {} - не очень удобно, давайте исправим это. Для этого нам нужно чуть-чуть изменить bindgen - чтоб генерировались не unsafe-обёртки над
winapi-функциями, а весь ансейф скрывался внутри самой обёртки.
Сами обёртки генерируются в функции
Изменим блок:
Код: Скопировать в буфер обмена
На:
Код: Скопировать в буфер обмена
Аналогично поступаем с другими блоками -
Так же по всему файлу заменим использование либы std на core, это поможет нам в дальнейшем:
Отлично! Теперь давайте сгенерируем биндинги:
Теперь можно изменить код нашего хелло-ворлда, убрав блок unsafe:
Код: Скопировать в буфер обмена
Компилируем, запускаем - ошибок нет, идём дальше.
Проделываем те же самые операции в файлах
https://crates.io/crates/spin (не забудьте добавить его как зависимость в функции
Код: Скопировать в буфер обмена
Импортируем макрос в
Ну и в финале добавляем в наши сгенерированные биндинги строки:
Код: Скопировать в буфер обмена
Запускаем, проверяем и идём дальше.
С биндингами разобрались, теперь нужно так же исправить крейт core для компиляции без стандартной библиотеки. Я понимаю что это нудно, поэтому прикреплю оба крейта в аттаче. Но я бы на вашем месте вручную всё сделал, для более полной картины представления о том как это всё устроено под капотом.
Теперь давайте напишем миниатюрный рантайм - аллокатор, panic-handler, некоторые CRT-функции.
Для начала копируем наши сгенерированные биндинги (core, implement, interface, windows) из папки
Для отладки удобно было бы использовать всякие фишки из стандартной библиотеки, не так ли? А production-ready билд уже
выдавать абсолютно голым. Давайте для этого создадим фичу, при активации которой будет компилироваться со стандартной либой.
Идём в
Код: Скопировать в буфер обмена
Теперь чуток исправим наш hello_world/main.rs, обавим в самом верху:
Код: Скопировать в буфер обмена
И атрибут
Код: Скопировать в буфер обмена
Давайте напишем простенький Makefile для удобства сборки:
Makefile: Скопировать в буфер обмена
Так же сразу изменим конфигурацию VSCode:
JSON: Скопировать в буфер обмена
Попробуем скомпилить:
Ловим ошибки, давайте разберемся как их исправить.
Одна из фишек раста - компилятор выдаёт понятные челловеку ошибки и, в некоторых случаях, возможные пути решения. Давайте исправим ошибки. Исправим третью ошибку изменение конфигурацию сборки для релиз-профиля, отключив раскрутку стека при панике, включив оптимизацию по размеру (оптимизация по размеру не обязательна):
Код: Скопировать в буфер обмена
Теперь давайте создадим файлик
Код: Скопировать в буфер обмена
Там напишем самый проcтой обработчик паники, а точнее затычку:
Код: Скопировать в буфер обмена
Осталось реализовать глобальный аллокатор - тут тоже ничего сложного нет, мы же уже умеем работать с winapi?)
Создаём файл
Код: Скопировать в буфер обмена
Теперь давайте напишем сам аллокатор, а точнее немного схитрим - скопируем стандартный из файла
Код: Скопировать в буфер обмена
Полный код будет в аттаче, но опять же желательно всё сделать самому для лучшего понимания происходящего.
Компилим:
И получаем бинарник весом 2.5 Кб, но хотелось бы знать откуда взялся этот вес, не так ли?
Ставим cargo-bloat:
В
Напишем простое правило в файле Makefile:
Makefile: Скопировать в буфер обмена
Запускаем и смотрим вывод:
Код: Скопировать в буфер обмена
Чистота!
На самом деле, в текущих реалиях дрочить на малый вес - это не больше чем отголоски прошлого, стереотипы "малый вес == хороший код" и тд. Мы же проделали все эти финты ушами с заделом на будущее - будем писать шеллкоды на расте.
Отлично, с этим справились, но какой-же это малваре-кодинг, если все импорты открыты? Давайте напишем динамический импорт с обработкой форвардов, который будет корректно резольвить большую часть апи винды.
Давайте обмозгуем как всё это реализовать. У нас есть макрос link!() (
Код: Скопировать в буфер обмена
Нам достаточно немного дописать этот макрос, чтоб адреса всех винапи резольвились в рантайме.
От этого и оттолкнёмся. Для начала обьеденим крейты windows и windows-core: создадим папку
Давайте добавим новую фичу в крейт windows в файл
Код: Скопировать в буфер обмена
Так же создадим новый модуль
Код: Скопировать в буфер обмена
Т.к адрес апи-функций мы будем искать по хэшу, давайте реализуем FNVA1 хэш-функцию в файле
Код: Скопировать в буфер обмена
Теперь давайте реализуем функцию получения адреса загруженного модуля-длл по хэшу имени. Список загруженных модулей в адрессное пространство процесса хранится в
Код: Скопировать в буфер обмена
И сразу же реализуем функцию получения адреса PEB для архитектур х86 и х64:
Код: Скопировать в буфер обмена
Элементами списка
Код: Скопировать в буфер обмена
Так же изменим структуру
Код: Скопировать в буфер обмена
Давайте теперь напишем саму функцию получения адреса загруженного модуля. Для этого нам нужно из PEB получить указатель на первый элемент односвязного списка, кастануть его к типу
Код: Скопировать в буфер обмена
Написать то написали, теперь его нужно протестировать и из интереса глянуть сколько он жрёт процессорного времени. Раст из
коробки предоставляет нам удобные юнит-тесты, бенчи.
Давайте в этом же файле обьявим модуль tests:
Код: Скопировать в буфер обмена
И напиишем простенький тест:
Код: Скопировать в буфер обмена
И запустим его командой (или напрямую из VSCode, как это делаю я):
И получаем:
Код: Скопировать в буфер обмена
Отлично, давайте теперь сравним скорость нашей функции get_module и апи GetModuleHandleW:
Код: Скопировать в буфер обмена
Получилось даже быстрее! Теперь давайте релизуем саму функцию получения адрес апи функции из загруженного модуля.
Для начала нам нужно получить адрес секции экспорта, тут ничего сложного нет:
Код: Скопировать в буфер обмена
Теперь нужно получить ординал нужной нам функции: итерируемся по дирктории экспорта, сравниваем хэш с искомым:
Код: Скопировать в буфер обмена
Осталось только получить виртуальный адрес функции:
Код: Скопировать в буфер обмена
Но некоторые апи могу жить в других модулях, а в загруженом модуле может быть только форвард:
Нам нужно распарсить отсюда имя модуля, имя функции, загрузить модуль и поискать функцию в таблице экспорта.
Ничего сложного тут нет, парсим форвард:
Код: Скопировать в буфер обмена
Загружаем модуль и парсим таблицу экспорта, в поисках искомой функции:
Код: Скопировать в буфер обмена
Не забываем написать тесты, запустить их. Теперь давайте сравним скорость выполнения нашей кастомной функции get_export_func и стандартой апи GetProcAddress.
Код: Скопировать в буфер обмена
Разница огромна. Давайте это оптимизируем. Постоянно скакать по таблице экспорта модуля накладная задача, поэтому давайте просто кэшировать адреса функций - реализация будет в аттаче к статье. Да, первый вызов функции будет такой же времязатратный, в таком случае можно перед стартом приложения получать адреса всех используемых функций.
Давайте глянем выхлоп бенча после применения небольших оптимизаций:
Уже неплохо. Осталось чуть-чуть переписать макросы линковки:
Код: Скопировать в буфер обмена
Давайте глянем что у нас в итоговом бинарнике:
Код: Скопировать в буфер обмена
Отлично. В этой статье мы разобрались как установить весь необходимый тулинг для удобной работы с растом, написали
(адаптировали под свои нужды, скорее) основу основ практически любого приложения под виндой. Весь код я писал параллельно со статьёй, поэтому оставил некоторые моменты за кадром. Как и обещал, в аттаче к статье прикреплю готовые исходники. Спасибо что дочитали до конца. Рад буду увидеть любой отклик от вас, будь он положительный или отрицательный.
В следующей статье в планах написать хеллоу ворлд от мира малвари - клиппер.
Автор reqwest
Источник https://xss.is/
Rust представляет из себя компилируемый системный язык программирования, одной из главных фишек которого является безопасность. Некоторые воспринимают его как замену С++, но это не совсем так. Раст - этакий прокаченный С++, такой же blazing fast, но, в отличии от плюсов - memory safe, за счёт таких механизмов как Borrow Checker (который поначалу отпугивает новичков). О самом языке, его изучению воду лить не буду - читаем растбук - самую прекрасную доку в мире! Давайте преступим.
Начнём с самого нудного - с установки всего необходимого. Я опишу максимально кратко, будут вопросы - пишите в теме, помогу.
Я использую Windows 10 20H2 x64, вы можете выбрать любую версию (на ваш вкус).
Для комфортной разработки нам потребуется установить следующий стек:
1) Visual Studio C++ - нам понадобятся только линкер, дебаггер, но я установлю визуалку целиком
2) Тулчейн раста - компилятор (rustc), менеджер пакетов (cargo) и тд
3) Visual Studio Code или VSCodium (версия VSCode без телеметрии)
3.1) Расширение rust-analyzer - Language Server Provider раста - превратит из нашего текстового реадктора VSCode почти в полноценную IDE
3.2) Расширение Even Better TOML - для удобной работы с файлами конфигурации TOML
3.3) C/C++ - для удобной пошаговой отладки нашего кода
4) GNU Make - не обязательно, использую по привычке
Установка Visual Studio C++ Build Tools
Качаем установщик Community-версии отсюда: https://visualstudio.microsoft.com/downloads/
В установщике выбираем вот эти пункты и устанавливаем:

По окончанию установщик попросит перезагрузить компьютер, перезагружаем.
Установка тулчейна Rust
Запускаем rustup, выбираем следующие настройки:

host triple i686-pc-windows-msvc
- для сборки под х86 с использованием линкера msvcnightly
- понадобится нам в дальнейшем, т.к в стейбле нет некоторых нужных нам фичПо окончанию установки в консоли увидим следующее:

Давайте проверим всё ли корректно установилось:

Отлично!
Установка VSCode
Качаем установщик отсюда: https://code.visualstudio.com/download
Проблем с установкой, думаю, возникнуть не должно. Сразу же установим плагины Even Better TOML, rust-analyzer. Если используете VSCodium - расширения С/С++ нет в репозиториях, поэтому качаем по ссылке: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools и устанавливаем вручную.
Установка GNU Make
Качаем, компилируем: https://www.gnu.org/software/make/
Я сохранил бинарник в папку
%ProgramFiles%/make-4.4.1/make.exe
, не забываем добавить в env %PATH%
.После установки давайте создадим простой проект.
Я создал папку
%USERPROFILE%/Desktop/projects
, переходим в неё:cd %USERPROFILE%/Desktop/projects
Теперь создадим проект:
cargo new hello_world

Cargo сгенерировал следующий проект:

Отлично, давайте запустим:

Видим ожидаемый результат.
Теперь давайте настроим проект для дальнейшей разработки. В настройках плагина rust-analyzer исправим:
1) Rust-analyzer > Check > Command -> clippy - будем использовать линтер clippy
2) Rust-analyzer > Debug > Engine -> ms-vscode.cpptools - для использования отладчика от визуалки
Теперь создадим пару файлов в папке %PROJECT_DIR%/.vscode launch.json, tasks.json
launch.json:
JSON: Скопировать в буфер обмена
Код:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug x86",
"type": "cppvsdbg", // юзаем откладчик msvc
"request": "launch",
"preLaunchTask": "build debug",
"program": "${workspaceFolder}/target/debug/hello_world.exe", // путь до скомпиленого бинарника
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"console": "integratedTerminal",
"symbolOptions": {
"searchPaths": [
"http://msdl.microsoft.com/download/symbols",
],
"cachePath": "%appdata%/debug_symbols", // в эту папку сохранятся дебагсимволы
},
}
]
}
JSON: Скопировать в буфер обмена
Код:
{
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"command": "build", // по дефолту бинарник собирается в дебаге
"problemMatcher": [
"$rustc"
],
"group": "build",
"label": "build debug" // этот лейбл используется в конфигурации запуска выше
}
]
}

Отлично, всё работает.
Давайте глянем на получившийся бинарник в релизе:
cargo build --release

Видим в импорте много api-ms-crt***.dll - не дело, нам не нужны лишние зависимости. Давайте исправим это. Слинкуем CRT статически: создаём файлик
%PROJECT_DIR%/.cargo/Config.toml
со следующим содержимым:Код: Скопировать в буфер обмена
Код:
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))']
rustflags = [
"-C", "target-feature=+crt-static",
]
рантайм с зависимостью от сишного рантайма (CRT). Давайте всё это дело выпилим, чтоб на выходе у нас был миниатюрный бинарник. Нам для этого нужно написать свой небольшой рантайм, реализовав глобальный аллокатор, некоторые CRT-функции. Звучит сложновато, но это не совсем так. Первое, что нам понадобится - научиться как-либо взаимодействовать с winapi. В этом нам помогут официальные биндинги от microsoft: https://github.com/microsoft/windows-rs
Есть 2 крейта: windows и windows-sys, нам нужен первый, с удобными абстракциями, но он зависит от стандартной библиотеки раста - не проблема. Биндинги генерируются на основе метаданных, поставляемых самими майками, но править уже сгенерированные биндинги чуть более чем бессмысленно, ведь у нас есть исходники самого биндгена.
Качаем исходники отсюда https://github.com/microsoft/windows-rs/tree/0.50.0 и давайте разбираться.
Я сохранил их в папку
%DESKTOP%/projects/windows-rs-0.50.0
. Давайте подключим этот крейт к нашему hello world'у:Код: Скопировать в буфер обмена
Код:
# hello_world/Cargo.toml:
[dependencies]
windows = { path = "../windows-rs-0.50.0/crates/libs/windows", features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"] }
Код: Скопировать в буфер обмена
Код:
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK};
use windows::core::w;
fn main() {
unsafe{ MessageBoxW(None, w!("hello"), w!("world!"), MB_OK);}
}
winapi-функциями, а весь ансейф скрывался внутри самой обёртки.
Сами обёртки генерируются в функции
crates/libs/bindgen/src/functions.rs -> gen_win_function()
- о происходящем внутри можно особо не вникать, ведь нам нужно исправить всего несколько строк:Изменим блок:
Код: Скопировать в буфер обмена
Код:
SignatureKind::Query(_) => {
...
quote! {
#doc
#features
#[inline]
pub unsafe fn #name<#generics>(#params) -> ::windows_core::Result<T> #where_clause {
#link
let mut result__ = ::core::ptr::null_mut();
#name(#args).from_abi(result__)
}
}
}
Код: Скопировать в буфер обмена
Код:
SignatureKind::Query(_) => {
...
quote! {
#doc
#features
#[inline]
pub fn #name<#generics>(#params) -> ::windows_core::Result<T> #where_clause {
#link
let mut result__ = ::core::ptr::null_mut();
unsafe { #name(#args).from_abi(result__) }
}
}
}
SignatureKind::QueryOptional(_) => {}
, SignatureKind::ResultValue => {}
, SignatureKind::ResultVoid => {}
и тд.Так же по всему файлу заменим использование либы std на core, это поможет нам в дальнейшем:
CTRL+F->::std -> ::core -> replace all
Отлично! Теперь давайте сгенерируем биндинги:
windows-rs-0.50.0> cargo run --bin tool_windows --release
Теперь можно изменить код нашего хелло-ворлда, убрав блок unsafe:
Код: Скопировать в буфер обмена
Код:
fn main() {
MessageBoxW(None, w!("hello"), w!("world!"), MB_OK);
}
Проделываем те же самые операции в файлах
com_methods.rs
, delegates.rs
. Некоторые фичи сидят в крейте alloc (например Vec/String/...) - импортируем их оттуда. Mutex/RwLock можем импортировать из крейта spin:https://crates.io/crates/spin (не забудьте добавить его как зависимость в функции
tools/windows/src/main.rs -> main()
), но проще просто пока что закомментить это. В аттаче прикреплю крейт с биндами со всеми фиксами. Так же нужно перенести макрос targets/lib.rs -> link!()
в core/lib.rs
, отредактировать файл windows-rs-0.50.0/crates/libs/bindgen/src/fynctions -> gen_link
:Код: Скопировать в буфер обмена
Код:
let tokens = tokens.trim_end_matches(", ");
format!(
r#"crate::link!("{library}" "{abi}"{symbol}{doc} fn {name}({tokens}){return_type});"#
)
.into()
windows-rs-0.50.0/libs/windows/src/lib.rs
:pub use windows_core::link;
Ну и в финале добавляем в наши сгенерированные биндинги строки:
Код: Скопировать в буфер обмена
Код:
#![no_std]
extern crate alloc;
С биндингами разобрались, теперь нужно так же исправить крейт core для компиляции без стандартной библиотеки. Я понимаю что это нудно, поэтому прикреплю оба крейта в аттаче. Но я бы на вашем месте вручную всё сделал, для более полной картины представления о том как это всё устроено под капотом.
Теперь давайте напишем миниатюрный рантайм - аллокатор, panic-handler, некоторые CRT-функции.
Для начала копируем наши сгенерированные биндинги (core, implement, interface, windows) из папки
windows-rs-0.50.0/crates/libs
в папку hello_world/libs
. Я добавил префикс windows- ко всем папкам для удобства.Для отладки удобно было бы использовать всякие фишки из стандартной библиотеки, не так ли? А production-ready билд уже
выдавать абсолютно голым. Давайте для этого создадим фичу, при активации которой будет компилироваться со стандартной либой.
Идём в
hello_world/Cargo.toml
и добавляем блок:Код: Скопировать в буфер обмена
Код:
[features]
std = []
Код: Скопировать в буфер обмена
Код:
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[cfg(feature = "std")]
extern crate std;
extern crate alloc;
no_managle
для точкии входа:Код: Скопировать в буфер обмена
Код:
#[cfg_attr(not(feature = "std"), no_mangle)]
fn main() { ... }
Makefile: Скопировать в буфер обмена
Код:
debug:
cargo build --features std
default:
set RUSTFLAGS=-Clink-arg=/ENTRY:main -Clink-arg=/SUBSYSTEM:WINDOWS && cargo build --release
std:
cargo build --release --features std
clean:
cargo clean
hello_world/.vscode/tasks.json
:JSON: Скопировать в буфер обмена
Код:
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"command": "make debug",
"problemMatcher": [
"$rustc"
],
"group": "build",
"label": "build debug"
}
]
}
make default
Ловим ошибки, давайте разберемся как их исправить.
error: no global memory allocator found but one is required; link to std or add `#[global_allocator]` to a static item that implements the GlobalAlloc trait
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality` | = note: this can occur when a binary crate with `#![no_std]` is compiled for a target where `eh_personality` is defined in the standard library = help: you may be able to compile for a target that doesn't need `eh_personality`, specify a target with `--target` or in `.cargo/config`
Одна из фишек раста - компилятор выдаёт понятные челловеку ошибки и, в некоторых случаях, возможные пути решения. Давайте исправим ошибки. Исправим третью ошибку изменение конфигурацию сборки для релиз-профиля, отключив раскрутку стека при панике, включив оптимизацию по размеру (оптимизация по размеру не обязательна):
Код: Скопировать в буфер обмена
Код:
[profile.release]
opt-level = "z"
lto = true
panic = "abort"
strip = true
hello_world/src/runtime/mod.rs
и обьявим его в main.rs
:Код: Скопировать в буфер обмена
Код:
#[cfg(not(feature = "std"))]
mod runtime;
Код: Скопировать в буфер обмена
Код:
#[cfg(not(test))]
#[panic_handler]
unsafe fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
Создаём файл
hello_world/src/runtime/allocator.rs
подключим его в mod.rs
:Код: Скопировать в буфер обмена
Код:
mod allocator;
#[global_allocator]
static ALLOCATOR: allocator::HeapAllocator = allocator::HeapAllocator;
%USERPROFILE%\.rustup\toolchains\%TARGET%\lib\rustlib\src\rust\library\std\src\sys\windows\alloc.rs
с минимальными правками:Код: Скопировать в буфер обмена
Код:
use windows::Win32::System::Memory::{
GetProcessHeap, HeapAlloc, HeapFree, HeapReAlloc, HEAP_FLAGS, HEAP_ZERO_MEMORY, HeapHandle,
};
static HEAP: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
...
Компилим:
make default
И получаем бинарник весом 2.5 Кб, но хотелось бы знать откуда взялся этот вес, не так ли?
Ставим cargo-bloat:
cargo install cargo-bloat
В
Cargo.toml
включим дебаг-символы:strip = true
Напишем простое правило в файле Makefile:
Makefile: Скопировать в буфер обмена
Код:
prof:
set RUSTFLAGS=-Clink-arg=/ENTRY:main -Clink-arg=/SUBSYSTEM:WINDOWS && cargo bloat --release
Код: Скопировать в буфер обмена
Код:
File .text Size Crate Name
0.7% 4.1% 21B [Unknown] _main
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
16.7% 100.0% 512B .text section size, the file size is 3.0KiB
На самом деле, в текущих реалиях дрочить на малый вес - это не больше чем отголоски прошлого, стереотипы "малый вес == хороший код" и тд. Мы же проделали все эти финты ушами с заделом на будущее - будем писать шеллкоды на расте.
Отлично, с этим справились, но какой-же это малваре-кодинг, если все импорты открыты? Давайте напишем динамический импорт с обработкой форвардов, который будет корректно резольвить большую часть апи винды.
Давайте обмозгуем как всё это реализовать. У нас есть макрос link!() (
windows-core/src/lib.rs
), отвечающий за линковку:Код: Скопировать в буфер обмена
Код:
pub fn MessageBoxW<...>(...) -> MESSAGEBOX_RESULT
{
crate::link!("user32.dll" "system" fn MessageBoxW(...) -> MESSAGEBOX_RESULT);
unsafe { MessageBoxW(...) }
}
Нам достаточно немного дописать этот макрос, чтоб адреса всех винапи резольвились в рантайме.
От этого и оттолкнёмся. Для начала обьеденим крейты windows и windows-core: создадим папку
windows/src/windows_core
, скопируем туда исходники крейта windows-core, переименуем lib.rs
в mod.rs
. Так же нужно будет сгенерировать биндинги снова, заменив ::windows_core
на crate::windows_core
в крейте windows-0.50.0/crates/libs/bindgen
.Давайте добавим новую фичу в крейт windows в файл
windows/Cargo.toml
:Код: Скопировать в буфер обмена
Код:
dyn_import = [
"Win32_Foundation",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Kernel",
"Win32_System_WindowsProgramming",
"Win32_System_Diagnostics_Debug",
"Win32_System_SystemInformation",
"Win32_System_Memory",
]
windows/src/windows_core/dyn_import/mod.rs
и обьявим его в файле windows/windows_core/mod.rs
:Код: Скопировать в буфер обмена
Код:
#[cfg(feature = "dyn_import")]
mod dyn_import;
#[cfg(feature = "dyn_import")]
pub use dyn_import::*;
windows/src/windows_core/fnva1.rs
:Код: Скопировать в буфер обмена
Код:
const FNV_OFFSET_BASIS_32: u32 = 0x811c9dc5;
const FNV_PRIME_32: u32 = 0x01000193;
pub const fn fnv1a_hash(bytes: &[u8]) -> u32 {
...
}
pub const fn fnv1a_hash_utf16(bytes: &[u16]) -> u32 {
...
}
#[inline(always)]
pub const fn fnv1a_hash_str(input: &str) -> u32 {
fnv1a_hash(input.as_bytes())
}
PEB->Ldr->InMemoryOrderModuleList
. Структура PEB нас уже есть в биндингах windows/src/Windows/Win32/System/Threading/mod.rs
, но она немного кастрированная, я её немного дополнил:Код: Скопировать в буфер обмена
Код:
#[repr(C)]
#[doc = "*Required features: `\"Win32_System_Threading\"`, `\"Win32_Foundation\"`, `\"Win32_System_Kernel\"`*"]
#[cfg(all(feature = "Win32_Foundation", feature = "Win32_System_Kernel"))]
pub struct PEB {
pub Reserved1: [u8; 2],
pub BeingDebugged: u8,
pub Reserved2: [u8; 1],
pub Reserved3: [*mut ::core::ffi::c_void; 2],
pub Ldr: *mut PEB_LDR_DATA,
pub ProcessParameters: *mut RTL_USER_PROCESS_PARAMETERS,
pub SubSystemData: *mut ::core::ffi::c_void,
pub ProcessHeap: super::Memory::HeapHandle,
pub Reserved4: [u8; 16],
pub AtlThunkSListPtr: *mut ::core::ffi::c_void,
pub Reserved5: *mut ::core::ffi::c_void,
pub Reserved6: u32,
pub Reserved7: *mut ::core::ffi::c_void,
pub Reserved8: u32,
pub AtlThunkSListPtr32: u32,
pub Reserved9: [*mut ::core::ffi::c_void; 45],
pub Reserved10: [u8; 96],
pub PostProcessInitRoutine: PPS_POST_PROCESS_INIT_ROUTINE,
pub Reserved11: [u8; 128],
pub Reserved12: [*mut ::core::ffi::c_void; 1],
pub SessionId: u32,
}
Код: Скопировать в буфер обмена
Код:
#[cfg(all(feature = "Win32_Foundation", feature = "Win32_System_Kernel"))]
impl PEB {
pub fn get_ptr() -> *mut PEB {
let mut ptr: *mut PEB;
unsafe {
#[cfg(target_arch = "x86")]
core::arch::asm!(
"mov {}, fs:[0x30]",
out(reg) ptr,
);
#[cfg(not(target_arch = "x86"))]
core::arch::asm!(
"mov {}, gs:[0x60]",
out(reg) ptr,
);
}
ptr
}
}
InMemoryOrderModuleList
являются указатели на структуру LDR_DATA_TABLE_ENTRY
, которая в наших биндах тоже урезанная (т.к андок), я её немного дополнил в файле windows/src/Windows/Wein32/System/WindowsProgramming/mod.rs
:Код: Скопировать в буфер обмена
Код:
pub struct LDR_DATA_TABLE_ENTRY {
pub InLoadOrderLinks: super::Kernel::LIST_ENTRY,
pub InMemoryOrderLinks: super::Kernel::LIST_ENTRY,
pub InInitializationOrderLinks: super::Kernel::LIST_ENTRY,
pub DllBase: *mut ::core::ffi::c_void,
pub EntryPoint: *mut ::core::ffi::c_void,
pub SizeOfImage: core::ffi::c_ulong,
pub FullDllName: super::super::Foundation::UNICODE_STRING,
pub BaseDllName: super::super::Foundation::UNICODE_STRING,
pub Flags: core::ffi::c_ulong,
pub LoadCount: core::ffi::c_ushort,
pub TlsIndex: core::ffi::c_ushort,
pub HashLinks: super::Kernel::LIST_ENTRY,
pub SectionPointer: *mut ::core::ffi::c_void,
pub CheckSum: core::ffi::c_ulong,
pub TimeDateStamp: u32,
pub LoadedImports: *mut ::core::ffi::c_void,
pub EntryPointActivationContext: *mut ::core::ffi::c_void,
pub PatchInformation: *mut ::core::ffi::c_void,
pub Unknown1: *mut ::core::ffi::c_void,
pub Unknown2: *mut ::core::ffi::c_void,
pub Unknown3: *mut ::core::ffi::c_void,
}
PEB_LDR_DATA
:Код: Скопировать в буфер обмена
Код:
pub struct PEB_LDR_DATA {
pub Reserved1: [u8; 4],
pub InLoadOrderModuleList: super::Kernel::LIST_ENTRY,
pub InMemoryOrderModuleList: super::Kernel::LIST_ENTRY,
}
LDR_DATA_TABLE_ENTRY
, получить хэш имени и сравнить с искомым, в случае успеха вернуть DllBase - ничего сложного. PEB получим с помощью написанного ранее метода PEB::get_ptr()
, как работать с односвязными списками мы знаем, так что ничего нам не мешает написать следующий код:Код: Скопировать в буфер обмена
Код:
fn slice_from_unicode_str(s: &UNICODE_STRING) -> &[u16] {
let len = if s.Length > 0 { s.Length / 2 } else { 0 };
unsafe { ::core::slice::from_raw_parts(s.Buffer.0, len as usize) }
}
pub fn get_module(name_hash: u32) -> Option<HMODULE> {
let peb = PEB::get_ptr();
let list_head = unsafe { (*(*peb).Ldr).InMemoryOrderModuleList };
let mut list_entry = list_head.Flink;
loop {
if unsafe { (*list_entry).Flink } == list_head.Flink {
break;
}
let module = list_entry as *mut LDR_DATA_TABLE_ENTRY;
let module_name = unsafe { slice_from_unicode_str(&(*module).BaseDllName) };
let module_hash = fnv1a_hash_utf16(module_name);
if module_hash == name_hash {
let addr = unsafe { (*module).DllBase };
if addr.is_null() {
return None;
}
return Some(HMODULE(addr as isize));
}
list_entry = unsafe { (*list_entry).Flink };
}
None
}
коробки предоставляет нам удобные юнит-тесты, бенчи.
Давайте в этом же файле обьявим модуль tests:
Код: Скопировать в буфер обмена
Код:
#[cfg(test)]
mod tests {
extern crate std;
extern crate test;
use super::*;
use crate::{windows_core::fnv1a_hash_str, s};
use windows::Win32::System::LibraryLoader::GetProcAddress;
use test::Bencher;
}
Код: Скопировать в буфер обмена
Код:
#[test]
fn get_module_test() {
let dlls = [
(fnv1a_hash_str("kernel32.dll"), true),
(fnv1a_hash_str("khyRyvgHWY.dll"), false),
(fnv1a_hash_str("ntdll.dll"), true),
(fnv1a_hash_str("UqpeNkOJcI.dll"), false),
];
for d in dlls {
match get_module(d.0) {
Some(v) => {
assert_ne!(v.0 as *const c_void, core::ptr::null_mut());
if !d.1 {
panic!("found dll where doesent exist");
}
}
None => {
if d.1 {
panic!("dll not found");
}
}
}
}
}
cargo test --package windows --lib -- windows_core::dyn_import::tests::get_module_test --exact --nocapture
И получаем:
Код: Скопировать в буфер обмена
Код:
running 1 test
test windows_core::dyn_import::tests::get_module_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
Код: Скопировать в буфер обмена
Код:
#[bench]
fn get_module_bench(b: &mut Bencher) {
let k32_hash = fnv1a_hash_str("kernel32.dll");
b.iter(|| {
let _ = get_module(k32_hash).unwrap();
});
}
#[bench]
fn get_module_handle_bench(b: &mut Bencher) {
let k32 = w!("kernel32.dll");
b.iter(|| {
let _ = GetModuleHandleW(k32).unwrap();
});
}
test windows_core::dyn_import::tests::get_module_bench ... bench: 86 ns/iter (+/- 4)
test windows_core::dyn_import::tests::get_module_handle_bench ... bench: 217 ns/iter (+/- 11)
Для начала нам нужно получить адрес секции экспорта, тут ничего сложного нет:
Код: Скопировать в буфер обмена
Код:
macro_rules! data_offset {
($data:expr, $offset:expr, $type:ty) => {
unsafe { ($data as *const u8).add($offset as usize) as *const $type }
};
}
fn get_export_dir(
dll: HMODULE,
dos: *const IMAGE_DOS_HEADER,
) -> (*const IMAGE_EXPORT_DIRECTORY, usize) {
#[cfg(target_arch = "x86")]
let nt_headers = data_offset!(dll.0, (*dos).e_lfanew, IMAGE_NT_HEADERS32);
#[cfg(target_arch = "x86_64")]
let nt_headers = data_offset!(dll.0, (*dos).e_lfanew, IMAGE_NT_HEADERS64);
let export = data_offset!(
dll.0,
(*nt_headers).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT.0 as usize]
.VirtualAddress,
IMAGE_EXPORT_DIRECTORY
);
let export_size = unsafe {
(*nt_headers).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT.0 as usize].Size
as usize
};
(export, export_size)
}
Код: Скопировать в буфер обмена
Код:
fn get_func_ordinal(
dll: HMODULE,
export_dir: *const IMAGE_EXPORT_DIRECTORY,
name_hash: u32,
) -> Option<*const u16> {
let addr_of_names = unsafe { (*export_dir).AddressOfNames } as usize;
let addr_of_ords = unsafe { (*export_dir).AddressOfNameOrdinals } as usize;
for i in 0..unsafe { (*export_dir).NumberOfNames } as usize {
let name_rva = data_offset!(dll.0, addr_of_names + i * core::mem::size_of::<u32>(), u32);
let name_va = data_offset!(dll.0, *name_rva, u8);
if unsafe { *name_va } != 0 {
let name = PCSTR::from_raw(name_va);
if fnv1a_hash(name.as_bytes()) == name_hash {
return Some(data_offset!(
dll.0,
addr_of_ords + i * core::mem::size_of::<u16>(),
u16
));
}
}
}
Option::None
}
Код: Скопировать в буфер обмена
Код:
let addr_of_funcs = unsafe { (*export).AddressOfFunctions };
let func_rva_addr =
data_offset!(dll.0, ((4 * *name_ordinal) as u32) + addr_of_funcs, u32);
let mut func_va_addr = data_offset!(dll.0, *func_rva_addr, c_void);
if func_va_addr.addr() > export.addr()
&& func_va_addr.addr() < export.addr() + export_size
{
func_va_addr = process_forward(func_va_addr)?;
}
Ok(func_va_addr)
api-ms-win-core-processthreads-l1-1-3.GetThreadDescription\0some trash here
Нам нужно распарсить отсюда имя модуля, имя функции, загрузить модуль и поискать функцию в таблице экспорта.
Ничего сложного тут нет, парсим форвард:
Код: Скопировать в буфер обмена
Код:
fn get_forward(buf: &[u8]) -> Result<([u8; 260], PCSTR), Err> {
let mut dll_name = [0_u8; 260];
match buf.iter().position(|b| *b == b'.') {
Some(v) => {
if v > dll_name.len() {
return Err(Err::DllNameTooMuch);
}
unsafe {
core::ptr::copy_nonoverlapping(buf.as_ptr(), dll_name.as_mut_ptr(), v);
core::ptr::copy_nonoverlapping(".dll".as_ptr(), dll_name[v..].as_mut_ptr(), 4);
}
Ok((dll_name, PCSTR::from_raw(buf[v + 1..buf.len()].as_ptr())))
}
None => Err(Err::ParseFuncForwardFailed),
}
}
Код: Скопировать в буфер обмена
Код:
fn process_forward(func_va_addr: *const c_void) -> Result<*const c_void, Err> {
let buf = PCSTR::from_raw(func_va_addr as *const u8);
let (dll_name, func_name) = get_forward(buf.as_bytes())?;
match LoadLibraryA(PCSTR::from_raw(dll_name.as_ptr())) {
Ok(v) => {
let func_hash = fnv1a_hash(func_name.as_bytes());
match get_export_func(v, func_hash) {
Ok(v) => Ok(v),
Err(_) => Err(Err::GetProcAddressFailed),
}
}
Err(e) => Err(Err::LoadLibraryFailed(e)),
}
}
Код: Скопировать в буфер обмена
Код:
test windows_core::dyn_import::tests::get_export_func_bench ... bench: 11,286 ns/iter (+/- 1,014)
test windows_core::dyn_import::tests::get_proc_address_bench ... bench: 115 ns/iter (+/- 4)
Давайте глянем выхлоп бенча после применения небольших оптимизаций:
test windows_core::dyn_import::tests::get_export_func_cached_bench ... bench: 2 ns/iter (+/- 0)
Уже неплохо. Осталось чуть-чуть переписать макросы линковки:
Код: Скопировать в буфер обмена
Код:
#[cfg(feature = "dyn_import")]
#[macro_export]
macro_rules! link {
($library:literal $abi:literal fn $name:ident($($arg:ident: $argty:ty),*)->$ret:ty) => (
let $name = {
//extern crate std;
//std::println!("{}", $library);
const DLL_NAME: u32 = $crate::windows_core::fnv1a_hash_str($library);
const FUNC_NAME: u32 = $crate::windows_core::fnv1a_hash_str(core::stringify!($name));
match $crate::windows_core::load_module(DLL_NAME) {
Ok(v) => {
match $crate::windows_core::get_export_func_cached(v, FUNC_NAME) {
Ok(v) => {
unsafe { core::mem::transmute::<*const core::ffi::c_void, unsafe extern "system" fn ($($arg: $argty),*) -> $ret>(v) }
}
Err(e) => core::panic!("{:?}", e),
}
}
Err(e) => core::panic!("{:?}", e),
}
};
)
}
#[cfg(not(feature = "dyn_import"))]
#[cfg(target_arch = "x86")]
#[macro_export]
macro_rules! link {
($library:literal $abi:literal fn $name:ident($($arg:ident: $argty:ty),*)->$ret:ty) => (
#[link(name = $library, kind = "raw-dylib", modifiers = "+verbatim", import_name_type = "undecorated")]
extern $abi {
pub fn $name($($arg: $argty),*) -> $ret;
}
)
}
#[cfg(not(feature = "dyn_import"))]
#[cfg(target_arch = "x86_64")]
#[macro_export]
macro_rules! link {
($library:literal $abi:literal fn $name:ident($($arg:ident: $argty:ty),*)->$ret:ty) => (
#[link(name = $library, kind = "raw-dylib", modifiers = "+verbatim")]
extern "system" {
pub fn $name($($arg: $argty),*) -> $ret;
}
)
}
pub use crate::link;
Код: Скопировать в буфер обмена
Код:
File .text Size Crate Name
13.0% 34.6% 531B windows windows::windows_core::dyn_import::get_export_func
6.4% 17.1% 263B windows _memcpy
4.5% 11.9% 183B windows windows::windows_core::dyn_import::cached::get_export_func_cached
4.2% 11.1% 170B windows windows::Windows::Win32::System::LibraryLoader::LoadLibraryA<windows::windows_core::strings::pcstr::PCSTR>
2.0% 5.4% 83B [Unknown] _main
1.1% 3.1% 47B windows windows::windows_core::fnva1::fnv1a_hash
0.7% 1.8% 28B spin spin::mutex::Mutex<array$<tuple$<u32,ptr_const$<core::ffi::c_void> >,1024> >::lock<array$<tuple$<u32,ptr_const$<core::ffi::c_void> >,1024> >
0.6% 1.6% 25B windows _memcpy
0.5% 1.2% 19B windows _memset
0.4% 1.0% 16B windows windows::windows_core::strings::pcstr::PCSTR::as_bytes
0.0% 0.1% 2B std core::panicking::panic_fmt
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
37.5% 100.0% 1.5KiB .text section size, the file size is 4.0KiB
(адаптировали под свои нужды, скорее) основу основ практически любого приложения под виндой. Весь код я писал параллельно со статьёй, поэтому оставил некоторые моменты за кадром. Как и обещал, в аттаче к статье прикреплю готовые исходники. Спасибо что дочитали до конца. Рад буду увидеть любой отклик от вас, будь он положительный или отрицательный.
В следующей статье в планах написать хеллоу ворлд от мира малвари - клиппер.
Автор reqwest
Источник https://xss.is/