🔊 Въведение

Предупреждение, тази статия ще бъде насочена директно към Linux🐧 разпространения на процесорна архитектура x86_64. Архитектурите на процесора x86_32, ARM32 и ARM64 няма да работят но са абсолютно жизнеспособни предвид незначителни промени в кода за обобщаване върху специфичните регистрови структури на архитектурата и кодиране на инструкции. Продължавайки, настройката ми за този урок ще бъде Windows машина и ще използвам WSL за странично зареждане на Ubuntu 20.04.4 LTS дистрибуция, от която ще работя. Избраният език за тази статия ще бъде Rust. До момента на писане най-актуалната стабилна версия на Rust е1.65.0която ще използвам. Използваните от мен зависимости на Rust Crate ще имат цитирани съответните им версии. Ако искате да пропуснете напред или просто да видите получения код, пълният проект може да бъде намерен на адрес https://github.com/0xFounders/ptrace_syscalls

📚 Маршрут

  1. Създайте нашия тестов процес на жертва
  2. Създайте нашия хост процес
  3. Извикване на системно повикване в процес жертва
  4. Аргументи на указателя
  5. Изхвърляне на ресурси/Бъдещи четения, Бъдеща работа

🧪 Създайте нашия процес на тестова жертва

За този проект ще ни трябва процес, който да използваме като наш тест. За предпочитане е нашият тестов процес да бъде продължителен процес с някакъв индикатор за нормален контролен поток, за да потвърдим, че не сме повредили нищо след увеличаването. Като такава, програма, състояща се от прост безкраен цикъл while, който се отпечатва на конзолата, ще служи като перфектна жертва на теста.

дневник — https://crates.io/crates/log

Сандъкът за трупи е просто лек, благословен™ сандък за дърводобив, който ще използваме за събиране на данни за трупи. Позволява ни да събираме регистрационни данни на различни нива за четимост: проследяване, лог, информация, предупреждение, грешка.

pretty_env_logger — https://crates.io/crates/pretty_env_logger

Касата за трупи е просто тънка фасада, pretty_env_logger всъщност ще се справи със събирането на събраните данни и извеждането им към конзолата вместо нас. Той също така предлага хубаво форматиране и цветно кодиран текст.

Внедряване

По-долу е моето просто изпълнение на идеален процес на жертва за тестване. Моля, имайте предвид, че ще работя с Работни пространства за товари, където контейнерът за жертви и контейнерът за хост ще бъдат заедно в работното пространство. Ако искате да преследвате различен работен процес, просто имайте предвид, че как изпълнявате приложението си с товар може да се различава.

За проверка на здравината ето как изглежда структурата на проекта в Visual Studio Code. Обърнете внимание как целевата папка е в родителската директория в сравнение с папката жертва. Това се дължи на нашия работен процес на работното пространство, който събира всички резултати за компилация на членове в обща целева папка.

🏠 Създайте нашия хост процес

Общ преглед на Ptrace

За да започнем с документацията на страницата с ръководство за Linux, можем да получим кратка същност на това, което интерфейсът Ptrace обикновено е проектиран да улеснява.

https://man7.org/linux/man-pages/man2/ptrace.2.html

Системното извикване ptrace() предоставя средство, чрез което един процес
(„трайсьорът“) може да наблюдава и контролира изпълнението на друг
процес („трайсьорът“), и прегледайте и променете паметта и регистрите на проследяващия
. Използва се предимно за прилагане на
отстраняване на грешки в точката на прекъсване и проследяване на системни повиквания.

Ние ще разширим границите на Ptrace, за да извикаме външно системни повиквания в отдалечен процес. Например, до края на тази статия ще можем да използваме системните извиквания mmap/munmap за разпределяне на памет в процеса на потребителското пространство, както и да поемем контрола върху указателя на инструкциите на приложението на потребителското пространство и регистрите за извикване на произволно системно извикване. Засега ще настроим нашия хост просто да се прикачи към процеса и да инспектира регистрите на текущия контролен поток. Ще ни трябват още няколко каси, за да направим това.

