Заточи эксплоит под себя!

В сети валяется множество демонстрационных (proof-of-concept) exploit'ов, создающих файл на «жертвенном» диске или выводящих сообщение об уязвимости на экран. Для атаки на удаленные системы они не пригодны, и даже если нам посчастливится встретить боевой exploit, открывающий shell, то вовсе не факт, что он заведется без предварительной доработки...

Начну с главного — с отречения. Ни к чему деструктивному не призываю и употребляю термин «хакер» со всем позитивом, на которое только способен. Атаковать чужие системы можно только с явного разрешения их владельцев, в противном случае это будет не хакерство, а чистая уголовщина со всеми отсюда вытекающими последствиями. В то же время протянуть шнурок к приятелю-хакеру и тестировать с ним exploit'ы, никакое законодательство не запрещает. На этом и закончим.

Где брать?

Поиск новых exploit'ов очень похож на охоту: всемирная сеть велика, а дичь гнездится там, где никогда бы не подумал ее искать. Самые свежие exploit'ы обычно выкладываются на немодерируемые хакерские форумы с высоким трафиком (среди которых выделяется lists.grok.org.uk/pipermail/full-disclosure/), откуда они с некоторой степенью оперативности попадают на wwwsecurityforcus.com и другие «накопители дыр», превратившиеся в последнее время в сплошные помойки, источающие зловонный запах давно непроветриваемых отстойников.

Кстати говоря, securityfocus как-то очень странно устроен. В разделе «exploit» обычно присутствует текст такого рода: «В настоящее время мы не знаем об exploit'е для этой дыры. Если ты считаешь, что мы ошибаемся или имеешь более свежую информацию, то, пожалуйста, напиши нам на vuldb@securityfocus.com.» Не верь им! Ссылки на exploit'ы часто (но не всегда!) находятся в соседнем разделе — «reference». Если же их там нет — вводишь название дыры (брать только значимые слова, например «Microsoft Internet Explorer Unspecified OBJECT Tag Memory Corruption Variant Vulnerability», а лучше всего отыскивается по запросу «IE OBJECT tag»), добавляешь ключевое слово «exploit» и идешь курить гугл, обязательно обращая внимание на дату публикации, а то ведь так недолго и в позапрошлогоднюю дырку залететь, а потом долго недоумевать, почему exploit не фурычит.

Модерируемые форумы (типа bugtraq на seclists.org) содержат более концентрированную информацию, но для того, чтобы откопать рабочий exploit, приходится очень долго ковыряться. Зачем гнаться за свежачком? Все равно, даже с учетом Windows Update, множество машин не латаются годами! Намного проще отправиться в «лавку exploit'ов», где выложен разный антиквариат, среди которого хотелось бы отметить Metaexpolit Framework Project (wwwmetasploit.com) – своеобразный универсальный «движок», изначально написанный на Perl'e, а начиная с версии 3.0 переписанный на Ruby и работающий как из командной строки, так и через Web-интерфейс. К движку подключаются «топливные модули» — гибкие и высококонфигурабельные exploit'ы, способные нести на своем борту любую боевую нагрузку (payload). Собственно говоря, разделение кода на «движок», «exploit» и «payload» есть главное преимущество Metaexpolit Framework'а перед обычными exploit'ами, где все эти три агрегата смешаны в кучу. Поэтому, чтобы подключить свою собственную боевую начинку, приходится каждый раз разбираться, что, как и куда. Исходный код движка распространяется бесплатно и неплохо документирован. Там же, на сайте проекта, можно найти достаточно оперативно пополняемую базу exploit'ов и минимальный комплект боевой нагрузки (wwwmetasploit.com/sc/win32msf20payloads.tar.gz).

Другой полезный сайт — MilW0rm (milw0rm.com) — содержит огромную коллекцию exploit'ов под всевозможные системы, достаточно оперативно обновляемую и к тому же неплохо классифицированную, что значительно упрощает поиск, избавляя тебя от необходимости качать все подряд. Здесь же находятся примеры shell-кода с готовой боевой нагрузкой и немногочисленный инструментарий.

