Уже описывал это, на крабе в частности. Вчера PSR1257 попытался решить, опишу подробно. Существует механизм доставки сообщений шадовом в юзермод http://virustech.org/f/viewtopic.php?id=38 Знакомая многим таблица apfnDispatch - через неё шадов вызывает хэндлеры первого уровня. При вызове теневых сервисов часто до возврата из сервиса управление возвращается в юзермод. Этот механизм допускает рекурсивные вызовы. При входе в ядро формируется трап-фрейм. Ссылка на него сохраняется в текущем ядерном стеке, ниже трап-фрейма как часть фрейма описывающего калбэк. Этот фрейм содержит ссылку на следующий фрейм, на трап-фрейм и часть стека ядра, сформированного при вызове KiCallUserMode(). Таким образом формируется цепочка, подобно цепочке трап-фреймов. Состояние задачи(трап-фрейм) восстанавливается посредством сервиса NtCallbackReturn или прерывания KiCallbackReturn(0x2B) в состояние, которое было сразу после формирования трап-фрейма при вызове сервиса, можно понимать как контекст потока на момент вызова сервиса с соответствующими поправками. Тогда при возврате в ядро поток продолжет исполнять код после сервиса, тоесть происходит нормальный возврат. Уровень вложенности ограничен размером ядерного стека. Можно использовать практически любые сервисы из которых вызываются шадовом калбэки. Как пример использую NtUserEnumDisplayMonitors, так как это голый стаб для функи EnumDisplayMonitors() удобный для вызова. Итак вызываем этот сервис, поток вернётся в юзермод для вызова калбэка для енума. Из калбэка восстанавливаем стек и выполняем возврат вручную, это делает код: Код (Text): ; + ; Сохранение контекста. ; o Eax: ID NtUserEnumDisplayMonitors. ; o Esi: адрес возврата. ; o Edi: ссылка на стек. ; TsSave proc C xor ecx,ecx push esp ; Параметр для калбэка - ссылка на стек для его восстановления. Call @f ; Ссылка на калбэк. ; Калбэк. ; typedef BOOL (CALLBACK* MONITORENUMPROC)(HMONITOR, HDC, LPRECT, LPARAM); mov esp,dword ptr [esp + 4*4] ; Восстанавливаем стек, ссылка для восстановления передаётся параметром. xor eax,eax retn @@: push ecx push ecx mov edx,esp ; BOOL ; NtUserEnumDisplayMonitors( ; IN HDC hdc, ; IN LPCRECT lprcClip, ; IN MONITORENUMPROC lpfnEnum, ; IN LPARAM dwData) Int 2EH ; NtUserEnumDisplayMonitors ; При восстановлении контекста возвращается не ноль. .if !Eax ; Калбэк не был вызван изза исчерпания лимита вызовов/вложенности(0x2C). add esp,4*4 ; Удаляем параметры сервиса. mov eax,STATUS_STACK_OVERFLOW retn .else mov esp,edi add eax,3*4 ; Ссылка на стек, который был при вызове TsLoad(для передачи параметров). jmp esi .endif TsSave endp Для восстановления контекста вызов такой: Код (Text): ; + ; Восстановление контекста. ; TsLoad proc C ; stdcall xor eax,eax mov edx,3*4 push eax push eax push esp mov ecx,esp Int 2BH add esp,3*4 retn ; В случае ошибки возвратит STATUS_NO_CALLBACK_ACTIVE. TsLoad endp Теперь нюансы. o Передача ссылки в пользовательский калбэк. Так заранее не известно сколько фреймов расположено в стеке после вызова сервиса и сколько локальных переменных передаём непосредственно ссылку на стек. Это универсально, ибо прототип калбэка фиксирован. o Трассировка. Прерывание при вызове сервиса должно быть вызвано со сброшенным TF, тоесть код не должен трассироваться. Это достигается посредством сервиса NtContinue, либо размещение данного кода гдето глубоко в коде, чтобы реверсер не трассировал его. Тогда при восстановлении контекста TF окажется сброшен даже при трассировке TsLoad(), что есть выход из под трассировки. o Переключение стека. При восстановлении контекста стек восстанавливается в состояние, которое было на момент вызова сервиса. При возврате регистр Esp восстанавливается, но без обращений к стеку сразу перезагружается инструкцией mov esp,edi, таким образом обращения к начальному стеку не происходит. o Сторожевые страницы. Для отлова обращений к памяти используются сторожевые страницы. Страница помечается атрибутом PAGE_GUARD, далее при обращении к ней генерируется #STATUS_GUARD_PAGE_VIOLATION, атрибут снимается и отладчик получает уведамление. В ядре ISR проверяет адрес к которому произошло обращение и если он принадлежит стеку, данный атрибут снимается, стек расширяется и выполняется возврат. Таким образом отладчик не получит уведомление при доступе к страницам стека которые помечены как сторожевые. o Блокирование стека. Сделать не доступными(PAGE_NOACCESS) страницы стека нельзя. В этом случае поток будет завершён ядром, под отладчиком возникнет исключение и сервис возвратит ошибку. o Восстановление сегментных регистров. Сегмент стека нельзя сделать инвалидным, так как возникнет ошибка и контекст восстановлен не будет. Остальные регистры будут восстановлены при возврате, таким образом посредством этого нельзя отследить куда выполняется возврат. - Вопрос как отловить куда будет выполнен возврат.
lhc645 Int 0x2B не дёргает KiUserCallbackDispatcher(), это не вызов калбэка, а возврат из него, я ведь описал.
Врятли это возможно из юзермода, не поставишь же бряк на KiCallbackReturn Как альтернатива может быть использован разбор и анализ кода в ClientMonitorEnumProc, но это сработает только для простейших случаев, т.к. внутри колбека могут быть вызваны другие сервисные ф-ции с установленными клиентскими колбеками и логика всех этих прыжков в ядро->юзермоде->ядро->... может быть нетривиальной для разбора.
TSS Всё нельзя, так как некоторые требуют возврат в ядро. Даже в этом случае EnterCrit() исполняется, что весьма не тру Были мысли использовать TIB, где границы стека определены, но без результатно. Мб у вас получится
Clerk ... Начал пытацо понять как это работает - немного потрассировал в Олли и немного думал. Вопрос. Верно ли я понял что вызов TsSave->Int 2Eh устанавливает обработчег по адресу cbRet (lea esi,offset cbRet), а после "возврата" (?) - вы называете это "возврат из колбэка" произойдет собственно shadoved call - cbRet, и именно в этом тонкость - допустим инициализация (TsSave) мной пропущена в процессе трассировки (те я не знаю где cbRet), но в момент TsLoad я не смогу выяснить это? Верно ли я понял? Выяснил что можно ставить BPM в Олли на некоторые части этого алгоритма (но не все). Точно стабильно работает BPM на cbRet. Следовательно задачу можно переформулировать - как отловить момент инициализации (т.е. TsLoad/что-то типа Int 2Eh)? Возможно (?) следующее решение: мониторить момент установки callback'а. Для этого - согласно сырцу - можно использовать тот факт что число callback'ов лимитировано (0x2C?). Тогда при неустановленном callback мы можем установить максимально возможное их число, но если уже есть один - мы не сможем. Не совсем понятно как в такой процедуре снимать установленные callback'и. Для поиска кода устанавливающего коллбэк можно использовать "деление кода пополам" - постепенно сужая диапазон поиска каждый раз следя за числом установленных callback'ов.
PSR1257 Про отслеживание реги калбэка просто забудьте, вы это место не найдёте. Всё что вы имеете это процедуру TsLoad(). Можно несколько десятков калбэков поставить.
Clerk О, еще три мелких вопроса - если можно: 1) Допустим я не знаю где находится TsSave, но я точно знаю что после вызова int 2Eh внутри TsSave() байтики _ПОСЛЕ_ int 2Eh должны быть _нетронуты_ (или изменены на эквивалентные) - они будут вызываны в момент вызова TsLoad() (верно?). В таком случае нужно обязательно килять int 2Eh _ПОСЛЕ_ вызова TsSave() - иначе это неплохой паттерн для поиска. Появляется возможность следить за записью в память с фильтром на int 2Eh. 2) Можно ли установить более 1 колбэка на один и тот же сервис (допустим на ту же NtUserEnumDisplayMonitors)? У меня повторная попытка установки второго callback'а аннулировала установку первого; 3) Это против BPM'ов которых только 4?
PSR1257 1. Не имеет значения способ вызова сервиса, это шлюз прерывания, сисентер или сискол. Сколько раз нужно повторить - код возврата из теневого калбэка вы не найдёте(не NtCallbackReturn). 2. Ничего не анулирует, вы криво вызывали. Сказал ведь что механизм калбэков основан на рекурсивных вызовах. Число их ограничено размером стека ядра.