Предполагается, что ОП хочет принудительно вернуть значение с A()
на B()
вместо main()
с того места, где A()
был вызван ранее ...
Я всегда верил, что знаю, как это может случиться, но никогда не пробовал сам. Так что я не удержался, чтобы немного повозиться.
Манипулирование возвратом вряд ли можно сделать переносимым, поскольку оно использует факты сгенерированного кода, которые могут зависеть от версии компилятора, настроек компилятора, платформы и т. Д.
Сначала я попытался выяснить некоторые подробности о coliru , которые я планировал использовать для игры:
#include <stdio.h>
int main()
{
printf("sizeof (void*): %d\n", sizeof (void*));
printf("sizeof (void*) == sizeof (void(*)()): %s\n",
sizeof (void*) == sizeof (void(*)()) ? "yes" : "no");
return 0;
}
Выход:
gcc (GCC) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
sizeof (void*): 8
sizeof (void*) == sizeof (void(*)()): yes
Демонстрация в реальном времени на coliru
Далее я сделал минимальный пример, чтобы получить представление о коде, который будет сгенерирован:
Исходный код:
#include <stdio.h>
void B()
{
puts("in B()");
}
void A()
{
puts("in A()");
}
int main()
{
puts("call A():");
A();
return 0;
}
Скомпилировано с x86-64 gcc 8.2
и -O0
:
.LC0:
.string "in B()"
B:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC0
call puts
nop
pop rbp
ret
.LC1:
.string "in A()"
A:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC1
call puts
nop
pop rbp
ret
.LC2:
.string "call A():"
main:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC2
call puts
mov eax, 0
call A
mov eax, 0
pop rbp
ret
Live Explore на Godbolt
На Intel x86 / x64:
call
сохраняет адрес возврата в стеке, прежде чем перейти к указанному адресу
ret
вставляет адрес возврата из стека в рег. ПК. еще раз.
(Другие процессоры могут делать это по-другому.)
Дополнительно
push rbp
mov rbp, rsp
интересен тем, что push
также хранит что-то в стеке, тогда как rsp
является регистром с текущим верхним адресом стека и rbp
его компаньоном, который обычно используется для относительной адресации локальных переменных.
Таким образом, локальная переменная (которая адресуется относительно rbp
- если не оптимизирована) может иметь фиксированное смещение по отношению к адресу возврата в стеке.
Итак, я добавил некоторый код в первый пример, чтобы войти в контакт:
#include <stdio.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
Выход:
&main(): 0x400613, &A(): 0x400553, &B(): 0x400542
call A():
in A()
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00
0x7ffcdedc9760: (+40) 50 06 40 00 00 00 00 00
0x7ffcdedc9768: (+48) 30 48 4a f3 3e 7f 00 00
0x7ffcdedc9770: (+56) 00 00 00 00 00 00 00 00
Демонстрация в реальном времени на coliru
Вот что я прочитал из этого:
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00 # local var. buffer
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00 # local var. pI (with address of buffer)
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00 # local var. i (4 bytes)
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00 # pushed rbp
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00 # 0x400649 <- Aha!
0x400649
- это адрес, который немного выше, чем адрес main()
(0x400613
). Учитывая, что в main()
был некоторый код до вызова A()
, это вполне логично.
Итак, если я хочу манипулировать обратным адресом, это должно произойти в pI + 32
:
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
exit(0);
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
printf("Possible candidate for ret address: %p\n", *(void**)(pI + 32));
*(void**)(pI + 32) = (byte*)&B;
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
т.е. Я "исправляю" адрес функции B()
в качестве адреса возврата в стек.
Выход:
&main(): 0x400696, &A(): 0x4005aa, &B(): 0x400592
call A():
in A()
0x7fffe0eb0858: (+ 0) 00 de ad be ef 4a 11 00
0x7fffe0eb0860: (+ 8) 58 08 eb e0 ff 7f 00 00
0x7fffe0eb0868: (+16) 80 08 eb e0 14 00 00 00
0x7fffe0eb0870: (+24) 80 08 eb e0 ff 7f 00 00
0x7fffe0eb0878: (+32) cc 06 40 00 00 00 00 00
0x7fffe0eb0880: (+40) e0 06 40 00 00 00 00 00
0x7fffe0eb0888: (+48) 30 c8 41 84 42 7f 00 00
0x7fffe0eb0890: (+56) 00 00 00 00 00 00 00 00
Possible candidate for ret address: 0x4006cc
in B()
Демонстрация в реальном времени на coliru
Et voilà: in B()
.
Вместо того, чтобы назначать адрес напрямую, того же можно достичь, сохранив строку длиной не менее 40 char
с в buffer
(только 8 char
с):
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
exit(0);
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
// provoke buffer overflow vulnerability
printf("Input: "); fflush(stdout);
fgets(buffer, 40, stdin); // <- intentionally wrong use
// show result
putchar('\n');
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
Скомпилировано и выполнено с:
$ gcc -std=c11 -O0 main.c
$ echo -e " \xa2\x06\x40\0\0\0\0\0" | ./a.out
Ввести точную последовательность байтов с клавиатуры может быть немного сложно. Копирование / вставка может работать. Для простоты я использовал echo
и перенаправление.
Выход:
&main(): 0x4007ba, &A(): 0x4006ba, &B(): 0x4006a2
call A():
in A()
0x7ffd1700bac8: (+ 0) 00 de ad be ef 4a 11 00
0x7ffd1700bad0: (+ 8) c8 ba 00 17 fd 7f 00 00
0x7ffd1700bad8: (+16) f0 ba 00 17 14 00 00 00
0x7ffd1700bae0: (+24) f0 ba 00 17 fd 7f 00 00
0x7ffd1700bae8: (+32) f0 07 40 00 00 00 00 00
0x7ffd1700baf0: (+40) 00 08 40 00 00 00 00 00
0x7ffd1700baf8: (+48) 30 48 37 0f 5b 7f 00 00
0x7ffd1700bb00: (+56) 00 00 00 00 00 00 00 00
Input:
in B()
Демонстрация в реальном времени на coliru
Обратите внимание, что ввод 32 пробелов (для выравнивания адреса возврата "\xa2\x06\x40\0\0\0\0\0"
с предполагаемым смещением) «уничтожает» все внутренние элементы A()
, которые хранятся в этом диапазоне. Это может иметь фатальные последствия для стабильности процесса, но, в конце концов, он достаточно неповрежден, чтобы достичь B()
и сообщить об этом на консоль.