I'm trying to modify a return address (some levels down) on the call stack. I need to do this when I am inside a signal handler. Therefore I'm doing the following:
#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();
}
The corresponding output is:
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]
At offset 0 is the base pointer of the previous stack, and at offset 1 us the return address.
As one can see in the output, the first return address is the first function in libc, but the next one is already baz and not foo or the other libc function as I would have expected.
When I remove the signal handler and place the logic to print the stack in foo I see all of my functions: foo, baz, bar, main...
What am I missing here? I do need to modify the return address to the function in witch the signal was triggered, i.e., foo, but this one is skipped in my stack unwind logic :(
P.S. I do know that it is not safe to use backtrace [2] in a signal handler, since it leads to undefined behaviour! Seems I'm being lucky here and the problem persists when I delete all the backtrace logic!
Also if anyone has any other ideas how to solve this problem I'm happy if you would share. I tried to use __builtin_frame_address() with an argument >0 but this crashes inside the signal handler [1]. There seems to be something different, and I can't find any information about the what.
[1] https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html
[2] https://www.gnu.org/software/libc/manual/html_node/Backtraces.html
The solution to modify the return address from within a signal handler, requires a different method to register the signal handler in the first place.
Frist the code:
#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 in conjunction with sigaction can be used to pass more information to the signal handler. If you do so, siginfo_t is passed to the signal handler which contains all sort of information about the signal itself.
Additionally a void *context is passed to the registered signal handler which contains information about the state of the registers and stack. You can use this to manipulate the return address into the signal raising function as you like.
Note that this is highly platform specific you should use your ucontext.h to see how the struct look for your specific platform.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With