Как я могу безопасно отцепить Win32 API, который блокирует?

у меня есть приложение он выполняет связку API с целевым приложением Win32 (которое использует ASIO) для наблюдения за трафиком именованных каналов, обработанным целью. Он устанавливает начало для транзакции с вызовом ReadFile или WriteFile и получает результат этого вызова, если вызов был сделан синхронно. Если вызов асинхронный, он также перехватывает завершение этого вызова через ловушку для GetQueuedCompletionStatus. Полученные данные отправляются в мой собственный пул потоков ASIO, который обслуживает соединение с именованным каналом к ​​Wireshark. Это на самом деле довольно мило.

Все отлично работает, кроме случаев, когда мне нужно вернуть программу в исходное состояние. Отключение функций работает нормально, за исключением того факта, что существующие вызовы могут быть заблокированы на неопределенный срок в GetQueuedCompletionStatus, если я не заставлю целевое приложение обрабатывать тонну запросов через именованный канал. Если я не буду ждать, пока эти потоки разблокируются, я вызову AV, когда GetQueuedCompletionStatus в конечном итоге разблокирует и вернется в пещеру, которой больше не существует.

Другая вещь, которую я попытался, состояла в том, чтобы отследить каждый вызов, сделанный GetQueuedCompletionStatus, и заставить функцию перехвата уведомлять сигнал всякий раз, когда параметр LpOverlapped объекта GetQueuedCompletionStatus соответствует соответствующему вызову PostQueuedCompletionStatus. Это разблокирует все, конечно, но это серьезно испортило код, который сделал вызов GetQueuedCompletionStatus, что привело к AV.

Кто-нибудь знает хороший способ справиться с этим? Если бы я мог сделать одно из следующего, это сработало бы:

  • Сделайте вызов с PostQueuedCompletionStatus, который ASIO будет игнорировать
  • Перехват фиктивного вызова, сделанного PostQueuedCompletionStatus, и перезапись возвращаемого значения в стеке
  • Создайте полупостоянную кодовую пещеру, содержащую батут для перехваченных блокирующих вызовов, который передает метод доступа к указателю на перехватывающие вызовы. Функция trampoline увидит, что если бы это значение не было нулевым, вместо этого она вызывала бы указатель этой функции, а не возвращала выполнение вызывающей стороне. После разблокировки диспетчерский код может установить указатель этой функции на адрес GetQueuedCompletionStatus, и тогда мы сможем вернуть правильный результат вызова вызывающей стороне.

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

0

Решение

Я сомневаюсь, что существует общее и безопасное решение, кроме простого сохранения загруженной DLL. Или воздерживаться от всех этих хуков в первую очередь … (пропустите до конца ответа для крупного спойлера!)


Аргумент совпадает с тем, что я написал в комментарии к статье.

Чтобы освободить библиотеку DLL, вы должны убедиться, что никто не выполняет код в библиотеке DLL, и что никто не собирается выполнять код в библиотеке DLL (кроме разрывающего потока, который делает то, что мы обсуждаем).

Возможно, код будет выполняться в DLL по двум причинам: либо мы собираемся вызвать его, либо собираемся вернуться к нему (или вернуться к коду, который будет возвращаться в DLL и т. Д.). Допустим, мы решили первую проблему, отцепив. Мы все еще остались со второй причиной. Может быть, мы уже внутри крючка.

Вы можете подумать о том, чтобы сделать что-то вроде этого:

  1. Выделите «пещеру кода», которая делает что-то вроде

    PreHookWriteFile:
    LOCK INC [ref_count]
    POP R15
    CALL HookWriteFile
    PostHookWriteFile:
    LOCK DEC [ref_count]
    JMP R15
    
  2. Крюк WriteFile с JMP [PreHookWriteFile]

  3. Выполните выпуск в выделенном потоке, который отсоединяет WriteFile, и подождите, пока пересчет упадет до нуля. Затем он освобождает пещеру кода.

Но есть две проблемы с этим. Во-первых, у нас нет места для хранения исходного обратного адреса. Мы не можем положить его в стек, так как HookWriteFile не ожидает увидеть его там, и мы не можем сохранить его в любом из энергонезависимых регистров, потому что нам нужно восстановить его в PostHookWriteFile прежде чем отскочить назад, но проблема в том, что у нас нет места для хранения вещей.

Во-вторых, поток релиза может заметить уменьшение до того, как произойдет переход, и преждевременно освободить кодовую пещеру.

Я не думаю, что имеет значение, насколько умными мы пытаемся быть __declspec(naked) функции или путем изменения прототипа), чтобы функции ловушки ожидали исходный адрес возврата в стеке. Проблема scond остается — вы уменьшаете счетчик ссылок до Вы возвращаетесь. Удалить код до того, как вы вернулись, небезопасно, но вы не можете уведомить о своем возвращении, поскольку после того, как вы вернулись, вы больше не контролируете ситуацию. Именно в этом причина FreeLibraryAndExitThread был создан.

Можно подумать о чем-то сумасшедшем, например, о приостановке всех потоков в DLL и прохождении их стеков, чтобы убедиться, что ни один из них не содержит кода из DLL, но это просто открывает еще одну банку с червями.


Но есть и хорошие новости.

Если вы действительно хотите отслеживать связь по именованному каналу и хотите ограничиться Windows 8 и более новыми версиями, есть решение, которое намного надежнее, документировано и поддерживается, занимает значительно меньше строк кода и не навязчиво. Коби Кахане написал мини-фильтр файловой системы, который делает именно это.

Это выходные события ETW, которые вы можете просматривать в Microsoft Message Analyzer или один из других инструментов, которые используют события ETW от провайдеров на основе манифеста. Если вам действительно нужно увидеть его в Wireshark, вы можете написать небольшого потребителя ETW, который будет обрабатывать события и отправлять их по каналу обратно в Wireshark. Это все еще будет легче, чем все эти крючки, и, конечно, безопаснее и намного менее грязно.

1

Другие решения

Других решений пока нет …