Как испортить стек в программе на C

Я должен изменить назначенный раздел function_b, чтобы он изменил стек таким образом, чтобы программа печатала:

Executing function_a
Executing function_b
Finished!

В этот момент он также печатает Executed function_b между Executing function_b и Finished!.

У меня есть следующий код, и я должен что-то заполнить в той части, где написано // ... вставьте сюда код

#include <stdio.h>

void function_b(void){
char buffer[4];

// ... insert code here

fprintf(stdout, "Executing function_b\n");
}

void function_a(void) {
int beacon = 0x0b1c2d3;
fprintf(stdout, "Executing function_a\n");
function_b();
fprintf(stdout, "Executed function_b\n");
}

int main(void) {
function_a();
fprintf(stdout, "Finished!\n");
return 0;
}

Я использую Ubuntu Linux с компилятором gcc. Я компилирую программу со следующими опциями: -g -fno-stack-protector -fno-omit-frame-pointer. Я использую процессор Intel.


person user7733386    schedule 18.03.2017    source источник
comment
В языке Си нет стека.   -  person too honest for this site    schedule 19.03.2017
comment
@Olaf Да, есть, даже если стандарт C не называет это «стеком».   -  person Gilles 'SO- stop being evil'    schedule 19.03.2017
comment
В какой среде вы работаете (операционная система, тип процессора, компилятор, параметры компилятора)? То, что вы хотите сделать, очень зависит от всех этих параметров.   -  person Gilles 'SO- stop being evil'    schedule 19.03.2017
comment
@Gilles Привет, спасибо за быстрый ответ. Я использую Ubuntu Linux с компилятором gcc. Я компилирую программу со следующими параметрами: -g -fno-stack-protector -fno-omit-frame-pointer. Я использую процессор Intel.   -  person user7733386    schedule 19.03.2017
comment
@Gilles: Пожалуйста, предоставьте ссылку на стандарт, где требуется стек (или что-то вроде стека, даже если он называется Fred).   -  person too honest for this site    schedule 19.03.2017
comment
Таким образом, для желаемого вывода необходимо пропустить эту строку: fprintf(stdout, "Executed function_b\n");?   -  person Arash    schedule 19.03.2017
comment
вам нужно увеличить адрес возврата function_b на n байтов - сколько именно байтов занимает fprintf(stdout, "Executed function_b\n");, но в этом случае я думаю, что невозможно вычислить это правильно, так как это будет работать как на x86, так и на x64, с любой оптимизацией и соглашением о вызовах. однако когда-то подобные задачи имеют абсолютно правильное решение - для примера . ваш вопрос также задан здесь   -  person RbMm    schedule 19.03.2017
comment
@ Жиль, какую терминологию использует стандарт C для описания понятия стека? Спасибо!   -  person sigjuice    schedule 19.03.2017
comment
@sigjuice - может быть, например, в стандарте C не существует регистров ЦП, но он действительно существует. и стек существует на платформе x86/x64, независимо от того, охвачен ли он стандартом c или нет   -  person RbMm    schedule 19.03.2017


Ответы (2)


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

void function_b(void) {
    char buffer[4];
    buffer[0] = 0xa1; // part 1
    buffer[1] = 0xb2;
    buffer[2] = 0xc3;
    buffer[3] = 0x04;
    register int * rsp asm ("rsp"); // part 2
    register size_t r10 asm ("r10");
    r10 = 0;
    while (*rsp != 0x04c3b2a1) {rsp++; r10++;} // part 3
    while (*rsp != 0x00b1c2d3) rsp++; // part 4
    rsp -= r10; // part 5
    rsp = (int *) ((size_t) rsp & ~0xF); // part 6
    fprintf(stdout, "Executing function_b\n");
}

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

  1. Сначала ставим в буфер сигнатуру, пусть это будет 4-х байтное целое число 0x04c3b2a1 (помните, что x86_64 имеет прямой порядок байтов).

  2. После этого мы объявляем две переменные для представления регистров: rsp — это указатель стека, а r10 — просто какой-то неиспользуемый регистр. Это позволяет не использовать операторы asm позже в коде, но при этом иметь возможность напрямую использовать регистры. Важно, что переменные на самом деле не занимают память стека, они сами являются ссылками на регистры процессора.

  3. После этого мы перемещаем указатель стека с шагом в 4 байта (поскольку размер int равен 4 байтам), пока не дойдем до buffer. Мы должны помнить смещение от указателя стека до первой переменной здесь, и мы используем r10 для его сохранения.

  4. Далее мы хотим узнать, как далеко в стеке находятся экземпляры function_b и function_a. Хорошее приближение — это расстояние между buffer и beacon, поэтому теперь мы ищем beacon.

  5. После этого мы должны вернуться от beacon, первой переменной function_a, к началу экземпляра всего function_a в стеке. Это мы делаем, вычитая значение, хранящееся в r10.

  6. Наконец, вот еще немного. По крайней мере, в моей конфигурации стек оказывается выровненным по 16 байтам, и в то время как массив buffer выравнивается по левому краю 16-байтового блока, переменная beacon выравнивается по правому краю такого блока. Или это что-то с похожим эффектом и другим объяснением?.. Так или иначе, поэтому мы просто очищаем последние четыре бита указателя стека, чтобы он снова выровнялся по 16 байтам. 32-битный GCC для меня ничего не выравнивает, поэтому вы можете пропустить или изменить эту строку.


