Стиль перехвата функций в C++

ОГЛАВЛЕНИЕ

Данная статья посвящена обходу функций в более безопасном стиле программирования на C++.

•    Скачать исходники - 2.16 Кб

Введение

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

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

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

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

Вы можете использовать приведенный код путем добавления patcher.cpp и patcher.h в ваш проект C++. Архив не содержит примеры.

Краткая информация

Бывает три типа таких ошибок (назовем их 3TM):
1.    Использование неверного соглашения о вызовах, не совпадающего с соглашением исправленной функции
2.    Использование неверных аргументов, переданных функции
3.    Использование неверного типа возвращаемой переменной

Очень легко избежать данных видов ошибок во время компиляции.

Рассмотрим функции send/recv в winsock.h:

int PASCAL FAR recv (
                     IN SOCKET s,
                     OUT char FAR * buf,
                     IN int len,
                     IN int flags);
int PASCAL FAR send (
                     IN SOCKET s,
                     IN const char FAR * buf,
                     IN int len,
                     IN int flags); 

И простейшие исправления для них:

int (PASCAL FAR *lpfn_send )( IN SOCKET s, IN const char FAR * buf,
                IN int len, IN int flags);
int PASCAL FAR my_send ( IN SOCKET s, IN const char FAR * buf, IN int len, IN int flags)
{
    return lpfn_send (s, buf, len, flags);
}
int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags);
int PASCAL FAR my_recv ( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags)
{
    return lpfn_recv (s, buf, len, flags);
}

Правила просты - если функция send имеет тип type_of_send, то исправленная функция my_send должна использовать тот же тип type_of_send. Функция-заглушка lpfn_send также должна иметь тип type_of_send. Если другая функция recv имеет другой тип type_of_recv, то исправленная функция my_recv должна использовать тот же тип type_of_recv. Функция-заглушка lpfn_recv также должна иметь тип type_of_recv.

И само исправление (пока теоретический код):

patch(send, my_send, lpfn_send);
patch(recv, my_recv, lpfn_recv);

Не видно никаких вынужденных приведений типов, никаких предупреждений, никаких ошибок. Гораздо легче писать и нет шанса сделать 3TM.

Прототип этой функции таков:

template<class T> patch(T, T, T&);

Данная конструкция помогает предотвратить 3TM. Если вы не навяжете приведение типа при передаче аргументов в функцию, то не сможете скомпилировать код с 3TM.

Использование указателей вместо ссылок мало отличается:

patch(send, my_send, &lpfn_send);
patch(recv, my_recv, &lpfn_recv);

Прототип функции таков:

template<class T> patch(T, T, T*);         

Это все о безопасности исправлений.

Типизация функции

Кое-что о правилах типов функций. Это может понадобиться при получении адреса функции через GetProcAddress. Возьмем простое объявление функции C++:

int func(int, char*, void*);  
Тип T функции будет:
int (*)(int, char*, void*)

Надо объявить указатель правильного типа и правильно привести тип при выполнении GetProcAddress:

int (*func_ptr)(int, char*, void*);
func_ptr = (     int (*)(int, char*, void*)     ) GetProcAddress(hModule, "func_name");

Более полное объявление функции таково:

return_type calling_convention function_name (список типов аргументов); 

Правило создания типа функции следующее:

return_type (calling_convention *) (список типов аргументов) 

Стандартное соглашение о вызовах для C++ в функции MSVC - __cdecl. Можно опустить его, но можно включить его в объявление. Соглашение о вызовах  вы должны добавлять при работе с функциями с разными соглашениями о вызовах. Пример функции выше с объявлением указателя и LoadLibrary с приведением типа становится:

int __cdecl func(int, char*, void*);
int (__cdecl* func_ptr)(int, char*, void*);
func_ptr = (  int (__cdecl*)(int, char*, void*)   )
        GetProcAddress(hModule, "func_name");

То же самое для функций send/recv выглядит так:

//объявления функций в winsock.h
int PASCAL FAR recv (
                     IN SOCKET s,
                     OUT char FAR * buf,
                     IN int len,
                     IN int flags);
int PASCAL FAR send (
                     IN SOCKET s,
                     IN const char FAR * buf,
                     IN int len,
                     IN int flags);

///////////////////////////////////////////////////
//объявления указателя и исправления в приложении
//опустим IN и OUT
int (PASCAL FAR *lpfn_send )( SOCKET , const char * , int , int) = send;
int PASCAL FAR my_send ( SOCKET s, const char * buf, int len, int flags);
int (PASCAL FAR *lpfn_recv )( SOCKET, char FAR *, int, int) = recv;
int PASCAL FAR my_recv ( SOCKET s, char FAR * buf, int len, int flags);

