Как мы можем узнать минимальный размер стека, необходимый программе, запущенной с помощью exec()?

В попытке избежать конфликта стека атак на мы попытались установить ограничение на размер стека с setrlimit(RLIMIT_STACK) примерно до 2 МБ.

Это ограничение подходит для внутренних нужд нашей программы, но затем мы заметили, что попытки exec() внешних программ начали терпеть неудачу на некоторых системах с этим новым ограничением. Одна система, которую мы исследовали с помощью приведенной ниже тестовой программы, по-видимому, имеет минимальный размер стека для exec()-программ чуть более 4 МБ.

Мой вопрос в том, как мы можем узнать безопасное минимальное значение размера стека в данной системе, чтобы exec() не потерпел неудачу?

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

Приведенная ниже тестовая программа написана в терминах system(), но симптом более низкого уровня ошибка в системном вызове execl(). В зависимости от хост-ОС, на которой вы тестируете, вы получаете либо errno == E2BIG, либо segfault в вызываемой программе, когда вы предоставляете вызываемой программе слишком мало места в стеке для запуска.

Построить с помощью:

$ CFLAGS="-std=c99 -D_POSIX_C_SOURCE=200809" make stacklim

Этот вопрос косвенно связан с "Проверить условие ошибки E2BIG в exec", но наш фактический вопрос отличается: мы вас интересуют потенциальные проблемы с переносимостью, которые вызывает установка этого ограничения.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

static enum try_code {
    raise_minimum,
    lower_maximum,
    failed_to_launch
}
try_stack_limit(rlim_t try)
{
    // Update the stack limit to the given value
    struct rlimit x;
    getrlimit(RLIMIT_STACK, &x);
    static int first_time = 1;
    if (first_time) {
        first_time = 0;
        printf("The default stack limit here is %llu bytes.\n", x.rlim_cur);
    }
    x.rlim_cur = try;
    setrlimit(RLIMIT_STACK, &x);

    // Test the limit with a do-nothing shell launch
    int status = system("exit");
    switch (status) {
        case 0: return lower_maximum;

        case -1:
            perror("Failed to start shell");
            return failed_to_launch;

        default:
            if (WIFEXITED(status) && WEXITSTATUS(status) == 127) {
                // system() couldn't run /bin/sh, so assume it was
                // because we set the stack limit too low.
                return raise_minimum;
            }
            else if (WIFSIGNALED(status)) {
                fprintf(stderr, "system() failed with signal %s.\n",
                        strsignal(WTERMSIG(status)));
                return failed_to_launch;
            }
            else {
                fprintf(stderr, "system() failed: %d.\n", status);
                return failed_to_launch;
            }
    }
}

int main(void)
{
    extern char **environ;
    size_t etot = 0;
    for (size_t i = 0; environ[i]; ++i) {
        etot += strlen(environ[i]) + 1;
    }
    printf("Environment size = %lu\n", etot + sizeof(char*));

    size_t tries = 0;
    rlim_t try = 1 * 1000 * 1000, min = 0, max = 0;
    while (1) {
        enum try_code code = try_stack_limit(try);
        switch (code) {
            case lower_maximum:
                // Call succeded, so lower max and try a lower limit.
                ++tries;
                max = try;
                printf("Lowered max to %llu bytes.\n", max);
                try = min + ((max - min) / 2);
                break;

            case failed_to_launch:
                if (tries == 0) {
                    // Our first try failed, so there may be a bug in
                    // the system() call.  Stop immediately.
                    return 2;
                }
                // Else, consider it a failure of the new limit, and
                // assume we need to limit it.

            case raise_minimum:
                // Call failed, so raise minimum and try a higher limit.
                ++tries;
                min = try > min ? try : min;
                rlim_t next = max ?
                        min + ((max - min) / 2) :
                        try * 2;
                if (next == try) {
                    printf("Min stack size here for exec is %llu.\n", max);
                    return 0;
                }
                else {
                    printf("Raising limit from %llu to %llu.\n", try, next);
                    try = next;
                }
                break;

            default:
                return 1;
        }
    }
}

