Что такое Fluke?
Дата: 06/12/2006
Тема: Все, что связано с Глобальной сетью


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

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

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

Перевод слова Fluke с английского - удача, или счастье.

Библиотеки

Если вы хотите использовать код разработанный в рамках этого проекта, то это легко можно сделать. Так как все модули выполнены как независимые библиотеки с гибким интерфейсом. Они доступны для скачивания через систему контроля версий svndir:trunk/library/fluke, svndir:trunk/library/iofluke. Подключение к проекту и управление вызовом функций проходит в три этапа, первый этап - подгрузка исполняемого кода, это описано в Fluke. Второй этап - перехват функций вводавывода, это описано IOFluke. Третий этап - создание среды для удобной работы с приложением на высокоуровневом языке, таким как Java или Visual Basic, это описано FlukeScript.

Fluke

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

Какая цель ставится чаще всего, когда мы говорим о подключении к приложению? Естественно вариантов может быть много, но среди основных - управление и контроль над приложением. То есть задача скорей всего будет сводиться к перехвату вызова функций и передачу их либо на сторону главного приложения либо обработка непосредственно внутри приложения, к которому произведено подключение. В этой статье я опишу процесс, способы и возможные варианты подключения к приложению. Перехват функций описан в следующей статье IOFluke.

Виды подключения

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

Перебрасывание функции

Вот исходная функция написанная на ассемблере, код для перебрасывания между процессами находиться в теле между комментариями function body. После вызова функции GetBody(), на выходе получается бинарный массив с ассемблерным кодом:

     xor eax,eax;
     nop;
     nop;
     nop;
     retn;

Вот пример функции:

 std::vector<unsigned char> GetBody()
 {
   unsigned char* start;
   unsigned char* end;
   __asm
   {
     call process;
     // function body start
     xor eax,eax;
     nop;
     nop;
     nop;
     retn;
     // function body end
 process:
     mov [end],offset process;
     pop eax;
     mov [start],eax;
   };
   return std::vector<unsigned char>(start,end);
 }

Пусть читателя не пугает простата примера, он в достаточной точности описывает механизм с использованием которого может быть реализован любой експлоит. Код вида xor eax,eax может быть заменен на код обработки и получения адреса системной библиотеки kernel32.dll, а так же всех адресов библиотечных функций. После чего, мы получаем динамическую, полно функциональную программу, для которой не имеет значения по каким адресам памяти быть запущенной. Этот прием использует регистр SEH.

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

Передача исполняемого кода

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

  • Используя WINAPI WriteProcessMemory.
  • Через программирование драйвера.

Так как схема с драйверами очень сильно завязана на версию операционной системы, я не считаю ее перспективной. Второй метод, через использование WriteProcessMemory, требует от нас аккуратности на процедуру помещения бинарных данных в исполняемый процесс.

Другими словами нам нужно ответить на вопрос в какую область памяти мы хотим положить данные. Это в первую очередь зависит от цели нашего внедрения. Если нашей задачей есть конкретное преобразование кода исполняемого процесса, то нам необходимо определить адрес в пространстве исполняемого процесса и произвести запись. Конечно, предварительно, остановив исполнение потока внутри целевого процесса. Другой вариант, если нам не имеет значения область памяти в которой будет исполнен наш код - важен лишь факт исполнения. А для этого придется использовать основные точки входа или остановки, которые диктует нам сама операционная система.

Среди таких точек может быть:

  • Точка входа в программу
  • Точка вызова любой библиотечной функции
  • Точка обработки очереди сообщений

Думаю, что список можно продолжить.

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

Подключение библиотеки

Реализация бинарного кода в библиотеке очень популярный метод написания контрольного модуля. Это решает очень много вопросов. Во-первых, мы не обязаны использовать ассемблер, во-вторых мы не тратим сил на динамическую адресацию, и в-третьих используем отладочную информацию для нашей библиотеки. Так же мы уходим от ограничения связанного с объемом исполняемого кода. Давайте рассмотрим процесс подключения любой библиотеки, к любому исполняемому процессу.

Синхронизация с основным приложением

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

Давайте посмотрим на проблему ближе, и тогда нам станет понятно почему необходимо использовать объекты синхронизации в приложении. Предположим нам необходимо с помощью мастер приложения создать несколько дочерних процессов, работая независимо с каждым. Для этого мы запускаем первый процесс и даем команду на подключение библиотеки к ново созданному процессу. Как известно, что ни один из доступных методов подключения не дает гарантии, что библиотека подгрузиться. Поэтому мы не знаем в какой момент мы можем запускать следующий процесс и подключать вторую библиотеку. Такая ситуация безусловно решается через доступные в Windows IPC механизмы, но на мой взгляд модуль Fluke обязан гарантировать и сигнализировать об ошибочных ситуациях. То есть в случае не возможности по каким либо причинам произвести подключение библиотеки, разработчик об этом узнает через исключительное событие и сможет произвести адекватное действие для устранение, что не приведет к нарушению логики внутри мастер приложения.

