Что такое хук в программировании
Перейти к содержимому

Что такое хук в программировании

  • автор:

Хуки — это просто

Хуки — это технология перехвата вызовов функций в чужих процессах. Хуки, как и любая достаточно мощная технология, могут быть использованы как в благих целях (снифферы, аудио\видеограбберы, расширения функционала закрытого ПО, логирование, багфиксинг) так и со злым умыслом (трояны, кряки, кейлоггеры). О хуках уже не раз писали и на Хабре и не на Хабре. Но вот в чём беда — почему-то каждая статья о хуках буквально со второго абзаца начинает рассказывать о «таблице виртуальных функций», «архитектуре памяти» и предлагает к изучению огромные блоки ассемблерного кода. Известно, что каждая формула в тексте снижает количество читателей вдвое, а уж такие вещи — так и вовсе вчетверо. Поэтому нужна статья, которая расскажет о хуках просто. Под катом нет ассемблера, нет сложных терминов и буквально два десятка строк очень простого кода на С++. Если вы давно хотели изучить хуки, но не знали с чего начать — начните с этой статьи.

Реальная задача

Для лучшего понимания того, что мы делаем — поставим себе какую-нибудь реальную задачу. Давайте, например сделаем так, чтобы браузер Firefox при заходе на Хабр писал в своём заголовке «Привет, Хабр!» вместо того, что там пишется сейчас (а сейчас там пришется «*** / Хабрахабр — Mozilla Firefox», где *** — меняется в зависимости от раздела). Да, я знаю, что это можно сделать правкой исходников Firefox, браузерными плагинами, юзерскриптами и еще десятком способов. Но мы в учебных целях сделаем это хуками.

Совсем чуть-чуть теории

Когда Вы запускаете любое приложение — операционная система создаёт его процесс. Грубо говоря, exe-файл копируется в память, далее определяется какие именно библиотеки (dll-файлы) ему нужны для работы (эта информация записана в начале каждого exe-файла), эти библиотеки ищутся (в папке с программой и в системных папках) и загружаются в память процесса. Потом определяется, какие именно функции библиотек использует программа и где они находятся (в какой библиотеке и где именно в этой библиотеке). Строится табличка вида «функция SomeFunction1() — библиотека SomeLibrary1.dll — %адрес_функции_SomeFunction1()%». Когда программе понадобиться вызвать эту функцию — она найдет в своей памяти нужную библиотеку, отсчитает нужный адрес и передаст туда управление.

Суть хукинга — заставить программу поверить, что нужная ей функция находится в другом месте.

Делается это таким образом — мы пишем свою библиотеку SomeLibrary2.dll, в которой будет находится наша функция SomeFunction2(). Далее мы загружаем эту библиотеку в память чужого процесса (в ОС Windows есть специальная функция для этого) и изменяем ту самую табличку, о которой я писал чуть выше, так, чтобы теперь она содержала запись «функция SomeFunction1() — библиотека SomeLibrary2.dll — %адрес_нашей_функции_SomeFunction2()%». Для того, чтобы понять, как вручную сделать всё описанное в этом абзаце, нужно знать весьма прилично всего — как устроена память в Windows, как вызываются функции, как им передаются аргументы и т.д. Это сложно. Ну на самом деле не очень, просто можно обойтись и без этого. Если вам это нужно — почитайте какую-нибудь продвинутую статью (а хоть бы из тех, что указаны в начале). Мы пойдем другим путем — используем готовую библиотеку Microsoft Detours, которая сделает всю грязную работу за нас.

Пару слов о Microsoft Detours

Проста в изучении и использовании
Весьма эффективна
Хорошая документация
Содержит много примеров в исходниках
Разработана Microsoft — неплохо «дружит» с ОС
Бесплатна для исследовательских целей и некоммерческих проектов
Не требует знания ассемблера

Закрыта
Стоит приличных денег для коммерческого использования или х64-архитектуры

В целом, я бы посоветовал начинать изучение хуков именно с Detours — если это будет всего лишь вашим разовым развлечением, то этого вполне хватит, у вас быстро всё получится и вам понравится. Если же хуки понадобятся в серьёзном проекте — вы легко переключитесь на бесплатные и открытые (но чуть более сложные) библиотеки типа mhook, купите Detours или напишете свой велосипед (для последних двух решений нужны весьма веские причины).
О том где взять и как собрать Detours я писал вот тут.

Хитрый план
  1. Понять, на какую функцию ставить хук.
  2. Сделать свою библиотеку с функцией, которая будет заменять исходную и делать нужные нам вещи.
  3. Установить хук (загрузить библиотеку в память нужного процесса и переставить указатель на нужную нам функцию).
  4. PROFIT!
