LINUX.ORG.RU

Сброс состояния процесса

 , ,


0

3

Привет, ЛОР!

Хочу странного. А именно, хочу сбросить состояние процесса как если бы он только начался после exec*(), но при этом оставаясь в рамках того же потока выполнения. Особенно хочу unmap() всех страниц, которые были замаплены до этого, кроме страниц с кодом и стеком. Как это нормально сделать?

Поясню зачем это нужно. Хочу сделать песочницы в программе для работы с критичными данными. Песочница представляет собой дочерний процесс, в котором удалены/закрыты все ресурсы кроме связи с родителем и включен Landlock. Как я себе это представляю:

  1. создаю пару pipe() или сокет для связи
  2. делаю fork() (на самом деле, clone(), но это не важно тут)
  3. закрываю все fd кроме созданных в пункте 1 через close_range()
  4. убираю всю память кроме страниц с кодом и стеком
  5. включаю landlock, запрещающий примерно всё кроме общения через fd из пункта 1

Вопрос тут в том, как реализовать шаг 4.

UPD:

Судя по всему, никак. Либо парсить /proc/self/maps и удалять лишние страницы, либо делать exec в себя и ветвление в самом начале main().

★★★★★

Последнее исправление: hateyoufeel (всего исправлений: 3)

Кажется самый простой способ:

В цикле делать fork() и ждать завершения дочернего процесса.

Дочерний процесс делает все свои дела и как только тебе надо сделать это восстановление завершается.

unDEFER ★★★★★
()
Ответ на: комментарий от unDEFER

В цикле делать fork() и ждать завершения дочернего процесса.

Я хочу это делать в дочернем процессе. Кажется, тут не совсем понятно зачем это всё нужно. Сейчас распишу в посте.

hateyoufeel ★★★★★
() автор топика

Состояние сериализовать, записать в переменную окружения, вызвать execve на собственный бинарник. В main должна быть предусмотрена ветка кода: если есть переменная окружения, десериализовать состояние из неё и применить. Кажется systemd pid 1 так делает, когда получает команду systemctl daemon-reexec.

iliyap ★★★★★
()
Ответ на: комментарий от iliyap

Состояние сериализовать, записать в переменную окружения, вызвать execve на собственный бинарник.

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

Читать /proc/self/maps и делать munmap() на это я тоже могу.

В main должна быть предусмотрена ветка кода: если есть переменная окружения, десериализовать состояние из неё и применить.

В идеале я хочу сделать этот функционал библиотекой, так что хотелось бы без переписывания main() по это дело.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от iliyap

Так ты отмапишь память, которую выделили какие-нибудь функции из glibc. И если их вызвать после анмапа, будет сегфолт на отсутствующей странице.

Да, я в курсе. У песочницы не должно быть динамической памяти, вызовы glibc тоже не предусмотрены. По памяти будет только стек и может быть небольшой буфер.

hateyoufeel ★★★★★
() автор топика

Я уж подумал моя старая тема как-то наверх вылезла

Очистить состояние процесса без execve()

там же и решение в конце, не идеальное, но единственное вменяемое.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)
Ответ на: комментарий от firkax

Про execve() и семейство на самого себя я в курсе, выше написал почему не хочу это делать.

там же и решение в конце, не идеальное, но единственное вменяемое.

Я вот в этом не уверен. Не верю, что нет ничего лучше тут.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Это буквально перехватчик вызовов, который прикидывается ядром используя множество интересных подходов, был ещё форк но нашел это

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

RedTerror
()
Ответ на: комментарий от hateyoufeel

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

firkax ★★★★★
()
Ответ на: комментарий от RedTerror

Это буквально перехватчик вызовов, который прикидывается ядром используя множество интересных подходов, был ещё форк но нашел это

Ну, да. И оно реализует функционал landlock. Зачем это нужно, если landlock уже есть?

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

UPD: почитал внимательнее. Оно использует landlock и реализует песочницу для сторонних процессов. Всё равно не то что нужно, сорян.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 3)
Ответ на: комментарий от firkax

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

Стек можно выделить новый, старый отправить в munmap().

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Ну вот, fexecve как раз это и сделает - код оставит, всё остальное выкинет. А всё нужное можно передать в argv или в анонимном tmpfs файле (оставив его в fd=3 и смапив после exec-а куда надо).

Штатного способа нет, не надейся, мне тоже это показалось неразумным но это так.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)
Ответ на: комментарий от firkax

Ну вот, fexecve как раз это и сделает - код оставит, всё остальное выкинет.