Написание библиотеки

Создание новой библиотеки - произвольный процесс в который практически не вноситься никаких ограничений. Для примера создаем проект в любой среде, и подключаем к нему статическую библиотеку, доступным нам способом. Исходники, которой доступны по адресу svndir:trunk/library/fluke. Далее в коде библиотеки необходимо создать глобальный объект унаследованный от класса Fluke::CFlukeSlave. И в конструкторе этого класса сделать вызов метода Fluke::CFlukeSlave::Create();. Вот пример:

 class MyApp:public Fluke::CFlukeSlave
 {
 public:
   MyApp()
   {
     Create();
   }
 };
 MyApp g_app;

Предварительно к проекту необходимо подключить файл svnfile:trunk/library/fluke/flukeslave.h. Который так же доступен в репозитории с проектом. На этом этапе подготовка библиотеке завершена.

Мастер приложение

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

Список всех функций которые поддерживает библиотека для подключения к процессу описаны здесь: svnfile:trunk/library/fluke/flukemaster.h.

Получение исходников

Для сборки библиотеки необходимо получить:

  • svndir:trunk/library/fluke
  • svndir:trunk/library/ErrorReport
  • svndir:trunk/library/debugger
Ссылки
IOFluke пространство памяти

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

Переопределение вводавывода

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

Замена библиотеки

Один из популярных методов перехвата, и самый простой - использование одноименной библиотеки. Если наша задача узнать какие данные проходят между исследуемым процессом и библиотечной функцией, то это очень просто реализовать. Нам понадобиться создать такую же библиотеку с тем же именем и набором функций, какие использует целевой процесс. После чего используя правило поиска библиотек в операционной системе Windows Dynamic-Link Library Search Order, мы смело можем подкладывать нашу новую библиотеку в каталог с исполняемым файлом. После запуска процесс будет использовать нашу подставную библиотеку заместо системной. Далее в зависимости от желаемого результата мы помещаем обработчик вызовов в саму библиотеку, либо программируем надстройку для передачи параметров в управляющий процесс.

Использование Hooks

В операционной системе Windows используется система ловушек, для слежения за некоторыми вызовами системных функций. Этот процесс подробно описан в документации на операционную систему Win32 Hooks. Для работы этого метода необходимо создать библиотеку с экспортируемой функцией, далее, следуя документации, вызвать из мастер приложения функцию с параметрами указывающими на конкретный процесс и на исследуемое действие. Функция SetWindowsHookEx принимает на вход процедуру обработчик, и как опциональный параметр идентификатор потока. Более подробное описание, так же, доступно в интернете в безграничном количестве.

Virtual Device Driver

Вот это уже нестандартный и мало известный способ слежение за работой системы, он использует в своей основе виртуальный драйвер устройств. Благодаря чему не только остается абсолютно не заметным, но еще и очень эффективным. Один из проектов, которые мне удалось найти в интернете с этой технологией это Regmon компании Sysinternals. Если вы обратитесь к документации на программу то найдете там три разных способа реализации такого алгоритма, базируются они на использовании Drivers Hooks.

Переопределение секции импорта

Основной смысл этой операции есть переопределение адреса функции, которую мы хотим перехватывать в специальной таблице, которая используется основным процессом для вызова библиотечных функций. Эта таблица находиться в исполняемом файле и модифицируется после загрузки, каждое поле которой указывает на адрес импортируемой функции. Поэтому перехват функции сводится к поиску этого адреса и замене на новый, адрес нашей подставной функции. Подробное описание структуры исполняемых файлов, на диске и в памяти можно найти в документации A Tour of the Win32 Portable Executable File Format. Интернет полон примерами работы и замещения функций через таблицу импорта.

Замена содержимого памяти

Из названия следует, что мы собираемся изменять содержимое функции для того, чтобы поместить безусловный переход на наш код, и вызвать соответствующий обработчик. Для этого нам необходимо найти адрес функции на памяти, запомнить старое содержимое функции и заменить ее на код команды безусловного перехода. Функция может быть двух типов, первая экспортируемая из текущего процесса или импортируемая из другого модуля. Тогда поиск такой функции сводится к вызову метода GetProcAddress. А замена содержимого есть процедура записи по этому адресу - новой команды ассемблера безусловного перехода на адрес нашей новой функции. В этом случае, как и в предыдущих я пока не стану рассматривать реализацию такого механизма подробней, она будет расскрыта при изучении пакета IOFluke.

Пример работы с библиотекой

