Как испортить стек в программе на 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
есть только одна локальная переменная, и мы можем найти адрес этой переменной, просто ища в памяти.
Сначала мы помещаем подпись в буфер, пусть это будет 4-байтовое целое число 0x04c3b2a1 (помните, что x86_64 имеет младший порядок байтов).
После этого мы объявляем две переменные для представления регистров:
rsp
является указателем стека, иr10
это просто какой-то неиспользованный регистр. Это позволяет не использоватьasm
операторы позже в коде, все еще будучи в состоянии использовать регистры напрямую. Важно, что переменные на самом деле не занимают стековую память, они сами являются ссылками на регистры процессора.После этого мы перемещаем указатель стека с шагом 4 байта (так как размер
int
4 байта), пока мы не доберемся доbuffer
, Мы должны запомнить смещение от указателя стека до первой переменной здесь, и мы используемr10
хранить его.Далее, мы хотим знать, как далеко в стеке находятся экземпляры
function_b
а такжеfunction_a
, Хорошее приближение - насколько далекоbuffer
а такжеbeacon
так что теперь мы ищемbeacon
,После этого мы должны оттолкнуть
beacon
первая переменнаяfunction_a
к началу экземпляра целогоfunction_a
в стеке. Что мы делаем, вычитая значение, хранящееся вr10
,Наконец, здесь приходит немного более странный момент. По крайней мере, в моей конфигурации стек выровнен по 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;
}