Перегрузить методы COM-класса методами из стандартного модуля с помощью взлома VTable

Быстрый вопрос - я только что тестировал методы перезаписи класса, изменяя записи в его VTable, используя низкоуровневый api памяти для копирования.

Задний план

У меня был некоторый успех, и я могу поменять местами 2 записи в VTable класса, если они имеют одинаковую подпись. Итак, определение класса вроде этого:

Option Explicit

Public Sub Meow()
    Debug.Print "Meow"
End Sub

Public Sub Woof()
    Debug.Print "Woof"
End Sub 

... генерирует VTable вроде этого:

VTable

... и я могу поменять местами записи в позициях 7 и 8, чтобы cls.Meow печатал Woof и наоборот. Я также могу поменять местами запись из VTable одного класса с VTable совершенно другого (при условии, что я не пытаюсь разыменовать неявный указатель this, вызывая Me.anything)

Так что я могу сделать еще один урок

Option Explicit

Public Sub Tweet()
    Debug.Print "Tweet"
End Sub

и поменять местами поведение Woof с одного на Tweet с другого. Не слишком сложно, могу поделиться кодом, если он понадобится людям.

Что я не могу сделать ...

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

На основании этой статьи, Похоже, что механизм COM, на котором построен VBA, требует двух методов класса, которые скрывает VBA:

  • У них есть неявный указатель this
  • Они возвращают HRESULT (typedef long)

Так я подумал

Public Sub Meow()

в модуле класса Class1 эквивалентно

Public Function Meow(ByVal this As LongPtr) As Long

Я также пробовал

Public Function Meow(ByRef meObj As Class1) As Long
Public Function Meow(ByRef meObj As Class1) As LongPtr 'but HResult is 32 bit int
Public Sub Meow(ByVal this As LongPtr)

и т.д. Но VBA всегда дает сбой, когда я пытаюсь вызвать метод из VTable. Так что я немного растерялся. Интересно, все ли по-другому на 64-битном компьютере, или стандартные функции модуля делают что-то странное со стеком вызовов. Дело в том, что я видел примеры кода, где весь VTable собран из стандартных функций модуля, поэтому я знаю, что это возможно, но просто не знаю, как правильно преобразовать подписи

Как я могу перезаписать запись VTable методом, определенным в стандартном модуле?


person Greedo    schedule 19.12.2020    source источник
comment
На x64 поведение точно такое же. Однако вы можете «перенаправить» методы IUnknown и IDispatch в функцию внутри модуля .bas (а также методы IEnumVariant в вашем связанном примере). Я предполагаю, что это работает, потому что они не VB. Я думаю, вы не можете перенаправить метод класса VB из-за того, как работают классы VB. Рассмотрим ключевое слово Me (которое ведет себя как функция / свойство get). Невозможно воспроизвести поведение Me в модуле .bas. За кулисами должно происходить больше событий. Может быть, соглашение о вызовах тоже другое.   -  person Cristian Buse    schedule 23.12.2020


Ответы (1)


Я лишь частично ответил на ваш вопрос. Я по-прежнему считаю, что ключевое слово Me играет роль в предотвращении «перенаправления» метода класса на метод внутри стандартного модуля .bas. Но это применимо только к раннему связыванию.

IDispatch :: Invoke действительно может без проблем вызывать метод внутри модуля .bas. Ваша исходная подпись метода была правильной:

Public Function Meow(ByRef meObj As Class1) As Long

Class1 код:

Option Explicit

Public Sub Meow()
    Debug.Print "Meow"
End Sub

Public Sub Woof()
    Debug.Print "Woof"
End Sub

Код в стандартном модуле .bas:

Option Explicit

