Exploits review (выпуск 7)

Автор: (c)Крис Касперски ака мыщъх

WikyBlog: HTML-инжектинг

brief

В популярном (и притом совершенно бесплатном!) программном обеспечении для создания blog'ов - WikyBlog (www.wikyblog.com), отпочковавшимся от не менее популярной свободной энциклопедии Wikipedia (www.wikipedia.org), обнаружены множественные дыры, связанные с некорректной фильтрацией пользовательского ввода данных в полях "login" и "search", размещенных на странице Панели Управления (www.wikyblog.com/Special/Main/ControlPanel) и позволяющие атакующему выполнять произвольный HTML/JavaScript/VBScript-код в контексте уязвимого сайта, воруя cookies, содержащие данные авторизации. Уязвимость была обнаружена хакерским коллективом HackersCenter IT Security Research Team [HSC], описавших ее на своем сайте в короткой заметке, датируемой 1 декабря 2006 года http://www.hackerscenter.com/archive/view.asp?id=26544 и уже на следующий день перекочевавшую на Security Focus: http://www.securityfocus.com/bid/21406.

targets

Уязвимости подвержена самая последняя на данный момент версия WikyBlog - 1.3.2, выпущенной 14 ноября 2006 года, о более древних версиях (доступных для скачивания на http://sourceforge.net/projects/wikyblog) пока ничего не известно;

exploit

Исходный текст proof-of-concept exploit'а (представляющий собой простейший XSS-скрипт) можно найти на сайте HTS-группы по ссылке, приведенной выше, там же находится и скриншот атакованного сайта (см. рис. 1);

solution

Ведущий разработчик WikiBlog'а (известный под ником Cobalt -justinms66@users.sourceforge.net) пока никак не отреагировал на сообщение об уязвимости, так что пользователям WikiBlog'ов ничего другого не остается, как сидеть на измене и ждать новостей (ну, или латать дыры самостоятельно, благо исходные тексты открыты);

www.wikyblog.com

Рисунок 1. Сайт www.wikyblog.com после атаки.

GNU GV: удаленное переполнение буфера

brief

6 октября Renaud Lifchitz (r.lifchitz@sysdream.com) - ведущий сотрудник компании Sysdream (www.sysdream.com) обнаружил ошибку переполнения в GNU gv, приводящую к возможности удаленного выполнения shell-кода в контексте уязвимого приложения. GNU gv - это де-факто стандартный вьювер ps и pdf файлов под X'ми, входящий практически во все LINUX-дистрибутивы и используемый некоторыми браузерами (в частности, Epiphany) по умолчанию, а также входящий в состав других продуктов, одним из которых является вьюер Evince. Дыра кроется в функции ps_gettext, находящейся в "ps.c" файле и представляет собой классический пример "срыва стека", возникающего при передаче слишком длинных комментариях в некоторых полях заголовка (например, поле "%%DocumentMedia"), копируемых в text-буфер фиксированного размера 257 байт со всеми вытекающими отсюда последствиями. Ошибка была подтверждена разработчиками тремя днями спустя, тогда же она появилась и на http://www.securityfocus.com/bid/20978;

targets

Уязвимости подвержена версия 3.6.2, остальные пока не проверялись, но, судя по всему, эта дыра присутствует и в них;

exploits

Для реализации атаки имеется большое количество вполне боевых exploit'ов, вот только некоторых из них: Linux IA32 Reverse TCP Shell on 192.168.110.247:4321 -http://www.securityfocus.com/data/vulnerabilities/exploits/hello-reverseshell.ps (ps-файл) и его исходный код на Си - www.securityfocus.com/data/vulnerabilities/exploits/evince-ps-field-bof.c; генератор ps-файлов с shell-кодом на борту в исходных текстах на Си: http://www.securityfocus.com/archive/1/452868;

solution

Обновленная версия GNU gv может быть скачана как непосредственно с его "родного" сайта http://www.gnu.org/software/gv, так и с сайтов производителей LINUX-дистрибутивов, большинство из которых уже выпустило свои заплатки;

Evince

Рисунок 2. Evince - один из многих просмотрщиков документов, использующих уязвимую версию GNU gv.

Linux Kernel: удаленное переполнение буфера

brief

В ходе очередной проверки исходных текстов ядра Linux'а, Евгений Тео (Eugene Teo), входящий в коллектив разработчиков, обнаружил довольно экзотичную ошибку целочисленного переполнения в функции Get_FDB_Entries, о чем и поведал народу на своем blog'е в заметке "MOKB-29-11-2006: Linux 2.6.7 - 2.6.18.3 get_fdb_entries() Integer Overflow", датируемой 29 ноября 2006 года: http://projects.info-pull.com/mokb/MOKB-29-11-2006.html; дыра кроется в функции get_fdb_entries (находящийся в файле net/bridge/br_ioctl.c), которая при передаче определенных аргументов (и наличии двух или более сетевых адаптеров на машине) может затирать память ядра функцией memcpy, что (при успешной атаке) позволяет выполнять shell-код на уровне нулевого кольца, то есть с наивысшими привилегиями! И, хотя возможность удаленных атак поставлена под сомнение, потенциальная угроза все-таки есть;

targets

Уязвимости подвержено множество версий семейства 2.6.x.x (и по некоторым данным некоторые версии семейства 2.4.x.x), неполный перечень которых содержится на http://www.securityfocus.com/bid/21353/info, причем в версии 2.6.18.4 уязвимость отсутствует;

exploit

На данный момент уязвимость не подкреплена никаким exploit'ом и вообще о ней очень мало что известно, что открывает большой оперативный простор для всевозможных экспериментов и исследований;

solution

Одновременно с публикацией сообщения о дыре был выпущен "лечебный" патч - "bridge: fix possible overflow in get_fdb_entries", выложенный на официальном сайте: http://www.kernel.org/git/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commit;h=ba8379b220509e9448c00a77cf6c15ac2a559cc7, а коллектив разработчиков ядра оперативно выпустил свежие версии 2.6.17.10 и 2.4.33.2 специально для устранения этой проблемы;

Фрагмент уязвимой функции

Рисунок 3. Фрагмент уязвимой функции get_fdb_entries.

Full disclose - MS Windows: отказ в обслуживании из-за переполнения в спулере печати

brief

2 декабря 2006 года польским хакером по кличке h07 (h07@interia.pl) был опубликован exploit (написанный на языке Питон), подключающийся к службе печати через NetBIOS и вызывающий необрабатываемое исключение в спулере печати, приводящее к отказу в обслуживании: http://downloads.securityfocus.com/vulnerabilities/exploits/21404.py. Ошибка кроется в функции GetPrinterData, экспортируемой динамической библиотекой WINSPOOL.DRV (да не введет нас ее расширение в заблуждение - никакой это не драйвер, а самая обыкновенная DLL, исполняющаяся на прикладном уровне), принимающей в одном из аргументов количество байт, которое необходимо выделить для записи конфигурации принтера, но не проверяющей его значение на "политкорретность", в результате чего при запросе >= 512Мбайт функция VirtualAlloc обламывается с выделением, возвращая вместо памяти нулевой указатель, сигнализирующий об ошибке, который также никто не проверяет и при попытке обращения к нему процессор генерирует исключение, приводящее к аварийному завершению процесса spoolsv.exe (Служба Печати), что, конечно, не смертельно, но все-таки очень неприятно. Тем не менее, возможность захвата управления отсутствует, что внушает некоторый оптимизм;

Боевой exploit

Рисунок 4. Боевой exploit польского хакера h07, срывающий крышу службе печати.

target

Уязвимости подвержена вся линейка Windows 2000 как со всеми установленными заплатками (вплоть до SP4), так и без них. О других системах ничего не известно, но, вероятнее всего, дыра присутствует и в них;

exploit

http://downloads.securityfocus.com/vulnerabilities/exploits/21404.py;

solution

Microsoft пока не представила никаких заплаток, что не есть хорошо. Можно даже сказать, что это совсем хреново. Отключать службу печати - не предлагать, поскольку количество принтеров в большинстве контор не совпадает с количеством машин и без разделения ресурсов никак не обойтись. Можно, правда, отсечь удаленных пользователей брандмауэром, но гораздо интереснее изготовить заплатку самому!

disclose

Начнем исследование с того, что заглянем в MSDN (например, тот, что идет в одном комплекте с Microsoft Visual Studio 6.0) и посмотрим на прототип функции GetPrinterData, который выглядит так:

DWORD GetPrinterData(
        HANDLE hPrinter,    // handle to a printer or print server
        LPTSTR pValueName,  // string that identifies the data to retrieve
        LPDWORD pType,      // variable that receives the type of data
        LPBYTE pData,       // buffer that receives the configuration data
        DWORD nSize,        // size, in bytes, of buffer
        LPDWORD pcbNeeded   // receives the required buffer size, in bytes
);

Листинг 1. Прототип функции GetPrinterData.

Параметр nSize задает размер буфера в байтах, выделяемого функцией. Вообще, это, конечно, глупость - поручать выделение памяти функции. Передача указателя на блок памяти, выделенный программистом, выглядела бы более логично и... безопасно. Но горячие парни из Microsoft программируют быстрее, чем думают, а думают они не головой, а совсем другой частью тела. Индусы, короче. Ну, что с них возьмешь?! Ладно, пускай и дальше не думают. Нам, хакерам, это только на пользу идет. Достаточно передавать в качестве nSize такой размер памяти, который заведомо не может быть выделен и результат себя ждать не заставит. Вне зависимости от количества физической памяти, адресное пространство процессов на 32-разрядных платформах составляет 4 Гбайта, из которых обычно половина выделяется системе, а половина - на стек, секции кода/данных PE-файла и всех загруженных динамических библиотек. Остаток занимает куча. На серверах имеется возможность ужать систему до одного гигабайта, отдав его куче, поэтому больше 3 Гигабайт запрашивать не имеет смысла. Все равно не дадут и крах наступает уже на отметке в 512 Мбайт.

Результаты поиска

Рисунок 5. Результаты поиска функции OpenPrinterEx в Platform SDK.

Но чтобы реализовать атаку, необходимо в первом параметре hPrinter передать дескриптор принтера, открываемый (по документации!) функцией OpenPrinter, экспортируемый все той же самой динамической библиотекой WINSPOOL.DRV. Вот только exploit использует не OpenPrinter, а OpenPrinterEx, которой ни в старом MSDN (тот, что идет с Visual Studio 6.0), ни в свежем Platform SDK что-то не наблюдается. Недокументированная функция? Но в таблице экспорта WINSPOOL.DRV она отсутствует (в чем легко убедится с помощью утилиты DUMPBIN.EXE "DUMPBIN /EXPORTS WINSPOOL.DRV > out") и возникает резонный вопрос - как же, черт возьми, все это работает?! А в том, что exploit работает - можно не сомневаться.

class OpenPrinterEx(Structure):
        alignment = 4
        opnum = 69
        structure = (
                ('printer', ':', B1),
                ('null', '<L=0'),
                ('str', '<L=0'),
                ('null2', '<L=0'),
                ('access', '<L=0'),
                ('level', '<L=1'),
                ('id1', '<L=1'),
                ('level2', '<L=10941724'),
                ('size', '<L=28'),
                ('id2', '<L=0x42424242'),
                ('id3', '<L=0x43434343'),
                ('build', '<L=2600'),
                ('major', '<L=3'),
                ('minor', '<L=0'),
                ('processor', '<L=0xFFFFFFFF'),
                ('client', ':', B2),
                ('user', ':', B2),
)
...
class GetPrinterData(Structure):
        alignment = 4
        opnum = 26
        structure = (
        ('handle', '%s'),
        ('value', ':', B2),
        ('offered', '<L'),
)

query = OpenPrinterEx()
printer = "\\\\%s\x00" % (host)
query['printer'] = B1()
query['printer']['id'] = 0x41414141
query['printer']['max'] = len(printer)
query['printer']['actual'] = len(printer)
query['printer']['str'] = printer.encode('utf_16_le')
...
query = GetPrinterData()
value = "blah_blah\x00"
query['handle'] = handle
query['value'] = B2()
query['value']['max'] = len(value)
query['value']['actual'] = len(value)
query['value']['str'] = value.encode('utf_16_le')
query['offered'] = memory_size

Листинг 2. Фрагмент exploit'а, демонстрирующий технику вызова GetPriterData, принимающую дескриптор, возвращенный OpenPrinterEx.

Поиск по сайту Microsoft дает всего лишь одну ссылку на OpenPrinterEx, вскользь упоминаемую при описании структуры PRINTPROVIDOR в DDK и реализуемую драйвером принтера: http://msdn2.microsoft.com/en-us/library/aa506552.aspx. Чуть более подробное описание содержится в технической документации на Самбу (см. раздел "Samba Printing Internals" - http://samba.org/samba/docs/man/Samba-Developers-Guide/devprinting.html), после прочтения которого становится ясно, что exploit вызывает OpenPrinterEx через механизм удаленного вызова процедур - Remote Procedure Call (RPC), без обращения к WINSPOOL.DRV. Собственно говоря, и в самом WINSPOOL.DRV функция OpenPrinter реализована через RPC (см. листинг 3).

.text:777D47B9 sub_777D47B9     proc near    ; CODE XREF: sub_777D4634+68^p
.text:777D47B9
.text:777D47B9 arg_0 = dword ptr  4
.text:777D47B9
.text:777D47B9                  lea   eax, [esp+arg_0]
.text:777D47BD                  push  eax
.text:777D47BE                  push  offset dword_777D1AD8
.text:777D47C3                  push  offset off_777D1A28
.text:777D47C8                  call  NdrClientCall2
.text:777D47CD                  add   esp, 0Ch
.text:777D47D0                  retn  14h
.text:777D47D0 sub_777D47B9     endp

Листинг 3. Фрагмент WINSPOOL.DRV, реализующий функцию OpenPrinter через механизм RPC.

При создании exploit'а на эти тонкости можно не обращать внимания, достаточно лишь взять любой сырец, печатающий на принтере через NetBIOS (в смысле - удаленно) и сразу же после OpenPrinter/OpenPrinterEx воткнуть вызов GetPriterData с некорректным значением nSize. Какая, в конце концов, разница - какие механизмы задействует Windows и какие функции при этом реально вызываются? Главное, что незалатанный спулер печати падает! А это - хорошо! Ну, кому-то, может быть и хорошо, а тому, кто падает - как-то не очень. Особенно, если падать приходится много раз на дню при печати многостраничного документа. Но как только мы захотим заштопать систему своими лапами, тут уже абстрагироваться от анатомических подробностей внутренней реализации Windows никак не получится.

Дизассемблирование динамической библиотеки

Рисунок 6. Дизассемблирование динамической библиотеки WINSPOOL.DRV, являющийся всего лишь "оберткой" вокруг реальных принтерных функций, вызываемых через механизм RPC.

На первый взгляд, проблема решается легкой правкой WINSPOOL.DRV - ставим в начало функции GetPrinterData переходник на свободное место, достаточно просторное для размещения нескольких машинных команд, проверяющих корректность аргумента nSize. Естественно, придется скорректировать контрольную сумму файла WINSPOOL.DRV (что можно сделать при помощи утилиты EDITBIN.EXE, входящей в состав SDK) и усмирить SFC, путем копирования исправленной версии WINSPOOL.DRV в WINNT\System32\dllcache (естественно, делать это надо при выключенном SFC или загрузившись с другой системы). Вот только... эффект от проделанной операции будет, мягко говоря, нулевой. А все потому, что WINSPOOL.DRV используется только локально, а при печати через NetBIOS все вызовы идут через RPC и перехватывать следует NdrClientCall2 из RPCRT4.DLL, описание которой отсутствует в SDK, но, к счастью, IDA Pro знает ее прототип: CLIENT_CALL_RETURN _imp_NdrClientCall2(PMIDL_STUB_DESC pStubDescriptor, PFORMAT_STRING pFormat,...), где pFormat - указатель на строку параметров, описывающих вызываемый метод и его параметры. В частности, метод GetPrinterData проходит под кодовым обозначаем 1Ah (см. листинг 2) и передается в 6-м, считая от нуля, байте форматной строки (которая на самом деле никакая не строка, поскольку содержит внутри себя нули и прочие непечатные символы). Параметр nSize передается через стек следующим образом (см. листинг 4):

.text:777D53D3          push   [ebp+pcbNeeded]
.text:777D53D6          push   [ebp+nSize]
.text:777D53D9          push   [ebp+pData]
.text:777D53DC          push   [ebp+pType]
.text:777D53DF          push   [ebp+pValueName]
.text:777D53E2          mov    eax, [ebp+var_44]
.text:777D53E5          push   dword ptr [eax+4]
.text:777D53E8          call   sub_777D542F
...
.text:777D542F sub_777D542F    proc near               ; CODE XREF: GetPrinterDataW+A4^p
.text:777D542F
.text:777D542F arg_0    = dword ptr  4
.text:777D542F
.text:777D542F          lea    eax, [esp+arg_0]
.text:777D5433          push   eax
.text:777D5434          push   offset pFormat
.text:777D5439          push   offset pStubDescriptor
.text:777D543E          call   NdrClientCall2
.text:777D5443          add    esp, 0Ch
.text:777D5446          retn   18h
.text:777D5446 sub_777D542F    endp
...
.text:777D20C0 pFormat  db    0                        ; DATA XREF: sub_777D542F+5vo
.text:777D20C1          db  48h                        ; H
.text:777D20C2          db    0
.text:777D20C3          db    0
.text:777D20C4          db    0
.text:777D20C5          db    0
.text:777D20C6          db  1Ah                        ; метод GetPrinterData
.text:777D20C7          db    0

Листинг 4. Вызов GetPrinterData через механизм RPC.

Таким образом, в момент вызова функции NdrClientCall2, указатель на параметры лежит в стеке по смещению [ESP-0Ch], а по смещению +14h от его начала находится nSize, который мы и должны проверять на корректность. Но прежде необходимо проанализировать указатель на форматную строку, находящуюся в стеке по смещению [ESP-08h], убедившись, что 6-й байт равен 1Ah, т.е. вызывается метод GetPrinterData, а не что-то иное. Ассемблерный код труда написать не составит и каждый сможет это сделать сам. Главное - не забывать о проверках на нулевые указатели, чтобы исправляя одну ошибку, не создавать на ее месте десяток новых. Также проверочный код нельзя размещать в секции данных - хоть там полно свободного места, но на машинах с аппаратной поддержкой DEP это работать не будет и тут же возникнет исключение. Поскольку механизм RPC - это, фактически, фундамент, на котором базируется Windows NT, править RPCRT4.DLL стоит с огромной осторожностью, поскольку если он окажется поврежден, загрузить систему не удастся. С другой стороны, при правке файла на диске, мы всегда сможем сделать откат, воткнув винчестер с поврежденной NT в компьютер вторым, или воспользовавшись консолью восстановления (находится на дистрибутивном CD) и скопировав оригинальный RPCRT4.DLL поверх исправленного.

В качестве альтернативного варианта, можно воспользоваться правкой RPCRT4.DLL в памяти по методике, описанной в статье "Метафизика wmf файлов": прописываем в следующей ветке реестра HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs специально созданную для этих целей динамическую библиотеку, которая будет отображаться на адресное пространство каждого процессора. Внутри DllEntry мы выполняем загрузку RPCRT4.DLL через LoadLibrary, правим ее и все! Если приложение не использует RPCRT4.DLL (что навряд ли), мы просто теряем немного памяти. Если же приложение подгружает RPCRT4.DLL через таблицу импорта или через LoadLibrary (что намного более вероятно), оно просто отсылается к уже загруженной (и исправленной!) копии RPCRT4.DLL и хакер не имеет никаких шансов атаковать систему! Естественно, по сравнению с правкой на диске, время загрузки файлов и потребность системы в памяти ощутимо возрастут, а риск угробить систему - все тот же. Если динамическая библиотека, осуществляющая правку, будет реализована с ошибками, система упадет прежде, чем загрузится пользовательский интерфейс и для исправления ситуации придется к помощи все той же консоли восстановления (втыкать винчестер с убитой NT "вторым"), удаляя нашу динамическую библиотеку нахрен. Впрочем, это уже детали. Главное, что изготовление заплаток своим лапами вполне возможно и пока другие ждут помощи от Microsoft, правильные хакеры защищаются самостоятельно, во всем полагаясь на свой хвост, который, если верить энциклопедиям, может достигать 45 см длины - http://ru.wikipedia.org/wiki/%D0%9C%D1%8B%D1%88%D1%8C. Ну, насчет 45 см - это они, конечно, загнули. В природе таких мышей не встречается, иначе меня начнет мучить жуткий комплекс неполноценности.

Мышь познается по длине хвоста

Рисунок 7. Мышь познается по длине хвоста.