тази грешка — https://crates.io/crates/thiserror

Друга щайга blassed™, която

предоставя удобен макрос за извличане на характеристиката std::error::Error на стандартната библиотека.

ние ще използваме тази грешка, за да улесним обработката на грешки, за да осигурим изживяване без паника.

sysinfo — https://crates.io/crates/sysinfo

Сандъкът sysinfo ни предоставя функция за качество на живот, за да намерим процес по име срещу Pid. Това ще бъде полезно при бързо създаване на прототипи, защото можем да кодираме твърдо името на нашия целеви процес като „жертва“. Бихте могли да обсъдите, че това е раздуване на функцията, за да включите цяла зависимост от кутия за просто получаване на pid на процеса по име. И бих се съгласил. Но мързелът 🦥 надделява, когато експериментирате...

nix — https://crates.io/crates/nix

Сандъкът nix ще ни предостави някои обвързвания за качество на живот за ptrace, които абстрахират някои от функционалностите, предоставящи типове резултати за по-лесно обработване на грешки. Той също така предоставя константи за картографиране/защити на страници и памет, от които ще се нуждаем по-късно в раздела Аргументи на указателя.

Внедряване

Демонстрация

Страхотно, успяхме да се прикрепим към процеса на жертвата и да прихванем текущите му регистри, като ги отпечатаме на конзолата.

Ако това не означава абсолютно нищо за вас, насърчавам ви да погледнете структурата на регистрите x86_64. По-конкретно обърнете внимание на регистъра за извличане, който е нашият текущ указател на инструкция и регистъра rax, регистърът на връщаната стойност. Регистърът за извличане ще бъде от особен интерес за нас, след като започнем да се опитваме да отвлечем контролния поток на процеса на жертвата 😈

🖥️ Извикване на системно повикване в процес на жертва

Преглед

  1. Използването на ptrace прихваща потока за контрол на процеса и получава текущите регистри
  2. Кеширайте текущите регистри, указателя на текущата инструкция и текущите инструкции
  3. Записване на инструкциите за сглобяване на показалеца на текущата инструкция, които извикват системно повикване
  4. Задайте всички съответни аргументи на регистъра за системното извикване
  5. Единична стъпка изпълнението на процеса очаква SIGTRAP като следващ сигнал
  6. Кеширайте получените регистри като резултат от системното извикване
  7. Възстановете оригиналните регистри и оригиналните инструкции, за да продължите нормалното изпълнение на приложението

Сглобяване на Syscall

Операционният код за системно повикване в x86_64 е 0x0F05. Така че имаме нужда текущият указател на инструкция да сочи към този код на операцията, за да отклоним контролния поток към системно повикване. Не можем просто да напишем два байта с ptrace, защото очаква WORD, което е много объркващо, защото това всъщност означава u64 на x86_64. Според моя опит в екосистемата на Windows WORD обикновено се отнася до u16. Това ми причини известно объркване по време на този проект. Независимо от това, можем лесно да разширим 2-байтовия код за операция в 8 байта с инструкции без операция, NOP (0x90). Техниката за разширяване на инструкция с NOPs за подравняването й понякога се нарича "NOP Sled".

syscalls — https://crates.io/crates/syscalls

Сандъкът за системни извиквания ще ни предостави константи за съответния номер/rax на системно извикване. Обърнете се към използването на Sysno enum в изпълнението.

Внедряване

За нашите опитни читатели вие незабавно ще разберете rax стойността на получения регистър. Rax регистърът ще съответства на върнатия аргумент на syscall. В този пример процесът на жертвата има pid от 13958 и системното извикване на getpid на хоста връща правилно същия pid от 13958 в rax регистъра. Оценявам добрата проверка на разума 🧠.

Сега какво ще кажете за syscall, приемащ аргумент, ще използвам exit, където първият аргумент съответства на кода за изход на приложението.

