суббота, 22 декабря 2012 г.

Определение функций класса с использованием статического и динамического анализа

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

Статическим анализом легко можно определять конструкторы классов, или факт того, является ли функция членом класса или нет.
Например, конструктор, как правило, инициализирует переменные класса, и в асм листинге все это выглядит достаточно узнаваемо:

3002E5A9 sub_3002E5A9    proc near                                  
3002E5A9                 xor     eax, eax
3002E5AB                 push    esi
3002E5AC                 mov     esi, ecx
3002E5AE                 mov     [esi], eax
3002E5B0                 mov     [esi+4], eax
3002E5B3                 mov     [esi+8], eax
3002E5B6                 mov     [esi+0Ch], eax
3002E5B9                 mov     [esi+10h], eax
3002E5BC                 mov     [esi+14h], eax
3002E5BF                 mov     [esi+18h], eax
3002E5C2                 mov     [esi+1Ch], eax
3002E5C5                 mov     [esi+20h], eax
3002E5C8                 mov     [esi+24h], eax
3002E5CB                 mov     [esi+28h], eax
3002E5CE                 mov     [esi+2Ch], eax
3002E5D1                 call    sub_300AC9DD
3002E5D6                 mov     eax, esi
3002E5D8                 pop     esi
3002E5D9                 retn
3002E5D9 sub_3002E5A9    endp

Что касается принадлежности функции к классу, тут тоже все очевидно. Как гласит стандарт, для не статических функций-членов ключевое слово this является не явно передаваемым в ф-цию адресом того объекта, для которого вызывается функция.

В асм листинке (для компиляторов от микрософт), указатель на объект класса всегда приходит в ф-цию не явно, через регистр ecx.

Пример:

3002F131                 lea     ecx, [esi+34h]
3002F134                 mov     [esi+2Ch], ebx
3002F137                 mov     [esi+30h], ebx
3002F13A                 call    sub_3002E5A9

Таким образом, статическим анализом довольно просто ответить на два вышеупомянутых вопроса. А вот ответить на вопрос, является ли ф-ция func1 членом класса A, или членом класса B - уже сложнее. Но использовав динамический анализ, это становится довольно простым делом.

Итак, используемые для статического анализа инструменты:

1) IDA PRO
2) Скрипты IdaPython ( генерация листинга ф-ций, принадлежащих тому или иному классу )

Инструменты для динамического анализа:

1) PIN ( http://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool )
2) Python скрипты для преобразования pin output logs в читабельный формат

Основная мысль:

* Имея на руках все ф-ции принадлежащие классам( все это определяется в статике ), прогоняем приложение в динамике(способов много, трассировка, эмуляция, динамическая рекомпиляция) и получаем значение ecx на входе в ф-цию.

Алгоритм(кратко):

1) В статике находим все функции, принадлежащие классам
2) Их адреса записываем в input data file for PIN
3) Пишем модуль для PIN, который на вход принимает файл с адресами ф-ций принадлежащих классам. А на выход дает файл содержащий пары: адрес ф-ции класса + значение ecx регистра.
4) Прогоняем исследуемое приложение под PIN, получаем output file с результатами
5) Парсим результаты, используя имена функций из базы IDA, в результате получаем: имя ф-ции + содержимое регистра ecx

Алгоритм(чуть более подробно):

Пожалуй единственное, что может вызвать затруднение, это нахождение всех ф-ций принадлежащих к классам.

Алгоритм примерно такой:

* перечисляем все ф-ции и исследуемом приложении
* строим для каждой ф-ции базовые блоки
* для каждого блока анализируем все инструкции
* у инструкций анализируем операнды
* вводим понятие состояний для регистров ( STATE_INIT, STATE_SAVE, STATE_RESTORE, STATE_MODIFICATE и так далее )
* ведем списки состояний для базовых блоков

Тогда для всех ф-ций, у второго операнда с типом reg и регистром ecx, нужно будет найти состояние STATE_SAVE. Причем в списке блока это состояние должно быть первым.

Пример:

3002F110 sub_3002F110    proc near
3002F110                 push    ebx
3002F111                 push    esi
3002F112                 push    [esp+8+arg_4]
3002F116                 mov     esi, ecx        <== интересующая инструкция
3002F118                 call    sub_301AC430

Список состояний для регистра ecx в данном случае будет содержать первым состоянием STATE_SAVE. Это означает, что регистр сразу же начинает использоваться, без инициализации. То есть, вспоминая про this, ф-ция является членом класса, записываем её в список.

* на выходе будем иметь список ф-ций принадлежащий классам исследуемого приложения.

Что касается модуля PIN, то там все тривиально:

1) устанавливается ф-ция IMG_AddInstrumentFunction, в колбеке которой указываем, за каким приложением/модулем нужно следить(нужно для оптимизации процесса).
2) устанавливается ф-ция INS_AddInstrumentFunction, в колбеке которой указываем, какие аргументы нас интересуют(адрес инструкции и содержимое ecx):

INS_InsertCall( ins, IPOINT_BEFORE, (AFUNPTR)InstructionHandler, IARG_INST_PTR, IARG_REG_VALUE, REG_ECX, IARG_END );

Результаты всего этого бедлама и их анализ:

Во-первых, несмотря на фильтрацию инструкций только от нужного exe/dll в PIN, результатов для большого приложения придется ждать довольно долго ( десятки минут ). Во-вторых, результаты придется анализировать.

