Фундаментальные основы хакерства

Грубые ошибки автора


1) "Объекты ядра защищены, и процесс, прежде чем оперировать с ними, должен запрашивать разрешение на доступ к ним. Процесс – создатель объекта может предотвратить несанкционированный доступ к этому объекту со стороны другого процесса" стр. 12

Насчет защиты Рихтер немного загнул – она есть только под Windows NT, но даже там (за исключением серверных приложений) обычно не используется. Поэтому, кто угодно может получить доступ к объектам ядра чужого процесса (за исключением системного) вызвав DuplicateHandle или обратившись к набору функций TOOLHELP32 – процесс и знать не будет, что дублируют его дескриптор!

И даже под NT, и даже с установленными атрибутами защиты в адресном пространстве процесса можно исполнить свой код, обращаясь к защищенному дескриптору от имени этого процесса. Делов-то!

Правильнее было бы говорить о защите от непреднамеренного

доступа к дескрипторам чужого процесса.

2) "…согласно принципу неопределенности Гейзенберга, чем точнее определяется один квант, тем больше ошибка в измерении другого" стр. 52

Это не программистская, но все-таки грубая ошибка. Принцип Гейзенберга в моем пересказе звучит так - нельзя одновременно определить координаты и импульс одной частицы, поскольку любое измерение чего бы то ни было невозможно без взаимодействия, а любое такое взаимодействие искажает свойства объекта измерений.

3) "…потоки с более высоким приоритетом всегда вытесняют потоки с более низким приоритетом независимо от того, исполняются последние или нет" стр. 65.

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

Исключение составляют аппаратные прерывания, обрабатываемые системой, но это совсем другой разговор. А на многопроцессорных машинах за каждым процессором закрепляются "свои" потоки и потоки одного процессора никогда не вытесняют потоки другого.


Правильно сказать так: а) потоки исполняются по очереди в согласии с приоритетом; б) при пробуждении потока он изменяет очередь исполнения, отбирая процессорное время у потоков с более низким приоритетом.

4) "Ни одна Win32-функция не возвращает уровень приоритета потока… Такая ситуация создана преднамеренно. Вспомните, что Microsoft может в любой момент изменить алгоритм распределения процессорного времени…" стр. 71



Неверно. Во-первых, явно пропущено слово "абсолютный", т.к. относительный приоритет автор сам только что получал функцией GetThreadPriority.

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

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

5) "Резервируя регион в адресном пространстве, система обеспечивает еще четную кратность размера региона размеру страницы. Так называется единица объема памяти, используемая системой при управлении памятью" стр. 86

Брр… не понял. Если проще – размер страницы всегда степень двойки, размер выделяемого региона всегда кратен размеру страниц, но не обязательно должен быть

четен
количеству страниц. Т.е. запрос на выделение трех страниц выделит именно три страницы, а не четыре или две.

Небольшое уточнение – страничная организация памяти - прерогатива в первую очередь процессора, а не системы.

6) "AllocationBase – Идентифицирует базовый адрес региона, включающего в себя адрес, указанный в lpAddress" стр. 117

Нет! В AllocationBase возвращается базовый адрес региона, ранее выделенного VirualAlloc или 0, если регион был выделен как-то иначе или вообще не был выделен.

7) "DLL-модулям куча по умолчанию не предоставляется, и поэтому при их компоновке нельзя применять параметр /HEAP" стр. 202

По умолчанию DLL-модулям выделяется 1 Мб кучи и его можно изменить ключом /HEAP.


Соответствующее поле PE- заголовка послушно изменится, но… этой кучей динамической библиотеке воспользоваться так и не удастся, поскольку стандартный загрузчик ОС всегда игнорирует это поле при подключении DLL.

8) "И последняя причина, по которой имеет смысл использовать в программе раздельные кучи, – локальный доступ… Обращаясь в основном к памяти, локализованной в небольшом диапазоне адресов, Вы снизите вероятность перекачки страниц между оперативной памятью и страничным фреймом" стр. 204

Это верно, но только по отношению к физическим

адресам. Логически же удаленные друг от друга адреса могут ютится и в смежных, и в далеко разнесенных страницах, - это уж как ОС заблагорассудится их скомбинировать.

Если данные занимают размер, превышающий размер страницы (обычно 4 Кб), то за счет фрагментации виртуальной памяти они наверняка окажутся в несмежных страницах, а потому ожидаемое ускорение "вылетит в трубу"!

8) "…поскольку, в операционную систему встроена поддержка синхронизующих объектов никогда не применяете этот метод [далее идет описание метода синхронизации с использованием переменной-флага, устанавливаемой в TRUE синхронизуемым потоком по завершению – КК]" стр. 217

Во-первых, ввиду пропуска Рихтером ключевого слова volatile, предложенный им способ действительно никогда не следует использовать – работать он, скорее всего, не будет. Оптимизирующие компиляторы, увидев цикл a la "while (!myvar)" подумают: раз переменная myvar явным образом не изменяется (во всяком случае в рамках одного потока), так заменим ее константой и перепишем цикл как: "while(1)". Ключевое же слово volatile сообщает компилятору, что переменная может модифицироваться в любой момент времени внешним кодом и "оптимизировать" ее не надо. Между прочим, это – камень преткновения очень многих начинающих программистов. Самое противное – прогон кода под отладчиком (отладочная версия обычно компилируется без оптимизации) работает на "ура", но финальная (оптимизированная) версия упорно не работает!



