Как испортить стек в программе на 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.

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, макрос просто компилируется в пустой оператор. Конечно, другие переменные и регистры могут быть напечатаны аналогичным образом.

Хорошо, давайте предположим, что эпилог (то есть код в } линия 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;
}
Другие вопросы по тегам