sys_call(Sysno::exit, 42, 0, 0, 0, 0, 0)?;

Хм какво ще кажете за такъв, при който аргумент е изходна променлива. Можем да използваме системното повикване време. Той очаква указател към променлива time_t, за да запише текущия времеви печат на unix.

// tyepdef time_t int64
time_t time(time_t *tloc);

Настройване на syscall и предоставяне на нашата изходна променлива

// Call time syscall
let mut output = 0i64;
let result = user_process.sys_call
 (Sysno::time, &mut output as *mut _ as u64, 0, 0, 0, 0, 0)?;
log::info!("Syscall Result: {:#?}", result);
log::info!("Time: {}", output);

И…. Нищо? Времето е нула, което не изглежда правилно.

Ако изпълним командата date, за да изведем текущото времево клеймо на unix, можем да забележим значително различния изход. Резултатът за времето трябва да е нещо поне в близост до изхода на командата за дата. Освен ако не сме в трескав сън 😴 и не се върнем назад във времето до 1 януари 1970 г. точно в полунощ (епоха), времето не трябва да е 0...

Unix времето е броят секунди, изминали от 00:00:00 UTC на 1 януари 1970 г., с изключение на високосните секунди. Това време е наречено Unix епоха, защото е началото на Unix времето.

↪️ Аргументи на указателя

Основната причина за нашия проблем с изпълнението на системното извикване на пъти е, че ние посочваме адрес на паметта в пространството на паметта на нашия хост процес. По същия начин, по който не можем да пишем в паметта на процеса на жертвата, без да използваме специфични операционни функции, жертвата не може да пише в паметта на процеса на нашия хост, без да използва тези операции. Можем да разрешим това, като вместо това директно разпределим памет в процеса жертва и използваме адреса на това пространство като аргумент, след което по-късно четем от тази памет, за да извлечем изхода. Не трябва да е изненада, че ще използваме mmap и munmap системни извиквания, за да управляваме данните в процеса на жертвата.

Mmap

mmap() създава ново съпоставяне във виртуалното адресно пространство на
извикващия процес. Началният адрес за новото съпоставяне е
посочен в addr. Аргументът за дължина указва дължината на
картографирането (която трябва да е по-голяма от 0).

Ако addr е NULL, тогава ядрото избира (подравнен по страница)
адрес, на който да създаде картографирането; това е най-преносимият
метод за създаване на ново картографиране.

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

Munmap

Функцията munmap() трябва да премахне всички съпоставяния за тези цели
страници, съдържащи която и да е част от адресното пространство на процеса
започвайки от addr и продължавайки за len байта. Допълнителни препратки
към тези страници ще доведат до генериране на SIGSEGV сигнал
към процеса. Ако няма съпоставяния в посочения
адресен диапазон, тогава munmap() няма ефект.

Внедряването може да изисква addr да е кратно на
размера на страницата, върнат от sysconf().

int munmap(void *addr, size_t len);

Решение

  1. Използвайте mmap syscall, за да резервирате виртуална памет в процеса на жертвата
  2. Четете или записвайте във виртуалната памет според нуждите
  3. Предоставяне на аргументи на указателя на системното извикване от препратки към виртуалната памет, която сме запазили в жертвата

Внедряване

⚠️Начинът, по който четем и записваме в паметта на процеса, е преработен, за да чете директно от файла с паметта на процеса. Това ни освобождава от изискванията на WORDограниченията за размерна ptrace. Поради това, че всичко по същество е файл в Linux, стандартната файлова функционалност на Rust може да се използва за взаимодействие с файла с паметта на процеса. Тази промяна в изпълнението ще направи взаимодействието с нашата mmap’d памет значително по-лесно. Вдъхновението за тази промяна е от страхотния кашон pete, предоставящ по-изчерпателен API за безопасно ptrace. Производният код на pete crate ще бъде цитиран съответно.

