перехват виртуальных методов C++ (Visual Studio)

Discussion in 'С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby' started by sn0w, 30 Oct 2015.

  1. sn0w

    sn0w Статус пользователя:

    Joined:
    26 Jul 2005
    Messages:
    1,023
    Likes Received:
    1,312
    Reputations:
    327
    итак решил накатать тутор, банальщина, но вдруг кто не врубается до сих пор из сишников)) собсна комменты в коде. и присобачил лейаут, как это всё выглядит в памяти.

    Code:
    #include <windows.h>
    
    //
    // для вывода отладочных сообщений
    //
    #define dmsg(X)    \
    {\
    char rpt[256];\
    wsprintf(rpt, "%s\n%s\t(%s)",X, __FUNCTION__, __FUNCDNAME__);\
    MessageBox(0, rpt, "debug", 0);\
    }
    
    //
    // наш тестовый класс
    //
    class CBase
    {
    private:
        int m_base0;
        int m_base1;
    public:
        CBase();
        virtual ~CBase();
        virtual void SetVar(int index, int value);
    };
    
    CBase::CBase()
    {
        dmsg("вызван конструктор");
        m_base0 = m_base1 = 0;
    }
    
    CBase::~CBase()
    {
        dmsg("вызван деструктор");
    }
    
    void CBase::SetVar(int index, int value)
    {
        dmsg("вызван родной метод");
    
        switch (index)
        {
        case 0:
            m_base0 = value;
            break;
        case 1:
            m_base1 = value;
            break;
        default:
            break;
        }
    
    }
    
    //
    // вспомогательный класс. нужен для того чтобы функция-перехватчик имела подходящее
    // соглашение о вызовах (calling convention: __thiscall)
    // кроме того эта функция(метод называется, по правильному) должен иметь одинаковый список аргументов
    // (имена значения не имеют) и тип возвращаемого значения, т.е. идентичный с CBase::SetVar
    //
    class Intercptr
    {
    public:
        void INTERCEPTOR(int a, int b);
    };
    
    //
    // а это перехватчик, число и тип аргументов как и тип возвращаемого значения идентичны с CBase::SetVar
    // ЭТО ОБЯЗАТЕЛЬНО, иначе всё упадёт.
    //
    void Intercptr::INTERCEPTOR(int a, int b)
    {
        char rpt[512];
        wsprintf(rpt, "аргументы: %d, %d", a, b);
        MessageBox(0, rpt, "вызван перехватчик", 0);
    }
    
    //
    // начало эксперимента, тащемта
    //
    int __stdcall WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
    {
        // создадим наш тестовый класс, в переменной pcb будет находится адрес, по которому
        // выделится память для экземляра класса
        CBase *pcb = new CBase();
    
        // тестовые операции
        //
        // основная фишка в том, что если метод виртуальный, то компилятор будет генерить совершенно
        // другой код для вызова этой функции - он будет извлекать адрес из специальной таблицы,
        // в которой по порядку указаны адреса всех виртуальных методов класса, в простом случае.
        // если же метод не виртуальный, то код идентичен тому, что генерится для вызова обычных функций,
        // за исключением того что через регистр ecx передаётся адрес текущего экземпляра класса (тот
        // что вызвал метод)
        pcb->SetVar(0, 1);
        pcb->SetVar(1, 2);
    
        // извлечем указатель на vftable:
        // если класс имеет виртуальные методы (даже один) то первым полем по адресу, который содержится в
        // pcb будет не первый член данных класса, а адрес, по которому находится массив адресов виртуальных
        // методов - таблица виртуальных функций - vftable.
        // а вот за указателем на нее будут уже последовательно располагаться члены m_base0 и m_base1
    
        // немного заковыристо выглядит, но это просто указатель на указатель на массив DWORD.
        // каждый DWORD в этом массиве - непосредственно адрес виртуального метода, так удобнее.
        // а сам массив, если вы догадались - и есть таблица vftable
        DWORD(**vftable)[1];
    
        vftable = (DWORD(**)[1]) pcb;
    
        // адреса функций в таблице идут по порядку, так как объявлены в классе:
        //vftable[0]: CBase::~CBase
        //vftable[1]: CBase::SetVar
        // в нашем классе их всего 2 - деструктор и SetVar
        //
        // небольшое колдовство с приведением типов к нужному виду, иначе компилер заругает:
        void(__thiscall Intercptr::*pInt)(int, int) = &Intercptr::INTERCEPTOR;
        void *pCastHelper = (void*&) pInt;
    
        // к слову сказать, данные-члены класса тоже расположены в памяти в строгом порядке,
        // как указано в объявлении класса
    
        // таблица находится в секции .rdata, права доступа к которой READ_ONLY, установим
        // возможность записи:
        DWORD op;
        VirtualProtect(*vftable, 4096, PAGE_READWRITE, &op);
    
        // теперь перепишем в таблице адрес CBase::SetVar адресом Intercptr::INTERCEPTOR
        (**vftable)[1] = (DWORD) pCastHelper;
    
        // протестируем:
        // тут должно произойти то чего мы добивались - должен быть вызван перехватчик
        pcb->SetVar(7, 7);
    
        delete pcb;
        // усё :)
    
        return 0;
    }


    [​IMG]

    после перехвата vftable станет такой:
    [​IMG]
     
    Precise, Shawn1x and qaz like this.