Хуки и выделение памяти

D3ad_Parad15e

Новичок
Автор темы
4
0
Добрый вечер. Почитал я несколько статей о хуках. Для создания обратного перехода (чтобы можно было вернуться в перехваченную функцию из своей собственной) - выделяется память при помощи VirtualAlloc, в которую записываются байты, на место которых пишется сам перехват. Как я понимаю, совершается это для того, чтобы после этого вернуться в функцию, которая хукается - и продолжить выполнения её кода так, словно ничего и не было.

* Вопрос первый: есть ли какие-то различия между new[] и VirtualAlloc в данном случае? Искал информацию в интернете по этому поводу - и понял так, что VirtualAlloc выделяет именно фиксированное число памяти, в то время как new при первом разе выделяет какое-то большое количество памяти, а потом, при каждом новом вызове - хватает из выделенном буфера память, оттого и работает он при каждом вызове быстрее, чем VirtualAlloc. Поправьте, если ошибаюсь. Мне просто интересно, почему используют именно его, а не operator new.

* Вопрос второй: фактически, можно ведь создать что-то вроде своего обработчика через naked? То есть, по сути, записать в naked функцию pusha, сделать jmp (или call, но здесь уже понадобится реализация передачи параметров) на функцию со своим кодом, а после вернуться на это же место, выполнить popa, выполнить код тех опкодов, которые были затёрты хуком и продолжить работу функции. Так вот, вопрос такой: будет ли это иметь разницу в производительности? И, как я понимаю, из-за этого уже не получится делать хуки при помощи операторов и лямбд (именно тех лямбд, в которых описан код, который должен выполняться при переходе).

* И третий вопрос. В случае, если вариант два является более производительным за счёт того, что там не выделяется динамически память, возможно ли сделать это как-то статически, дабы не создавать naked функцию? К примеру, байтовый массив, в котором будут записаны необходимые опкоды. По факту - как с VirtualAlloc, но память будет выделена не динамически, а, грубо говоря, глобально, так, словно была создана какая-то static переменная? То есть, вызывается функция для хука, и там создаётся эта самая переменная, которая будет жить до конца работы программы. Хоть я и сомневаюсь, что это возможно без реализации каких-нибудь макросов, но, попытка - не пытка, возможно, я чего-то не знаю.
 
Решение
Вопрос первый: есть ли какие-то различия между new[] и VirtualAlloc в данном случае? Искал информацию в интернете по этому поводу - и понял так, что VirtualAlloc выделяет именно фиксированное число памяти, в то время как new при первом разе выделяет какое-то большое количество памяти, а потом, при каждом новом вызове - хватает из выделенном буфера память, оттого и работает он при каждом вызове быстрее, чем VirtualAlloc. Поправьте, если ошибаюсь. Мне просто интересно, почему используют именно его, а не operator new.
Да, так оно и есть. Но есть одно главное отличие: operator new - стандартная фича плюсов, и она будет работать везде, где поддержан C++. VirtualAlloc же есть только на винде.
Второе отличие - VirtuaAlloc аллоцирует...

RedHolms

Известный
Проверенный
617
360
* Вопрос первый: есть ли какие-то различия между new[] и VirtualAlloc в данном случае?
Связанно это с тем, что каждая зона памяти имеет свои "права" (Чтение, запись и исполнение)
Выделяя память через new[] или malloc, CRT выделяет память только с правами на чтение и запись (в теории, это нигде не прописано), а выделяя память через VirtualAlloc мы явно указываем, что мы потом хотим исполнять данный кусок памяти

Память вещь сложная, по мере изучения cs (computer science) и операционных систем разница между ними станет тебе более явной

Вопрос второй: фактически, можно ведь создать что-то вроде своего обработчика через naked?
В теории, это можно сделать, но есть главная проблема - DLL'ки размещаются в памяти динамически (т.е. при каждом запуске в другом месте), а функции вызываются относительным адресом (вроде в x86 есть инструкция для вызова функции по реальному адресу, поищи инфу в инете)

И да, производительность естественно меньше, но разница мизерная, и ради нескольких миллисекунд не думаю, что стоит заморачиваться

