Техника защиты компакт-дисков от копирования

Взаимодействие посредствомчерез портовы ввода/вывода


Операционная система Windows NT тщательно оберегает порты ввода/вывода от посягательства со стороны прикладных приложений. Мера эта вынужденная и реализованная под давлением выбранной политики безопасности. Свобода прикладных приложений умышленно ограничивается так, чтобы предотвратить возможные "террористические акты", направленные на подрыв системы или несанкционированный захват конфиденциальной информации. Правом непосредственного доступа к оборудованию обладают лишь драйверадрайвераы и динамические библиотеки, исполняющиеся в режиме ядра (см. разд. "Доступ посредствомчерез SCSI мини- порта интерфейса SCSI" этой главы).

Поневоле вспоминаются слова одного из "отцов-основателей" США, что нация, обменявшая свободу на безопасность, не заслуживает ни того, ни другого. ИИ, правда! Как будто бы нельзя "завесить" систему посредствомчерез техот же тнтерфейсов SPTI или //ASPI! Причем для этого даже не понадобится обладать правами администратора! Какая там политика безопасности, какое к черту разграничение доступа, когда интерфейс ASPI дает доступ к диску на секторном уровне безо всяких проверок на предмет правомерности осуществления этой операции. Хоть сейчас внедряй boot-вирусы в загрузочный сектор внедряй! И это при том, что отсутствие доступа к портам ввода/вывода существенно усложняет задачу управления оборудованием и уж тем более создания надежных и трудноломаемых защитных механизмов!

Операционные системы семейства Windows 9x ведут себя более "демократично", однако их "снисходительность" распространяется исключительно на программы операционной системы MS-DOS программы, а win32-приложения возможности прямого доступа к портам, увы, лишены.

Тем не менее, управлять оборудованием с прикладного уровня все-таки возможно. Существует по меньшей мере два пути решения этой проблемы:

q      а) создание драйвера-посредника, реализующего более или менее прозрачный интерфейс для взаимодействия с портами через механизм IOCTL; и


q      б) модификация карты разрешения ввода-вывода (I/O Permission Map, –— IOPM) с таким расчетом, чтобы обращение к портам перешло в разряд непривилегированных операций, осуществимых и с прикладного уровня.

ДалееНиже оба этих способа будут подробнои рассмотрены. Начнем с интерфейсного драйвера.

В состав NT DDK входит весьма любопытный учебный драйвер PORTIO, создающий виртуальное устройство и реализующий специальный IOCTL-интерфейс, посредством которого прикладные приложения могут манипулировать с портами этого устройства произвольным образом (его исходный текст, с минимумом необходимых комментариев расположен в каталоге: "\NTDDK\src\general\portio)"). Конечно, виртуальное устройство, –— это не совсем то, что нам нужно, поскольку диапазон принадлежащих ему портов ввода/вывода не может пересекаться с портами, принадлежащими другими устройствам, в противном случае система "грязно выругается" и поставит в "диспетчере устройств" восклицательный знак, предупреждая пользователя об имеющемся конфликте ресурсов. И хотя на работоспособность системы такой конфликт никак не повлияет, созерцание восклицательных знаков уж точно не пойдет в прокна пользу пользователям нашей программы.

На самом деле, драйверу, работающему в режиме ядра, никто не запрещает обращаться к любым портам, каким ему только вздумается. Достаточно исключить из тела [Y133] файла genport.c следующие строки (листинг 1.4.24), и мы сможем с его помощью читать весь диапазон портов ввода/вывода.

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

if (nPort >= pLDI->PortCount ||

      (nPort + DataBufferSize) > pLDI->PortCount ||



      (((ULONG_PTR)pLDI->PortBase + nPort) & (DataBufferSize - 1)) != 0)



{

      return STATUS_ACCESS_VIOLATION;   // Illegal port number

}

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

Листинг 2.1.4.2625. Вычисление действительного адреса порта через базовый

case IOCTL_GPD_READ_PORT_UCHAR:

    *(PUCHAR)pIOBuffer=READ_PORT_UCHAR((PUCHAR)((ULONG_PTR)pLDI->PortBase+nPort));

    break;