Повторю, функции семейства exec*() не подходят, поскольку требуют правок в main(), чтобы это всё работало. Я этого делать не хочу. Создание чистого стека и munmap() на лишние страницы выглядит куда релевантнее, тогда уж.

Штатного способа нет, не надейся, мне тоже это показалось неразумным но это так.

Лялекс сосёт :(

Пойду нахрен патч в ядро сделаю для этого, а то ж стыд и позор какой-то прости хоспидя…

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

Создание чистого стека и munmap() на лишние страницы выглядит куда релевантнее, тогда уж.

А вдруг за время работы процесса кто-то смапил в него дополнительные страницы кода?

firkax ★★★★★
()
Ответ на: комментарий от firkax

Создание чистого стека и munmap() на лишние страницы выглядит куда релевантнее, тогда уж.

А вдруг за время работы процесса кто-то смапил в него дополнительные страницы кода?

Да и похрен? Код – не данные. Если кто-то засунул секреты в страницы с кодом, то он дебил и это не лечится. Вариант, что разработчик – полный кретин и вообще не понимает что делает, я не хочу рассматривать.

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

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от mittorn

exec требует загрузить бинарник заново и начинает выполнение с main(). Мне нужно не перезагружать бинарник и при этом начать выполнение с произвольной функции.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

А представь что это взломщик, который получил возможно манипулировать флагами mmap в главном процессе, а так же грузить произвольный код в песочницы. И вот он ставит где-то PROT_EXEC чтобы эта страница с паролями отправилась его полезной нагрузке.

firkax ★★★★★
()
Ответ на: комментарий от firkax

А представь что это взломщик, который получил возможно манипулировать флагами mmap в главном процессе

Это за пределами тех сценариев, которые я рассматриваю. Сценарий, который я рассматриваю, это изолировать потенциально небезопасный код, обрабатывающий данные извне, в отдельные анклавы и обеспечить для данных там только один вход и один выход. Таким образом, если в этом коде переполнится память или ещё что, сдохнет только один анклав. Родительский процесс запишет это в лог и пойдёт дальше.

Можно сделать LD_PRELOAD и перехватить main или _start.

Это через ещё большую жопу. Можно ещё про __attribute__((constructor)) вспомнить, но я не буду сюда лезть.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Так смотри, вот пришли данные извне, каким-то образом они взломали один из анклавов, но он не упал а отправил на выход другие хакерские данные, которые ты видимо уже не считаешь «данными извне», и эти данные каким-то образом спровоцируют флаг PROT_EXEC в mmap-е который выделяет память для криптографических ключекй авторизации. Другой анклав, тоже взломанный, получит эти ключи т.к. они теперь ни от кого не чистятся, и дальше уже что-то сделает чтобы они отправились по сети в нужное место.

Это через ещё большую жопу

Но это штатный способ работы некоторых системных утилит.

И так вроде всё просто получается, мелкая библиотека на несколько кб, вся суть которой в jmp на твой код из фейкового main-а.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

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

На этом можно задумываться о добавлении в код защиты от жыдорептилоедов и вставлять в комментарии молитву Ave Maria для защиты от духов.

Но это штатный способ работы некоторых системных утилит.

Потому что некоторые системные утилиты в лялексе написаны через жопу. Как и весь лялекс. Я бы хотел избежать необходимости этому уподобиться.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Лялекс сосёт :(

Это ты у нас тугодум ))) При чём тут Linux к твоей безграмотности?

Пойду нахрен патч в ядро сделаю для этого, а то ж стыд и позор какой-то прости хоспидя…

давно всё реализовано и используется. Могу дать подсказку сибирякам - eBPF.

Eulenspiegel
()
Ответ на: комментарий от Eulenspiegel

ps - чтобы самому не писать, в userspace - https://cilium.io/get-started/

там революция уже прошла, а вы всё в…

p.s (копипаста с инета, ИМХО, не просто так оно появилось)

"Добавлена поддержка криптографической верификации загружаемых BPF-программ по цифровой подписи. В дальнейшем данная возможность будет расширена средствами для определения правил загрузки подписанных BPF-программ и предоставления непривилегированным пользователям возможности использования верифицированных BPF-программ."
Eulenspiegel
()
Последнее исправление: Eulenspiegel (всего исправлений: 1)
Ответ на: комментарий от Eulenspiegel

давно всё реализовано и используется. Могу дать подсказку сибирякам - eBPF.

Что eBPF? Продолжи мысль, пожалуйста.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от Eulenspiegel

eBPF такая себе песочница, а попись проверяется там ввиду того чтобы ты малварь в ядро не притащил, потому как там огого какие возможности, да и руткиты все дела, тебе должно это быть известно

