Этот 32-битный пример иллюстрирует, как вы можете понять это, см. Ниже для 64-битного:
#include <stdio.h>
void function() {
char buffer[64];
char *p;
asm("lea 4(%%ebp),%0" : "=r" (p)); // loads address of return address
printf("%d\n", p - buffer); // computes offset
buffer[p - buffer] += 9; // 9 from disassembling main
}
int main() {
volatile int x = 7;
function();
x++;
printf("x = %d\n", x); // prints 7, not 8
}
В моей системе смещение равно 76. Это 64 байта буфера (помните, что размер стека уменьшается, поэтому начало буфера далеко от адреса возврата) плюс любой другой детрит между ними.
Очевидно, что если вы атакуете существующую программу, вы не можете ожидать, что она вычислит ответ для вас, но я думаю, что это иллюстрирует принцип.
(Также нам повезло, что +9
не переносится в другой байт. В противном случае однобайтовое приращение не установит адрес возврата, как мы ожидали. Этот пример может сломаться, если вам не повезло с адресом возврата в main
)
Я как-то упустил 64-битность исходного вопроса. Эквивалент для x86-64 равен 8(%rbp)
, потому что указатели имеют длину 8 байт. В этом случае моя тестовая сборка выдает смещение 104. В приведенном выше коде подставьте 8(%%rbp)
, используя двойной %%
, чтобы получить один %
в выходной сборке. Это описано в этом документе ABI . Искать 8(%rbp)
.
В комментариях есть жалоба на то, что 4(%ebp)
так же волшебно, как 76
или любое другое произвольное число. Фактически значение регистра %ebp
(также называемого «указатель кадра») и его отношение к расположению адреса возврата в стеке стандартизировано. Одна иллюстрация, которую я быстро погуглил, - здесь . Эта статья использует терминологию «базовый указатель». Если вы хотите использовать переполнение буфера на других архитектурах, это потребует столь же подробного знания соглашений о вызовах этого ЦП.