среда, 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, то лучший способ - это пройти все самому.