После того как мы рассмотрели все методы перехвата управления, давайте рассмотрим как это делается с использованием библиотеки IOFluke. Реализация методов скрыта внутри кода, и не обременяет разработчика. Но для полного понимания ее функций, необходимо знать о методах перехвата функций, которые реализованы внутри библиотеки:

  • Замещение адреса в таблице импорта
  • Замещение памяти внутри процесса.

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

Определение типа вызова функций

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

Возможные типы вызова функций:

  • this call
  • stdcall
  • cdecl
  • fastcall

Каждая функция должна быть привязана к одному из приведенных выше типу вызова, успех внедрения в чужой процесс напрямую зависит от успеха данной операции. Для того чтобы определить тип вызова, можно воспользоваться разными средствами. Самый простой способ - обратится к документации, если перехватываемая функция подробно описана, то обязательно среди всех параметров указано соглашение вызова. Именно его необходимо указать. Другой способ использовать утилиту Depends.Exe, при просмотре имен экспортируемых функций мы можем увидеть декоррированые имена. По измененному имени функции, в некоторых случаях, можно понять о количестве параметров и способе передачи их внутрь функции, что позволит определить тип вызова функции. Если выше описанные способы не применимы для определения соглашения о вызове функции, необходимо произвести анализ кода, просмотрев проект и по структуре кода, способу помещения параметров, способу возврата из функции можно однозначно определить искомый тип функции.

Замещение таблицы импорта

Мы подошли к самому интересному - использование библиотеки. В этой секции будет рассказано о использовании части библиотеки IOFluke для перехвата импортируемых функций. Механизм перехвата достаточно простой:

  • создание функции
  • создание связи между старым именем функции и адресом новой
  • поиск и перенаправление вызова.

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

Создадим класс с функциями перехвата:
 class CClientUOSecure
 {
   static void hook_closesocket(CMemMngr* mngr,va_list args)
   {
     CClientUOSecure* _this=dynamic_cast<CClientUOSecure*>(mngr);
     // пустая функция, не производит никакого действия
   }
   static void hook_send(CMemMngr* mngr,va_list args)
   {
     CClientUOSecure* current=dynamic_cast<CClientUOSecure*>(mngr);
 
     SOCKET s=va_arg(args,SOCKET);
     char* buf=va_arg(args,char*);
     int len=va_arg(args,int);
     int flags=va_arg(args,int);
 
     // производим обработку параметров и вызова дополнительных функций
   }
   static void hook_send_leave(CMemMngr* mngr,int *retval)
   {
     CClientUOSecure* current=dynamic_cast<CClientUOSecure*>(mngr);
     
     // производим изменения результата, возвращаемого функцией send
   }
 };

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

Далее в коде библиотеки необходимо сопоставить имя импортируемой функции с адресом наших процедур:

 CatchImportFunction("send","wsock32.dll",hook_send,hook_send_leave);
 CatchImportFunction("closesocket","wsock32.dll",hook_closesocket);

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

Замещение памяти

Алгоритм замещения функций идентичен алгоритму замещения функций в секции импорта. Поэтому сразу перейдем к примеру.

Класс с функциями перехватчиками:

 class CClientUOCommands
 {
   static void FromClient(CMemMngr*,va_list args)
   {
     CClientUOCommands* pclt=dynamic_cast<CClientUOCommands*>(client);
 
     unsigned char* input=va_arg(args,unsigned char*);
     int size=va_arg(args,int);
   }
   static void SetNewPlayersCoord(CMemMngr* client,int *retval)
   {
     CClientUO* pclt=dynamic_cast<CClientUO*>(client);
   }
 };

Вызов соответствия адреса функции адресу перехватчика:

 CatchRetFunction(0x004C0E30,FromClient);
 CatchRetFunction(0x00477C80,0,SetNewPlayersCoord);

В первом случае мы перехватываем данные которые передаются в функцию FromClient в коде исполняемого процесса, и имеем возможность изменить эти параметры. Во втором случае SetNewPlayersCoord перехватчик получает управление после выполнения указанной функции и имеет возможность изменить результат возврата, а так же данные на памяти которые оригинальная функция могла изменить.

Анализ кода

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

  • Дизассемблер
  • Отладочная информация
  • Анализ интерфейсов объектов
  • Анализаторы статических библиотек

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

Получение исходников

Для сборки библиотеки необходимо получить исходный код к следующим модулям:

  • svndir:trunk/library/iofluke
  • svndir:trunk/library/debugger
  • svndir:trunk/library/misc

Автор: Алексей Кузнецов
Источник: www.gzproject.ru







Это статья Информационный проект Ynks.Net
http://www.ynks.net

URL этой статьи:
http://www.ynks.net/modules.php?name=News&file=article&sid=863