Популярный Packet Storm (wwwpacketstormsecurity.org) значительно реже обновляется, да и коллекция exploit'ов у него победнее будет, зато на нем выложено умопомрачительное количество статей и

всякого полезного инструментария: от сканеров безопасности до мелких утилит в десяток строк.

Кстати говоря, чаще всего узнаю о новых дырах не через форумы, а от знакомых. Достаточно завести обширную переписку — и можно быть в курсе дел, происходящих на всех континентах! Ведь силами одного человека отслеживать появление новых дыр просто нереально, разве что полностью посвятить свою жизнь уязвимостям.

Чем компилировать?

Чаще всего exploit'ы пишутся на Си/Си++, Perl, Python и PHP, реже — на всякой экзотике типа Ruby, причем тип языка указывается далеко не всегда, а о версии транслятора и ключах компиляции остается только догадываться. Вот такая культура программирования, с которой нам приходится жить.

Ладно, Perl узнается с первого взгляда по строке «#!/usr/bin/perl», идущей впереди листинга. Если же ее нет — смотрим на следующее:

* присутствуют директивы в стиле «use IO::Socket;»

* точка с запятой ставится в конце каждой строки

* тело функций и многострочечных циклов/операторов if заключено в фигурные скобки

* отступ внутри тела роли не играет и часто отсутствует

* многострочечные строковые константы соединяются через точку