Выглядит все примерно следующим образом:
...
sub_30090C3F, ecx = 0x141af0a0
sub_30069312, ecx = 0x12f460
sub_30070AA7, ecx = 0x14192000
sub_30070AA7, ecx = 0x14192000
sub_3008FB93, ecx = 0x141af0a0
sub_30084CA2, ecx = 0x141af0a0
sub_3000D6B6, ecx = 0x189dc8
sub_3000242F, ecx = 0x189de4
...

Сразу заметно, что для ф-ций sub_30090C3F, sub_3008FB93, sub_30084CA2 значение ecx одинаково. Следовательно, они принадлежат к одному классу. Виртуальные ф-ции для такого способа также не являются проблемой. Конструкторы находятся также, они просто будут первыми в списке.

Еще стоит упомянуть о том, что при выполнении исследуемой программы, вызываются не все ф-ции. Соответственно, не все ф-ции принадлежащие классам можно восстановить.

Но полное покрытие это уже совсем другая задача, и вобще говоря, еще не решенная (см. информацию по symbolic execution).

понедельник, 17 сентября 2012 г.

Забавный трюк с именами файлов

В unicode есть специальный символ, “right to left override” (RLO) придуманный для языков, читаемых справа налево. Однако, этот символ используют, чтобы скрыть реальное расширение файла.

Пример: "MicrosoftTestCPUlexe.doc”, выглядит как doc файл, однако это не так.
Все дело как раз в RLO символе, помещенном перед d. Таким образом, файл "MicrosoftTestCPUlcod.exe, отображается как "MicrosoftTestCPUlexe.doc”.

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

среда, 5 сентября 2012 г.

Максимальный размер физ. памяти в Win7 Home Premium

Как известно, windows поставляется в ввиде клиентских и серверных версий, которые в свою очередь разделяются на редакции, например Windows 7 Home Premium, Windows 7 Ultimate и так далее. Каждая редакция имеет свой набор фич и возможностей. К фичам относятся BitLocker, VHD Booting и т.д. Кроме этого редакции также различаются и макс. количеством поддерживаемой физ. памяти.

Инфу о поддерживаемых фичах для каждой редакции можно найти в мсдн, но можно и посмотреть самим, она находится в реестре, в HKLM\SYSTEM\CurrentControlSet\Control\ProductOptions. Точнее, там находится кешированный кусок из файла C:\Windows\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\SoftwareProtectionPlatform\tokens.dat, который содержит среди прочего и лицензию на копию windows.
Судя по всему, активация win7 проходит через замену именно этого файла.

Достать информацию о фичах установленной версии винды можно тулзой с www.winsiderss.com/tools/slpolicy.htm. Тулза выводит всю информацию о системе, для моей win7 home premium выдало примерно такое:

SlPolicy v1.05 - Show Software Licensing Policies
Copyright (C) 2008-2011 Winsider Seminars & Solutions Inc.
www.winsiderss.com

Kernel

Maximum Memory Allowed (IA64): 16384

...

Dynamic Partitioning Supported: No
Virtual Dynamic Partitioning Supported: No
Memory Mirroring Supported: No
Native VHD Boot Supported: No
Bad Memory List Persistance Supported: No

...

Desktop Window Manager

Transparency Allowed: Yes
Flip 3D Allowed: Yes
Thumbnails Allowed: Yes
Animated Transitions Allowed: Yes
Desktop Composition Allowed: Yes

...

На этом любопытство насчет количества памяти было удовлетворено, правда осталось неясным, как происходит повышение количества памяти при смене лицензии, неужто там есть магический jump, ведь бинарь ядра один и тотже, меняются лишь условия в boot-time ( в зависимости от типа лицензии ). Кто-нибудь знает ответ?

updated:

Величину Maximum Memory Allowed также можно получить, передав строку "Kernel-WindowsMaxMemAllowedx64" в сервисную функцию
NTSTATUS ZwQueryLicenseValue( PUNICODE_STRING Name, ULONG *Type, PVOID Buffer, ULONG Length, ULONG *DataLength );

среда, 25 июля 2012 г.

Anti-Rop

Microsoft аннонсировала $250,000 за наиболее "эксплойто-подавляющую" технику.

Уже известны три финалиста, которые акцентировали свое внимание на anti-rop технологии, придуманными ими же.

Первый из них, Jared DeMott, придумал технику, названную им "/ROP", которая проверяет целевой адрес каждой return инструкции и затем сравнивает её с легитимным списком.

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

Третий исследователь Vasilis Pappas, назвал свою задумку "kBouncer". Когда идет выполнение rop кода, control-flow выглядит необычно и атаку можно легко обнаружить. kBouncer основывается на фиче мониторинга производительности, которая существует в новейших Intel процессорах(Last Branch Recording).

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

Микрософт врятли будет выкидывать деньги просто так, поэтому логично предположить, что одна из предложенных технологий (или все три?)  в каком то виде будут реализованы либо в windows 8, либо в next service pack windows 7.

Поживем-увидим.

воскресенье, 22 июля 2012 г.

Patch-Guard 1

Введение

Устав от бесконечных патчей ядра как антивирусными продуктами, так и малварью, Microsoft в 2005м году ввели технологию защиты ядра от изменений - Kernel Patch Protection, или Patch Guard. PG разработан только для х64 систем, он появился в системах Windows XP и Windows Server 2003 Service Pack 1.

В данной статье будет рассмотрена реализация этой технологии( PG версии 1 ). Методов обхода PG в данной статье нет.

Итак, при патче некоторых системных структур или патче кода ядра( и не только его ) через некоторый промежуток времени произойдет BSOD с багчеком CRITICAL_STRUCTURE_CORRUPTION(0x00000109).

Чтобы получить некоторое общее представление о том, что контролирует PG, обратимся к описанию данного багчека, которое можно найти в msdn.