Sub Test()
    Dim c As Object 'Must be late-binded!
    Dim vTblPtr As LongPtr
    Dim vTblMeowPtr As LongPtr
    Dim originalMeow As LongPtr
    '
    Set c = New Class1
    c.Meow 'Prints "Meow" to the Immediate Window
    '
    'The address of the virtual table
    vTblPtr = MemLongPtr(ObjPtr(c))
    '
    'The address of the Class1.Meow method within the virtual table
    vTblMeowPtr = vTblPtr + 7 * PTR_SIZE
    '
    'The current address of the Class1.Meow method
    originalMeow = MemLongPtr(vTblMeowPtr)
    '
    'Replace the address of Meow with the one in a .bas module
    MemLongPtr(vTblMeowPtr) = VBA.Int(AddressOf Moew)
    '
    c.Meow 'Prints "Meow in .bas" to the Immediate Window
    '
    'Revert the original address
    MemLongPtr(vTblMeowPtr) = originalMeow
    '
    c.Meow 'Prints "Meow" to the Immediate Window
End Sub

Public Function Moew(ByVal this As Class1) As Long
    Debug.Print "Meow in .bas"
End Function

Я использовал LibMemory для памяти. манипуляции.

Если вы измените метод класса Meow на Function вместо Sub, тогда вам понадобится дополнительный параметр ByRef в конце списка параметров в методе Meow в модуле .bas.

ИЗМЕНИТЬ №1

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

Это означает, что:

Public Function Meow(ByRef this As Class1) As Long

приведет к сбою приложения

Но это работает:

Public Function Moew(ByVal this As Class1) As Long
    Debug.Print "Meow in .bas"
End Function

потому что передача ByVal заставляет QueryInterface и AddRef на IUnknown (с Release при выходе из области видимости)

Это тоже работает:

Public Function Moew(ByRef this As IUnknown) As Long
    Debug.Print "Meow in .bas"
End Function

ИЗМЕНИТЬ №2

Приносим извинения за повторную правку.

Метод Invoke не работает с указателем на IUnknown. Он работает с указателем на IDispatch. Это можно проверить с помощью:

Public Function Moew(ByVal this As LongPtr) As Long
    Debug.Print this
    Debug.Print "Meow in .bas"
End Function

который выведет ptr в интерфейс IDispatch. Итак, почему ByRef this As Class1 терпит неудачу? А почему работают ByVal this As Class1 и ByRef this As IUnknown?

ByRef this As Class1
Я считаю, что адрес VarPtr (this) недоступен для VB, поэтому мы читаем память, чего не должны. Это не похоже на то, что в интерфейсе IUnknown есть дополнительные AddRef или Release, потому что метод никогда не вызывается с использованием этого объявления. Приложение просто аварийно завершает работу, когда Invoke пытается вызвать метод.

ByVal this As Class1
Метод просто создает переменную VB (в пространстве памяти VB) и вызывает AddRef.

ByRef this As IUnknown
Поскольку это не двойной интерфейс, выполняется вызов QueryInterface и AddRef. Адрес памяти this находится в пространстве локальной памяти, как и во втором примере.

person Cristian Buse    schedule 06.01.2021
comment
Спасибо за продолжение! Один вопрос; вы используете ByVal this, а не ByRef; это опечатка? А вам повезло с вызовом каких-либо методов или свойств this? - person Greedo; 07.01.2021
comment
Извинения. Я даже не осознавал. Я сделал это по привычке. Давным-давно, подключая IUnknown :: Release, я обнаружил, что когда функция, не относящаяся к VB, вызывает функцию VB, лучше передать параметр экземпляра ByVal, чтобы избежать сбоев. Я подумаю над объяснением, и, если придумаю достойное, я соответствующим образом отредактирую ответ. Да, использование кода в ответе позволяет мне получить доступ ко всем методам класса в Meow методе модуля .bas. Поднятие ошибок тоже работает правильно. - person Cristian Buse; 07.01.2021
comment
@Greedo Отредактировал ответ, надеюсь, я прав в своем предположении. Кстати, кажется, ты всегда задаешь правильные вопросы. Отличная работа! - person Cristian Buse; 07.01.2021
comment
@Greedo Я сделал еще одно изменение. Надеюсь, на этот раз я все понял. - person Cristian Buse; 11.01.2021
comment
@Greedo Ваш вопрос помог мне обновить логику в перенаправлении экземпляра (см. Правка №1). Спасибо за это! Кажется, что действительно точка входа в класс при раннем связывании имеет некоторые дополнительные механизмы безопасности в отличие от позднего связывания. - person Cristian Buse; 11.02.2021