четверг, 28 апреля 2011 г.

Немного про application verifier

Windows предоставляет несколько возможностей для перехвата ф-ций, для целей верификации, отладки или логгирования, это технология shim, hot-patch, application verifier(чтото наверняка еще забыл), про application verifier сегодняшняя заметка. Про него уже ктото писал, но мне было интересно разобраться самому. Что есть application verifier описано в http://msdn.microsoft.com/en-us/library/ms220948%28v=vs.90%29.aspx.

Если совсем кратко, то application verifier это технология поддерживаемая нативным лоадером PE файлов, позволяющая грузиться перед kernel32.dll и до обработки лоадером импортов и TLS.

Чтобы зарегистрировать дллку провайдера нужно прописаться в реестр следующим образом:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\calc.exe]
"GlobalFlag"="0x02000100"
"VerifierDebug"="0xffffffff"
"VerifierDlls"="test.dll"

test.dll нужно запихнуть в windows/system32.

Лоадер при запуске calc.exe будет подгружать все дллки провайдеров, которые для него описаны, а их entries помещать в список AVrfpVerifierProvidersList. Первая длл в этом списке всегда будет "verifier.dll". Далее, лоадер будет обходить этот список и загружать все длл в нем, вызывая их точки входа с reason равной DLL_PROCESS_VERIFIER. Если открыть "verifier.dll", то можно будет увидеть:

BOOL __stdcall DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID context)
{
...
  if ( fdwReason == 4 ) // DLL_PROCESS_VERIFIER
 {
      if ( context )
        *(_DWORD *)context = &dword_5B1F6258;
 }
...
}

.data:5B1F6258                  dd 1Ch                               // id ( на хп и вин7 id различаются )
.data:5B1F625C                 dd offset off_5B1F6208     // ссылка на другую структуру ( см. ниже )
.data:5B1F6260                 dd 0
.data:5B1F6264                 dd 0

.data:5B1F6208 off_5B1F6208    dd offset aNtdll_dll    ; "ntdll.dll" // имя длл ( капитан очевидность приветствует вас :) )
.data:5B1F620C                 dd 0
.data:5B1F6210                 dd 0
.data:5B1F6214                 dd offset off_5B1F6008  // ссылка на еще одну структуру с ф-циями ( см. ниже )

.data:5B1F6008 off_5B1F6008    dd offset aNtallocatevirt ; "NtAllocateVirtualMemory" // имя ф-ции
.data:5B1F600C                 dd 0
.data:5B1F6010                 dd offset sub_5B1F2366 // ф-ция которой лоадер заместит NtAllocateVirtualMemory

.data:5B1F6014                 dd offset aNtfreevirtualm ; "NtFreeVirtualMemory"
.data:5B1F6018                 dd 0
.data:5B1F601C                 dd offset sub_5B1F23F8

Таким образом, в точке входа дллки вашего провайдера нужно создать эти структуры и передать их лоадеру, это даст возможность подмены ф-ций своими. Таким образом, если shim дает возможность перехвата ф-ций легальным способом на уровне win api, то application verifier, поддерживаемый загрузчиком, дает возможность перехватывать ф-ции на уровне native.

воскресенье, 24 апреля 2011 г.

Безопасный ядерный код

В чем сложность написания безопасного кода в ring0?
Чем это отличается от написания ring3 кода?
Какие инструменты использовать для написания безопасного кода?
Об этом сегодняшняя минизаметка.

Прежде всего нужно сказать о различиях между юзермодным и ядерным кодом:

Во-первых - время разработки. Юзермодный код пишется быстро, он относительно стабилен.
Самое страшное, что может случиться, это падение программы с access violation, а в случае же с драйвером, падение драйвера - это падение всей операционной системы в синий экран смерти(BSOD). Так что волей-неволей, драйверописателю приходится проверять код на виртуальной машине, а это время.

Во-вторых - объем знаний, ring3 разработчику достаточно знать msdn и какие-нибудь библиотеки типа boost'a, если чего-то не известно - открывается документация и там все написано. Для ring0 разработчика знания msdn недостаточно, нужно знать всю систему windows, все её шестеренки и механизмы сформировавшиеся за 25+ летнюю историю этой ОС, которые зачастую нигде не описаны, так что довольно часто приходится самому разбираться в кишках ОС, а для этого есть лишь один источник информации - анализ бинарного кода, а тут требуются знания reverse engineering'a, ассемблера и кучи всяких вспомогательных инструментов/языков помогающих в анализе.

В-третьих - среда. Для той же отладки в юзермоде существуют удобные отладчики с отличной usability. Для ядра существует по сути один инструмент - windbg с неудобным интерфейсом прошлого века(softice вымер, syser нестабилен).