Куда ставить хук
  1. Вместо SendMessage может использоваться PostMessage или что-то еще
  2. SendMessage может быть вообще не функцией, а макросом, ссылающимся на другую функцию (в дальнейшем мы увидим, что так оно и есть)
  3. Firefox, как некоторые кроссплатформенные приложения, может вообще не использовать функции Windows для рисования стандартных элементов окна, используя вместо этого какие-то собственные кросплатформенные элементы GUI (к счастью, это не так — но вдруг!)

Переходим в Firefox, открываем Хабр, дожидаемся изменения заголовка на нужный и возвращаемся в Api Monitor чтобы остановить мониторинг. Скорее всего, вы будете удивлены количеством вызванных функций — их могут быть сотни тысяч буквально за несколько секунд мониторинга. А мы ведь еще и следим далеко не за всем. Да-да, это всё реально происходит внутри безобидного открытия всего одного сайта в браузере! А вы еще жалуетесь, что эта пара секунд — слишком долго. 🙂

Найти нужную нам функцию поможет поиск по вкладке с результатами мониторинга. Вбиваем в поиск «WM_SETTEXT» и убеждаемся, что действительно имеются вызовы функции SendMessageW с этим параметром — с высокой вероятностью это и есть установка заголовка окна. Обратите внимание на «W» в конце названия функции — оно означает, что используется её юникодная версия. Для установки хуков важно знать точное имя подменяемой функции и теперь мы его знаем.

Делаем свою библиотеку

1. Запускаем Visual Studio.
2. Создаём новый проект: File->New->Project. Тип Visual C++ -> Win32 -> Win32 Project. В диалоге создания проекта указываем тип «Dll».
3. Открываем файл dllmain.cpp и пишем туда вот такой код:

#include #include "C:\Program Files\Microsoft Research\Detours Express 3.0\src\detours.h" LRESULT (WINAPI * TrueSendMessageW)(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) = SendMessageW; __declspec(dllexport) LRESULT WINAPI MySendMessageW(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) < if (Msg == WM_SETTEXT && wcsstr((LPCTSTR)lParam, L"/ Хабрахабр - Mozilla Firefox") != NULL) return TrueSendMessageW(hWnd, Msg, wParam, (LPARAM)L"Привет, Хабр!"); return TrueSendMessageW(hWnd, Msg, wParam, lParam); >BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved) < if (dwReason == DLL_PROCESS_ATTACH) < DetourRestoreAfterWith(); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)TrueSendMessageW, MySendMessageW); DetourTransactionCommit(); >else if (dwReason == DLL_PROCESS_DETACH) < DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)TrueSendMessageW, MySendMessageW); DetourTransactionCommit(); >return TRUE; > 

4. Открываем свойства проекта и на вкладке настроек линкера добавляем в поле Additional Dependencies значение «C:\Program Files\Microsoft Research\Detours Express 3.0\lib.X86\detours.lib». Внимание, у вас путь может быть другой — смотря куда установили библиотеку Detours.

5. Компилируем проект: Build -> Build Solution. На выходе получаем длл-ку (пусть будет называться hooktest.dll)

Давайте разберем исходник. В начале мы подключаем заголовочные файлы Windows (чтобы пользоваться функцией SendMessageW) и Detours (чтобы иметь возможность ставить\снимать хуки).
В сложной на первый взгляд строке №3 мы всего лишь сохраняем реальный указатель на функцию SendMessageW в переменную TrueSendMessageW. Это нам понадобиться для двух целей:

  1. Для вызова настоящей функции SendMessageW из нашей «подделки».
  2. Для восстановления указателя на реальную функцию в момент, когда мы захотим снять хук.

Функция DllMain вызывается операционной системой в определенных случаях — например, в моменты аттача\детача библиотеки к процессу. Тут тоже всё просто. В момент аттача нам нужно установить хуки, в момент детача — снять. Библиотека Detour требует делать это транзакциями, и в этом есть смысл — представьте себе что будет, если сразу несколько желающих захотят поставить хуки в один процесс. Самое важное в этом коде это строка

DetourAttach(&(PVOID&)TrueSendMessageW, MySendMessageW); 

Именно она заставляет процесс «поверить» что теперь вместо настоящей функции SendMessageW нужно вызывать нашу MySendMessageW. Ради этой строки всё и затевалось. Если кому интересно, однажды я писал аналог этой функции вручную. С учетом всех возможных комбинаций типов функций и архитектур это заняло у меня несколько недель. Вы вот только что их сэкономили — поздравляю.

Устанавливаем хук

Microsoft Detours предлагает разные варианты установки хуков — мы воспользуемся самым простым. В комплекте примеров, которые идут с библиотекой, есть программа withdll.exe — она принимает в качестве параметров путь к приложению и библиотеку, которую нужно подгрузить в память этого приложения после его запуска. Запускаем всё это как-то вот так:

