Особенности хуков SSDT
По-моему нет ничего более банального, чем хучить API ядра через патч таблицы сервисов. Все это делали 100 раз, и вот на 101 раз выясняется, что есть особенности.
Захучили мы все API SDDT одним и тем же переходником, который просто логирует вызовы. Т.е. ничего не делает, кроме как логирует аргументы и передает их в оригинальную функцию. Все замечательно, работает. А потом вдруг бац и падает в глубинах NtReadVirtualMemory в Exception:
WARNING: Continuing a non-continuable exception Access violation - code c0000005 (!!! second chance !!!) nt!MmProbeAndLockPages+0x67b:
Пересмотрели все что только можно, падает и все. Методом «половинного деления» было выяснено, что если не хучить NtContinue не падает. Что же такого в NtContinue? А вот что:
__stdcall NtContinue(x, x) proc near Context = dword ptr 8 TestAlert = byte ptr 0Ch push ebp mov ebx, large fs:124h mov edx, [ebp+3Ch] mov [ebx+134h], edx mov ebp, esp mov eax, [ebp+0]
ebp+3Ch — это не стековый аргумент, у функции всего два стековых аргумента, это обращение к Trap Frame! Изначально рассчитывается, что NtContinue будет вызываться из KiFastCallEntry. А в пределах KiFastCallEntry EBP — это не указатель на стековый фрейм, он содержит указатель на Trap Frame. То есть пока цепочка вот такая:
KiFastCallEntry -> NtContinue
все хорошо, за счет того, что EBP не меняется.
А если
KiFastCallEntry -> HookCode -> NtContinue
то функция HookCode изменяет EBP, ибо следует конвенции stdcall, а не stdcall + EBP. Таким образом универсальный хук должен еще учитывать тот факт, что есть API, которые обращаются к TrapFrame. NtContinue не одинока, есть еще NtRaiseException:
__stdcall NtRaiseException(x, x, x) proc near ExceptionRecord = dword ptr 8 Context = dword ptr 0Ch SearchFrames = dword ptr 10h push ebp mov ebx, large fs:124h mov edx, [ebp+3Ch] mov [ebx+134h], edx mov ebp, esp mov ebx, [ebp+0]
С точно такими же замашками.
Для чего эти API нужен trap frame? Чтобы быстро выходить в ring-3, не возвращаясь в KiFastCallEntry.