Зайцев Олег
25.01.2005, 17:23
Термин RootKit (https://www.anti-malware.ru/threats/rootkits) исторически пришел из мира Unix, и под этим термином понимается набор утилит, которые хакер устанавливает на взломанном им компьютере после получения первоначального доступа. Этот набор, как правило, включает в себя разнообразные утилиты для заметания следов вторжения в систему, хакерский инструментарий (снифферы, сканеры) и троянские программы, замещающие основные утилиты Unix. RootKit позволяет хакеру закрепиться во взломанной системе и сокрыть следы своей деятельности.
В системе NT/W2K/XP RootKit принято считать программу, которая внедряется в систему и перехватывает системные функции (API). Перехват и модификация низкоуровневых API функций в первую очередь позволяет такой программе достаточно качественно маскировать свое присутствие в системе. Кроме того, как правило, RootKit может маскировать присутствие в системе любых описанных в его конфигурации процессов, папок и файлов на диске, ключей в реестре. Многие RootKit устанавливают в систему свои драйвера и сервисы (они, естественно, также являются «невидимыми»).
Перед рассмотрением принципов работы руткита на платформе Windows нужно кратко и упрощенно рассмотреть принцип вызова API функций, размещенных в DLL. Для этого существует два базовых способа:
1.Раннее связывание (статически импортируемые функции). Этот метод основан на том, что на стадии компиляции программы компилятор “знает”, какие функции и из каких библиотек использует программа. Опираясь на эту информацию, он формирует так называемую таблицу импорта EXE файла. Таблица импорта – это особая структура (ее местоположение и размер описываются в заголовке EXE файла), которая содержит список используемых программой библиотек и список импортируемых из каждой библиотеки функций (для каждой функции в таблице имеется поле для хранения адреса, но на стадии компиляции адрес неизвестен). В процессе загрузки EXE файла система анализирует его таблицу импорта, поочередно загружает все перечисленные там DLL и производит занесение в таблицу импорта реальных адресов функций этих DLL. У раннего связывания есть существенный плюс – на момент запуска программы все необходимые DLL оказываются загруженными, таблица импорта заполнена – и все это делается системой, без участия программы. Но отсутствие в процессе загрузки указанной в его таблице импорта DLL (или отсутствие в DLL требуемой функции) приведет к ошибке загрузки программы. Кроме того, очень часто нет необходимости загружать все используемые программой DLL в момент запуска программы. На рисунке 1 показан процесс раннего связывания – в момент загрузки происходит заполнение адресов в таблице импорта (шаг 1), в момент вызова функции из таблицы импорта берется адрес функции (шаг 2) и происходит собственно вызов функции (шаг 3).;
http://z-oleg.com/secur/rootkit1.jpg
Рис. 1 Механизмы вызова API функции из программы
2.Позднее связывание. Отличается от раннего тем, что загрузка DLL производится динамически при помощи функции API LoadLibrary (LoadLibrary находится в kernel32.dll, поэтому, если не прибегать к хакерским приемам, то kernel32.dll придется загружать статически). При помощи LoadLibrary программа может загрузить интересующую ее библиотеку в любой момент времени. Соответственно для получения адреса функции применяется функция kernel32.dll GetProcAddress. На рисунке шаг 4 соответствует загрузке библиотеки при помощи LoadLibrary и определению адресов при помощи GetProcAddress. Далее можно вызывать функции DLL (шаг 5), но, естественно, при этом таблица импорта не нужна. Чтобы не вызывать GetProcAddress для каждого вызова функции из DLL, программист может однократно определить адреса интересующих его функций и сохранить их в массиве или некоторых переменных.
Независимо от метода связывания системе необходимо знать, какие функции экспортирует DLL. Для этого у каждой DLL имеется таблица экспорта – таблица, в которой перечислены экспортируемые DLL функции, их номера (ординалы) и адреса функций.
Теперь вернемся к нашему руткиту. Его задача – перехватить функции API и исказить их работу. Даже поверхностный анализ рисунка, показывающий принцип работы программы с DLL можно выделить минимум 5 методов:
1.Модификация машинного кода прикладной программы.
http://z-oleg.com/secur/rootkit2.jpg
Схема метода показана на рис. 2. В этом случае модифицируется машинный код, отвечающий в прикладной программе за вызов той или иной функции API. Это очень сложно реализовать, т.к. существует множество языков программирования и версий компиляторов. Но теоретически подобное возможно (при условии, что внедрение будет идти в заранее заданную программу известной версии – можно проанализировать ее машинный код и написать перехватчик)
2.Модификация таблицы импорта.
http://z-oleg.com/secur/rootkit3.jpg
Данная операция описана в книге Рихтера и является одним из классических методов. Идея метода проста – руткит находит в памяти таблицу импорта программы и исправляет адреса интересующих его функций на адреса своих перехватчиков (естественно, он предварительно где-то запоминает правильные адреса). Программе, естественно, все равно – в момент вызова API функции она считывает ее адрес из таблицы и передает по этому адресу управление. Схема этого метода показана на рис. 3. Но у методики есть существенный минус (и его хорошо видно на схеме) - перехватываются только статически импортируемые функции. Но есть плюс – методика очень проста в реализации и есть масса примеров, демонстрирующих ее реализацию. Единственная сложность – это поиск таблицы импорта в памяти – но тут на помощи приходит фирма Microsort, которая поставляет набор API для работы с образом программы в памяти – поэтому перехватчик получается очень простым (исходный текст rootkit на C занимает несколько листов печатного текста);
3.Перехват функций LoadLibrary и GetProcAddress.
http://z-oleg.com/secur/rootkit4.jpg
Методика перехвата этих двух функций может быть любой (в классической реализации применяется методика 2 – модификация таблицы импорта). Идея методики проста – если перехватить GetProcAddress, то при запросе адреса можно выдавать не реальные адреса функций, а адреса своих перехватчиков. Как и в методе 2, программе все равно – она получает адрес и выполняет вызов функции … У данного метода есть минус – он не перехватывает статически импортируемые функции;
4.***Метод, сочетающий методику 2 и 3.
http://z-oleg.com/secur/rootkit5.jpg
При этом модифицируется таблица импорта и кроме необходимых для работы RootKit функций обязательно перехватываются LoadLibrary и GetProcAddress – в результате при вызове статически импортируемых функций искаженные адреса берутся из таблицы импорта, при динамическом определении адреса вызывается перехваченная RootKit функция GetProcAddress, которая возвращает искаженные адреса. В результате у программы не остается шансов узнать правильный адрес функции и вызвать ее;
5.***Модификация программного кода API функции.
http://z-oleg.com/secur/rootkit6.jpg
Данный метод ощутимо сложнее в реализации, чем подмена адреса. Идея методики состоит в том, что RootKit находит в памяти машинный код интересующих его функций API и модифицирует его. В результате этого уже нет надобности менять таблицы импорта, подсовывать программе искаженные адреса и т.п. – все остается «как есть» за одним исключением – теперь уже по «правильному» адресу внутри «правильной» DLL находится машинный код RootKit. Как правило, вмешательство в машинный код DLL минимально – в начале функции размещается не более 2-3 команд, передающих управление на основную функцию - перехватчик. Правда, при таком методе перехвата для вызова пораженной функции RootKit должен сначала восстановить ее машинный код, затем передать ему управление и после выполнения опять исказить его первые команды. Для выполнения данной операции RootKit должен сохранять исходный машинный код для каждой модифицированной им функции.
Перечисленные пять методик являются классическими и работают в любой операционной системе. Это важный момент, поскольку я неоднократно встречал в Интернет и литературе утверждения, что RootKit существуют только в NT. В данной статье не описан шестой метод – перехват на самом низком уровне (ниже функций API, с которыми общается программа). Подобные методики перехвата при штатной реализации требуют написания драйвера, и описанные в литературе методы рассчитаны в основном на NT.
Типовая для NT методика основана на том, что в ней существует KeServiceDescriptorTable – таблица адресов точек входа сервисов ядра NT. Через эту таблицу производится вызов всех функций ядра NT (я не буду вдаваться в теорию – по этому вопросу целые книги написаны, наиболее значимая и интересная – книга Свена Шрайбера Undocumented Windows 2000). Модификация этой таблицы позволяет подменить сервис ядра своим сервисом, т.е. по идеологии это метод 2, только вместо модификации таблицы импорта каждой загруженной программы происходит модификация KeServiceDescriptorTable. Этот метод часто называют перехватом Native API и, естественно, он работает только в NT (и соответственно W2K, XP, W2003).
В системе NT/W2K/XP RootKit принято считать программу, которая внедряется в систему и перехватывает системные функции (API). Перехват и модификация низкоуровневых API функций в первую очередь позволяет такой программе достаточно качественно маскировать свое присутствие в системе. Кроме того, как правило, RootKit может маскировать присутствие в системе любых описанных в его конфигурации процессов, папок и файлов на диске, ключей в реестре. Многие RootKit устанавливают в систему свои драйвера и сервисы (они, естественно, также являются «невидимыми»).
Перед рассмотрением принципов работы руткита на платформе Windows нужно кратко и упрощенно рассмотреть принцип вызова API функций, размещенных в DLL. Для этого существует два базовых способа:
1.Раннее связывание (статически импортируемые функции). Этот метод основан на том, что на стадии компиляции программы компилятор “знает”, какие функции и из каких библиотек использует программа. Опираясь на эту информацию, он формирует так называемую таблицу импорта EXE файла. Таблица импорта – это особая структура (ее местоположение и размер описываются в заголовке EXE файла), которая содержит список используемых программой библиотек и список импортируемых из каждой библиотеки функций (для каждой функции в таблице имеется поле для хранения адреса, но на стадии компиляции адрес неизвестен). В процессе загрузки EXE файла система анализирует его таблицу импорта, поочередно загружает все перечисленные там DLL и производит занесение в таблицу импорта реальных адресов функций этих DLL. У раннего связывания есть существенный плюс – на момент запуска программы все необходимые DLL оказываются загруженными, таблица импорта заполнена – и все это делается системой, без участия программы. Но отсутствие в процессе загрузки указанной в его таблице импорта DLL (или отсутствие в DLL требуемой функции) приведет к ошибке загрузки программы. Кроме того, очень часто нет необходимости загружать все используемые программой DLL в момент запуска программы. На рисунке 1 показан процесс раннего связывания – в момент загрузки происходит заполнение адресов в таблице импорта (шаг 1), в момент вызова функции из таблицы импорта берется адрес функции (шаг 2) и происходит собственно вызов функции (шаг 3).;
http://z-oleg.com/secur/rootkit1.jpg
Рис. 1 Механизмы вызова API функции из программы
2.Позднее связывание. Отличается от раннего тем, что загрузка DLL производится динамически при помощи функции API LoadLibrary (LoadLibrary находится в kernel32.dll, поэтому, если не прибегать к хакерским приемам, то kernel32.dll придется загружать статически). При помощи LoadLibrary программа может загрузить интересующую ее библиотеку в любой момент времени. Соответственно для получения адреса функции применяется функция kernel32.dll GetProcAddress. На рисунке шаг 4 соответствует загрузке библиотеки при помощи LoadLibrary и определению адресов при помощи GetProcAddress. Далее можно вызывать функции DLL (шаг 5), но, естественно, при этом таблица импорта не нужна. Чтобы не вызывать GetProcAddress для каждого вызова функции из DLL, программист может однократно определить адреса интересующих его функций и сохранить их в массиве или некоторых переменных.
Независимо от метода связывания системе необходимо знать, какие функции экспортирует DLL. Для этого у каждой DLL имеется таблица экспорта – таблица, в которой перечислены экспортируемые DLL функции, их номера (ординалы) и адреса функций.
Теперь вернемся к нашему руткиту. Его задача – перехватить функции API и исказить их работу. Даже поверхностный анализ рисунка, показывающий принцип работы программы с DLL можно выделить минимум 5 методов:
1.Модификация машинного кода прикладной программы.
http://z-oleg.com/secur/rootkit2.jpg
Схема метода показана на рис. 2. В этом случае модифицируется машинный код, отвечающий в прикладной программе за вызов той или иной функции API. Это очень сложно реализовать, т.к. существует множество языков программирования и версий компиляторов. Но теоретически подобное возможно (при условии, что внедрение будет идти в заранее заданную программу известной версии – можно проанализировать ее машинный код и написать перехватчик)
2.Модификация таблицы импорта.
http://z-oleg.com/secur/rootkit3.jpg
Данная операция описана в книге Рихтера и является одним из классических методов. Идея метода проста – руткит находит в памяти таблицу импорта программы и исправляет адреса интересующих его функций на адреса своих перехватчиков (естественно, он предварительно где-то запоминает правильные адреса). Программе, естественно, все равно – в момент вызова API функции она считывает ее адрес из таблицы и передает по этому адресу управление. Схема этого метода показана на рис. 3. Но у методики есть существенный минус (и его хорошо видно на схеме) - перехватываются только статически импортируемые функции. Но есть плюс – методика очень проста в реализации и есть масса примеров, демонстрирующих ее реализацию. Единственная сложность – это поиск таблицы импорта в памяти – но тут на помощи приходит фирма Microsort, которая поставляет набор API для работы с образом программы в памяти – поэтому перехватчик получается очень простым (исходный текст rootkit на C занимает несколько листов печатного текста);
3.Перехват функций LoadLibrary и GetProcAddress.
http://z-oleg.com/secur/rootkit4.jpg
Методика перехвата этих двух функций может быть любой (в классической реализации применяется методика 2 – модификация таблицы импорта). Идея методики проста – если перехватить GetProcAddress, то при запросе адреса можно выдавать не реальные адреса функций, а адреса своих перехватчиков. Как и в методе 2, программе все равно – она получает адрес и выполняет вызов функции … У данного метода есть минус – он не перехватывает статически импортируемые функции;
4.***Метод, сочетающий методику 2 и 3.
http://z-oleg.com/secur/rootkit5.jpg
При этом модифицируется таблица импорта и кроме необходимых для работы RootKit функций обязательно перехватываются LoadLibrary и GetProcAddress – в результате при вызове статически импортируемых функций искаженные адреса берутся из таблицы импорта, при динамическом определении адреса вызывается перехваченная RootKit функция GetProcAddress, которая возвращает искаженные адреса. В результате у программы не остается шансов узнать правильный адрес функции и вызвать ее;
5.***Модификация программного кода API функции.
http://z-oleg.com/secur/rootkit6.jpg
Данный метод ощутимо сложнее в реализации, чем подмена адреса. Идея методики состоит в том, что RootKit находит в памяти машинный код интересующих его функций API и модифицирует его. В результате этого уже нет надобности менять таблицы импорта, подсовывать программе искаженные адреса и т.п. – все остается «как есть» за одним исключением – теперь уже по «правильному» адресу внутри «правильной» DLL находится машинный код RootKit. Как правило, вмешательство в машинный код DLL минимально – в начале функции размещается не более 2-3 команд, передающих управление на основную функцию - перехватчик. Правда, при таком методе перехвата для вызова пораженной функции RootKit должен сначала восстановить ее машинный код, затем передать ему управление и после выполнения опять исказить его первые команды. Для выполнения данной операции RootKit должен сохранять исходный машинный код для каждой модифицированной им функции.
Перечисленные пять методик являются классическими и работают в любой операционной системе. Это важный момент, поскольку я неоднократно встречал в Интернет и литературе утверждения, что RootKit существуют только в NT. В данной статье не описан шестой метод – перехват на самом низком уровне (ниже функций API, с которыми общается программа). Подобные методики перехвата при штатной реализации требуют написания драйвера, и описанные в литературе методы рассчитаны в основном на NT.
Типовая для NT методика основана на том, что в ней существует KeServiceDescriptorTable – таблица адресов точек входа сервисов ядра NT. Через эту таблицу производится вызов всех функций ядра NT (я не буду вдаваться в теорию – по этому вопросу целые книги написаны, наиболее значимая и интересная – книга Свена Шрайбера Undocumented Windows 2000). Модификация этой таблицы позволяет подменить сервис ядра своим сервисом, т.е. по идеологии это метод 2, только вместо модификации таблицы импорта каждой загруженной программы происходит модификация KeServiceDescriptorTable. Этот метод часто называют перехватом Native API и, естественно, он работает только в NT (и соответственно W2K, XP, W2003).