withdll.exe -d:hooktest.dll "C:\Program Files\Mozilla Firefox\firefox.exe" 
PROFIT!

Успехов в изучении хуков.

Хуки. Управление функциональными компонентами

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

Мы можем создавать свои хуки, однако React по умолчанию уже предоставляет ряд встроенных хуков:

  • useState : предназначен для управления состоянием компонентов
  • useEffect : предназначен для перехвата различного рода изменений в компонентах, которые нельзя обработать внутри компонентов
  • useContext : позволяет подписываться на контекст React
  • useReducer : позволяет управлять локальным состоянием сложных компонентов
  • useCallback : позволяет управлять функциями обратного вызова
  • useMemo : предназначен для управления мемоизированными (грубо говоря кэшированными) значениями
  • useRef : возвращать некоторое изменяемое значение, например, ссылку на html-элементы DOM, которыми затем можно управлять в коде JavaScript
  • useImperativeHandle : настраивает объект, который передается родительскому компоненту при использовании ref
  • useLayoutEffect : аналогичен хуку useEffect() , но вызывается синхронно после всех изменений в структуре DOM
  • useDebugValue : предназначен для отображения некоторого значения в целях отладки
  • useId : предназначен для генерации уникального идентификатора
  • useTransition : применяется для создания переходов при рендеринге
  • useDeferredValue : позволяет отложить рендеринг некритичных частей структуры DOM
  • useSyncExternalStore : предназначен для синхронизации данных с внешними хранилищами
  • useInsertionEffect : предназначен для библиотек, которые используют CSS в JS, для оптимизации при внедрении стилей при рендеринге

Переход от классов к хукам

Рассмотрим простейший пример, как мы можем перейти от классов к хукам. Допустим, у нас есть следующий класс-компонент:

    METANIT.COM        

Здесь определен компонент ClickButton, который принимает через props некоторое значение increment . В конструкторе определяется состояние в виде переменной counter , которая равна 0. Кроме того, в классе определяется метод press() , в котором изменяется состояние компонента.

Для изменения состояния вызывается другой метод — incrementCounter , который берет из props значение increment и использует его для увеличения значения переменной counter .

В коде класса-компонента определяется кнопка, по нажатию на которую как раз и вызывается метод press() :

В итоге по нажатию на кнопку мы получим увеличение переменной counter :

Переход от классов-компонентов к хукам в React

Теперь определим аналогичный компонент с использованием хуков:

    METANIT.COM        

В данном случае определен функциональный компонент ClickButtonHook , так как мы не можем использовать хуки в классах-компонентах, а только в функциях.

Вначале определяем переменные состояния:

const [count, setCount] = React.useState(0);

В данном случае определяются две переменных: count и setCount . Переменная count хранит состояние компонента, а переменная setCount позволяет изменять значение переменной count.

В функцию useState() передается число 0 — это то значение, которое по умолчанию получает переменная count .

Для изменения состояния в компоненте определена функция press() , которая выполняет выражение setCount(count + props.increment); — к переменной count прибавляется значение increment из props . Это выражение ( count + props.increment ) и определяет новое значение переменной count . Таким образом, состояние компонента ClickButtonHook будет изменено.

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

Подключение хуков

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

import React, < useState >from "react"; function ClickButtonHook(props) < const [count, setCount] = React.useState(0); const press= () =>setCount(count + props.increment); return 
Counter:
Increment:
; >

Ограничения при использовании хуков

Хуки имеют ряд ограничений при определении и использовании:

  • Хуки вызываются только на верхнем уровне (top-level) компонента. Они НЕ вызываются внутри циклов, условных конструкций, внутри стандартных функций javascript.
  • Хуки можно вызывать только из функциональных компонентов React, либо из других хуков. Но их нельзя вызывать из классов-компонентов. Функциональные компоненты можно определять как обычные функции:

function ClickButtonHook(props) < const [count, setCount] = React.useState(0); const press= () =>setCount(count + props.increment); return 
Counter:
Increment:
; >

Либо в виде стрелочных функций:

const ClickButtonHook = (props)=> < const [count, setCount] = React.useState(0); const press= () =>setCount(count + props.increment); return 
Counter:
Increment:
; >

Что такое хуки и как их использовать: краткий гайд с примерами

Хуки — это технология, которая перехватывает вызовы функций и помогает использовать возможности React без написания классов.

Детям из Мариуполя нужно 120 ноутбуков для обучения — подари старое «железо», пусть оно работает на будущее Украины

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

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

Встроенные хуки

Хуки можно создавать самостоятельно, но в React существует ряд встроенных хуков, которыми можно пользоваться прямо сейчас.

useState()

Хук состояния — useState() — добавляет динамическую логику функциональных компонентов:

Курс QA Manual.

