суббота, 21 мая 2011 г.

Анти-обход перехвата APC dispatcher'a

Нашел у себя в закромах интересный способ обхода перехвата диспетчера APC, который придумал Clerk:

Запуск пользовательского потока в обход перехвата диспетчера APC(R3).
 o Восстанавливаем RtlpCalloutEntryList, для исключения глобальных VEH.
 o Создаём удалённый поток.
 o Формируем SEH-фрейм.
 o Взводим TF в контексте.
Поток стартует с взведённым TF. После исполнения первой инструкции диспетчера апк (KiUserApcDispatcher()) генерируется трассировочное исключение. Управление получает диспетчер исключений (KiUserExceptionDispatcher()). Векторные обработчики не вызываются (RtlCallVectoredExceptionHandlers()). Вызывается установленный SEH. Если диспетчер APC захвачен сплайсингом, останов генерируется после исполнения джампа. Если диспетчер APC перенаправлен на диспетчер исключений, посредством установки точки останова, прежде будет вызван SEH(если хэндлер установлен как VEH).

А я затем задумался, можно ли перехватить диспетчер APC в ring3, чтобы даже при установленном TF можно было сплайсить себе наздоровье?

Казалось бы - нет, нереально. Даже учитывая тот факт, что исключение будет сгенерировано лишь на второй инструкции, это ничего не дает.
Да, можно вместо первой записать short jump, но после прыжка опять таки окажемся в SEH.
Сбросить TF флаг наверное также не получится, т.к. lea edi, [esp+arg_C] это всего 4 байта. См. код диспатчера:

.text:7C90EAC0 _KiUserApcDispatcher@20 proc near
.text:7C90EAC0
.text:7C90EAC0 arg_C           = byte ptr  10h
.text:7C90EAC0
.text:7C90EAC0                 lea     edi, [esp+arg_C]
.text:7C90EAC4                 pop     eax
.text:7C90EAC5                 call    eax
.text:7C90EAC7                 push    1
.text:7C90EAC9                 push    edi
.text:7C90EACA                 call    _ZwContinue@8   ; ZwContinue(x,x)
.text:7C90EACF                 nop

Однако, мне пришел в голову один способ обхода, который требует изменения даже не байтов, а всего 1го бита.

Речь идет о фиче Single-Stepping on Branches, Exceptions, and Interrupts.

Устанавливаем BTF флаг (Single-step on branches) в регистре IA32_DEBUGCTL MSR.
Intel пишет, что исключение будет сгенерировано процессором только при branch, services an interrupt, or generates an exception, то есть мы отключаем по сути TF флаг до первого бранча. А это дает возможность спокойно перехватывать диспетчер APC сплайсингом, но через call myFunc, а не через джамп. А уже в myFunc можно делать все что угодно, включая сброс TF флага.

p.s. Кстати как дополнение, всплыл способ обнаружения( до этого мне не известный ) VmWare и VirtualBox - они не виртуализируют IA32_DEBUGCTL MSR, т.е. детект выглядит как запись в IA32_DEBUGCTL MSR любого значения и чтение его, прочитанное значение всегда будет нулем в виртуалках.

воскресенье, 15 мая 2011 г.

Про статический анализ бинарного кода

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

Пример:

открываем notepad.exe в IDA, находим кусок высокоуровнего seh'а(scopetable entry):

.text:01001898 stru_1001898    _msEH <0FFFFFFFFh, offset loc_100752A, offset loc_100753E>

Смотрим сами обработчики - ссылок из кода на них нет, но(!) есть data xref:

.text:01007528                 jmp     short loc_1007557
.text:0100752A
.text:0100752A loc_100752A:                            ; DATA XREF: .text:stru_1001898
.text:0100752A                 mov     eax, [ebp+ms_exc.exc_ptr]
.text:0100752D                 mov     ecx, [eax]
.text:0100752F                 mov     ecx, [ecx]
.text:01007531                 mov     [ebp+var_28], ecx
.text:01007534                 push    eax
.text:01007535                 push    ecx
.text:01007536                 call    _XcptFilter
.text:0100753B                 pop     ecx
.text:0100753C                 pop     ecx
.text:0100753D                 retn
.text:0100753E
.text:0100753E loc_100753E:                            ; DATA XREF: .text:stru_1001898
.text:0100753E                 mov     esp, [ebp+ms_exc.old_esp]
.text:01007541                 mov     esi, [ebp+var_28]
.text:01007544                 cmp     [ebp+var_1C], 0
.text:01007548                 jnz     short loc_1007551
.text:0100754A                 push    esi             ; int
.text:0100754B                 call    ds:_exit
.text:01007551 ; ---------------------------------------------------------------------------