При работе над решением мне пригодился следующий макрос:

#ifdef DEBUG
#define show_sp() \
    do { \
        register void * rsp asm ("rsp"); \
        fprintf(stdout, "stack pointer is %016X\n", rsp); \
    } while (0);
#else
#define show_sp() do{}while(0);
#endif

После этого, когда вы вставляете show_sp(); в свой код и компилируете с -DDEBUG, он печатает значение указателя стека в соответствующий момент. При компиляции без -DDEBUG макрос просто компилируется в пустой оператор. Конечно, другие переменные и регистры можно вывести аналогичным образом.

person Gassa    schedule 19.03.2017
comment
это вообще неверно. не решение просто добавить указатель стека. например, функция может изменить энергонезависимые регистры и перед этим сохранить их в стеке. вы не восстановите исходные значения. мы не можем знать, насколько глубоко beacon в стеке относительный адрес возврата и A good approximation is how far are buffer and beacon - на самом деле неправильно. Я уже не говорю, что с оптимизацией вообще не будет никаких beacon в стеке. все неправильно от начала до конца. и исходный вопрос тоже не имеет правильного решения. - person RbMm; 19.03.2017
comment
только 6. действительно верно на платформе x64 - Стек всегда выравнивается по 16 байтам, когда выполняется инструкция вызова. Когда инструкция call помещает адрес возврата, стек выравнивается по 16 байтам. Код пролога вызываемой функции выровняет стек как обычно. - person RbMm; 19.03.2017
comment
comment
@RbMm Спасибо за комментарии! Приятно видеть, что я на самом деле получил теоретически обоснованную часть решения. Конечно, если оптимизация включена, нам пришлось бы замусорить всю программу volatile и еще чем-то. Но точные параметры командной строки даны в задании, так что суть вопроса скорее в том, чтобы заставить ее работать один раз, здесь и сейчас, чем в наброске теоретически правильного решения, которого, как вы говорите, не существует изначально. - person Gassa; 19.03.2017
comment
@RbMm Я вижу, что в вопросе, который вы связали, основное отличие, похоже, заключается в том, что он изменяет экземпляр function_b вместо того, чтобы возиться с регистрами. Так что любой код входа/выхода функции работает корректно. Но все равно возможный необходимый код при выходе из function_a все равно не будет вызываться, если только нам не удастся пропустить из него хотя бы один fprintf. И это потребует (непереносимых) догадок о том, сколько байтов на самом деле занимают инструкции вызова и подготовки, верно? В любом случае, я был бы рад увидеть здесь более правильный ответ. - person Gassa; 19.03.2017
comment
вы меняете указатель стека (rsp или esp на основе x64/x86) в function_b на значение, которое должно быть в function_a, и, наконец, вместо выполнения эпилога A вы выполняете эпилог B. это правильно, если эпилог этих двух функций одинаков. оно не эквивалентно/симметрично - но это предположение действительно может быть верным. на основе этого я вставляю собственное решение, и оно основано только на этом предположении. и работал с любой оптимизацией, даже если маяк будет удален компилятором - person RbMm; 19.03.2017
comment
сколько байтов занимает fprintf вызов для настройки обратного адреса на этом количестве байтов - невозможно вычислить - могут быть очень разные реализации. но я вставляю другое решение (для CL ) компилятора. но если вы знаете аналог для _AddressOfReturnAddress для GCC - вы также можете легко протестировать мой код или GCC. работает с любой оптимизацией и не зависит от x86 или x64 - person RbMm; 19.03.2017

хорошо, предположим, что эпилог (то есть код в } строке) для function_a и для function_b одинаков

несмотря на то, что функции A и B не симметричны, мы можем предположить это, потому что они имеют одинаковую сигнатуру (без параметров, без возвращаемого значения), одинаковые соглашения о вызовах и одинаковый размер локальных переменных (4 байта - int beacon = 0x0b1c2d3 против char buffer[4];) и с оптимизацией - обе должны быть удалены, потому что неиспользованный. но мы не должны использовать дополнительные локальные переменные в function_b, чтобы не нарушить это предположение. самый проблемный момент здесь - то, что function_A или function_B будет использовать энергонезависимые регистры (и в результате сохранить его в прологе и восстановить в эпилоге) - но однако похоже здесь не место для этого.