Выполнение всех этих условий свидетельствует о том, что перед нами Perl. Язык Python внешне похож на него, но содержит ряд принципиальных отличий (и обычно предваряется строкой «#!/usr/bin/python», которой, впрочем, может и не быть):

* присутствуют директивы в стиле «import socket», «import sys»

* точка с запятой в конце строки не ставится

* тело функций и многострочечных циклов, операторов if не берется в скобки

* отступ внутри тела функций, оператор if и циклов строго обязателен

* многострочечные строковые константы соединяются, как в Си («<ENTER>»)

Выполнение всех этих условиях — верный признак Питона, который, как и Perl, портирован на множество платформ и распространяется на бесплатной основе.

Проблемы вызывает комплект поставки. Достаточно часто хакеры выкладывают не весь exploit, а только его часть, и транслятор начинает материться на отсутствующие включаемые файлы/библиотеки. Такие exploit'ы следует сразу отправлять в топку, хотя при наличии большого количества свободного времени и некоторого опыта работы с языком недостающие файлы можно (теоретически) воссоздать и самостоятельно. Но зачем?!

Исключение составляют листинги, содержащие в себе строку «This file is part of the Metasploit Framework» и являющиеся модулями Framework'а, без которого они, естественно, не запускаются. Присутствие такой строки необязательно, но сама структура модуля настолько характерна, что, увидев такую штуку один-единственный раз, будешь распознавать ее всегда. Например: milw0rm.com/exploits/1788.

С диалектами Си/Си++ все намного сложнее. Компиляторов просто море, а разногласиями в понимании Стандарта можно было бы утопить целый океан, поэтому очень часто случается так, что программу, написанную под один компилятор, не удается (без переделок) откомпилировать ничем другим. Последняя версия компилятора далеко не всегда оказывается самой лучшей. В особенности это касается gcc, в ядро которого вносится большое количество изменений, зачастую не без ущерба для скорости и обратной совместимости.

Первым делом необходимо определить: приплюснутый это Си или классический? Вот характерные черты приплюснутого:

* объявление переменных по месту использования, а не в начале функции

* наличие таких ключевых слов, как «класс» и двух двоеточий «::»

* использование new для выделения памяти или явное преобразование типа перед malloc()

* отсутствует printf, а весь ввод/вывод осуществляется операторами «<<» и «>>»

Если хоть одно из этих условий выполняется, то программа явно написана на приплюснутом Си, в противном случае используется классический. Кстати говоря, Си/Си++ отличается от perl/python своими директивами «#include» и еще тем, что символ «#» в нем никогда не используется для оформления комментариев.

В отличие от интерпретируемых языков, библиотеки которых более или менее стандартизированы, Си-компиляторы включают в себя большое количество системно-зависимых библиотек, в результате чего программа может вызывать функции, отсутствующие в нашем трансляторе или использовать специфические особенности конкретной версии языка. В первую очередь, это касается сырых сокетов (по-разному реализованных в Linux и *BSD) и прочих системно-зависимых фич. Некоторые exploit'ы пишутся в расчете на Windows и вместо «общепринятых» функций типа fopen()/fclose() используют громоздкие API-вызовы CreateFile/CloseHandle. Откомпилировать такой exploit можно и под *nix'ом, но для этого придется заменять API-вызовы на соответствующие им Си-функции или syscall'ы. Самое неприятное состоит в том, что у Microsoft имеется свой собственный, особый взгляд на интерфейс сокетов, и для переноса Windows-кода, работающего с сокетами, под *nix приходится искать альтернативный *nix-exploit. Формальным признаком форточной природы кода является наличие функции WSAStartup, которая в *nix-подобных системах и не ночевала. Но классический Си — это только цветочки. Самое страшное, как всегда, впереди.

Приплюснутый Си — это настоящий кошмар. Компиляторы (и поставляемые вместе с ними библиотеки) различаются просто колоссально! Приходится иметь в своем распоряжении целую артиллерию gcc различных версий, а в рукавах держать всякую экзотику типа Intel C++, но и тогда будут встречаться программы, которые упорно не хотят компилироваться!

Яркий тому пример — milw0rm.com/shellcode/656 (прилагается к статье под именем beta.cpp). Пропускаем его через gcc и получаем следующий список ошибок (не считая варнингов):

Список ошибок, выдаваемый компилятором gcc, при попытке трансляции файла beta.cpp

beta.cpp:34:21: windows.h: No such file or directory

beta.cpp: In function `int main(int, char**, char**)':

beta.cpp:165: error: `stricmp' undeclared (first use this function)

beta.cpp:185: error: `strnicmp' undeclared (first use this function)

beta.cpp:245: error: `isalnum' undeclared (first use this function)

beta.cpp:250: error: `isprint' undeclared (first use this function)

beta.cpp:339: error: invalid conversion from `void*' to `char*'

beta.cpp:356: error: `O_BINARY' undeclared (first use this function)

beta.cpp:361: error: `lseek' undeclared (first use this function)

beta.cpp:377: error: invalid conversion from `void*' to `char*'

beta.cpp:384: error: `read' undeclared (first use this function)

beta.cpp:398: error: `close' undeclared (first use this function)

С ошибкой 34 все понятно — программа усиленно косит под форточки, прихватив с собой файл <windows.h>, но тут же использует стандартные POSIX-вызовы: open() со странным флагом O_BINARY, lseek(), read() и close(), которых ни в самом Windows, ни в одном из win32-компиляторов никогда не существовало (см. ошибки 356, 361, 384 и 398). Убираем строку «#include <windows.h>», меняя ее на «#include <unistd.h>», и удаляем глупый флаг O_BINARY, поскольку по умолчанию файл уже является двоичным (узнать, какие заголовочные файлы соответствуют данной функции, можно из man'а, например, «man 2 open»). Чтобы избавиться от предупреждения «this file include <malloc.h> which is deprecated, use <stdlib.h> instead», удаляем и «#include <malloc.h>».

Ошибки 245 и 250 устраняются подключением их «родного» заголовочного файла, в котором они были объявлены «#include <ctype.h>» (см. «man isalnum»). А вот функций stricmp() и strnicmp() в gcc действительно нет, однако они могут быть заменены на аналогичные им strcmp() и strncmp() даже без коррекции аргументов!

Ошибки 339 и 377 исправляются еще проще: достаточно взять строку «buffer = malloc(MAX_BUFFER_SIZE)» и добавить явное преобразование типов, также называемое кастингом «buffer = (char *)malloc(MAX_BUFFER_SIZE)».

Исправленный вариант лежит в файле beta-fixed.cpp и компилируется без всяких нареканий.

Будем считать, что с идентификацией транслятора мы разобрались, и exploit откомпилировался нормально, но... это еще не конец, а только начало. Ведь программный листинг - это только оболочка, образно говоря, «тетива», а разящие острие — загадочный и таинственный shell-код, помещенный в строковый «иероглифический» массив вроде «\x29\xc9\x83...\xe9\xb0\xd9». Что делать, если он не работает или работает не так, как нам этого хочется?

Доработка напильником

Shell-код имеет сложную структуру и обычно состоит из нескольких частей. Например, exploit milw0rm.com/exploits/1075, приложенный в файле 1075.с, использует целых 6 «иероглифических» массивов: dce_rpc_header1, tag_private, dce_rpc_header2, dce_rpc_header3, offsets, bind_shellcode. Первые пять — это служебные структуры, атакующие жертву, срывающие буферу крышу и передающие управление на bind_shellcode. Последний представляет собой «чистый» shell-код, который может быть беспрепятственно заменен любым другим. На самом деле, тут все не так просто, и произвола хоть отбавляй. Как минимум, необходимо убедиться, что мы используем shell-код, совместимый с атакуемой системой, и точки входа у них совпадают. Часто (но не всегда) точка входа расположена в самом начале shell-кода, реже — в его конце или середине. Гораздо хуже, если exploit написан «пионером», и все блоки идут одним большим кусом, внутри которого присутствует и shell-код.

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

Простейший конвертор для преобразования строковых констант в двоичный код

#include <stdio.h>

int main(void)

{

FILE *f;

if (f = fopen("shellcode", "wb"))

fwrite(shellcode, sizeof(shellcode), 1, f);

exit(0);

}

Сам shell-код должен быть размещен в массиве, объявленном как «char shellcode[]» (см. прилагаемый файл hex2bin.c) и приведенным к синтаксису Си (то есть, если shell-код выдернут из perl'а, необходимо удалить точки в конце строковых констант). Компилируем наш импровизированный конвертор, запускаем его на выполнение — и тут же на диске образуется файл «shellcode», который можно загрузить в HTE, IDA Pro или любой другой дизассемблер по вкусу, не забывая, конечно, переключить его в 32-битный режим.

В данном случае мы получим следующий код:

Первые 16-байт shell-кода содержат осмысленный код расшифровщика

00000000: 29C9 sub ecx,ecx

00000002: 83E9B0 sub ecx,-050 ;"P"

00000005: D9EE fldz

00000007: D97424F4 fstenv [esp][-000C]

0000000B: 5B pop ebx

0000000C: 81731319F50437 xor d,[ebx][00013],03704F519 ; "7?o?"

00000013: 83EBFC sub ebx,-004 ; "?"

00000016: E2F4 loop 00000000C (1)

Ага! Вполне типичный расшифровщик. Значит, точка входа в shell-код действительно находится в начале массива, и он может быть беспрепятственно заменен любым таким же. Если же вместо осмысленного кода нас встречает мусор, то нужно последовательно отступать на один байт до тех пор, пока мы не получим что-то удобоваримое. Естественно, для этого необходимо знать ассемблер и хотя бы в общих чертах представлять себе устройство операционной системы.

Правильно спроектированный shell-код работает на всех версиях операционных систем, для которых он предназначен, однако в последнее время все чаще и чаще приходится сталкиваться с «пионерством», которое привязано к фиксированным адресам и функционирует только под определенной сборкой Linux-ядра или заранее заданным сервис-паком, наложенным на Windows.

*nix-подобные системы в этом плане менее изменчивы, и проблема «фиксированных адресов» здесь практически сведена на нет. Обычно shell-код вызывает необходимые ему функции через системные вызовы, интерфейс с которыми обеспечивается прерыванием INT 80h или дальним вызовом по адресу 0007h:00000000h, что позволяет shell-коду функционировать под всей линейкой осей, для которых он предназначен. Тем не менее, определенные системные вызовы в различных версиях ядер реализованы неодинаково, что порождает проблемы совместимости. К счастью, базовый набор системных вызовов остается единым для всех осей, и грамотно спроектированный exploit поражает как Linux, так и BSD.

Заключение

Последние версии *nix'ов оснащены довольно мощными защитными механизмами: неисполняемым стеком, рандомизатором адресного пространства и т.д. Обычным exploit'ом такую штуку уже не пробить, а потому техника написания shell-кодов в ближайшем будущем обещает круто измениться, но прежде чем бросаться на неисполняемый стек, необходимо разобраться в существующих exploit'ах, что мы сейчас и попытались сделать.