RedTerror
()
Ответ на: комментарий от cobold

А если отбросить эту словесную шелуху, то от чего ты хочешь clone()?

От clone3() вместо fork() я в первую очередь хочу получить pidfd сразу же и сунуть новый стек. Там есть такая классная фича.

Из man clone3:

           struct clone_args {
               u64 flags;        /* Flags bit mask */
               u64 pidfd;        /* Where to store PID file descriptor
                                    (int *) */
               u64 child_tid;    /* Where to store child TID,
                                    in child's memory (pid_t *) */
               u64 parent_tid;   /* Where to store child TID,
                                    in parent's memory (pid_t *) */
               u64 exit_signal;  /* Signal to deliver to parent on
                                    child termination */
               u64 stack;        /* Pointer to lowest byte of stack */
               u64 stack_size;   /* Size of stack */
               u64 tls;          /* Location of new TLS */
               u64 set_tid;      /* Pointer to a pid_t array
                                    (since Linux 5.5) */
               u64 set_tid_size; /* Number of elements in set_tid
                                    (since Linux 5.5) */
               u64 cgroup;       /* File descriptor for target cgroup
                                    of child (since Linux 5.7) */
           };

Потому что все остальные твои хотелки говорят что ты хочешь exec

Я хочу почти exec(), только вместо другого бинарника я хочу вызвать функцию из текущего.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Пример того что ты хочешь

// clone3_sandbox.cpp
// Демонстрация: clone3() + pidfd + свой стек + "почти exec" функция.

#include <cerrno>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>

#include <linux/sched.h>   // struct clone_args (может потребоваться свежий linux-headers)
#include <sys/syscall.h>   // SYS_clone3
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

// На некоторых системах ещё нет объявленного SYS_clone3 в заголовках:
#ifndef SYS_clone3
#  if defined(__x86_64__)
#    define SYS_clone3 435
#  else
#    error "SYS_clone3 not defined for this architecture, set it manually"
#  endif
#endif

// Размер стека дочернего процесса (примерное значение, можно изменить)
constexpr std::size_t CHILD_STACK_SIZE = 1 << 20; // 1 MiB

// "Почти exec" — функция, которую должен выполнить дочерний процесс
// вместо обычного кода main().
int sandbox_main(void *arg) {
    // Здесь можно:
    //  - закрыть лишние fd;
    //  - сделать mmap/munmap по своему вкусу;
    //  - включить Landlock и т.п.
    // В примере просто пишем что‑то и выходим.
    std::cout << "sandbox_main(): pid=" << getpid()
              << ", ppid=" << getppid() << "\n";
    // Возвращаем код выхода процесса
    return 0;
}

// Обёртка вокруг системного вызова clone3().
long my_clone3(struct clone_args *args, size_t size) {
    return syscall(SYS_clone3, args, size);
}

int main() {
    // Выделяем стек для дочернего процесса.
    // Стек растёт вниз, поэтому "вершина" будет child_stack + size.
    void *child_stack = std::malloc(CHILD_STACK_SIZE);
    if (!child_stack) {
        std::perror("malloc for child stack");
        return 1;
    }

    // pidfd, который заполнит ядро.
    int pidfd = -1;

    // Структура clone_args. В linux-headers новые поля могут отличаться,
    // но базовый набор такой.
    struct clone_args args {};
    std::memset(&args, 0, sizeof(args));

    args.flags       = CLONE_PIDFD;     // хотим pidfd + отдельный процесс (как fork)
    args.pidfd       = reinterpret_cast<__u64>(&pidfd);
    args.exit_signal = SIGCHLD;         // как у fork()
    args.stack       = reinterpret_cast<__u64>(
        static_cast<char*>(child_stack) + CHILD_STACK_SIZE
    ); // указатель на верх стека
    args.stack_size  = CHILD_STACK_SIZE;

    // Вызов clone3().
    long ret = my_clone3(&args, sizeof(args));
    if (ret < 0) {
        int err = errno;
        std::cerr << "clone3 failed: " << std::strerror(err) << " (" << err << ")\n";
        std::free(child_stack);
        return 1;
    }

    if (ret == 0) {
        // Дочерний процесс: выполняем нашу "почти exec" функцию и выходим.
        int ec = sandbox_main(nullptr);
        // Важно: не возвращаемся в main родителя.
        _exit(ec);
    }

    // Родительский процесс: ret — это PID ребёнка, pidfd уже записан.
    pid_t child_pid = static_cast<pid_t>(ret);
    std::cout << "parent: child pid=" << child_pid
              << ", pidfd=" << pidfd << "\n";

    // Здесь можно пользоваться pidfd (pidfd_send_signal, poll, и т.п.).
    // Для простоты — обычный waitpid.
    int status = 0;
    if (waitpid(child_pid, &status, 0) == -1) {
        std::perror("waitpid");
    } else {
        if (WIFEXITED(status)) {
            std::cout << "parent: child exited with code "
                      << WEXITSTATUS(status) << "\n";
        } else if (WIFSIGNALED(status)) {
            std::cout << "parent: child killed by signal "
                      << WTERMSIG(status) << "\n";
        }
    }

    std::free(child_stack);
    return 0;
}
anonymous
()
Ответ на: комментарий от anonymous