Успішна кар’єра в IT-сфері без необхідності глибоких знань програмування. Очікуйте заробітню плату в розмірі 600$ після завершення навчання.

const [state, setState] = useState(initialState);

Хук возвращает функцию и значение состояния для обновления.

Пример. Рендеринг счетчика. При нажатии на кнопку его значение увеличится:

import React, < useState >from 'react'; function Example() < // Объявляем новую переменную состояния "count" const [count, setCount] = useState(0); return ( 

Вы нажали раз

); >

Функциональный компонент наделяется внутренним состоянием, которое будет сохраняться между рендерами.

useCallback()

Хук useCallback() оптимизирует рендеринг компонентов:

const memoizedCallback = useCallback( () => < doSomething(a, b); >, [a, b], );

А также возвращает мемоизированную версию Callback (когда результат выполнения сохраняется). При этом массив зависимостей не передается.

useContext()

Хук useContext() передает данные дочерним элементам:

const value = useContext(MyContext);

А также возвращает текущее значение контекста.

useMemo()

Хук useMemo() позволяет запоминать функции без необходимости их вызова при каждом новом рендеринге:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

А еще — возвращает мемоизированное значение.

Це хороший спосіб розвитку вашої кар’єри в IT-індустрії. Після проходження курсу Mate гарантує вам офер мрії.

Функция, которая передается хуком useMemo , запускается при рендере.

useEffect()

Хук эффекта — useEffect() — выполняет в компонентах функций такие эффекты как вызов API, запросов и подобные:

useEffect(didUpdate);

Функция, которую передает useEffect() , запускается только после фиксации рендера на экране.

Пример. Установка компонентом заголовка документа после обновления DOM:

import React, < useState, useEffect >from 'react'; function Example() < const [count, setCount] = useState(0); // По принципу componentDidMount и componentDidUpdate: useEffect(() => < // Обновляем заголовок документа, используя API браузера document.title = `Вы нажали $раз`; >); return ( 

Вы нажали раз

); >

React запускает функции с эффектами после каждого рендера.

useReduce()

Хук useReduce() — альтернатива хуку useState() , но используется для сложной логики состояния, управляет внутренним состоянием более сложного компонента:

function Todos() < const [todos, dispatch] = useReducer(todosReducer); // .

useRef()

Хук useRef() создает изменяемую переменную (упрощает доступ к элементам React и узлам DOM):

const refContainer = useRef(initialValue);

Хук возвращает ref-объект, который будет сохраняться в течение времени жизни самого компонента.

Пример. Доступ к потомку:

function TextInputWithFocusButton() < const inputEl = useRef(null); const onButtonClick = () =>< // `current` указывает на смонтированный элемент `input` inputEl.current.focus(); >; return ( <> type="text" />  ); >

Хук useRef() содержит изменяемое значение .current . При каждом рендере хук useRef() дает один и тот же объект с ref .

Курс Delivery Management.

За 2 місяці на практиці прокачаєте делівері-майндсет, щоб ефективно управляти проєктами та будувати стійкі бізнес-процеси.

useImperativeHandle()

Хук useImperativeHandle() настраивает объект при использовании ref , передающийся родительскому компоненту.

Пример. Использование useImperativeHandle с forwardRef:

function FancyInput(props, ref) < const inputRef = useRef(); useImperativeHandle(ref, () =>( < focus: () => < inputRef.current.focus(); >>)); return . />; > FancyInput = forwardRef(FancyInput)

Хук useImperativeHandle() контролирует возвращаемое значение. Довольно часто этот хук используют в библиотеках компонентов, где нужна кастомизация поведения элементов DOM.

useDebugValue()

Хук useDebugValue() отображает значение для отладки.

Пример. Создание своего React-хука:

import React, from 'react' function useRandomNumber(min, max) < const [number, setNumber] = useState(null); useEffect(() =>< const range = Math.random() * (max - min) + min setNumber(Math.floor(range)) >, []) useDebugValue(number > 50 ? 'Number more the 50' : 'Number less then 50'); return number; > const UseDebugValuePage = () => < const number = useRandomNumber(1, 100) return ( 

useDebugValue

Random number:

) > export default UseDebugValuePage

Хук useDebugValue() будет вызван тогда, когда будет открыт React DevTools.

useLayoutEffect()

Хук useLayoutEffect() — альтернатива хука useEffect() , но вызывается в DOM после изменений в структуре:

import React, from 'react' const UseLayoutEffectPage = () => < const ref = React.useRef() useEffect(() =>< ref.current.value = 'New value' console.log('useEffect'); >) useLayoutEffect(() => < console.log(ref.current.value) >) return ( 

useLayoutEffect

value='Old value' />
) > export default UseLayoutEffectPage

Хук useLayoutEffect() запускается в React только после фиксации в DOM всех обновлений.

