Автор: (c)Крис Касперски
Отлаженная программа - это такая программа, для
которой еще не найдены условия, в которых
она откажет.
Программистский фольклор
Секторный уровень взаимодействия всегда привлекал как создателей защитных механизмов, так и разработчиков утилит, предназначенных для копирования защищенных дисков. Еще большие перспективы открывает чтение/запись "сырых" (RAW) секторов - это наиболее низкий уровень общения с диском, какой только штатные приводы способны поддерживать. Большинство защитных механизмов именно так, собственно, и работают. Одни из них прячут ключевую информацию в каналы подкода, другие тем или иным образом искажают коды ECC/EDC, третьи используют нестандартную разметку и т.д. и т.п.
Существует множество способов для работы с диском на секторном уровне и ниже будет описан добрый десяток из них. Большая часть рассматриваемых здесь методик рассчитана исключительно на Windows NT/W2K/XP и не работает в Windows 9x, которой, по-видимому, придется разделить судьбу мамонтов, а потому интерес к ней стремительно тает как со стороны пользователей, так и со стороны программистов. Конечно, какое-то время она еще продержится на плаву, но в долгосрочной перспективе я бы не стал на нее закладываться, особенно учитывая тот факт, что Windows 9x не в состоянии поддерживать многопроцессорные системы, а победоносное шествие Hyper-Threading уже не за горами.
В силу того, что секторный уровень доступа к диску изначально ориентирован на создателей (ломателей) защитных механизмов, данная публикация выкрашена ярко-хакерской краской и рассказывает не только о самих методиках низкоуровневого управления устройствами, но и описывает технику взлома каждого из них. Забегая вперед, заметим, что сломать можно все [1]! Так что не стоит, право же, переоценивать стойкость механизмов, препятствующих несанкционированному копированию лазерных дисков. Если кому-то особо приспичит, вашу программу все равно взломают! Как? А вот об этом и будет рассказано ниже. Как говорится - "кто предупрежден, тот вооружен". Ну, а коль уж совсем невмоготу - используйте прямой доступ к портам ввода/вывода с прикладного уровня. Нет, вы не ослышались - в Windows NT это действительно возможно и ниже будет рассказано, как.
Управление драйверами устройств в операционных системах семейства Windows осуществляется посредством вызова функции DeviceIoControl, отвечающей за посылку специальных FSCTL/IOCTL команд. Префикс FS- свидетельствует о принадлежности данной команды к файловой системе и в контексте настоящей публикации не представляет для нас никакого интереса. Команды с префиксом IO- относятся к устройству, а точнее - к его драйверу. Функция DeviceIoControl просто передает такую команду как она есть, совершенно не задумываясь о ее "физическом смысле". Следовательно, совершенно бессмысленно искать перечень доступных IOCTL-команд в описании DeviceIoControl. Их там нет! Точнее, здесь приводятся лишь стандартные IOCTL-команды, а вся остальная информация по этому вопросу содержится в DDK. Там, в частности, мы найдем, что для чтения отдельных секторов используется команда IRP_MJ_READ, а если нам необходимо прочесть сектор в "сыром" виде, то стоит воспользоваться командой IOCTL_CDROM_RAW_READ. Также обратите свое внимание на команду IOCTL_CDROM_READ_Q_CHANNEL, обеспечивающую извлечение информации из Q-канала подкода. К сожалению, возможности такого способа чтения сырых секторов ограничены лишь CDDA-дисками, поскольку с не-аудиодисков драйвер CDFS сырое чтение не поддерживает.
Функции DeviceIoControl всегда предшествует вызов CreateFile, открывающей соответствующее устройство, которое задается в виде "\\.\X:", где X - буквенное обозначение того привода, с которым мы собрались работать.
Поскольку DeviceIoControl не относится к числу наиболее часто вызываемых функций, защитный механизм, базирующийся на ее использовании, очень легко запеленговать. Достаточно поставить на DeviceIoControl точку останова и дождаться, пока передаваемая ей IOCTL-команда не примет одно из перечисленных выше значений. На CreateFile точку останова лучше не ставить, т.к. это даст множество ложных срабатываний (CreateFile вызывается всякий раз при открытии\создании какого-либо файла). А вот попробовать поискать в теле программы текстовую строку "\\.\" все-таки стоит. И если она действительно будет найдена, вам останется лишь подбежать курсором к перекрестной ссылке и долбануть по Enter'у. Все! Защитный код перед вами!
Для лучшего понимания данного способа взаимодействия между прикладной программой и драйвером ниже приведен ключевой фрагмент функции, как раз и осуществляющей такое взаимодействие (обработка ошибок по соображениям наглядности опущена):
Листинг 1. Функция, демонстрирующая технику чтения сырых секторов через CDFS-драйвер (только для CDDA-дисков!).
Еще один демонстрационный пример приведен ниже. Он иллюстрирует технику чтения TOC (Table of Content) - своеобразный аналог таблицы разделов лазерных аудиодисков.
Листинг 2. Еще один пример программы, взаимодействующей с CDFS-драйвером через IOCTL и читающей содержимое TOC'а (с расшифровкой), изучение которого бывает полезно при анализе некоторых защищенных дисков.
Операционная система Windows NT выгодно отличается тем, что поддерживает режим блочного чтения с устройства - так называемый cooked mode, в котором все содержимое диска трактуется как один большой файл. По этому "файлу" можно перемещаться вызовом функции SetFilePointer и читать/писать отдельные сектора посредством вызовов ReadFile/WriteFile, соответственно. Текущая позиция указателя задается в байтах (не секторах!), однако значение указателя обязано быть кратным логической длине сектора (512 байт для гибких/жестких дисков и 2048 байт для CD-ROM), в противном случае произойдет ошибка. Количество байт, читаемых (записываемых) за один раз, также должно укладываться в целое число секторов. Попытка прочитать сектор по "кусочкам" ни к чему не приведет.
Несмотря на всю изящность и простоту программной реализации, данному способу взаимодействия с приводом присущи серьезные недостатки. Во-первых, он не работает с файловыми системами отличными от ISO 9660/Juliet и High Sierra File System. В переводе на нормальный человеческий язык это обозначает, что для чтения секторов с аудиодисков режим блочного чтения непригоден и походит лишь для обработки дисков с данными. Во-вторых, чтение "сырых" секторов в cooked-mode невозможно и нам придется довольствоваться лишь той их частью, что содержит пользовательские данные (User-Data). Такое положение дел значительно ослабляет стойкость защитного механизма и позволяет легко ввести его в заблуждение. Допустим, защита, основанная на привязке к физическим дефектам поверхности носителя, пытается прочесть ключевой сектор на предмет проверки его читабельности. Поскольку содержимое кодов коррекции защитному механизму недоступно, он не может отличить действительные физические дефекты от их грубой имитации (то есть умышленного искажения ECC/EDC кодов копировщиком с целью эмуляции неустранимых ошибок чтения).
Проверить, использует ли защита данный способ доступа к диску или нет, можно следующим образом: просто установите точку останова на функцию CreateFile, заставив отладчик всплывать в том и только в том случае, если первые четыре символа имени открываемого файла равны "\\.\" (то есть функция открывает не файл, а устройство). Например, это может выглядеть так: "bpx CreateFileA if (*esp->4=='\\\\.\\')", затем нам останется лишь убедиться в том, что за последней косой чертой следует буква именно того привода, который нам нужен (на компьютере автора это привод "\\.\G:"). Дождавшись выхода из функции CreateFile по "P RET" и подсмотрев возращенный ей дескриптор устройства (который будет содержаться в регистре EAX), мы сможем перехватить все вызовы SetFilePointer/ReadFile, анализ окрестностей которых и разоблачит алгоритм работы защитного механизма.
Демонстрационный пример, приведенный ниже, представляет собой вполне законченную утилиту для "грабежа" дисков с данными на секторном уровне с последующей записью всего награбленного в файл.
Листинг 3. Пример, демонстрирующий технику чтения секторов в cooked-mode.
Одной из интереснейших архитектурных особенностей операционной системы Windows NT является ее умение взаимодействовать с IDE-устройствами через SCSI-интерфейс! К сожалению, данная технология чрезвычайно скудно документирована - Platform SDK, MSDN, DDK содержат лишь обрывки информации, а имеющиеся примеры крайне ненаглядны и к тому же выполнены с большим количеством фактических ошибок, так что разобраться с ними под силу лишь профессионалу, ну или очень настырному новичку [2]. И судя по сообщениям в конференциях, достаточно многим программистам осилить технику управления устройствами через SCSI-интерфейс так и не удается, поэтому имеет смысл рассмотреть эту проблему поподробнее.
Для решения поставленной задачи нам понадобится:
а) Описание SCSI-интерфейса (рекомендую "The Linux SCSI programming HOWTO", который можно найти здесь: http://www.ibiblio.org/pub/Linux/docs/HOWTO/other-formats/pdf/SCSI-Programming-HOWTO.pdf);
б) Описание ATAPI-интерфейса для CD-ROM/DVD накопителей (см. "ATA Packet Interface for CD-ROMs" и "Specification for ATAPI DVD Devices", причем спецификации на DVD гораздо лучше и полнее описывают CD-ROM, чем их родная документация; не самые свежие, но вполне подходящие ревизии можно найти здесь: www.stanford.edu/~csapuntz/specs/INF-8020.PDF и ftp.seagate.com/sff/INF-8090.PDF);
в) Описание форматов хранения данных на лазерных дисках (см. standard ECMA-130 "Data interchange on read-only 120 mm optical data disks", который можно найти здесь: http://www.ecma-international.org/publications/files/ecma-st/Ecma-130.pdf);
г) Помимо этого годится любая литература, так или иначе затрагивающая вопросы программирования CD-ROM; нелишним будет почитать "ATAPI(IDE) CD Информация к размышлению" от Константина Норватова и "Особенности программирования CD-ROM'а на Спектруме" от Влада Сотникова.
Итак, что же такое SCSI? Это - стандартизованный платформенно-независимый интерфейс, обеспечивающий согласованное взаимодействие различных устройств и высокоуровневых приложений. Собственно, аббревиатура SCSI именно так и расшифровывается - Small Computer System Interface (Системный Интерфейс Малых Компьютеров). Благодаря SCSI для низкоуровневого управления устройствами совершенно необязательно прибегать к написанию собственных драйверов (писать драйвер только для того, чтобы прорваться сквозь ограничения API - чистейший маразм) и эту задачу можно решить и на прикладном уровне, посылая устройству специальные CDB-блоки, содержащие стандартные или специфичные для данного устройства команды управления вместе со всеми необходимыми им параметрами. Собственно, "CDB" так и расшифровывается - Command Descriptor Block. Пример одного из таких блоков приведен ниже:
Cмещение, байт |
Содержимое |
|
0x0 |
0x28 |
Код команды "read sector" |
0x1 |
0x00 |
Зарезервировано |
0x2 |
0x00 |
Номер сектора - 0х69 |
0x3 |
0x00 |
|
0x4 |
0х00 |
|
0x5 |
0x69 |
|
0x6 |
0x00 |
Кол-во секторов |
0x7 |
0x01 |
|
0x8 |
0x00 |
Зарезервировано |
0x9 |
0x00 |
Зарезервировано |
0xA |
0x00 |
Зарезервировано |
Таблица 1. Пример CDB блока, который будучи переданным SCSI-устройству, заставит его прочитать 0x69-й сектор.
Первый байт блока представляет собой команду операции (в нашем случае: 0x28 - чтение одного или нескольких секторов), а все остальные байты блока - параметры данной команды. Причем обратите внимание на тот факт, что младший байт слова располагается по большему адресу, то есть все происходит не так, как в привычном нам IBM PC! Поэтому если передать в качестве номера первого сектора последовательность 0x69 0x00 0x00 0х00, то прочитается 0x6900000 сектор, а вовсе не 0x90000069, как можно было того ожидать!
Краткое описание стандартных SCSI-команд можно найти в том же "The Linux SCSI programming HOWTO", однако для наших целей их навряд ли окажется достаточно и команды, специфичные для CD-ROM дисков, мы рассмотрим отдельно. Однако это произойдет не раньше, чем мы разберемся, как CDB-блоки упаковываются в SRB-конверт (SCSI Request Block), без которого операционная система просто не поймет, что же мы хотим сделать (как известно, машинная программа выполняет то, что ей приказали сделать - иногда это совпадает с тем, что от нее хотели, иногда нет).
Структура SRB-блока подробно описана в NT DDK, поэтому не будем подробно на ней останавливаться и пробежимся по основным полям лишь вкратце.
Заполнив поля структуры SCSI_REQUEST_BLOCK подобающим образом, мы можем передать SRB-блок выбранному нами устройству посредством функции DeviceIoControl, просто задав соответствующий код IOCTL. Вот, собственно, и все! Заглотив наживку, операционная система передаст CDB-блок соответствующему устройству и оно выполнит (или не выполнит) содержащиеся в нем (СDB-блоке) команды. Обратите внимание: CDB-блок обрабатывается не драйвером устройства, но самим устройством, а потому мы имеем практически неограниченные возможности по управлению последним. И все это - с прикладного уровня!
Теперь о грустном. Процедура управлениями устройствами довольно капризна и одно-единственное неправильно заполненное поле может обернуться полным нежеланием устройства выполнять передаваемые ему команды. Вместо этого будет возвращаться код ошибки или вовсе не возвратится ничего. К тому же, малейшая неаккуратность может запросто испортить данные на всех жестких дисках, а потому с выбором значений TargetID и lun вы должны быть особенно внимательными! (Для автоматического определения физического адреса CD-ROM'а можно использовать SCSI-команду SCSI_INQUIRY - см. демонстрационный пример \NTDDK\src\win_me\block\wnaspi32 из DDK). Однако довольно говорить об опасностях (без них жизнь была бы слишком скучной), переходим к самому интересному - поиску того самого IOCTL-кода, который этот SRB-блок, собственно, и передает.
Оказывается, напрямую это сделать не так-то просто, точнее - легальными средствами вообще невозможно! Создатели Windows по ряду соображений решили предоставить полный доступ к полям структуры SCSI_REQUEST_BLOCK только писателям драйверов, а прикладных программистов оставили наедине с структурами SCSI_PASS_THROUGH и SCSI_PASS_THROUGH_DIRECT, схожими по назначению с SRB, но несколько ограниченными в своей функциональности. К счастью, на содержимое CDB-блоков не было наложено никаких ограничений, а потому возможности для низкоуровневых операций с железом у нас все-таки остались. Подробнее обо всем этом можно прочитать в разделе "9.2 SCSI Port I/O Control Codes" из NT DDK, а также из исходного текста демонстрационного примера "\NTDDK\src\storage\class\spti" из того же DDK (обратите внимание на файл spti.htm, лежащий в этом же каталоге, который достаточно подробно описывает суть управления устройством через SСSI-интерфейс).
Согласно наименованию каталога с демонстрационным примером, данный способ взаимодействия с устройством носит название SPTI и расшифровывается как SCSI Pass Through IOCTLs - т.е. SCSI, проходящий через IOCTL. Кратко перечислим основные особенности и ограничения SPTI-интерфейса. Во-первых, для передачи CDB-блоков устройству вы должны обладать привилегиями администратора, что не всегда удобно. Во-вторых, использование многоцелевых команд запрещено (т.е. мы не можем отдать команду копирования данных с устройства А на устройство Б в обход процессора, хотя такие команды у современных приводов есть и было бы очень здорово копировать лазерные диски, совершенно не загружая процессор). В-третьих, реверсивное (то бишь, двунаправленное) перемещение данных не поддерживается и в каждый момент времени данные могут перемещаться либо от устройства к компьютеру, либо от компьютера к устройству, но не то и другое одновременно!). В-четвертых, при установленном class-драйвере для целевого устройства, мы должны направлять CDB-блоки именно class-драйверу, но не самому SCSI-устройству. То есть, для управления CD-ROM'ом вы должны взаимодействовать с ним через устройство \\.\X:, где X - буква привода, попытка же обращения к "\\.\Scsi0:" возвратит ошибку (и это, как показывает практика, основной камень преткновения неопытных программистов, начинающих программировать раньше, чем читать документацию [3]). Наконец, в пятых, сама структура SCSI_PASS_THROUGH_DIRECT содержит значительно меньше полей, причем значение полей PathId, TargetId и Lun игнорируются! Физический адрес устройства на шине определяется непосредственно самой операционной системой по символьному имени дескриптора устройства, которому, собственно, и посылается SCSI_PASS_THROUGH_DIRECT запрос.
Листинг 5. Формат структуры SCSI_PASS_THROUGH_DIRECT (структура SCSI_PASS_THROUGH во всем похожа на нее, но не обеспечивает передачу данных через DMA).
К счастью, цензура в основном коснулась тех полей, которые все равно практически не используются в реальной жизни, так что мы ровным счетом ничего не потеряли. Заполняем оставшиеся поля и наша структура готова!
Естественно, прежде чем передать ее устройству, нам необходимо получить дескриптор этого самого устройства. Это можно сделать так:
Листинг 6. Открытие привода для получения дескриптора, использующегося для его уприавления.
Убедившись, что hCD не равно INVALID_HANDLE_VALUE, передаем полученный дескриптор вместе с самой структурой IOCTL_SCSI_PASS_THROUGHT_DIRECT функции DeviceIoControl, вызывая ее следующим образом:
Листинг 7. Передача структуры IOCTL_SCSI_PASS_THROUGH.
Где srb - и есть заполненный экземпляр структуры IOCTRL_SCSI_PASS_THROUGHT, а returned - переменная, в которую будет записано количество байт, возращенных устройством. В свою очередь, sense_buf - это тот самый буфер, в котором заполненный нами экземпляр IOCTL_SCSI_PASS_THROUGHT_DIRECT возвращается назад, да не один, а вместе с sense info - кодом ошибки завершения операции. Если же операция завершилась без ошибок, то sense info не возвращается и sense_buf содержит только IOCTL_SCSI_PASS_THROUGHT. Позиция размещения sense info в буфере определяется содержимым поля SenseInfoOffset, значение которого должно быть подобрано так, чтобы не "наступать на пятку" структуре IOCTRL_SCSI_PASS_THROUGHT, т.е., попросту говоря, минимально возможное смещение Sense Info равно: srb.SenseInfoOffset = sizeof(SCSI_PASS_THROUGH_DIRECT). Обратите внимание, SenseInfoOffset - это не указатель на Sense Info, но индекс первого байта Sense Info в возвращаемом буфере!
Для определения факта наличия ошибки необходимо проанализировать количество байт, возращенных функцией DeviceIoControl в переменной returned. Если оно превышает размер структуры IOCTL_SCSI_PASS_THROUGHT, то в буфере находится sense info, а раз есть sense info, то есть и ошибка! Формат sense info приведен на рисунке ниже:
Рисунок 1. Формат SENSE INFO, возвращаемой устройством в случае возникновения ошибки.
Первый байт указывает на тип ошибки и обычно принимает значение 70h (текущая ошибка - current error) или 71h (отсроченная ошибка - deferred error). Коды ошибок с 72h по 7Eh зарезервированы, причем ошибки с кодом 7Eh указывают на нестандартный (vendor-specific) sense info формат. Коды ошибок с 00h по 6Fh в спецификации CD-ROM ATAPI не определены и потому их использование нежелательно (данное предостережение, разумеется, адресовано не программистам, а разработчикам аппаратуры).
Описание ошибки кодируется тройкой чисел: Sense Key, Additional Sense Code (дополнительный смысловой код, сокращенно - ASC) и Additional Sense Code Qualifier (ASCQ). Вершину этой иерархической пирамиды возглавляет Sense Key, содержащее общую категорию ошибки (genetic categories), затем идет дополнительный смысловой код, более детально описывающий ошибку и, наконец, на самом низу иерархии находится квалификатор дополнительного смыслового кода, уточняющий непосредственно сам дополнительный смысловой код. Если ошибка исчерпывающее описывается одним лишь Sense Key и ASC, то ASCQ в таком случае отсутствует (точнее, находится в неопределенном состоянии).
Расшифровка основных кодов ошибок описывается в двух таблицах, приведенных ниже. Стоит сказать, что для анализа ошибки значение Sense Key, в общем-то, некритично, т.к. гарантируется, что каждый ASC принадлежит только одному Sense Key и напротив - один и тот же ASCQ может принадлежать нескольким различным ASC и потому в отрыве от последнего он бессмысленнен.
Sense Key |
Описание |
00h |
NO SENSE. Нет дополнительной sense info. Операция выполнена успешно. |
01h |
RECOVERED ERROR (восстановленная ошибка). Операция выполнена успешно, но в процессе ее выполнения возникли некоторые проблемы, устраненные непосредственно самим приводом. За дополнительной информацией обращайтесь к ключам ASC и ASCQ. |
02h |
NOT READY (не готов). Устройство не готово. |
03h |
MEDIUM ERROR (ошибка носителя). В процессе выполнения операция произошла неустранимая ошибка, вызванная, по всей видимости, дефектами носителя или ошибкой записи данных. Данный sense key может возвращаться и в тех случаях, когда привод оказывается не в состоянии отличить дефект носителя от аппаратного сбоя самого привода. |
04h |
HARDWARE ERROR (аппаратная ошибка). Неустранимая аппаратная ошибка (например, отказ контроллера). |
05h |
ILLEGAL REQEST (неверный запрос). Неверные параметры, переданные приводу в CDB-пакете (например, начальный адрес больше конечного). |
06h |
UNIT ATTENTION (модуль требует внимания). Носитель заменен или выполнен сброс контроллера привода. |
07h |
DATA PROTECT (защищенные данные). Попытка чтения защищенных данных. |
8h -0Ah |
Зарезервировано |
0Bh |
ABORTED COMMAND (команда прервана). По тем или иным причинам выполнение команды было прервано. |
0Eh |
MISCOMPARE (ошибка сравнения). Исходные данные не соответствуют данным, прочитанным с носителя. |
0Fh |
Зарезервировано |
Таблица 2. Основные Sense Key (категории ошибок) и их описания.
ASC | ASCQ | DROM | Описание |
00 | 00 | DROM | NO ADDITIONAL SENSE INFORMATION |
00 | 11 | R | PLAY OPERATION IN PROGRESS |
00 | 12 | R | PLAY OPERATION PAUSED |
00 | 13 | R | PLAY OPERATION SUCCESSFULLY COMPLETED |
00 | 14 | R | PLAY OPERATION STOPPED DUE TO ERROR |
00 | 15 | R | NO CURRENT AUDIO STATUS TO RETURN |
01 | 00 | R | MECHANICAL POSITIONING OR CHANGER ERROR |
02 | 00 | DROM | NO SEEK COMPLETE |
04 | 00 | DROM | LOGICAL DRIVE NOT READY - CAUSE NOT REPORTABLE |
04 | 01 | DROM | LOGICAL DRIVE NOT READY - IN PROGRESS OF BECOMING READY |
04 | 02 | DROM | LOGICAL DRIVE NOT READY - INITIALIZING COMMAND REQUIRED |
04 | 03 | DROM | LOGICAL DRIVE NOT READY - MANUAL INTERVENTION REQUIRED |
05 | 01 | DROM | MEDIA LOAD - EJECT FAILED |
06 | 00 | DROM | NO REFERENCE POSITION FOUND |
09 | 00 | DRO | TRACK FOLLOWING ERROR |
09 | 01 | RO | TRACKING SERVO FAILURE |
09 | 02 | RO | FOCUS SERVO FAILURE |
09 | 03 | RO | SPINDLE SERVO FAILURE |
11 | 00 | DRO | UNRECOVERED READ ERROR |
11 | 06 | RO | CIRC UNRECOVERED ERROR |
15 | 00 | DROM | RANDOM POSITIONING ERROR |
15 | 01 | DROM | MECHANICAL POSITIONING OR CHANGER ERROR |
15 | 02 | DRO | POSITIONING ERROR DETECTED BY READ OF MEDIUM |
17 | 00 | DRO | RECOVERED DATA WITH NO ERROR CORRECTION APPLIED |
17 | 01 | DRO | RECOVERED DATA WITH RETRIES |
17 | 02 | DRO | RECOVERED DATA WITH POSITIVE HEAD OFFSET |
17 | 03 | DRO | RECOVERED DATA WITH NEGATIVE HEAD OFFSET |
17 | 04 | RO | RECOVERED DATA WITH RETRIES AND/OR CIRC APPLIED |
17 | 05 | DRO | RECOVERED DATA USING PREVIOUS SECTOR ID |
18 | 00 | DRO | RECOVERED DATA WITH ERROR CORRECTION APPLIED |
18 | 01 | DRO | RECOVERED DATA WITH ERROR CORRECTION & RETRIES APPLIED |
18 | 02 | DRO | RECOVERED DATA - THE DATA WAS AUTO-REALLOCATED |
18 | 03 | R | RECOVERED DATA WITH CIRC |
18 | 04 | R | RECOVERED DATA WITH L-EC |
1A | 00 | DROM | PARAMETER LIST LENGTH ERROR |
20 | 00 | DROM | INVALID COMMAND OPERATION CODE |
21 | 00 | DROM | LOGICAL BLOCK ADDRESS OUT OF RANGE |
24 | 00 | DROM | INVALID FIELD IN COMMAND PACKET |
26 | 00 | DROM | INVALID FIELD IN PARAMETER LIST |
26 | 01 | DROM | PARAMETER NOT SUPPORTED |
26 | 02 | DROM | PARAMETER VALUE INVALID |
28 | 00 | ROM | NOT READY TO READY TRANSITION, MEDIUM MAY HAVE CHANGED |
29 | 00 | ROM | POWER ON, RESET OR BUS DEVICE RESET OCCURRED |
2A | 00 | ROM | PARAMETERS CHANGED |
2A | 01 | ROM | MODE PARAMETERS CHANGED |
30 | 00 | ROM | INCOMPATIBLE MEDIUM INSTALLED |
30 | 01 | RO | CANNOT READ MEDIUM - UNKNOWN FORMAT |
30 | 02 | RO | CANNOT READ MEDIUM - INCOMPATIBLE FORMAT |
39 | 00 | ROM | SAVING PARAMETERS NOT SUPPORTED |
3A | 00 | ROM | MEDIUM NOT PRESENT |
3F | 00 | ROM | ATAPI CD-ROM DRIVE OPERATING CONDITIONS HAVE CHANGED |
3F | 01 | ROM | MICROCODE HAS BEEN CHANGED |
40 | NN | ROM | DIAGNOSTIC FAILURE ON COMPONENT NN (80H-FFH) |
44 | 00 | ROM | INTERNAL ATAPI CD-ROM DRIVE FAILURE |
4E | 00 | ROM | OVERLAPPED COMMANDS ATTEMPTED |
53 | 00 | ROM | MEDIA LOAD OR EJECT FAILED |
53 | 02 | ROM | MEDIUM REMOVAL PREVENTED |
57 | 00 | R | UNABLE TO RECOVER TABLE OF CONTENTS |
5A | 00 | DROM | OPERATOR REQUEST OR STATE CHANGE INPUT (UNSPECIFIED) |
5A | 01 | DROM | OPERATOR MEDIUM REMOVAL REQUEST |
63 | 00 | R | END OF USER AREA ENCOUNTERED ON THIS TRACK |
64 | 00 | R | ILLEGAL MODE FOR THIS TRACK |
B9 | 00 | R | PLAY OPERATION OBORTED |
BF | 00 | R | LOSS OF STREAMING |
Таблица 3. Основные ASC и ASCQ-коды.
Как видите - все просто! Единственное, с чем мы еще не разобрались - это ATAPI. Поскольку мы не собираемся взаимодействовать с ATAPI-интерфейсом напрямую (этой возможности "благодаря" архитекторам Windows мы, увы, лишены), промчимся галопом лишь по ключевым аспектам и особенностям. Как пишет Михаил Гук в своей книге "Интерфейсы персональных компьютеров" - "Для устройств, логически отличающихся от жестких дисков - оптических, магнитооптических, ленточных и любых других - в 1996 г. была принята спецификация ATAPI. Это пакетное расширение интерфейса, которое позволяет передавать по шине ATA-устройству блоки командной информации, структура которых была позаимствована из SCSI". Теперь, по крайней мере, становится понятно, почему Windows так лихо "превращает" ATAPI-устройства в SCSI. Если отбросить аппаратные различия интерфейсов, которые с программного уровня все равно не видны, то ATAPI-интерфейс будет очень напоминать SCSI. Во всяком случае, управление ATAPI-устройствами осуществляется посредством тех самых CDB-блоков, которые мы уже рассматривали выше.
Естественно, чтобы управлять устройством, необходимо знать - какими именно командами оно управляется. Для получения этой информации нам понадобится "ATAPI Packet Commands for CD-ROM devices". Откроем его на описании команды READ CD command (код 0xBEh) и обнаружим таблицу следующего содержания:
Рисунок 2. Описание команды READ CD.
Первый байт, представляющий собой код выполняемой команды, никаких вопросов не вызывает, но вот дальше мы сталкиваемся с полем Expected Sector Type, задающим тип требуемого сектора. Перевернув несколько страниц вперед, мы найдем коды, соответствующие всем существующим типам секторов: CDDA, Mode 1, Mode 2, Mode 2 Form 1 и Mode 2 Form 2. Если же тип сектора заранее неизвестен, передавайте с этим полем 0x0, что обозначает "нас устроит любой тип сектора".
Следующие четыре байта занимает адрес первого читаемого сектора, заданный в формате LBA (т.е. Logical Block Address). За этой страшной аббревиатурой скрывается элегантный способ сквозной нумерации секторов. Если вы когда-то программировали древние жесткие диски, то наверняка помните, какие громоздкие расчеты приходилось выполнять, чтобы определить к какой головке, цилиндру, сектору каждый байт прилежит. Теперь же можно обойтись безо всех этих заморочек. Первый сектор имеет номер 0, затем идет 1, 2, 3... и так до последнего сектора диска. Только помните, что порядок байт в этом двойном слове обратный, т.е. старший байт старшего слова идет первым.
Байты с шестого по восьмой оккупировал параметр, задающий количество читаемых секторов. Вот какая несправедливость, однако - для адреса сектора выделяется четыре байта, а для количества читаемых секторов только три. Шутка! Вы же ведь не собираетесь читать весь диск за раз?! Порядок байт здесь тоже обратный, так что не ошибитесь, иначе при попытке считать один-единственный сектор вы запросите добрую половину диска целиком!
Девятый байт наиболее интересен, ибо он хранит флаги, определяющие - какие части сектора мы хотим прочитать. Помимо пользовательских данных, мы можем запросить синхробайты, заголовок (Header), EDC/ECC коды и даже флаги ошибок чтения (для взлома некоторых защит это самое то, правда эту возможность поддерживают не все приводы).
Десятый байт отвечает за извлечение данных их подканалов, однако поскольку эти же самые данные уже содержаться в заголовке, то без них можно, в принципе, и обойтись.
Наконец, последний - одиннадцатый, считая от нуля байт, никак не используется и зарезервирован на будущее, а потому для гарантии совместимости с новыми моделями приводов, он должен быть равен нулю.
Естественно, в зависимости от рода и количества запрашиваемых данных, длина возращенного сектора может варьироваться в очень широких пределах. Вот, смотрите:
Рисунок 3. Взаимосвязь рода запрошенных данных и длины возвращаемого сектора.
Рисунок 4. Внутренний мир Windows NT. IDE-устройства с прикладного уровня видятся как SCSI. Разумеется, на физическом уровне с приводом не происходит никаких изменений и CD-ROM привод с IDE-интерфейсом так IDE-приводом и остается со всеми присущими ему достоинствами и недостатками. Однако IRP-запросы к этому драйверу, проходя через Storage Class Driver, транслируются в SRB (SCSI request block). Затем SRB-запросы попадают в Storage port driver (т.е. непосредственно в сам драйвер привода), где они заново транслируются в конкретные физические команды данного устройства (см. рис. 3). Подробности этого увлекательного процессора можно почерпнуть из NT DDK (см. "1.1 Storage Driver Architecture"), здесь же достаточно указать на тот немаловажный факт, что кроме команд семейства IRP_MJ_ххх мы также можем посылать устройству и SRB-запросы, которые обладают значительно большей свободой и гибкостью. Однако такое взаимодействие невозможно осуществить непосредственно с прикладного уровня, поскольку IRP-команды относятся к числу приватных команд, в то время как API-функция DeviceIoControl передает лишь публичные команды, явно обрабатываемые драйвером в диспетчере IRP_MJ_DEVICE_CONTROL.
Давайте теперь, в порядке закрепления всего вышесказанного, попытаемся создать программу, которая бы читала сектора с лазерных дисков в сыром виде. Ее ключевой фрагмент (вместе со всеми необходимыми комментариями) приведен ниже:
Листинг 8. Функция, читающая сектора в сыром виде через SPTI.
Остается только сказать, что защитные механизмы, взаимодействующие с диском через SPTI, элементарно ломаются установкой точки останова на функции CreateFile/DeviceIoControl. Для предотвращения "лишних" всплытий отладчика фильтр точки останова должен реагировать только на те вызовы CreateFile, чей первый слева аргумент равен "\\.\X:" или "\\.\CdRomN". Соответственно, второй слева аргумент функции DeviceIoControl должен представлять собой либо IOCTL_SCSI_PASS_THROUGHT, либо IOCTL_SCSI_PASS_THROUGHT_DIRECT, шестнадцатеричные значения кодов которых 0x4D004 и 0x4D014, соответственно.
Вот два основных недостатка интерфейса SPTI (только что описанного выше): для взаимодействия с устройством он требует наличия прав администратора и, что еще хуже, SPTI поддерживается только операционными системами семейства NT и отсутствует на Windows 9x/ME. Единственный легальный способ дотянуться до CD-ROM'а под Windows 9x - воспользоваться 16-разрядным шлюзом, напрямую обращающемуся к MS-DOS драйверу MSCDEX, который обеспечивает значительно большую функциональность, нежели Windows-драйвер. Естественно, параллельная поддержка двух семейств операционных систем требует от программиста значительных усилий, что существенно повышает себестоимость программного продукта.
Для упрощения разработки кросс-платформенных приложений фирма Adaptec разработала специальный системно-независимый интерфейс, позволяющий управлять различными SCSI-устройствами с прикладного уровня, и назвала его ASPI - Advanced SCSI Programming Interface (хотя неофициально его расшифровывают как Adaptec SCSI Programming Interface, поскольку это больше соответствует истине).
Системнонезависимость интерфейса ASPI обеспечивается двухуровневой моделью его организации: архитектурно он состоит из низкоуровневого драйвера и прикладной библиотеки-обертки. ASPI-драйвер разрабатывается с учетом специфики конкретной операционной системы и отвечает за непосредственной управление SCSI-шиной (реальной или виртуальной - не суть важно). Поскольку интерфейс между операционной системой и драйверам меняется от одной операционной системы к другой, для сокрытия всех этих различий используется специальная ASPI-библиотека, предоставляющая единый унифицированный интерфейс для всех операционных систем.
Рассмотрим, как осуществляется внедрение ASPI-интерфейса в операционную систему на примере Windows Me. На самом высоком уровне иерархии находятся прикладные библиотеки WNASPI32.DLL и WINASPI.DLL для 32- и 16-разрядных приложений, соответственно. Они экспортируют три базовых ASPI-функции: GetASPI32DLLVersion, GetASPI32SupportInfo и SendASPI32Command (причем, последняя - самая важная) и три вспомогательных: GetASPI32Buffer, FreeASPI32Buffer, TranslateASPI32Address (последняя - только в 32-разрядной версии библиотеки).
Посредством функции DeviceIoControl они взаимодействуют с ASPI-драйвером, расположенном "ниже" и в зависимости от версии операционной системы называющимся либо APIX.VXD (Windows 9x), либо ASPI.SYS (Windows NT) [4] и создающим в процессе своей инициализации устройство с непроизносимым названием MbMmDp32. Только не спрашивайте меня, как это абракадабра расшифровывается - ответ похоронен в застенках компании Adaptec.
В принципе, ничего не мешает взаимодействовать с ASPI-драйвером и напрямую - в обход библиотеки WNASPI32.dll. Собственно, многие разработчики защитных механизмов именно так и поступают. Достаточно лишь дизассемблировать WNASPI32.dll и разобраться каким ASPI-командам какие IOCTL-коды соответствуют (ASPI-протокол по понятным соображениям не документирован). Действительно, на SendASPI32Command очень легко поставить бряк и тогда хакер мгновенно локализует защитный код. С вызовами же DeviceIoControl в силу их многочисленности взломщикам справиться намного труднее. К тому же начинающие ломатели защит (а таких среди хакеров - большинство) весьма смутно представляют себе архитектуру ввода-вывода и уж тем более не разбираются в ASPI-протоколе. Впрочем, для опытных хакеров такая защита - не преграда (подробнее см. "Способы разоблачения защитных механизмов").
Сам же ASPI-драйвер "подключен" к SCSI и IDE/ATAPI портам, за счет чего он позволяет управлять всеми этими устройствами (и приводами CD-ROM, в том числе).
Рисунок 5. Архитектура подсистемы ввода-вывода Windows 98. Клиентские модули (на данной схеме они обозначены цифрами 1, 2 и 3) посылают свои запросы драйверу файловой системы - Instable File System (обозначенному цифрой 6). В распоряжении клиентских модулей также имеются библиотеки ASPI для 32- и 16-разрядных приложений соответственно (они обозначены цифрами 4 и 6). От всей системы они стоят особняком, поскольку разработаны независимой компанией Adaptec и представляют собой факультативные компоненты. Драйвер файловой системы перенаправляет полученный им запрос на один их следующих специализированных драйверов, среди которых присутствует и драйвер привода CD-ROM - CDFS.VxD, обозначенный цифрой 8. В его задачи входит поддержка файловых систем лазерных дисков, как то - ISO 9660, High Sierra или другие файловые системы. Уровнем ниже лежит Volume Tracker (цифра 14), отслеживающий смену диска в накопителе, а еще ниже находится непосредственно сам драйвер, поддерживающий данную модель CD-ROM - так называемый CD type specific driver, реализуемый драйвером CDVSD.VxD и среди прочих обязанностей отвечающий за назначение буквы приводу. Это и есть секторный уровень взаимодействия с диском, никаких файловых систем здесь нет и в помине. Несмотря на то, что данный драйвер специфичен для конкретной модели привода CD-ROM, он совершенно независим от его физического интерфейса, поскольку опирается на CD-ROM device SCSI'zer (цифра 21), преобразующий IOP-запросы, поступающие от вышележащих драйверов, в SRB-пакеты, направляемые нижележащим драйверам (подробнее об этом см. раздел "Доступ через SCSI-порт"). Еще ниже находится SCSI CD-ROM helper (цифра 23), обеспечивающий стыковку SCSI'zer-а с SCSI-портом. Сам же SCSI-port, создаваемый менеджером SCSI-портов (цифра 26) представляет собой унифицированное системно-независимое средство взаимодействия драйверов среднего уровня с физическим (или виртуальным) оборудованием. К одному из таких SCSI-портов и подключается ASPI-драйвер (цифра 18), реализованный в файле APIX.VxD и восходящий к своим "оберткам" - WNASPI32.DLL и WNASPI.DLL (цифры 11 и 12 соответственно). Ниже SCSI-менеджера расположены драйвера мини-портов, переводящие SCSI-запросы в язык конкретной интерфейсной шины. В частности, драйвер, обеспечивающий поддержку IDE-устройств, реализован в файле ESDI_506.PDR (цифра 29). Естественно, при желании мы можем общаться с IDE-устройствами и через IDE/ATAPI-порты (цифра 25), реализованные все тем же драйвером ESDI_506.PDR (ASPI-драйвер по соображениям производительности именно так, собственно, и поступает). Левую часть блок-схемы, изображающую иерархию драйверов прочих дисковых устройств мы не рассматриваем, т.к. она не имеет никакого отношения к теме нашего обсуждения.
Для программирования под ASPI требуется как минимум две вещи: ASPI-драйвер и ASPI-SDK. Драйвер можно бесплатно скачать с сервера самой Adaptec (ею разработаны драйвера для следующих операционных систем: MS-DOS, Novell, Windows 9x, Windows NT/W2KXP), а вот SDK с некоторого момента распространяется за деньги. И хотя его стоимость чисто символическая (что-то около 10$, если мне не изменяет память), неразвитость платежных систем в России превращает процесс покупки в довольно затруднительное дело. Однако все необходимое для работы (документация, заголовочные файлы, библиотеки) можно позаимствовать из... Windows Me DDK (кстати, входящего в состав DDK для Windows 2000). Так что, если у вас уже есть W2K DDK, вам не о чем беспокоиться. В противном случае попробуйте обратиться к MSDN, распространяемым вместе с Microsoft Visual Studio 6.0. Здесь вы найдете документацию и заголовочные файлы, ну а недостающие библиотеки из соответствующих DLL можно получить и самостоятельно (lib.exe с ключом /DEF), либо же вовсе обойтись без них, загружая все необходимые функции через LoadLibrary/GetProcAddress.
Поскольку ASPI-интерфейс хорошо документирован (руководство по программированию насчитывает порядка 35 листов), то его освоение не должно вызвать никаких непреодолимых проблем (во всяком случае, после знакомства с SPTI). К тому же, в Windows Me DDK входит один законченный демонстрационный пример использования ASPI, найти который можно в папке "\src\win_me\block\wnaspi32\". Несмотря на досадный суффикс "Me", он отлично уживается и с другими операционными системами, как-то: Windows 98, Windows 2000, Windows XP и т.д.
Впрочем, реализован этот пример на редкость криво и с большим количеством ошибок, а его наглядность такова, что менее наглядного примера для демонстрации ASPI пожалуй и не подобрать! Уж лучше исследовать исходные тексты программы CD slow, которые можно легко найти в Интернете (однако она написана на ассемблере, а с ассемблером знаком не всякий).
Кратко перечислим основные недочеты демонстрационного примера aspi32ln.c: во-первых, это не консольная программа, а GUI'ная, а потому большая часть ее кода к ASPI вообще никакого отношения не имеет. Во-вторых, используется единая функция для получения уведомлений сразу от выполнения двух команд: SCSI_INQUIRY и SCSI_READ10, причем последняя в половине случаев заменена своей константой 0x28, что тоже не способствует ее пониманию. В-третьих, накопители на CD-ROM программой поддерживаются лишь частично. Плохо спроектированная архитектура программы не позволила разработчикам осилить поставленную перед ними задачу. Поэтому ветка, отвечающая за чтение с CD-ROM в функции ASPI32Post специальным образом закомментирована. Если же наложенную блокировку убрать, то при чтении станет происходить ошибка, поскольку программа ориентирована лишь на те накопители, чей размер сектора составляет 0x200 байт. Приводы CD-ROM дисков, чей сектор вчетверо больше, очевидно к этой категории не относятся и, чтобы не переписывать всю программу целиком, единственное, что можно сделать - это увеличить размер запрашиваемого блока данных до 0х800 байт (с жестких дисков будет считываться по четыре сектора за раз, что вполне допустимо). Наконец, в-пятых, инкремент (т.е. вычисление адреса следующего считываемого блока) реализован через одно место и поэтому вообще неработоспособен.
Ладно, не будет увлекаться критикой сопроводительных примеров (даже плохой программный код все же лучше, чем совсем ничего) и перейдем непосредственно к изучению ASPI-интерфейса, а точнее - его важнейшей команды SendASPI32Command, обеспечивающей передачу SRB-блоков устройству (со всеми остальными командами вы без труда справитесь и самостоятельно).
Структура SRB_ExecSCSICmd, в которую, собственно, и упаковывается SRB-запрос, как две капли воды похожа на SCSI_PASS_THROUGH_DIRECT. Во всяком случае, между ними больше сходства, чем различий. Вот, взгляните сами:
typedef struct { BYTE SRB_Cmd; // ASPI command code = SC_EXEC_SCSI_CMD BYTE SRB_Status // ASPI command status byte BYTE SRB_HaId; // ASPI host adapter number BYTE SRB_Flags; // ASPI request flags DWORD SRB_Hdr_Rsvd; // Reserved, MUST = 0 BYTE SRB_Target; // Target's SCSI ID BYTE SRB_Lun; // Target's LUN number WORD SRB_Rsvd1; // Reserved for Alignment DWORD SRB_BufLen; // Data Allocation Length LPBYTE SRB_BufPointer; // Data Buffer Pointer BYTE SRB_SenseLen; // Sense Allocation Length BYTE SRB_CDBLen; // CDB Length BYTE SRB_HaStat; // Host Adapter Status BYTE SRB_TargStat; // Target Status LPVOID SRB_PostProc; // Post routine BYTE SRB_Rsvd2[20]; // Reserved, MUST = 0 BYTE CDBByte[16]; // SCSI CDB BYTE SenseArea[SENSE_LEN+2]; // Request Sense buffer } SRB_ExecSCSICmd, *PSRB_ExecSCSICmd;
Листинг 9. Структура SRB_ExecSCSICmd.
Обратите внимание: для взаимодействия с устройством вам совершенно незачем знать его дескриптор! Достаточно указать его физический адрес на шине (т.е. правильно заполнить поля SRB_HaId и SRB_Target).... а как их узнать? Да очень просто: достаточно разослать по всем физическим адресам команду INQUIRY (код 12h). Устройства, реально (и/или виртуально) подключенные к данному порту, вернут идентификационную информацию (среди прочих полезных данных содержащую и свое имя), а несуществующие устройства не вернут ничего и операционная система отрапортует об ошибке.
Простейшая программа опроса устройств может выглядеть, например, так:
Листинг 10. Последовательный опрос портов на предмет наличия подключенных к ним устройств.
Листинг 11. Устройства, подключенные к компьютеру автора. Первая слева цифра - adapter ID, следующая за ней - target ID.
Другое немаловажное достоинство ASPI-интерфейса по сравнению с SPTI состоит в поддержке асинхронного режима обработки запросов. Отдав запрос на чтение такого-то количество секторов, вы можете продолжить выполнение своей программы, не дожидаясь пока процесс чтения секторов полностью не завершится. Конечно, для достижения аналогичного результата при использовании интерфейса SPTI достаточно всего лишь создать еще один поток, но... это уже не так элегантно и красиво.
Листинг 12. Демонстрационный пример программы, осуществляющий сырое чтение сектора с CD-диска.
Откомпилировав этот пример и запустив его на выполнение, убедитесь, что он успешно работает как под Windows 9x, так и под Windows NT, причем не требуя у вас наличия прав администратора! С одной стороны это, бесспорно хорошо, но с другой... наличие ASPI-драйвера создает огромную дыру в системе безопасности, позволяя зловредным программам вытворять с вашим оборудованием все, что угодно. Заразить MBR/boot-сектора? Пожалуйста! Уничтожить информацию со всего диска целиком - да проще этого ничего нет! Поэтому, если вы заботитесь о собственной безопасности - удалите ASPI32-драйвер со своего компьютера (для этого достаточно удалить файл ASPI.SYS из каталога WINNT\System32\Drivers). Разумеется, сказанное относиться только к NT, поскольку в операционных системах Windows 9x прямой доступ к оборудованию можно заполучить и без этого.
Как уже говорилось выше (см. "Доступ через SPTI"), независимо от физического интерфейса дискового накопителя (SCSI или IDE) мы можем взаимодействовать с ним через унифицированный SCSI-интерфейс. Другими словами, драйвер конкретного устройства (и привода CD-ROM, в частности) полностью абстрагирован от особенностей реализации шинного интерфейса данного устройства. Даже если завтра появятся накопители, работающие через инфракрасный порт, драйвер CDROM.SYS ничего об этом не "узнает" и будет по-прежнему управлять ими через SCSI-порт.
Даже если на вашем компьютере не установлено ни одного SCSI-контролера, пара-тройка вполне работоспособных SCSI-портов у вас обязательно есть. Конечно, это виртуальные, а не физические порты, но с точки зрения программного обеспечения они выглядят точь-в-точь как настоящие. Попробуйте с помощью функции CreateFile отрыть устройство "\\.\SCSI0:" и оно успешно откроется, подтверждая наличие существования виртуальных SCSI-портов (только не забудьте про двоеточие на конце). Посылая определенные IOCTL-команды SCSI-порту, мы можем управлять подключенным к этому порту физическим или виртуальным устройством. Да! Между SCSI-портом (виртуальным) и интерфейсной шиной (физической) расположен еще один уровень абстракции, занимаемый SCSI мини-портом, который, собственно, и "отвязывает" драйвер SCSI-порта от конкретного физического оборудования (подробнее см. "Доступ через SCSI мини-порт").
Естественно, прежде чем посылать IOCTL-команды в SCSI-порт, неплохо бы узнать, какое именно оборудование к этому порту подключено. Существует множество способов решения этой проблемы: от послать устройству команду идентификации IOCTL_SCSI_GET_INQUIRY_DATA (см. исходный текст демонстрационного примера в NT DDK "NTDDK\src\storage\class\spti") и тогда оно среди прочей информации сообщит нам - как его зовут (типа "PHILIPS CDRW2412A") до заглянуть в таблицу объектов, чем мы сейчас и займемся. В состав NT DDK входит утилита objdir.exe которая, как и следует из ее названия, позволяет отображать содержимое дерева объектов в виде директории. Устройства, доступные для открытия функции CreateFile хранятся в каталоге с довольно нелепым именем "\DosDevices\", глядя на которое можно подумать, что оно содержит имена устройств, видимых из-под MS-DOS, которою Windows NT вынуждена эмулировать для сохранения обратной совместимости. На самом же деле этот каталог активно используется win32-подсистемой Windows NT и всякий раз, когда функция CreateFile обращается к тому или иному логическому устройству (например, пытается открыть файл "C:\MYDIR\myfile.txt"), подсистема win32 обращается к каталогу "\DosDevices\", чтобы выяснить - с каким именно внутренним устройством это логическое устройство связано. Внутренние устройства видны лишь из-под Native-NT, а для всех ее подсистем они лишены всякого смысла. В частности, диск "С:" под Native-NT зовется как "\Device\HarddiskVolume1", а полный путь к файлу myfile.txt выглядит так: "\Device\HarddiskVolume1\MYDIR\myfile.txt". Только не пытайтесь "скормить" эту строчку функции CreateFile - она скорее поперхнется, чем поймет, что же от нее хотят.
Таким образом, каталог "\DosDevices\" служит своеобразным связующим звеном между подсистемой win32 и ядром системы Windows NT. Вот и давайте, в плане возращения к нашим баранам, посмотрим с каким native-устройством ассоциировано логическое устройство "SCSI". Запустив objdir с ключом "\Dos\Devices" и не забыв перенаправить весь вывод в файл ("objdir \DosDevices | MORE" - как альтернативный результат), мы среди моря прочей информации обнаружим следующие строки (при отсутствии DDK можно воспользоваться отладчиком Soft-Ice в котором для достижения аналогичного результата следует набрать команду "objdir \??" - именно так (два знака вопроса), поскольку директория \DosDevices на самом деле никакая не директория, а символическая ссылка на директорию \?? или, если так угодно, ее ярлык):
Листинг 13. Взаимосвязь логических SCSI-устройств с native-NT устройствами.
Оказывается, устройства SCSI0: и SCSI1: представляют собой ни что иное, как символические ссылки на IDE-порты с номерами 0- и 1- соответственно. Впрочем, устройства с именами IdePort0 и IdePort1 не являются IDE-портами в физическом смысле этого слова. Это виртуальные SCSI-порты, создаваемые драйвером ATAPI.SYS в процессе его инициализации. Он же создает символические связи "\DosDevices\SCSI0:" и "\DosDevices\SCSI1:" к ним, а также ярлыки "\Device\ScsiPort0" и "\Device\ScsiPort1", недоступные подсистеме win32, но предназначенные для внутреннего использования исключительно на уровне драйверов. Разумеется, ATAPI.SYS не только создает все вышеперечисленные устройства, но и обслуживает их, предоставляя драйверам более высоких уровней унифицированный интерфейс для взаимодействия с установленным оборудованием.
А вот устройство с именем "SCSI2:" ни с какими физическими шинами вообще не связано и к соответствующему ему SCSI-порту подключен виртуальный привод CD-ROM, создаваемый программой Alcohol 120%, а точнее ее драйвером - AXSAKI.SYS! Драйвера высокого уровня (в частности драйвер CDROM.SYS), не заподозрив никакого подвоха, будут работать с виртуальным диском точно также, как и с настоящим, что, собственно, и не удивительно, т.к. концепция SCSI-порта обеспечивает независимость драйверов верхнего уровня от особенностей оборудования, с которым они, с позволения сказать, "работают". Именно поэтому под Windows NT так легко реализуются эмуляторы физических устройств!
Управлять SCSI-устройствами можно и с прикладного уровня через STPI-интерфейс, однако вместо буквенного имени привода следует задавать имя SCSI-порта, к которому этот привод подключен. Основное достоинство такого способа управления заключается в том, что для взаимодействия с приводом совершенно необязательно обладать правами администратора! Привилегий простого смертного пользователя будет более чем достаточно. К тому же, прямая работа со SCSI-портом несколько производительнее взаимодействия с устройством через длинную цепочку драйверов верхнего уровня многочисленных фильтров, окружающих их.
Однако все попытки передачи SRB-блока через SCSI-порт заканчиваются неизменной ошибкой. Следующий код наотрез отказывается работать. Почему?
Листинг 15. Пример неправильной работы с виртуальным SCSI-портом.
Зарубежные телеконференции буквально кишат вопросами на этот счет - у одних этот код исправно работает, у других - нет (и их большинство). А ответ, между тем, находится в DDK (если, конечно, читать его сверху вниз, а не наискосок по диагонали). Вот, пожалуйста, цитата из раздела 9.2 SCSI Port I/O Control Codes: "If a class driver for the target type of device exists, the request must be sent to that class driver. Thus, an application can send this request directly to the system port driver for a target logical unit only if there is no class driver for the type of device connected to that LU [5]" ("Если класс-драйвер для целевого устройства установлен, управляющие запросы должны посылаться класс-драйверу, но не самому порту устройства. Таким образом, приложения могут посылать непосредственные запросы драйверу системного порта для целевых логических устройств, только если класс-драйвер для соответствующего типа устройств, подключенных к данному LU, не установлен"). В переводе на нетехнический язык: непосредственное управление портом с прикладного уровня возможно для тех и только тех устройств, чей класс-драйвер не установлен. Скажем, если вы подключили к компьютеру какую-то нестандартную железяку, то управлять ей напрямую через SCSI-порт вполне возможно (ведь класс-драйвера для нее нет!) Но приводы CD-ROM, про которые мы собственно и говорим, совсем иное дело! Класс-драйвер для них всегда установлен и потому операционная система всячески препятствует прямому взаимодействую с оборудованием через SCSI-порт, поскольку это единственный надежный путь избежать конфликтов.
Выходит, доступ к приводам через SCSI-порт невозможен? И так, и не так! Прямой доступ к SCSI-порту действительно блокируется системой, но та же самая система предоставляет возможность управления устройством через SCSI мини-порт. Мини-порт? Что это такое?! А вот об мы сейчас и расскажем!
Рисунок 6. Архитектура подсистемы ввода/вывода в Windows NT.
Драйвер SCSI мини-порта и есть тот самый драйвер, за счет которого системе удается абстрагироваться от особенностей физических интерфейсов конкретного оборудования. Условимся для краткости называть его просто "мини-драйвером", хотя это будет и не совсем верно, поскольку помимо SCSI мини-портов, существуют драйвера для видео и сетевых мини-портов. Однако поскольку ни те, ни другие к рассматриваемому нами контексту никоим боком не относятся, то и никаких разночтений не возникает.
Иерархически драйвер мини-порта располагается между физическими (виртуальными) устройствами, подключенными к тем или иным интерфейсным шинам компьютера (IDE/PCI/SCSI) и драйвером SCSI-порта. Драйвер мини-порта представляет собой системно-независимый драйвер, но в то же время зависимый от специфики конкретных HBA (Host Bus Adapter), то есть того самого физического/виртуального оборудования, которое он обслуживает. Драйвер мини-порта экспортирует ряд функций семейства ScsiPortXXX, предназначенных для использования драйверами верхних уровней и обычно реализуется как динамическая библиотека (то есть DLL), естественно исполняющаяся в нулевом кольце "ядерного" уровня.
Именно он транслирует SCSI-запросы в команды подключенного к нему устройства, именно он создает виртуальные SCSI-порты с именам типа "\Device\ScsiPortx", именно он обеспечивает поддержку накопителей с физическими интерфейсами, отличными от SCSI-интерфейса. ATAPI.SYS, обслуживающий CD-ROM приводы с ATAPI-интерфейсом, DISK.SYS, обслуживающий жесткие диски - все они реализованы как драйвера мини-порта.
Управление мини-портом осуществляется посредством специального IOCTL-кода, передаваемого функции DeviceIoControl и определенного в файле NTDDSCSI.H как IOCTL_SCSI_MINIPORT. Если же у вас нет NT DKK, то вот его непосредственное значение: 0x4D008. Естественно, прежде чем вызывать DeviceIoControl, соответствующий SCSI-порт должен быть заблаговременно открыт функцией CreateFile. Это может выглядеть, например, так:
Листинг 16. Открытие SCSI-порта для управления драйвером мини-порта. Причем, обратите внимание: имя порта должно выглядеть как "SCSIx:", но не как "ScsiPortx", причем, в его конце обязательно должен присутствовать символ двоеточия, иначе ничего не получится.
Здесь мы открываем первый, считая от нуля, SCSI-порт, который, как мы уже знаем, соответствует первому каналу IDE или, говоря другими словами, Secondary IDE-контроллеру (на компьютере автора привод CD-ROM висит именно на нем). Для определения расположения приводов на неизвестном нам компьютере можно воспользоваться IOCTL-кодом IOCTL_SCSI_GET_INQUIRY_DATA, который заставит драйвер мини-порта перечислить все имеющиеся в его наличии оборудования, после чего нам останется только определить его тип (подробнее см. "NTDDK\SRC\STORAGE\CLASS\SPTI").
Однако управление мини-портом осуществляется совсем не так, как SCSI-портом! На этом уровне никаких стандартных команд уже не существует и мы вынуждены работать с учетом специфики и особенностей реализации конкретного оборудования. Вместо SRB-запросов мини-драйверу передается структура SRB_IO_CONTROL, определенная следующим образом:
Листинг 17. Назначение полей структуры SRB_IO_CONTROL, обеспечивающей управление драйвером мини-порта.
Ну, с полем HeaderLength все более или менее ясно, но вот что это за сигнатура такая?! Дело в том, что коды управления драйверами мини-порта не стандартизованы и определяются непосредственно самим разработчиком данного драйвера, а потому коды команд одного драйвера навряд ли подойдут к другому. Вот во избежание междоусобных конфликтов каждый драйвер мини-порта и имеет уникальную сигнатуру, которую тщательно сверяет с сигнатурой переданной приложением в поле Signature структуры SRB_IO_CONTROL. И если эти сигнатуры не совпадают, драйвер отвечает: SRB_STATUS_INVALID_REQUEST (типа, отвали, моя черешня). К сожалению, интерфейс штатных мини-драйверов ATAPI.SYS и DISK.SYS абсолютно незадокументирован и если вы не умеете дизассемблировать, то вам остается лишь посочувствовать. Дизассемблер же сразу показывает, что сигнатуры обоих драйверов выглядят как "SCSIDISK", а сигнатура мини-драйвера от Alcohol 120% - "Alcoholx" (впрочем, последний в силу своей внештатности не представляет для нас особенного интереса).
С кодами команды разобраться сложнее. Правда, специалисты постоянно читающие MSDN и потому неплохо в нем ориентирующиеся, вероятно, смогут вспомнить, что: "...this specification describes the API for an application to issue SMART commands to an IDE drive under Microsoft Windows 95 and Windows NT. Under Windows 95, the API is implemented in a Vendor Specific Driver (VSD), Smartvsd.vxd. SMART functionality is implemented as a "pass through" mechanism whereby the application sets up the IDE registers in a structure and passes it to the driver through the DeviceIoControl API" ("...эта спецификация описывает API для приложений, передающих SMART-команды жестким дискам с IDE-интерфейсов под Microsoft Windows 95 и Windows NT. Под Windows 95 API реализовано в драйвере, специфичном для конкретного производителя (VSD - Vendor Specific Driver), называемом Smartvsd.vxd. SMART-функциональность реализована как "pass through"-механизм, посредством которого приложения устанавливают IDE-регистры, передавая их драйверу через специальную структуру, помещаемую во входной буфер функции DeviceIoControl ")
Ага! Один из драйверов позволяет нам манипулировать регистрами IDE-контроллера по своему усмотрению, то есть фактически предоставляет низкоуровневый доступ к диску! Очень хорошо! Интерфейс со SMART-драйвером достаточно хорошо документирован (см. "MSDN -> Specifications -> Platforms -> SMART IOCTL API Specification"), правда, раздражает гробовое молчание насчет Windows NT. То, что в NT никакх VxD нет - это и ежу ясно. Но, в то же время, заявляется, что SMART API в ней как будто бы реализован... Если напрячь свою голову и проявить чудеса интуиции, можно догадаться, что поддержка SMART в NT обеспечивается штатными средствами! Весь вопрос в том: какими именно средствами, и как? Ни SDK, ни DDK не содержат никакой информации на этот счет, но вот копание в заголовочных файлов из комплекта NT DDK может кое-что дать! Смотрите, что обнаруживается в файле scsi.h при тщательном его просмотре:
Листинг 18. Команды управления SMART в Windows NT, которые мы можем передавать драйверу мини-порта через поле ControlCode структуры SRB_IO_CONTROL.
Оторви Тигре хвост, если в Windows NT функциональность SMART реализуется не в драйвере мини-порта! И дизассемблирование ATAPI.SYS действительно подтверждает это! Вот вам и качество документации от Microsoft - уродство сплошное в стиле "маразм крепчает". Какой смысл включать в заголовочный файл IOCTL-команды, но не документировать их?! Причем, согласно лицензии, дизассемблирование любых компонентов операционной системы запрещено. Ладно, не будет скулить по поводу и без, а лучше еще раз перечитаем "SMART IOCTL API Specification", откуда поймем, что для управления драйвером мини-порта под Windows NT в поле ControlCode структуры SRB_IO_CONTROL мы должны передать код одной из приведенных выше команд. Пусть это будет, например, IOCTL_SCSI_MINIPORT_IDENTIFY.
Сразу же за концом структуры SRB_IO_CONTROL должна быть расположена структура SENDCMDINPARAMS, определенная следующим образом:
typedef struct _SENDCMDINPARAMS { DWORD cBufferSize; // размер буфера в байтах или нуль IDEREGS irDriveRegs; // структура, содержащая значение IDE-регистров BYTE bDriveNumber; // физический номер диска, считая от нуля BYTE bReserved[3]; // зарезервировано DWORD dwReserved[4]; // зарезервировано BYTE bBuffer[1]; // отсюда начинается входной буфер } SENDCMDINPARAMS, *PSENDCMDINPARAMS, *LPSENDCMDINPARAMS;
Листинг 19. Структура SENDCMDINPARAMS, дающая прямой доступ к IDE-регистрам.
То есть входной буфер функции DeviceIoControl должен выглядеть так:
Рисунок 7. Структура входного буфера функции DeviceIoControl для управления драйвером мини порта под Windows 9x/NT.
Первый элемент структуры - cBufferSize, содержащий размер bBuffer'a слишком очевиден и неинтересен. А вот структура IDREGS представляет собой настоящий клад, вот взгляните сами (только не упадите со стула, ибо потрясение будет столь же острым, сколько и глубоким):
Листинг 20. Структура IDEREGS, предоставляющая низкоуровневый доступ к IDE-регистрам.
Всякий, кто читал спецификацию на ATA/ATPI и хоть однажды сталкивался с программированием устройств с интерфейсом IDE, должен немедленно узнать до боли знакомые регистры Command, Drive/Head, Cylinder High, Cylinder Low, Sector Number, Sector Count и Features, правда, в структуре IDEREGS они перечислены почему-то в обратном порядке, но это уже мелочи реализации. Главное, что с помощью этой структуры мы можем вытворять с приводом все мыслимые и немыслимые фокусы, на которые только способно железо. Даже не вериться, что в подсистеме безопасности существует такая дыра размерами со слонопотама. И это при том, что для управления мини-портом наличие прав администратора совсем не обязательно! Дрожа и подпрыгивая от нетерпения, наскоро заполняем оставшиеся поля структуры SENDCMDINPARAMS, как-то: bDriveNumber - физический номер привода, считая от нуля и буфер для передачи данных [6] (но ведь мы пока не собираемся записывать никаких данных на диск, верно? вот и оставим это поле пустым).
Увы! При попытке "скормить" приводу команду, отличную от команд семейства SMART, нас постигает глубокое разочарование, ибо драйвер мини-порта далеко не дурак и проверяет содержимое структуры IDEREGS перед ее передачей IDE-приводу. Исключение составляет лишь команда идентификации драйва - 0xEC, о чем Microsoft прямо и заявляет "There are three IDE commands supported in this driver, ID (0xEC), ATAPI ID (0xA1), and SMART (0xB0). The "subcommands" of the SMART commands (features register values) are limited to the currently defined values (0xD0 through 0xD6, 0xD8 through 0xEF). SMART subcommand 0xD7, write threshold value, is not allowed. Any other command or SMART subcommand will result in an error being returned from the driver. Any SMART command that is not currently implemented on the target drive will result in an ABORT error from the IDE interface" ("Только три IDE-команды поддерживаются этим драйвером: ID (код 0xEC), ATAPI ID (0xA1) и SMART (0xB0). "Подкоманды" базовой команды SMART (передаваемые через feature-регистр) ограничены лишь теми значениями, которые специфицированы на настоящий момент: от 0xD0 до 0xD6 и от 0xD8 до 0xEF. Использование подкоманды с кодом 0xD7, записывающей пороговое значение SMART, заблокировано. Любые другие команды и подкоманды будут игнорироваться драйвером, и возвращать сообщение об ошибке. Любые SMART-команды, что не реализованы на текущий момент, в целевом приводе будет возвращать ABORT-ошибку").
Кажется, что это полный провал, но нет! Ведь эту проверку, в принципе, можно и отключить! Давайте дизассемблируем драйвер ATAPI.SYS и посмотрим, что мы можем сделать.
Листинг 21. Фрагмент дизассемблерного листинга драйвера ATAPI.SYS, отвечающий за проверку передаваемых IDE-команд на соответствие принадлежности к "белому" списку.
Таким образом, чтобы разрешить драйверу отправлять IDE-приводу любые команды, мы должны удалить условный переход, расположенный по адресу 0х12437 (в листинге он выделен красным цветом) на безусловный переход, передающий управление на команду записи по адресу 0x12491. Только не забудьте после модификации драйвера скорректировать его контрольную сумму, что можно сделать, например, с помощью утилиты EDITBIN.EXE, входящей в состав Microsoft Visual Studio, иначе Windows NT наотрез откажется загружать такой хакнутый драйвер.
Разумеется, такую операцию допустимо проделывать только на своем собственном драйвере, поскольку всем остальным навряд ли понравится дыра, проделанная в системе безопасности! К тому же, распространение модифицированного ATAPI.SYS вопиющим образом нарушает авторское право самой Microsoft со всеми вытекающими отсюда последствиями. Тем не менее, ваше приложение может безбоязненно "патчить" ATAPI.SYS непосредственно на компьютерах пользователей, естественно, запрашивая у них подтверждение на правомерность такой операции (или, на худой конец, просто упоминая этот аспект в сопроводительной документации).
В любом случае, данный способ взаимодействия с приводом не стоит сбрасывать со счетов, поскольку это значительно усложняет взлом защиты, созданной на его основе. Ведь далеко не все хакеры осведомлены о тонкостях управления мини-портом и потому с вероятностью близкой к единице сядут в глубокую лужу, если, конечно, не упадут в яму информационного вакуума.
Пример программы, приведенной ниже, как раз и демонстрирует передачу ATA-команд IDE-приводу через драйвер мини-порта.
Листинг 22. Пример программы, демонстрирующей технику взаимодействия со SCSI мини-портом.
Операционная система Windows NT тщательно оберегает порты ввода/вывода от посягательства со стороны прикладных приложений. Мера эта вынужденная и реализованная под давлением выбранной политики безопасности. Свобода прикладных приложений умышленно ограничивается так, чтобы предотвратить возможные "террористические акты", направленные на подрыв системы или несанкционированный захват конфиденциальной информации. Правом непосредственного доступа к оборудованию обладают лишь драйвера и динамические библиотеки, исполняющиеся в режиме ядра (см. "доступ через SCSI мини-порт").
Поневоле вспоминаются слова одного из отцов-основателей США, что нация, обменявшая свободу на безопасность не заслуживает ни того, ни другого. И правда! Как будто бы нельзя завесить систему через тот же SPTI/ASPI! Причем для этого даже не понадобится обладать правами администратора! Какая там политика безопасности, какое к черту разграничение доступа, когда ASPI дает доступ к диску на секторном уровне безо всяких проверок на предмет правомерности осуществления этой операции. Хоть сейчас boot-вируса в загрузочный сектор внедряй! И это при том, что отсутствие доступа к портам ввода/вывода существенно усложняет задачу управления оборудованием и уж тем более создания надежных и трудноломаемых защитных механизмов!
Операционные системы семейства Windows 9x ведут себя более демократично, однако их снисходительность распространяется исключительно на MS-DOS программы, а win32-приложения возможности прямого доступа к портам, увы, лишены.
Тем не менее, управлять оборудованием с прикладного уровня все-таки возможно. Существует по меньшей мере два пути решения этой проблемы: а) создание драйвера-посредника, реализующего более или менее прозрачный интерфейс для взаимодействия с портами через механизм IOCTL и б) модификация карты разрешения ввода-вывода (I/O Permission Map - IOPM) с таким расчетом, чтобы обращение к портам перешло в разряд непривилегированных операций, осуществимых и с прикладного уровня. Ниже оба этих способа будут подробно рассмотрены. Начнем с интерфейсного драйвера.
В состав NT DDK входит весьма любопытный учебный драйвер PORTIO, создающий виртуальное устройство и реализующий специальный IOCTL-интерфейс, посредством которого прикладные приложения могут манипулировать с портами этого устройства произвольным образом (его исходный текст с минимумом необходимых комментариев расположен в каталоге: "\NTDDK\src\general\portio"). Конечно, виртуальное устройство - это не совсем то, что нам нужно, поскольку диапазон принадлежащих ему портов ввода/вывода не может пересекаться с портами, принадлежащими другими устройствам, в противном случае система грязно выругается и поставит в "диспетчере устройств" восклицательный знак, предупреждая пользователя о имеющемся конфликте ресурсов. И хотя на работоспособность системы такой конфликт никак не повлияет, созерцание восклицательных знаков уж точно не пойдет на пользу пользователям нашей программы.
На самом деле, драйверу, работающему в режиме ядра, никто не запрещает обращаться к любым портам, каким ему только вздумается. Достаточно исключить из тела genport.c следующие строки и мы сможем с его помощью читать весь диапазон портов ввода/вывода
Листинг 23. Проверка адресов портов к которым происходит обращение на принадлежность к диапазону портов виртуального устройства, созданного драйверов. Для того, чтобы иметь возможность обращаться к любым портам, эти строки следует удалить.
Также следует обратить внимание на то, что драйвер ожидает получить не абсолютный адрес порта, а относительный, отсчитываемый от адреса базового порта, задаваемого при добавлении виртуального устройства в систему. Взгляните на следующие строки:
Листинг 24. Вычисление действительного адреса порта через базовый.
Очевидно, что текст, выделенный жирным шрифтом, следует удалить - в этом случае драйвер сможет оперировать абсолютными, а не относительными портами и мы без труда сможем прорваться к любому порту системы! Причем, если мы перенесем модифицированный нами драйвер на Windows 9x, наши приложения будут работать в обоих операционных системах и останутся зависимыми разве что от самого оборудования. Но, с другой стороны, всякий, кто стремится дорваться до портов, должен отдавать себе отчет в том - зачем это ему нужно и какие сложности ему придется преодолеть?
Конечно, поскольку возможность бесконтрольного доступа ко всем имеющимся портам ввода/вывода существенно ослабляет и без того уязвимую операционную систему, нелишним будет ввести в драйвер кое-какие дополнительные проверки и ограничения. Скажем, запретить прямое обращение ко всему, что не является CD-ROM приводом. В противном случае, если ваша программа получить сколь-нибудь широкое распространение, толпы вандалов ринуться писать зловредных троянских коней, военная мощь которых окажется практически безграничной и совладеть с ними будет очень трудно. С другой стороны, за все время существования интерфейса ASPI не было зафиксировано ни одной попытки использовать его для деструктивных целей, хотя такая возможность до сих пор имеется.
Другой недостаток предложенного способа управления устройствами заключается в его катастрофически низком быстродействии. Вызовы DeviceIoControl распадаются на десятки тысяч машинных команд (!), "благодаря" чему время обработки запросов становится слишком большим, а измерение физических характеристик спиральной дорожки (если мы действительно захотим эти характеристики измерять) - неточным. К тому же, функция DeviceIoControl громоздка и неизящна, а самое неприятное в том, что на нее очень легко поставить BreakPoint и потому участь такой защиты заранее предрешена. Во времена MS-DOS, когда взаимодействие с оборудованием осуществлялось посредством машинных команд IN и OUT, локализовать защитный код в теле программы было значительно сложнее, а управлять устройствами с их помощью существенно легче и, главное, намного производительнее.
Считается, что в среде Windows NT прямое обращение к портам возможно только на уровне ядра, а приложения вынуждены общаться с портами через высокоуровневый интерфейс, предоставляемый драйвером. И хотя этот интерфейс может быть полностью прозрачным (драйверу ничего не стоит перехватить исключение, возникающие при попытке чтения/записи в порт с прикладного уровня и выполнить этот запрос самостоятельно), это все-таки не то...
На самом деле, выполнять команды IN/OUT можно и на прикладном уровне, правда не без помощи недокументированных возможностей операционной системы и документированных, но малоизвестных особенностей реализации защищенного режима работы в процессорах Intel 80386+. Вот с процессоров мы, пожалуй и начнем. Давайте откроем "Instruction Set Reference" и посмотрим как "устроена" машинная команда OUT. Среди прочей полезной информации мы найдем и ее псевдокод, которой выглядит приблизительно так:
Листинг 25. Псевдокод инструкции OUT.
Обратите внимание! Обнаружив, что полномочий текущего уровня привилегий категорически недостаточно для выполнения данной машинной инструкции, процессор не спешит выбросить исключение general protection fault, а дает ей еще один шанс, осуществляя дополнительную проверку на предмет состояния карты разрешения ввода/вывода (I/O permission bitmap) и, если бит памяти, соответствующий данному порту не равен единице, то вывод в порт осуществляется несмотря ни на какие запреты со стороны CPL!
Таким образом, для взаимодействия с потами с прикладного уровня нам достаточно всего лишь скорректировать карту разрешения ввода/вывода, после чего подсистема защиты операционной системы Windows NT перестанет нам мешать, поскольку контроль доступа к портам осуществляется не на программном, а на аппаратном уровне и, если процессор перестанет выбрасывать исключения, операционная система ничего не узнает о происходящем!
Проблема в том, что подавляющее большинство авторов книг по ассемблеру о карте разрешения ввода/вывода даже не упоминают и лишь немногие программисты знают о ее существовании - те, кто предпочитает оригинальную документацию корявым переводом и пересказам.
Обратившись к "Architecture Software Developer's Manual Volume 1: Basic Architecture" мы узнаем, что карта ввода/вывода находится в сегменте состояния задачи (TSS - Task State Segment) - точнее ее действительное смещение относительно начала TSS определяется 32-битным полем, расположенном в 0x66 и 0x67 байтах сегмента состояния задачи. Нулевой бит этой карты отвечает за нулевой порт, первый - за первый, второй - за второй и т.д. вплоть до старшего бита 0x2000-байта, отвечающего за 65535-й порт. Битовую карту завершает так называемый байт-терминатор, имеющий значение 0xFF. Вот, собственно, и все. Порты, чьи биты сброшены в нулевое значение, доступны с прикладного уровня безо всяких ограничений. Разумеется, сама карта ввода/вывода доступа лишь драйверам, но не приложениям, поэтому без написания собственного драйвера нам все равно не обойтись. Однако этот драйвер будет работать только на стадии своей инициализации, а весь дальнейший ввод/вывод пойдет напрямую, даже если выгрузить драйвер из памяти.
Теперь плохая новость. В Windows NT смещение карты ввода/вывода по умолчанию находится за пределами сегмента состояния задачи и потому модифицировать карту ввода/вывода не так-то просто, поскольку ее вообще нет! Процессор, кстати говоря, на такую ситуацию реагирует вполне спокойно, но доступ к портам ввода/вывода с прикладного уровня все-таки запрещает.
На самом деле, карта ввода/вывода в TSS все-таки есть, но она умышленно заблокирована системой, чтобы не дать прикладным приложениям своевольничать. Исключение составляют лишь высокопроизводительные графические библиотеки, напрямую обращающиеся к портам ввода/вывода с прикладного режима. Как нетрудно догадаться, такой трюк дает Microsoft значительную фору перед конкурентами, вынужденными управлять портами либо с уровня ядра, либо через интерфейс, предоставляемый видео драйвером. Естественно, оба этих способа значительно проигрывают в производительности прямому доступу к портам.
Однако попытка подкорректировать указатель на карту ввода/вывода ни к чему не приводит, поскольку коварная NT хранит копию этого значения в контексте процесса, а потому при переключении контекста указатель на прежнюю карту автоматически восстанавливается. С одной стороны это хорошо, поскольку каждый процесс может иметь свою собственную карту ввода/вывода, а с другой... штатная документация от Microsoft не содержит и намека на то, как с этой картой работать.
Правда, можно схитрить и увеличить размер сегмента состояния задачи так, чтобы адрес карты ввода/вывода, прежде указывающий за его конец, теперь приходился на действительную и подвластную нам область памяти. Правда, поскольку в хвосте последней страницы, занятой TSS имеется всего лишь 0xF55 байт, максимальный размер карты, которую мы только можем создать в этом промежутке, охватывает всего лишь 31.392 портов ввода/вывода. Хотя, если говорить честно, остальные порты нам все равно вряд ли понадобиться, так что ничего трагичного в таком ограничении и нет.
Впрочем, существуют и более изящные способы решения этой проблемы. Усилиями Дейла Робертса были обнаружены три полностью недокументированные функции: были Ke386SetIoAccessMap(), Ke386QueryIoAccessMap() и Ke386IoSetAccessProcess() которые, как и следует из их названий, обеспечивают вполне легальный способ управления картой ввода/вывода. "Полностью недокументированные" в том смысле, что даже заголовочные файлы из DDK не содержат их прототипов (а, как известно, в заголовочных файлах DDK перечислено множество недокументированных функций). Тем не менее, библиотека NTOSKRNL их все-таки экспортирует и они легко доступы с уровня драйверов.
Подробнее обо всем этом можно прочитать в статье их первооткрывателя - Дейла Робертса, перевод которой можно найти, в частности, по следующему адресу: http://void.ru/?do=printable&id=701. Здесь же мы рассмотрим их лишь кратко. Итак, функция Ke386SetIoAccessMap принимает два аргумента: двойное слово, которое буду установленным в единицу, заставляет функцию копировать карту ввода/вывода, указатель на которую передан ей со вторым аргументом. Функция Ke386QueryIoAccessMap принимает те же самые аргументы, но осуществляет прямо противоположную операцию, извлекая текущую карту ввода/вывода из сегмента состояния задачи и копируя ее в указанный буфер. Наконец, функция Ke386IoSetAccessProcess принимает со своим вторым аргументом указатель на структуру процесса, полученный вызовом документированной функции PsGetCurrentProcess(). Первый аргумент играет ту же самую роль, что и в предыдущих функциях: нулевое значение переводит указатель на карту ввода/вывода за границы TSS, тем самым запрещая доступ к портам с прикладного уровня, а единичное - активизирует ранее переданную карту ввода/вывода.
Пример, приведенный ниже, все это, собственно, и демонстрирует:
Листинг 26. Демонстрационный пример драйвера, открывающему прямой доступ к портам ввода/вывода на прикладном уровне.
Листинг 27. Пример ввода/вывода в порт с прикладного уровня.
Теперь поговорим о том, как данный способ взаимодействия с портами ввода/вывода может быть использован на благо защитных механизмов. Допустим, наша защита привязывается к физическому дефекту поверхности лазерного диска. Тогда все, что нам надо - попытаться как можно незаметнее прочитать этот сектор и, если он действительно не читается, диск можно считать оригинальным и - наоборот. Прямое управление приводом через порты ввода/вывода с вероятностью близкой к единице останется незамеченным даже бывалыми хакерами, потому такой вариант им попросту не придет в голову! Единственное, о чем следует позаботиться, это не дать обнаружить защитный код по перекрестным ссылкам, оставленных тем ругательным сообщением, которое выводится на экран в том случае если диск признан пиратским.
Тем не менее, матерых хакеров на такую наживку не возьмешь! Злорадно ухмыльнувшись они просто поставят точку останова на ввод/вывод в порты 0x1F7/0x177 (для Primary и Secondary приводов соответственно). А, чтобы не утонуть в море обращений к приводу через функции API, они задействуют условные точки останова, приказывая отладчику всплывать только в том случае, если адрес машинной команды, осуществляющей ввод/вывод находится ниже адреса 0x70000000, т.е., другими словами, принадлежит пользовательскому приложению, а не ядру.
Но что нам мешает с прикладного уровня выполнить команду ввода/вывода по адресу, принадлежащему ядру? Достаточно просто просканировать верхнюю половину адресного пространство на предмет наличия команд OUT DX, AL (опкод 0xEE) и IN AL, DX (опкод 0xEC). Спрашиваете: а как мы сможем вернуть управление? Да очень просто - с помощью обработки структурных исключений. Если машинная команда, следующая за IN/OUT возбуждает исключение (а таких команд - пруд пруди), то, перехватив его, мы сможем продолжить выполнение программы, как ни в чем не бывало.
Достоинство этого приема в том, что точка останова, поставленная хакером на порты ввода/вывода не сработает (точнее, сработает, но будет тут же проглочена фильтром), а недостаток: неоправданное усложнение защитного механизма.
Знаменитый MSCDEX, созданный еще во времена царствования MS-DOS, несмотря на свои многочисленных недостатки, все-таки обеспечивал программистов всем необходимым им функционалом и достаточно полно поддерживал возможности существующих в то время приводов. Так, например, чтение отдельных секторов осуществлялось функцией 1508h прерывания INT 2Fh, а если возникала необходимость спуститься на "сырой" уровень, мы всегда могли попросить MSCDEX передать приводу ATAPI-пакет напрямую, чем занималась функция 1510h того же прерывания (загляните в Interrupt List, если нуждаетесь в более подробной информации).
Забавно, но возможности штатного драйвера "новейшей" и "могучей" Windows 9x, не в пример беднее и спуститься на секторный уровень, при этом не разорвав себе задницу, под ее управлением, по-видимому, нельзя. Судя по всему, архитекторы системы сочли секторный обмен ненужным и к тому же системно-зависимым, а "правильные" приложения должны разрабатываться как полностью переносимые и довольствующиеся исключительно стандартными вызовами интерфейса Win32 API. Все остальное - от лукавого!
Между тем, для сохранения обратной совместимости с программами, написанными для MS-DOS и Windows 3.1, операционная система Windows 95 поддерживает MSCDEX-интерфейс, причем, по соображениям производительности, реализует его не в "настоящем" MSCDEX, который и вовсе может отсутствовать на диске, а в CD-ROM драйвере, исполняющемся в 32-разрядном защищенном режиме. Выходит, что весь необходимый нам функционал в системе все-таки есть, а, значит, есть и надежда как-то до него добраться. Естественно, с уровня ядра эта задача решается без проблем, но... писать свой собственный драйвер только для того, чтобы пробить интерфейсную шахту к уже существующему драйверу - это маразм как-то!
К счастью, готовый (и даже задокументированный!) интерфейс между win32-приложениями и MSCDEX-драйвером в системе Windows 9x действительно есть. К несчастью, он реализован через жопу и... именно через жопу и не надо пытаться вычеркнуть эту фразу, иначе я шибко разозлюсь (последняя фраза предназначается в первую очередь для редакторов). В общих чертах схема прокладывания туннеля к MSCDEX'у выглядит приблизительно так: создав 16-разрядную DLL, мы получаем возможность взаимодействовать с DPMI [7] через функции прерывания INT 31h. Конкретно нас будет интересовать функция 1508h - DPMI Simulate Real Mode Interrupt, позволяющая вызывать прерывания реального режима из защищенного. Обращаясь к эмулятору MSCDEX-драйвера через родное для него прерывание INT 2Fh, мы можем делать с приводом практически все, что нам только вздумается, поскольку интерфейс MSCDEX'а, как уже отмечалось, могуч и велик.
Таким образом, вырисовывается следующий программистский маршрут: win32 приложение -> 16-разрядная DLL -> DMPI Simulate RM Interrupt -> MSCDEX -> CDFS. Не слишком ли наворочено, а? Уж лучше воспользоваться ASPI (благо, в Windows 95 оно есть) или засесть за написание своего собственного драйвера. Тем не менее, даже если вы не собираетесь управлять приводом через MSCDEX, знать о существовании такого способа взаимодействия с оборудованием все-таки небесполезно, особенно, если вы планируете занимаетесь взломом чужих программ. В этом случае, точки останова, установленные на API функции ничего не дадут, поскольку чтение секторов осуществляется через прерывания INT 31h (DMPI) и INT 2Fh. К сожалению, прямая установка точек останова на последние дает очень много ложных срабатываний, а применение фильтров навряд ли окажется эффективным, поскольку количество возможных вариаций слишком велико. Уж лучше поискать вызовы прерываний в дизассемблерном тексте программы!
Волнительную информацию по этому вопросу можно найти в технической заметке Q137813, входящей в состав MSDN, распространяемым вместе с Microsoft Visual Studio и озаглавленную как "How Win32 Applications Can Read CD-ROM Sectors in Windows 95". Полный перечень DMPI- и MSCDEX-функций содержится в Interrupt-List'е Ральфа Брауна, так что никаких проблем с использованием данного приема у Вас возникнуть не должно (правда, раздобыть компилятор, способный генерировать 16-разрядный код и линкер под Windows 3.1 сегодня не так-то просто! К слову сказать, Microsoft Visual Studio 6.0 для этой цели уже не подходит, ибо начиная с некоторой версии - уже сейчас и не вспомню какой - он утратил возможность создания проектов под MS-DOS/Windows 3.1).
Ниже приводится ключевой фрагмент, позаимствованный из MSDN, и демонстрирующий технику вызова прерываний реального режима из 16-разрядных DLL, исполняющихся в среде Windows.
Листинг 28. Ключевой фрагмент программы, демонстрирующей технику взаимодействия с драйвером MSCDEX из 16-разрядного защищенного режима.
Несмотря на то, что Windows позволяет управлять устройствами и с прикладного уровня, достаточно многие разработчики предпочитают осуществлять такое управление через свой собственный драйвер, который может взаимодействовать с приводом как напрямую, так и через его драйвер. Последний способ более предпочтителен, поскольку он позволяет абстрагироваться от конкретного оборудования и обеспечивает единый унифицированный интерфейс для всех приводов. Большинство таких драйверов "подключаются" к ATAPI и/или SCSI-порту и взаимодействуют с диском приблизительно также, как и ASPI-драйвер, уже рассмотренный нами.
Взаимодействие с прикладными приложениями обычно осуществляется посредством специальных кодов IOCTL, передаваемых драйверу функцией DeviceIoControl. "Специальных", потому что, разработка протокола взаимодействия драйвера с устройством целиком лежит на совести (и фантазии) создателя этого самого драйвера и никакой стандартизацией здесь даже отдаленно не пахнет! К тому же, DeviceIoControl - это не единственно возможный вариант. Драйверу, исполняющемуся в нулевом кольце, формально доступны все ресурсы операционной системы и при желании можно осуществить самые крутые извращения. Например, взаимодействовать с приложением через общую область памяти. Тогда точки останова, установленные на DeviceIoControl не найдут никакого результата! Однако подавляющее большинство драйверов работают через IOCTL и не блещут оригинальностью. В каком-то смысле такая позиция вполне оправдана. Действительно, с ростом извращенности драйвера увеличивается и его конфликтность, а совместимость с другими программами (и операционными системами) резко падает. К тому же, навороченный драйвер значительно труднее довести до ума, чем простой. С другой стороны, неизвращенный драйвер очень легко взломать и его разработка ничем не оправдает себя. Уж лучше воспользоваться тем же ASPI, который обеспечивает полнофункциональный низкоуровневый и при этом системно-независимый интерфейс. Тогда вам не придется создавать реализации своего драйвера под все существующие операционные системы и лихорадочно переписывать код при выходе новых версий Windows.
В сводной таблице, приведенной ниже, приведены основные характеристики всех вышеописанных методик доступа. Как видно, наибольшее количество очков набрал метод доступа через ASPI, обеспечивающий простой, симпатичный и к тому же системно-независимый интерфейс управления накопителями. Следом на ним идет STPI, основой недостаток которого заключается в том, что он поддерживается лишь операционными системами семейства NT и не работает на "народной" Windows 9x. Неплохой идеей выглядит создание собственного драйвера - будучи реализованным под Windows NT и Windows 9x (кстати, WDM-драйвера на уровне исходного кода совместимы с этими двумя системами), обеспечит возможность работы ваших приложений как в NT, так и в 9x.
CDFS |
cocked |
MSCDEX |
ASPI |
SPTI |
SCSI port |
mini-port |
own driver |
IOPM |
|
Windows 9x |
- |
- |
+ |
+ |
- |
- |
- |
+ |
н/д |
Windows NT |
+ |
+ |
- |
+ |
+ |
+ |
+ |
+ |
+ |
Требует прав админа |
нет |
нет |
- |
нет |
да |
нет |
нет |
хз [8] |
* [9] |
Поддерживает CDDA |
да |
нет |
да |
да |
да |
да |
да |
да |
да |
Поддерживает CD data |
да |
да |
да |
да |
да |
да |
да |
да |
да |
Сырое чтение с CDDA |
да |
нет |
да |
да |
да |
да |
да |
да |
да |
Сырое чтение с Cddata |
нет |
нет |
да |
да |
да |
да |
да |
да |
да |
Потенциально опасен |
нет |
нет |
нет |
да |
нет |
нет |
нет |
да |
да |
Хорошо документировано? |
да |
да |
да |
да |
нет |
нет |
нет |
да |
нет |
Легко использовать? |
да |
да |
нет |
да |
да |
да |
нет |
нет |
нет |
Таблица 4. Различные методы доступа в сравнении.
Защита, нуждающаяся в низкоуровневом доступе с CD, обязательно выдаст себя наличием функций DeviceIoControl и/или SendASPI32Command в таблице импорта. Если же защитный механизм загружает эти функции динамически, поймать его за хвост можно установкой точек останова на LoadLibrary/GetProcAddress (однако опытные программисты могут отважится на самостоятельный поиск требуемых им функций в памяти и это отнюдь не такая трудная задача, какой она кажется!)
Также в теле программы могут присутствовать строки: "\\.\", "SCSI", "CdRom", "wnaspi32.dll" и другие. Установив точку останова на первый байт строки мы сможем мгновенно локализовать защитный код при первом его к ним обращении. Чтобы этого не произошло, разработчики часто шифруют все текстовые строки, однако большинство из них ограничивается примитивной статической шифровкой (которая обычно осуществляется ASPack'ом или подобными ему программами), а потому, если дождаться завершения расшифровки и вызвать отладчик после, а не до запуска программы, все текстовые строки предстанут перед нами в прямом виде! Динамическая шифровка намного надежней. В этом случае текстовые строки расшифровываются непосредственно перед их передачей в соответствующую API-функцию, а потом зашифровываются вновь. Но и динамическую шифровку при желании можно преодолеть! Достаточно поставить условную точку останова на функцию CreateFile, которой эти текстовые строки и передаются, всплывая в том, и только в том случае, если первые четыре байта имени файла равны "\\.\". Пример ее вызова может выглядеть, например, так: "bpx CreateFileA if (*esp->4=='\\\\.\\')", после чего останется только пожинать урожай.
Естественно, под "урожаем" понимается: во-первых, имя самого открываемого файла, а точнее драйвера (это уже многое что дает), и во-вторых, возращенный функцией CreateFile дескриптор. Далее можно поступить двояко: либо установить точку останова на ту ячейку памяти в которой этот дескриптор сохраняется, либо установить условную точку останова на функцию DeviceIoControl, отлавливая только те ее вызовы, которые нам необходимы. Пример сеанса работы с отладчиком приведен ниже:
Листинг 29. Пример изобличения и разоблачения защитного механизма в Soft-ice.
Как видно, поиск DeviceIoControl не занял много времени. Остается проанализировать передаваемый ей код IOCTL (в нашем случае IOCTL_SCSI_PASS_THROUGHT_DIRECT) и его параметры, передаваемые через стек одним двойным словом выше.
Некоторые разработчики помещают критическую часть защитного кода в драйвер, надеясь, что хакеры там ее не найдут. Наивные! Драйвера в силу своего небольшого размера очень просто анализируются и спрятать защитный код там попросту негде. А вот если "размазать" защиту по нескольким мегабайтам прикладного кода, то на ее анализ уйдет чертова уйма времени и если у хакера нет никаких особых стимулов для взлома (как то - спортивный интерес, повышение собственного профессионализма), то он скорее приобретет легальную версию, чем в течении нескольких недель будет метаться от дизассемблера к отладчику.
Какие же фокусы используют разработчики, чтобы затруднить анализ драйверов? Ну, вот например, шифруют текстовую строку с символьным именем устройства, которое создает драйвер при своей загрузке. В результате, хакер точно знает, что защитный код открывает устройство "\\.\MyGoodDriver", но не может быстро установить какому именно драйверу это имя соответствует. Если же шифровка отсутствует, то задача решается простым контекстным поиском. Вот, например, захотелось нам узнать - какой именно драйвер создает устройство с именем MbMmDp32 - заходим Far'ом в папку WINNT\System32\Drivers, нажимаем <ALT-F7> и в строку поиска вводим "MbMmDp32", не забыв установить флажок "Use all installed character tables" (в противном случае Far ничего не найдет, т.к. строка должна задаваться в уникоде). Прошуршав некоторое время диском, Far выдаст единственно правильный ответ: ASPI32.SYS. Это и есть тот самый драйвер, который нам нужен! А теперь представьте, что строка с именем зашифрована... Если драйвер загружается динамически то это еще полбеды: просто ставим точку останова на IoCreareDevice и ждем всплытия отладчика. Затем даем P RET и по карте загруженных моделей (выдаваемых командой mod) смотрим - кто "проживает" в данном регионе памяти. С драйверами, загружающимися вместе с самой операционной системой справиться значительно сложнее и, как правило, отыскивать нужный драйвер приходится методом "тыка". Часто в этом помогает дата создания файла, - драйвер, устанавливаемый защищенным приложением, должен иметь туже самую дату создания, что и остальные его файлы. Однако защитный механизм может свободно манипулировать датой создания по своему усмотрению, так что это не очень-то надежный примем. Хороший результат дает сравнение содержимого директории WINNT\System32\Drivers до и после инсталляции защищенного приложения, - очевидно, защита может скрываться только среди вновь появившихся драйверов.
В качестве закрепления всего вышесказанного и обретения минимальных практических навыков, давайте исследуем несколько популярных программ, работающих с лазерными дисками на низком уровне, на предмет выяснения как именно осуществляется такое взаимодействие.
Вызывав незаменимый Soft-ice и установив точку останова на "bpx CreateFileA if (*esp->4=='\\\\.\\')" мы будем последовательно запускать три следующих программы: Alcohol 120%, Easy CD Creator и Clone CD, каждый раз отмечая имя отрываемого устройства. Итак...
Alcohol 120% в зависимости от настроек может обращаться к диску тремя путями: через собственный драйвер (по умолчанию), через ASPI/SPTI интерфейс и через ASPI Layer. Начнем с "собственного драйвера". Установка точки останова на CreateFileA показывает, что Алкоголь открывает устройство "\\.\SCSI2:" (естественно, на других компьютерах номер может быть и другим), и дальнейшая проверка подтверждает, что функция DeviceIoControl получает тот же самый дескриптор, что возвратился при открытии устройства SCSI! Следовательно, под "собственным" драйвером Алкоголик понимает тот самый драйвер мини-порта, которой он и установил в систему при своей установке. Теперь изменим настройки Алкоголика там, чтобы он работал через SPTI/ASPI интерфейс. После перезапуска программы (а при смене метода доступа Алкоголь требует обязательного перезапуска), мы снова словим открытие устройства "\\.\SCSI2", а затем произойдет открытие диска "\\.\G:" (естественно, на других компьютерах буква может быть и иной). Собственно, при взаимодействии с устройством через SPTI интерфейс, именно так все и происходит. Точнее, должно происходить. Алкоголь открывает диск "\\.\G:" многократно, что указывает на корявость его архитектуры. Это существенно усложняет нашу задачу, поскольку мы вынуждены следить за всеми дескрипторами одновременно и если упустить хотя бы один из них, реконструированный алгоритм работы программы окажется неверным (разве не интересно узнать, как именно Алкоголь осуществляет копирование защищенных дисков?). Наконец, переключив Алкоголь на последний оставшийся способ взаимодействия с диском, мы получим следующий результат: "\\.\\SCSI2", "\\.\MbMmDp32", "\\.\G:". Устройство с именем "MbMmDp32" и есть уже знакомый нам ASPI-драйвер. Правда, не совсем понятно, зачем Алкоголь явно открывает диск "\\.\G:", ведь ASPI-интерфейс этого не требует.
Easy CD Creator обращается к приводу непосредственно по его "родному" имени (в моем случае это "CDR4_2K"), а затем открывает устройство "MbDlDp32", которое сам CDR4_2K, собственно, и регистрирует. Следовательно, Easy CD Creator работает с диском через свой собственный драйвер и, чтобы разобраться с ним, нам потребуется: а) дизассемблировать драйвер CDR4_2K и проанализировать каким IOCTL-кодам какие действия драйвера соответствуют; б) отследить все вызовы DeviceIoControl (просто поставьте на нее условную точку останова, всплывающую при передаче "своего" дескриптора, возращенного функцией CreateFileA("\\\\.\\CRDR_2K",...) и CreateFileA("\\\\.\\MbDlDp32",...). Оформим последовательность IOCTL-вызовов в виде импровизированной программы, мы сможем воссоздать протокол взаимодействия с диском и найти защиту (если она там есть).
Clone CD. Точка останова, установленная на функцию CreateFileA показывает, что Clone CD общается с диском через свой собственный драйвер - \\.\ELBYCDIO, причем по не совсем понятным причинам его открытие происходит в цикле, так что дескриптор драйвера возвращается многократно.
Один забавный прием напоследок. Если приложение, взаимодействующие с CD, выполняет операцию которая не должна быть ни при каких обстоятельствах прервана, можно воспользоваться ICTL-командой блокировки лотка - IOCTL_CDROM_MEDIA_REMOVAL (а вот ее непосредственное значение: 0x24804). При попытке сделать диску "eject" при заблокированном лотке мой PHILIPS CDW начинает злобно моргать красным огоньком показывания, что диск "IN", но он "is locked". Вплоть до момента разблокирования лотка извлечь диск можно разве булавкой или перезагрузкой операционной системы.
Уже одно это создает богатое поле для всевозможных пакостей со стороны многочисленных злоумышленников, да и просто некорректно работающих программ, успевающих умереть от критической ошибке прежде чем разблокировать лоток. Как с этим бороться? Да очень просто - разблокировать лоток самостоятельно!
Дело в том, что система не требует, чтобы разблокирование выполнялось в контексте того процесса который выполнил блокирование. Она просто ведет счет блокировок и если он равен нулю, лоток свободен. Соответственно, если счет блокировок равен например шести - мы должны шесть раз вызывать команду разблокирования, прежде чем лазерный диск удастся извлечь на свет божий.
Утилита, исходный текст которой приведен ниже, позволяет манипулировать счетчиком блокировок диска по вашему собственному усмотрению. Аргумент командой строки "+" увеличивает значение счетчика на единицу, а "-" - уменьшает. При достижении счетчиком нуля дальнейшие попытки его уменьшения не возымеют никакого действия.
Как это можно использовать? Ну например для преждевременного извлечения диска из записывающей программы, что полезно для экспериментов. Другое применение: отлучаясь от своего компьютера на несколько минут, вы можете заблокировать диск, чтобы быть уверенными, что окружающие коллеги его не упрут. А если все-таки упрут (перезагрузив компьютер) заблокируйте лотки их CD-ROM'ов - пусть теперь перезагружаются!
Листинг 30. Утилита для блокирования/разблокирования лотка в CD-ROM'е.
Появление высокоскоростных приводов CD-ROM породило огромное количество проблем и по общему мнению пользователей плюсов здесь гораздо меньше, чем минусов. Этого реактивный гул, вибрация, разорванные в клочья диски - скажите, на кой черт все это вам нужно? К тому же, многие из алгоритмов привязки к CD на высоких скоростях чувствуют себя крайне неустойчиво и защищенный диск запускается далеко не с первого раза, если вообще запускается. Какой же из всего этого выход? Естественно - тормозить! Благо, команду SET CD SEED (опкод 0BBh) большинство приводов все-таки поддерживает. Казалось бы, задал нужные параметры и вперед! Ан нет, - тут все не так просто...
Неприятность первая (маленькая, но зато досадная). Скорость задается не в "исках", а в килобайтах секунду (именно в килобайтах, а не байтах!). Причем однократной скорости передачи соответствует пропускная способность в 176 килобайт в секунду. А двукратной? Думаете, 176 x 2 == 352? А вот и нет - 353! Зато трехкратная скорость вычисляется в полном соответствии с привычной нам математикой: 176 x 3 == 528, но уже четырех кратная скорость опять отклоняется от "иксов": 176 x 4 == 704, против 706 по стандарту. Неправильно заданная скорость приводит к установке скорости на ступень меньшей ожидаемой, причем соответствие между исками и ступенями далеко не однозначное. Допустим, привод поддерживает следующий ряд скоростей: 16x, 24x, 32x и 40х. Если заданная скорость (в килобайтах в секунду) не дотягивает до нормативной скорости 32 "иска", то привод переходит на ближайшую "снизу" поддерживаемую им скорость, т.е. в нашем случае 16х. Отсюда мораль - для перевода "иксов" в килобайты в секунду их нужно умножать не на 176, а на 177!
Неприятность вторая (крупнее и досаднее). Команды, выдающей полный список поддерживаемых скоростей в стандартной спецификации нет и добывать эту информацию приходится исключительно методом перебора. Корректно работающая программа перед началом такого перебора должна убедиться в отсутствии носителя в приводе, а если он там есть - принудительно открыть лоток. Дело в том, что раскручивание некачественного CD-ROM диска до высоких скоростей может привести к его разрыву и вытекающей отсюда порче самого привода. Пользователь должен быть абсолютно уверен в том, что установленный в привод диск будет вращаться именно с той скоростью, с которой его просят и ваша программа не станет самопроизвольно увеличивать скорость без видимых на то причин.
Неприятность третья (или тихий ужас). Некоторые приводы (в частности TEAK 522E) успешно заглатывают команду SET CD SPEED и подтверждают факт изменения скорости, возвращая в MODE SENSE ее новое значение, однако физически скорость диска остается неизменной вплоть до тех пор, пока к нему не произойдет того или иного обращения. Поэтому, вслед за SET CD SPEED недурно бы дать команду чтения сектора с диска, если, конечно, диск вообще присутствует. Изменять же скорость привода без диска в лотке - совершенно бессмысленная операция, пригодная разве что для построения ряда поддерживаемых скоростей, т.к. после вставки нового диска в привод прежние скоростные установки оказываются недействительными и наиболее оптимальная (с точки зрения привода!) скорость для каждого диска определяется индивидуально. Также привод вправе изменять скорость диска по своему усмотрению, понижая ее, если чтение идет неважно и, соответственно, увеличивая обороты если все идет хорошо.
[1] На самом деле, это утверждение не совсем верно. Некоторые из защит от копирования на бытовом оборудовании не могут быть взломаны в принципе. В частности, защиты аудиодисков, основанные на искажении TOC'a, приводят к нечитабельности такого диска компьютерными приводами CD-ROM, но на аудиоплеерах, не слишком дотошно анализирующих TOC, такой диск воспроизводится вполне нормально. Единственный способ скопировать такой диск в цифровом виде - пропатчить прошивку CD-ROM привода, убрав из нее ряд "лишних" проверок.
[2] В общем-то, это вполне логично - ведь Microsoft не имеет к ATAPI/SCSI-интерфейсам ни малейшего отношения и их стандартизацией занимаются совершенно иные комитеты. Однако в "приличных домах" так все-таки не поступают. Вместо того, чтобы оставить программиста со своими проблемами наедине, составителям документации могли бы, по крайней мере, нарисовать общую картину взаимодействия. Попробуйте выкачать из сети тысячи страниц технической документации (большей частью ненужной, но кто ж это знает заранее?) и, проштудировав ее всю, попытаться свести эту разрозненную картину воедино.
[3] Как вариант - можно обращаться к устройству "\\.\ CdRom0" или "\\.\ CdRom1" без знака двоеточия на конце, где 0 и 1 - порядковый номер CD-ROM привода в системе. Вопреки распространенному заблуждению, гласящему что устройство "\\.\ CdRom0" расположено на более низком уровне чем "\\.\ X:" с точки зрения операционной системы это синонимы и чтобы убедиться в этом, достаточно заглянуть в содержимое таблицы объектов (objdir "\DosDevice"), доказывающее, что "\\.\X:" представляет собой ни что иное, как символическую ссылку на \\.\CdRomN.
[4] В 16-разрядных приложениях взаимодействие с драйвером осуществляется через функцию 1868h прерывания 2Fh - подробности этого процесса можно узнать, дизассемблируя winaspi.dll. Она, кстати, совсем крошечная - всего 5 килобайт.
[5] См. также техническую заметку Q137247 из MSDN "IOCTL_SCSI_MINIPORT and IOCTL_SCSI_PASS_THROUGH Limitations".
[6] Внимание! Именно буфер, а не указатель на него.
[7] DPMI (DOS Protected Mode Interface) - интерфейс, спроектированный специально для того, чтобы разработчики приложений защищенного режима, исполняющихся в среде MS-DOS, могли пользоваться функциями 16-разрядной операционной системы реального режима, коей MS-DOS и является.
[8] Здесь и далее "хз" обозначает "зависит от реализации". ;)
[9] Установка драйвера требует наличия прав администратора на локальной машине, но вот его последующее использование - нет.