Спасибо, анон! Я бы сам не догадался в нейросеть засунуть запрос!

А теперь покажи в твоём коде очистку адресного пространства в ребёнке после вызова clone3.

// "Почти exec" — функция, которую должен выполнить дочерний процесс
// вместо обычного кода main().
int sandbox_main(void *arg) {
    // Здесь можно:
    //  - закрыть лишние fd;
    //  - сделать mmap/munmap по своему вкусу;
    //  - включить Landlock и т.п.
    // В примере просто пишем что‑то и выходим.
    std::cout << "sandbox_main(): pid=" << getpid()
              << ", ppid=" << getppid() << "\n";
    // Возвращаем код выхода процесса
    return 0;
}

Я что-то не знаю про C++ и там теперь комментарии на русском языке работают как код?

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Ну тогда или как анон говорит или ptrace, то есть ты как бы хочешь форкнуть и передать управление в определенное место и положить туда данные для обработки вызываемой функции этих данных и вернуть их обратно, если крашнется то через fd отправить что крашнулось при таких то данных а ещё ты наверное хочешь интроспекцию чтобы понимать сколько форков с какими данными, да?

RedTerror
()
Ответ на: комментарий от hateyoufeel

ну теоретически, раз у тебя один поток, ты можешь распарсить /proc/self/maps и размаппить все регионы кроме vdso и бинаря, после чего закрыть все дескрипторы и прыгнуть в нужную функцию. Проблема здесь в том, что загруженные shared object'ы имеют своё изменённое состояние и могут ссылаться на маппинги, которые они создали, так что это должен быть или код совершенно без зависимостей (включая libc), или тебе всё же придётся как-то явно корректно деинициализировать все ресурсы, которые были выделены

mittorn ★★★★★
()
Ответ на: комментарий от cobold

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

Его спич наверное навеян как выше подчеркивали связью его идеи с COOCON от Павла

RedTerror
()
Ответ на: комментарий от cobold

Что то ты не договариваешь. fork() + exec() самого себя с нужными параметрами дают и пид и новый стэк

Форк и exec самого себя не дают передачи управления в нужную функцию, только в main(). Плюс есть прочие странные артефакты, про которые @firkax в своём треде писал: например, между стартом родительского процесса и вызовом exec бинарник по /proc/self/exe может измениться, или возможны проблемы с chroot и неймспейсами.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от Eulenspiegel

используется для песочниц и доступа на уровне ядра.

Продолжай. Как мне это здесь поможет на шаге 4? Про seccomp-bpf я в курсе, но это замена landlock и никак не поможет почистить страницы памяти, доставшиеся от родителя.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от RedTerror

Его спич наверное навеян как выше подчеркивали связью его идеи с COOCON от Павла

Не. Я только сегодня в толксах это увидел. Тред вообще с той хернёй не связан никак.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Ну ты просто странного хочешь, вызвать форк с чистой памятью, на определенную функцию сразу прыгнуть и туда данные подять и снять результат

Это как бы хуки у frida или манипуляции с ptrace

RedTerror
()
Ответ на: комментарий от RedTerror

Ну ты просто странного хочешь, вызвать форк с чистой памятью, на определенную функцию сразу прыгнуть и туда данные подять и снять результат

Это называется secure enclave. Оно странное только с точки зрения окостенелого API юниксов, придуманного на коленке немытыми хиппи 50 лет назад. В остальном ничего странного тут нет.

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

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

По итогу скатывается в трастлеты у arm, мираж и ocaml

Если распишешь не теорию, а конкретно к чему это хочешь приложить, можно найти решение, пока что мы говорим об абстрактном решении, нехватает вводных

RedTerror
()
Ответ на: комментарий от Eulenspiegel

не надо делать такие костыли.

Написал он и тут же предложил компилировать код в байткод виртуальной машины и пихать этот байткод в ядро, обильно приправив всё вагоном маркетингового булшита.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)