суббота, 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'а.

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

пятница, 30 декабря 2011 г.

Мысли вслух

Подумалось недавно, как много Microsoft делает для обеспечения безопасности своих ОС: куча защитных механизмов против эксплойтов( aslr/dep/safeseh/function pointer obfuscation/sehop/stack cookies и т.д. ), uac, встроенная в ядро система защиты от патчей ( patch guard ) призванная бороться в том числе и с руткитами, скоро появится безопасная загрузка через UEFI, призванная бороться с буткитами(в win8), цикл SDL, призванный делать код безопасным еще на этапе написания, тулзы повышающие безопасность устаревших ОС семейства windows (EMET),  средство удаления вредоносных программ и даже бесплатный антивирус (Microsoft Security Essentials).

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

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

Пока все выглядит как вечная война щита и меча, кончится ли она когда нибудь? Поживем, увидим.

p.s. Всех с Наступающим!