Очевидно, что текст, выделенный жирным шрифтом следует удалить, –— в этом случае драйвер сможет оперировать абсолютными, а не относительными портами, и мы без труда сможем "прорваться" к любому порту системы! Причем, если мы перенесем модифицированный нами драйвер на Windows 9x, наши приложения будут работать в обеих операционных системах и останутся зависимыми разве что от самого оборудования. Но, с другой стороны, всякий, кто стремится дорватьсядобраться до портов, должен отдавать себе отчет в том, зачем это ему нужно и какие сложности ему придется преодолеть.

Конечно, поскольку возможность бесконтрольного доступа ко всем имеющимся портам ввода/вывода существенно ослабляет и без того уязвимую операционную систему, нелишним будет ввести в драйвер кое-какие дополнительные проверки и ограничения. Скажем, запретить прямое обращение ко всему, что не является приводом CD-ROM приводом. В противном случае, если ваша программа получит сколь- ни будь широкое распространение, толпы вандалов ринуться писать зловредных троянских коней, военная мощь которых окажется практически безграничной, и совладеть с ними будет очень и очень трудно. С другой стороны, –— за все время существования интерфейса ASPI не было зафиксировано ни одной попытки использовать его для деструктивных целей, хотя такая возможность до сих пор имеется.

Другой недостаток предложенного способа управления устройствами заключается в его катастрофически низком быстродействии.


Вызовы функции DeviceIoControl распадаются на десятки тысяч машинных команд (!), "благодаря" чему время обработки запросов становится слишком большим, а измерение физических характеристик спиральной дорожки (если мы действительно захотим эти характеристики измерять) –— неточным. К тому же, функция DeviceIoControl громоздка и неизящна, а самое неприятное в том, что на нее очень легко поставить BreakPoint[Y134] [n2k135] , и потому участь такой защиты заранее предрешена. Во времена операционной системы MS-DOS, когда взаимодействие с оборудованием осуществлялось посредством машинных команд IN и OUT, локализовать защитный код в теле программы было значительно сложнее, а управлять устройствами с их помощью существенно легче и –— главное –— намного производительнее.

Считается, что в среде Windows NT прямое обращение к портам возможно только на уровне ядра, а приложения вынуждены общаться с портами через высокоуровневый интерфейс, предоставляемый драйвером. И хотя этот интерфейс может быть полностью прозрачным (драйверу ничего не стоит перехватить исключение, возникающее при попытке чтения/записи в порт с прикладного уровня, и выполнить этот запрос самостоятельно), и это все-таки это не то…

На самом деле, выполнять команды IN/OUT можно и на прикладном уровне, правда не без помощи недокументированных возможностей операционной системы и документированных, но малоизвестных особенностей реализации защищенного режима работы в процессорах Intel 80386+. Вот с процессоров мы, пожалуй, и начнем. Давайте откроем [Y136] [n2k137] "Instruction Set Reference" и посмотрим, как "устроена" машинная команда OUT. Среди прочей полезной информации мы найдем и ее псевдокод, которой выглядит приблизительно так, как это показано в листинге 1.4.26.:

Листинг 2.1.4.2726. Псевдокод инструкции OUT

if ((PE == 1) && ((CPL > IOPL) || (VM == 1)))

{

      /* Protected mode with CPL > IOPL or virtual-8086 mode */



      if (Any I/O Permission Bit for I/O port being accessed == 1)

            #GP(0);           /* I/ O operation is not allowed */

      else

            DEST ß SRC;      /* Writes to selected I/O port */

}

      else

{

      /* Real Mode or Protected Mode with CPL <= IOPL */

      DEST ß SRC;            /* Writes to selected I/O port */

}

Обратите внимание! Обнаружив, что полномочий текущего уровня привилегий категорически недостаточно для выполнения данной машинной инструкции, процессор не спешит выбросыватьитьвыдать исключение general protection fault, а предоставляетдает ей еще один шанс, осуществляя дополнительную проверку на предмет состояния карты разрешения ввода/вывода (I/O permission bitmap) и, если бит памяти, соответствующий данному порту не равен единице, то вывод в порт осуществляется несмотря ни на какие запреты со стороны CPL[Y138] [n2k139] !

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

Проблема в том, что подавляющее большинство авторов книг по ассемблеру о карте разрешения ввода/вывода даже не упоминают, и лишь немногие программисты знают о ее существовании –— те, кто предпочитает оригинальную документацию корявым переводам и пересказам.

