Введение
В этой статье мы рассмотрим различные методы и техники устранения уязвимостей в программном обеспечении
с закрытым исходным кодом.
Для начала определимся со следующими понятиями:
- Patch - в нашем случае это заплатка для уязвимости, выпущенная официальным разработчиком ПО.
- HotPatch, HotFix - неофициальный патч, сделанный сторонними разработчиками до выхода официального патча.
- 0-day - термин употребляется как к уязвимостям, так и к эксплойтам. Как правило, это
свежевыпущенные эксплойты, патчей для которых еще не вышло.
Появление 0-day эксплойтов и уязвимостей приводит к огромному риску атаки, в то время как ожидание
выпуска официальных патчей от разработчиков может занять не один день. Ярким примером могут выступать
критические уязвимости, позволяющие удаленное выполнение кода в программном обеспечении Microsoft,
на выпуск официального патча для которых уходило по 1-2 недели во время вирусных эпидемий,
распространяющихся как раз через эти уязвимости.
План работы:
1. Разбор реальной HotPatch-системы для уязвимости в Microsoft Windows.
2. Объяснение работы важных техник системного программирования для разработки HotPatch-системы.
3. Различные проблемы в проектировании HotPatch-системы.
4. Ссылки на дополнительный материал.
Как работают Hotpatches?
Для примера возьмем хотпатч Ильфака Гуифанова (автор дизассемблера IDA Pro) для уязвимости
в обработке WMF-файлов, позволяющее удаленное выполнение кода (ms06-001):
http://www.microsoft.com/technet/security/advisory/912840.mspx
Эксплойт был обнародован 27.12.2005, а официальный патч Microsoft выпустила только 05.01.2006, в то
время как хотпатч был выпущен 31.12.2005 Ильфаком Гуифановым и опубликован в своем блоге по адресу:
http://www.hexblog.com/2005/12/wmf_vuln.html
За этот период в сеть прорвалось более 100 модификаций червей, эксплуатирующих эту уязвимость.
Суть уязвимости состояла в создании вредоносного WMF-файла, в заголовке которой содержался бы вызов
функции META_ESCAPE с подфункцией SETABORTPROC, которая принимает два параметра: дескриптор контекста
устройства и, собственно, сам шеллкод.
Теперь, когда мы имеем небольшое представление об уязвимости можно перейти к разбору самого хотпатча.
1) Установка.
Копируется в C:\%windir%\%system32%\wmfhotfix.dll.
Затем прописывает DLL в параметре AppInit_DLLs по адресу:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows
Данный метод используется для глобально внедрения DLL во все процессы. Об этом мы поговорим чуть позже.
2) Хотпатчинг.
После внедрения в процесс DLL загружает уязвимую библиотеку gdi32.dll и получает адрес уязвимой функции
Escape. Далее перезаписывает первые 5 байт функции на 5-байтовый переход на адреса обработчика. Такую
технику перехвата принято называть сплайсинг ("Splicing", "In-Line JMP Hooking"):
Листинг 1: Начало уязвимой функции GDI32.Escape до перехвата.
Адрес Байты Код Комментарий
77F27FBB: 8BFF MOV EDI,EDI ;
77F27FBD: 55 PUSH EBP ; пролог к функции
77F27FBE: 8BEC MOV EBP,ESP ;
77F27FC0: 83EC 40 SUB ESP,40
...
Листинг 2: Начало уязвимой функции GDI32.Escape после перехвата.
Адрес Байты Код Комментарий
77F27FBB: E9 40900D98 JMP wmfhotfi.10001000 ; код обработчика, см. листинг 3
77F27FC0: 83EC 40 SUB ESP,40
...
Обработчик, который проверяет параметры, передающиеся функции Escape(), и если 2ой аргумент равен SETABORTPROC
(в wingdi.h он обозначен как 9), то возвращает 0. В ином случае переходит на вторую инструкцию Escape, минуя
установленный перехват (в листинге 2 это адрес 0x77F27FC0).
Листинг 3: Код обработчика в wmfhotfix.dll.
Адрес Байты Код Комментарий
10001000: 55 PUSH EBP
10001001: 8BEC MOV EBP,ESP
10001003: 837D 0C 09 CMP DWORD PTR SS:[EBP+C],9 ; проверка на SETABORTPROC == 9
10001007: 74 0C JE SHORT wmfhotfi.10001015
10001009: 2B25 00300010 SUB ESP,DWORD PTR DS:[10003000]
1000100F: FF25 04300010 JMP DWORD PTR DS:[10003004] ; GDI32.77F27FC0
10001015: 33C0 XOR EAX,EAX
10001017: 5D POP EBP
10001018: C2 1400 RETN 14
3) Проверка на наличие уже установленного хотпатча
Проверка на наличие уже установленного хотпатча осуществляется проверкой первого байта уязвимой функции
на наличие jmp (0xE9) и части кода функции-обработчика (см. листинг 3):
Байты Код
33c0 xor eax, eax
5d pop ebp
c2 1400 retn 14
Важные техники системного программирования,
используемые при разработке Hotpatches
В этом разделе мы очень подробно рассмотрим теоретические основы всех необходимых техник, которые могут
понадобиться при разработке Hotpatch-систем.
1. Внедрение в адресное пространство чужого процесса.
Прежде чем модифицировать код уязвимого приложения и устанавливать перехваты нам необходимо попасть в
его адресное пространство. Сделать это можно как минимум двумя основными методами.
1.1 AppInit DLLs
Этот метод мы уже упоминали при разборке WMF Hotpatch. Суть заключается в том, чтобы прописать полный путь
до своей DLL (либо просто имя DLL, если она находится в директории %system32%) в параметре AppInit_DLLs
(тип REG_SZ), который располагается в реестре по адресу:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows
После чего DLL будет автоматически загружена операционной системой для всех приложений, проецирующих в свое
адресное пространство user32.dll.
Если в параметре уже есть какая-то dll, то дописать следующую можно через пробел или запятую.
Следует также отметить, что для внедрения DLL во все процессы необходима перезагрука, т.к в процессы, загруженные
до установки DLL в AppInit_DLLs она не будет загружаться.
1.2 DLL-Injection
Один из самых удобных и универсальных методов. Заключается в том, чтобы записать в память чужого процесса
свой код (VirtualAllocEx/WriteProcessMemory) и затем выполнить его (CreateRemoteThread).
Внедряемый шеллкод будет состоять из вызова LoadLibrary для загрузки необходимой DLL и вызова ExitThread для
завершения потока в удаленном процессе.
На этой основе построен метод "Process-Injection", только в этом случае внедряемый код будет внедряться полностью
без никаких DLL.
Алгоритм таков:
1) Открытие процесса (OpenProcess) с правами доступа PROCESS_ALL_ACCESS.
2) Получив дескриптор процесса выделяем в нем память под внедряемый код (VirtualAllocEx).
3) Записать код (WriteProcessMemory).
4) Запустить его как поток (CreateRemoteThread).
Внимание. Для модификации важных системных процессов нам необходимо получить права отладчика (SeDebugPrivilege)
для нашего процесса, сделать это можно с помощью функции AdjustTokenPrivileges.
Если в случае использования AppInit DLLs мы имели уже готовый глобальный перехват (т.е распространенный
на все процессы), то при использовании данного метода для достижения такого же результата нам необходимо
установить перехват на CreateProcessW, CreateProcessA, CreateProcessAsUserW, CreateProcessAsUserA (желательно
перехватывать и ANSI и UNICODE версии функций), внедряя в запущенные приложения свою DLL.
2. In-Line Hooking (Splicing).
Данный метод является самым актуальным. Мы рассматривали его в действии при разборе WMF Hotpatch.
Он заключается непосредственно в модификации кода самой функции. Обычно это перезапись первых 5 байт
функции на инструкцию вида jmp addr, где addr - адрес функции-обработчика (4 байта). На опкод jmp нам
понадобится 1 байт (0xE9).
Практически любой код API-функции в Windows начинается одинаково. Этот код принято называть
прологом (или преамбулой).
Начиная с XP SP2 пролог выглядит следующим образом и размер еге составляет 5 байт:
Байты Код
8bff mov edi, edi
55 push ebp
8bec mov ebp, esp
А до SP2 выглядит так, размер всего 3 байта:
Байты Код
55 push ebp
8bec mov ebp, esp
Данный пролог в функции будет автоматически добавлен компилятором, если использовать флаг /hotpatch.
Microsoft, понимая актуальность темы создания хотпатчей сделала это для установки 5-байтового jmp
вначале функции.
Рассмотрим конкретный пример простейшей программы.
Листинг 4: Наша Программа
#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
return 1;
}
А теперь сравним код с использованием флага /hotpatch и без него, загрузив скомпилированный exe в дизассемблер:
Листинг 5. Компиляция без использования флага /hotpatch.
Адрес Байты Код
13001000: 55 push ebp
13001001: 8BEC mov ebp, esp
13001011: B801000000 mov eax, 00000001
13001016: 5D pop ebp
13001017: C21000 ret 0010
Листинг 6. Компиляция с флагом /hotpatch.
Адрес Байты Код Комментарий
13001010: 8BFF mov edi, edi ; добавленные два байта компилятором
13001012: 55 push ebp
13001013: 8BEC mov ebp, esp
13001023: B801000000 mov eax, 00000001
13001028: 5D pop ebp
13001029: C21000 ret 0010
Но вернемся к сплайсингу.
Можно подумать, что установка 5 байтового jmp актуальна только в SP2, но это не так. Просто начиная с SP2 Microsoft упростило нашу задачу, но не искоренило вовсе для более ранних версий.
Универсальное решение для установки такого перехвата заключается в том, чтобы перед перезаписью начала функции мы сохранили затираемые N-байт в 16-байтовый буфер, который изначально будет иметь следующий вид:
Листинг 7: Подготавливаем буфер с кодом.
Байты Код
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
E9 jmp
90 nop
90 nop
90 nop
90 nop
NOP - инструкция-пустышка, она ничего не делает.
Теперь рассмотрим пример перехватываемой функции:
Листинг 8. Пример перехватываемой функции:
Адрес Байты Код
7C901231: 55 PUSH EBP
7C901232: 8BEC MOV EBP,ESP
7C901234: 83EC 04 SUB ESP,4
7C901237: 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
7C90123A: 0345 12 ADD EAX,DWORD PTR SS:[EBP+12]
7C90123D: 8BE5 MOV ESP,EBP
7C90123F: 5D POP EBP
7C901240: C2 0400 RETN 4
Мы видим, что затерев первые 5 байт мы нарушим инструкцию по адресу 0x7C901234, оставив там лишь 0x04.
Для этих целей нам необходим движок дизассемблера длин. Таковым могу посоветовать движок
"Catchy32 v1.6 - Length Disassembler Engine 32bit by sars", который вернет вам длину опкода.
Поочередно вычисляем длины команд и если их сумма больше или равна 5, то записываем эти байты в начале
буфера. В нашем случае это 6 байт:
Адрес Байты Код
7C901231: 55 PUSH EBP
7C901232: 8BEC MOV EBP,ESP
7C901234: 83EC 04 SUB ESP,4
Теперь, записав все необходимые байты, нам надо запомнить адрес 0x7C901237, перезаписав его в последние
4 байта нашего буфера.
Листинг 9: Подготовленный буфер с началом функции:
Байты Код
55 PUSH EBP
8BEC MOV EBP,ESP
83EC 04 SUB ESP,4
90 nop
90 nop
90 nop
90 nop
90 nop
E9 3712907C JMP 7C901237
Когда все готово, можно установить перехват, перезаписав 5 байт переходом на наш обработчик. Шестой
байт так и оставляем (0x40, см. Листинг 8), хотя можно установить туда NOP, во избежание каких-либо конфликтов.
Листинг 9: Перехваченная функция.
Адрес Байты Код Комментарий
7C901235: E9 XXXXXXXX JMP xxxxxxxx
7C901236: 83EC 04 DB 4 ; оставшийся 6 байт от sub esp,4 (83EC04)
7C901237: 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
7C90123A: 0345 12 ADD EAX,DWORD PTR SS:[EBP+12]
7C90123D: 8BE5 MOV ESP,EBP
7C90123F: 5D POP EBP
7C901240: C2 0400 RETN 4
Теперь, чтобы вызвать оригинальную функцию, нам необходимо разместить ее в памяти. Затем, когда обработчик
по адресу xxxxxxxx сделает свои дела - он передаст управление на буфер (call buffer).
Данный метод не будет работать, если размер функции меньше 5 байт. Подсчитать размер функции можно
побайтовым переходом до инструкции RET (опкоды: 0xC2, 0xC3, 0xCA, 0xCB).
Кроме того, не стоит забывать, что jmp можно организовать также с помощью инструкций:
№ Байты Код
1) E8 XXXXXXXX call addr
2) 68 XXXXXXXX push addr
C3 ret
Это может понадобится для каких-либо других целей.
Что можно еще сделать? К примеру, немного модифицировать код функции.
Допустим, имеется код вида:
call some_function
jnz sub_func
...
Мы можем заменить инструкцию jnz на аналогичную инструкцию с таким же размером, например:
call some_function
jmp sub_func
...
Удаления кода можно добиться перезаписью ненужных нам инструкций на NOP.
Есть еще одна очень важная деталь, относящаяся к многопоточным приложениям. Если во время незаконченной
модификации кода какой-то поток обратится к этой функции, то неминуема ошибка, которая приведет к исключению.
В этом случае необходимо перед модификацией останавливать все потоки. После модификации потоки возобновить.
Алгоритм следующий:
1) перечислить все потоки текущего процесса (CreateToolhelp32Snapshot с опцией TH32CS_SNAPTHREAD)
2) открыть поток (OpenThread с опцией THREAD_SUSPEND_RESUME)
3) приостановить выполнение потока с помощью функции SuspendThread,
либо возобновить поток с помощью ResumeThread.
3. IAT-hooking
Данный метод заключается в замене адреса оригинальной функции на адрес обработчика в таблице импорта (IAT, Import Address Table) процесса. Таблица импорта заполняется при загрузке того или иного модуля.
Вызов функции через таблицу импорта обычно выглядит так:
Где addr - адрес в таблице импорта, в которой находится адрес необходимой API-функции.
Либо так:
Где addr1 - адрес с инструкцией вида jmp dword ptr[addr2]. А addr2, соответственно, адрес в таблице импорта,
содержащий адрес необходмой API-функции.
Например, сделаем программу с вызовом функции MessageBox и посмотрим в отладчик:
Листинг 10. Вызов API-функции через таблицу импорта.
Адрес Байты Код
00401003 6A 00 PUSH 0
00401005 6A 00 PUSH 0
00401007 6A 00 PUSH 0
00401009 6A 00 PUSH 0
0040100B FF15 94404000 CALL DWORD PTR DS:[00404094]
Переходим по данному адресу и обнаруживаем адрес функции MessageBox:
Листинг 11. Адрес API-функции в таблице импорта.
Адрес Байты Комментарий
00404094 0B05D777 ; 77D7050B
В целом алгоритм перехвата следующий:
1) получение оригинального адреса функции (GetProcAddress).
2) получение дескриптора таблицы импорта (IMAGE_IMPORT_DESCRIPTOR).
3) поиск необходимого элемента в дескрипторе, соответствующей необходимому модулю.
4) поиск адреса функции в таблице импорта перебором элементов массива, на который указывает поле FirstThunk.
5) замена оригинального адреса на адрес обработчика (WriteProcessMemory/VirtualProtect).
Еще один важный момент - это установка перехвата на GetProcAddress, чтобы приложение возвращало не настоящий адрес,
а адрес обработчика.
Этот метод достаточно прост в реализации, но надеяться на таблицу импорта нельзя, т.к приложение
может и не использовать ее для вызова API-функций. Кроме того, этот метод применим только для API.
4. Конфликты, связаные с использованием вышеописаных техник.
Практически все вышеописанные техники могут быть заблокированы проактивными защитами (к примеру:
Outpost Pro, Kaspersky Internet Security, Zone Alarm, AVZ), однако с использованием правил можно разрешить
тому или иному приложению "подозрительные" действия. Почему так происходит?
Прежде всего стоит отметить, что все эти техники особенно часто используются вредоносным
программным обеспечением для внедрения в адресное пространство каких-либо процессов для различных целей.
Например, с помощью этих техник вредоносное программное обеспечение может:
- скрывать какую-либо активность в системе (процессы, файлы и т.д), перехватывая необходимые API-функции.
- обходить персональный файрвол, внедряясь в адресное пространство доверенного процесса.
- изменять ход работы какой-либо программы.
и т.д.
Различные проблемы в проектировании HotPatch-системы
1. Поиск кода.
Самая основная проблема заключается в различных версиях программного обеспечения.
В качестве примера могут выступать модули Windows (user32.dll, gdi32.dll, kernel32.dll и т.д), код и адреса
которых меняются из версиии в версию.
С API-функциями все просто - достаточно получить адрес функции через GetProcAddress, и на этом проблема решена.
А с "псевдофункциями" возникает ряд проблем для поиска их адреса.
1) Если код уязвимой функции во всех версиях ПО одинаковый, но адреса разные, то мы можем
вычислить CRC32-хэш кода функции и посчитать ее размер. Затем поиск функции сводится к перебору кода
побайтово с учетом размера и вычислением его CRC32. Если наш CRC32-хэш будет совпадать с вычисленным,
то функция найдена.
2) Если код уязвимой функции модифицировался из версии в версию, то нам поможет дизассемблирование
ПО для поиска необходимого адреса по какой-либо "маске" кода, который будет совпадать во всех версиях,
используя движок дизассемблирования.
Начало функции может начинаться со стандартных преамбул, которые мы уже рассматривали.
Либо что-то вроде этого:
Листинг 12. Вызов функции через таблицу импорта.
Адрес Байты Код Комментарий
77F23356 90 NOP
77F23357 90 NOP
77F23358 90 NOP
77F23359 90 NOP
77F2335A 90 NOP
77F2335B 8BFF MOV EDI,EDI ; начало функции...
...
Такой NOP-ряд между функциями вы можете встретить практически в любом модуле Windows XP.
Либо какими-то другими закономерностями - все зависит от ситуации и самого ПО.
Конец функции практически всегда будет оканчиваться различными вызовами RET.
Между началом и концом функции должна быть какая-то сигнатура, т.е часть кода, совпадаемая
во всех версиях билда уязвимой функции.
3) Составление базы необходимых адресов по каждой версии уязвимого ПО.
Достаточно трудоемкая работа, но более простая в реализации.
2. Обработчик.
После обнаружения уязвимого кода нам необходимо произвести какие-либо действия, устраняющие уязвимость.
Здесь практически все зависит от сути самой уязвимости и методах ее эксплуатации.
1) Например, в случае переполнения буфера, если длина строки больше N байт, то мы можем усечь ее длину.
2) Если, например, количество элементов массива больше чем 0x3FFFFFFF, то вернуть ошибку.
3) Если уязвимый код выполняется в отдельном потоке, то можно просто завершать данный поток через ExitThread.
и т.д.
Заключение
Разработка хотпатч-систем требует хороших навыков в области отладки, дизассемблирования и системного
программирования. И несмотря на достаточно большой объем материала, мы рассмотрели тему лишь поверхностно.
Поэтому в конце статьи я привел ссылки на дополнительный материал, рекоммендуемый к прочтению.
Дополнительные ссылки
1. 90210 - Система перехвата функций API платформы Win32.
http://www.wasm.ru/article.php?article=1021007
2. Ms-Rem - Перехват API-функций в Windows NT. Часть 1: Основы Перехвата.
http://www.wasm.ru/article.php?article=apihook _1
3. Ms-Rem - Перехват API-функций в Windows NT. Часть 2: Методы внедрения кода.
http://www.wasm.ru/article.php?article=apihook_2
4. Тихомиров В.А. - Перехват API-функций в Windows NT/2000/XP.
http://www.rsdn.ru/article/baseserv/IntercetionAPI.xml
5. Майкл Говард - Защита кода с помощью средств защиты Visual C++.
http://www.microsoft.com/technet/security/bulletin/ms08-021.mspx
6. Microsoft Support - Перехват переполнения стека в приложении Visual C++.
http://support.microsoft.com/kb/315937
7. http://wasm.ru/ - Лучший отечественный ресурс о системном программировании.
8. http://phrack.org/ - Культовый интернет-журнал о различных аспектах компьютерной безопасности,
выпускаемый с 1985 года.
9. http://metasploit.com/ - Бесплатная среда разработки и тестированию эксплойтов.
Ресурс содержит множество информации о уязвимостях и различных техниках
эксплуатации.
10. http://milw0rm.com/ - Крупнейший архив эксплойтов, ежедневное появление 0-day. Присутствует
различная документация по безопасности.
11. http://int3.by.ru/ - Домашняя страница автора.
(c) cch, 2008
|