Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #48, НОЯБРЬ 2004 г.

Игра в прятки

Дмитрий Коваленко aka Ingrem

Спецвыпуск: Хакер, номер #048, стр. 048-046-3


Обычно сплайсинг реализуется так:

1) В адресное пространство чужого процесса тем или иным способом внедряется код, и ему передается управление.

2) Получив управление, внедренный код находит адрес перехватываемой API-функции и меняет первые пять байт на инструкцию jmp XXX, где вместо XXX стоит адрес нового обработчика, который находится в том же внедренном коде. Оригинальное начало функции (переписываемые пять байт) при этом сохраняются для использования в дальнейшем. На адресные пространства других процессов все эти манипуляции не влияют.

Теперь, если процесс вызовет измененную API, сперва выполнится jmp XXX и управление получит внедренный обработчик.

Правда, тут возникает одна проблема. В большинстве случаев обработчику, перед тем как вернуть управление процессу, нужно самому вызвать эту же API - например, чтобы проанализировать и подправить результат ее выполнения. Поэтому каждый раз перед тем, как вызывать перехватываемую API, обработчик вынужден восстанавливать в ней первые пять байт. Естественно, сразу после вызова обработчик опять вписывает в начало API jmp XXX. И так все время :).

Все бы хорошо, но Windows - система многозадачная. Потоки по очереди то запускаются, то приостанавливаются. Где гарантия, что система не остановит поток обработчика как раз в тот момент, когда он восстановил начало API? И не запустит другой поток, который вызовет эту же API? Тогда вызов пройдет мимо обработчика и наш процесс (или ключ реестра, или что-либо еще, скрываемое с помощью сплайсинга) из невидимого превратится в видимый :(. А это не есть гуд.

Из-за этого неприятного момента сплайсинг используют с некоторой опаской. Но попробуем изменить положение дел. Известно, что многие "нормальные" документированные API являются надстройками над Native API библиотеки ntdll.dll. Поэтому для наших целей достаточно научиться перехватывать некоторые из Native API. Дизассемблировав ntdll.dll и изучив код нескольких Native API, мы видим, что все они имеют одинаковую структуру (см. листинг).

; Начало Native API

B8XXXXXXXX mov eax, XXXXXXXX ; в eax - номер системного сервиса

8D542404 lea edx, dword ptr [esp+04] ; в edx - указатель на стек с параметрами

CD2E int 2E ; вызываем системный сервис

C21000 ret YYYY ; очищаем стек

; Конец Native API

Вместо XXXXXXXX стоит двойное слово - номер системного сервиса. Естественно, у каждой Native API он свой. Кроме того, номера одной и той же функции могут отличаться в разных билдах и сервиспаках Windows (не говоря уже о разных версиях). Как и обычным API, параметры Native API передаются в стеке. Исходя из этого, легко догадаться, что YYYY - их общий размер.

Итак, в плане кода все Native API на одно лицо. Тогда организуем сплайсинг несколько нетрадиционно:

1. В адресное пространство чужого процесса внедряется код (например, с помощью установки глобального хука).

2. Получив управление, код находит адрес перехватываемой Native API и проверяет первый байт по этому адресу. Там должен быть 0xB8 - опкод инструкции mov.

Назад на стр. 048-046-2  Содержание  Вперед на стр. 048-046-4