person Warren Young    schedule 23.06.2017    source источник
comment
Вы пытались вызвать getrlimit при запуске, сохранить значение, а затем вызвать setrlimit с этим значением перед вызовом execl?   -  person dbush    schedule 23.06.2017
comment
Я даже не понимаю проблемы: «Но если он слишком сильно разрастется и окажется слишком близко к другой области памяти, программа может перепутать стек с другой областью памяти». ммм.. как, не генерируя ошибку страницы?   -  person ThingyWotsit    schedule 23.06.2017
comment
Если вы пытаетесь предотвратить использование конфликта стека, устанавливая ограничения ресурсов, то следует ожидать, что некоторые программы не смогут работать с указанным вами ограничением. По сути, именно так этот подход эффективен — в той мере, в какой он эффективен — при решении проблемы. Ни в коем случае не ясно, можно ли установить какое-либо ограничение, которое позволяло бы всем хорошим программам обеспечивать адекватную защиту от эксплойта. Конечно, разрешение всего, что нужно любой программе для запуска, кажется, полностью противоречит цели.   -  person John Bollinger    schedule 23.06.2017
comment
@JohnBollinger: мы заинтересованы только в предотвращении атак столкновений стека против нашей собственной программы. Если вы попробуете установить слишком низкий лимит, используя приведенную выше тестовую программу, против более существенной программы (скажем, Vim), вы заметите, что она даже не запускается перед смертью. Разве вы не можете сказать, что все программы начинают выделять мегабайты данных в стеке еще до того, как они будут запущены?   -  person Warren Young    schedule 23.06.2017
comment
ИМО, это временное решение по сравнению с реальным решением: надлежащие проверки пределов индексации массива. Просто не превышайте фиксированные размеры массива в стеке (например, int arr[100]), проверяя ограничения, не позволяйте VLA становиться слишком большими (также ограничивайте alloca) и ограничивайте рекурсию, вы не получите стека область кучи (что потребовало бы увеличения стека до терабайт в 64-битной системе, прежде чем они столкнутся). Большинство pgms построены для размера стека по умолчанию (например, Linux: 8 МБ), поэтому не нарушайте их. Перекодируйте свою программу, чтобы проверить все пределы, а не использовать rlimit. А как насчет других пгм?   -  person Craig Estey    schedule 23.06.2017
comment
Вы можете получить сбой кучи в стек, только если вы индексируете размер массива, выделенного там (или не проверяете NULL return from (e.g.) malloc, sbrk). Вы хотите выполнить ручную проверку предельных значений, чтобы массив стека не заходил слишком далеко и не перезаписывал память стека выше (включая адрес возврата fnc) для выполнения произвольного кода в стеке при возврате fnc-- более распространенный вектор атаки. Или превышение одного массива кучи для перехода в другой [обычно прерывает pgms, но может быть создано как атака]. rlimit не смягчает их. P.S. Что такое Паскаль? :-)   -  person Craig Estey    schedule 23.06.2017
comment
@WarrenYoung: Это было правдой давным-давно. За последние 20-30 лет многое изменилось. Жаль, что учебники того времени до сих пор используются при обучении проектированию ОС.   -  person too honest for this site    schedule 23.06.2017
comment
@CraigEstey: Я хочу сказать, что вы можете сколько угодно морализировать о правильном способе написания программ, но это само по себе не предотвращает непреднамеренные уязвимости. Такие охранники, канарейки разных видов и т. д. тоже ценны. Если бы все, что для этого требовалось, — это список лучших практик, мы бы не видели, как одни и те же уязвимости появляются снова и снова на протяжении десятилетий.   -  person Warren Young    schedule 23.06.2017
comment
В системе с виртуальной памятью все это упражнение несколько спорно — вы можете просто установить очень высокий предел и полагаться на то, что система выделит столько виртуальных страниц, сколько вам может понадобиться — память, которая не используется, просто останется в подкачке.   -  person tofro    schedule 23.06.2017
comment
@WarrenYoung, на самом деле, в каком-то смысле я я говорю это. RLIMIT_STACK — это ограничение на размер самого стека. Сколько из этого пространства (в настоящее время) используется — это совершенно другой вопрос. Если начальный размер стека программы больше вашего предела, то она должна завершиться ошибкой при запуске, даже если изначально использовалось очень мало запрошенного пространства.   -  person John Bollinger    schedule 23.06.2017
comment
Похоже, вы не читаете новости. Вопрос касается этой штуки, где Qualys labs рекламировала что они смогли скомпрометировать почти все на 32-битных системах.   -  person Antti Haapala    schedule 23.06.2017
comment
Кроме того, цитата с веб-страницы: (например, в некоторых случаях наш эксплойт Sudo stack-clash выделяет всего 137 МБ памяти кучи и почти не использует память стека); или ваши лимиты будут слишком низкими и сломают законные приложения.   -  person Antti Haapala    schedule 23.06.2017
comment
Я использовал много дополнительного кода защиты для обнаружения переполнения [во время выполнения]. В качестве простого примера см. мой ответ: stackoverflow.com/questions/34888974/ Вы также можете добавить начальное число значения для проверки переполнения с большей детализацией и добавления канареечного потока, который постоянно отслеживает область кучи. На практике я обнаружил, что переполнение чаще всего происходит вверх. Итак, моя точка зрения заключается в том, что трюк с rlimit не так полезен (например, 2 МБ против 8 МБ относительно спорный вопрос, но добавляет сложности, которые могут быть источником ошибок)   -  person Craig Estey    schedule 23.06.2017
comment
Дело вовсе не в переполнении буфера. Здесь нет неопределенного поведения C-мудрого, просто сам компилятор/среда выполнения не работает.   -  person Antti Haapala    schedule 23.06.2017
comment
Переключитесь на 64-битную ОС. В любом случае это правильно, учитывая, что большинство современных процессоров лучше всего работают в 64-битном режиме.   -  person rustyx    schedule 26.06.2017
comment
@RustyX: Система, в которой была обнаружена проблема, — это CentOS 7, которая доступна только в 64-разрядной версии для систем Intel. Ну, есть вариант, созданный сообществом для 32-битных систем, но мы его не используем.   -  person Warren Young    schedule 26.06.2017


