Linux x64 разматывает стек внутри обработчика сигнала, чтобы изменить адрес возврата

Я пытаюсь изменить адрес возврата (несколько уровней вниз) в стеке вызовов. Мне нужно сделать это, когда я нахожусь внутри обработчика сигнала. Поэтому я делаю следующее:

#include <csignal>
#include <cstdint>
#include <iostream>

// To print stacktrace
#include <execinfo.h>
#include <stdlib.h>

void printAround(uint64_t* p, int min=0, int max=3) {
for(int i = min; i <= max; ++i) {
std::cout << std::dec << ((i >= 0) ? " " : "") << i << ": "<< std::hex
<< reinterpret_cast<uint64_t>(*(p + i))
<< std::dec << std::endl;
}
std::cout << "================================================" << std::endl;
}

void sigHandler(int signum) {
register uint64_t* EBP asm ("rbp");
printAround(EBP);

uint64_t *oldEBP = reinterpret_cast<uint64_t*>(*EBP);
printAround(oldEBP);

oldEBP = reinterpret_cast<uint64_t*>(*oldEBP);
printAround(oldEBP);

/* PRINT STACK TRACE!! POSSIBLY UNSAFE! */
void *array[10];
size_t size;
char **strings;
size_t i;
size = backtrace(array, 10);
strings = backtrace_symbols(array, size);
std::cout << "\nObtained " << size << " stack frames.\n";
for (i = 0; i < size; i++) {
std::cout << strings[i] << "\n";
}
free(strings);
/* END PRINT STACK TRACE !! */
}

int foo(void) {
std::raise(SIGTRAP);
return 5;
}

int baz(void) {
return foo() + 10;
}

int bar(void) {
return baz() + 15;
}

int main(int argc, char **argv) {
// SIGTRAP is 0xCC
std::signal(SIGTRAP, &sigHandler);
return bar();
}

Соответствующий вывод:

 0: 7ffda9664a10
1: 7faf5777c4b0
2: 1
3: 0
================================================
0: 7ffda9664a20
1: 557f2a7bdf21
2: 7ffda9664a30
3: 557f2a7bdf2f
================================================
0: 7ffda9664a30
1: 557f2a7bdf2f
2: 7ffda9664a50
3: 557f2a7bdf59
================================================

Obtained 9 stack frames.
./main(+0xe41) [0x557f2a7bde41]
/lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7faf5777c4b0]
/lib/x86_64-linux-gnu/libc.so.6(gsignal+0x38) [0x7faf5777c428]
./main(+0xf11) [0x557f2a7bdf11]  => foo
./main(+0xf21) [0x557f2a7bdf21]  => baz
./main(+0xf2f) [0x557f2a7bdf2f]  => bar
./main(+0xf59) [0x557f2a7bdf59]  => main
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7faf57767830]
./main(+0xba9) [0x557f2a7bdba9]

По смещению 0 находится базовый указатель предыдущего стека, а по смещению 1 используется адрес возврата.

Как видно из вывода, первый адрес возврата — это первая функция в libc, но следующая уже baz и не foo или другую функцию libc, как я и ожидал.

Когда я удаляю обработчик сигнала и помещаю логику для печати в стек foo Я вижу все свои функции: foo, baz, bar, main

Что мне здесь не хватает? Мне нужно изменить адрес возврата функции, когда сигнал был запущен, то есть foo, но этот пропускается в моей логике размотки стека 🙁

Постскриптум Я знаю, что небезопасно использовать backtrace [2] в обработчике сигналов, так как это ведет к неопределенному поведению! Кажется, мне здесь везет, и проблема остается, когда я удаляю всю логику возврата!

Также, если у кого-то есть другие идеи, как решить эту проблему, я буду рад, если вы поделитесь. Я пытался использовать __builtin_frame_address() с аргументом> 0, но происходит сбой внутри обработчика сигнала [1]. Кажется, что-то другое, и я не могу найти информацию о чем.

[1] https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html

[2] https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

1

Решение

Хм, с чего мне начать …

Во-первых, я боюсь, что x64 по умолчанию не имеет указателей фреймов, поэтому нет простого способа восстановить стек, следуя цепочке rbps. Более того, даже если вы перекомпилировали весь участвующий код (включая Glibc!) С -fno-omit-frame-pointer Фрейм обработчика сигнала, вероятно, настроен ядром очень особым образом, поэтому нет гарантии, что вы сможете развернуть его с помощью указателя кадра. Кстати, это, вероятно, причина __builtin_frame_address сбои во время выполнения.

Далее вы упоминаете, что хотите размотаться, изменив адрес возврата за спиной компилятора. Ничто не было бы лучшим рецептом для крушения во время выполнения. Компилятор сохраняет тонны важной информации в функциональных рамках. Эта информация сохраняется строгими контрактами между вызывающими и вызываемыми функциями (так называемое «соглашение о вызовах»). Изменив адрес возврата, вы отбросите всю эту информацию и, скорее всего, вернетесь к целевому коду со случайным мусором в регистрах.

только Разумный способ реализовать разматывание стека состоит в использовании (или, по крайней мере, повторной реализации) существующих разматывателей (в libgcc, libunwind или, предпочтительно, в libbacktrace).

2

Другие решения

Решение для изменения адреса возврата из обработчика сигнала требует другого метода для регистрации обработчика сигнала в первую очередь.

Фрист код:

#include <csignal>
#include <cstdint>
#include <iostream>

void signal_handler(int signal, siginfo_t *si, void *context)
{
const int return_delta = 2;
((ucontext_t*)context)->uc_mcontext.gregs[REG_RIP] += return_delta;
}int foo(void)
{
asm(".byte 0xcc\n");

// "for(;;) ;" in a way that prevents the compiler from recognizing the
// remainder of the function as dead code and optimizing it away...
asm(".byte 0xEB\n");
asm(".byte 0xFE\n");

return 5;
}int baz(void) {
return foo() + 10;
}int bar(void) {
return baz() + 15;
}int main(int argc, char **argv)
{
struct sigaction sa = {0};
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = signal_handler;
sa.sa_flags = SA_SIGINFO;

// Install signal handler
sigaction(SIGTRAP, &sa, NULL);

// So that we see some output
size_t i{1000};
while(i--) {
std::cout << bar() << std::endl;
}
return 0;
}

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

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

Обратите внимание, что это очень зависит от платформы, вы должны использовать ucontext.h чтобы увидеть, как структура выглядит для вашей конкретной платформы.

1

По вопросам рекламы [email protected]