//////////////////////////////////////////////////
//присвоение указателя с GetProcAddress:
HINSTANCE hInstanceWs2 = GetModuleHandleA("ws2_32.dll");
lpfn_send = (int(PASCAL FAR*)(SOCKET,const char*,int,int))
            GetProcAddress(hInstanceWs2, "send");
lpfn_recv = (int(PASCAL FAR*)(SOCKET,char*,int,int))
            GetProcAddress(hInstanceWs2, "recv");

//делать так не рекомендуется, но допускается:
lpfn_send = (int(__stdcall*)(SOCKET,const char*,int,int))
            GetProcAddress(hInstanceWs2, "send");
lpfn_recv = (int(__stdcall*)(SOCKET,char*,int,int))GetProcAddress(hInstanceWs2, "recv");

В большинстве случаев для функций WinAPI не надо вызывать GetModuleHandle/GetProcAddress, так как библиотеки и функции загружаются в некие стандартные адреса. Поэтому вышеприведенные приведения типов не нужны. Просто задайте адрес функции при инициализации или позже:

lpfn_send = send;
lpfn_recv = recv;

Реальный пример

Пора использовать реальный код. Смотрите CPatcher.zip. Был создан простой класс по имени CPatch. Два конструктора класса весьма похожи на теоретическую функцию patch(...), описанную выше:

class CPatch
{
...
CPatch(){}
CPatch(CPatch&){}
...
public:
...
template<class TFunction>explicit CPatch
    (TFunction FuncToHook, TFunction MyHook, TFunction& NewCallAddress,
             bool patch_now = true, bool set_forever = false)
...
template<class TFunction>explicit CPatch
    (TFunction FuncToHook, TFunction MyHook, TFunction* NewCallAddress,
         bool patch_now = true, bool set_forever = false)
...
template<class TFunction>explicit CPatch(TFunction& NewCallAddress,
    TFunction MyHook, bool patch_now = true, bool set_forever = false)
...
template<class TFunction>explicit CPatch(TFunction* NewCallAddress,
    TFunction MyHook, bool patch_now = true, bool set_forever = false)
...

Как использовать

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

#include<winsock2.h>
#include "patcher.h"
int (PASCAL FAR *lpfn_send )( IN SOCKET s,
    IN const char FAR * buf, IN int len, IN int flags);
int PASCAL FAR my_send ( IN SOCKET s, IN const char FAR * buf,
    IN int len, IN int flags)
{
    return lpfn_send (s, buf, len, flags);
}
int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf,
    IN int len, IN int flags);
int PASCAL FAR my_recv ( IN SOCKET s, OUT char FAR * buf,
    IN int len, IN int flags)
{
    return lpfn_recv (s, buf, len, flags);
}

....
{
   CPatch patch_for_send(send, my_send, lpfn_send);
   CPatch patch_for_recv(recv, my_recv, lpfn_recv);
   //теперь функции send и recv исправлены
   .....
}//исправления удаляются автоматически

Можете попробовать сделать некоторые 3TM, чтобы увидеть, что компилятор не позволит вам сделать ошибки. Попробуйте использовать что-то другое вместо PASCAL. Или попробуйте изменить тип аргумента. Или попробуйте случайно использовать recv вместо send:

patch_for_send(send, my_recv, lpfn_send); 

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

CPatch patch_for_send(send, my_send, &lpfn_send);
CPatch patch_for_recv(recv, my_recv, &lpfn_recv);  

Внутри обоих вызовов конструкторов находится одна и та же нешаблонная и защищенная функция HookFunction. Единственная разница в том, что в первом варианте используется &NewCallAddress, а во втором - NewCallAddress.
Вы можете инициализировать указатели-заглушки на реальные функции, т.е. lpfn_send = send и lpfn_recv = recv:

int (PASCAL FAR *lpfn_send )( IN SOCKET s, IN const char FAR * buf,
        IN int len, IN int flags)
                     = send;

int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags)
                     = recv;

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

CPatch patch_for_send(lpfn_send, my_send);
CPatch patch_for_recv(lpfn_recv, my_recv);

Теперь можно применять и удалять исправление столько раз, сколько вам угодно.

  //исправления не нужны
  patch_for_send.remove_patch();
  patch_for_recv.remove_patch();
  //исправления нужны прямо сейчас! :)
  patch_for_send.set_patch();
  patch_for_recv.set_patch();
  // исправления не нужны
  patch_for_send.remove_patch();
  patch_for_recv.remove_patch();
  //исправления снова нужны
  patch_for_send.set_patch();
  patch_for_recv.set_patch();
}
//если вы забыли удалить какие-то исправления, не беда.
//Деструктор удаляет их автоматически

