Перехват (горячее исправление) функций-членов класса. Изменение записей vtable - PullRequest
2 голосов
/ 08 августа 2011

У меня есть 2 вопроса после довольно длинной преамбулы.

Глядя на любой указатель функции как на void*, я могу изменить его первые инструкции, преобразовать их в jmp (либо32-битный относительный или 64-битный абсолютный, через r11, в зависимости от x86 / x86-64).Я полагаю, что просмотр кода функции как данных недопустим как в C, так и в C ++, но, похоже, он каким-то образом не поддерживается, как в MSVC (Win32), так и в GCC (OS X).В Интернете есть несколько мест, где говорят, что приведение указателей на void* является недопустимым .

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

К счастью, чтобы подключить Direct3D9, я работаю с такими вещами, как IDirect3DDevice9, у которых есть vtablepDev типа IDirect3DDevice9* достаточно, чтобы я посмотрел на pDev как на PVOID*.Тогда первое значение в pDev - это адрес массива указателей на функции (vtable):

// IDirect3DDevice9::Present()
typedef HRESULT (CALLBACK *PRESENT_PROC)(
    LPDIRECT3DDEVICE9, const RECT*,
    const RECT*,
    HWND,
    const RGNDATA*
);

PVOID (*vPtr)[] = reinterpret_cast<PVOID (*)[]>(
    *reinterpret_cast<PVOID*>(pDev)
);
PRESENT_PROC pDevicePresent = reinterpret_cast<PRESENT_PROC>(
    (*vPtr)[17]
);

, поскольку Present является 18-й записью.

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

reinterpret_cast<PVOID>(pDev->lpVtbl->Present)

без ошибок.

Первая проблема. Я не удивительныйПрограммист на C ++;Как мне получить указатель, , на функцию-член , чтобы я мог перезаписать исполняемые байты этой функции.Для нечленов я делаю:

#include <windows.h>
#include <cstdio>
using namespace std;

const unsigned char OP_JMP = 0xE9;  // 32 bit relative jmp
const SIZE_T SIZE_PATCH = 5;        // jmp dword ptr distance; 1 byte + 4 bytes
typedef void (*MyProc)();

void SimpleFunction1()
{
    printf("foo\n");
}

void SimpleFunction2()
{
    printf("bar\n");
}

int main()
{
    PBYTE foo = reinterpret_cast<PBYTE>(SimpleFunction1);
    PBYTE bar = reinterpret_cast<PBYTE>(SimpleFunction2);

    DWORD oldProtection;
    // make sure the bytes of the function are writable
    // by default they are only readable and executable
    BOOL res = VirtualProtect(
        foo,
        SIZE_PATCH,
        PAGE_EXECUTE_READWRITE,
        &oldProtection
    );
    if (!res) return 1;

    // be mindful of pointer arithmetic
    // works with PBYTE, won't with PDWORD
    DWORD distanceToNewFoo = bar - foo - SIZE_PATCH;

    *foo = OP_JMP;
    *reinterpret_cast<PDWORD>(foo + 1) = distanceToNewFoo;

    // called though the pointer instead of foo()
    // to make sure the compiler won't inline or do some other stupid stuff
    reinterpret_cast<MyProc>(foo)(); // will print "bar\n"
    return 0;
}

и что-то в том же духе для x86-64.Для виртуального члена объекта я получаю указатель foo от самого vtable, как я показал выше:

reinterpret_cast<FUNC_TYPE>(
    *(reinterpret_cast<void**>(
        *reinterpret_cast<void**>(objptr)) + n
    )
)

Вторая проблема. Не могу япросто изменить записи из vtable для моего объекта?Вот пример, само собой разумеется, что он не работает для объекта pDev, как взято непосредственно из Direct3D, но Такси , кажется, использует этот метод:

#include <cstdio>
using namespace std;

class BaseClass
{
public:
    BaseClass(int a = 0, int b = 0);
    int GetA();
    int GetB();
    virtual void Test();
private:
    int _a;
    int _b;
};

BaseClass::BaseClass(int a, int b) :
    _a(a),
    _b(b)
{
}

int BaseClass::GetA()
{
    return _a;
}

int BaseClass::GetB()
{
    return _b;
}

void BaseClass::Test()
{
    printf("test %d; %d\n", _a, _b);
}

void TheNewFunction(BaseClass *bc)
{
    printf("I am an intruder\n");
}

typedef void (*PROC_TYPE)(BaseClass *);

