🔊 Введение

Предупреждаем, что эта статья предназначена непосредственно для 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

Журнальный ящик — это всего лишь тонкий фасад, довольно_env_logger на самом деле будет обрабатывать собранные данные и выводить их на консоль для нас. Он также из коробки обеспечивает хорошее форматирование и цветной кодированный текст.

Выполнение

Ниже представлена ​​моя простая реализация идеального процесса-жертвы для тестирования. Обратите внимание, я буду работать с Cargo Workspaces, где корзина-жертва и корзина-хозяин будут находиться в рабочей области вместе. Если вы хотите использовать другой рабочий процесс, просто имейте в виду, что запуск вашего приложения с помощью груза может отличаться.

Для проверки работоспособности вот как выглядит структура проекта в Visual Studio Code. Обратите внимание, что целевая папка находится в родительском каталоге, а не в папке жертвы. Это связано с нашим рабочим процессом рабочей области, который собирает все выходные данные сборки участников в общую целевую папку.

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

Обзор трассировки

Чтобы начать с документации man-страницы Linux, мы можем получить краткий обзор того, для чего обычно предназначен интерфейс Ptrace.

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

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

Мы будем расширять возможности Ptrace, чтобы извне вызывать системные вызовы в удаленном процессе. Например, к концу этой статьи мы сможем использовать системные вызовы mmap/munmap для выделения памяти в процессе пользовательского пространства, а также управлять указателем инструкций и регистрами приложения пользовательского пространства для вызова любого произвольного системного вызова. Сейчас мы настроим наш хост так, чтобы он просто присоединялся к процессу и проверял регистры текущего потока управления. Для этого нам понадобится еще несколько ящиков.

эта ошибка — https://crates.io/crates/thiserror

Еще один ящик blessed™, который

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

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

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

Крейт sysinfo предоставляет нам функцию качества жизни, позволяющую найти процесс по имени, а не по идентификатору. Это будет полезно при быстром прототипировании, потому что мы можем жестко закодировать имя нашего целевого процесса как «жертва». Вы можете поспорить, что это раздувание функций, чтобы включить целую зависимость ящика для простого получения pid процесса по имени. И я бы согласился. Но лень 🦥 берет верх при экспериментах…

никс — https://crates.io/crates/nix

Крейт nix предоставит нам некоторые качественные привязки к ptrace, которые абстрагируются от некоторых функций, предоставляющих типы результатов для упрощения обработки ошибок. Он также предоставляет константы для сопоставления/защиты страниц и памяти, которые нам понадобятся позже в разделе Аргументы указателя.

Выполнение

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

Круто, мы смогли подключиться к процессу-жертве и перехватить его текущие регистры, выводя их на консоль.

Если это абсолютно ничего для вас не значит, я рекомендую вам взглянуть на структуру регистров x86_64. В частности, обратите внимание на регистр rip, который является нашим текущим указателем инструкции, и регистр rax, регистр возвращаемого значения. Регистр rip будет представлять для нас особый интерес, как только мы начнем пытаться перехватить поток управления процессом-жертвой 😈

🖥️ Вызвать системный вызов в процессе жертвы

Обзор

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

Сборка системных вызовов

Код операции для системного вызова в x86_64 — 0x0F05. Поэтому нам нужно, чтобы текущий указатель инструкции указывал на этот код операции, чтобы перенаправить поток управления на системный вызов. Мы не можем просто записать два байта с помощью ptrace, потому что он ожидает WORD, что сильно сбивает с толку, поскольку на самом деле это означает u64 на x86_64. WORD обычно относится к 16-летнему возрасту, судя по моему опыту работы с экосистемой Windows. Это вызвало у меня некоторое замешательство во время этого проекта. Несмотря на это, мы можем легко расширить 2-байтовый код операции до 8 байтов с помощью инструкций No Operation, NOP (0x90). Метод расширения инструкции с помощью NOP для ее выравнивания иногда называют «следом NOP».

системные вызовы — https://crates.io/crates/syscalls

Крейт системных вызовов предоставит нам константы для соответствующего номера системного вызова/rax. Обратитесь к использованию перечисления Sysno в реализации.

Выполнение

Для наших сообразительных читателей вы сразу поймете результирующее значение rax регистра. Регистр rax будет соответствовать возвращаемому аргументу системного вызова. В этом примере процесс-жертва имеет pid 13958, а системный вызов хоста getpid правильно возвращает тот же pid 13958 в регистре rax. Я ценю хорошую проверку на вменяемость 🧠.

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

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

Хм, как насчет того, где аргумент является выходной переменной. Мы можем использовать системный вызов time. Он ожидает указатель на переменную time_t для записи текущей временной метки unix.

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