Во-вторых, не стоит совсем уж отказываться от "ручной" синхронизации потоков. Накручивать пустой цикл в ожидании результатов работы конечно глупо, но вот если в это время заняться чем-нибудь другим, попутно периодически контролируя состояние переменной флага… А, собственно, почему это должен быть именно флаг?! Пусть один поток сообщает в этой переменной другому потоку процент выполненной им работы. Например, загружая файл с дискеты, сети или другого медленного носителя, можно немедленно выводить скаченные данные на экран, если только один поток сообщит другому: какое именно количество на данный момент скачено.

9) "Потом создавал буфер в адресном пространстве своего процесса и помещал в него машинный код, который выполнял такие операции… call LoadLibraryA… Все правильно, я сам брал машинные команды соответствующие каждой инструкции языка ассемблера, и заполнял ими буфер" стр. 624

Вот именно – "команды, соответствующие каждой инструкции языка ассемблера", - т.к. каждой инструкции ассемблера соответствует от одной до нескольких команд процессора. Не все они равнозначны, причем, машинный код, сгенерированный всеми известными мне ассемблерами, неперемещаем, поскольку все вызовы в нем относительны, т.е. аргумент инструкции call представляет собой не смещение функции LoadLibraryA, а разницу ее смещения и смещения конца инструкции call. Поскольку, адрес верхушки стека разнится от одной версии ОС к другой, созданный Рихтером код окажется работоспособен только в той ОС для которой он предназначен, да и то лишь в том случае, если перед ассемблированием использовать директиву ORG xxx, где xxx – смещение начала буфера. (Рихтер об этом вообще ничего не говорит!)

Выходов два – либо формировать машинные команды вручную, принудительно выбирая абсолютную адресацию (всякий ли знает, как это делать?), либо использовать регистровые вызовы, т.е. mov reg, offset LoadLibraryA.; call reg. Кстати, адрес LoadLibraryA – у Рихтера константа, определяющаяся на этапе ассемблирования, но ведь она неодинакова в различных ОС!



10) "…потом я изменил структуру CONTEXT… так, чтобы установить указатель стека на участок памяти перед моим машинным кодом, а указатель команд – на первый байт этого кода" стр. 624

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

11) "Разрабатывая ThreadFunct, я должен постоянно помнить, что после копирования в удаленное адресное пространство функция будет находиться по виртуальному адресу, который почти наверняка не совпадет с ее адресом в локальном адресном пространстве. Это значит, надо написать функцию, не делающую внешних ссылок! Это очень трудно!" стр. 632

"Не делающую внешних ссылок", - не только литературно, но и технически некорректное выражение. Точнее:

а) все машинные команды этой функции для обращения к коду и переменным самой этой функции должны использовать только относительную адресацию;

б) для обращения к коду и переменным, не принадлежащим этой функции – только абсолютную адресацию;

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

Но и это еще не все! Многие компиляторы могут принудительно вставлять в код неперемещаемые вызовы своих собственных функций. Например, Microsoft Visual C++ для контроля сбалансированности стека до и после вызова функции обращается к служебной процедуре __chkesp. Хорошо, если разработчики компилятора предусмотрели ключи, запрещающие подобную "самодеятельность", но так бывает не всегда.

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

12) "Флаг FILE_FLAG_POSIX_SEMANTICS сообщает, что при доступе к файлу следует применить правила POSIX.


Файловые системы, использующие POSIX, чувствительны к регистру в именах файлов… В то же время MS-DOSб 16-разрядная Windows и Win32 к регистру букв в именах файлов не чувствительны. Поэтому, будьте крайне осторожны, используя FILE_FLAG_POSIX_SEMANTICS. Файл, при создании которого установлен этот флаг, может оказаться недоступным из приложений MS-DOS, 16-рязрядной Windows и Win32" стр. 472

Во-первых, Win32 тут явно "третий лишний" – если Win32 поддерживает POSIX этим самым флагом – какие могут быть проблемы? Кстати, по поводу POSIX – его не поддерживает FAT, поэтому файл, созданный на FAT-диске, регистр игнорирует – создаваться-то с указанным регистром символов он создается, но вот возможности создания двух файлов с одинаковыми именами, но разными регистрами нет, помимо этого при открытии файла идентичность регистра не проверяется даже если установлен FILE_FLAG_POSIX_SEMANTICS.

Другое отличие POSIX – обратный (ну, в смысле прямой) наклон черты разделителя, т.е. к файлу "TEST\test" доступ теперь осуществляется так: "TEST/test".

Во-вторых, фраза "может оказаться недоступным" слишком витиевата, чтобы быть полезной. Почему бы ни ответить когда именно он оказывается недоступным? А вот когда. Если на NTFS-диске в одной директории содержится два и более файлов с одинаковыми именами, но разными регистрами, то из-под Windows-16 и MS-DOS виден только первый (в порядке создания) из них. Во всех остальных случаях, файл созданный с флагом FILE_FLAG_POSIX_SEMANTICS, доступен отовсюду – можете не волноваться!


Содержание раздела