Правила хуков

Хуки — это тоже функции JavaScript , но чтобы их правильно использовать, нужно следовать кое-каким правилам:

  • Хуки можно вызывать исключительно на верхнем уровне внутри компонента. Они не могут быть вызваны из вложенных функций или внутри других циклов.
  • Хуки вызываются только из функциональных компонентов React.
  • Используются вместо классов, внутри них они не работают.

Как создать хуки?

Пусть в компоненте, принадлежащем чату (приложению), отображается уведомление о нахождении пользователя «В сети»:

import React, < useState, useEffect >from 'react'; function FriendStatus(props) < const [isOnline, setIsOnline] = useState(null); useEffect(() => < function handleStatusChange(status) < setIsOnline(status.isOnline); >ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => < ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); >; >); if (isOnline === null) < return 'Загрузка. '; >return isOnline ? 'В сети' : 'Не в сети'; >

Давайте сделаем имена пользователей из списка контактов, которые находятся онлайн, зелеными.

Чтобы решить эту задачу, в компонент FriendListItem можно скопировать логику, которая приведена выше:

import React, < useState, useEffect >from 'react'; function FriendListItem(props) < const [isOnline, setIsOnline] = useState(null); useEffect(() => < function handleStatusChange(status) < setIsOnline(status.isOnline); >ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => < ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); >; >); return ( >> ); >

Конечно, этот способ совсем не простой и короткий.

Поделимся логикой с FriendStatus и FriendListItem . В React существует два способа разделения логики:

  • render props;
  • компоненты высшего порядка.

Но использование хуков избавляет от добавления дополнительных функций в дерево компонентов.

Разделить логику между двумя функциями JavaScript можно, если извлечь ее в еще одну, третью функцию.

Первый пользовательский хук — useFriendStatus() :

import < useState, useEffect >from 'react'; function useFriendStatus(friendID) < const [isOnline, setIsOnline] = useState(null); useEffect(() => < function handleStatusChange(status) < setIsOnline(status.isOnline); >ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => < ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); >; >); return isOnline; >

Этот хук подписывает нас на статус пользователя. friendID является здесь аргументом:

function useFriendStatus(friendID) < const [isOnline, setIsOnline] = useState(null); // . return isOnline; >

Наша изначальная цель — удалить повторяющуюся логику из FriendStatus и FriendListItem .

Когда логика извлечена из useFriendStatus , можно ее использовать:

function FriendStatus(props) < const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) < return 'Loading. '; >return isOnline ? 'Online' : 'Offline'; > function FriendListItem(props) < const isOnline = useFriendStatus(props.friend.id); return ( >> ); >

Что здесь произошло? Мы извлекли код из двух функций и поместили в отдельную функцию.

Важно! Используйте use в начале каждого хука. Это нужно для автоматической проверки правил хуков.

Хуки, используемые в двух компонентах, не имеют одинакового состояния. Каждый вызов хука изолирован.

Как мы уже говорили, хуки — это те же функции. А потому можно передавать информацию между ними.

Давайте рассмотрим пример использования компонента, демонстрирующего, находится ли пользователь сейчас онлайн:

const friendList = [ < id: 1, name: 'Ольга' >, < id: 2, name: 'Жанна' >, < id: 3, name: 'Елизавета' >, ]; function ChatRecipientPicker() < const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID); return ( <> />  ); >

Идентификатор пользователя сохраняется в recipientID и обновляется в случае выбора в другого пользователя.

Хук useState() возвращает значение recipientID , а значит его можно передать в useFriendStatus в качестве аргумента:

const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID);

Так можно узнать, находится ли пользователь в сети.

Пользовательские хуки гибкие, их можно использовать в связке друг с другом.

Ограничения при использовании хуков

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

Например, в одном компоненте можно использовать сразу несколько хуков эффектов или состояний:

function Form() < // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use an effect for persisting the form useEffect(function persistForm() < localStorage.setItem('formData', name); >); // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // 4. Use an effect for updating the title useEffect(function updateTitle() < document.title = name + ' ' + surname; >); // . >

React определяет порядок, при котором вызываются хуки. Именно так он узнает, какому useState() какое состояние соответствует. Поскольку в примере порядок вызовов одинаков при каждом рендере, он является рабочим:

// ------------ // First render // ------------ useState('Mary') // 1. Initialize the name state variable with 'Mary' useEffect(persistForm) // 2. Add an effect for persisting the form useState('Poppins') // 3. Initialize the surname state variable with 'Poppins' useEffect(updateTitle) // 4. Add an effect for updating the title // ------------- // Second render // ------------- useState('Mary') // 1. Read the name state variable (argument is ignored) useEffect(persistForm) // 2. Replace the effect for persisting the form useState('Poppins') // 3. Read the surname state variable (argument is ignored) useEffect(updateTitle) // 4. Replace the effect for updating the title // .