⚠️За да осигурим донякъде завършен, правилен API, трябва да преместим по-голямата част от нашия код в отделен lib файл, така че да имаме namespace/mod файл, за да можем да използваме модификатори за видимост. С целия код в main.rs основната функция може да има достъп до функции, които наистина трябва да се третират като частни в реализациите, защото са дефинирани в един и същи файл.

Остава един сценарий, който не разгледахме сега, какво ще кажете за аргумент на указател за въвеждане в системно извикване. „chdir sycall“ трябва да е достатъчен.

int chdir(const char *path);

И с няколко дребни редакции на main.rs wallah.

use std::ffi::CString;

use host::{HostError, HostResult, UserProcess};
use nix::{
    libc::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE},
    unistd::Pid,
};
use syscalls::Sysno;
use sysinfo::{ProcessExt, System, SystemExt};

fn main() -> HostResult<()> {
    pretty_env_logger::formatted_builder()
        .filter_level(log::LevelFilter::Trace)
        .init();

    let process_name = "victim";
    log::info!("Host Process Pid: {}", std::process::id());

    // Create sysinfo object and refresh to collect current os state
    let mut sys = System::new_all();
    sys.refresh_all();

    // Find our target process or die
    let process = sys
        .processes_by_name(process_name)
        .take(1)
        .next()
        .ok_or_else(|| HostError::ProcessNotFound(process_name.to_string()))?;

    // Cast our sysinfo::Pid into a nix::unistd::Pid
    let pid = Pid::from_raw(process.pid().into());

    // Attach to the process
    let user_process = UserProcess::attach(pid)?;

    // Refactor out the expect later, but the input should never fail because we know the input does not contain an internal 0 byte.
    let output_message = CString::new("/home/chase").expect("CString::new failed");

    // We want the bytes of the Cstring.
    let output_message = output_message.as_bytes();

    // Allocate 8 bytes of data, i64 is 8 bytes
    let mut user_memory = user_process.allocate_memory(
        0,
        output_message.len() as u64,
        (PROT_READ | PROT_WRITE) as u64,
        (MAP_PRIVATE | MAP_ANONYMOUS) as u64,
        u64::MAX,
        0,
    )?;
    log::info!("UserMemory Result Address: {:#X}", user_memory.address());

    // Read the memory and demonstrate it is zero'd out
    let read = user_process.read_user_memory(&user_memory, user_memory.len() as usize)?;
    log::info!("Allocated Memory: {:?}", read);

    // Write to the memory out cstring
    user_process.write_user_memory(&mut user_memory, 0, output_message)?;

    // We can check if the call succeeded by the resultant rax value.
    let result = user_process
        .sys_call(Sysno::chdir, user_memory.address(), 0, 0, 0, 0, 0)?
        .rax;

    log::info!("Result {result:?}");

    Ok(())
}

Заключение

Създадохме доста приличен API за използване на ptrace на x86_64 за извикване на syscall в отдалечени процеси. Това е доста готино, но в момента сме ограничени до възможностите на предоставените системни извиквания. Наистина би било много полезно, ако в бъдеще можем да извикаме функции на потребителското пространство в процеса на жертвата, като libc puts, за да можем да извеждаме към конзолата на жертвата. В следващата статия ще направим точно това и ще научим как да идентифицираме адреса на функция на потребителското пространство във външен процес с coup de grâceизвикване на dlopen в процеса жертва за зареждане на произволен споделен обект в неговото адресно пространство техника, известна като инжектиране на процес 💉.

Изхвърляне на ресурси/Бъдещи четения, Бъдеща работа

Окончателен проект Github Project

Ресурси

Бъдеща работа

  • Поддържа x86_32
  • Поддържа ARM32
  • Поддържа ARM64
  • (Може би?) Поддържайте аргументи на стека, напр. syscall извиква с повече от количеството аргументи на регистъра. Сравнително съм сигурен, че x86_64 няма системни извиквания, изискващи аргументи на стека, напр. повече от 6 аргумента, въпреки че не съм сигурен, че това е вярно за другите архитектури.