IDA прекрасно все находит и резолвит, т.к. есть:

.text:0100739D                 push    70h
.text:0100739F                 push    offset stru_1001898
.text:010073A4                 call    __SEH_prolog

А теперь идем в тот же notepad.exe, делаем поиск по ф-ции GetSystemTimeAsFileTime, по xref прыгаем на место вызова её и видим:

.text:010070CC sub_10070B1     endp
.text:010070CC
.text:010070CC ; ---------------------------------------------------------------------------
.text:010070CF                 db 5 dup(0CCh)
.text:010070D4 ; ---------------------------------------------------------------------------
.text:010070D4                 mov     edi, edi
.text:010070D6                 push    ebp
.text:010070D7                 mov     ebp, esp
.text:010070D9                 sub     esp, 10h
.text:010070DC                 mov     eax, dword_1009604
.text:010070E1                 test    eax, eax
.text:010070E3                 jz      short loc_10070EC
.text:010070E5                 cmp     eax, 0BB40h
.text:010070EA                 jnz     short loc_1007139
.text:010070EC
.text:010070EC loc_10070EC:                            ; CODE XREF: .text:010070E3
.text:010070EC                 push    esi
.text:010070ED                 lea     eax, [ebp-8]
.text:010070F0                 push    eax
.text:010070F1                 call    ds:GetSystemTimeAsFileTime
.text:010070F7                 mov     esi, [ebp-4]

Опачки, ф-ция не распозналась IDA'ой. Превратим принудительно её в ф-цию нажав p, после чего видно, что на ф-цию нет перекрестных ссылок, её никто не вызывает из данного модуля. То есть в графе, если им описать данный модуль по xref'aм, эта ф-ция будет отсутствовать.

На логичный вопрос - зачем она тогда нужна в коде, если ее никто не вызывает? Ответ прост - раз есть, то скорее всего кто-то вызывает( или в микрософте любят компилировать с выключенной оптимизацией =] ), и этот кто-то - просто другой модуль ( CRT ) и делается это динамически:

notepad зовет _initterm, который находится в рантаймовской либе msvcrt.dll:

msvcrt!_initterm+0xb:
77c39d72 8b06            mov     eax,dword ptr [esi]
77c39d74 85c0            test    eax,eax
77c39d76 7402            je      msvcrt!_initterm+0x13 (77c39d7a)
77c39d78 ffd0            call    eax <= вызов нашей ф-ции (0x010070D4)

Можно конечно эвристически определить, что по такому-то адресу находится код, на который никто не ссылается, собс-но так IDA и поступает, но, если представить на секунду, что код может быть в принципе каким угодно ( обфусцированным, зашифрованным и т.д.), то эвристика не поможет, а ссылок нa него нет - итог: fail.

В принципе это все достаточно очевидно, но видимо не для всех(надеюсь данный пример поможет).

среда, 11 мая 2011 г.

Еще немного про Image Loader

Стало любопытно, какой процент от всех функций ntdll.dll составляет код загрузчика pe файлов, в результате появилась такая статистика( xp sp2 ):

Общее число ф-ций ntdll.dll(включая Zw/Nt) - 2138
Число Zw/Nt ф-ций - 283

Общее число ф-ций используемых лоадером(включая Zw/Nt) - 602
Число Zw/Nt ф-ций используемых лоадером - 54

То есть лоадер в своей работе использует чуть меньше трети всех ф-ций ntdll.dll.

Список Nt/Zw ф-ций используемых лоадером( xp sp2/sp3 ):