Обратившись к [Y140] [n2k141] "Architecture Software Developer's Manual Volume 1: Basic Architecture", мы узнаем, что карта ввода/вывода находится в сегменте состояния задачи (TSS –— Task State Segment), –— точнее, ее действительное смещение относительно начала TSS определяется 32-битным полем, расположенном в 0x66 (102) и 0x67 (103) байтах сегмента состояния задачи.


Нулевой бит этой карты отвечает за нулевой порт, первый –— за первый, второй –— за второй и т. д. вплоть до старшего бита 0x2000 (8192) -байта, отвечающего за 65 535 порт. Битовую карту завершает так называемый байт-терминатор, имеющий значение 0xFF. Вот, собственно, и все. Порты, чьи биты сброшены в нулевое значение, доступны с прикладного уровня безо всяких ограничений. Разумеется, сама карта ввода/вывода доступна лишь драйверам, но не приложениям, поэтому без написания собственного драйвера нам все равно не обойтись. Однако этот драйвер будет работать только на стадии своей инициализации, а весь дальнейший ввод/вывод пойдет напрямую, даже если выгрузить драйвер из памяти.

Теперь плохая новость. В Windows NT смещение карты ввода/вывода по умолчанию находится за пределами сегмента состояния задачи и потому модифицировать карту ввода/вывода не так-то просто, поскольку ее вообще нет! Процессор, кстати говоря, на такую ситуацию реагирует вполне спокойно, но доступ к портам ввода/вывода с прикладного уровня тем не менее запрещает.

На самом же деле карта ввода/вывода в сегменте TSS все-таки есть, но она умышленно заблокирована системой, чтобы не дать прикладным приложениям своевольничать. Исключение составляют лишь высокопроизводительные графические библиотеки, напрямую обращающиеся к портам ввода/вывода с прикладного режима. Как нетрудно догадаться, такой трюк дает Microsoft значительную фору перед конкурентами, вынужденными управлять портами либо с уровня ядра, либо посредствомчерез интерфейса, предоставляемогоый видеодрайвером. Естественно, оба этих способа значительно проигрывают в производительности прямому доступу к портам.

Однако попытка подкорректировать указатель на карту ввода/вывода ни к чему не приводит, поскольку коварная операционная система Windows NT хранит копию этого значения в контексте процесса, а потому при переключении контекста указатель на прежнюю карту автоматически восстанавливается. С одной стороны это хорошо, так как каждый процесс может иметь свою собственную карту ввода/вывода, а с другой… штатная документация от Microsoft не содержит и намека на то, как с этой картой работать.



Правда, можно схитрить и увеличить размер сегмента состояния задачи так, чтобы адрес карты ввода/вывода, прежде указывающий за его конец, теперь приходился на действительную и подвластную нам область памяти. А поскольку в хвосте последней страницы, занятой сегментом TSS, имеется всего лишь 0xF55 (3925) байт, максимальный размер карты, которую мы только можем создать в этом промежутке, охватывает всего лишь 31 .392 портов ввода/вывода. Хотя, если говорить честно, остальные порты нам все равно вряд ли понадобятся, так что ничего трагичного в таком ограничении и нет.

Впрочем, существуют и более изящные способы решения этой проблемы. Усилиями Дейла Робертса были обнаружены три полностью недокументированные функции: Ke386SetIoAccessMap(), Ke386QueryIoAccessMap() и Ke386IoSetAccessProcess(), которые, как и следует из их названий, обеспечивают вполне легальный способ управления картой ввода/вывода. "Полностью недокументированные" в том смысле, что даже заголовочные файлы из DDK не содержат их прототипов (а, как известно, в заголовочных файлах DDK перечислено множество недокументированных функций). Тем не менее, библиотека NTOSKRNL их все-таки экспортирует, и они легко доступы с уровня драйверов.

Подробнее обо всем этом можно прочитать в статье их первооткрывателя –— Дейла Робертса, –— перевод которой можно найти, в частности, по следующему адресу: http://void.ru/?do=printable&id=701. Здесь же мы рассмотрим их лишь кратко. Итак, функция Ke386SetIoAccessMap принимает два аргумента: двойное слово, которое будучи установленным в единицу, заставляет функцию копировать карту ввода/вывода указатель, на которую передан ей со вторым аргументом. Функция Ke386QueryIoAccessMap принимает те же самые аргументы, но осуществляет прямо противоположную операцию, извлекая текущую карту ввода/вывода из сегмента состояния задачи и копируя ее в указанный буфер. Наконец, функция Ke386IoSetAccessProcess принимает со своим вторым аргументом указатель на структуру процесса, полученный вызовом документированной функции PsGetCurrentProcess().