поэтому мой следующий код основан на этом предположении - epilogueA == epilogueB (на самом деле решение @Gassa также основано на нем.

также нужно очень четко указать, что function_a и function_b не должны быть встроенными. это очень важно - без этого никакое решение невозможно. поэтому я позволил себе добавить атрибут noinline к function_a и function_b. примечание - не изменение кода, а добавление атрибута, что автор этой задачи неявно подразумевает, но не указывает явно. не знаю, как в GCC пометить функцию как noinline, но в CL для этого используется __declspec(noinline).

следующий код, который я пишу для компилятора CL, где существует следующая встроенная функция

void * _AddressOfReturnAddress();

но я думаю, что GCC также должен иметь аналог этой функции. также я использую

void* _ReturnAddress();

но на самом деле _ReturnAddress() == *(void**)_AddressOfReturnAddress() и мы можем использовать только _AddressOfReturnAddress(). простое использование _ReturnAddress() делает исходный (но не двоичный) код меньше и читабельнее.

и следующий код работает как для x86, так и для x64. и этот код работает (проверено) с любой оптимизацией.

несмотря на то, что я использую 2 глобальные переменные - код является потокобезопасным - действительно мы можем вызывать main из нескольких потоков одновременно, вызывать его несколько раз - но все будет работать правильно (только, конечно, как я говорю в начале, если epilogueA == epilogueB)

надеюсь, комментарии в коде достаточно сами объяснили

__declspec(noinline) void function_b(void){
    char buffer[4];

    buffer[0] = 0;

    static void *IPa, *IPb;

    // save the IPa address
    _InterlockedCompareExchangePointer(&IPa, _ReturnAddress(), 0);

    if (_ReturnAddress() == IPa)
    {
        // we called from function_a
        function_b();
        // <-- IPb
        if (_ReturnAddress() == IPa)
        {
            // we called from function_a, change return address for return to IPb instead IPa
            *(void**)_AddressOfReturnAddress() = IPb;
            return;
        }
        // we at stack of function_a here.
        // we must be really at point IPa
        // and execute fprintf(stdout, "Executed function_b\n"); + '}' (epilogueA)
        // but we will execute fprintf(stdout, "Executing function_b\n"); + '}' (epilogueB)
        // assume that epilogueA == epilogueB
    }
    else
    {
        // we called from function_b
        IPb = _ReturnAddress();
        return;
    }

    fprintf(stdout, "Executing function_b\n");
    // epilogueB
}

__declspec(noinline) void function_a(void) {
    int beacon = 0x0b1c2d3;
    fprintf(stdout, "Executing function_a\n");
    function_b();
    // <-- IPa
    fprintf(stdout, "Executed function_b\n");
    // epilogueA
}

int main(void) {
    function_a();
    fprintf(stdout, "Finished!\n");
    return 0;
}
person RbMm    schedule 19.03.2017
comment
Выглядит хорошо. Однако у меня возникли проблемы с переводом этого в GCC. Я использую __builtin_return_address(1) вместо _ReturnAddress(), как в вашей ссылке. Часть _InterlockedCompareExchangePointer, возможно, __atomic_compare_exchange, как здесь. Я не вижу прямого эквивалента _AddressOfReturnAddress и пытался использовать что-то вроде это ТАК ответ, но он нарушает логику if (_ReturnAddress() == IPa). - person Gassa; 19.03.2017
comment
@Gassa _AddressOfReturnAddress является ключевой точкой, абсолютно обязательно иметь поддержку компилятора для решения. _ReturnAddress() == *(void**)_AddressOfReturnAddress так что не надо - person RbMm; 19.03.2017
comment
@Gassa - может быть это_AddressOfReturnAddress()=(void**)__builtin_frame_address (0) + 1 и нужно использовать __builtin_return_address(0) но не 1 как _ReturnAddress(). но не уверен, где действительно точка __builtin_frame_address (0) - точно ли она на размер регистра меньше, чем адрес обратного адреса. не использовать gcc для проверки - person RbMm; 19.03.2017
comment
Спасибо, я пробовал это, но на самом деле это не сработало. Я сейчас вышел. Для справки, моя текущая попытка вставлена ​​сюда. - person Gassa; 19.03.2017
comment
@Gassa - у меня нет gcc - поэтому я не могу проверить, где __builtin_frame_address(0) фактическая точка. нужно просто посмотреть сгенерированный ассемблерный код. и сравните с __builtin_return_address(0). какой код генерирует __builtin_return_address(0) (предположим, что-то вроде mov rax,[rsp+N]) и __builtin_frame_address(0), какой код (предположим, lea rax,[rsp+M]) и как связаны N и M для одной и той же функции? они равны или говорят M=N-sizeof(void*) ? - person RbMm; 19.03.2017