NtTestAlert
ZwDelayExecution
NtAllocateVirtualMemory
NtQueryPerformanceCounter
NtSetEventBoostPriority
NtReleaseKeyedEvent
NtCreateEvent
NtClose
NtFreeVirtualMemory
NtQueryVirtualMemory
NtQuerySystemTime
NtQuerySystemInformation
ZwQueryInformationProcess
ZwProtectVirtualMemory
NtReleaseSemaphore
NtOpenKeyedEvent
ZwQueryInformationFile
ZwQueryEaFile
NtCreateFile
ZwSetInformationFile
NtFsControlFile
NtQueryVolumeInformationFile
NtOpenFile
ZwQueryAttributesFile
ZwOpenDirectoryObject
ZwDeviceIoControlFile
ZwWaitForSingleObject
ZwRaiseException
NtQueryDebugFilterState
NtWaitForKeyedEvent
NtOpenSymbolicLinkObject
ZwQuerySymbolicLinkObject
ZwSetInformationProcess
NtUnmapViewOfSection
NtCreateSection
ZwMapViewOfSection
ZwAreMappedFilesTheSame
ZwRaiseHardError
NtOpenSection
ZwQuerySection
ZwOpenProcessToken
ZwQueryInformationToken
ZwOpenKey
ZwQueryValueKey
NtOpenThreadTokenEx
ZwOpenProcessTokenEx
ZwTerminateThread
ZwTerminateProcess
ZwFlushInstructionCache
NtQueryInstallUILanguage
NtQueryDefaultLocale
ZwQueryDefaultUILanguage
ZwSetValueKey
ZwCreateKey


В последующих версиях windows число сервисных ф-ций используемых лоадером примерно остается на одном уровне (vista sp0: 50, win7 sp0: 53)

Пример цепочки вызовов приводящей к ZwDeviceIoControlFile:
PE Loader => RtlAllocateHeap => AcquireBufferLocation => GetPidInfo => GetLoggerInfo => WmipSendWmiKMRequest => WmipDeviceIoControl => ZwDeviceIoControlFile

четверг, 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 будет выглядеть одинаково и её легко можно найти по сигнатуре.

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

четверг, 31 марта 2011 г.

История одного бага

Баг был простой - на машине тестера все зависало, при воспроизведении же на моей машине вместо зависания я увидел BSOD:

TERMINAL_SERVER_DRIVER_MADE_INCORRECT_MEMORY_REFERENCE (cf)
Arguments:
Arg1: 8c8d715c, memory referenced
Arg2: 00000008, value 0 = read operation, 1 = write operation
Arg3: 8c8d715c, If non-zero, the instruction address which referenced the bad memoryaddress.
Arg4: 00000002, Mm internal code.
    A driver has been incorrectly ported to Terminal Server. It is referencing session space addresses from the system process context. 
    Probably from queueing an item to a system worker thread. The broken driver's name is displayed on the screen.

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

WRITE_ADDRESS:  8c8d715c

FAULTING_IP:
win32k!NtGdiFlushUserBatch+0
8c8d715c ??              ???

IMAGE_NAME:  win32k.sys
...
STACK_TEXT: 
8570b674 818d873f 00000003 85700d84 00000000 nt!RtlpBreakWithStatusInstruction
8570b6c4 818d91ac 00000003 c04646b8 00000000 nt!KiBugCheckDebugBreak+0x1c
8570ba70 818a9ef2 00000050 8c8d715c 00000008 nt!KeBugCheck2+0x5f4
8570bae8 8188fa74 00000008 8c8d715c 00000000 nt!MmAccessFault+0x106
8570bae8 8c8d715c 00000008 8c8d715c 00000000 nt!KiTrap0E+0xdc
8570bb70 8188c90c c000000d 83925000 00000120 win32k!NtGdiFlushUserBatch
8570bb8c 8187f751 badb0d00 8570bc04 82004398 nt!KiFastCallEntry+0xcc
8570bbfc 81a7755e 80000150 00000000 00000000 nt!ZwWriteFile+0x11
8570bc4c 81a7721b 83925000 83925000 00000001 nt!EtwpRealtimeSaveBuffer+0x113
8570bc60 81a70ccd 83925000 00000000 83925000 nt!EtwpFlushBufferToRealtime+0x54
8570bc84 81a7105f 00000000 00000000 82f84648 nt!EtwpFlushBuffer+0xa2
8570bd3c 81a70a49 00000001 00000000 82f84398 nt!EtwpFlushActiveBuffers+0x25d
8570bd7c 81a254a8 82f84648 85700680 00000000 nt!EtwpLogger+0x233
8570bdc0 8189145e 81a70816 82f84648 00000000 nt!PspSystemThreadStartup+0x9d
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16