Первый аргумент играет ту же самую роль, что и в предыдущих функциях: нулевое значение переводит указатель на карту ввода/вывода за границы сегмента TSS, тем самым запрещая доступ к портам с прикладного уровня, а единичное –— активизирует ранее переданную карту ввода/вывода.

Пример драйвера, открывающего прямой доступ к портам ввода/вывода на прикладном уровне и приведенный в листинге 1.4.27ниже, все это, собственно, и демонстрирует.:

Листинг 2.1.4.2827. [/etc/GIVEIO.c] Демонстрационный пример драйвера, открывающего прямой доступ к портам ввода/вывода на прикладном уровне

/*----------------------------------------------------------------------------

 *

 *                  ДРАЙВЕР. РАЗРЕШАЕТ ВЫПОЛНЕНИЕ

 *            МАШИННЫХ КОМАНД IN/OUT НА ПРИКЛАДНОМ УРОВНЕ

 *            ===========================================

 *

 *   ВНИМАНИЕ! Я, Крис Касперски, не имею никакого отношения к этой программе!

 * -------------------------------------------------------------------------

 *

 * GIVEIO.SYS: by Dale Roberts

 * КОМПИЛЯЦИЯ: Используйте средство DDK BUILD

 * НАЗНАЧЕНИЕ: Предоставить доступ к прямому в/в процессам режима пользователя

----------------------------------------------------------------------------*/

#include <ntddk.h>

/* Имя нашего драйвера устройства */

#define DEVICE_NAME_STRING L"giveio"

// Структура" IOPM. это просто массив байт размером 0x2000, содержащий

// 8К * 8 бит == 64К бит IOPM, которые  покрывают всё  64 Кб  адресное

// пространство ввода/вывода x86 процессоров.

// Каждый нулевой бит  предоставляет  доступ к соответствующему  порту

// для user-mode процесса; каждый единичный бит запрещает доступ к в/в

// через соответствующий порт

#define IOPM_SIZE 0x2000

typedef UCHAR IOPM[IOPM_SIZE];

// массив нулей, который  копируется в настоящую IOPM в TSS посредством

// вызова dsKe386SetIoAccessMap()

// необходима память выделяется во время загрузки драйвера



IOPM *IOPM_local = 0;

// это две полностью недокументированные функции, которые мы используем,

// чтобы дать доступ к в/в вызывающему процессу

// * Ke386IoSetAccessMap()     - копирует  переданную  карту в/в в  TSS

// * Ke386IoSetAccessProcess() - изменяет указатель смещения IOPM, после

//                               чего только  что  скопированная  карта

//                               начинает использоваться

void Ke386SetIoAccessMap(int, IOPM *);

void Ke386QueryIoAccessMap(int, IOPM *);

void Ke386IoSetAccessProcess(PEPROCESS, int);

// ОСВОБОДИТЬ ВСЕ ВЫДЕЛЕННЫЕ РАНЕЕ ОБЪЕКТЫ

VOID GiveioUnload(IN PDRIVER_OBJECT DriverObject)

{

      UNICODE_STRING uniDOSString;

      WCHAR DOSNameBuffer[] = L"\\DosDevices\\" DEVICE_NAME_STRING;

      if(IOPM_local) MmFreeNonCachedMemory(IOPM_local, sizeof(IOPM));

      RtlInitUnicodeString(&uniDOSString, DOSNameBuffer);

      IoDeleteSymbolicLink (&uniDOSString);

      IoDeleteDevice(DriverObject->DeviceObject);

}

//----------------------------------------------------------------------------

//      устанавливаем IOPM (карту разрешения в/в) вызывающего процесса так, чтобы

//      ему предоставлялся  полный  доступ к в/в. Массив  IOPM_local[]  содержит

//      одни нули, соответственно, IOPM обнулится.

//      Если OnFlag == 1, процессу предоставляется доступ к в/в;

//      Если он равен 0, доступ запрещается.

//----------------------------------------------------------------------------

VOID SetIOPermissionMap(int OnFlag)