Настройка системного вызова и предоставление нашей выходной переменной

// 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, мы увидим сильно отличающийся вывод. Результат времени должен быть как минимум близок к выводу команды date. Если только мы не находимся в лихорадочном сне 😴 и не вернулись во времени к 1 января 1970 года ровно в полночь (эпоху), время не должно быть 0…

Время Unix — это количество секунд, прошедших с 00:00:00 UTC 1 января 1970 года, за исключением дополнительных секунд. Это время называется эпохой Unix, потому что это начало времени Unix.

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

Основная причина нашей проблемы с запуском системного вызова times заключается в том, что мы указываем адрес памяти в пространстве памяти нашего хост-процесса. Точно так же, как мы не можем писать в память процесса-жертвы, не используя определенные функции, жертва не может писать в память нашего хост-процесса, не используя эти операции. Мы можем решить эту проблему, вместо этого напрямую выделив память в процессе-жертве и используя адрес этого пространства в качестве аргумента, а затем прочитав эту память для получения вывода. Неудивительно, что мы будем использовать системные вызовы mmap и munmap для управления данными в процессе-жертве.

Мап

mmap() создает новое сопоставление в виртуальном адресном пространстве
вызывающего процесса. Начальный адрес для нового сопоставления
указан в addr. Аргумент длины указывает длину
сопоставления (которая должна быть больше 0).

Если адрес равен NULL, тогда ядро ​​выбирает (выровненный по странице)
адрес, по которому создается отображение; это наиболее переносимый
метод создания нового сопоставления.

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

Мунмап

Функция munmap() должна удалить любые сопоставления для тех полных
страниц, содержащих любую часть адресного пространства процесса
, начиная с адреса и продолжая len байт. Дальнейшие ссылки
на эти страницы должны привести к генерации сигнала SIGSEGV
для процесса. Если в указанном диапазоне
адресов нет отображений, функция munmap() не действует.

Реализация может потребовать, чтобы адрес был кратен
размеру страницы, возвращаемому функцией sysconf().

int munmap(void *addr, size_t len);

Решение

  1. Используйте системный вызов mmap для резервирования виртуальной памяти в процессе-жертве
  2. Чтение или запись виртуальной памяти по мере необходимости
  3. Предоставьте аргументы указателя системного вызова из ссылок на виртуальную память, которую мы зарезервировали в жертве.

Выполнение

⚠️Чтение и запись в память процесса были реорганизованы для прямого чтения из файла памяти процесса. Это освобождает нас от требований WORDограничений размера ptrace. Поскольку в Linux все по существу является файлом, стандартные функции файлов Rust можно использовать для взаимодействия с файлом памяти процесса. Это изменение реализации значительно упростит взаимодействие с нашей mmap-памятью. Вдохновением для этого изменения послужил удивительный ящик pete, предоставляющий более полный безопасный API ржавчины ptrace. Код, производный от ящика для животных, будет цитироваться соответственно.

⚠️Чтобы предоставить несколько полный, правильный API, нам нужно переместить большую часть нашего кода в отдельный файл lib, чтобы у нас был файл пространства имен/модификаторов, чтобы иметь возможность использовать модификаторы видимости. Со всем кодом в main.rs функция main может обращаться к функциям, которые действительно должны рассматриваться как частные в реализациях, поскольку они определены в том же файле.

Остался один сценарий, который мы не рассмотрели сейчас, как насчет аргумента указателя ввода в системном вызове. Достаточно 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 для вызова системного вызова в удаленных процессах. Это довольно круто, но в настоящее время мы ограничены возможностями предоставленных системных вызовов. На самом деле было бы очень полезно, если бы в будущем мы могли вызывать функции пользовательского пространства в процессе-жертве, такие как libc puts, чтобы мы могли выводить данные на консоль жертвы. В следующей статье мы сделаем именно это и узнаем, как идентифицировать адрес функции пользовательского пространства во внешнем процессе с помощью смертельного удара, вызывая dlopen в процессе-жертве для загрузки произвольного общий объект в его адресное пространство методом, известным как внедрение процесса 💉.

Дамп ресурсов / Будущие чтения, Будущая работа

Финальный проект Github Project

Ресурсы

Будущая работа

  • Поддержка x86_32
  • Поддержка ARM32
  • Поддержка ARM64
  • (Может быть?) Аргументы стека поддержки, например. системные вызовы с большим количеством аргументов регистра. Я относительно уверен, что x86_64 не имеет системных вызовов, требующих аргументов стека, например. более 6 аргументов, хотя я не уверен, что это верно для других архитектур.