Тип, описание:

0x0, A generic data region
0x1, A function modification or the Itanium-based function location
0x2, A processor interrupt dispatch table (IDT)
0x3, A processor global descriptor table (GDT)
0x4, A type-1 process list corruption
0x5, A type-2 process list corruption
0x6, A debug routine modification
0x7, A critical MSR modification


Итак, список довольно внушительный. Контролируются системные таблицы, MSR'ы, недопускается патч функций ядра и некоторых других модулей( об этом ниже ) и т.д.

Самозащита PG и её отключение
                                                                                               
Кроме того, что патч гвард защищает ядро ОС от изменений, он еще защищается и от любопытных глаз. Как он это делает?

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

Патчгвард инициализируется незаметным способом на этапе инициализации ядра:

KeInitSystem:
        mov     rcx, qword ptr cs:KiTestDividend
        mov     edx, 0CB5FA3h
        call    KiDivide6432
        cmp     eax, 5EE0B7E5h
        jnz     loc_83B82C
...
loc_83B82C:
        mov     ecx, 5Dh
        call    KeBugCheck

Сама же функция KiDivide6432 просто выполняет деление:
   
KiDivide6432    proc near
        mov     eax, ecx
        mov     r8, rdx
        shr     rcx, 20h
        mov     edx, ecx
        div     r8d
        retn
KiDivide6432    endp

Переменная которая будет разделена на 0CB5FA3 расположена таким образом, что перекрывается с переменной для обнаружения отладчика:

dq nt!KiTestDividend L1
fffff800011766e0  004b5fa3a053724c