{

      Ke386IoSetAccessProcess(PsGetCurrentProcess(), OnFlag);

      Ke386SetIoAccessMap(1, IOPM_local);

}

void GiveIO(void)

{

      SetIOPermissionMap(1);

}

//----------------------------------------------------------------------------

//      cлужебный обработчик для user-mode вызова CreateProcess().

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

//      DriverEntry().


когда  user-mode  приложение  вызывает  CreateFile(), эта

//      функция получает управление всё ещё  в контексте  вызвавшего приложения,

//      но  с  CPL (текущий  уровень  привилегий  процессора)  установленым в 0.

//      Это  позволяет  производить  операции  возможные  только  в kernel mode.

//      GiveIO вызывается для предоставления вызывающему процессу доступа к в/в.

//      Все, что приложение режима пользователя, которому  нужен доступ  к  в/в,

//      должно сделать - это открыть данное  устройство, используя  CreateFile()

//      Никаких других действий не нужно.

//----------------------------------------------------------------------------

NTSTATUS GiveioCreateDispatch(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)

{

      GiveIO(); // give the calling process I/O access

      Irp->IoStatus.Information      = 0;

      Irp->IoStatus.Status            = STATUS_SUCCESS;

      IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;

}

//----------------------------------------------------------------------------

//      процедура входа  драйвера. эта процедура  вызывается  только  раз  после

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

//      драйвера. в нашем случае она выделяет память для массива IOPM  и создаёт

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

//      она   также   создаёт   символическую  ссылку  на  драйвер   устройства.

//      что позволяет user-mode  приложению  получить  доступ к нашему  драйверу

//      используя \\.\giveio нотацию.

//----------------------------------------------------------------------------

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath)

{

      NTSTATUS      status;

      PDEVICE_OBJECT      deviceObject;

      UNICODE_STRING      uniNameString, uniDOSString;

      WCHAR NameBuffer[]      = L"\\Device\\" DEVICE_NAME_STRING;



      WCHAR DOSNameBuffer[]      = L"\\DosDevices\\" DEVICE_NAME_STRING;

      // выделим буфер для локальной IOPM и обнулим его

      IOPM_local = MmAllocateNonCachedMemory(sizeof(IOPM));

      if(IOPM_local == 0) return STATUS_INSUFFICIENT_RESOURCES;

      RtlZeroMemory(IOPM_local, sizeof(IOPM));

      // инициализируем драйвер устройства и объект устройства (device object)

      RtlInitUnicodeString(&uniNameString, NameBuffer);

      RtlInitUnicodeString(&uniDOSString, DOSNameBuffer);

      status = IoCreateDevice(DriverObject, 0, &uniNameString,FILE_DEVICE_UNKNOWN,

                                          0, FALSE, &deviceObject);

      if(!NT_SUCCESS(status)) return status;

      status = IoCreateSymbolicLink (&uniDOSString, &uniNameString);

      if (!NT_SUCCESS(status)) return status;

      // инициализируем точки входа драйвера в объекте драйвера

      // всё, что нам нужно, это операции создания (Create) и выгрузки (Unload)

      DriverObject->MajorFunction[IRP_MJ_CREATE] = GiveioCreateDispatch;

      DriverObject->DriverUnload = GiveioUnload;

      return STATUS_SUCCESS;

}

Пример демонстрации ввода/вывода в порт с прикладного уровня показан в листинге 1.4.28.

Листинг 2.1.4.2928. [/etc/GIVEIO.demo.c] Пример ввода/вывода в порт с прикладного уровня

/*----------------------------------------------------------------------------

 *

 *            ДЕМОНСТРАЦИЯ ВЫЗОВА IN/OUT НА ПРИКЛАДНОМ УРОВНЕ

 *      (внимание! драйвер  GIVEIO.SYS должен быть предварительно загружен!)

 *      ====================================================================

 *

 *   ВНИМАНИЕ! Я, Крис Касперски, не имею никакого отношения к этой программе!

 * -------------------------------------------------------------------------

 *

 * GIVEIO.TST: by Dale Roberts

 * НАЗНАЧЕНИЕ: Тестирование драйвера GIVEIO, производя какой-нибудь в/в.

 *           : (мы обращаемся к внутреннему динамику PC)



----------------------------------------------------------------------------*/

#include <stdio.h>

#include <windows.h>

#include <math.h>

#include <conio.h>