Но что получится, если поместить хук в условие?

// �� We're breaking the first rule by using a Hook in a condition if (name !== '') < useEffect(function persistForm() < localStorage.setItem('formData', name); >); >

Условие (name !== '') имеет статус true для первого рендера. Запускаем хук. Но при следующем рендеринге пользователь может очистить форму, и тогда условие будет false. Порядок вызовов хука изменен:

useState('Mary') // 1. Read the name state variable (argument is ignored) // useEffect(persistForm) // �� This Hook was skipped! useState('Poppins') // �� 2 (but was 3). Fail to read the surname state variable useEffect(updateTitle) // �� 3 (but was 4). Fail to replace the effect

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

Компоненты чаще всего определяются как обычные функции:

function ClickButtonHook(props) < const [count, setCount] = React.useState(0); const press= () =>setCount(count + props.increment); return 
Counter:
Increment:
; >

Также они могут определяться в виде стрелочных функций:

const ClickButtonHook = (props)=> < const [count, setCount] = React.useState(0); const press= () =>setCount(count + props.increment); return 
Counter:
Increment:
; >

Как подключать хуки

При условии, если компоненты расположены в отдельных файлах, встроенные хуки можно подключить, используя такой способ:

import React, < useState >from "react"; function ClickButtonHook(props) < const [count, setCount] = React.useState(0); const press= () =>setCount(count + props.increment); return 
Counter:
Increment:
; >

Заключение

В сравнении с классами, у хуков есть ряд своих преимуществ. Упрощается логика жизненного цикла, появляются возможности гибкой оптимизации (мемоизация), нет необходимости сохранять имена методов и работать с прототипами.

Кастомные хуки позволяют быстрее и легче шейрить логику (она сразу закладывается в хуки). Это ведет к уменьшению вложенности дерева компонентов. React получает возможность пристальнее отслеживать изменения в компонентах.

Код, в котором используются хуки, прекрасно поддается минимизации. Хуки позволяют выделить одну логику, которая будет управлять определенным побочным эффектом.

Без них такая логика при наличии нескольких побочных эффектов в компоненте разбивалась бы на разные методы жизненного цикла. А благодаря мемоизации компоненты не будут обновлены или пересозданы. В memo можно поместить все функциональные компоненты.

Но наравне с широкими возможностями хуки имеют и ограничения. Стоит хорошо разобраться в том, как они работают, особенно понять, что такое мемоизация. Только тогда можно научиться писать читабельный и лаконичных код с использованием хуков.

Курс Business English для проджект-менеджерів.

Курс на якому ви здолаєте всі бар’єри кроскультурної комунікації, навчитеся розв’язувати конфлікти, попереджати ризики та ефективно презентувати результати роботи іноземним стейкхолдерам.

Windows hook: просто о сложном

image

Что такое хук?
Что такое хук функций и для чего он нужен? В переводе с английского «hook» — ловушка. Поэтому о назначении хуков функции в Windows можно догадаться — это ловушка для функции. Иными словами, мы ловим функцию и берем управление на себя. После этого определения нам открываются заманчивые перспективы: мы можем перехватить вызов любой функции, подменить код на свой, тем самым изменив поведение любой программы на то, которое нам нужно (конечно, в рамках определенных ограничений).

Целью данной статьи является демонстрация установки хука и его непосредственная реализация.

— Нельзя поверить в невозможное!
— Просто у тебя мало опыта, – заметила Королева. – В твоем возрасте я уделяла этому полчаса каждый день! В иные дни я успевала поверить в десяток невозможностей до завтрака!

Где мне реально пригодились эти знания

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

• Контроль входящего http-траффика и подмена «взрослого» контента на более безобидный.
• Логирование информации в случае копирования каких-либо файлов с подконтрольной сетевой папки.
• Незначительная модификация кода в проекте, от которого были утеряны исходники (да, и такое тоже случается)

Методы установки хуков

Давайте перейдем от общих фраз к более детальному рассмотрению хуков. Мне известно несколько разновидностей реализации хука:

● Использование функции SetWindowsHookEx. Это весьма простой, оттого и ограниченный, метод. Он позволяет перехватывать только определенные функции, в основном связанные с окном (например, перехват событий, которые получает окно, щелчков мышкой, клавиатурного ввода). Достоинством этого метода является возможность установки глобальных хуков (например, сразу на все приложениях перехватывать клавиатурный ввод).
● Использование подмены адресов в разделе импорта DLL. Суть метода заключается в том, что любой модуль имеет раздел импорта, в котором перечислены все используемые в нем другие модули, а также адреса в памяти для экспортируемых этим модулем функций. Нужно подменить адрес в этом модуле на свой и управление будет передано по указанному адресу.
● Использование ключа реестра HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_Dlls. В нем необходимо прописать путь к DLL, но сделать это могут только пользователи с правами администратора. Этот метод хорош, если приложение не использует kernel32.dll (нельзя вызвать функцию LoadLibrary).
● Использование инъектирования DLL в процесс. На мой взгляд, это самый гибкий и самый показательный способ. Его-то мы и рассмотрим более подробно.