И третий вопрос. В случае, если вариант два является более производительным за счёт того, что там не выделяется динамически память, возможно ли сделать это как-то статически, дабы не создавать naked функцию?
Проблема опять же в "правах" зон памяти
Зона статической памяти(там, где хранятся статические переменные) не имеет права на исполнение
 
  • Нравится
Реакции: D3ad_Parad15e

D3ad_Parad15e

Новичок
Автор темы
4
0
Связанно это с тем, что каждая зона памяти имеет свои "права" (Чтение, запись и исполнение)
Выделяя память через new[] или malloc, CRT выделяет память только с правами на чтение и запись (в теории, это нигде не прописано), а выделяя память через VirtualAlloc мы явно указываем, что мы потом хотим исполнять данный кусок памяти

Память вещь сложная, по мере изучения cs (computer science) и операционных систем разница между ними станет тебе более явной


В теории, это можно сделать, но есть главная проблема - DLL'ки размещаются в памяти динамически (т.е. при каждом запуске в другом месте), а функции вызываются относительным адресом (вроде в x86 есть инструкция для вызова функции по реальному адресу, поищи инфу в инете)

И да, производительность естественно меньше, но разница мизерная, и ради нескольких миллисекунд не думаю, что стоит заморачиваться


Проблема опять же в "правах" зон памяти
Зона статической памяти(там, где хранятся статические переменные) не имеет права на исполнение
Спасибо за ответ. Ещё, как я читал, VirtualAlloc выделяет большой блок памяти (4 кб), и постоянный вызов по сути каждый раз выделяет такой блок памяти. То есть, для того, чтобы не растрачивать память попусту, стоит выделять её один раз, а дальше записывать в память, и делать смещение указателя на размер хука, а после записывать новый хук уже по данному смещению и так далее, верно?
 

RedHolms

Известный
Проверенный
617
360
Спасибо за ответ. Ещё, как я читал, VirtualAlloc выделяет большой блок памяти (4 кб), и постоянный вызов по сути каждый раз выделяет такой блок памяти. То есть, для того, чтобы не растрачивать память попусту, стоит выделять её один раз, а дальше записывать в память, и делать смещение указателя на размер хука, а после записывать новый хук уже по данному смещению и так далее, верно?
Тут не уверен, но скорее всего да
Вся память поделена на страницы, размерами по 4 кб, как раз таки эти страницы и имеют права на чтение, запись и исполнение
Очень упрощённо говоря, это наименьшая единица выделения памяти
 
  • Нравится
Реакции: D3ad_Parad15e

kin4stat

mq-team
Всефорумный модератор
2,730
4,710
Вопрос первый: есть ли какие-то различия между new[] и VirtualAlloc в данном случае? Искал информацию в интернете по этому поводу - и понял так, что VirtualAlloc выделяет именно фиксированное число памяти, в то время как new при первом разе выделяет какое-то большое количество памяти, а потом, при каждом новом вызове - хватает из выделенном буфера память, оттого и работает он при каждом вызове быстрее, чем VirtualAlloc. Поправьте, если ошибаюсь. Мне просто интересно, почему используют именно его, а не operator new.
Да, так оно и есть. Но есть одно главное отличие: operator new - стандартная фича плюсов, и она будет работать везде, где поддержан C++. VirtualAlloc же есть только на винде.
Второе отличие - VirtuaAlloc аллоцирует страницу памяти, а не отдает кусок из заранее выделенной памяти. Также operator new юзает под капотом HeapAlloc на винде, а не VirtualAlloc.
Почему для хуков юзают VirtualAlloc: operator new выделяет невыровненный кусок памяти, а также выделяет ее с правами ReadWrite, когда у VirtualAlloc можно выбрать самому сразу, с какой защитой будет выдана страница памяти. Выровненный кусок памяти нужен для скорости. Процессор читает память в кеш кусками по 64 байта. Когда мы используем VirtuaAlloc, процессору нужно лишь подгрузить начало страницы, и он уже будет обладать всеми нужными инструкциями. В случае operator new, ему придется подгрузить невыровненный кусок памяти, который в худшем случае будет содержать всего один байт инструкций, а все остальное будет мусором, что побудит его подгрузить еще 64 байта памяти в кеш линию

