Познаём malware-кодинг на Rust под Windows

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/
В установщике выбираем вот эти пункты и устанавливаем:
image.png


По окончанию установщик попросит перезагрузить компьютер, перезагружаем.

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


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


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


Отлично!

Установка 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
image.png


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


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


Видим ожидаемый результат.

Теперь давайте настроим проект для дальнейшей разработки. В настройках плагина 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", // в эту папку сохранятся дебагсимволы
            },
        }
    ]
}
tasks.json:
JSON: Скопировать в буфер обмена
Код:
{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "cargo",
            "command": "build", // по дефолту бинарник собирается в дебаге
            "problemMatcher": [
                "$rustc"
            ],
            "group": "build",
            "label": "build debug" // этот лейбл используется в конфигурации запуска выше
        }
    ]
}
Поставим бряк, запустим:
image.png


Отлично, всё работает.

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


Видим в импорте много 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",
]
Компилируем, смотрим импорты... Красота? Не очень. Лучше бы их вообще не было, да и вес бинарника многоват. Давайте исправим это. Раст хоть и позиционируется как язык с обилием 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 и давайте разбираться.
Я сохранил их в папку %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-функции нужно оборачивать в блок unsafe {} - не очень удобно, давайте исправим это. Для этого нам нужно чуть-чуть изменить bindgen - чтоб генерировались не unsafe-обёртки над
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 = []
Теперь чуток исправим наш hello_world/main.rs, обавим в самом верху:
Код: Скопировать в буфер обмена
Код:
#![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 для удобства сборки:
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
Так же сразу изменим конфигурацию VSCode: 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;
Там напишем самый проcтой обработчик паники, а точнее затычку:
Код: Скопировать в буфер обмена
Код:
#[cfg(not(test))]
#[panic_handler]
unsafe fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}
Осталось реализовать глобальный аллокатор - тут тоже ничего сложного нет, мы же уже умеем работать с winapi?)
Создаём файл 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::*;
Т.к адрес апи-функций мы будем искать по хэшу, давайте реализуем FNVA1 хэш-функцию в файле 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,
}
И сразу же реализуем функцию получения адреса PEB для архитектур х86 и х64:
Код: Скопировать в буфер обмена
Код:
#[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,
}
Давайте теперь напишем саму функцию получения адреса загруженного модуля. Для этого нам нужно из PEB получить указатель на первый элемент односвязного списка, кастануть его к типу 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");
                }
            }
        }
    }
}
И запустим его командой (или напрямую из VSCode, как это делаю я):
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
Отлично, давайте теперь сравним скорость нашей функции get_module и апи GetModuleHandleW:
Код: Скопировать в буфер обмена
Код:
#[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)),
    }
}
Не забываем написать тесты, запустить их. Теперь давайте сравним скорость выполнения нашей кастомной функции get_export_func и стандартой апи GetProcAddress.
Код: Скопировать в буфер обмена
Код:
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/
 
Сверху Снизу