среда, 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), т.к. на тот момент не было возможности проверить на живой машине.

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

понедельник, 28 марта 2011 г.

Еще один способ детекта скрытых процессов и потоков

Давным-давно появились штуки типа DKOM, позволяющие скрывать потоки/процессы/(да и другие объекты ядра) от посторонних глаз, через удаление их из списков ОС( PsActiveProcessLinks/SessionProcessLinks и т.д. ).

Но так получилось, что теже самые потоки/процессы удаленные из одних мест, вполне себе продолжали жить в других(как списках, так и модулях).
Яркий пример подобного можно было наблюдать в 2008м, когда EP_X0FF опубликовал детект процессов(CsrWalker) через перечисление их в сервере подсистемы вин32(csrss).

Однако скрыв потоки/процессы и в этом месте нужно не забыть скрыть их и в третьем.
Третье место находится в ядре графической подсистемы - в win32k.sys.

Каким образом в этом списке оказываются процессы и потоки?

При конвертации потока в gui-поток, то есть тогда, когда поток делает свой первый вызов из shadow sdt.

Конвертация осуществляется ф-цией PsConvertToGuiThread, которая создает новый, больший по размеру ядерный стек для потока(для механизма рекурсивных вызовов шадоу сервисов) и налету заменяет его, устанавливает у потока поле ServiceTable, а также вызывает 2 коллаута:

typedef NTSTATUS (*PKWIN32_PROCESS_CALLOUT)( IN PEPROCESS Process, IN BOOLEAN Initialize );
typedef NTSTATUS (*PKWIN32_THREAD_CALLOUT)( IN PETHREAD Thread, IN PSW32THREADCALLOUTTYPE CalloutType );

extern PKWIN32_PROCESS_CALLOUT PspW32ProcessCallout;
extern PKWIN32_THREAD_CALLOUT  PspW32ThreadCallout;

Которые и добавляют потоки в списки win32k.sys.

Что такое коллаут и с чем его едят?

При создании сессии smss.exe загружает ядро графической подсистемы win32k.sys через полудокументированный вызов: 

NtSetSystemInformation( SystemLoadAndCallImage, ... ), который маппит win32k.sys в сессинное пространство и вызывает точку входа драйвера.

А в точке входа вызывается ф-ция:

PsEstablishWin32Callouts( W32pProcessCallout,
                              W32pThreadCallout,
                              UserGlobalAtomTableCallout,
                              UserPowerEventCallout,
                              UserPowerStateCallout,
                              UserJobCallout,
                              (PVOID)NtGdiFlushUserBatch);

Которая инициализирует указатели валидными адресами ф-ций.
То есть коллаут это просто указатель который будет проинициализирован при загрузке win32k.sys.

Таким образом при конвертации потока в гуишный вызовется соответствующие коллауты для потока и процесса.
Внутри которых будут выделены новые entry принадлежащие win32k.sys и представленные структурами PROCESSINFO и THREADINFO.
Данные коллауты также вызываются и при уничтожении потока, чтобы соответственно отлинковаться из списков.

PPROCESSINFO gppiList;     // глобальный список процессов
ULONG gdwGuiThreads;       // счетчик гуишных потоков, апдейтится каждый раз при создании нового гуишного треда

Добравшись до процесса можно получить список его потоков:

typedef struct tagPROCESSINFO
{
    W32PROCESS;
    PTHREADINFO     ptiList;
...
}PROCESSINFO;

Ну и соот-но для любого потока есть указатель на структуру процесса:

typedef struct tagTHREADINFO
{
    W32THREAD;
    PTL             ptl;
    PPROCESSINFO    ppi;
...
}THREADINFO;

Кроме того, в данных структурах много интересной информации, такой как очереди сообщений(input/post), Integrity Level (vista+, т.к. UIPI появилась с висты), инфа о win station и десктопе и т.д.

Таким образом, недостаточно хорошо спрятанный процесс/поток может быть найден через перечисление процессов и их потоков в ядре графической подсистемы.
Ограничение: НЕ-gui потоки не будут найдены.

вторник, 15 марта 2011 г.

Легальный способ уронить windows в BSOD из юзермода


Ага, есть и такой. Правда нужны привелегии администратора.

Фича связана с аудитом, в реестре есть параметр:
HKLM\SYSTEM\CurrentControlSet\Control\Lsa\CrashOnAuditFail(http://technet.microsoft.com/en-us/library/cc963220.aspx), который вызовет BSOD при переполнении журнала аудита.

Как воспроизвести:

1) HKLM\SYSTEM\CurrentControlSet\Control\Lsa\CrashOnAuditFail устанавливаем в 1 (The feature is on. The system halts when it cannot record an event in the Security Log.)

2) restart ( чтобы изменения вступили в силу )

2) ставим для журнала аудита минимальный размер ( 64 кб ), чтобы он переполнился поскорее

3) включаем аудит на создание и удаление процессов

4) выполняем нечто вроде:

for (i; i < MAX_PROCESS_COUNT; i++)
        WinExec("C:\\WINDOWS\\NOTEPAD.EXE", SW_SHOW );

чтобы заполнить журнал аудита

5) как только журнал заполнится система сгенерит BSOD с багчеком 0xc0000244.

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

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

Если посмотреть стек вызовов на момент бсода, то:

kd> k
ChildEBP RetAddr 
f7a34900 804f7bad nt!RtlpBreakWithStatusInstruction
f7a3494c 804f879a nt!KiBugCheckDebugBreak+0x19
f7a34d2c 804f8cc5 nt!KeBugCheck2+0x574
f7a34d4c 80646c0b nt!KeBugCheckEx+0x1b
f7a34d74 80534c12 nt!PopGracefulShutdown+0x15d
f7a34dac 805c621c nt!ExpWorkerThread+0x100
f7a34ddc 80541de2 nt!PspSystemThreadStartup+0x34
00000000 00000000 nt!KiThreadStartup+0x16

Можно увидеть, что код выполняется в рабочем потоке, т.е. не в контексте вызывающего.
А вот если бы бсод случался в контексте вызывающего, то через перехват соответствующих ф-ций (любым способом) генерирующих багчек, и через откат состояний можно было бы возвращать управление на саму проверку аудита. Таким образом можно было бы перехватить кучу действий при включенном аудите (http://technet.microsoft.com/ru-ru/library/cc779526%28WS.10%29.aspx), причем обнаружить подобный перехват было бы нелегко.
Но увы и ах, не судьба =]