typedef struct {

      short int pitch;

      short int duration;

} NOTE;

// ТАБЛИЦА НОТ

NOTE notes[] = {{14, 500}, {16, 500}, {12, 500}, {0, 500}, {7, 1000}};

// УСТАНОВКА ЧАСТОТЫ ДИНАМИКА PC В ГЕРЦАХ

// ДИНАМИК УПРАВЛЯЕТСЯ ТАЙМЕРОМ INTEL 8253/8254 С ПОРТАМИ В/В 0X40-0X43

void setfreq(int hz)

{

      hz = 1193180 / hz;          // базовая частота таймера 1.19MHz

      _outp(0x43, 0xb6);          // Выбор таймера 2, операция записи,режим 3

      _outp(0x42, hz);            // устанавливаем делитель частоты

      _outp(0x42, hz >> 8);       // старший байт делителя

}

//-----------------------------------------------------------------------------

//      длительность ноты задается в долях частоты 400 Hz, число 12 задает масштаб

//      Cпикер управляется через порт 0x61. Установка двух младших битов разрешает

//      канал 2 таймера 8253/8254 и включает динамик.

//-----------------------------------------------------------------------------

void playnote(NOTE note)

{

      _outp(0x61, _inp(0x61) | 0x03);            // включаем динамик

            setfreq((int)(400 * pow(2, note.pitch / 12.0))); Sleep(note.duration);

      _outp(0x61, _inp(0x61) & ~0x03);            // выключаем

}

//----------------------------------------------------------------------------

//      открытие и закрытие устройства GIVEIO, что дает нам прямой доступ к в/в;

//      потом пытаемся проиграть музыку

//----------------------------------------------------------------------------

int main()

{

      int            i;

      HANDLE   h;

     

      h = CreateFile("\\\\.\\giveio", GENERIC_READ, 0, NULL, OPEN_EXISTING,

                              FILE_ATTRIBUTE_NORMAL, NULL);

      if(h == INVALID_HANDLE_VALUE)

      {



            printf("Couldn't access giveio device\n"); return -1;

      }

      CloseHandle(h);

      for(i = 0; i < sizeof(notes)/sizeof(int); ++i) playnote(notes[i]);

      return 0;

}

Теперь поговорим о том, как данный способ взаимодействия с портами ввода/вывода может быть использован на благо защитных механизмов. Допустим, наша защита привязывается к физическому дефекту поверхности лазерного диска. Тогда все, что нам надо –— попытаться как можно незаметнее прочитать этот сектор: если он действительно не читается, диск можно считать оригинальным и наоборот. Прямое управление приводом через порты ввода/вывода с вероятностью близкой к единице останется незамеченным даже бывалыми хакерами, потому такой вариант им попросту не придет в голову! Единственное, о чем следует позаботиться, –— не дать обнаружить защитный код по перекрестным ссылкам, оставленных "ругательным" сообщением, которое выводится на экран в том случае, если диск признан пиратским.

Тем не менее, матерых хакеров на такую наживку не возьмешь! Злорадно ухмыльнувшись, они просто поставят точку останова на ввод/вывод в порты 0x1F7/0x177 (для Primary и Secondary приводов соответственно). А чтобы не утонуть в море обращений к приводу посредствомчерез [n2k142] функции API, задействуют условные точки останова, приказывая отладчику "всплывать" только в том случае, если адрес машинной команды, осуществляющей ввод/вывод, находится ниже адреса 0x70000000, т. е., другими словами, принадлежит пользовательскому приложению, а не ядру.

Но что нам мешает с прикладного уровня выполнить команду ввода/вывода по адресу, принадлежащему ядру? Достаточно просто просканировать верхнюю половину адресного пространствао на предмет наличия команд OUT DX, AL (опкод 0xEE) и IN AL, DX (опкод 0xEC). Спрашиваете: а как мы сможем вернуть управление? Да очень просто, –— с помощью обработки структурных исключений. Если машинная команда, следующая за IN/OUT, возбуждает исключение (а таких команд –— хоть пруд пруди), то, перехватив его, мы сможем как ни в чем не бывало продолжить выполнение программы как ни в чем не бывало.

Достоинство этого приема в том, что точка останова, поставленная хакером на порты ввода/вывода, не сработает (точнее, сработает, но будет тут же проглочена фильтром), а недостаток: неоправданное усложнение защитного механизма.


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