Если вы хотите удалить исправление навсегда, передайте true параметру по умолчанию, которому неявно присваивается false:

patch_for_send.remove_patch(true); //true = навсегда

Теперь используем подготовленное исправление. Оно не применяется сразу:

CPatch patch_for_send(send, my_send, lpfn_send, false);
CPatch patch_for_recv(recv, my_recv, &lpfn_recv, false);
//исправление готово, но не применяется. Вызываем set_patch, чтобы применить его.

или:

CPatch patch_for_send(&lpfn_send, my_send, false);
CPatch patch_for_recv(lpfn_recv, my_recv, false);

Если вы хотите установить исправление навсегда, задайте последнему параметру true. Этому параметру по умолчанию неявно задано false.

//сразу применить навсегда:
CPatch patch_for_send(&lpfn_send, my_send, false, true);
//применить позже навсегда:
CPatch patch_for_recv(lpfn_recv, my_recv, true, true);

Теперь деструктор не удалит исправление. Но все же его можно удалить через функцию remove_patch.

Функция-член patched() сообщает, установлено исправление или нет. Если вы применили/вызвали? set_patch, то функция возвращает true. Если вы удалили исправление, функция возвращает false. Функция ok() сообщает, может  быть создана генерация исправления или нет, и можно ли переписать исполнимые команды или нет. Если ok() возвращает false, то set_patch/remove_patch ничего не делает.

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

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

Если вы хотите увидеть, как исправление применяется/удаляется, ознакомьтесь с реализацией функций CPatch::set_patch/CPatch::remove_patch. Эти функции заменяют начальные исполнимые команды функции на jmp к функции-ловушке. Мы старались сделать их как можно проще.

Большая часть работы осуществляется в CPatch::HookFunction(...). Эта функция генерирует исполнимые команды для заглушки, вычисляет смещения для команд jmp и вызывает set_patch, если объект намерен установить исправление сразу. Он вызывается лишь единожды внутри конструктора. Адрес для команды jmp вычисляется относительно адреса первого байта после конца команды jmp. Он относителен адрес jmp + 5.

Функция okToRewriteTragetInstructionSet разбирает команду, устанавливаемую при запуске исправленной функции. Она возвращает минимальное количество N байт, нуждающихся в переписывании. N должно быть не меньше 5 байт, так как команда jmp требует 5 байт. Функция-заглушка содержит ровно N + 5 байт, заполненных первыми N байтами из запуска исправленной функции и jmp к следующей команде исправленной функции. Теперь можно вызывать заглушку вместо оригинальной исправленной функции. Полагаем, что okToRewriteTragetInstructionSet может быть неполной. Были добавлены только команды, найденные при отладке. Для большинства функций WinAPI этого должно хватить. Но если не хватит, можете добавить новые команды в okToRewriteTragetInstructionSet, скопировав и вставив их из отладчика.
Данный «исправитель» может использоваться не только в текущем приложении. Вы можете создать DLL(динамическая библиотека) и вставить ее в любой процесс.

Ввести DLL в процесс весьма просто. Сначала откройте процесс. Найдите идентификатор процесса в диспетчере задач Windows. Получив идентификатор процесса (PID), откройте процесс, выделите буфер внутри этого процесса и вызовите LoadLibrary в этот процесс с помощью CreateRemoteThread:

HANDLE hProcess = OpenProcess(
        PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
         PROCESS_VM_OPERATION |PROCESS_VM_WRITE | PROCESS_VM_READ |
         PROCESS_TERMINATE , FALSE, PID);
if(hProcess)
{
   #define INJECTED_DLL L"InjectDllTest.dll"

   DWORD dwThreadId;
   LPVOID pvProcMem = VirtualAllocEx(hProcess, 0, sizeof(INJECTED_DLL),
                                           MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
   SIZE_T written;
   WriteProcessMemory(hProcess, pvProcMem, INJECTED_DLL, sizeof(INJECTED_DLL), &written);

   DWORD oldProtectRemoteThreadProc;
   HANDLE hThread = CreateRemoteThread(hProcess, 0, 0,
                                          (LPTHREAD_START_ROUTINE)LoadLibraryW,
                                          pvProcMem, 0, &dwThreadId);
   DWORD wtso = WAIT_FAILED;
   wtso = WaitForSingleObject(hThread, 1000);
   //проверьте wtso, чтобы убедиться, что ваш код успешно вставлен

Не используйте #define и sizeof для передачи имени вставляемой DLL. Этот код должен быть как можно проще.

ProcessID можно найти с помощью EnumProcesses. Откройте каждый процесс, проверьте имя с помощью GetModuleBaseName.

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