Безопасная установка перехвата на многопроцессорной системе

Тема в разделе "WASM.NT.KERNEL", создана пользователем ers, 23 май 2010.

  1. ers

    ers New Member

    Публикаций:
    0
    Регистрация:
    21 май 2010
    Сообщения:
    3
    Меня интересуют методы безопасной и надежной установки перехвата функций режима ядра, используемые в современном защитном ПО (антивирусы, файрволлы) с точки зрения
    * Правильной работы на многопроцессорных системах
    * Сложности обхода (возможности вызова исходной функции)

    Для синхронизации при установке и снятии перехвата я посылаю DPC на все процессоры, кроме текущего, с использованием KeSetTargetProcessorDpc, и жду в цикле сигнала (посредством обращения к разделяемой переменной) от всех DPC о том, что соответствующие процессоры переведены на высокий уровень IRQL. Далее устанавливаю перехват как на однопроцессорной системе и изменяю разделяемую переменную, давая понять остальным процессорам, что можно переходить на исходный IRQL. При этом у меня есть несколько вопросов:

    1. Можно ли вызывать оригинальную функцию на повышенном IRQL, как это делается в статье Ms-Rem'a? Ведь большенство функций рассчитано на на уровень PASSIVE_LEVEL и работает с указателями на память пользовательского режима...
    2. Если так делать нельзя, то при восстановлении оригинального заголовка функции и понижении IRQL до исходного уровня, другой поток может вызвать оригинальную функцию без перехвата - как предотвратить такую возможность?
    3. Исходя из предыдущих вопросов - какой способ перехвата надежнее в плане защищенности от обхода из пользовательского режима - сплайсинг, или запись в SDT?
    4. Стоит ли кроме этого пытаться защититься от снятия перехватов из режима ядра (RootkitUnhooker и др.), или современный защитный софт ограничивается только контролем загрузки новых драйверов ввиду нецелесообразности борьбы с угрозами из ядра?
    5. Есть ли разница между обнулением IF инструкцией cli и повышением IRQL до HIGH_LEVEL? Стоит ли ограничиваться уровнем (IPI_LEVEL - 1) для сохранения возможности межпроцессорного взаимодействия, учитывая, что исходные DPC уже доставлены всем процессорам?

    Заранее благодарю за помощь!
     
  2. slesh

    slesh New Member

    Публикаций:
    0
    Регистрация:
    6 фев 2009
    Сообщения:
    214
    Сплайсинг всегда надежнее, потому что спалить чуть труднее чем хук в SDT. Да и к тому же при сплайсинге вызов оригинальной функции тебе не поможет для обхода. Но сплайсинг намного труднее реализовать чем хук в SDT. Большинство антивирей и фаеров ставят хуки именно в SDT
    (запусти gmer и сразу увидишь)

    Я чтобы ставить хук в SDT юзаю такой вот код:
    Код (Text):
    1. // функция установки и снятия перехвата
    2. BOOL SetHook(USHORT id, ULONG NewAddr, PULONG OldAddr)
    3. {
    4.     BOOL ret = false;
    5.     ULONG Addr;
    6.     PHYSICAL_ADDRESS PhysicalAddr;
    7.     ULONG VirtualAddr;
    8.    
    9.     Addr = (ULONG)KeServiceDescriptorTable->ServiceTable; // получаем адрес таблицы сервисов
    10.     if (Addr) // если нет ошибки
    11.     {
    12.         Addr += 4 * id; // вычислим оффсет адреса функции которую необходимо перехватить
    13.         if (OldAddr) // если указана память для сохранения старого значения
    14.         {
    15.             *(ULONG*)OldAddr = *(ULONG*)Addr; // сохраним старый адрес
    16.         }
    17.        
    18.         // получим физический адрес, зная виртуальный
    19.         PhysicalAddr = MmGetPhysicalAddress((void*)Addr);
    20.         if (PhysicalAddr.QuadPart)
    21.         {
    22.             // выделим себе виртуальную память по физ. адресу
    23.             VirtualAddr = (ULONG)MmMapIoSpace(PhysicalAddr, 4, 0);
    24.             if (VirtualAddr)
    25.             {
    26.                 *(ULONG*)VirtualAddr = NewAddr; // установим перехват
    27.                 ret = true;
    28.                 MmUnmapIoSpace((void*)VirtualAddr, 4); // освободим вирт. страницу памяти
    29.             }
    30.         }
    31.     }
    32.    
    33.     return ret;
    34. }
    Таким вот образом ты убиваешь сразу двух зайцев: обходишь защиту памяти и нет нужны в синхронизации при работе в многопроцессорной системе.
     
  3. ant_man

    ant_man New Member

    Публикаций:
    0
    Регистрация:
    16 дек 2004
    Сообщения:
    23
    повышение IRQL, не гарантирует безпроблемного сплайсинга, т.к. остается вероятность существования замороженного потока как раз в промежутке заменяемых команд. Запись в SDT такой проблемы лишена из-за атомарности доступа к адресу функции в памяти.

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

    В современных 64 битных виндовсах МС запретила и сплайс и патч SDT под страхом BSOD'a (см. PatchGuard), надо использовать сервисы перехвата предоставляемые ОС.
     
  4. Clerk

    Clerk Забанен

    Публикаций:
    0
    Регистрация:
    4 янв 2008
    Сообщения:
    6.689
    Адрес:
    РБ, Могилёв
    KeIpiGenericCall().
     
  5. ers

    ers New Member

    Публикаций:
    0
    Регистрация:
    21 май 2010
    Сообщения:
    3
    Спасибо за помощь, с синхронизацией и сплайсингом разобрался, сейчас возникла новая проблема, возможно связанная с использованием функций-трамплинов. В памяти при установленном перехвате все выглядит вот так, правильность генерации Trampoline функции проверял отладчиком:
    Код (Text):
    1. // пример для NtOpenProcess
    2.  
    3. NTSTATUS NewNtOpenProcess(...)
    4. {
    5.   // some code
    6.   return TrampolineNtOpenProcess(...)
    7.   /* в дизассемблере:
    8.   push ... ; параметры
    9.   call TrampolineNtOpenProcess
    10.   pop ebp
    11.   ret 2Ch ; <-- ! BSOD !
    12.   */
    13. }
    14.  
    15. __declspec(naked) NTSTATUS TrampolineNtOpenProcess(...)
    16. {
    17.   nop nop ... // сюда копируются первые "N" байт исходной функции
    18.   nop nop ... // сюда записывается переход на "хвост" оригинала: push offset NtOpenProcess + N , ret
    19. }
    20.  
    21. NTSTATUS NtOpenProcess(...)
    22. {
    23.   // измененный заголовок
    24.   push offset NewNtOpenProcess
    25.   ret
    26.   .... // продолжение
    27. }
    Вот код установки перехвата, при необходимости могу выложить остальное:
    Код (Text):
    1. // ProcAddress - NtOpenProcess, HookAddress - NewNtOpenProcess, TrampolineAddress - TrampolineNtOpenProcess, HeaderBuffer - первые 6 байт оригинала
    2. void HookFunction(PVOID ProcAddress, PVOID HookAddress, PVOID TrampolineAddress, PVOID HeaderBuffer)
    3. {
    4.     ULONG OldCR0;
    5.     KIRQL OldIrql;
    6.     JUMP_FAR jmp_to_hook, jmp_to_original;
    7.     ULONG TrampolineHeaderSize;
    8.     ULONG InstructionSize;
    9.     PUCHAR InstructionPtr;
    10.     BOOLEAN LdeStatus;
    11.  
    12.     KdPrint((DRIVERNAME " - HookFunction, ProcAddress = %lx\n", (ULONG)ProcAddress));
    13.    
    14.     // инициализация структур дальнего переходa следующего вида:
    15.     // push address; ret;
    16.     jmp_to_hook.pushop = jmp_to_original.pushop = 0x68;
    17.     jmp_to_hook.retop = jmp_to_original.retop = 0xc3;
    18.     jmp_to_hook.address = HookAddress;
    19.  
    20.     // запрет прерываний, сброс бита WP регистра CR0
    21.     LockAllProcessors(&OldIrql);
    22.     __asm
    23.     {
    24.         mov eax, cr0
    25.         mov OldCR0, eax
    26.         and eax,0xFFFEFFFF    
    27.         mov cr0, eax
    28.     }
    29.     //
    30.         // вычисление размера сохраняемого заголовка для функции-трамплина
    31.         // с использованием дизассемблера длин инструкций Lde
    32.         TrampolineHeaderSize = 0;
    33.         InstructionPtr = (PUCHAR)ProcAddress;
    34.         do
    35.         {
    36.             LdeStatus = Lde(InstructionPtr, &InstructionSize);
    37.             ASSERT(LdeStatus != FALSE);
    38.             InstructionPtr += InstructionSize;
    39.             TrampolineHeaderSize += InstructionSize;
    40.         }
    41.         while(TrampolineHeaderSize < sizeof(JUMP_FAR));
    42.  
    43.         // копирование кода в Trampoline-функцию - заголовок + переход на оригинал
    44.         jmp_to_original.address = (PVOID)((ULONG)ProcAddress + TrampolineHeaderSize);
    45.         RtlCopyMemory(TrampolineAddress, ProcAddress, TrampolineHeaderSize);
    46.         RtlCopyMemory((PVOID)((ULONG)TrampolineAddress + TrampolineHeaderSize), &jmp_to_original, sizeof(JUMP_FAR));
    47.  
    48.         // сохранение исходного заголовка функции
    49.         RtlCopyMemory(HeaderBuffer, ProcAddress, sizeof(JUMP_FAR));
    50.  
    51.         // запись структуры перехода на место заголовка
    52.         RtlCopyMemory(ProcAddress, &jmp_to_hook, sizeof(JUMP_FAR));
    53.         //
    54.     // разрешение прерываний, восстановление регистра CR0
    55.     __asm
    56.     {
    57.         mov eax, OldCR0    
    58.         mov cr0, eax                          
    59.     }
    60.     UnlockAllProcessors(OldIrql);
    61. }
    Для определения "N" пришлось использовать дизассемблер длин инструкций, так как, к примеру, в функции могут идти подряд две пятибайтовые команды, тогда в трамплин должны записываться обе инструкции (то есть 10 байт, а не 6) и осуществляться переход на третью инструкцию оригинала.

    Проблема заключается в том, что при выходе из функции перехвата (в данном примере NewNtOpenProcess) при определенных условиях случается BSOD, цепочка вызовов выглядит таким образом: NewNtOpenProcess -> TrampolineNtOpenProcess -> NtOpenProcess -> NewNtOpenProcess(возврат из NtOpenProcess) -> 0x0000000(возврат из NewNtOpenProcess) -> BSOD. То есть при выполнении команды ret функции перехвата возврат осуществляется не на вызвавшую ее функцию, а на нулевой байт памяти - это может быть связано с повреждением стека в каком-то месте в данной цепочке вызовов?

    Еще одна странность - поведение драйвера почему-то зависит от количества и типа перехватываемых функций. При перехвате NtOpenFile и NtCreateFile возникает описанный BSOD; при перехвате NtOpenProcess и NtCreateFile вновь запускаемые программы отображаются в "упрощенным интерфейсе" как в Windows 2000 / 9x (NtOpenProcess не блокирует открытие процессов, просто ставится перехват и передается управление оригиналу); при перехвате только одной из функций все работает нормально... Подскажите пожалуйста, в чем может быть проблема и такое странное поведение, я уже устал с бубном плясать :) Спасибо!