Автор: (c)Крис Касперски ака мыщъх
Секторный уровень взаимодействия всегда привлекал как создателей защитных механизмов, так и разработчиков утилит, предназначенных для копирования защищенных дисков. Еще большие перспективы открывает чтение/запись "сырых" (RAW) секторов - это наиболее низкий уровень общения с диском, какой только штатные приводы способны поддерживать. Большинство защитных механизмов именно так, собственно, и работает. Одни из них прячут ключевую информацию в каналы подкода, другие тем или иным образом искажают коды ECC/EDC, третьи используют нестандартную разметку и т.д., и т.п.
Существует множество способов для работы с диском на секторном уровне и ниже будет описан добрый десяток из них. Большая часть рассматриваемых здесь методик рассчитана исключительно на Windows NT/W2K/XP и не работает в Windows 9x, которой, по-видимому, придется разделить судьбу мамонтов, а потому интерес к ней стремительно тает как со стороны пользователей, так и со стороны программистов. Конечно, какое-то время она еще продержится на плаву, но в долгосрочной перспективе я бы не стал на нее закладываться, особенно учитывая тот факт, что Windows 9x не в состоянии поддерживать многопроцессорные системы, а победоносное шествие Hyper-Threading уже не за горами.
В силу того, что секторный уровень доступа к диску изначально ориентирован на создателей (ломателей) защитных механизмов, данный раздел выкрашен ярко-хакерской краской и рассказывает не только о самих методиках низкоуровневого управления устройствами, но и описывает технику взлома каждого из них. Забегая вперед, заметим, что сломать можно все! Так что не стоит, право же, переоценивать стойкость механизмов, препятствующих несанкционированному копированию лазерных дисков. Если кому-то особо приспичит, вашу программу все равно взломают! Как? Вот об этом и будет рассказано ниже. Как говорится, кто предупрежден - тот вооружен. Ну, а коль уж совсем невмоготу - используйте прямой доступ к портам ввода/вывода с прикладного уровня. Нет, вы не ослышались - в Windows NT это действительно возможно и ниже будет рассказано как.
Существует множество способов взаимодействия с оборудованием. В зависимости от специфики решаемой задачи и специфики самого оборудования предпочтение отдается либо тем, либо иным интерфейсам управления. На самом высоком уровне интерфейсной иерархии располагается семейство API-функций операционной системы, реализующих типовые операции ввода/вывода (такие, например, как открыть файл, прочитать данные из файла и др.). Для подавляющего большинства прикладных программ этого оказывается более, чем достаточно, однако даже простейший копировщик на этом наборе, увы, не напишешь и приходится спускаться, по меньшей мере, на один уровень вглубь, обращаясь непосредственно к драйверу данного устройства.
Стандартные дисковые драйвера, входящие в состав операционных систем Windows 9x и NT, поддерживают довольно ограниченное количество типовых команд (прочитать сектор, просмотреть TOC и т.д.), не позволяющих в должной мере реализовать все возможности современных приводов CD-ROM/R/RW, однако для написания простейших защитных механизмов их функционала окажется вполне достаточно.
Подавляющее большинство защитных механизмов данного типа безупречно копируется штатными копировщиками, что, собственно, и неудивительно - ведь и копировщик, и "защита" кормятся из одной и той же кормушки, простите - используют идентичный набор управляющих команд, работающий с устройством на логическом уровне.
Для создания устойчивой к взлому защиты мы должны опуститься на самое дно колодца, заговорив с устройством на родном для него "языке". Несмотря на то, что контроллеры оптических накопителей поддерживают высокоуровневый набор управляющих команд (намного более высокоуровневый, чем приводы гибких дисков); несмотря на то, что интерфейс привода абстрагирован от конкретного физического оборудования; несмотря на то, что CD-ROM/R/RW диски изначально не были ориентированы на защиту, создание практически некопируемых дисков на этом уровне все-таки возможно.
Вопреки расхожему мнению, для низкоуровневого управления накопителями совершенно необязательно прибегать к написанию своего собственного драйвера. Все необходимые драйвера давно уже написаны за нас и на выбор разработчика предоставляется несколько конкурирующих интерфейсов, обеспечивающих низкоуровневое взаимодействие со SCSI/ATAPI-устройствами с прикладного уровня. Это и ASPI, и SPTI, и MSCDEX (ныне практически забытый, но все же поддерживаемый операционными системами Windows 98 и Windows Me). Каждый из интерфейсов имеет свои достоинства и свои недостатки, поэтому коммерческие программные пакеты вынуждены поддерживать их все.
Поскольку программирование оптических накопителей выходит далеко за рамки предмета защиты лазерных дисков, то интерфейсы взаимодействия с устройствами будут рассмотрены максимально кратко и упрощенно. К слову сказать, ряд книг, посвященных непосредственно управлению SCSI/ATAPI-устройствами, значительно проигрывает настоящему разделу (взять, к примеру, "Программирование устройств SCSI и IDE" Всеволода Несвижского, описывающего исключительно интерфейс ASPI и к тому же описывающего его неправильно).
Информации, приведенной ниже, вполне достаточно для самостоятельного изучения всех вышеперечисленных интерфейсов с абсолютного нуля. Даже если вам никогда до этого не приходилось сталкиваться с программированием SCSI/ATAPI-устройств, вы вряд ли будете испытывать какие-либо затруднения по ходу чтения книги (не говоря уж о том, что данная книга научит вас основам шпионажа за чужими программами и взлому оных, но это строго между нами!).
Управление драйверами устройств в операционных системах семейства Windows осуществляется посредством вызова функции DeviceIoControl, отвечающей за посылку специальных FSCTL/IOCTL - команд. Префикс FS- свидетельствует о принадлежности данной команды к файловой системе и в контексте настоящей публикации не представляет для нас никакого интереса. Команды с префиксом IO- относятся к устройству ввода/вывода, а точнее - к его драйверу.
Функция DeviceIoControl просто передает такую команду, как она есть, совершенно не задумываясь о ее "физическом смысле". Следовательно, совершенно бессмысленно искать перечень доступных IOCTL - команд в описании DeviceIoControl. Их там нет! Точнее, здесь приводятся лишь стандартные IOCTL-команды, а вся остальная информация по этому вопросу содержится в DDK.
Там, в частности, мы найдем, что для чтения оглавления диска используется команда IOCTL_CDROM_READ_TOC, а для перечисления адресов сессий многосессионных дисков - IOCTL_CDROM_GET_LAST_SESSION. Также обратите свое внимание на команду IOCTL_CDROM_READ_Q_CHANNEL, обеспечивающую извлечение информации из Q-канала подкода (для извлечения ключевых меток это актуально).
Чтение "сырых" секторов осуществляется командой IOCTL_CDROM_RAW_READ, возможности которой, к сожалению, ограничены лишь CDDA-дисками только. Посекторное чтение с CDDATA-дисков ни на сыром, ни на "сухом" уровнях не поддерживается. В соответствии с принятой политикой безопасности, никакое приложение не должно действовать в обход системы безопасности, в противном случае злоумышленник сможет без труда дорваться до конфиденциальных данных, просто прочитав диск на секторном уровне. Штатные драйвера, которыми укомплектованы операционные системы семейства Windows, всецело следуют этим требованиям, хотя сторонние разработчики могут, при желании, и нарушить этот запрет. В состав NT DDK входит исходный текст демонстрационного CD-ROM-драйвера ("NTDDK\src\storage\class\cdrom\"), который после небольшой обработки напильником, согласиться читать диски всех типов, не задавая при этом глупых вопросов. Найдите в теле файла cdrom.c следующую строку "if rawReadInfo->TrackMode == CDDA) {" и перейдите к ветке, чей OperationCode равен SCSIOP_READ. А теперь модифицируйте код так, чтобы она получала управление во всех остальных случаях.
Замечание: функция IRP_MJ_READ, присутствующая в DDK, и по идее обеспечивающая возможность чтения отдельных логических блоков, является внутренней функцией драйвера и доступ к последней с прикладного уровня закрыт; пытаться использовать ее в паре с DeviceIoControl - бессмысленно.
IOCTL-команда | Описание |
IOCTL_CDROM_CHECK_VERIFY,IOCTL_STORAGE_CHECK_VERIFY (0x24800h) | определяет факт смены диска (открытия/закрытия лотка) |
IOCTL_CDROM_CLOSE_DOOR IOCTL_STORAGE_LOAD_MEDIA (0x2D480Ch) | закрывает лоток привода |
IOCTL_CDROM_FIND_NEW_DEVICES,IOCTL_STORAGE_FIND_NEW_DEVICES (0x24818h) | перечисляет новые приводы, подключенные после загрузки системы или последнего вызова данной команды |
IOCTL_CDROM_GET_CONTROL | сообщает текущую позицию воспроизведения аудио |
IOCTL_CDROM_GET_DRIVE_GEOMETRY (0x2404Ch) | определяет тип лазерного диска и его геометрию (кол-во секторов на диске, размер одного сектора и т.д.) |
IOCTL_CDROM_GET_LAST_SESSION (0x24038h) | перечисляет стартовые адреса сессий и записывает их в буфер TOC, читаемый IOCTL_CDROM_READ_TOC |
IOCTL_CDROM_GET_VOLUME (0x24014h) | возвращает текущий уровень громкости с CD-ROM |
IOCTL_CDROM_PAUSE_AUDIO (0x2400Ch) | временно останавливает воспроизведение аудио |
IOCTL_CDROM_PLAY_AUDIO_MSF (0x24018h) | инициирует процесс воспроизведения аудио от сих до сих |
IOCTL_CDROM_RAW_READ (0x2403Eh) | сырое чтение секторов с аудиодисков |
IOCTL_CDROM_READ_Q_CHANNEL (0x2402Ch) | чтение данных Q-канала подкода |
IOCTL_CDROM_READ_TOC (0x24000h) | читает оглавление диска |
IOCTL_CDROM_RESUME_AUDIO (0x24010h) | продолжить воспроизведение аудио |
IOCTL_CDROM_SEEK_AUDIO_MSF (0x24004h) | позиционирует оптическую головку |
IOCTL_CDROM_SET_VOLUME (0x24028h) | установить уровень громкости с CD-ROM |
IOCTL_CDROM_STOP_AUDIO (0x24008h) | останавливает воспроизведение аудио |
Таблица 1. Описание IOCTL-команд штатного CD-ROM драйвера (за более подробной информацией обращайтесь к DDK).
Функции DeviceIoControl всегда предшествует вызов CreateFile, возвращающей дескриптор соответствующего устройства, задаваемого в виде "\\.\X:", где X - буквенное обозначение того привода, с которым мы собрались работать, причем флаг dwCreationDisposition должен быть установлен в состояние OPEN_EXISTING, иначе вы потерпите неудачу. Типовой пример вызова функции приведен ниже (замечание: Windows NT регистрирует устройство с именем \\.\CdRomx, где x - номер привода, считая от нуля, которое ссылается на тот же самый драйвер, что и буквенное обозначение диска и обладает тем же самым набором функций):
HANDLE hCD; // дескриптор привода hCD=CreateFile("\\\\.\\X:", GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0,0); if (hCD == INVALID_HANDLE_VALUE) // ошибка
Листинг 1. Пример открытия устройства.
Прототип самой же DeviceIoControl выглядит так:
BOOL DeviceIoControl( HANDLE hDevice, // дескриптор устройства DWORD dwIoControlCode, // IOCTL-код команды для выполнения LPVOID lpInBuffer, // указатель на входной буфер (Irp->AssociatedIrp.SystemBuffer) DWORD nInBufferSize, // размер входного буфера в байтах LPVOID lpOutBuffer, // указатель на выходной буфер (Irp->AssociatedIrp.SystemBuffer) DWORD nOutBufferSize, // размер выходного буфера в байтах LPDWORD lpBytesReturned, // указатель на счетчик кол-ва возвращенных байт LPOVERLAPPED lpOverlapped // указатель на структуру для асинхронных операций );
Листинг 2. Прототип функции DeviceIoControl.
Здесь:
Если операция завершилась успешно, функция возвращает ненулевое значение, и нуль - в противном случае. За более подробной информацией об ошибке возвращайтесь к GetLastError.
Передача IOCTL-команд устройству не требует наличия прав администратора (за тем исключением, когда устройство открывается с флагом GENETIC_WRITE), что значительно увеличивает "эргономичность" защитных механизмов, базирующихся на ее основе (Кстати, о защитных механизмах, точнее - о их стойкости ко взлому. Поскольку функция DeviceIoControl к числу популярных явно не относится, она демаскирует штаб-квартиру защитного механизма, и его становится очень легко "запеленговать". Достаточно поставить на DeviceIoControl точку останова и дождаться, пока передаваемая ей IOCTL-команда не примет одно из вышеперечисленных значений. На CreateFile точку останова лучше не ставить, т.к. это даст множество ложных срабатываний (CreateFile вызывается всякий раз при открытии/создании какого либо файла). А вот попробовать поискать в теле программы текстовую строку "\\.\" все-таки стоит. И, если она действительно будет найдена, вам останется лишь подбежать курсором к перекрестной ссылке и долбануть по Enter'у. Все! Защитный код - перед вами!)
Для лучшего понимания данного способа взаимодействия между прикладной программой и драйвером ниже приведен ключевой фрагмент функции, как раз и осуществляющей такое взаимодействие (обработка ошибок по соображениям наглядности опущена):
//--[ReadCDDA]----------------------------------------------------------------- // // читает сектор в "сыром" виде с CDDA-дисков // ========================================== // ARG: // drive - имя устройства, с которого читать (например "\\\\.\\X:") // start_sector - номер первого читаемого сектора // n_sec - сколько секторов читать // // RET: // == 0 - ошибка // != 0 - указатель на буфер, содержащий считанные сектора // // NOTE: // функция поддерживает только диски тех типов, что поддерживает драйвер // CDFS, который она и использует, а штатный драйвер Windows NT поддерживает // лишь CDDA-диски //---------------------------------------------------------------------------- char* ReadCDDA(char *drive, int start_sector, int n_sec) { // поддерживаемые типы треков typedef enum _TRACK_MODE_TYPE { YellowMode2, // native MODE 2 (не CD-data) XAForm2, // XA MODE 2 Form 2 (VideoCD) CDDA // Audio-CD } TRACK_MODE_TYPE, *PTRACK_MODE_TYPE; // аргумент IOCTL-команды IOCTL_RAW_READ typedef struct __RAW_READ_INFO { LARGE_INTEGER DiskOffset; // смещение в байтах лог. блоков ULONG SectorCount; // кол-во секторов для чтения TRACK_MODE_TYPE TrackMode; // режим читаемого трека } RAW_READ_INFO, *PRAW_READ_INFO; #define CDROM_RAW_SECTOR_SIZE 2352 #define CDROM_SECTOR_SIZE 2048 int a; HANDLE hCD; DWORD x_size; char *szDrive; BOOL fResult = 0; unsigned char *buf; RAW_READ_INFO rawRead; // ПОДГОТАВЛИВАЕМ СТРУКТУРУ RAW_READ_INFO, передаваемую драйверу CD-ROM'а rawRead.TrackMode = CDDA; // тип диска - Audio CD rawRead.SectorCount = n_sec; // кол-во читаемых секторов rawRead.DiskOffset.QuadPart = start_sector * CDROM_SECTOR_SIZE; // ^^^^^^^^^^^^^^^^^^ // стартовый сектор задается отнюдь не своим логическим номером, // а номером своего первого байта. сквозная нумерация байтов от первого // до последнего байта диска теоритически обеспечивает полное абстрагирование // от конкретного оборудования (размер одного сектора возвращается IOCTL- // командой IOCTRL_CDROM_GET_DRIVE_GEOMETRY), но практически архитекторами // драйвера допущен грубый ляп, "благодаря" которому драйвер принимает вовсе // не сквозные номера байт, а start_address * CDROM_SECTOR_SIZE, где // SECTOR_SIZE - размер логического блока, который в данном случае равен // стандартному размеру сектора CDDATA-диска (2048 байт для справки), // в то время как размер сектора CDDA-дисков составляет 2352 байта // поэтому DiskOffset равен start_secor * CDROM_SECTOR_SIZE, а размер буфера // должен быть равен start_secor * CDROM_RAW_SECTOR_SIZE // ВЫДЕЛЯЕМ ПАМЯТЬ buf = malloc(CDROM_RAW_SECTOR_SIZE * n_sec); // ПОЛУЧАЕМ ДЕСКРИПТОР УСТРОЙСТВА hCD = CreateFile(drive,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,0,0); if (hCD != INVALID_HANDLE_VALUE) // ПЕРЕДАЕМ ДРАЙВЕРУ ПРИВОДА КОМАНДУ IOCTL_CDROM_RAW_READ fResult = DeviceIoControl( hCD, 0x2403E /* IOCTL_CDROM_RAW_READ */, &rawRead, sizeof(RAW_READ_INFO), buf, CDROM_RAW_SECTOR_SIZE*n_sec, &x_size, (LPOVERLAPPED) NULL); // ВЫВОДИМ РЕЗУЛЬТАТ (если есть, что выводить) if (fResult) for (a = 0; a <= x_size; ++a) printf("%02X%s",buf[a],(a%24)?" ":"\n"); else printf("-ERROR"); printf("\n"); // СВАЛИВАЕМ CloseHandle(hCD); return (fResult)?buf:0; }
Листинг 3. Функция, демонстрирующая технику чтения сырых секторов через CDFS-драйвер (только для CDDA дисков!).
Еще один демонстрационный пример приведен ниже. Он иллюстрирует технику чтения TOC (Table of Content) - своеобразный аналог таблицы разделов лазерных аудиодисков.
/*---------------------------------------------------------------------------- * * ЧТЕНИЕ И РАСШИФРОВКА TOC * ======================== * * build 0x001 @ 26.05.2003 ----------------------------------------------------------------------------*/ main(int argc, char **argv) { int a; HANDLE hCD; unsigned char *buf; WORD TOC_SIZE; BYTE n_track; DWORD x_size,b; #define DEF_X "\\\\.\\G:" // привод по умолчанию #define argCD ((argc>1)?argv[1]:DEF_X) // ПРОВЕРКА АРГУМЕНТОВ if (argc < 2) {fprintf(stderr, "USAGE: IOCTL.read.TOC \\\\.\\X:\n"); return 0;} // TITLE fprintf(stderr,"simple TOC reader via IOCTL\n"); // ВЫДЕЛЯЕМ ПАМЯТЬ buf = (char *) malloc(buf_len); // ОТКРЫВАЕМ УСТРОЙСТВО hCD=CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); // ВЫХОДИМ, ЕСЛИ ОШИБКА if (hCD == INVALID_HANDLE_VALUE) {fprintf(stderr,"-ERR: %x\n", GetLastError()); return 0;} // ПЕРЕДАЕМ ДРАЙВЕРУ КОМАНДУ CDROM_READ_TOC if (DeviceIoControl( hCD, 0x24000 /* IOCTL_READ_TOC */, 0, 0, buf, buf_len, &x_size, 0) != 0) { // ПОЛУЧАЕМ ДЛИНУ ТОС'а (она записана в обратном порядке) TOC_SIZE = buf[0]*0x100L + buf[1]; printf("TOC Data Length........%d\n",TOC_SIZE); // декодируем остальную информацию printf("First Session Number...%d\n",buf[2]); printf("Last Session Number....%d\n\n",(n_track=buf[3])); for (a = 1; a <= n_track; a++) { printf("track %d\n{\n",a); printf("\treserved.............%x\n",buf[a * 8 - 4]); printf("\tADR|control..........%d\n",buf[a * 8 - 3]); printf("\ttrack number.........%d\n",buf[a * 8 - 2]); printf("\treserved.............%d\n",buf[a * 8 - 1]); printf("\treserved.............%d\n",buf[a * 8 + 0]); printf("\tmin..................%d\n",buf[a * 8 + 1]); printf("\tsec..................%d\n",buf[a * 8 + 2]); printf("\tframe................%d\n",buf[a * 8 + 3]); printf("}\n\n"); } // выводим содержимое TOC'a в "сыром" виде printf("\n\t\t\t* * * RAW * * *\n"); for(a = 0; a < x_size; a++) printf("%02X%s",(unsigned char)buf[a],((a+1)%22)?" ":"\n"); printf("\n\t\t\t* * * * * * *\n"); } }
Листинг 4. Еще один пример программы, взаимодействующей с 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, анализ окрестностей которых и разоблачит алгоритм работы защитного механизма.
Демонстрационный пример, приведенный ниже, представляет собой вполне законченную утилиту для "грабежа" дисков с данными на секторном уровне с последующей записью всего награбленного в файл.
/*---------------------------------------------------------------------------- * * ЧИТАЕТ СЕКТОРА С CD-ROM В БЛОЧНОМ РЕЖИМЕ * ======================================== * * данная программа работает только под Windows NT, не требуя для себя * прав администратора * * Build 0x001 @ 19.05.03 ---------------------------------------------------------------------------- */ #include <windows.h> #include <winioctl.h> #include <stdio.h> // ПАРАМЕТРЫ ПО УМОЛЧАНИЮ #define DEF_FN "sector" #define DEF_TO 0x666 #define DEF_FROM 0x000 #define CDROM_SECTOR_SIZE 2048 // for MODE1/MODE2FORM1 only! // АРГУМЕНТЫ КОМАНДНОЙ СТРОКИ #define argCD (argv[1]) #define argFN ((argc > 2)?argv[2] :DEF_FN) #define argFROM ((argc > 3)?atol(argv[3]):DEF_FROM) #define argTO ((argc>4)?(atol(argv[4])>argFROM)?atol(argv[4]):argFROM:DEF_TO) main(int argc, char **argv) { int a; FILE *f; HANDLE hCD; char *buf; DWORD x_read; char buf_n[1024]; // ПРОВЕРЯЕМ АРГУМЕНТЫ if (argc<2) { printf("USAGE: cooked.sector.read PhysCD [filename] [from] [to]\n"); printf("\tPhysCD - physical name of CD (\"\\\\.\\G:\")\n"); printf("\tfilename - file name to store follow sector\n"); printf("\tfrom - start sector\n"); printf("\tto - end sector\n"); return 0; } // TITLE fprintf(stderr,"cooked sector reader for NT\n"); // ВЫДЕЛЯЕМ ПАМЯТЬ buf=malloc(CDROM_SECTOR_SIZE);if (!buf){printf("-ERR:low memory\n");return -1;} // ОТКРЫВАЕМ УСТРОЙСТВО hCD=CreateFile(argCD, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); if (hCD == INVALID_HANDLE_VALUE) { printf("-ERR: error CreateFile(%s,....)\n", argCD); return -1; } // INFO printf("read sector from %04d to %04d in %s file\n", argFROM, argTO, argFN); // ПОЗИЦИОНИРУЕМ УКАЗАТЕЛЬ НА ПЕРВЫЙ ЧИТАЕМЫЙ СЕКТОР SetFilePointer (hCD, CDROM_SECTOR_SIZE * argFROM, NULL, FILE_BEGIN); // ЧИТАЕМ СЕКТОРА ОДИН ЗА ДРУГИМ for (a = argFROM; a <= argTO; a++) { // читаем очередной сектор if (ReadFile(hCD, buf, CDROM_SECTOR_SIZE, &x_read, NULL) && x_read) { // записываем только что считанный сектор в файл sprintf(buf_n,"%s[%04d].dat",argFN, a); if (f=fopen(buf_n,"wb")){fwrite(buf, 1, x_read, f); fclose(f);} printf("sector [%04d.%04d] read\r",a, argTO); } else { printf("sector %04d read error\n",a); } } }
Листинг 5. Пример, демонстрирующий технику чтения секторов в cooked-mode.
Одно из интереснейших архитектурных особенностей операционной системы Windows NT заключается в ее умении взаимодействовать с IDE-устройствами через SCSI-интерфейс! К сожалению, данная технология чрезвычайно скудно документирована - Platform SDK, MSDN, DDK содержат лишь обрывки информации, а имеющиеся примеры крайне ненаглядны и к тому же выполнены с большим количеством фактических ошибок, так что разобраться с ними под силу лишь профессионалу или очень настырному новичку. И, судя по сообщениям в телеконференциях, многим программистам осилить технику управления устройствами через SCSI-интерфейс так и не удалось, поэтому имеет смысл рассмотреть эту проблему поподробнее.
Для решения поставленной задачи нам понадобится:
Итак, что же такое SCSI? Это - стандартизованный, платформенно-независимый интерфейс, обеспечивающий согласованное взаимодействие различных устройств и высокоуровневых приложений. Собственно, аббревиатура SCSI именно так и расшифровывается - Small Computer System Interface (Системный Интерфейс Малых Компьютеров). Благодаря SCSI для низкоуровневого управления устройствами совершенно необязательно прибегать к написанию собственных драйверов (писать драйвер только для того, чтобы прорваться сквозь ограничения API - чистейший маразм) и эту задачу можно решить и на прикладном уровне, посылая устройству специальные CDB-блоки, содержащие стандартные или специфичные для данного устройства команды управления вместе со всеми необходимыми им параметрами. Собственно, "CDB" так и расшифровывается - Command Descriptor Block. Пример одного из таких блоков приведен ниже:
Смещение, байт | Содержимое | |
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 | Зарезервировано |
Таблица 2. Пример CDB блока, который, будучи переданным SCSI-устройству, заставляет его прочитать 0x69-сектор.
Первый байт блока представляет собой команду операции (в нашем случае: 0x28 - чтение одного или нескольких секторов), а все остальные байты блока - параметры данной команды. Причем, обратите внимание на тот факт, что младший байт слова располагается по большему адресу, то есть все происходит не так, как в привычном нам IBM PC! Поэтому, если передать в качестве номера первого сектора последовательность 0x69 0x00 0x00 0х00, то почитается 0x6900000 сектор, а вовсе не 0x00000069, как можно было того ожидать!
Краткое описание стандартных SCSI-команд можно найти в том же "The Linux SCSI programming HOWTO", однако для наших целей их вряд ли окажется достаточно, и команды, специфичные для CD-ROM дисков, мы рассмотрим отдельно. Однако это произойдет не раньше, чем мы разберемся, как CDB-блоки упаковываются в SRB-конверт (SCSI Request Block), без которого операционная система просто не поймет - что же мы хотим сделать (как известно, машинная программа выполняет то, что ей приказали сделать, иногда это совпадает с тем, что от нее хотели, иногда нет).
Структура SRB-блока подробно описана в NT DDK, поэтому не будем подробно на ней останавливаться и пробежимся по основным полям лишь вкратце.
typedef struct _SCSI_REQUEST_BLOCK { USHORT Length; // длина структуры SCSI_REQUEST_BLOCK UCHAR Function; // функция (обычно SRB_FUNCTION_EXECUTE_SCSI == 0, т.е. // отправить устройству команду на выполнение) UCHAR SrbStatus; // здесь устройство отображает прогресс выполнения // команды, наиболее часто встречаются значения: // SRB_STATUS_SUCCESS == 0x1 - команда завершена успешно // SRB_STATUS_PENDING == 0x0 - команда еще выполняется // SRB_STATUS_ERROR == 0x4 - произошла ошибка // также возможны и другие значения, перечисленные в DDK UCHAR ScsiStatus; // здесь устройство возвращает статус завершения команды // и, если не SUCCESS, значит, произошел ERROR UCHAR PathId // SCSI-порт, на котором сидит контроллер устройства // для "виртуальных" SCSI устройств всегда 0 UCHAR TargetId; // контроллер устройства на шине // для IDE устройств обычно 0 - primary, 1 - secondary UCHAR Lun; // логический номер устройства внутри контроллера // для IDE устройств обычно 0 - master, 1 - slayer CHAR QueueTag; // обычно не используется и должно быть равно нулю CHAR QueueAction; // обычно не используется и должно быть равно нулю CHAR CdbLength; // длина CDB-блока, для ATAPI-устройств всегда 12 (0Ch) CHAR SenseInfoBufferLength; // длина SENSE-буфера (о нем ниже) LONG SrbFlags; // флаги. обычно принимают два значения // SRB_FLAGS_DATA_IN == 0x40 - перемещение данных от // устройства к компьютеру (чтение) // SRB_FLAGS_DATA_OUT == 0x80 - перемещение данных от // компьютера к устройству (запись) ULONG DataTransferLength; // длина блока читаемых/записываемых данных LONG TimeOutValue; // время вылета по тайм-ауту в секундах PVOID DataBuffer; // указатель на буфер c читаемыми/записываемыми данными PVOID SenseInfoBuffer; // указатель на SENSE буфер (о нем - ниже) struct _SCSI_REQUEST_BLOCK *NextSrb; // указатель на след. SRB. Обычно не исп. PVOID OriginalRequest; // указатель на IRP. Практически не используется PVOID SrbExtension; // обычно не используется и должно быть равно нулю UCHAR Cdb[16]; // собственно, сам CDB-блок } SCSI_REQUEST_BLOCK, *PSCSI_REQUEST_BLOCK;
Листинг 6. Кратное описание структуры SCSI_REQUEST_BLOCK.
Заполнив поля структуры 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:" возвратит ошибку (и это, как показывает практика, основной камень преткновения неопытных программистов, начинающих программировать раньше, чем читать документацию).
В-пятых, на максимальный размер пересылаемых данных (MaximumTransferLength) наложены жесткие ограничения, диктуемые спецификой используемого оборудования и обслуживающего его драйвера мини порта. Ограничения касаются как предельно допустимого размера блока данных, так и количества занятых им физических страниц. Для определения конкретных характеристик следует послать устройству команду IOCTL_SCSI_GET_CAPABILITIES, которая возвратит структуру IO_SCSI_CAPABILITIES (ищите ее определение в NTDDSCSI.h), содержащую среди всего прочего значения MaximumTransferLength и MaximumPhysicalPages_in_bytes. Максимальный размер пересылаемых данных вычисляется по следующей формуле: largest transfer = min(MaximumTransferLength, MaximumPhysicalPages_in_bytes). Как вариант можно ограничиться 64 килобайтовыми блоками, гарантированно поддерживаемых всеми устройствами. Буфер так же должен быть выровнен на величину кратную AlignmentMask, возвращаемую в структуре IO_SCSI_CAPABILITIES. Степень выравнивания, обеспечиваемая функцией malloc, для этих целей оказывается вполне достаточной и при ее использовании никаких проблем не возникает. Другое дело, если выделение памяти осуществляется конструкцией "char buf[BUF_SIZE]", в этом случае работоспособность вашей программы уже не гарантируется.
В-шестых, сама структура SCSI_PASS_THROUGH_DIRECT содержит значительно меньше полей, причем значения полей PathId, TargetId и Lun просто игнорируются! Физический адрес устройства на шине определяется непосредственно самой операционной системой по символьному имени дескриптора устройства, которому, собственно, и посылается SCSI_PASS_THROUGH_DIRECT запрос.
typedef struct _SCSI_PASS_THROUGH_DIRECT { USHORT Length; // размер структуры SCSI_PASS_THROUGH_DIRECT UCHAR ScsiStatus; // статус выполнения SCSI-команды устройством UCHAR PathId; // игнорируется UCHAR TargetId; // игнорируется UCHAR Lun; // игнорируется UCHAR CdbLength; // длина CDB-пакета, посылаемая устройству, байты UCHAR SenseInfoLength; // длина SENSE-буфера для возращения ошибки UCHAR *DataIn; // направление передачи данных ULONG DataTransferLength; // размер буфера для обмена данными в байтах ULONG TimeOutValue; // время вылета по тайм-ауту PVOID DataBuffer; // указатель на буфер для обмена данными ULONG SenseInfoOffset; // указатель на SENSE-буфер с информацией о error UCHAR Cdb[16]; // буфер с CDB-пакетом (16 байт максимум) }SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT;
Листинг 7. Формат структуры SCSI_PASS_THROUGH_DIRECT (структура SCSI_PASS_THROUGH во всем похожа на нее, но не обеспечивает передачу данных через DMA).
К счастью, "цензура", в основном, коснулась тех полей, которые в реальной жизни все равно практически не используются, так что мы ровным счетом ничего не потеряли. Заполняем оставшиеся поля, и наша структура готова!
Естественно, прежде чем передать ее устройству, нам необходимо получить дескриптор этого самого устройства. Это можно сделать так:
HANDLE hCD = CreateFile ("\\\\.\\X:", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
Листинг 8. Открытие привода для получения дескриптора, использующегося для его управления.
Убедившись, что hCD не равно INVALID_HANDLE_VALUE, передаем полученный дескриптор вместе с самой структурой IOCTL_SCSI_PASS_THROUGHT_DIRECT функции DeviceIoControl, вызывая ее следующим образом:
DeviceIoControl(hCD, 0x4D014h /* IOCTL_SCSI_PASS_THROUGH_DIRECT */, &srb, sizeof(SCSI_PASS_THROUGH_DIRECT), sense_buf, SENSE_SIZE, &returned, 0);
Листинг 9. Передача структуры IOCTL_SCSI_PASS_THROUGH.
Где srb и есть заполненный
экземпляр структуры IOCTRL_SCSI_PASS_THROUGHT_DIRECT,
а returned -
переменная, в которую будет записано количество байт, возвращенных устройством.
В свою очередь, sense_buf - это тот самый
буфер, в котором заполненный нами экземпляр IOCTL_SCSI_PASS_THROUGHT_DIRECT возвращается назад, да не один, а
вместе с sense инфой -
кодом ошибки завершения операции. Если же операция завершилась без ошибок, то 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 | Зарезервировано. |
Таблица 3. Основные 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 |
Таблица 4. Основные 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 (код BEh) и вы обнаружите таблицу следующего содержания:
Рисунок 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 (т.е. непосредственно в сам драйвер привода), где они заново транслируются в конкретные физические команды данного устройства. Подробности этого увлекательного процессора можно почерпнуть из NT DDK (см. "1.1 Storage Driver Architecture"), здесь же достаточно указать на тот немаловажный факт, что кроме команд семейства IRP_MJ_ххх мы также можем посылать устройству и SRB-запросы, которые обладают значительно большей свободной и гибкостью. Однако, такое взаимодействие невозможно осуществить непосредственного с прикладного уровня, поскольку IRP-команды относятся к числу приватных команд, в то время как API-функция DeviceIoControl передает лишь публичные команды, явно обрабатываемые драйвером в диспетчере IRP_MJ_DEVICE_CONTROL.
Давайте теперь, в порядке закрепления всего вышесказанного, попытаемся создать программу, которая бы читала сектора с лазерных дисков в "сыром" виде. Ее ключевой фрагмент (вместе со всеми необходимыми комментариями) приведен ниже:
#define RAW_READ_CMD 0xBE // ATAPI RAW READ #define WHATS_READ 0xF8 // Sync & All Headers & User Data + EDC/ECC #define PACKET_LEN 2352 // длина одного сектора //#define WHATS_READ 0x10 // User Data //#define PACKET_LEN 2048 // длина одного сектора //-[SPTI_RAW_SECTOR_READ]------------------------------------------------------ // функция читает один или несколько секторов с CDROM в сыром (RAW) виде, // согласно переданным флагам // // ARG: // CD - что открывать (типа "\\\\.\\X:" или "\\\\.\\CdRom0") // buf - буфер куда читать // buf_len - размер буфера в байтах // StartSec - с какого сектора читать, считая от нуля // N_SECTOR - сколько секторов читать // flags - что читать (см. спецификацию на SCSI/ATAPI) // // RET: // !=0 - функция завершилась успешно // ==0 - функция завершилась с ошибкой // // NOTE: // - работает только под NT/W2K/XP и требует прав администратора // // - 64 Кб данных за раз максимум //----------------------------------------------------------------------------- SPTI_RAW_SECTOR_READ(char *CD,char *buf,int buf_len,int StartSec,int N_SEC,char flags) { HANDLE hCD; SCSI_PASS_THROUGH_DIRECT srb; DWORD returned, length, status; // ОТКРЫВАЕМ УСТРОЙСТВО hCD = CreateFile ( driver, GENERIC_WRITE|GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,0,OPEN_EXISTING,0,0); if (hCD == INVALID_HANDLE_VALUE) { printf("-ERR: open CD\n"); return 0;} // ФОРМИРУЕМ SRB memset(&srb,0,sizeof(SCSI_PASS_THROUGH_DIRECT)); // инициализация srb.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); srb.PathId = 0; // SCSI controller ID srb.TargetId = 6; // игнорируется srb.Lun = 9; // игнорируется srb.CdbLength = 12; // длина CDB пакета srb.SenseInfoLength = 0; // нам не нужна SenseInfo srb.DataIn = SCSI_IOCTL_DATA_IN; // мы будем читать srb.DataTransferLength = PACKET_LEN*N_SECTOR; // сколько мы будем читать srb.TimeOutValue = 200; // время выхода по TimeOut srb.DataBufferOffset = buf; // указатель на буфер srb.SenseInfoOffset = 0; // SenseInfo на не нужна // CDB-пакет, содержащий команды ATAPI srb.Cdb[0] = RAW_READ_CMD; // читать сырой сектор srb.Cdb[1] = 0x0; // формат диска - любой // номер первого сектора для чтения, причем сначала передается старший // байт старшего слова, а потом младший байт младшего слова srb.Cdb[2] = HIBYTE(HIWORD(StartSector)); srb.Cdb[3] = LOBYTE(HIWORD(StartSector)); srb.Cdb[4] = HIBYTE(LOWORD(StartSector)); srb.Cdb[5] = LOBYTE(LOWORD(StartSector)); // количество секторов для чтения srb.Cdb[6] = LOBYTE(HIWORD(N_SECTOR)); srb.Cdb[7] = HIBYTE(LOWORD(N_SECTOR)); srb.Cdb[8] = LOBYTE(LOWORD(N_SECTOR)); srb.Cdb[9] = flags; // что читать srb.Cdb[10] = 0; // Sub-Channel Data Bits srb.Cdb[11] = 0; // reserverd // ОТПРАВЛЯЕМ SRB-блок ATAPI-устройству status = DeviceIoControl(hCD, IOCTL_SCSI_PASS_THROUGH_DIRECT, &srb, sizeof(SCSI_PASS_THROUGH_DIRECT), &srb, 0, &returned, 0); return 1; }
Листинг 10. Функция, читающая сектора в сыром виде через 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) и создающим в процессе своей инициализации устройство с непроизносимым названием 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/W2K/XP), а вот 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;
Листинг 11. Структура SRB_ExecSCSICmd.
Обратите внимание: для взаимодействия с устройством вам совершенно незачем знать его дескриптор! Достаточно указать его физический адрес на шине (т.е. правильно заполнить поля SRB_HaId и SRB_Target)... а как их узнать? Да очень просто - достаточно разослать по всем физическим адресам команду INQUIRY (код 12h). Устройство, реально (и/или виртуально) подключенные к данному порту вернут идентификационную информацию (среди прочих полезных данных содержащую и свое имя), а несуществующие устройства не вернут ничего и операционная система отрапортует об ошибке.
Простейшая программа опроса устройств может выглядеть, например, так:
#define MAX_ID 8 #define MAX_INFO_LEN 48 SEND_SCSI_INQUITY() { #define MAX_LUN 8 // макс. возможное кол-во логических устройств BYTE AdapterCount; DWORD ASPI32Status; unsigned char buf[0xFF]; unsigned char str[0xFF]; unsigned char CDB[ATAPI_CDB_SIZE]; long a, real_len, adapterid, targetid; // получаем кол-во адаптеров на шине ASPI32Status = GetASPI32SupportInfo(); AdapterCount = (LOBYTE(LOWORD(ASPI32Status))); // готовим CDB-блок memset(CDB, 0, ATAPI_CDB_SIZE); CDB[0] = 0x12; // INQUIRY CDB[4] = 0xFF; // размер ответа // спамим порты в надежде найти тех, кто нам нужен for (adapterid = 0; adapterid < MAX_LUN; adapterid++) { // внимание! нельзя здесь ^^^^^^^^^^^^^ использовать AdapterCount, // как это рекомендуется в некоторых руководствах, поскольку номера // адаптеров устройств далеко не всегда идут вплотную друг к другу, // и если в нумерации возникает "разрыв", одно или более устройств // останутся необнаруженными for (targetid = 0; targetid < MAX_ID; targetid++) { a = SEND_ASPI_CMD(adapterid, targetid, CDB, ATAPI_CDB_SIZE, 0, buf, 0xFF, ASPI_DATA_IN); if (a == SS_COMP) { real_len=(buf[4]>MAX_INFO_LEN)?buf[4]:MAX_INFO_LEN; memcpy(str,&buf[8],real_len);str[real_len]=0; printf("%d.%d <-- %s\n",adapterid, targetid, str); } } } }
Листинг 12. Последовательный опрос портов на предмет наличия подключенных к ним устройств.
Результат работы программы на компьютере автора выглядит так (обратите внимание, что адреса устройств, подключенных к виртуальной SCSI-шине, созданной драйвером ASPI, могут и не соответствовать их физическим адресам; в данном случае, привод PHILIPS висящий на физическом IDE-порту с номером 0, попал на виртуальный порт с номером 1, поскольку нулевой порт занят драйвером Virtual Clone CD, при удалении последнего из системы, соответствие виртуальных и физических адресов полностью восстанавливается, однако ручаться за это нельзя):
0.0 <-- ELBY DVD-ROM 1.0 1.0 <-- IBM-DTLA-307015 TX2O 1.1 <-- PHILIPS CDRW2412A P1.55VO1214DM10574 2.0 <-- ST380011A 3.06 2.1 <-- TEAC CD-W552E 1.09 3.0 <-- AXV CD/DVD-ROM 2.2a 3.1 <-- AXV CD/DVD-ROM 2.2a 3.2 <-- AXV CD/DVD-ROM 2.2a
Листинг 13. Устройства, подключенные к компьютеру автора. Первая слева цифра - adapter ID, следующая за ней - target ID.
Другое немаловажное достоинство ASPI-интерфейса по сравнению с SPTI состоит в поддержке асинхронного режима обработки запросов. Отдав запрос на чтение такого-то количество секторов, вы можете продолжить выполнение своей программы, не дожидаясь, пока процесс чтения секторов полностью не завершится. Конечно, для достижения аналогичного результата при использовании интерфейса SPTI достаточно всего лишь создать еще один поток, но... это уже не так элегантно и красиво.
#include "scsidefs.h" #include "wnaspi32.h" void ASPI32Post (LPVOID); #define F_NAME "raw.sector.dat" /* ASPI SRB packet length */ #define ASPI_SRB_LEN 0x100 #define RAW_READ_CM 0xBE #define WHATS_READ 0xF8 // Sync & All Headers & User Data + EDC/ECC #define PACKET_LEN 2352 //#define WHATS_READ 0x10 // User Data //#define PACKET_LEN 2048 #define MY_CMD RAW_READ_CMD HANDLE hEvent; //-[DWORD READ_RAW_SECTOR_FROM_CD]--------------------------------------------- // функция читает один или несколько секторов с CD-ROM в "сыром" // (RAW) виде, согласно переданным флагам // // ARG: // adapter_id - номер шины (0 - primary, 1 - secondary) // read_id - номер устройства на шине (0 - master, 1 - slaeyer) // buf - буфер, куда читать // buf_len - размер буфера в байтах // StartSector - с какого сектора читать, считая от нуля // N_SECTOR - сколько секторов читать \ // flags - что читать (см. спецификацию на ATAPI) // // RET: // - ничего не возвращает // // NOTE: // - функция возвращает управления до завершения выполнения запроса, // поэтому на момент выхода из нее, содержимое буфера с данными еще // пусто, и реально он заполняется только при вызове функции // ASPI32Post (вы можете модифицировать ее по своему усмотрению) // для сигнализации о завершении операции рекомендуется использовать // события (Event) // // - функция работает и под 9x/ME/NT/W2K/XP и _не_ требует для себя прав // администратора. Однако ASPI-драйвер должен быть установлен. //----------------------------------------------------------------------------- READ_RAW_SECTOR_FROM_CD(int adapter_id,int read_id,char *buf,int buf_len, int StartSector,int N_SECTOR,int flags) { PSRB_ExecSCSICmd SRB; DWORD ASPI32Status; // выделяем память для SRB-запроса SRB = malloc(ASPI_SRB_LEN); memset(SRB, 0, ASPI_SRB_LEN); // ПОДГОТОВКА SRB-блока SRB->SRB_Cmd = SC_EXEC_SCSI_CMD; // выполнить SCSI команду SRB->SRB_HaId = adapter_id; // ID адаптера SRB->SRB_Flags = SRB_DIR_IN|SRB_POSTING; // асинхр. чтение данных SRB->SRB_Target = read_id; // ID устройства SRB->SRB_BufPointer= buf; // сюда читаются данные SRB->SRB_BufLen = buf_len; // длина буфера SRB->SRB_SenseLen = SENSE_LEN; // длина SENSE-буфера SRB->SRB_CDBLen = 12; // размер ATAPI-пакета SRB->CDBByte [0] = MY_CMD; // ATAI-команда SRB->CDBByte [1] = 0x0; // формат CD - любой // номер первого сектора SRB->CDBByte [2] = HIBYTE(HIWORD(StartSector)); SRB->CDBByte [3] = LOBYTE(HIWORD(StartSector)); SRB->CDBByte [4] = HIBYTE(LOWORD(StartSector)); SRB->CDBByte [5] = LOBYTE(LOWORD(StartSector)); // кол-во читаемых секторов SRB->CDBByte [6] = LOBYTE(HIWORD(N_SECTOR)); SRB->CDBByte [7] = HIBYTE(LOWORD(N_SECTOR)); SRB->CDBByte [8] = LOBYTE(LOWORD(N_SECTOR)); SRB->CDBByte [9] = flags // что читать? SRB->CDBByte [10] = 0; // данные подканала не нужны SRB->CDBByte [11] = 0; // reserverd // адрес процедуры, которая будет получать уведомления SRB->SRB_PostProc = (void *) ASPI32Post; // посылаем SRB-запрос устройству SendASPI32Command(SRB); // возвращаемся из функции _до_ завершения выполнения запроса return 0; } //---------------------------------------------------------------------------- // эта callback-функция вызывается самим ASPI и получает управление при // при завершении выполнения запроса или же при возникновении ошибки. // в качестве параметра она получает указатель на экземпляр структуры // PSRB_ExecSCSICmd, содержащей всю необходимую информацию (статус, указатель // на буфер и т.д.) //---------------------------------------------------------------------------- void ASPI32Post (void *Srb) { FILE *f; // наш запрос выполнен успешно? if ((((PSRB_ExecSCSICmd) Srb)->SRB_Status) == SS_COMP) { // ЭТОТ КОД ВЫ МОЖЕТЕ МОДИФИЦИРОВАТЬ ПО СВОЕМУ УСМОТРЕНИЮ //------------------------------------------------------- // записывает содержимое сектора в файл // внимание! PSRB_ExecSCSICmd) Srb)->SRB_BufLen содержит не актуальную // длину прочитанных данных, а размер самого буфера. если количество // байт, возвращенных устройством, окажется меньше размеров буфера, то // его хвост будет содержать мусор! здесь мы используем поле SRB_BufLen // только потому, что при вызове функции SendASPI32Command тщательно // следим за соответствием размера буфера количеству возвращаемой нам // информации if (f=fopen(F_NAME, "w")) { // записывает сектор в файл fwrite(((PSRB_ExecSCSICmd) Srb)->SRB_BufPointer,1, ((PSRB_ExecSCSICmd) Srb)->SRB_BufLen, f); fclose(f); } // кукарекаем и "размораживаем" поток, давая понять, что процедура // чтения закончилась MessageBeep(0); SetEvent(hEvent); //-------------------------------------------------------- } } main(int argc, char **argv) { void *p; int buf_len, TIME_OUT = 4000; if (argc<5) { fprintf(stderr,"USAGE:\n\tRAW.CD.READ.EXE adapter_id"\ ", read_id, StartSector, n_sec\n"); return 0; } // вычисляем длину буфера и выделяем для него память // ВНИМАНИЕ: таким образом можно юзать только до 64КБ // если же вам требуются буфера больших объемов, // используйте функцию GetASPI32Buffer buf_len = PACKET_LEN*atol(argv[4]); p = malloc(buf_len); // создаем событие if ((hEvent = CreateEvent(NULL,FALSE,FALSE,NULL)) == NULL) return -1; // читаем один или несколько секторов с CD READ_RAW_SECTOR_FROM_CD(atol(argv[1]), atol(argv[2]),p,buf_len, atol(argv[3]), atol(argv[4]),WHATS_READ); // ждем завершения выполнения операции WaitForSingleObject(hEvent, TIME_OUT); return 0; }
Листинг 14. Демонстрационный пример программы, осуществляющий сырое чтение сектора с 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 на самом деле никакая не директория, а символическая ссылка на директорию \?? или, если так угодно, ее ярлык):
Scsi0: SymbolicLink - \Device\Ide\IdePort0 Scsi1: SymbolicLink - \Device\Ide\IdePort1 Scsi2: SymbolicLink - \Device\Scsi\axsaki1
Листинг 15. Взаимосвязь логических 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-порт заканчиваются неизменной ошибкой. Следующий код наотрез отказывается работать. Почему?
// получаем дескриптор SCSI-порта hCD = CreateFile ("\\\\.\\SCSI1", GENERIC_WRITE|GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,0,OPEN_EXISTING,0,0); // ФОРМИРУЕМ SRB-блок ... // ОТПРАВЛЯЕМ SRB-блок непосредственно на SCSI-порт status = DeviceIoControl(hCD, IOCTL_SCSI_PASS_THROUGH_DIRECT, &srb, sizeof(SCSI_PASS_THROUGH), &srb, 0, &returned, FALSE);
Листинг 17. Пример неправильной работы с виртуальным 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" ("Если класс-драйвер для целевого устройства установлен, управляющие запросы должны посылаться класс-драйверу, но не самому порту устройства. Таким образом, приложения могут посылать непосредственные запросы драйверу системного порта для целевых логических устройств, только если класс-драйвер для соответствующего типа устройств, подключенных к данному 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. Это может выглядеть, например, так:
h = CreateFile("\\\\.\\SCSI1:", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,OPEN_EXISTING, 0, NULL);
Листинг 18. Открытие 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, определенная следующим образом:
typedef struct _SRB_IO_CONTROL { ULONG HeaderLength; // sizeof(SRB_IO_CONTROL) UCHAR Signature[8]; // сигнатура мини-драйвера ULONG Timeout; // макс. время ожидания выполнения запроса в сек ULONG ControlCode; // код команды ULONG ReturnCode; // здесь нам вернут статус завершения ULONG Length; // длина всего передаваемого буфера целиком } SRB_IO_CONTROL, *PSRB_IO_CONTROL;
Листинг 19. Назначение полей структуры 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 при тщательном его просмотре:
// // SMART support in atapi // #define IOCTL_SCSI_MINIPORT_SMART_VERSION ((FILE_DEVICE_SCSI<<16)+0x0500) #define IOCTL_SCSI_MINIPORT_IDENTIFY ((FILE_DEVICE_SCSI<<16)+0x0501) #define IOCTL_SCSI_MINIPORT_READ_SMART_ATTRIBS ((FILE_DEVICE_SCSI<<16)+0x0502) #define IOCTL_SCSI_MINIPORT_READ_SMART_THRESHOLDS ((FILE_DEVICE_SCSI<<16)+0x0503) #define IOCTL_SCSI_MINIPORT_ENABLE_SMART ((FILE_DEVICE_SCSI<<16)+0x0504) #define IOCTL_SCSI_MINIPORT_DISABLE_SMART ((FILE_DEVICE_SCSI<<16)+0x0505) #define IOCTL_SCSI_MINIPORT_RETURN_STATUS ((FILE_DEVICE_SCSI<<16)+0x0506) #define IOCTL_SCSI_MINIPORT_ENABLE_DISABLE_AUTOSAVE ((FILE_DEVICE_SCSI<<16)+0x0507) #define IOCTL_SCSI_MINIPORT_SAVE_ATTRIBUTE_VALUES ((FILE_DEVICE_SCSI<<16)+0x0508) #define IOCTL_SCSI_MINIPORT_EXECUTE_OFFLINE_DIAGS ((FILE_DEVICE_SCSI<<16)+0x0509) #define IOCTL_SCSI_MINIPORT_ENABLE_DISABLE_AUTO_OFFLINE ((FILE_DEVICE_SCSI<<16)+0x050a)
Листинг 20. Команды управления 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;
Листинг 21. Структура SENDCMDINPARAMS, дающая прямой доступ к IDE-регистрам.
То есть, входной буфер функции DeviceIoControl должен выглядеть так:
Рисунок 7. Структура входного буфера функции DeviceIoControl для управления драйвером мини порта под Windows 9x/NT.
Первый элемент структуры - cBufferSize, содержащий размер bBuffer'a слишком очевиден и не интересен. А вот структура IDREGS представляет собой настоящий клад, вот взгляните сами (только не упадите со стула, ибо потрясение будет столь же острым, сколь и глубоким):
typedef struct _IDEREGS { BYTE bFeaturesReg; // IDE Features-регистр BYTE bSectorCountReg; // IDE SectorCount-регистр BYTE bSectorNumberReg; // IDE SectorNumber-регистр BYTE bCylLowReg; // IDE CylLowReg-регистр BYTE bCylHighReg; // IDE CylHighReg-регистр BYTE bDriveHeadReg; // IDE DriveHead-регистр BYTE bCommandReg; // командный регистр BYTE bReserved; // зарезервировано } IDEREGS, *PIDEREGS, *LPIDEREGS;
Листинг 22. Структура IDEREGS, предоставляющая низкоуровневый доступ к IDE-регистрам.
Всякий, кто читал спецификацию на ATA/ATPI и хоть однажды сталкивался с программированием устройств с интерфейсом IDE, должен немедленно узнать до боли знакомые регистры Command, Drive/Head, Cylinder High, Cylinder Low, Sector Number, Sector Count и Features, правда, в структуре IDEREGS они перечислены почему-то в обратном порядке, но это уже мелочи реализации. Главное, что с помощью этой структуры мы можем вытворять с приводом все мыслимые и немыслимые фокусы, на которые только способно железо. Даже не верится, что в подсистеме безопасности существует такая дыра размерами со слонопотама. И это при том, что для управления мини портом наличие прав администратора совсем не обязательно! Дрожа и подпрыгивая от нетерпения, наскоро заполняем оставшиеся поля структуры SENDCMDINPARAMS, как-то: bDriveNumber - физический номер привода, считая от нуля и буфер для передачи данных (но ведь мы пока не собираемся записывать никаких данных на диск, верно? Вот и оставим это поле пустым).
Увы! При попытке "скормить" приводу команду, отличную от команд семейства 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 и посмотрим, что мы можем сделать.
.text:00013714 aScsidisk db 'SCSIDISK',0 ; DATA XREF: SCSI_MINIPORT+CCvo ; вот она наша сигнатура ^^^^^^^^ ; .text:000137DF .text:000137DF loc_137DF: ; CODE XREF: SCSI_MINIPORT+B5^j .text:000137DF mov [edi], ebx .text:000137E1 mov eax, [ebx+18h] .text:000137E4 push 8 ; длина сравниваемой строки .text:000137E6 add eax, 4 .text:000137E9 push offset aScsidisk ;эталонная сигнатура .text:000137EE push eax ; сигнатура, переданная приложением .text:000137EF call ds:RtlCompareMemory ; сигнатуры совпадают? .text:000137F5 cmp eax, 8 .text:000137F8 jnz loc_13898 ; нет, не совпадают, сваливаем отсюда .text:000137F8 .text:000137FE mov esi,[ebx+18h] .text:00013801 mov eax,[esi+10h] ; извлекаем ControlCode .text:00013804 cmp eax, 1B0500h ; IOCTL_SCSI_MINIPORT_SMART_VERSION .text:00013809 jz loc_1389F ; обработка ...SMART_VERSION .text:0001380F mov ecx, 1B0501h ; IOCTL_SCSI_MINIPORT_IDENTIFY .text:00013814 cmp eax, ecx .text:00013816 jz short loc_1382D ; обработка ...IDENTIFY .text:00013818 jbe short loc_13898 ; IF ControlCode < IDENTIFY THEN на выход .text:0001381A cmp eax, 1B050Ah ; IOCTL_SCSI_MINIPORT_ENABLE_DISABLE... .text:0001381F ja short loc_13898 ; IF ControlCode > ENABLE_DISAB... на выход .text:00013821 push ebx .text:00013822 push edi .text:00013823 call sub_12412 ; обрабатываем остальные SMART-команды .text:00013828 jmp loc_1393E .text:0001382D ; ----------------------------------------------------------- .text:00012412 sub_12412 proc near ; CODE XREF: SCSI_MINIPORT+106vp ... .text:00012433 cmp [ebp+var_1E], 0B0h ; SMART-command .text:00012437 jnz loc_12633 ; если это не SMART, то выходим .text:00012437 ; отсюда начинаются проверки .text:0001243D movzx eax, [ebp+var_1C] .text:00012441 mov eax, [ebx+eax*4+0B0h] ; загружаем Drive/Head-регистр в EAX .text:00012448 test al, 1 ; сравниваем младший бит AL с единицей .text:0001244A jz loc_1262F ; если младший бит равен нулю, выходим .text:00012450 test al, 2 ; сравниваем следующий бит AL с единицей .text:00012452 jnz loc_1262F ; если он не равен нулю, то выходим .text:00012458 mov al, [ebp+var_24] ; загружаем Feature-регистр в AL .text:0001245B cmp al, 0D0h ; это SMART READ DATA? .text:0001245D mov [ebx+0CCh], al .text:00012463 jz loc_12523 ; если да, то переходим к его обработке .text:00012469 cmp al, 0D1h ; это Obsolete? .text:0001246B jz loc_12523 ; если да, то переходим к его обработке .text:00012471 cmp al, 0D8h ; это SMART ENABLE OPERATIONS? .text:00012473 jz short loc_12491 ; если да, то переходим к его обработке .text:00012475 cmp al, 0D9h ; это SMART DISABLE OPERATIONS? .text:00012477 jz short loc_12491 ; если да, то переходим к его обработке .text:00012479 cmp al, 0DA ; это SMART RETURN STATUS? .text:0001247B jz short loc_12491 ; если да, то переходим к его обработке .text:0001247D cmp al, 0D2h ; это SMART ENBL/DSBL ATTRIBUTE AUTOSAVE? .text:0001247D cmp al, 0D2h ; процессор, ты не ошибся в натуре?! .text:0001247F jz short loc_12491 ; если да, то переходим к его обработке .text:00012481 cmp al, 0D4h ; это SMART EXECUTE OFF-LINE IMMEDIATE? .text:00012483 jz short loc_12491 ; если да, то переходим к его обработке .text:00012485 cmp al, 0D3h ; это SMART SAVE ATTRIBUTE VALUES? .text:00012487 jz short loc_12491 ; если да, то переходим к его обработке .text:00012489 cmp al, 0DBh ; это SMART ENABLE OPERATIONS? .text:0001248B jnz loc_12633 ; если нет, то сваливаем .text:00012491 .text:00012491 loc_12491: ; CODE XREF: sub_12412+61^j .text:00012491 ; отсюда начинается обработка команд .text:00012491 ; .text:00012491 push 1 .text:00012493 pop eax .text:00012494 cmp ds:0FFDF02C0h, eax .text:0001249A jnz short loc_124A5 .text:0001249C cmp dword ptr [ebx+4], 640h .text:000124A3 jz short loc_124A7 .text:000124A5 .text:000124A5 loc_124A5: ; CODE XREF: sub_12412+88^j .text:000124A5 xor eax, eax .text:000124A7 .text:000124A7 loc_124A7: ; CODE XREF: sub_12412+91^j .text:000124A7 ; отсюда начинается запись в порт! .text:000124A7 ; .text:000124A7 mov esi, ds:WRITE_PORT_UCHAR .text:000124AD test al, al .text:000124AF jz short loc_124C0 .text:000124B1 mov al, [ebp+var_1C] .text:000124B4 shr al, 1 .text:000124B6 and al, 1 .text:000124B8 push eax .text:000124B9 push 432h .text:000124BE call esi ; WRITE_PORT_UCHAR
Листинг 23. Фрагмент дизассемблерного листинга драйвера ATAPI.SYS, отвечающий за проверку передаваемых IDE-команд на соответствие принадлежности к "белому" списку.
Таким образом, чтобы разрешить драйверу отправлять IDE-приводу любые команды, мы должны удалить условный переход, расположенный по адресу 0х12437 (в листинге он выделен жирным шрифтом) на безусловный переход, передающий управление на команду записи по адресу 0x12491. Только не забудьте после модификации драйвера скорректировать его контрольную сумму, что можно сделать, например, с помощью утилиты EDITBIN.EXE, входящей в состав Microsoft Visual Studio, иначе Windows NT наотрез откажется загружать такой хакнутый драйвер.
Разумеется, такую операцию допустимо проделывать только на своем собственном драйвере, поскольку всем остальным вряд ли понравится дыра, проделанная в системе безопасности! К тому же, распространение модифицированного ATAPI.SYS вопиющим образом нарушает авторское право самой Microsoft со всеми вытекающими отсюда последствиями. Тем не менее, ваше приложение может безбоязненно патчить ATAPI.SYS непосредственно на компьютерах пользователей, естественно, запрашивая у них подтверждение на правомерность такой операции (или, на худой конец, просто упоминая этот аспект в сопроводительной документации).
В любом случае, данный способ взаимодействия с приводом не стоит сбрасывать со счетов, поскольку это значительно усложняет взлом защиты, созданной на его основе. Ведь далеко не все хакеры осведомлены о тонкостях управления мини портом и потому с вероятностью близкой к единице сядут в глубокую лужу, если, конечно, не упадут в яму информационного вакуума.
Пример программы, приведенной ниже, как раз и демонстрирует передачу ATA-команд IDE-приводу через драйвер мини порта.
int ATAPI_MINIPORT_DEMO(void) { int a; HANDLE h; char *buf; int LU = 0; DWORD returned; int controller; char ScsiPort [16]; char buffer [sizeof (SRB_IO_CONTROL) + SENDIDLENGTH]; SRB_IO_CONTROL *p = (SRB_IO_CONTROL *) buffer; SENDCMDINPARAMS *pin = (SENDCMDINPARAMS *) (buffer + sizeof (SRB_IO_CONTROL)); // перебираем оба IDE-контроллера в цикле for (controller = 0; controller < 2; controller++) { // формируем ScsiPort для каждого из котроллеров sprintf (ScsiPort, "\\\\.\\Scsi%d:", controller); // открываем соответствующий ScsiPort h= CreateFile (ScsiPort,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0,0); if (h == INVALID_HANDLE_VALUE) { // ЕСЛИ ПРОИЗОШЛА ОШИБКА - СВАЛИВАЕМ printf("-ERR:Unable to open ScsiPort%d\n",controller);return -1; } // перебираем оба устройства на каждом из IDE-контроллеров for (LU = 0; LU < 2; LU++) { // инициализируем входной буфер memset (buffer, 0, sizeof (buffer)); // ПОДГОТАВЛИВАЕМ СТРУКТУРУ SRB_IO_CONTROL, // предназначенную для драйвера мини порта p -> Timeout = 10000; // ждать до черта p -> Length = SENDIDLENGTH; // макс. длина p -> HeaderLength = sizeof (SRB_IO_CONTROL); // размер заголовка p -> ControlCode = IOCTL_SCSI_MINIPORT_IDENTIFY; // ^^^ код команды, посылаемой драйверу // сигнатура. для ATAPI.SYS это "SCSIDISK" strncpy ((char *) p -> Signature, "SCSIDISK", 8); // ПОДГОТАВЛИВАЕМ СТРУКТУРУ SENDCMDINPARAMS, // содержащую ATA-команды, передаваемые IDE-приводу pin -> bDriveNumber = LU; pin -> irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY; // ПОСЫЛАЕМ УПРАВЛЯЮЩИЙ ЗАПРОС ДРАЙВЕРУ МИНИ ПОРТА if (DeviceIoControl (h, IOCTL_SCSI_MINIPORT, buffer, sizeof (SRB_IO_CONTROL) + sizeof (SENDCMDINPARAMS) - 1, buffer, sizeof (SRB_IO_CONTROL) + SENDIDLENGTH, &returned, 0)) if (buffer[98]!=0) {// в ответ нам возвращается строка с идентификационным // именем IDE-привода, которую мы и выводим на экран for (a = 98; a < 136; a+=2 ) printf("%c%c",buffer[a+1],buffer[a]); printf("\n"); } } CloseHandle (h); // закрыть дескриптор данного SCSI мини порта } return 0; }
Листинг 24. Пример программы, демонстрирующий технику взаимодействия со 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 следующие строки, и мы сможем с его помощью читать весь диапазон портов ввода/вывода:
if (nPort >= pLDI->PortCount || (nPort + DataBufferSize) > pLDI->PortCount || (((ULONG_PTR)pLDI->PortBase + nPort) & (DataBufferSize - 1)) != 0) { return STATUS_ACCESS_VIOLATION; // Illegal port number }
Листинг 25. Проверка адресов портов, к которым происходит обращение на принадлежность к диапазону портов виртуального устройства, созданного драйверов. Для того, чтобы иметь возможность обращаться к любым портам, эти строки следует удалить.
Также следует обратить внимание на то, что драйвер ожидает получить не абсолютный адрес порта, а относительный, отсчитываемый от адреса базового порта, задаваемого при добавлении виртуального устройства в систему. Взгляните на следующие строки:
case IOCTL_GPD_READ_PORT_UCHAR: *(PUCHAR)pIOBuffer=READ_PORT_UCHAR((PUCHAR)((ULONG_PTR)pLDI->PortBase+nPort)); break;
Листинг 26. Вычисление действительного адреса порта через базовый.
Очевидно, что текст, выделенный жирным шрифтом, следует удалить - в этом случае драйвер сможет оперировать абсолютными, а не относительными портами, и мы без труда сможем прорваться к любому порту системы! Причем, если мы перенесем модифицированный нами драйвер на Windows 9x, наши приложения будут работать в обеих операционных системах и останутся зависимыми разве что от самого оборудования. Но, с другой стороны, всякий, кто стремится дорваться до портов, должен отдавать себе отчет в том, зачем это ему нужно и какие сложности ему придется преодолеть.
Конечно, поскольку возможность бесконтрольного доступа ко всем имеющимся портам ввода/вывода существенно ослабляет и без того уязвимую операционную систему, нелишним будет ввести в драйвер кое-какие дополнительные проверки и ограничения. Скажем, запретить прямое обращение ко всему, что не является CD-ROM приводом. В противном случае, если ваша программа получит сколь-нибудь широкое распространение, толпы вандалов ринуться писать зловредных троянских коней, военная мощь которых окажется практически безграничной, и совладеть с ними будет очень трудно. С другой стороны, за все время существования интерфейса ASPI не было зафиксировано ни одной попытки использовать его для деструктивных целей, хотя такая возможность до сих пор имеется.
Другой недостаток предложенного способа управления устройствами заключается в его катастрофически низком быстродействии. Вызовы DeviceIoControl распадаются на десятки тысяч машинных команд (!), "благодаря" чему время обработки запросов становится слишком большим, а измерение физических характеристик спиральной дорожки (если мы действительно захотим эти характеристики измерять) - неточным. К тому же, функция DeviceIoControl громоздка и неизящна, а самое неприятное в том, что на нее очень легко поставить BreakPoint и потому участь такой защиты заранее предрешена. Во времена MS-DOS, когда взаимодействие с оборудованием осуществлялось посредством машинных команд IN и OUT, локализовать защитный код в теле программы было значительно сложнее, а управлять устройствами с их помощью существенно легче и - главное - намного производительнее.
Считается, что в среде Windows NT прямое обращение к портам возможно только на уровне ядра, а приложения вынуждены общаться с портами через высокоуровневый интерфейс, предоставляемый драйвером. И хотя этот интерфейс может быть полностью прозрачным (драйверу ничего не стоит перехватить исключение, возникающее при попытке чтения/записи в порт с прикладного уровня, и выполнить этот запрос самостоятельно), это все-таки не то...
На самом деле, выполнять команды IN/OUT можно и на прикладном уровне, правда не без помощи недокументированных возможностей операционной системы и документированных, но малоизвестных особенностей реализации защищенного режима работы в процессорах Intel 80386+. Вот с процессоров мы, пожалуй, и начнем. Давайте откроем "Instruction Set Reference" и посмотрим, как "устроена" машинная команда OUT. Среди прочей полезной информации мы найдем и ее псевдокод, которой выглядит приблизительно так:
if ((PE == 1) && ((CPL > IOPL) || (VM == 1))) { /* Protected mode with CPL > IOPL or virtual-8086 mode */ if (Any I/O Permission Bit for I/O port being accessed == 1) #GP(0); /* I/O operation is not allowed */ else DEST <- SRC; /* Writes to selected I/O port */ } else { /* Real Mode or Protected Mode with CPL <= IOPL */ DEST <- SRC; /* Writes to selected I/O port */ }
Листинг 27. Псевдокод инструкции 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, тем самым запрещая доступ к портам с прикладного уровня, а единичное - активизирует ранее переданную карту ввода/вывода.
Пример, приведенный ниже, все это, собственно, и демонстрирует:
/*---------------------------------------------------------------------------- * * ДРАЙВЕР. РАЗРЕШАЕТ ВЫПОЛНЕНИЕ * МАШИННЫХ КОМАНД IN/OUT НА ПРИКЛАДНОМ УРОВНЕ * =========================================== * * ВНИМАНИЕ! Я, Крис Касперски, не имею никакого отношения к этой программе! * ------------------------------------------------------------------------- * * GIVEIO.SYS: by Dale Roberts * КОМПИЛЯЦИЯ: Используйте средство DDK BUILD * НАЗНАЧЕНИЕ: Предоставить доступ к прямому в/в процессам режима пользователя ----------------------------------------------------------------------------*/ #include <ntddk.h> /* Имя нашего драйвера устройства */ #define DEVICE_NAME_STRING L"giveio" // Структура" IOPM. это просто массив байт размером 0x2000, содержащий // 8К * 8 бит == 64К бит IOPM, которые покрывают всё 64 Кб адресное // пространство ввода/вывода x86 процессоров. // Каждый нулевой бит предоставляет доступ к соответствующему порту // для user-mode процесса; каждый единичный бит запрещает доступ к в/в // через соответствующий порт #define IOPM_SIZE 0x2000 typedef UCHAR IOPM[IOPM_SIZE]; // массив нулей, который копируется в настоящую IOPM в TSS посредством // вызова dsKe386SetIoAccessMap() // необходима память выделяется во время загрузки драйвера IOPM *IOPM_local = 0; // это две полностью недокументированные функции, которые мы используем, // чтобы дать доступ к в/в вызывающему процессу // * Ke386IoSetAccessMap() - копирует переданную карту в/в в TSS // * Ke386IoSetAccessProcess() - изменяет указатель смещения IOPM, после // чего только что скопированная карта // начинает использоваться void Ke386SetIoAccessMap(int, IOPM *); void Ke386QueryIoAccessMap(int, IOPM *); void Ke386IoSetAccessProcess(PEPROCESS, int); // ОСВОБОДИТЬ ВСЕ ВЫДЕЛЕННЫЕ РАНЕЕ ОБЪЕКТЫ VOID GiveioUnload(IN PDRIVER_OBJECT DriverObject) { UNICODE_STRING uniDOSString; WCHAR DOSNameBuffer[] = L"\\DosDevices\\" DEVICE_NAME_STRING; if(IOPM_local) MmFreeNonCachedMemory(IOPM_local, sizeof(IOPM)); RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); IoDeleteSymbolicLink (&uniDOSString); IoDeleteDevice(DriverObject->DeviceObject); } //---------------------------------------------------------------------------- // устанавливаем IOPM (карту разрешения в/в) вызывающего процесса так, чтобы // ему предоставлялся полный доступ к в/в. Массив IOPM_local[] содержит // одни нули, соответственно, IOPM обнулится. // Если OnFlag == 1, процессу предоставляется доступ к в/в; // Если он равен 0, доступ запрещается. //---------------------------------------------------------------------------- VOID SetIOPermissionMap(int OnFlag) { Ke386IoSetAccessProcess(PsGetCurrentProcess(), OnFlag); Ke386SetIoAccessMap(1, IOPM_local); } void GiveIO(void) { SetIOPermissionMap(1); } //---------------------------------------------------------------------------- // cлужебный обработчик для user-mode вызова CreateProcess(). // эта функция введена в таблицу вызовов функций объекта драйвера с помощью // DriverEntry(). когда user-mode приложение вызывает CreateFile(), эта // функция получает управление всё ещё в контексте вызвавшего приложения, // но с CPL (текущий уровень привилегий процессора) установленым в 0. // Это позволяет производить операции возможные только в kernel mode. // GiveIO вызывается для предоставления вызывающему процессу доступа к в/в. // Все, что приложение режима пользователя, которому нужен доступ к в/в, // должно сделать - это открыть данное устройство, используя CreateFile() // Никаких других действий не нужно. //---------------------------------------------------------------------------- NTSTATUS GiveioCreateDispatch(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp) { GiveIO(); // give the calling process I/O access Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } //---------------------------------------------------------------------------- // процедура входа драйвера. эта процедура вызывается только раз после // загрузки драйвера в память. она выделяет необходимые ресурсы для работы // драйвера. в нашем случае она выделяет память для массива IOPM и создаёт // устройство, которое может открыть приложение режима пользователя. // она также создаёт символическую ссылку на драйвер устройства. // что позволяет user-mode приложению получить доступ к нашему драйверу // используя \\.\giveio нотацию. //---------------------------------------------------------------------------- NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath) { NTSTATUS status; PDEVICE_OBJECT deviceObject; UNICODE_STRING uniNameString, uniDOSString; WCHAR NameBuffer[] = L"\\Device\\" DEVICE_NAME_STRING; WCHAR DOSNameBuffer[] = L"\\DosDevices\\" DEVICE_NAME_STRING; // выделим буфер для локальной IOPM и обнулим его IOPM_local = MmAllocateNonCachedMemory(sizeof(IOPM)); if(IOPM_local == 0) return STATUS_INSUFFICIENT_RESOURCES; RtlZeroMemory(IOPM_local, sizeof(IOPM)); // инициализируем драйвер устройства и объект устройства (device object) RtlInitUnicodeString(&uniNameString, NameBuffer); RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); status = IoCreateDevice(DriverObject, 0, &uniNameString,FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject); if(!NT_SUCCESS(status)) return status; status = IoCreateSymbolicLink (&uniDOSString, &uniNameString); if (!NT_SUCCESS(status)) return status; // инициализируем точки входа драйвера в объекте драйвера // всё, что нам нужно, это операции создания (Create) и выгрузки (Unload) DriverObject->MajorFunction[IRP_MJ_CREATE] = GiveioCreateDispatch; DriverObject->DriverUnload = GiveioUnload; return STATUS_SUCCESS; }
Листинг 28. Демонстрационный пример драйвера, открывающего прямой доступ к портам ввода/вывода на прикладном уровне.
/*---------------------------------------------------------------------------- * * ДЕМОНСТРАЦИЯ ВЫЗОВА IN/OUT НА ПРИКЛАДНОМ УРОВНЕ * (внимание! драйвер GIVEIO.SYS должен быть предварительно загружен!) * ==================================================================== * * ВНИМАНИЕ! Я, Крис Касперски, не имею никакого отношения к этой программе! * ------------------------------------------------------------------------- * * GIVEIO.TST: by Dale Roberts * НАЗНАЧЕНИЕ: Тестирование драйвера GIVEIO, производя какой-нибудь в/в. * : (мы обращаемся к внутреннему динамику PC) ----------------------------------------------------------------------------*/ #include <stdio.h> #include <windows.h> #include <math.h> #include <conio.h> typedef struct { short int pitch; short int duration; } NOTE; // ТАБЛИЦА НОТ NOTE notes[] = {{14, 500}, {16, 500}, {12, 500}, {0, 500}, {7, 1000}}; // УСТАНОВКА ЧАСТОТЫ ДИНАМИКА PC В ГЕРЦАХ // ДИНАМИК УПРАВЛЯЕТСЯ ТАЙМЕРОМ INTEL 8253/8254 С ПОРТАМИ В/В 0X40-0X43 void setfreq(int hz) { hz = 1193180 / hz; // базовая частота таймера 1.19MHz _outp(0x43, 0xb6); // Выбор таймера 2, операция записи,режим 3 _outp(0x42, hz); // устанавливаем делитель частоты _outp(0x42, hz >> 8); // старший байт делителя } //----------------------------------------------------------------------------- // длительность ноты задается в долях частоты 400 Hz, число 12 задает масштаб // Cпикер управляется через порт 0x61. Установка двух младших битов разрешает // канал 2 таймера 8253/8254 и включает динамик. //----------------------------------------------------------------------------- void playnote(NOTE note) { _outp(0x61, _inp(0x61) | 0x03); // включаем динамик setfreq((int)(400 * pow(2, note.pitch / 12.0))); Sleep(note.duration); _outp(0x61, _inp(0x61) & ~0x03); // выключаем } //---------------------------------------------------------------------------- // открытие и закрытие устройства GIVEIO, что дает нам прямой доступ к в/в; // потом пытаемся проиграть музыку //---------------------------------------------------------------------------- int main() { int i; HANDLE h; h = CreateFile("\\\\.\\giveio", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(h == INVALID_HANDLE_VALUE) { printf("Couldn't access giveio device\n"); return -1; } CloseHandle(h); for(i = 0; i < sizeof(notes)/sizeof(int); ++i) playnote(notes[i]); return 0; }
Листинг 29. Пример ввода/вывода в порт с прикладного уровня.
Теперь поговорим о том, как данный способ взаимодействия с портами ввода/вывода может быть использован на благо защитных механизмов. Допустим, наша защита привязывается к физическому дефекту поверхности лазерного диска. Тогда все, что нам надо - попытаться как можно незаметнее прочитать этот сектор: если он действительно не читается, диск можно считать оригинальным и наоборот. Прямое управление приводом через порты ввода/вывода с вероятностью близкой к единице останется незамеченным даже бывалыми хакерами, потому такой вариант им попросту не придет в голову! Единственное, о чем следует позаботиться - не дать обнаружить защитный код по перекрестным ссылкам, оставленных ругательным сообщением, которое выводится на экран в том случае, если диск признан пиратским.
Тем не менее, матерых хакеров на такую наживку не возьмешь! Злорадно ухмыльнувшись, они просто поставят точку останова на ввод/вывод в порты 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 через функции прерывания 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.
BOOL FAR PASCAL MSCDEX_ReadSector(BYTE bDrive, DWORD StartSector, LPBYTE RMlpBuffer) { RMCS callStruct; BOOL fResult; // Prepare DPMI Simulate Real Mode Interrupt call structure with // the register values used to make the MSCDEX Absolute read call. // Then, call MSCDEX using DPMI and check for errors in both the DPMI // call and the MSCDEX call BuildRMCS (&callStruct); callStruct.eax = 0x1508; // MSCDEX функция "ABSOLUTE READ" callStruct.ebx = LOWORD(RMlpBuffer); // смещение буфера для чтения сектора callStruct.es = HIWORD(RMlpBuffer); // сегмент буфера для чтения сектора callStruct.ecx = bDrive; // буква привода 0=A, 1=B, 2=C и т.д. callStruct.edx = 1; // читаем один сектор callStruct.esi = HIWORD(StartSector); // номер читаемого сектора(старшее слово) callStruct.edi = LOWORD(StartSector); // номер читаемого сектора(младшее слово) // вызываем прерывание реального режима if (fResult = SimulateRM_Int (0x2F, &callStruct)) fResult = !(callStruct.wFlags & CARRY_FLAG); return fResult; } BOOL FAR PASCAL SimulateRM_Int(BYTE bIntNum, LPRMCS lpCallStruct) { BOOL fRetVal = FALSE; // Assume failure __asm { push di ; сохраняем регистр DI mov ax, 0300h ; DPMI Simulate Real Mode Interrupt mov bl, bIntNum ; номер прерывания реального режима для вызова mov bh, 01h ; бит 0 = 1; все остальные должны быть равны нулю xor cx, cx ; ничего не копируем из стека PM в стек RM les di, lpCallStruct ; указатель на структуру со значением регистров int 31h ; шлюз к DMPI jc END1 ; если ошибка, то прыгаем на END1 mov fRetVal, TRUE ; все ОК END1: pop di ; восстанавливаем регистр DI } // возвращаемся return (fRetVal); }
Листинг 30. Ключевой фрагмент программы, демонстрирующей технику взаимодействия с драйвером 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 | + | + | - | + | + | + | + | + | + |
требует прав админа | нет | нет | - | нет | да | нет | нет | хз | * |
Поддерживает CDDA | да | нет | да | да | да | да | да | да | да |
Поддерживает CD data | да | да | да | да | да | да | да | да | да |
Сырое чтение с CDDA | да | нет | да | да | да | да | да | да | да |
Сырое чтение с CDdata | нет | нет | да | да | да | да | да | да | да |
Потенциально опасен | нет | нет | нет | да | нет | нет | нет | да | да |
Хорошо документирован? | да | да | да | да | нет | нет | нет | да | нет |
Легко использовать? | да | да | нет | да | да | да | нет | нет | нет |
Таблица 5. Различные методы доступа в сравнении. Неблагоприятные характеристики выделены красным шрифтом.
Защита, нуждающаяся в низкоуровневом доступе с CD, обязательно выдаст себя наличием функций DeviceIoControl и/или SendASPI32Command в таблице импорта. Если же защитный механизм загружает эти функции динамически, поймать его за хвост можно установкой точек останова на LoadLibrary/GetProcAddress (однако, опытные программисты могут отважиться на самостоятельный поиск требуемых им функций в памяти и это отнюдь не такая трудная задача, какой она кажется).
Также в теле программы могут присутствовать строки: "\\.\", "SCSI", "CdRom", "wnaspi32.dll" и другие. Установив точку останова на первый байт строки, мы сможем мгновенно локализовать защитный код при первом его к ним обращении. Чтобы этого не произошло, разработчики часто шифруют все текстовые строки, однако большинство из них ограничивается примитивной статической шифровкой (которая обычно осуществляется ASPack'ом или подобными ему программами), а потому, если дождаться завершения расшифровки и вызвать отладчик после, а не до запуска программы, все текстовые строки предстанут перед нами в прямом виде! Динамическая шифровка намного надежней. В этом случае текстовые строки расшифровываются непосредственно перед их передачей в соответствующую API-функцию, а потом зашифровываются вновь. Но и динамическую шифровку при желании можно преодолеть! Достаточно поставить условную точку останова на функцию CreateFile, которой эти текстовые строки и передаются, всплывая в том и только в том случае, если первые четыре байта имени файла равны "\\.\". Пример ее вызова может выглядеть, например, так: "bpx CreateFileA if (*esp->4=='\\\\.\\')", после чего останется только пожинать урожай.
Естественно, под "урожаем" понимается, во-первых, имя самого открываемого файла, а точнее - драйвера (это уже многое что дает), и, во-вторых, возращенный функцией CreateFile дескриптор. Далее можно поступить двояко: либо установить точку останова на ту ячейку памяти, в которой этот дескриптор сохраняется, либо установить условную точку останова на функцию DeviceIoControl, отлавливая только те ее вызовы, которые нам необходимы. Пример сеанса работы с отладчиком приведен ниже:
:bpx CreateFileA if (*esp->4=='\\\\.\\') (ставим точку останова) :x (выходим из отладчика) ... (отладчик немного думает, а потом всплывает в момент вызова CreateFileA) :P RET (выходим из CreateFileA) :? eax (узнаем значение дескриптора) 00000030 0000000048 "0" (ответ отладчика) :DeviceIoControlA if (*esp->4==0x30) (ставим точку останова на DeviceIoCntrl) (подумав, отладчик всплывает в момент вызова DeviceIoControl) :P RET (выходим из DeviceIoControl) : U (все! мы нашли защиту!) 001B:00401112 LEA ECX,[EBP-38] 001B:00401115 PUSH ECX ; 001B:00401116 PUSH 0004D004 ; вот он, IOCTL_SCSI_PASS_THROUGH_DIRECT! 001B:0040111B MOV EDX,[EBP-0C] 001B:0040111E PUSH EDX 001B:0040111F CALL [KERNEL32!DeviceIoControl]
Листинг 31. Пример изобличения и разоблачения защитного механизма в 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 до и после инсталляции защищенного приложения - очевидно, защита может скрываться только среди вновь появившихся драйверов.
Для защиты дисков от копирования, приступать к созданию своей собственной программы прожига совершенно необязательно. Вместо этого вы можете манипулировать с "сырыми" образами дисков, поддерживаемыми Алкоголиком или Clone CD. Несмотря на то, что все эти программы налагают на записываемые ими образы определенные ограничения, создание качественных защитных механизмов все-таки остается возможным. Другими словами, эти программы спокойно записывают то, что самостоятельно скопировать оказываются не в состоянии!
При создании собственного копировщика защищенных дисков без умения прожигать диски можно, в принципе, и обойтись - достаточно лишь подготовить образ диска (т.е. корректно прочитать защищенный диск), ну а тиражирование "хакнутого" образа уже не проблема. Лучше сфокусироваться непосредственно на анализе защищенных дисков, чем изобретать велосипед, в очередной раз разрабатывая то, что уже давно разработано до вас. Алкоголь и CloneCD имеют превосходные возможности прожига, но вот читающий движок у них явно слабоват, и даже незначительные искажения служебных структур лазерного диска способны сбить их столку.
Если же, несмотря ни на что, вы по прежнему убеждены, что свой собственный Нерон, сжигающий Рим, вас все-таки нужен, что ж! добро пожаловать в гости к Демону Максвелла, попасть в лапы к которому гораздо сложнее, чем вырваться из них. Шутка! А вся доля правды в том, что техника прожига дисков - чрезвычайно обширный вопрос, даже краткое изложение которого потребовало бы отдельной книги. Одних лишь Стандартов и Спецификаций по SCSI-командам здесь окажется более чем недостаточно, поскольку в них опущены многочисленные подробности процесса генерации различных структур данных, требующихся приводу для корректной записи исходного образа на лазерный диск. Лучшим из имеющихся пособий по отжигу дисков автор считает приложение "Functional Requirements for CD-R (Informative)" к документу "SCSI-3 Multimedia Commands", электронную версию которого можно найти здесь: http://www.t10.org/ftp/t10/drafts/mmc/mmc-r10a.pdf (обратите внимание, что в более поздних ревизиях документа это приложение было изъято).
Попробуйте также обратиться к исходным текстам утилиты CDRTOOLS, которые можно найти здесь: http://prdownloads.sourceforge.net/cdromtool/cdromtool_2002-11-26.zip?download. Конечно, семь с небольшим мегабайт исходных текстов - не самое лучшее средство для вдохновения, но более простые программы прожига автору неизвестны.
Более трудоемким (но вместе с тем и соблазнительным!) способом является дизассемблирование исполняемых файлов Алкоголя, Clone CD, CDRWin и других программ, включая монстроузного Нерона. Собственно, полное дизассемблирование проводить совершенно необязательно, достаточно перехватить передаваемые приводы SCSI-команды и проанализировать последовательность их вызовов, не забывая при этом о значениях аргументов, в которых все ключевые структуры данных, собственно, и содержатся.
В зависимости от способа, выбранного разработчиком приложения для взаимодействия с устройством, шпионаж осуществляется либо перехватом функции DeviceIoControl с аргументами IOCTL_SCSI_PASS_THROUGH/IOCTL_SCSI_PASS_THROUGH_DIRECT (4D004h/4D014h), либо SendASPI32Command для SPTI и ASPI интерфейсов, соответственно. Приложения, взаимодействующие с приводом через свой собственный драйвер, также поддаются перехвату, но универсальных решений здесь нет, и каждый конкретный случай следует рассматривать индивидуально.
Давайте исследуем копировщик Алкоголь на предмет выявления алгоритма очистки и прожига CD-RW дисков (CD-R диски прожигаются аналогичным образам но, по понятным причинам, не поддаются очистке). Итак, запускаем Алкоголика, переходим к вкладке "Настройки", щелкаем по ссылке "Общие" и в ниспадающем боксе "Интерфейс управления дисками" выбираем "Интерфейс WinASPI Layer (безопасный режим)", если только он уже не был выбран ранее. После смены интерфейса Алкоголик потребует перезапуска программы - что ж, выходим из него и тут же запускаем вновь, убеждаясь в работоспособности последнего.
Теперь вызываем soft-ice (или любой другой отладчик, поддерживающий точки останова на API функции) и, предварительно загрузив ASPI-экспорт в память (NuMega Symbol Loader -> File -> Load Exports -> wnaspi32.dll), открываем процесс Alcohol.exe, при необходимости распаковав (по обыкновению он упакован UPX'ом).
Пытаемся установить точку останова на SendASPI32Command, отдавая отладчику следующую команду "bpx SendASPI32Command", но ничего хорошего из этого у нас не получается - soft-ice ругается, что не может найти такую функцию, несмотря на то, что ее имя написано без ошибок. Это не покажется удивительным, если предположить, что wnaspi32.dll загружается динамически в ходе выполнения программы и на этапе загрузки Alcohol.exe адреса ASPI-функций еще не известны.
Можно поставить точку останова на LoadLibraryA, отслеживая загрузку всех динамических библиотек, но, поскольку Алкоголь загружает огромное количество разнообразных DLL, на отладку уйдет чудовищное количество времени, в течении которого мы будем тупо пялиться на экран, монотонно выбивая комбинацию <CTRL-D> на клавиатуре. Более прогрессивным средством мониторинга будут установка условной точки останова, которая автоматически отсечет все заведомо ложные вызовы. Соответствующая ей команда может выглядеть, например, так: "bpx LoadLibraryA IF *(ESP->4) == "SANW"", где SANW - это четыре первых символа имени "wnaspi32.dll", записанные задом наперед с учетом регистра, выбранного разработчиком программы (если регистр наперед неизвестен, можно использовать функцию сравнения нечувствительную к регистру).
Затем команда "bpx GetProcAddress" позволит перехватить загрузку всех ASPI-функций и SendASPI32Command в том числе. Имя загружаемой функции может быть просмотрено командой "d esp -> 4". Дождавшись появления SendASPI32Command, жмем "P RET" и, установив точку останова на "BPX EAX", давим <Ctrl-D> для выхода из soft-ice (все остальные точки останова при желании можно удалить).
По факту всплытия отладчика, наскоро набиваем команду "d esp -> 4", и в окне дампа памяти появляется содержимое структуры SRB_ExecSCSICmd. Теперь 30h байт по счету - это первый байт CDB-пакета (внимание! это именно первый байт пакета, а не указатель на сам пакет); 03h и 10h байты - это флаги направления передачи данных и указатель на буфер обмена соответственно.
Ниже приведены примеры шпионских протоколов, перехваченных в процессе очистки и прожига CD-RW болванки:
1E 00 00 00 01 00 <- PREVENT REMOVAL (ON) -----------+ 51 00 00 00 00 00 <- READ DISK INFORMATION-------+ | 1E 00 00 00 00 00 <- PREVENT REMOVAL (OFF) ------|---+ BB 00 FF FF FF FF <- SET SPEED ----------+ | | 5A 00 2A 00 00 00 <- MODE SENSE -----+ | | | BB 00 FF FF 02 C2 <- ----------------|---+ | | 5A 00 2A 00 00 00 <- ----------------+ | | 1E 00 00 00 00 00 <- ----------------------------|---+ 51 00 00 00 00 00 <- ----------------------------+ A1 11 00 00 00 00 <- BLANK
Листинг 32. Содержимое перехваченных CDB-блоков, посылаемых Алкоголиком устройству при быстрой очистке лазерного диска.
Обратите внимание, что для очистки диска Алкоголь использует SCSI-команду BLANK, подробное описание которой содержится в документах "Multimedia Commands - 4" и "Information Specification for ATAPI DVD Devices". Именно BLANK, а не ERASE, как пытается убедить нас товарищ Всеволод Несвижский в своей книге "Программирование устройств SCSI и IDE". Приведенные им листинги программ не работают, да и не должны работать в принципе. Команда ERASE (10), с кодом операции 2Ch, местами упоминается как команда с кодом операции 1Ch, соответствующим SCSI-команде RECEIVE DIAGNOSTIC RESULTS, кстати, не поддерживаемой оптическими накопителями вообще. Команды ERASE (12) не то чтобы совсем не существовало в природе, однако согласно приведенному автору коду операции - ACh - это есть ни что иное, как GET PERFORMANCE. Интересно, как автор планировал что-либо стирать с ее помощью?
Ладно, оставим дохлое дело критики в стороне и продолжим нашу шпионскую деятельность, наблюдая за процессом прожига лазерного диска. Последовательность, SCSI-команд, посылаемых устройству, будет следующей:
выбираем "прожиг" в меню BB 00 FF FF FF FF <- SET SPEED 5A 00 2A 00 00 00 <- MODE SENSE AC 00 00 00 00 52 <- GET PERFORMANCE появляется диалог "запись" 1E 00 00 00 00 01 <- PREVENT REMOVAL (LOCK) 51 00 00 00 00 00 <- READ DISK INFORMATION 1E 00 00 00 00 00 <- PREVENT REMOVAL (UNLOCK) запись диска в прогрессе 43 02 04 00 00 00 <- READ ATIP 51 00 00 00 00 00 <- READ DISK INFORMATION ... 52 00 00 00 00 00 <- READ TRACK/ZONE INFORMATION 5A 00 05 00 00 00 <- MODE SENSE 55 10 00 00 00 00 <- MODE SELECT 51 00 00 00 00 00 <- READ DISK INFORMATION 2A 00 FF FF D2 AC <- WRITE(10) -+ 2A 00 00 00 D2 BC <- -----------+-- write Lead-In 2A 00 00 00 D2 CC <- -----------+ ... 2A 00 00 00 65 B3 <- WRITE(10) -+ 2A 00 00 00 65 CD <- -----------+-- write track 2A 00 00 00 65 E7 <- -----------+
Листинг 33. Содержимое перехваченных CDB-блоков, посылаемых Алкоголиком устройству при прожиге образа лазерного диска.
В заключение отметим перечень SCSI-команд, непосредственно относящихся к записи и рекомендованных для внимательнейшего изучения. Это: BLANK, CLOSE TRACK/SESSION, FORMAT UNIT, READ BUFFER CAPACITY, READ DISC INFORMATION, READ MASTER CUE, READ TRACK INFORMATION, REPAIR TRACK, RESERVE TRACK, SEND CUE SHEET, SEND OPC INFORMATION, SYNCHRONIZE CACHE, WRITE (10). Все вышеперечисленные команды относятся к Стандарту MMC-1 и потому максимально просты для понимания. Сам текст Стандарта можно найти здесь: http://www.t10.org/ftp/t10/drafts/mmc/mmc-r10a.pdf.
Если приложение, взаимодействующие с CD, выполняет операцию, которая не должна быть ни при каких обстоятельствах прервана, можно воспользоваться ICTL-командой блокировки лотка - IOCTL_CDROM_MEDIA_REMOVAL (а вот и ее непосредственное значение: 0x24804). При попытке сделать диску "eject" при заблокированном лотке мой PHILIPS CDRW начинает злобно моргать красным огоньком, показывая, что диск "IN", но он "is locked". Вплоть до момента разблокирования лотка извлечь диск можно разве булавкой или перезагрузкой операционной системы.
Уже одно это создает богатое поле для всевозможных пакостей со стороны многочисленных злоумышленников, да и просто некорректно работающих программ, успевающих умереть от критической ошибки прежде, чем разблокировать лоток. Как с этим бороться? Да очень просто - разблокировать лоток самостоятельно!
Дело в том, что система не требует, чтобы разблокирование выполнялось в контексте того процесса, который выполнил блокирование. Она просто ведет счет блокировок, и если тот равен нулю - лоток свободен. Соответственно, если счет блокировок равен, например, шести - мы должны шесть раз вызывать команду разблокирования, прежде чем лазерный диск удастся извлечь на свет божий.
Утилита, исходный текст которой приведен ниже, позволяет манипулировать счетчиком блокировок диска по вашему собственному усмотрению. Аргумент командной строки "+" увеличивает значение счетчика на единицу, а "-" - уменьшает. При достижении счетчиком нуля дальнейшие попытки его уменьшения не возымеют никакого действия.
Как это можно использовать? Ну, например, для преждевременного извлечения диска из записывающей программы, что полезно для экспериментов. Другое применение: отлучаясь от своего компьютера на несколько минут, вы можете заблокировать диск, чтобы быть уверенными, что окружающие коллеги его не упрут. А если все-таки упрут (перезагрузив компьютер) заблокируйте лотки их CD-ROM'ов - пусть теперь перезагружаются!
/*---------------------------------------------------------------------------- * * БЛОКИРУЕТ/РАЗБЛОКИРУЕТ ЛОТОК CD-ROM * =================================== * * build 0x001 @ 04.06.2003 ----------------------------------------------------------------------------*/ #include <windows.h> #include <winioctl.h> #include <stdio.h> #define IOCTL_CDROM_MEDIA_REMOVAL 0x24804 main(int argc, char **argv) { BOOL act; DWORD xxxx; HANDLE hCD; PREVENT_MEDIA_REMOVAL pmrLockCDROM; // ПРОВЕРКА АРГУМЕНТОВ if (argc<3){printf("USAGE: CD.lock.exe \\\\.\\X: {+,-}\n"); return -1;} if (argv[2][0]=='+') act=TRUE; // УВЕЛИЧИТЬ СЧЕТЧИК БЛОКИРОВОК else if (argv[2][0]=='-') act=FALSE; // УМЕНЬШИТЬ СЧЕТЧИК БЛОКИРОВОК else {printf(stderr,"-ERR: in arg %c\n",argv[2][0]); return -1;} // ПОЛУЧИТЬ ДЕСКРПИТОР УСТРОЙСТВА hCD=CreateFile(argv[1],GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,0,0); if (hCD == INVALID_HANDLE_VALUE) {printf("-ERR: get CD-ROM\n");return -1;} // ЗАБЛОКИРОВАТЬ/РАЗБЛОКИРОВАТЬ ЛОТОК CD-ROM'а pmrLockCDROM.PreventMediaRemoval = act; DeviceIoControl (hCD, IOCTL_CDROM_MEDIA_REMOVAL, &pmrLockCDROM, sizeof(pmrLockCDROM), NULL, 0, &xxxx, NULL); }
Листинг 34. Утилита для блокирования/разблокирования лотка в CD-ROMе.
Появление высокоскоростных приводов CD-ROM породило огромное количество проблем и по общему мнению пользователей плюсов здесь гораздо меньше, чем минусов. Этого реактивный гул, вибрация, разорванные в клочья диски - скажите, на кой черт все это вам нужно? К тому же, многие из алгоритмов привязки к CD на высоких скоростях чувствуют себя крайне неустойчиво, и защищенный диск запускается далеко не с первого раза, если вообще запускается. Какой же из всего этого выход? Естественно - тормозить! Благо, команду SET CD SPEED (опкод 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, недурно бы дать команду чтения сектора с диска, если, конечно, диск вообще присутствует. Изменять же скорость привода без диска в лотке - совершенно бессмысленная операция, пригодная разве что для построения ряда поддерживаемых скоростей, т.к. после вставки нового диска в привод прежние скоростные установки оказываются недействительными и наиболее оптимальная (с точки зрения привода!) скорость для каждого диска определяется индивидуально. Так же привод вправе изменять скорость диска по своему усмотрению, понижая ее, если чтение идет неважно и, соответственно, увеличивая обороты, если все идет хорошо.
В качестве закрепления всего вышесказанного и обретения минимальных практических навыков, давайте исследуем несколько популярных программ, работающих с лазерными дисками на низком уровне, на предмет выяснения: как именно осуществляется такое взаимодействие.
Вызвав незаменимый 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-вызовов в виде импровизированной программы, мы сможем воссоздать протокол взаимодействия с диском и найти защиту (если она там есть).
Точка останова, установленная на функцию CreateFileA показывает, что Clone CD общается с диском через свой собственный драйвер - \\.\ELBYCDIO, причем по не совсем понятным причинам его открытие происходит в цикле, так что дескриптор драйвера возвращается многократно.