db nt!KdDebuggerNotPresent L1
fffff800`011766e7  00     

Таким образом без отладчика она равна:  014b5fa3a053724c
А с отладчиком данная переменная равна: 004b5fa3a053724c

Таким образом при включенном отладчике результат деления будет: 004b5fa3a053724c / 0CB5FA3 = 5EE0B7E5, то есть как раз той величине, с которой сравнивается. Но без отладчика, результат будет другой: 014b5fa3a053724c / 0CB5FA3 = 1A11F49AE.

В описании команды div сказано:

"Команда выполняет целочисленное деление операндов с выдачей результата деления в виде частного и остатка от деления. При выполнении операции деления возможно возникновение исключительной ситуации: 0 — ошибка деления. Эта ситуация возникает в одном из двух случаев: делитель равен 0 или частное слишком велико для его размещения в регистре eax/ax/al."

Видно, что результат 1A11F49AE явно в eax не поместится, так что возникает исключение Divide Error, которое задействует обработчик int 0.

IDT на х64 системах выглядит несколько иначе, чем на х86 системах, в плане названия ф-ций обработчиков:

0000000000831A90 KiInterruptInitTable dq 0
0000000000831A98 off_831A98      dq offset KiDivideErrorFault
0000000000831AA0                 dq 1
0000000000831AA8                 dq offset KiDebugTrapOrFault
0000000000831AB0                 dq 30002h
0000000000831AB8                 dq offset KiNmiInterrupt
0000000000831AC0                 dq 303h
0000000000831AC8                 dq offset KiBreakpointTrap
0000000000831AD0                 dq 304h
0000000000831AD8                 dq offset KiOverflowTrap

KiDivideErrorFault это нужный нам обработчик.

Далее через цепочку ф-ций дело приходит в: KiDivideErrorFault => KiExceptionDispatch => KiDispatchException => KiPreprocessFault => KiOpDecode => KiOpLocateDecodeEntry(вызов через KiOpOneByteTable[index]) => KiOp_Div

KiOp_Div:

call    sub_403AA0 // вызов неизвестной ф-ции

Которая становится немного более известной:

sub_403AA0      proc near
    mov     eax, eax
KiFilterFiberContext proc near
...
KiFilterFiberContext endp

В виде псевдокода KiFilterFiberContext выглядит как:

BOOLEAN KiFilterFiberContext( PCONTEXT contextRecord )
{
    PUCHAR exceptionAddress = (PUCHAR)KiDivide6432; // берется адрес ф-ции KiDivide6432, которая была вызвана для инициализации PG

    exceptionAddress += 0xB; // к этому адресу прибавляется 0xB, таким образом он будет равен адресу вызвавшему исключение

    if ( contextRecord->Rip != exceptionAddress ) // если адрес другой - пропускаем его
        return FALSE;

    if ( !KiInitializePatchGuard() )  // если исключение вызвано инструкцией div r8d внутри ф-ции KiDivide6432, то вызываем ф-цию инициализации PG
    {
        contextRecord->Rdx = 0xFFFF;
        return FALSE;
    }

    contextRecord->Rdx = 0xFFFFFF; // если все прошло хорошо - ресетим rdx так, чтобы исключения не было. Таким образом, в этой точке PG полностью проинициализирован и ядро продолжит загрузку

    return TRUE;
}

Стек инициализации патч гварда:

nt!KiFilterFiberContext+0x2a
nt!KiOp_Div+0x29
nt!KiPreprocessFault+0xc7
nt!KiDispatchException+0x85
nt!KiExceptionExit
nt!KiDivideErrorFault+0xb7
nt!KiDivide6432+0xb

Разобравшись с инициализацией PG встречаем еще одну проблему - невозможность поставить любой брекпойнт. Точнее, поставить то мы его сможем, но PG спустя время выдаст нам BSOD, ведь брекпойнт это int 3 в коде, а значит патч, а патчить что-либо под PG строго запрещается.

Любой, у кого есть хотя бы небольшой опыт отладки сразу же вспомнит про другой способ установки брекпойнтов - memory breakpoints, работающие через DR регистры процессора. Что же, это нам подходит. Однако и тут PG подложил нам свинью - он сбрасывает DR регистры! Вот как выглядит функция с помощью которой он это делает:

KiNoDebugRoutine proc near
    xor     eax, eax
    mov     dr7, rax
    retn
KiNoDebugRoutine endp

PG просто обнуляет управляющий регистр, и все наши брекпойнты сбрасываются!

Далее, предположим, мы поставили брекпойнт, самый обычный, через bp address. PG обнаружил патч, сгенерировал BSOD. Как нам найти первоисточник, то есть функцию самого PG, которая ответственна за проверку целостности кода? Казалось бы ответ очевиден, BSOD генерируется известной функцией - KeBugCheckEx, ставим брекпойнт на нее, и смотрим стек вызовов, среди него будет и функция PG.

Однако тут мы встречаем проблему номер три - стек вызовов пустой, все регистры обнулены! Это еще один пусть защиты PG от изучения.

Делаются все эти нехорошие действия тут:

SdbpCheckDll    proc near
arg_20          = qword ptr  28h
arg_28          = qword ptr  30h
arg_30          = qword ptr  38h

        mov     rsi, [rsp+arg_28]
        mov     rdi, [rsp+arg_20]
        mov     r10, [rsp+arg_30]
        xor     eax, eax

loc_567411:

        mov     [r10], rax
        sub     r10, 8
        cmp     r10, rsp
        jnb     short loc_567411
        mov     [rsp+arg_20], rdi
        mov     rbx, rax
        mov     rdi, rax
        mov     rbp, rax
        mov     r10, rax
        mov     r11, rax
        mov     r12, rax
        mov     r13, rax
        mov     r14, rax
        mov     r15, rax
        jmp     rsi            // rsi = nt!KeBugCheckEx
SdbpCheckDll    endp

В псевдокоде:

//
// Ф-ция зануляет в стеке обширный кусок данных, что не дает восстановить по колстеку причину падения.
// Также обнуляются все регистры. После чего вызывается BSOD с багчеком CRITICAL_STRUCTURE_CORRUPTION(109h)
//

VOID SdbpCheckDll( __in ULONG BugCheckCode, __in ULONG_PTR P1, __in ULONG_PTR P2, __in ULONG_PTR P3, __in ULONG_PTR P4, __in PVOID KeBugCheckExPtr, __in ULONG unkPtr )
{
    ULONG size = (ULONG)( (PUCHAR)unkPtr - (PUCHAR)rsp );
    size = size / sizeof(ULONG64);

    memset( rsp, 0, size );

    __asm
    {
        mov     [rsp+28h], rdi ; обновляется P4, он нужен для вывода информации в bsod'е, т.к. показывает тип испорченных данных
        xor     eax, eax
        mov     rbx, rax
        mov     rdi, rax
        mov     rbp, rax
        mov     r10, rax
        mov     r11, rax
        mov     r12, rax
        mov     r13, rax
        mov     r14, rax
        mov     r15, rax
    }

    return KeBugCheckExPtr( BugCheckCode, P1, P2, P3, P4 );
}

Теперь, когда вся информация есть, можно заставить PG работать вместе с отладчиком. Чтобы это сделать, нужно скопировать ядро, переименовать его, например, в MyKernel.exe пропатчить его на диске.

Патчить нужно, чтобы решить три проблемы перечисленные выше:

1) Заставить PG работать вместе с отладчиком, для этого патчим константу таким образом, чтобы сгенерировалось исключение даже с включенным отладчиком. Собственно, это довольно тривиально, поэтому я не буду расписывать такие вещи подробно.

2) Заставить PG не обнулять отладочные регистры - заполняем nop'ами функцию KiNoDebugRoutine.

3) Заставить PG не обнулять стек, чтобы можно было увидеть call-stack.

После всех патчей, нужно прописать наше ядро в boot.ini следующим образом:

multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="My Kernel Windows XP DEBUG" /noexecute=optin /fastdetect /debug /debugport=com1 /kernel=MyKernel.exe

Напомню, что все эксперименты ведутся на 64х битной Windows XP.

Теперь пару слов о том, почему патчится файл на диске, а не в памяти. Дело в том, что код PG находится в discardable секциях, то есть, данные секции будут выгружены из памяти сразу после инициализации ядра.

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

Теперь можно спокойно подключать отладчик, беспрепятственно ставить hardware breakpoints и смотреть стеки вызовов в случае bsod'a вызванного PG'ом.

Инициализация PG
           
В начале статьи была упомянута функция KiInitializePatchGuard, самое время рассмотреть, что она делает.

Первым делом проверяется переменная InitSafeBootMode, и если ОС загружена в safe mode - инициализация PG завершается. Далее случайным образом выбирается тэг для памяти, из массива тагов 'AcpSFileIpFIIrp MutaNtFsNtrfSemaTCPc', в этой памяти будет храниться контекст PG.

Контекст заполняется так: копируется содержимое CmpAppendDllSection, представляющее собой функцию расшифровки, получаются адреса нужных PG функций. Случайным образом выбирается ключ шифрования для контекста PG, входом для генератора случайных чисел является инструкция rdtsc. Через cpuid определяется число валидных для адреса битов( для 64х битных платформ в действительности адреса 48-ми разрядные, а не 64х, как можно было бы подумать ), затем эта информация сохраняется в контексте PG. Далее опять выделяется память, случайного размера, в нее копируется контекст и код PG из секции INITKDBG. После этого, подсчитываются хеши для секций ntoskrnl.exe таких как .pdata, .edata, .idata, потом подсчитываются хеши для hal.dll и для ndis.sys.

Выглядит это в отладчике примерно так ( например, для секции .pdata ntoskrnl.exe ):

kd> dqs fffffadfe78bbb37
fffffadf`e78bbb37  00000000`00000001                                                    // type   
fffffadf`e78bbb3f  fffff800`011a2000 nt!CcCleanSharedCacheMapList <PERF> (nt+0x1a2000)    // section virtual address
fffffadf`e78bbb47  43986eee`00080bbc                                                    // hash | section virtual Size

После подсчета хешей секций, считаются хеши для системных структур:

KiServiceTable
KeServiceDescriptorTable
Gdt
Idt


Далее опять выделяется память и в нее копируется весь контекст вместе с хешами.
Запоминаются адреса KdpStub, KdpTrap, KiDebugRoutine. Генерируется случайный индекс, по нему из таблицы DPC процедур выбирается одна, адрес её запоминается в контексте PG. После этого, весь контекст зашифровывается с ранее выбранным случайно сгенерированным ключем. Далее случайным образом выбирается период для таймера, инициализируется таймер с DPC, которая запустится после окончания таймаута.

Этим действием заканчивается инициализация PG. По сути инициализация состоит в сборе информации о целостности системы, сохранению её в контексте PG, и подготовке DPC с зашифрованным контекстом, в котором находятся как хеши, так и определенная часть кода PG. Исходный же код PG будет удален из памяти загрузчиком ядра, так как находится в выгружаемой после инициализации секции ( discardable ).

Проверка целостности ОС

При инициализации PG выбиралась одна DPC из массива функций:
                                                                                             
KiInitializePatchGuard:

INIT:0000000000821BFE                 lea     rcx, cs:400000h
INIT:0000000000821C05                 mov     rax, [rcx+rax*8+42F4A8h] ; 82F4A8 - по этому адресу будет массив DPC процедур патч гварда
...
INIT:000000000082F4A8                 dq offset KiScanReadyQueues
INIT:000000000082F4B0                 dq offset ExpTimeRefreshDpcRoutine
INIT:000000000082F4B8                 dq offset ExpTimeZoneDpcRoutine

Эти DPC функции играют ключевую роль в проверке целостности ОС.

Вернемся к инициализации DPC, она выполняется функцией VOID KeInitializeDpc( __out PRKDPC Dpc, __in PKDEFERRED_ROUTINE DeferredRoutine, __in_opt PVOID DeferredContext );

Ранее, при заполнении контекста PG через cpuid было получено число битов валидных для адреса. При инициализации DPC, в DeferredContext идет рандомный адрес, который содержит заведомо большее число битов, чем поддерживается.

Теперь рассмотрим, что произойдет в самой функции DPC, когда сработает таймер PG и она выполнится:

ExpTimeRefreshDpcRoutine proc near
{
.text:                 mov     [rsp+arg_0], rcx
.text:                 sub     rsp, 68h
.text:                 mov     eax, 1
.text:                 xadd    [rdx], eax ; cb419eef84149c09 <====== GP fault, goto KiGeneralProtectionFault

kd> !pte cb419eef84149c09
        VA cb419eef84149c09
PXE at FFFFF6FB7DBED9E8    PPE at FFFFF6FB7DB3DDF0    PDE at FFFFF6FB67BBE100    PTE at FFFFF6CF77C20A48
contains 0000000000000000
not valid

Адрес естественно невалидный, что приводит нас к исключению, которое может быть обработано в SEH. Самое интересное, что эти DPC существовали и до PG, то есть это часть системы, и инженеры микрософт довольно искусно
вплели в них незаметный способ перехода на код проверки целостности системы - через исключения и обработку их в SEH.

Итак, теперь нужно увидеть SEH обработчики, поможет это сделать команда отладчика .fnent:

Описание команды: The .fnent command displays information about the function table entry for a specified function.

kd> .fnent nt!ExpTimeRefreshDpcRoutine
Debugger function entry 00000000`007c9298 for:
(fffff800`01101090)   nt!ExpTimeRefreshDpcRoutine   |  (fffff800`01101180)  
nt!ExpCenturyDpcRoutine
Exact matches:
    nt!ExpTimeRefreshDpcRoutine = <no type information>

