Article: Жизнь после смерти Author: Great ICQ #: 893-894 Date: 14.02.2007 Lang: C/C++ kernel & user mode Note: Эта тема затрагивалась уже многими авторами. Как пережить BSoD и не уронить систему? Предлагались варианты поставить загрушки вида RET или JMP $ на KeBugCheckEx. Мы снова затронем эту тему, но поподробнее. Что же происходит, например, при выполнении следующего кода в режиме ядра: XOR EAX,EAX MOV [EAX],EAX Как нетрудно догадаться, производится попытка обращения по нулевому указателю. Первые 10000 байт как раз зарезервированы для отлова таких ошибок и памяти там быть не может. Результат очевиден - Page Fault, за которым следует сразу BSoD. Но зачем рушить всю систему, если один драйвер что-то сделал не так? С одной стороны, ход мыслей разработчиков ОС вполне очевиден. Они ориентируются на худшее, ведь если ошибка будет в драйвере файловой системы, можно так и винт угробить. С другой стороны, ошибки в коде ОС и драйверах, с ней поставляемых, случаются довольно редко. Наиболее распространенная причина ошибок - драйвера сторонних производителей, в том и числе и тех, кто только начинает разбираться в программировании на ядерном уровне в Windows. И показ синего экрана с остановкой системы тут не лучший выход. Идеальный вариант с моей точки зрения - показ диалогового окна с ошибкой и выгрузка проблемного драйвера. До выгрузки драйвера мы замахиваться не будем, а вот заморозить выполнение проблемного кода - легко! Разберемся, что нам надо сделать, чтобы продолжить работу. 1) размаскировать все прерывания, если они были замаскированы. Это сделает вызов KeLowerIrql(PASSIVE_LEVEL). 2) в бесконечном цикле вызывать ZwYieldExecution, чтобы заморозить поток. Между этими двумя основными действиями мы сделаем дополнительные: 3) залогируем ошибку через DbgPrint() 4) сохраним параметры багчека (стоп-код и аргументы) 5) установим и сбросим заранее заготовленный объект "событие" (Event), который просигнализирует нашей юзермодной программе о том, что произошла ошибка. Она запросит у нашего драйвера параметры багчека, которые мы аккуратно сохранили в пункте 4) и выдаст месажбокс. Юзермодная часть будет делать просто ожидание объекта "событие", запрос у драйвера параметров ошибки и вывод месажбокса. Теперь перейдем подробнее к реализации. Нам потребуется реализовать маленький движок для сплайсинга кода ядра. Я накатал вот что: Code: // // Splicing routines // NTSTATUS SetKernelSplicingHook(PVOID pfnDst, PVOID pfnHook, BYTE buffer[5]) { __try { // Backup RtlCopyMemory(buffer, pfnDst, 5); // Write JMP DWORD offset = (DWORD) pfnHook - (DWORD) pfnDst - 5; *(BYTE*)pfnDst = 0xE9; // JMP FAR *(DWORD*)((DWORD)pfnDst+1) = offset; return STATUS_SUCCESS; } __except(EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } NTSTATUS UnsetKernelSplicingHook(PVOID pfnDst, BYTE buffer[5]) { __try { // Restore RtlCopyMemory(pfnDst, buffer, 5); return STATUS_SUCCESS; } __except(EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } Думаю тут ничего в комментариях не нуждается, я и так все подробно расписал в своей статье про сплайсинг. Надо еще узнать адрес, чего, собственно, сплайсить-то. Тут есть один нюанс. Реализация функций KeBugCheck* в Win2000 и WinXP разная. Windows2000: Code: KeBugCheck(code) { KeBugCheckEx(code,0,0,0,0); } KeBugCheckEx(code,par1,par2,par3,par4) { // основная работа по показу BSoD'а } Windows XP: Code: KeBugCheck(code) { KeBugCheck2(code,0,0,0,0,0); } KeBugCheckEx(code,par1,par2,par3,par4) { KeBugCheck2(code,par1,par2,par3,par4,0); } KeBugCheck2(code,par1,par2,par3,par4,unknown) { // основная работа по показу BSoD'а } В случае Win2000 всю работу выполняет функция KeBugCheckEx. В случае WinXP KeBugCheck и KeBugCheckEx - обертки для специальной неэкспортируемой функции ядра KeBugCheck2, которая и показывает синий экран. Мы будем ориентироваться на XP по причине того, что она больше сейчас распространена. Поэтому и сплайсить мы будем функцию KeBugCheck2. Нужно получить ее адрес. Вот тут облом.. она же не экспортируется ядром. Но мы найдем ссылку на нее из KeBugCheckEx. За подробностями прошу в мою статью "цветной экран смерти", а тут я только лишь приведу код вычисления адреса KeBugCheck2: Code: PVOID GetKeBugCheck2Address() { // Acquire KeBugCheck2 address DWORD addr = (DWORD) &KeBugCheckEx; addr += 0x17; // offset of CALL _KeBugCheck2@20 return (PVOID) (addr + 4 + *(DWORD*)addr); // jump to KeBugCheck2 } Все готово. Пора приступать к написанию драйвера. Сперва рассмотрим DriverEntry. Нам надо создать девайс. Мы его назовем "bugcheckeventdevice". Код создания девайса и символической ссылки весьма стандартен для не-PnP драйверов и пояснений не требует: Code: NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { NTSTATUS status; DPRINT("[~] Driver loading"); RtlInitUnicodeString(&DeviceName, L"\\Device\\bugcheckeventdevice"); RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\bugcheckeventdevice"); status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceObject); if (!NT_SUCCESS(status)) { DPRINT("[-] Failed to create first device. IoCreateDevice returned %x", status); return STATUS_UNSUCCESSFUL; } deviceObject->Flags |= DO_BUFFERED_IO; status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName); if (!NT_SUCCESS(status)) { DPRINT("[-] Failed to create first symbolic link. IoCreateSymbolicLink returned %x", status); IoDeleteDevice(deviceObject); return STATUS_UNSUCCESSFUL; } Дальше мы создаем объект "событие" под названием "BugCheckEvent" с начальным состоянием Nonsignaled. Следующий код отвечает за это: Code: RtlInitUnicodeString(&EventName, L"\\BaseNamedObjects\\BugCheckEvent"); OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa, &EventName, 0, 0, 0); status = ZwCreateEvent(&hEvent, EVENT_ALL_ACCESS, &oa, SynchronizationEvent, FALSE); if (!NT_SUCCESS(status)) { DPRINT("[-] Failed to create event. ZwCreateEvent returned %x", status); IoDeleteDevice(deviceObject); IoDeleteSymbolicLink(&SymbolicLinkName); return STATUS_UNSUCCESSFUL; } DPRINT("[+] Event created, handle is %08x", hEvent); Дальше мы устанавливаем функции диспетчеризации: Code: DriverObject->DriverUnload = DriverUnload; DriverObject->MajorFunction [IRP_MJ_CREATE] = DriverObject->MajorFunction [IRP_MJ_CLOSE ] = DriverCreateClose; DriverObject->MajorFunction [IRP_MJ_DEVICE_CONTROL ] = DriverIoControl; После чего делаем сплайсинг на KeBugCheck2 и завершаем загрузку драйвера: Code: __try { status = SetKernelSplicingHook(GetKeBugCheck2Address(), KeBugCheck2, SplicingBuffer); } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); } if(!NT_SUCCESS(status)) { DPRINT("[-] Cannot set splicing hook [%08x]", status); IoDeleteDevice(deviceObject); IoDeleteSymbolicLink(&SymbolicLinkName); ZwClose(hEvent); return STATUS_UNSUCCESSFUL; } DPRINT("[+] Driver initialization successful"); return STATUS_SUCCESS; } Для девайса определяем функцию-заглушку на пакеты IRP_MJ_CREATE/IRP_MJ_CLOSE: Code: NTSTATUS DriverCreateClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { // Dummy. Simply indicate success. Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } Теперь определим функцию для обработки IRP_MJ_DEVICE_CONTROL. Юзермодная часть будет обращаться к нам через DeviceIoControl и требовать параметры багчека. Следующий код их отдаст: Code: // IOCTL #define FILE_DEVICE_BUGCHECKDRIVER 0x00003656 #define IOCTL_BUGCHECKDRIVER_GET_BUGCHECK_INFO CTL_CODE( FILE_DEVICE_BUGCHECKDRIVER, 0x00, METHOD_BUFFERED, FILE_ANY_ACCESS) NTSTATUS DriverIoControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PIO_STACK_LOCATION pisl = IoGetCurrentIrpStackLocation(Irp); NTSTATUS status = STATUS_UNSUCCESSFUL; ULONG BuffSize = pisl->Parameters.DeviceIoControl.InputBufferLength; PUCHAR pBuff = (PUCHAR)Irp->AssociatedIrp.SystemBuffer; Irp->IoStatus.Information = 0; switch(pisl->Parameters.DeviceIoControl.IoControlCode) { case IOCTL_BUGCHECKDRIVER_GET_BUGCHECK_INFO: __try { // If bug check info present & buffer's length is greater or equal necessary length, write info to system buffer if( bBugCheckInfoPresent && pBuff && pisl->Parameters.DeviceIoControl.OutputBufferLength >= (sizeof(DWORD)*5) ) { ((DWORD*)pBuff)[0] = BugCheckCode; for(int i=0;i<4;i++) // Copy bug check parameters ((DWORD*)pBuff)[i+1] = BugCheckParameters[i]; Irp->IoStatus.Information = sizeof(DWORD)*5; // Copy 5 DWORDs to user buffer status = STATUS_SUCCESS; } } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); DPRINT("[!] IOCTL_BUGCHECKDRIVER_GET_BUGCHECK_INFO: Exception occurred: %08x", status); } break; } Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } Функция выгрузки драйвера полностью противоположна загрузке: Code: void DriverUnload(IN PDRIVER_OBJECT DriverObject) { // Delete device IoDeleteSymbolicLink (&SymbolicLinkName); if(deviceObject) IoDeleteDevice (deviceObject); // Delete event ZwClose(hEvent); // Remove hook from KeBugCheck2 NTSTATUS status; __try { status = UnsetKernelSplicingHook(GetKeBugCheck2Address(), SplicingBuffer); } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); } if(!NT_SUCCESS(status)) DPRINT("[-] Cannot unset splicing hook [%08x]", status); DPRINT ("[+] Driver unloaded"); } Удаление ссылки, устройства, события и снятие хука. Прежде, чем реализовывать новый обработчик KeBugCheck2, мы напишем функцию, которая быстро поставит и снимет сигнализированное состояние для "события" BugCheckEvent: Code: // Pulse event NTSTATUS PulseEvent() { __try { // Initialize NTSTATUS status; HANDLE NewHandle; OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa, &EventName, 0, 0, 0); // Open status = ZwOpenEvent(&NewHandle, EVENT_MODIFY_STATE, &oa); if(!NT_SUCCESS(status)) return status; // Pulse DPRINT("[~] Trying to pulse event %08x", NewHandle); status = ZwPulseEvent(NewHandle, 0); if(!NT_SUCCESS(status)) return status; // Close ZwClose(NewHandle); return STATUS_SUCCESS; } __except(EXCEPTION_EXECUTE_HANDLER) { // Exception occurred return GetExceptionCode(); } } Тут все просто. Теперь самое главное - новый обработчик KeBugCheck2: Code: BOOLEAN bBugCheckInfoPresent = 0; DWORD BugCheckCode, BugCheckParameters[4]; BYTE SplicingBuffer[5]; void NTAPI KeBugCheck2( IN DWORD iBugCheckCode, IN DWORD iBugCheckParameter1, IN DWORD iBugCheckParameter2, IN DWORD iBugCheckParameter3, IN DWORD iBugCheckParameter4, IN DWORD iUnknown) { // // Lower IRQL to PASSIVE_LEVEL // KIRQL Irql = KeGetCurrentIrql(); if(Irql > PASSIVE_LEVEL) KeLowerIrql(PASSIVE_LEVEL); // Log error DPRINT("[*] KeBugCheck2 call - FATAL SYSTEM ERROR. BugCheck code %08x", iBugCheckCode); // // Write bug check info // __try { BugCheckCode = iBugCheckCode; BugCheckParameters[0] = iBugCheckParameter1; BugCheckParameters[1] = iBugCheckParameter2; BugCheckParameters[2] = iBugCheckParameter3; BugCheckParameters[3] = iBugCheckParameter4; bBugCheckInfoPresent = TRUE; } __except(EXCEPTION_EXECUTE_HANDLER) { DPRINT("[!] KeBugCheck2: Exception occurred while saving bug check parameters: %08x", GetExceptionCode()); } // // Pulse event, user mode process will wake up and show dialog box // PulseEvent(); // // Hang current thread // while(1) ZwYieldExecution(); } Все, как мы и распланировали. Ядерная часть нашей системы готова. Драйвер можно собрать и, вообще говоря, загрузить. По логам DbgPrint'а можно убедиться, что все идет нормально. Примемся за пользовательскую часть. Опишем функцию, которая будет ждать событие по его имени: Code: bool WaitForEvent(char* name) { HANDLE hEvent; DWORD error; do { hEvent = OpenEvent(SYNCHRONIZE, FALSE, name); error = GetLastError(); Sleep(1); } while(!hEvent && error == ERROR_FILE_NOT_FOUND); if(!hEvent) { return false; } WaitForSingleObject(hEvent, INFINITE); CloseHandle(hEvent); return true; } Ею мы будем пользоваться. Мы будем в цикле ждать наступления события и, когда оно наступит, выполнять нужные нам действия: Code: int APIENTRY WinMain(HINSTANCE,HINSTANCE,LPSTR,int) { while(1) { if(!WaitForEvent("BugCheckEvent")) return printf("[!] Failed\n"); EventSet(); } return 0; } Все действия будет выполнять функция EventSet(). Она запросит у драйвера BugCheck-код и аргументы и покажет месажбокс: Code: // IOCTL #define FILE_DEVICE_BUGCHECKDRIVER 0x00003656 #define IOCTL_BUGCHECKDRIVER_GET_BUGCHECK_INFO CTL_CODE( FILE_DEVICE_BUGCHECKDRIVER, 0x00, METHOD_BUFFERED, FILE_ANY_ACCESS) void EventSet() { struct { DWORD BugCheckCode; DWORD BugCheckParameters[4]; } BugCheckInfo = {0}; BOOL res = 0; HANDLE hFile = CreateFile("\\\\.\\bugcheckeventdevice", GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); DWORD ret; if(hFile != INVALID_HANDLE_VALUE) { res = DeviceIoControl(hFile, IOCTL_BUGCHECKDRIVER_GET_BUGCHECK_INFO, 0, 0, &BugCheckInfo, sizeof(BugCheckInfo), &ret, 0); CloseHandle(hFile); } char buffer[1024]; if(res) sprintf(buffer, "Warning!! The fatal kernel mode error occurred at address 0x%08x\n" "\n" "BugCheck code: %08x\n" "Parameters:\n" " BugCheckParameter1: 0x%08x\n" " BugCheckParameter2: 0x%08x\n" " BugCheckParameter3: 0x%08x\n" " BugCheckParameter4: 0x%08x\n" "\n" "It is HIGHLY RECOMMENDED to reboot as soon as possible, because if you\n" " continue working, it may cause unrecoverable errors or simply system hang.\n", 0, BugCheckInfo.BugCheckCode, BugCheckInfo.BugCheckParameters[0], BugCheckInfo.BugCheckParameters[1], BugCheckInfo.BugCheckParameters[2], BugCheckInfo.BugCheckParameters[3] ); else sprintf(buffer, "Warning!! The fatal kernel mode error occurred at address 0x%08x\n" "\n" "Unfortunately, it was unable to get bugcheck information.\n" "Additional information is not available.\n" "\n" "It is HIGHLY RECOMMENDED to reboot as soon as possible, because if you\n" " continue working, it may cause unrecoverable errors or simply system hang.\n", 0, BugCheckInfo.BugCheckCode, BugCheckInfo.BugCheckParameters[0], BugCheckInfo.BugCheckParameters[1], BugCheckInfo.BugCheckParameters[2], BugCheckInfo.BugCheckParameters[3] ); MessageBox(0, buffer, "Fatal system error", MB_ICONERROR|MB_OK); } Вот и все. Программу можно собрать и запустить. Она спокойно и мирно будет ждать наступления события, а пока оно не наступит, она не будет жрать процессорное время, потому что я написал вызов Sleep(1). Теперь проверка. Попробуем вызвать KeBugCheckEx(1,2,3,4,5). Результат не заставит себя ждать: Все ништяк. Теперь "боевая" проверка, попробуем сделать ошибку страницы при высоком IRQL: Как видно, нас спасли от краха. Если бы не наш драйвер, уже давно бы красовался синий экран IRQL_NOT_LESS_OR_EQUAL. Теперь несколько замечаний. Во-первых, система еще очень сырая и требует доработки и тщательной наладки. Было бы неплохо заставить драйвер определять и причину сбоя. Пока что сбойный адрес я смело забиваю нулями. Во-вторых, это далеко не панацея от всех бед. Можно так продержаться от 3-4 до 6 раз, дальше система начнет тормозить из-за того, что мы не выгружаем проблемные драйвера, а просто их "вешаем". Было бы кстати реализовать выгрузку проблемного драйвера из памяти вместо входа в бесконечный цикл. В-третьих, я это писал для разработчиков драйверов с целью, чтобы можно было посмотреть на свою ошибку, сохранить все данные и ребутнуться. Продолжать работу после "краха" системы (который не произошел все же) не желательно по причинам, описанным мной в самом начале статьи. Еще было бы неплохо скидывать крешдамп так же, как это делает винда, для последующего анализа. Использовать "родной" IoWriteCrashDump тут не получится, потому что винда записывает крешдамп аккурат в сектора, занимаемые страничным файлом, чтобы не дергать лишний раз драйвер ФС, а при загрузке потом она уже извлекает из страничного файла дамп. Нас это не совсем устраивает и нужно реализовать нормальный сброс крешдампа сразу в обычный файл. Но все равно, это лучше, чем ничего, то есть, чем простое зависание системы с синим экраном. Это позволяет, по крайней мере, выяснить причину ошибки и корректно завершить работу. Если что-то не понятно, спрашивай. Как всегда, сорсы можно забрать у меня на сайте: http://cribble.by.ru/bugcheck/ На последок хочу выразить отдельную благодартность W[4Fh]LF за ответы на вопросы по поводу создания событий в режиме ядра =) Респект те, чувак Удачи, пока!
протев, кк об этом писал =/ поставить сайс и не ипаццо... и ещё, не обижайся плз, но ф-ции GetKeBugCheck2Address и SetKernelSplicingHook это ЛОЛ =))
Поставить RET? Если будет каскад исключений, все повиснет. Поставить JMP $? Повиснет сразу же. Не у всех он ставится ) кучу проверок будет ставить не красиво Вообще я собирался сделать switch() на *NtBuildNumber и для вин2000 ставить хук на KeBugCheckEx сразу, а для XP вычислять офсет. Ну а вообще я еще в начале написал, что драйвер только для XP пока что.
писать нестабильный код тоже некрасиво))) NtBuildNumber-ом дело не ограничится, т.к. в комплект поставки дистриба входят несколько ядер, которые выбираются в зависимости от аппаратной платформы (например, у меня на ноуте дефолтно ставится ядро с PAE) плюс билды ядра меняются в сервиспаках или даже в отдельных патчах
я кое-че добавил: - теперь херачит на Windows 2000 и Windows XP - можно выбрать - продолжить работу или все-таки свалить систему. (пока что просто выдается сохраненный багчек с параметрами, было бы неплохо еще и скопировать стек, чтобы получившийся крешдамп можно было анализировать) - добавил, наконец, отключение WP - все же вставил проверку на всякий случай, как посоветовал Cr4sh %) Надо бы еще добавить: - определение сбойного драйвера - выгрузку сбойного драйвера - если юзер решил все же свалить систему, сымитировать ошибочный контекст, чтобы получился читабельный крешдамп - "живую" генерацию крешдампа работающей системы (это вообще можно отдельной прогой +)) сцылко: http://gr8.cih.ms/bugcheck2/
Воообщем-то я фигню сморозил насчет while(1) ZwYieldExecution(); Это все равно тормозит поток, правда, не так сильно, как while(1);, но все же. Лучший вариант: Code: // // Hang current thread // LARGE_INTEGER i; i.QuadPart = -9223372036854775807; KeDelayExecutionThread(KernelMode, FALSE, &i); Планировщик не передаст управление нашему потоку ближайший 701 миллион лет. Надеюсь, я не доживу )