опрос второй: фактически, можно ведь создать что-то вроде своего обработчика через naked? То есть, по сути, записать в naked функцию pusha, сделать jmp (или call, но здесь уже понадобится реализация передачи параметров) на функцию со своим кодом, а после вернуться на это же место, выполнить popa, выполнить код тех опкодов, которые были затёрты хуком и продолжить работу функции. Так вот, вопрос такой: будет ли это иметь разницу в производительности?
Разницу в перфе оно будет иметь. Т.к. когда мы создаем хук через аналогичную функцию, мы сохраняем только нужные регистры, и имеем право изменять некоторые регистры, т.к. они являются volatile. Т.е. мы сохраняем на стек 2-3 регистра, и перезатираем 2-3 регистра, не сохраняя их, т.к. до этого вызывающая сторона уже позаботилась об их сохранении, либо не использовала их вообще. Когда мы делаем pusha и popa, мы сохраняем все регистры. Само копирование регистров не сильно ощутимо. Ощутимо количество памяти, которое мы при этом дергаем(стек).
И, как я понимаю, из-за этого уже не получится делать хуки при помощи операторов и лямбд (именно тех лямбд, в которых описан код, который должен выполняться при переходе).
При должной обертке, можно реализовать и такое. Например kthook научился так делать недавно - https://www.blast.hk/threads/101004/post-1063328

В случае, если вариант два является более производительным за счёт того, что там не выделяется динамически память, возможно ли сделать это как-то статически, дабы не создавать naked функцию? К примеру, байтовый массив, в котором будут записаны необходимые опкоды. По факту - как с VirtualAlloc, но память будет выделена не динамически, а, грубо говоря, глобально, так, словно была создана какая-то static переменная? То есть, вызывается функция для хука, и там создаётся эта самая переменная, которая будет жить до конца работы программы
Эту память все еще аллоцирует система при подгрузке приложения в память с HDD. Она точно также через условный VirtualAlloc создает страницы памяти, загружает туда код, размечает секции и прочее.

Также, если мы будем создавать хуки на такой основе - мы будем раздувать размер файла, вместо того чтобы выделять нужное количество памяти когда нам нужно в рантайме.
 
  • Нравится
Реакции: D3ad_Parad15e

D3ad_Parad15e

Новичок
Автор темы
4
0
Да, так оно и есть. Но есть одно главное отличие: operator new - стандартная фича плюсов, и она будет работать везде, где поддержан C++. VirtualAlloc же есть только на винде.
Второе отличие - VirtuaAlloc аллоцирует страницу памяти, а не отдает кусок из заранее выделенной памяти. Также operator new юзает под капотом HeapAlloc на винде, а не VirtualAlloc.
Почему для хуков юзают VirtualAlloc: operator new выделяет невыровненный кусок памяти, а также выделяет ее с правами ReadWrite, когда у VirtualAlloc можно выбрать самому сразу, с какой защитой будет выдана страница памяти. Выровненный кусок памяти нужен для скорости. Процессор читает память в кеш кусками по 64 байта. Когда мы используем VirtuaAlloc, процессору нужно лишь подгрузить начало страницы, и он уже будет обладать всеми нужными инструкциями. В случае operator new, ему придется подгрузить невыровненный кусок памяти, который в худшем случае будет содержать всего один байт инструкций, а все остальное будет мусором, что побудит его подгрузить еще 64 байта памяти в кеш линию


Разницу в перфе оно будет иметь. Т.к. когда мы создаем хук через аналогичную функцию, мы сохраняем только нужные регистры, и имеем право изменять некоторые регистры, т.к. они являются volatile. Т.е. мы сохраняем на стек 2-3 регистра, и перезатираем 2-3 регистра, не сохраняя их, т.к. до этого вызывающая сторона уже позаботилась об их сохранении, либо не использовала их вообще. Когда мы делаем pusha и popa, мы сохраняем все регистры. Само копирование регистров не сильно ощутимо. Ощутимо количество памяти, которое мы при этом дергаем(стек).