Метод инъектирования

Инъектирование возможно, потому что функция ThreadStart, которая передается функции CreateThread, имеет схожую сигнатуру с функцией LoadLibrary (да и вообще структура dll и исполняемого файла очень схожи). Это позволяет указать метод LoadLibrary в качестве аргумента при создании потока.

Алгоритм инъектирования DLL выглядит так:

1. Находим адрес функции LoadLibrary из Kernel32.dll для потока, куда мы хотим инжектировать DLL.
2. Выделяем память для записи аргументов этой функции.
3. Создаем поток и в качестве ThreadStart функции указываем LoadLibrary и ее аргумент.
4. Поток идет на исполнение, загружает библиотеку и завершается.
5. Наша библиотека инъектирована в адресное пространство постороннего потока. При этом при загрузке DLL будет вызван метод DllMain с флагом PROCESS_ATTACH. Это как раз то место, где можно установить хуки на нужные функции. Далее рассмотрим саму установку хука.

Установка хука

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

1. Находим адрес функции, вызов которой мы хотим перехватывать (например, MessageBox в user32.dll).
2. Сохраняем несколько первых байтов этой функции в другом участке памяти.
3. На их место вставим машинную команду JUMP для перехода по адресу подставной функции. Естественно, сигнатура функции должна быть такой же, как и исходной, т. е. все параметры, возвращаемое значение и правила вызова должны совпадать.
4. Теперь, когда поток вызовет перехватываемую функцию, команда JUMP перенаправит его к нашей функции. На этом этапе мы можем выполнить любой нужный код.

Далее можно снять ловушку, вернув первые байты из п.2 на место.

Итак, теперь нам понятно, как внедрить нужную нам DLL в адресное пространство потока и каким образом установить хук на функцию. Теперь попробуем совместить эти подходы на практике.

Тестовое приложение

Наше тестовое приложение будет довольно простым и написано на С#. Оно будет содержать в себе кнопку для показа MessageBox. Для примера, установим хук именно на эту функцию. Код тестового приложения:

public partial class MainForm : Form < [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type); public MainForm() < InitializeComponent(); this.Text = "ProcessID: " + Process.GetCurrentProcess().Id; >private void btnShowMessage_Click(Object sender, EventArgs e) < MessageBox(new IntPtr(0), "Hello World!", "Hello Dialog", 0); >> 

В качестве инъектора рассмотрим два варианта. Инъекторы, написанные на С++ и С#. Почему на двух языках? Дело в том, что многие считают, что С# — это язык, в котором нельзя использовать системные вещи, — это миф, можно :). Итак, код инъектора на С++:

#include "stdafx.h" #include #include #include int Wait(); int main() < // Пусть до библиотеки, которую хотим инъектировать. DWORD processId = 55; char* dllName = "C:\\_projects\\CustomHook\\Hooking\\Debug\\HookDll.dll"; // Запрашиваем PID процесса куда хотим инъектировать. printf("Enter PID to inject dll: "); std::cin >> processId; // Получаем доступ к процессу. HANDLE openedProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); if (openedProcess == NULL) < printf("OpenProcess error code: %d\r\n", GetLastError()); return Wait(); >// Ищем kernel32.dll HMODULE kernelModule = GetModuleHandleW(L"kernel32.dll"); if (kernelModule == NULL) < printf("GetModuleHandleW error code: %d\r\n", GetLastError()); return Wait(); >// Ищем LoadLibrary (Суффикс A означает что работаем в ANSI, один байт на символ) LPVOID loadLibraryAddr = GetProcAddress(kernelModule, "LoadLibraryA"); if (loadLibraryAddr == NULL) < printf("GetProcAddress error code: %d\r\n", GetLastError()); return Wait(); >// Выделяем память под аргумент LoadLibrary, а именно - строку с адресом инъектируемой DLL LPVOID argLoadLibrary = (LPVOID)VirtualAllocEx(openedProcess, NULL, strlen(dllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (argLoadLibrary == NULL) < printf("VirtualAllocEx error code: %d\r\n", GetLastError()); return Wait(); >// Пишем байты по указанному адресу. int countWrited = WriteProcessMemory(openedProcess, argLoadLibrary, dllName, strlen(dllName), NULL); if (countWrited == NULL) < printf("WriteProcessMemory error code: %d\r\n", GetLastError()); return Wait(); >// Создаем поток, передаем адрес LoadLibrary и адрес ее аргумента HANDLE threadID = CreateRemoteThread(openedProcess, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryAddr, argLoadLibrary, NULL, NULL); if (threadID == NULL) < printf("CreateRemoteThread error code: %d\r\n", GetLastError()); return Wait(); >else < printf("Dll injected!"); >// Закрываем поток. CloseHandle(openedProcess); return 0; > int Wait() < char a; printf("Press any key to exit"); std::cin >> a; return 0; > 

