1. Header
В прошлой статье я выложил информацию общего плана, которая необходима для тех,
кто услышал о эксплоитах впервые, а тем более никогда не вникал в основную идею.
Теперь приступлю к изложению непосредственно практики, которая на самом
деле и является воплощением предыдущей статьи.
Сразу хочу предупредить, что данный шелл не совершенен и максимально упрощен,
к примеру вместо получения адреса необходимых функций с помощью пары
LoadLibrary/GetProcAddress используются прямые ссылки, что локализирует
действие данного шелла на те системы, на которых адреса, зашитые в шелл
совпадут с реальными адресами функций в DLL. Очевидно от чего это зависит -
если Windows загрузит DLL по другой базе, то шелл вылетит с сообщением...
...где 0х77е8898b адрес jmp esp в kernel32.dll в моей системе.
Поэтому в данном шелле подбор адресов должен производится чисто индивидуально
для каждой системы. Далее я опишу как определять эти адреса. Зачем так стараться?
ИМХО: это улучшит навыки любого кто самостоятельно найдет нужный адрес своим
девайсом(то есть ручками). А вообще нужно действовать несколько иначе: для особо
продвинутых подскажу идеи:
а) Использовать ссылки из таблицы импорта
б) Использовать LoadLibrary/GetProcAddress
Но эта тема будет обсуждаться в других статьях.
Начнем...
2. Sections
Пишем собственную прогу вида
"owerflow.c"
#include <stdio.h>
#include <string.h>
int test(char *big)
{
char buffer[100]; // переполняемый буфер
strcpy(buffer,big);// собственно само переполнение
return 0;
}
int main ()
{
char big[512];
gets(big); // получение текствой строки-сюда-то мы и передаем наш шелл
test(big); // вызов уязвимой функции
return 0;
}
В этом коде нет ничего сверхъестественного, а потому идем далее. Как определить,
что переполняется, где переполняется и чего куда передавать?
Элементарно! Берем, запускаем нашу программу owerflow.exe(для танкистов -
owerflow.exe получается путем компиляции owerflow.c ) и передаем ей строку
вида:
Аааааа.........ааааааааа - где-то примерно символов 110 для уверенности. И что мы видим?
Доигрались - скажете вы. А на самом-то деле все отлично прошло, вы взорвали
буфер и как результат(об этом я и упоминал в первой части), перезаписали адрес
возврата из функции кодом 0х61(тоесть символом 'a'). А теперь я вас хочу спросить:
кто мешает вам вместо бессмысленных строк символов передать строку опкодов,
которая получила гордое название шелл-кода? Никто! При внимательном
рассмотрении сложившейся ситуации под четким оком SoftIce, легко понять что
произошло: благодаря специфике стека наша строка затерла собой как сохраненное
значение ebp, так и адрес возврата. Обратите внимание, что адрес возврата
затирается 104,105,106,107 символами нашей строки(это видно тогда, когда вместо
ааа..аааа передется последовательность символов с ASCII кодами начиная с 32 по 256),
поэтому необходимо сформировать строку так, чтобы 104-107 байты содержали адрес, по
которому нужно передать управление. Теперь выясним это самый адрес, но сперва замечу,
что байты с 100 по 103 перекрывают сохраненное значение EBP - это нам тоже пригодится
для формирования стэка, но об этом позже. Посмотрев в SoftIce содержимое регистра esp
в момент переполнения, легко установить, что там содержится адрес байта нашей строки,
следующего за последним из четырех байтов, перекрывающих
EIP.
Отлично! Осталось заполнить строку, начиная со 108-позиции, опкодами и передать
управление по адресу в esp. Для этого снова переполним программу при запущенном Sice
и когда он всплывет введем команду:
:S 10000000 l ffffffff FF e4
где 10000000-ffffffff-диапазон поиска, а FF e4-опкод инструкции jmp esp
получим:
;Pattern found at xxxxxxxx <- этот адрес может отличаться(у меня он равен 77e98601,
что соответствует ntdll.dll).
Мы определили адрес jmp esp-теперь мы передадим этот адрес в позиции 104-107 и получим,
что при переполнении в eip будет помещен адрес инструкции jmp esp из ntdll.dll, которая
и перебросит нас на 108-позицию нашей строки. Осталось эту самую строку наполнить
опкодами. В качестве шелла обычно используют код, реализующий загрузку консоли(для
виндов это аналогично окну Command Prompt). Для этого составим программу на C:
"winexec.c"
#include <windows.h>
typedef (*PFUNK)(char*,DWORD);
int main ()
{
HMODULE hDll=LoadLibrary("kernel32.dll");
PFUNK pFunc=(PFUNK) GetProcAddress(hDll,"WinExec");
(*pFunc)("cmd.exe //K start cmd.exe",SW_SHOW);
}
WinExec исполняет программу, требует 2 параметра и располагается в kernel32.dll.
Все это работает потому, что kernel32.dll использует любая программа и потому, что адрес не
содержит нулевых байтов, наличие которых недопустимо. В переменной pFunc получим адрес
WinExec, у каждого он будет свой. Теперь нам нужно сформировать асм-код, вызывающий WinExec.
Вот он:
Этот код проверялся в Visual C++6.0 и все работает отлично. Ну теперь осталось
сформировать строку из опкодов. А где их взять? Да в том же Visual C++ Debugger. Просто
при трассировке из контекстного меню выберите опцию Code Bytes при включенном
Disassembly mode и вы получите необходимые опкоды. Осталось только собрать все воедино:
Ну вот вроде и все. Единственное: добавлю про ebp - этот регистр играет важную роль в
нашем нелегком деле. Нужно где-то формировать стэк, но где? А почему не использовать
под стэк наш буфер, заполненный NOP? Так и забилдим, под SoftIce посмотрим содержимое ESP
и отнимем от него 64h либо зададим искать строку 0х9090909090 - кто как желает,
главное найти адрес начала буфера. Затем этот адрес поместим в EBP (помните в начале
я акцентировал внимание на том, что байты с 100 по 103 перекрывают ebp - ну так и
поместим найденный адрес в эти байты предварительно удалив из него нули). А как? Да
очень просто - сделать Исключающее ИЛИ в терминах булевой алгебры, либо по-простому XOR.
Тоесть иксорим начальный адрес, передаем в ebp, а затем в шелле снова делаем
XOR EBP,0xFFFFFFFF и все! Теперь у нас есть стек.
Недостатками данного шелла являются прямые ссылки на функции, возможно я поправлю эти
фичи и запортирую новый шелл, гораздо более универсальный.