В-четвертых - окружение. Для ring3 программы есть несколько слоев абстракции, навернутых над системными вызовами ядра(int2e/sysenter/syscall), это native api и win api. Они нужны для одной простой цели - для совместимости с разными версиями операционной системы. То есть интерфейсы для юзермодных программ всегда одни и теже, программист об этом даже не думает. В ring0 естественно есть такие же незыблемые интерфейсы предоставляемые ядром, но часто бывают задачи, требующие поиска/выполнения недокументированных ф-ций, что приводит к тестированию этого дела на каждой ОС или даже на каждом сервис паке.

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

Как максимизировать надежность кода?

Просто следовать правилам:

Следовать code-standard'у, использовать SAL Annotations, ASSERT's, проверять аргументы ф-ций, проверять любые данные пришедшие из юзермода (try/except + probe write/read), использовать статические анализаторы кода ( prefast / lint ) которые могут найти какие-то ошибки на этапе компиляции, использовать юнит-тесты и различного рода ревью кода ( self-review/lightweight code review/formal code review ).
Также не нужно забывать о динамических анализаторах, самый известный из которых это driver verifier, он подменяет многие ф-ции своими, следит за утечками памяти, irp'ами, хендлами и т.д. Плюс не надо забывать о контроле выделяемой памяти ( использовать тэги при выделении памяти ), проверяя расход памяти с помощью pooltag/driver verifier/windbg.
Если есть какие-то сомнения, насчет производительности кода, можно использовать всякие профайлеры/счетчики ( wmi/etw/kernrate ).

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

Когда все эти стадии пройдены, можно сказать, что ваш код готов идти в production. Однако и там будут случаться периодически оказии, и придется фиксить баги. От этого никуда не деться, пока нет инструментов, дающих 100%-ную проверку вашего кода ( по крайней мере для ring0 кода ).

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

Ну вот наверное и все, о чем я хотел рассказать.

вторник, 19 апреля 2011 г.

К вопросу о поиске неэкспортируемых символов

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

А если отказаться от построения графа кода, то к примеру поиск ф-ции в ntdll.dll может быть полным кошмаром, изза его размазанности по всему модулю и дроблению на chunks.

Пример подобной ф-ции - LdrpCallInitRoutine(из нее вызываются точки входа длл).
Практически все пути к ней от экспортируемых ф-ций идут внутри chunks(плюс они довольно сильно меняются в разных ОС), и простой дизассемблер тут не поможет, нужно строить граф кода.

Однако, как ни странно, на помощь приходит самый дубовый и примитивный подход - поиск по сигнатуре. А все потому, что она изза своих особенностей реализована не в С коде, а в ассемблере(win2k\private\ntos\dll\i386\ldrthunk.asm):

;++
;
; VOID
; LdrpCallInitRoutine(
;    IN PDLL_INIT_ROUTINE InitRoutine,
;    IN PVOID DllHandle,
;    IN ULONG Reason,
;    IN PCONTEXT Context OPTIONAL
;    )
;
; Routine Description:
;
;    This function calls an x86 DLL init routine. It is robust against DLLs that don't preserve EBX or fail to clean up enough stack.
;    The only register that the DLL init routine cannot trash is ESI.
;
; Arguments:
;
;    InitRoutine - Address of init routine to call
;    DllHandle - Handle of DLL to call
;    Reason - one of the DLL_PROCESS_... or DLL_THREAD... values
;    Context - context pointer or NULL
;
; Return Value:
;
;    FALSE if the init routine fails, TRUE for success.
;
;--

cPublicProc _LdrpCallInitRoutine , 4

InitRoutine     equ [ebp + 8]
DllHandle       equ [ebp + 12]
Reason          equ [ebp + 16]
Context         equ [ebp + 20]

stdENDP _LdrpCallInitRoutine
        push    ebp
        mov     ebp, esp
        push    esi         ; save esi across the call
        push    edi         ; save edi across the call
        push    ebx         ; save ebx on the stack across the call
        mov     esi,esp     ; save the stack pointer in esi across the call
        push    Context
        push    Reason
        push    DllHandle
        call    InitRoutine
        mov     esp,esi     ; restore the stack pointer in case callee forgot to clean up
        pop     ebx         ; restore ebx
        pop     edi         ; restore edi
        pop     esi         ; restore esi
        pop     ebp
        stdRET  _LdrpCallInitRoutine

_TEXT   ends
        end

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

Конечно это скорее исключение, чем правило, но тем не менее, лично мне это сэкономило массу времени.