Теперь тоже самое, но только на С#. Оцените, насколько код более компактен, нет буйства типов (HANDLE, LPVOID, HMODULE, DWORD, которые, по сути, означают одно и тоже).

public class Exporter < [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out IntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out IntPtr lpThreadId); [DllImport("kernel32.dll", SetLastError = true)] public static extern Int32 CloseHandle(IntPtr hObject); >public class Injector < public static void Inject(Int32 pid, String dllPath) < IntPtr openedProcess = Exporter.OpenProcess(ProcessAccessFlags.All, false, pid); IntPtr kernelModule = Exporter.GetModuleHandle("kernel32.dll"); IntPtr loadLibratyAddr = Exporter.GetProcAddress(kernelModule, "LoadLibraryA"); Int32 len = dllPath.Length; IntPtr lenPtr = new IntPtr(len); UIntPtr uLenPtr = new UIntPtr((uint)len); IntPtr argLoadLibrary = Exporter.VirtualAllocEx(openedProcess, IntPtr.Zero, lenPtr, AllocationType.Reserve | AllocationType.Commit, MemoryProtection.ReadWrite); IntPtr writedBytesCount; Boolean writed = Exporter.WriteProcessMemory(openedProcess, argLoadLibrary, System.Text.Encoding.ASCII.GetBytes(dllPath), uLenPtr, out writedBytesCount); IntPtr threadIdOut; IntPtr threadId = Exporter.CreateRemoteThread(openedProcess, IntPtr.Zero, 0, loadLibratyAddr, argLoadLibrary, 0, out threadIdOut); Exporter.CloseHandle(threadId); >> 

Инъектируемая библиотека

Теперь самое интересное — код библиотеки, которая устанавливает хуки. Эта библиотека написана на С++, пока без аналога на C#.

// dllmain.cpp : Defines the entry point for the DLL application. #include "stdafx.h" #include #define SIZE 6 // Объявления функций и кастомных типов typedef int (WINAPI *pMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT); int WINAPI MyMessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT); void BeginRedirect(LPVOID); pMessageBoxW pOrigMBAddress = NULL; BYTE oldBytes[SIZE] = < 0 >; BYTE JMP[SIZE] = < 0 >; DWORD oldProtect, myProtect = PAGE_EXECUTE_READWRITE; BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) < switch (ul_reason_for_call) < case DLL_PROCESS_ATTACH: // Уведомим пользователя что мы подключились к процессу. MessageBoxW(NULL, L"I hook MessageBox!", L"Hello", MB_OK); // Идем адрес MessageBox pOrigMBAddress = (pMessageBoxW)GetProcAddress(GetModuleHandleW(L"user32.dll"), "MessageBoxW"); if (pOrigMBAddress != NULL) < BeginRedirect(MyMessageBoxW); >break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; > return TRUE; > void BeginRedirect(LPVOID newFunction) < // Массив-маска для записи команды перехода BYTE tempJMP[SIZE] = < 0xE9, 0x90, 0x90, 0x90, 0x90, 0xC3 >; memcpy(JMP, tempJMP, SIZE); // Вычисляем смещение относительно оригинальной функции DWORD JMPSize = ((DWORD)newFunction - (DWORD)pOrigMBAddress - 5); // Получаем доступ к памяти VirtualProtect((LPVOID)pOrigMBAddress, SIZE, PAGE_EXECUTE_READWRITE, &oldProtect); // Запоминаем старые байты memcpy(oldBytes, pOrigMBAddress, SIZE); // Пишем 4байта смещения. Да, код рассчитан только на x86 memcpy(&JMP[1], &JMPSize, 4); // Записываем вместо оригинальных memcpy(pOrigMBAddress, JMP, SIZE); // Восстанавливаем старые права доступа VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL); > int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType) < // Получаем доступ к памяти VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL); // Возвращаем старые байты (иначе будет переполнение стека) memcpy(pOrigMBAddress, oldBytes, SIZE); // Зовем оригинальную функцию, но подменяем заголовок int retValue = MessageBoxW(hWnd, lpText, L"Hooked", uiType); // Снова ставим хук memcpy(pOrigMBAddress, JMP, SIZE); // Восстанавливаем старые права доступа VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL); return retValue; >

Ну и несколько картинок напоследок. До установки хука:

image

И после установки:

image

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *