When making an unsigned int pointer point to it's own address you can increment the return address by 10 to skip a line after the function returns. Example:
#include <stdio.h>
void f()
{
unsigned int *array = (unsigned int *) &array;
array[4] = array[4] + 10;
}
void main()
{
f();
printf("I am skipped\n");
}
Why must it be 10 that you increment by? What is the significance of this number and what is happening behind the scenes.
program on main [✘!?] via C v7.5.0-gcc ❯ gcc stack_overflow_question.c -o question; ./question
(no output) Therefore, line was skipped.
The code has undefined behavior: the pointer is initialized to point to itself and the code accesses memory at index 4 from this address, ie: 16 bytes beyond the address of the pointer in memory.
On the target architecture, with some luck, this address refers to the return address in the stack for the function f()
and the code patches this address to point 10 bytes further, which of course is very risky but seems to be executable code after the call to the function printf
.
As you observe, the effect is that printf
does not get called and the program exits normally.
Be aware that this behavior is very specific to the compiler, architecture, operating system and configuration used and by no means guaranteed.
Undefined behavior means anything can happen, including nothing visible or dire side effects.
As can be seen on the assembly output on godbolt.org, the stack frame of function f()
indeed has the return address 16 bytes from the location where the compiler (both gcc and clang) allocate the array
pointer (rbp-8
).
Yet gcc and clang generate different code for the printf("I am skipped\n")
, gcc translates this call to puts("I am skipped")
even without any optimisation turned on. Patching the return address by 10 bytes may work for one compiler and not the other, and probably does not work at all if optimisations are turned on.
There is no portable way to determine how to patch memory to skip a given line of code, and if you manage to achieve that, be aware that this result is very brittle as any change in the code or environment may break it.
Patching an existing executable to skip some code is less brittle as the patched code will not change until a new version is installed, but modern CPUs have special devices to make it harder, such as authenticated code pointers that would make the posted code fail even if the offsets are correct.