Ответы (2)


При вызове exec() отображение памяти для вызывающего процесса становится недействительным. Он изменен для размещения нового исполняемого файла. Память стека, память кучи, глобальные данные и память кода выделяются для нового исполняемого файла. Сопоставления обычно определяются во время компоновки, а память выделяется языковыми библиотеками до вызова main().

ссылка: http://man7.org/linux/man-pages/man2/execve.2.html

person Michaël Roy    schedule 23.06.2017
comment
Это верно, насколько это возможно, но это не объясняет, почему exec() терпит неудачу, если недостаточно памяти зарезервировано для стека в вызывающей программе, или как вызывающая программа может знать, какое значение она должна зарезервировать. Чтобы взять результат моего теста выше, откуда в CentOS 7 берется значение 4 мегабайта и изменения, и как программа может узнать это значение перед вызовом exec()? - person Warren Young; 24.06.2017

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

struct rlimit g_default_stack_limit;  /* put in global scope */
getrlimit(RLIMIT_STACK, &g_default_stack_limit);

struct rlimit our_stack_limit;
memcpy(&our_stack_limit, &g_default_stack_limit, sizeof(our_stack_limit));
our_stack_limit.rlim_cur = 2000000;   /* some lower value */
setrlimit(RLIMIT_STACK, &our_stack_limit);

Затем восстановите это начальное значение перед запуском внешней программы и повторно примените новый предел после того, как будет создан дочерний элемент fork() или завершится синхронный вызов программы (например, через system()):

struct rlimit our_stack_limit;
getrlimit(RLIMIT_STACK, &our_stack_limit);
setrlimit(RLIMIT_STACK, &g_default_stack_limit);

if (system(...) == 0) {
    ....
}

setrlimit(RLIMIT_STACK, &our_stack_limit);

Это начальное значение может быть значением по умолчанию операционной системы или ограничением, установленным программой, вызвавшей вашу программу. В любом случае, это почти наверняка правильное начальное значение для передачи программам, которые по очереди вызываются вашей программой.

person Warren Young    schedule 23.06.2017
comment
Вы проверили размер среды, которую вы передаете (неявно) новому исполняемому файлу? (Для каждого элемента ent в environ подсчитайте strlen(ent)+1+sizeof char*.) Если в вашей системе Centos окажется, что это мегабайт или около того, загадка разгадана. - person rici; 24.06.2017
comment
@rici Я добавил расчет для этого в тестовую программу выше. В системе CentOS, которой требуется более 4 МБ стека для выполнения другой программы, размер среды составляет около 2,4 КБ. Но на почти неиспользуемой виртуальной машине CentOS 7, на которой я тестировал, требования к стеку составляли всего несколько десятков КБ. Есть еще идеи? - person Warren Young; 26.06.2017
comment
нет, но здесь есть кое-что, что я хотел бы отследить. У меня есть небольшая коллекция явно надежных отчетов о загадочных E2BIG, но я могу воспроизвести только те, в которых среда или список argv были каким-то образом раздуты. - person rici; 26.06.2017