Просмотр полной версии : Хуки и выделение памяти
RedHolms
18.06.2022, 11:42
* Вопрос первый: есть ли какие-то различия между new[] и VirtualAlloc в данном случае?
Связанно это с тем, что каждая зона памяти имеет свои "права" (Чтение, запись и исполнение)
Выделяя память через new[] или malloc, CRT выделяет память только с правами на чтение и запись (в теории, это нигде не прописано), а выделяя память через VirtualAlloc мы явно указываем, что мы потом хотим исполнять данный кусок памяти
Память вещь сложная, по мере изучения cs (computer science) и операционных систем разница между ними станет тебе более явной
Вопрос второй: фактически, можно ведь создать что-то вроде своего обработчика через naked?
В теории, это можно сделать, но есть главная проблема - DLL'ки размещаются в памяти динамически (т.е. при каждом запуске в другом месте), а функции вызываются относительным адресом (вроде в x86 есть инструкция для вызова функции по реальному адресу, поищи инфу в инете)
И да, производительность естественно меньше, но разница мизерная, и ради нескольких миллисекунд не думаю, что стоит заморачиваться
И третий вопрос. В случае, если вариант два является более производительным за счёт того, что там не выделяется динамически память, возможно ли сделать это как-то статически, дабы не создавать naked функцию?
Проблема опять же в "правах" зон памяти
Зона статической памяти(там, где хранятся статические переменные) не имеет права на исполнение
D3ad_Parad15e
18.06.2022, 14:04
Связанно это с тем, что каждая зона памяти имеет свои "права" (Чтение, запись и исполнение)
Выделяя память через new[] или malloc, CRT выделяет память только с правами на чтение и запись (в теории, это нигде не прописано), а выделяя память через VirtualAlloc мы явно указываем, что мы потом хотим исполнять данный кусок памяти
Память вещь сложная, по мере изучения cs (computer science) и операционных систем разница между ними станет тебе более явной
В теории, это можно сделать, но есть главная проблема - DLL'ки размещаются в памяти динамически (т.е. при каждом запуске в другом месте), а функции вызываются относительным адресом (вроде в x86 есть инструкция для вызова функции по реальному адресу, поищи инфу в инете)
И да, производительность естественно меньше, но разница мизерная, и ради нескольких миллисекунд не думаю, что стоит заморачиваться
Проблема опять же в "правах" зон памяти
Зона статической памяти(там, где хранятся статические переменные) не имеет права на исполнение
Спасибо за ответ. Ещё, как я читал, VirtualAlloc выделяет большой блок памяти (4 кб), и постоянный вызов по сути каждый раз выделяет такой блок памяти. То есть, для того, чтобы не растрачивать память попусту, стоит выделять её один раз, а дальше записывать в память, и делать смещение указателя на размер хука, а после записывать новый хук уже по данному смещению и так далее, верно?
Используй kthook и будет счатье хД
GitHub - kin4stat/kthook (https://github.com/kin4stat/kthook/)
Contribute to kin4stat/kthook development by creating an account on GitHub.
github.com
p.s. @kin4stat (https://www.blast.hk/members/161331/) обнови документацию для новичков, ктхук в массы!
RedHolms
18.06.2022, 14:09
Спасибо за ответ. Ещё, как я читал, VirtualAlloc выделяет большой блок памяти (4 кб), и постоянный вызов по сути каждый раз выделяет такой блок памяти. То есть, для того, чтобы не растрачивать память попусту, стоит выделять её один раз, а дальше записывать в память, и делать смещение указателя на размер хука, а после записывать новый хук уже по данному смещению и так далее, верно?
Тут не уверен, но скорее всего да
Вся память поделена на страницы, размерами по 4 кб, как раз таки эти страницы и имеют права на чтение, запись и исполнение
Очень упрощённо говоря, это наименьшая единица выделения памяти
kin4stat
18.06.2022, 14:57
Вопрос первый: есть ли какие-то различия между 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
18.06.2022, 17:58
Да, так оно и есть. Но есть одно главное отличие: 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
18.06.2022, 18:37
То есть, по сути, можно сказать, что скорость работы при обращении к буферу, который был выделен через VirtualAlloc будет даже быстрее за счёт того, что там отсутствуют опкоды для pusha и popa. Следовательно и памяти (я не считаю выделенный блок в 4 кб целиком, а именно ту часть, что занята под буфер) будет занято меньше за счёт отсутствия тех двух опкодов. Надеюсь, правильно понял.
Процессору без разницы как интерпретировать память - как инструкции или как данные. Поэтому опкоды в памяти ни на что не влияют.
К примеру, для записи затёртых опкодов понадобилось 6 байт, + 5 байт - call на наш код, + ещё 5 байт - прыжок на продолжение оригинала. Выходит 16 байт. Они прибавляются к указателю на выделенный ранее VirtualAlloc и при следующем хуке запись пройдёт по этому указателю и к нему прибавится вновь какой-то оффсет. И так далее.
просто call сделать нельзя - все поедет.
Берем из расчета: jmp в месте хука + трамплин(затертые инструкции + jmp back)
Но там еще несколько проблем, кроме раздувания памяти.
Секция данных защищена от выполнения, и сделано это не просто так.
А еще там не будет выравнивания адресов
D3ad_Parad15e
18.06.2022, 19:28
Секция данных защищена от выполнения, и сделано это не просто так.
Если секция данных - имеется ввиду то место, куда вставляется хук, то её параметры защиты можно изменить при помощи VirtualProtect.
просто call сделать нельзя - все поедет.
Берем из расчета: jmp в месте хука + трамплин(затертые инструкции + jmp back)
Да, я это и имел ввиду. Но, можно же сделать jmp на адрес трамплина (со смещением от оригинала и - 5), в котором будет call на инжектнутый код, после которого идут затёртые инструкции и Jmp на смещение от тех самых затёртых инструкций (грубо говоря, чтобы код продолжал выполняться в том месте, где начинаются новые инструкции, которые не были тронуты).
Поправьте, если не прав.
vBulletin® v3.8.14, Copyright ©2000-2026, vBulletin Solutions, Inc. Перевод: zCarot