Перегружать методы 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 следующим образом:

... и я могу поменять местами записи в позициях 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 методом, определенным в стандартном модуле?

На x64 поведение точно такое же. Однако вы можете «перенаправить» методы IUnknown и IDispatch в функцию внутри модуля .bas (а также методы IEnumVariant в вашем связанном примере). Я предполагаю, что они работают, потому что они не VB. Я думаю, что вы не можете перенаправить метод класса VB из-за того, как работают классы VB. Рассмотрим ключевое слово Me (которое ведет себя как функция/свойство). Вы не могли бы воспроизвести поведение Me в модуле .bas. Должно быть больше вещей, происходящих за кулисами. Возможно, соглашение о вызовах также отличается.

Cristian Buse 22.12.2020 22:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
235
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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 находится в локальной памяти, как и во втором примере.

Спасибо за продолжение! Один вопрос; вы используете ByVal вместо ByRef; это опечатка? Удалось ли вам вызвать какие-либо методы или свойства this?

Greedo 07.01.2021 00:53

Извинения. Я даже не понял. Я сделал это по привычке. Давным-давно, перехватывая IUnknown::Release, я обнаружил, что когда не-VB-функция вызывает VB-функцию, лучше передать параметр экземпляра ByVal, чтобы избежать сбоев. Я придумаю объяснение, и если я придумаю достойное, я соответствующим образом отредактирую ответ. Да, использование кода в ответе позволяет мне получить доступ ко всем методам класса в методе Meow модуля .bas. Поднятие ошибок также работает корректно.

Cristian Buse 07.01.2021 08:49

@Greedo Отредактировал ответ, надеюсь, я прав в своем предположении. Кстати, ты всегда задаешь правильные вопросы. Отличная работа!

Cristian Buse 07.01.2021 11:56

@Greedo Я сделал еще одно редактирование. Надеюсь, на этот раз я правильно понял.

Cristian Buse 11.01.2021 14:13

@Greedo Ваш вопрос помог мне обновить логику в логике перенаправления экземпляра (см. Редактирование № 1). Спасибо вам за это! Кажется, что действительно точка входа в класс при раннем связывании имеет некоторые дополнительные механизмы безопасности, в отличие от позднего связывания.

Cristian Buse 11.02.2021 16:44

Другие вопросы по теме