hack.connect
 
[ru.scene.community]
HackConnect.E-zine issue #3
// 00111 Устранение уязвимостей в ПО с закрытым исходным кодом. Разработка HotPatches.

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

Для начала определимся со следующими понятиями:
- 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) процесса. Таблица импорта заполняется при загрузке того или иного модуля. Вызов функции через таблицу импорта обычно выглядит так:
  call dword ptr[addr]

Где addr - адрес в таблице импорта, в которой находится адрес необходимой API-функции.
Либо так:
  call addr1

Где 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
/* ----------------------------------------------------[contents]----------------------------------------------------- */