Первой мыслью было посмотреть, какой же логгер ответственнен за эти вызовы.

В ядре есть массив этих самых логгеров:

kd> dds WmipLoggerContext
818ff800  00000000
818ff804  00000000
818ff808  839960c8
818ff80c  82f84648 <========= а вот и виновник торжества
818ff810  82b43908
818ff814  82b43388
818ff818  82fb5008
818ff81c  82fe9a48
...
Каждый логгер представлен структурой WMI_LOGGER_CONTEXT.
Чтобы посмотреть имя:

kd> dt nt!_WMI_LOGGER_CONTEXT 82f84648 LoggerName
   +0x030 LoggerName : _UNICODE_STRING "Eventlog-Security"

Информации было достаточно для того, чтобы поискать проблему в google:
Eventlog-Security+win32k.sys+TERMINAL_SERVER_DRIVER_MADE_INCORRECT_MEMORY_REFERENCE, но поиск ничего не дал.

Вернувшись к проблемному адресу:
WRITE_ADDRESS:  8c8d715c  

Видно, что тот невалиден:

kd> u 8c8d715c
win32k!NtGdiFlushUserBatch:
8c8d715c ??              ???

Далее была мысль, что smss.exe не успел стартовать и загрузить win32k.sys, но в списке загруженных модулей тот присутствовал:

kd> lm
start    end        module name
...    
8b835000 8b850000   luafv      (pdb symbols)          c:\symbols\luafv.pdb\C177291432194CC8A5D6B9E0834207602\luafv.pdb
8c800000 8c9ff000   win32k     (pdb symbols)          c:\symbols\win32k.pdb\593611FD42154641A8CC37E05A5F704D2\win32k.pdb
95000000 95009000   TSDDD      (pdb symbols)          c:\symbols\TSddd.pdb\C75665DBDE5247248B6C0D9389C4D3261\TSddd.pdb
...

И уже после этого я вспомнил, что win32k.sys грузится в сессионное адресное пространство, проверим, установив контекст сессии:

kd> !session -s 0

После чего он стал валидным:
kd> u 8c8d715c
win32k!NtGdiFlushUserBatch:
8c8d715c 6838010000      push    138h
8c8d7161 6860739c8c      push    offset win32k!__safe_se_handler_table+0x50d8 (8c9c7360)
8c8d7166 e839100000      call    win32k!_SEH_prolog4 (8c8d81a4)

То есть, логгер "Eventlog-Security" при наступлении какого-то события писал в real-time режиме в файл, и дальше вызывал код из win32k.sys который был недоступен из системного контекста, т.к. располагался в АП сессии.

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

Сперва была мысль о коллауте, который вызывался из менеджера системных сервисов:

KiSystemServiceAccessTeb:
        or      ebx, [ecx]+TbGdiBatchCount ; may cause an inpage exception
        jz      short Kss40             ; if z, no batched calls
        push    edx                     ; save address of user arguments
        push    eax                     ; save service number
        call    [_KeGdiFlushUserBatch]  ; flush GDI user batch

Однако бряк поставленный сюда не сработал, да и непонятно с чего бы системному потоку быть gui-потоком.

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

Таким образом нашлась и причина падения - некорректные id у ф-ций NtCreateThread и NtWriteProcessMemory для vista sp0, которые я взял из таблицы в инете(http://www.metasploit.com/users/opcode/syscalls.html), т.к. на тот момент не было возможности проверить на живой машине.

Т.е. принцип "недоверяй ничему, пока сам не проверишь" в ядерном программировании с использованием всяких недокументированных вещей справедлив как никогда =].