BeginAddress      = 00000000`00101090
EndAddress        = 00000000`001010bd
UnwindInfoAddress = 00000000`0013101c

Unwind info at fffff800`0113101c, 10 bytes
  version 1, flags 1, prolog 9, codes 1
  handler routine: nt!_C_specific_handler (fffff800`01029830), data 1
  00: offs 9, unwind op 2, op info c    UWOP_ALLOC_SMALL.

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

Псевдокод этой ф-ции:

VOID sub_565530( __in ULONG64 xoredPgContext )
{
    PatchGuardContext *pgContext = (PatchGuardContext*)( ROL( xoredPgContext, 0x10 ) );

    KiNoDebugRoutine();

    pgContext->pgWorkItem.List = NULL;
    pgContext->pgWorkItem.WorkerRoutine = (PUCHAR)pgContext + pgContext->AddressOfWorkItemFunc; // FsRtlUninitializeSmallMcb
    pgContext->pgWorkItem.Parameter = pgContext;

    return pgContext->ExQueueWorkItemPtr( &pgContext->pgWorkItem, DelayedWorkQueue );
}

Функция FsRtlUninitializeSmallMcb ответственна за проверку целостности ОС. Вернее, если быть точным, то она вызывает FsRtlMdlReadCompleteDevEx и освобождает предыдущий контект.

FsRtlMdlReadCompleteDevEx делает самопроверку контекста PG, и если данные в нем изменены - нас ждет неминуемый BSOD.

Далее рандомно выбирается число, которое идет в отдельный switch, каждый case которого - это проверка определенных данных ОС, то есть подсчет хешей и сравнение их с вычисленными хешами на этапе инициализации PG. После того, как проверка сделана, вызывается либо BSOD с кодом CRITICAL_STRUCTURE_CORRUPTION если найдены несовпадения, либо выполнение идет дальше - на создание новой области памяти с рандомным тэгом и рандомным размером. После чего контекст туда перекопируется, предварительно зашифровавшийся с новым ключем, выберется новая рандомная DPC функция из массива функций, выберется случайный таймаут и новая DPC будет ждать своего часа проверки целостности ОС.

Таким образом, код PG вместе с контекстом постоянно кочует в памяти, меняя тэги, ключи шифрования и размер памяти, в которой он сидит.

Заключение
                                                                                               
Вот и подошел к концу небольшой рассказ о внутренностях Patch Guard. Была рассмотрена первая версия, появившаяся аж 7 лет назад, после этой версии вышли как минимум еще 2, в vista и в win7.

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

Но в любом случае, Patch Guard получился довольно интересным решением, и если вы хотите разобраться как работает Patch Guard, то лучший способ - это пройти все самому.

воскресенье, 29 апреля 2012 г.

Типы объектов ядра

Стало любопытно, как изменялось количество объектов ядра в разных ОС семейства NT.

Начнем с WindowsXP. Cписок объектов ядра выглядит следующим образом:

IoFileObjectType
MmSectionObjectType
IoControllerObjectType
IoDriverObjectType
IoDeviceObjectType
IoCompletionObjectType
LpcPortObjectType
LpcWaitablePortObjectType
ObpDirectoryObjectType
ObpSymbolicLinkObjectType
PsThreadType
PsProcessType
PsJobType
SeTokenObjectType
WmipGuidObjectType
ExCallbackObjectType
ExEventObjectType
ExSemaphoreObjectType

ExTimerObjectType
ExEventPairObjectType
ExMutantObjectType
ExProfileObjectType
ExpKeyedEventObjectType
CmpKeyObjectType
DbgkDebugObjectType


Набор объектов ядра в Windows Vista был существенно расширен, по сравнению с WindowsXP.

Прежде всего, в Vista появились advanced local procedure calls (ALPC), что привело к замене объектов LpcPortObjectType и LpcWaitablePortObjectType которые существовали в WindowsXP на объект AlpcPortObjectType.

Также был усовершенствован механизм реализации пула потоков - его перенесли в ядро, что привело к появлению нового объекта ядра: ExpWorkerFactoryObjectType.

Был переделан механизм сессий (изоляция нулевой сессии и т.д.), что вылилось в добавление нового объекта: MmSessionObjectType.

Новый механизм под названием менеджер транзакций привнес в ядро сразу 4 новых объекта: TmEnlistmentObjectType, TmTransactionManagerObjectType, TmTransactionObjectType, TmResourceManagerObjectType.

Ну и наконец, реализация механизма event tracing'a также добавила новый объект: EtwpRegistrationObjectType.

В Windows7 количество объектов ядра также увеличилось, по сравнению с Vista.

Добавлены объекты UserApcReserve и IoCompletionReserve, они служат для хранения соответствующих объектов (apc/completion port) в заранее выделенной памяти, а не из пула. Оба этих объекта создаются через native-api NtAllocateReserveObject.

Обновление механизма ETW в Windows7 также привнесло в ядро новый объект: EtwpRealTimeConnectionObjectType.

Изменение в Power Management'е привело к появлению еще одного объекта: PopPowerRequestObjectType.

четверг, 26 апреля 2012 г.

Privilege escalation protections in Windows 8

Грядущий выход windows 8 принесет несколько новинок в плане безопасности, о двух из которых, относящихся к защите от повышения привилегий и будет сегодняшняя заметка.

Первая из них закрывает целый класс атак(kernel-mode NULL dereference), связанных с повышением привилегий.

Сценарий атаки следующий: 

1) находится уязвимость, позволяющая записать ноль по любому адресу в ядре
2) в юзермоде выделяется память по нулевому адресу (флаг MEM_TOP_DOWN)
3) перезаписывается указатель в HAL_DISPATCH
4) вызывается NtQueryIntervalProfile которая в приводит к вызову ф-ции из HAL_DISPATCH и соответственно к вызову кода в нулевой странице

Таких уязвимостей было открыто довольно много и Microsoft в windows 8 решила закрыть саму возможность реализации атаки, запретив выделять нижние 64кб юзерской памяти.

И второе нововведение - поддержка SMEP.

Supervisor Mode Execution Protection (SMEP) - это технология, призванная предотвратить выполнение кода в ядре из тех страниц, которые помечены как юзерские.

То есть, каждая страничка в системе будет помечена определенным битом, который либо будет давать ей право исполняться в ядре, либо нет.
Реакцией на попытку выполнения странички недопустимой для исполнения в ядре будет fault.

Аппаратная поддержка SMEP появилась в процессорах на базе архитектуры Ivy-Bridge, которые были презентованы буквально на днях.

Не знаю, насколько эти новинки будут эффективными, и как скоро появятся публикации по их обходу, но общая тенденция развития windows в плане безопасности безусловно радует.

суббота, 31 марта 2012 г.

Карта распределения Page Fault's при старте приложения

В Windows используется концепция demand paging, означающая, что физическая память выделяется только при обращении к виртуальному адресу, но никак не при выделении виртуальных адресов.

На уровне ОС это реализуется через дерево VAD, привязанное к процессу(EPROCESS->VadRoot) и описывающее диапазоны и другие атрибуты этой выделенной ранее через сервисные функции памяти. При обращении к памяти срабатывает обработчик страничных фолтов(KiTrap0E), который через VAD ищет соответствующий диапазон, и в случае успеха(для private memory это наличие атрибута закоммиченой памяти) из free/standby/zeroed списков выделяется физическая страница и заполняется PDE/PTE(проставляются атрибуты, valid, pfn и т.д.).

Стало интересно, какие странички подгружаются первыми при старте приложения.
Логично было бы предположить, что это страничка с apc диспатчером и другие страницы ntdll.dll, однако это не так.

Оказалось, что первая страничка это KUSER_SHARED_DATA, к которой идет обращение при создании секции для ехе процесса:

MiMapViewOfImageSection()
{
...
    if ( ControlArea->Segment->u2.ImageInformation->ImageContainsCode &&
         ((ControlArea->Segment->u2.ImageInformation->Machine < USER_SHARED_DATA->ImageNumberLow) ||
        (ControlArea->Segment->u2.ImageInformation->Machine > USER_SHARED_DATA->ImageNumberHigh) )
...
}

Затем при создании и инициализации PEB будет второй #PF:

MmCreatePeb()
{
...
    Status = MiCreatePebOrTeb (TargetProcess, sizeof(PEB), (PVOID)&PebBase);
...
    try
    {
        PebBase->InheritedAddressSpace = InitialPeb->InheritedAddressSpace;
...
}

Следом идет создание TEB для первичного потока, при её инициализации случится еще один #PF:

MmCreateTeb()
{
...
    Status = MiCreatePebOrTeb (TargetProcess, TebSize, (PVOID) &TebBase);
...
    try
    {
        TebBase->NtTib.ExceptionList = EXCEPTION_CHAIN_END; // только на x86
...
}

Перед тем, как запустить юзермодную APC стартующую поток, ядро копирует контекст в юзермодный стек, что приводит к #PF:

KiInitializeUserApc()
{
...
    RtlCopyMemory ((PULONG)(UserStack + sizeof(KAPC_RECORD)), &ContextFrame, sizeof(CONTEXT));
...
    TrapFrame->Eip = (ULONG)KeUserApcDispatcher;
...
}

И только потом, при переходе в user mode, при выполнении KiUserApcDispatcher подгрузится страничка ntdll.dll.
Хотя подгрузится это не совсем подходящее слово, т.к. подгрузилась с диска эта dll еще при старте ОС, а в последующих случаях, т.е. при старте новых процессов все её странички разрешались ввиде soft fault's, т.е. из памяти, без обращений к диску.

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

Необходимость может наступить, например, при достижении лимита(данные для х86):

ULONG PspMinimumWorkingSet = 20;
ULONG PspMaximumWorkingSet = 45;

Soft fault's практически бесплатны в плане временных затрат( мне думается самое затратное там - это переключение контекста при срабатывании обработчика страничного фолта ), и в дальнейшем при необходимости, ранее выкинутые странички будут вновь добавлены в рабочий набор процесса.

воскресенье, 11 марта 2012 г.

Определение id некоторых сервисных функций без таблиц

Многие программы на х86 перехватывают функции через патч SDT, индексы этих функций меняются в зависимости от версии ОС.

Решение проблемы - это как правило хардкод индексов в коде, но есть и определенные хитрости, позволяющие избежать хардкода в некоторых случаях.

Рассмотрим любую Zw функцию, экпортируемую ядром, например ZwDuplicateToken:

nt!ZwDuplicateToken:
804fe30c b845000000      mov     eax,45h
804fe311 8d542404          lea     edx,[esp+4]
804fe315 9c                    pushfd
804fe316 6a08                 push    8
804fe318 e874f10300       call    nt!KiSystemService (8053d491)
804fe31d c21800             ret     18h

Можно заметить, что id можно получить отсюда, например таким кодом:

offsetToId = (PUCHAR)ZwDuplicateToken;
id = *(PULONG)(offsetToId + 1);

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

вторник, 28 февраля 2012 г.

Баг PsSetCreateThreadNotifyRoutine на WinXp

Забавный баг существует на Windows XP, в нотификаторе на создание потока нельзя получить объект потока по приходящему в него tid'у.

А все потому, что:

NTSTATUS PspCreateThread(...)
{
...
   ExGetCallBackBlockRoutine - вызов колбека на XP
...
   ObInsertObject - а добавление объекта в таблицу хендлов идет уже после
...
}

На Windows Vista баг пофикшен:

NTSTATUS PspInsertThread(...)
{
...
     call    ObInsertObject - сначала объект добавляется в таблицу хендлов
...
     mov     [ebp+arg_14], offset _PspCreateProcessNotifyRoutine
     push    [ebp+arg_14]
     call    _ExReferenceCallBackBlock
...
     call    dword ptr [edi+4] - а уже потом идет вызов колбека
...
     call    _ExDereferenceCallBackBlock
...
}

четверг, 23 февраля 2012 г.

Branch Tracing в ядре

Оказывается, в ядре windows есть и поддержка Branch Tracing(начиная с висты).
Этой функциональностью пользуется driver verifier:

На этапе инициализации:

IoInitSystem(...)
{
...
    if ( ViVerifierEnabled )
        VfNotifyVerifierOfEvent(0); // initialize branch tracing
...
}

Phase1InitializationDiscard(...):
{
...
    push    5
    pop     eax
    call    VfNotifyVerifierOfEvent(x) // start branch tracing
...
}


VOID VfNotifyVerifierOfEvent( ULONG eventId )
{
    switch ( eventId )
    {
        case 0:                      
             VfInitializeBranchTracing();
        break;
 ...
        case 5:
             VfStartBranchTracing();
        break; 

    }


Включение branch tracing:

VfStartBranchTracing
{
...
    push    0
    push    0C0h
    push    1D9h // IA32_DEBUGCTL
    WRMSR(x,x,x)
...
}

К сожалению, чаще всего драйвера тестируются на виртуалках, а там branch tracing не поддерживается, поэтому полноценно поиграться с ним не выйдет.

пятница, 27 января 2012 г.

Bound import

О bound import'e писали неоднократно, поэтому подробно останавливаться на нем не вижу смысла, напомню лишь что это, в общих чертах.

Bound import позволяет сэкономить время загрузки модуля, за счет кеширования адресов ф-ций в импорте.
То есть, системный загрузчик не делает получение адресов ф-ций из их имен, если выполнены некоторые условия для этого.

Условия следующие:

* Виртуальный адрес у директории bound import'a не должен быть равен нулю
* временные штампы в IMAGE_BOUND_IMPORT_DESCRIPTOR и в хедере импортируемого модуля совпадают
* Для dll: модуль не должен быть ребазирован (незнаю правда, насколько эта информация справедлива для систем с aslr)

Стандартные приложения windows используют bound import очень активно.

Мне стало интересно, много ли инструкций экономит эта фича.

Проверялось число инструкций для notepad.exe ( win xp sp3 ), с apc диспатчера, то есть с первых инструкций(в UM) после старта процесса и до entry point приложения.

Для notepad без bound import'a число инструкций = 2280899
Для notepad с bound import'ом  число инструкций = 2268677

Разница 12222, т.е. всего пол процента от общего числа инструкций ( до точки входа ).

Однако стоит помнить, что обход импорта это рекурсивная процедура:

LdrpWalkImportDescriptor => LdrpLoadImportModule => LdrpWalkImportDescriptor
LdrpWalkImportDescriptor => LdrLoadDll => LdrpWalkImportDescriptor

То есть прирост в производительности от использования bound import'a может быть заметным, если задействовано большое количество импортируемых модулей.

среда, 25 января 2012 г.

Когда postmortem analysis бесполезен

Чаще всего при BSOD'ах помогает анализ креш дампа, но бывают случаи, когда такой анализ бесполезен.

Такие ситуации могут быть, когда существует системный поток или воркитем, которые по каким-то причинам ( ошибки в проектировании )
продолжают работать даже тогда, когда драйвер выгружен, что приводит к ошибкам доступа к выгруженной памяти и соответственно к багчеку:

DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS (ce)
A driver unloaded without cancelling timers, DPCs, worker threads, etc.
The broken driver's name is displayed on the screen.
Arguments:
Arg1: f7e7f81a, memory referenced
Arg2: 00000008, value 0 = read operation, 1 = write operation
Arg3: f7e7f81a, If non-zero, the instruction address which referenced the bad memory
    address.
Arg4: 00000000, Mm internal code.

Debugging Details:
------------------

WRITE_ADDRESS:  f7e7f81a

FAULTING_IP:
Template+181a
f7e7f81a ??              ???
...
PROCESS_NAME:  System
...

STACK_TEXT: 
f7f5a84c 804f7bad 00000003 f7e7f81a 00000000 nt!RtlpBreakWithStatusInstruction
f7f5a898 804f879a 00000003 00000000 c07bf3f8 nt!KiBugCheckDebugBreak+0x19
f7f5ac78 804f8cc5 00000050 f7e7f81a 00000008 nt!KeBugCheck2+0x574
f7f5ac98 8051cc5f 00000050 f7e7f81a 00000008 nt!KeBugCheckEx+0x1b
f7f5acf8 8054052c 00000008 f7e7f81a 00000000 nt!MmAccessFault+0x8e7
f7f5acf8 f7e7f81a 00000008 f7e7f81a 00000000 nt!KiTrap0E+0xcc
WARNING: Frame IP not in any known module. Following frames may be wrong.
f7f5ad80 00000000 8162dd80 00000000 00000000 <Unloaded_Template.sys>+0x181a

В этом случае из дампа невозможно понять кто виноват, т.к. доступа к памяти с драйвером нет, она не валидна:

kd> !pte f7e7f81a
                    VA f7e7f81a
PDE at C0603DF8            PTE at C07BF3F8
contains 000000000101F163  contains 0000000000000000
pfn 101f      -G-DA--KWEV   not valid

Когда системный поток или воркитем один, то логика нас приводит к анализу именно его. Но что делать, когда их несколько?

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

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

Далее в отладчике на живом запущенном драйвере находим границы ф-ции системных потоков:

kd> ? InitSystemThread1 - SystemThread1
Evaluate expression: 336 = 00000150 - размер ф-ции

Выводим листинг всей ф-ции:

kd> u f77fd6f0 f77fd6f0+150h

f77fd6f0 55              push    ebp
f77fd6f1 8bec            mov     ebp,esp
f77fd6f3 6aff            push    0FFFFFFFFh
f77fd6f5 68d81180f7      push    offset Template!__safe_se_handler_table+0x8 (f78011d8)
f77fd6fa 6808dd7ff7      push    offset Template!except_handler3 (f77fdd08)
f77fd6ff 64a100000000    mov     eax,dword ptr fs:[00000000h]
f77fd705 50              push    eax
...

Делаем тоже самое для остальных потоков, после чего добиваемся воспроизведения BSOD'а.

После чего, обладая асм листингами всех рабочих потоков, легко можно будет найти виновника торжества и устранить проблему.