int main()
{
    BaseClass foo(5, 56);
    PROC_TYPE proc = 0;
    proc = reinterpret_cast<PROC_TYPE>(
        *reinterpret_cast<void**>(
            *reinterpret_cast<void**>(&foo)
        )
    );
    proc(&foo);
    reinterpret_cast<void**>(
        *reinterpret_cast<void**>(&foo)
    )[0] = reinterpret_cast<void*>(TheNewFunction);

    foo.Test(); // runs same old Test(); maybe due to compiler optimization?
    proc = reinterpret_cast<PROC_TYPE>(
        *reinterpret_cast<void**>(
            *reinterpret_cast<void**>(&foo)
        )
    );

    proc(&foo); // runs TheNewFunction
    BaseClass *goo = &foo;
    goo->Test(); // runs TheNewFunction
    return 0;
}

Ответы [ 6 ]

6 голосов
/ 27 мая 2013

Самый быстрый способ выполнить это уродливое приведение (функция-член к void *) - это печально известный union_cast<>:

template <class T1, class T2>
T1 union_cast(T2 v)
{
  static_assert(sizeof(T1) >= sizeof(T2), "Bad union_cast!");
  union UT {T1 t1; T2 t2;} u {};
  u.t2 = v;
  return u.t1;
}

Используйте вот так:

class MyClass
{
public:
  void foo(int);
};

auto p = union_cast<void *>(&MyClass::foo);

Теперь я дал тебе заряженное ружье с предохранителем. Пожалуйста, используйте с осторожностью ...

5 голосов
/ 08 августа 2011

Как вы указали, ваш метод не является "переносимым", но на самом деле он работает в вашем конкретном случае.И проблем нет ИМХО.

Первый ответ:

Это синтаксис для работы с указателями на функции-члены:

class SomeClass {
    int SomeFunc(int, int);
};

int (SomeClass::* pfn)(int, int); // variable pfn is a pointer to a SomeClass's member function

pfn = &SomeClass::SomeFunc; // assign this variable to the member function with the adequate prototype.

SomeClass obj; // instance of this class

int res = (obj.*pfn)(1, 2); // call the member function pointer

Второй ответ:

Вы можете изменить членов vtable напрямую, но вы должны знать, что этим вы подкласс все объекты этого класса (и, возможно, некоторые производныеклассы).

Если вы хотите создать подкласс только одного конкретного объекта, вы должны создать другую таблицу функций и переписать объект vtable, чтобы он указывал на новую таблицу функций.

2 голосов
/ 23 октября 2012
class Original {
public:
    int a;

    Original* open(const char *filename, int openmode) {
        printf("Original: %s and %d // %d\n", filename, openmode, this->a);
        this->a = openmode;
        return this;
    }
};

class Group {
public:
    Original* my_open(const char *filename, int openmode) {
        Original *self = (Original*) this;
        printf("Fake:: %s and %d // %d\n", filename, openmode, self->a);
        return self;
    }
};

#define GetPointerToClassMethod(RETURN, CLASS, METHOD, ...) __GetPointerToClassMethod<CLASS, RETURN (CLASS::*)(__VA_ARGS__)>(&CLASS::METHOD)

template <class _Class, class _MethodPrototype>
LPVOID __GetPointerToClassMethod(_MethodPrototype method) {
    return *(LPVOID*) &method;
}

int main() {
    Original o;
    o.open("Dorian", 15);

    DirectHookProcedure(
        GetPointerToClassMethod(Original*, Original, open, const char*, int),
        GetPointerToClassMethod(Original*, Group, my_open, const char*, int)
    );

    o.open("Langbeck", 20);
    return 0;
}
2 голосов
/ 08 августа 2011

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

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

1 голос
/ 08 августа 2011

Вы, конечно, правы, что в некоторых случаях, как этот

BaseClass foo(5, 56);
//...
foo.Test(); // runs same old Test(); maybe due to compiler optimization?

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

Когда вы вызываете указатель на объект, разработчики компилятора не удосужились проверить, всегда ли это один и тот же тип. В вашем коде они наверняка могли это видеть, но, возможно, такая оптимизация недостаточно полезна в реальном коде? Или они могут добавить его в следующем выпуске компилятора. : -)

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

0 голосов
/ 12 августа 2011

Практически переносимое решение первой проблемы - va_list в вспомогательной функции:

void *DisMember(size_t size, ...)
{
    if (size != sizeof(void *)) return NULL;
    va_list args;
    va_start(args, size);
    void *res = va_arg(args, void *);
    va_end(args);
    return res;
}

// snip
void Base::MyMethod() { /* ... */ }
// snip

void *anything = DisMember(sizeof(void (Base::*)()), &Base::MyMethod);

Вальдо прибил вторую.

...