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

ОГЛАВЛЕНИЕ

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

Пора использовать реальный код. Смотрите 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, содержащей эту функцию.