Тренируемся на кошках. Для того, чтоб не читать нудную теорию, я решил показать на примере как это происходит.
Для реализации нашей задумки нам потребуется:
1. Компилятор языка C/C++. Я использовал Microsoft Visual Studio 2005, но с Runtime Library от Microsoft Visual С++ 6.0(Необходимо для того, чтоб видеть коды ошибок во время испытаний)
2. WinHex(для работы с HEX значениями)
3. OllyDBG(отладчик, можно использовать любой)
4. IDA Pro 4(дизассемблер)
5. Трезвый рассудок и прямые руки
Для начала нам стоит ознакомиться с тестовым стендом. Это будет небольшая программа. Я надеюсь с кодом разберетесь
Code
#include <stdio.h>
#include <string.h>
int root()
{
printf("You have a root!\n");
return 0;
}
int main()
{
char login[16];
char passwd[16];
printf("login: ");
gets(login);
printf("passwd: ");
gets(passwd);
if (!strcmp(login, "bob") && ~strcmp(passwd, "god"))
{
printf("Hello, bob!\n");
}
return 0;
}
Забиваем это в среду разработки и компилируем. Для тех, кто использует Visual Studio рекомендую собирать Debug проект.
Запускаем нашу программу. Пробуем ввести логин вида: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
и пароль вида: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Нажимаем ввод. Что получилось? Windows ругнулась сообщением вида: Инструкция по адресу 0x41414141 обратилась к памяти по адресу 0x41414141... Откуда взялось это значение? Постойте! А ведь 0x41 это HEX ASCII значение буквицы "а".
Значит, во-первых, переполнение произошло в буфере логина, а во-вторых, данный тип переполнения допускает передачу управления на произвольный код, поскольку регистр – указатель команд переметнулся на содержащийся в хвосте буфера адрес. Случайным образом по адресу 0x41414141 оказался расположен бессмысленный мусор, возбуждающий процессор вплоть до исключения, но этому горю легко помочь!
Для начала нам предстоит выяснить, какие по счету символы логина попадают в адрес возврата. В этом нам поможет последовательность в стиле "qwerty...zxcvbnm", вводим ее и... система сообщает, что "инструкция по адресу 0x7a6c6b6a обратилась к памяти". Запускаем WinHex и набиваем эти "7A 6C 6B 6A" на клавиатуре. Получается "zlkj". Значит, в адрес возврата попали 17-й, 18-й, 19-й и 20-й символы логина (на архитектуре x86 младший байт записывается по меньшему адресу, то есть машинное слово, образно выражаясь, становится к лесу передом, а к нам задом).
Быстренько дизассемблируем программу и находим там функцию root!
При нормальном развитии событий она никогда не получает управления. Если, конечно, не подсунуть адрес ее начала вместо адреса возврата. А какой у root'а адрес? Смотрим – 00401150h. Перетягиваем младшие байты на меньшие адреса и получаем 50 11 40 00. Именно в таком виде адрес возврата хранится в памяти. Хорошо, что ноль в нем встретился лишь однажды, аккурат оказавшись на его конце. Пусть он и будет тем нулем, что служит завершителем всякой ASСII-строки. Символам с кодами 50h и 40h соответствуют буквицы "P" и "@". Символу с кодом 11h соответствует комбинация >Ctrl-Q< или >Alt<+>0, 1, 7< (нажмите Alt, введите на цифровой клавиатуре 0, 1 и 7, отпустите Alt).
Задержав дыхание, вновь запускаем программу и вводим "qwertyuiopasdfghP^Q@", пароль можно пропустить. Собственно говоря, символы "qwertyuiopasdfgh" могут быть любыми, главное, чтобы "P^Q@" располагались в 17-й, 18-й и 19-й позициях. Нуль, завершающий строку, вводить не надо: функция gets засунет его самостоятельно.
Если все сделано правильно, то программа победоносно выведет на экран "Your have root", подтверждая, что атака сработала. Правда, по выходе из root'а программа немедленно грохнется, так как в стеке находится мусор, но это уже неважно, ведь функция root отработала и стала не нужна.
Пишем примитивный шелл код.
Вообще говоря, организовать удаленный shell не так-то просто: необходимо, как минимум, установить TCP/UDP-соединение, попутно обманув доверчивый firewall, создать пайпы, связать их дескрипторами ввода/вывода терминальной программы, а самому работать диспетчером, гоняя данные между сокетами и пайпами.
Если среди читателей наберется кворум, эту тему можно будет осветить во всех подробностях, пока же ограничится локальным shell'ом.
Вновь запускаем нашу демонстрационную программу, срываем буфер, вводя строку "AAA...", но вместо того чтобы нажать "ОК" в диалоге критической ошибки приложения, давим "отмену", запускающую отладчик (для этого он должен быть установлен). Конкретно нас будет интересовать содержимое регистра ESP в момент сбоя. На моей машине он равен 0012FF94h, у тебя это значение может отличаться. Вводим этот адрес в окне дампа и, прокручивая его вверх/вниз, находим нашу строку "ААААА". В моем случае она расположена по адресу 0012FF80h.
Теперь мы можем изменить адрес возврата на 12FF94h, и тогда управление будет передано на первый байт переполняющегося буфера. Остается лишь подготовить shell-код. Чтобы вызвать командный интерпретатор в осях семейства NT, необходимо дать команду WinExec("CMD", x). В 9x такого файла нет, но зато есть command.com, который есть анахронизм. На языке ассемблера этот вызов может выглядеть так:
00000000: 33C0 xor eax,eax
00000002: 50 push eax
00000003: 68434D4420 push 020444D43 ;" DMC"
00000008: 54 push esp
00000009: B8CA73E977 mov eax,077E973CA ;"wesE"
0000000E: FFD0 call eax
00000010: EBFE jmps 000000010
Здесь мы используем целый ряд хитростей и допущений, подробный разбор которых требует целой книги. Если говорить кратко, то 77E973CAh – это адрес API-функции WinExec, жестко прописанный в программу и добытый путем анализа экспорта файла KERNEL32.DLL утилитой DUMPBIN. Это грязный и ненадежный прием, так как в каждой версии оси адрес функции свой и правильнее было бы добавить в shell-код процедуру обработки экспорта, описанную в следующей статье. Почему вызываемый адрес предварительно загружается в регистр EAX? Потому что call 077E973CAh на самом деле ассемблируется в относительный вызов, чувствительный к местоположению call'а, что делает shell-код крайне немобильным.
Почему в имени файла "CMD" (020444D43h, читаемое в обратном порядке) стоит пробел? Потому что в shell-коде не может присутствовать символ нуля – он служит завершителем строки. Если хвостовой пробел убрать, то получится 000444D43h, а это уже не входит в наши планы. Вместо этого мы делаем XOR eax, eax, обнуляя EAX на лету и запихивая его в стек для формирования нуля, завершающего строку "CMD ". Но непосредственно в самом shell-коде этого нуля нет!
Поскольку в отведенные нам 16 байт shell-код влезать никак не хочет, а оптимизировать его уже некуда, мы прибегаем к вынужденной рокировке и перемещаем shell-код в парольный буфер, отстоящий от адреса возврата на 32 байта. Учитывая, что абсолютный адрес парольного буфера равен 12FF70h (у вас он может быть другим!), shell-код будет выглядеть так (просто переводим hex-коды в ASCII-символы, вводя непечатные буквицы через alt+num):
login :1234567890123456>alt-112<>alt-255<>alt-18<
passwd:3>alt-192<PhCMD T>alt-184<>alt-202<s>alt-233<w>alt-255<>alt-208<>alt-235<>254<
Вводим это в программу. Логин срывает стек и передает управление на парольный буфер, где лежит shell-код. На экране появляется приглашение командного интерпретатора. Все! Теперь с системой можно делать что угодно!
Заключение.
Повторяя выше описанные действия вы поймете как этот механизм работает. Попробуйте повторить =) Вам понравиться =)
В следующий раз мы продолжим беседовать на эту тему.
з.ы. Ознакомьтесь со статьей: UNIX Assembly Codes Development for Vulnerabilities Illustration Purposes" – великолепное руководство по технике переполнения буферов и захвату контроля удаленной машиной.