При должной обертке, можно реализовать и такое. Например kthook научился так делать недавно - https://www.blast.hk/threads/101004/post-1063328


Эту память все еще аллоцирует система при подгрузке приложения в память с HDD. Она точно также через условный VirtualAlloc создает страницы памяти, загружает туда код, размечает секции и прочее.

Также, если мы будем создавать хуки на такой основе - мы будем раздувать размер файла, вместо того чтобы выделять нужное количество памяти когда нам нужно в рантайме.
Спасибо за развёрнутый ответ. У меня есть ещё пара вопросов:
1) Касательно этого момента:
Эту память все еще аллоцирует система при подгрузке приложения в память с HDD. Она точно также через условный VirtualAlloc создает страницы памяти, загружает туда код, размечает секции и прочее.
То есть, по сути, можно сказать, что скорость работы при обращении к буферу, который был выделен через VirtualAlloc будет даже быстрее за счёт того, что там отсутствуют опкоды для pusha и popa. Следовательно и памяти (я не считаю выделенный блок в 4 кб целиком, а именно ту часть, что занята под буфер) будет занято меньше за счёт отсутствия тех двух опкодов. Надеюсь, правильно понял.
2) Вы писали о том, что постоянное такое использование приведёт к раздутию файла. Собсна, к этому у меня идёт следующий вопрос:
Ещё, как я читал, VirtualAlloc выделяет большой блок памяти (4 кб), и постоянный вызов по сути каждый раз выделяет такой блок памяти. То есть, для того, чтобы не растрачивать память попусту, стоит выделять её один раз, а дальше записывать в память, и делать смещение указателя на размер хука, а после записывать новый хук уже по данному смещению и так далее, верно?
К примеру, для записи затёртых опкодов понадобилось 6 байт, + 5 байт - call на наш код, + ещё 5 байт - прыжок на продолжение оригинала. Выходит 16 байт. Они прибавляются к указателю на выделенный ранее VirtualAlloc и при следующем хуке запись пройдёт по этому указателю и к нему прибавится вновь какой-то оффсет. И так далее.
 

kin4stat

mq-team
Всефорумный модератор
2,730
4,710
То есть, по сути, можно сказать, что скорость работы при обращении к буферу, который был выделен через VirtualAlloc будет даже быстрее за счёт того, что там отсутствуют опкоды для pusha и popa. Следовательно и памяти (я не считаю выделенный блок в 4 кб целиком, а именно ту часть, что занята под буфер) будет занято меньше за счёт отсутствия тех двух опкодов. Надеюсь, правильно понял.
Процессору без разницы как интерпретировать память - как инструкции или как данные. Поэтому опкоды в памяти ни на что не влияют.
К примеру, для записи затёртых опкодов понадобилось 6 байт, + 5 байт - call на наш код, + ещё 5 байт - прыжок на продолжение оригинала. Выходит 16 байт. Они прибавляются к указателю на выделенный ранее VirtualAlloc и при следующем хуке запись пройдёт по этому указателю и к нему прибавится вновь какой-то оффсет. И так далее.
просто call сделать нельзя - все поедет.
Берем из расчета: jmp в месте хука + трамплин(затертые инструкции + jmp back)

Но там еще несколько проблем, кроме раздувания памяти.

Секция данных защищена от выполнения, и сделано это не просто так.
А еще там не будет выравнивания адресов
 

D3ad_Parad15e

Новичок
Автор темы
4
0
Секция данных защищена от выполнения, и сделано это не просто так.
Если секция данных - имеется ввиду то место, куда вставляется хук, то её параметры защиты можно изменить при помощи VirtualProtect.
просто call сделать нельзя - все поедет.
Берем из расчета: jmp в месте хука + трамплин(затертые инструкции + jmp back)
Да, я это и имел ввиду. Но, можно же сделать jmp на адрес трамплина (со смещением от оригинала и - 5), в котором будет call на инжектнутый код, после которого идут затёртые инструкции и Jmp на смещение от тех самых затёртых инструкций (грубо говоря, чтобы код продолжал выполняться в том месте, где начинаются новые инструкции, которые не были тронуты).

Поправьте, если не прав.