Персональный блог Игоря Антонова aka "spider_net"

Перехват WinAPI с помощью Delphi


Рубрика: Delphi -> Программирование -> Журнал Хакер -> Статьи
Метки: | | |
Просмотров: 5575
Перехват WinAPI с помощью Delphi

В одном из номеров нашего журнала в рубрике 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

    Оставьте комментарий!
    comments powered by HyperComments