Восстановление паролей к хэшам

SAMInside
Скачать v2.6.3.0
Купить лицензию
Архив обновлен: 11.10.2008

PasswordsPro
Скачать v2.4.4.1
Купить лицензию
Архив обновлен: 17.11.2008


23.10.2008
Вышла EGB v1.4
Обсудить (комментариев: 44)

11.10.2008
Вышла PasswordsPro v2.4.4.1
Обсудить (комментариев: 7)

28.09.2008
Вышла EGB v1.3
Обсудить (комментариев: 16)

Словари
Файлов: 72

Библиотека
Документов: 309

Генератор хэшей
Алгоритмов: 104

Новости интернета
RSS-лент: 11

Подписка на новости
Подписчиков: 795

Дополнительные сервисы

Всего запросов: 15367080
Всего посещений: 2593501
Посетителей сегодня: 1568
Форум
Сообщений: 12634
Последнее: 02.12.2008, 14:55
  База хэшей
Найдено паролей: 806076
Всего хэшей в базе: 27173283
 

Способы взаимодействия с диском на секторном уровне

Автор: (c)Крис Касперски

Отлаженная программа - это такая программа, для
которой еще не найдены условия, в которых она откажет.
Программистский фольклор

Секторный уровень взаимодействия всегда привлекал как создателей защитных механизмов, так и разработчиков утилит, предназначенных для копирования защищенных дисков. Еще большие перспективы открывает чтение/запись "сырых" (RAW) секторов - это наиболее низкий уровень общения с диском, какой только штатные приводы способны поддерживать. Большинство защитных механизмов именно так, собственно, и работают. Одни из них прячут ключевую информацию в каналы подкода, другие тем или иным образом искажают коды ECC/EDC, третьи используют нестандартную разметку и т.д. и т.п.

Существует множество способов для работы с диском на секторном уровне и ниже будет описан добрый десяток из них. Большая часть рассматриваемых здесь методик рассчитана исключительно на Windows NT/W2K/XP и не работает в Windows 9x, которой, по-видимому, придется разделить судьбу мамонтов, а потому интерес к ней стремительно тает как со стороны пользователей, так и со стороны программистов. Конечно, какое-то время она еще продержится на плаву, но в долгосрочной перспективе я бы не стал на нее закладываться, особенно учитывая тот факт, что Windows 9x не в состоянии поддерживать многопроцессорные системы, а победоносное шествие Hyper-Threading уже не за горами.

В силу того, что секторный уровень доступа к диску изначально ориентирован на создателей (ломателей) защитных механизмов, данная публикация выкрашена ярко-хакерской краской и рассказывает не только о самих методиках низкоуровневого управления устройствами, но и описывает технику взлома каждого из них. Забегая вперед, заметим, что сломать можно все [1]! Так что не стоит, право же, переоценивать стойкость механизмов, препятствующих несанкционированному копированию лазерных дисков. Если кому-то особо приспичит, вашу программу все равно взломают! Как? А вот об этом и будет рассказано ниже. Как говорится - "кто предупрежден, тот вооружен". Ну, а коль уж совсем невмоготу - используйте прямой доступ к портам ввода/вывода с прикладного уровня. Нет, вы не ослышались - в Windows NT это действительно возможно и ниже будет рассказано, как.

Доступ через CDFS-драйвер

Управление драйверами устройств в операционных системах семейства Windows осуществляется посредством вызова функции DeviceIoControl, отвечающей за посылку специальных FSCTL/IOCTL команд. Префикс FS- свидетельствует о принадлежности данной команды к файловой системе и в контексте настоящей публикации не представляет для нас никакого интереса. Команды с префиксом IO- относятся к устройству, а точнее - к его драйверу. Функция DeviceIoControl просто передает такую команду как она есть, совершенно не задумываясь о ее "физическом смысле". Следовательно, совершенно бессмысленно искать перечень доступных IOCTL-команд в описании DeviceIoControl. Их там нет! Точнее, здесь приводятся лишь стандартные IOCTL-команды, а вся остальная информация по этому вопросу содержится в DDK. Там, в частности, мы найдем, что для чтения отдельных секторов используется команда IRP_MJ_READ, а если нам необходимо прочесть сектор в "сыром" виде, то стоит воспользоваться командой IOCTL_CDROM_RAW_READ. Также обратите свое внимание на команду IOCTL_CDROM_READ_Q_CHANNEL, обеспечивающую извлечение информации из Q-канала подкода. К сожалению, возможности такого способа чтения сырых секторов ограничены лишь CDDA-дисками, поскольку с не-аудиодисков драйвер CDFS сырое чтение не поддерживает.

Функции DeviceIoControl всегда предшествует вызов CreateFile, открывающей соответствующее устройство, которое задается в виде "\\.\X:", где X - буквенное обозначение того привода, с которым мы собрались работать.

Поскольку DeviceIoControl не относится к числу наиболее часто вызываемых функций, защитный механизм, базирующийся на ее использовании, очень легко запеленговать. Достаточно поставить на DeviceIoControl точку останова и дождаться, пока передаваемая ей IOCTL-команда не примет одно из перечисленных выше значений. На CreateFile точку останова лучше не ставить, т.к. это даст множество ложных срабатываний (CreateFile вызывается всякий раз при открытии\создании какого-либо файла). А вот попробовать поискать в теле программы текстовую строку "\\.\" все-таки стоит. И если она действительно будет найдена, вам останется лишь подбежать курсором к перекрестной ссылке и долбануть по Enter'у. Все! Защитный код перед вами!

Для лучшего понимания данного способа взаимодействия между прикладной программой и драйвером ниже приведен ключевой фрагмент функции, как раз и осуществляющей такое взаимодействие (обработка ошибок по соображениям наглядности опущена):

//--[ReadCDDA]-----------------------------------------------------------------
//
//     читает сектор в сыром виде с CDDA-дисков
// ========================================
// ARG:
//   drive              - имя устройства, с которого читать (например "\\\\.\\X:")
//   start_sector       - номер первого читаемого сектора
//   n_sec              - сколько секторов читать
//
// RET:
//   == 0               - ошибка
//   != 0               - указатель на буфер, содержащий считанные сектора
//
// NOTE:
//   1) функция поддерживает только диски тех типов, что поддерживает драйвер
//   CDSF, который она и использует, а штатный драйвер Windows NT поддерживает
//   лишь CDDA-диски
//----------------------------------------------------------------------------
char* ReadCDDA(char *drive, int start_sector, int n_sec)
{
     int a;
     HANDLE hCD;
     DWORD x_size;
     char *szDrive;
     BOOL fResult = 0;
     unsigned char *buf;
     RAW_READ_INFO rawRead;
     
     #define CDROM_SECTOR_SIZE 2352
     #define CDROM_DATA_TO_SEC 2048
     
     // ПОДГОТАВЛИВАЕМ СТРУКТУРУ RAW_READ_INFO, передаваемую драйверу CD-ROM'а
     rawRead.TrackMode       = CDDA ;     // тип диска - Audio CD
     rawRead.SectorCount     = n_sec;     // кол-во читаемых секторов
     rawRead.DiskOffset.QuadPart = start_sector * CDROM_DATA_TO_SEC;
     
     // ВЫДЕЛЯЕМ ПАМЯТЬ
     buf = malloc(CDROM_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_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;
}

Листинг 1. Функция, демонстрирующая технику чтения сырых секторов через CDFS-драйвер (только для CDDA-дисков!).

Еще один демонстрационный пример приведен ниже. Он иллюстрирует технику чтения TOC (Table of Content) - своеобразный аналог таблицы разделов лазерных аудиодисков.

/*----------------------------------------------------------------------------
*
*               ЧТЕНИЕ И РАСШИФРОВКА TOC
*               ========================
*
* build 0x001 @ 26.05.2003
----------------------------------------------------------------------------*/

main(int argc, char **argv)
{
     int a;
     HANDLE hCD;
     char *buf;
     WORD TOC_SIZE;
     BYTE n_track;
     DWORD x_size,b;
     
     // ПРОВЕРКА АРГУМЕНТОВ
     if (argc < 2)
     {
               fprintf(stderr, "USAGE: CDDA.read.toc.exe \\\\.\\X:\n");
               return 0;
     }
     
     // TITLE
     fprintf(stderr, "TOC.view DEMO (only 01b mode!)\n");
     
     // ВЫДЕЛЯЕМ ПАМЯТЬ
     buf = 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, 0, 0, buf, buf_len, &x_size, 0) != 0)
     {
               // ПОЛУЧАЕМ ДЛИНУ ТОС'а (она записана в обратном порядке)
               *((BYTE *)(&TOC_SIZE)) = buf[1]; *((BYTE *)(&TOC_SIZE + 1)) = buf[0];
               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("\tfra..................%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");
     }
}

Листинг 2. Еще один пример программы, взаимодействующей с CDFS-драйвером через IOCTL и читающей содержимое TOC'а (с расшифровкой), изучение которого бывает полезно при анализе некоторых защищенных дисков.

Доступ через cooked-моде (режим блочного чтения)

Операционная система 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 В БЛОЧНОМ РЕЖИМЕ
*         ========================================
*
* Build 0x001 @ 19.05.03
----------------------------------------------------------------------------*/

#include <windows.h>
#include <winioctl.h>
#include "ntddcdrm.h"
#include <stdio.h>

// ПАРАМЕТРЫ ПО УМОЛЧАНИЮ
#define _xTo    0x666
#define _xSec   0x001
#define _xFrom  0x000

main(int argc, char **argv)
{
   int a;
   FILE *f;
   HANDLE hCD;
   char *buf;
   DWORD dwSize;
   DWORD x_read;
   char buf_n[1024];
   
   int xTo = _xTo;
   int xSec = _xSec;
   int xFrom = _xFrom;
   
   // ПРОВЕРЯЕМ АРГУМЕНТЫ
   if (argc < 2)
   {
      printf("USAGE:CD.read.sector.exe PhysCD [filename][xSec][from][to]\n");
      printf("\tPhysCD - physical name of CD (\"\\\\.\\G:\")\n");
      printf("\tfilename - file name to store follow sector\n");
      printf("\txSec - sector per block\n");
      printf("\tfrom - start sector\n");
      printf("\tto - end sector\n");
      return 0;
   }
   if (argc > 3) xSec = atol(argv[3]);
   if (argc > 4) xFrom = atol(argv[4]); if (argc > 5) xTo = atol(argv[5]);
   
   // ВЫДЕЛЯЕМ ПАМЯТЬ
   buf = malloc(dwSize); if (!buf) {printf("-ERR: low memory\n"); return -1;}
   
   // ОТКРЫВАЕМ УСТРОЙСТВО
   hCD = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
   if (hCD == INVALID_HANDLE_VALUE){printf("-ERR CreateFile\n"); return -1;}
   
   // ОПРЕДЕЛЯЕМ КОЛ-ВО БАЙТ, КОТОРЫЕ НАДО СЧИТАТЬ
   dwSize = xSec * 2048;
   
   // ПОЗИЦИОНИРУЕМ УКАЗАТЕЛЬ НА ПЕРВЫЙ ЧИТАЕМЫЙ СЕКТОР
   SetFilePointer(hCD, dgCDROM. BytesPerSector * xFrom, NULL, FILE_BEGIN);
   
   // ЧИТАЕМ СЕКТОРА ОДИН ДА ДРУГИМ
   for (a = xFrom; a < xTo ; a += xSec)
   {
      // ЧИТАЕМ ОЧЕРЕДНОЙ СЕКТОР
      if ((ReadFile(hCD, buf, dwSize, &x_read, NULL)) && (argc > 2))
      {
         // ЗАПИСЫВАЕМ ТОЛЬКО ЧТО СЧИТАННЫЙ СЕКТОР В ФАЙЛ
         sprintf(buf_n, "%s[%04dx%d].dat", argv[2], a * xSec + xFrom, xSec);
         if ((f = fopen(buf_n, "w"))){fwrite(buf, 1, dwSize, f); fclose(f);}
      }
   }
}

Листинг 3. Пример, демонстрирующий технику чтения секторов в cooked-mode.

Доступ через SPTI

Одной из интереснейших архитектурных особенностей операционной системы Windows NT является ее умение взаимодействовать с IDE-устройствами через SCSI-интерфейс! К сожалению, данная технология чрезвычайно скудно документирована - Platform SDK, MSDN, DDK содержат лишь обрывки информации, а имеющиеся примеры крайне ненаглядны и к тому же выполнены с большим количеством фактических ошибок, так что разобраться с ними под силу лишь профессионалу, ну или очень настырному новичку [2]. И судя по сообщениям в конференциях, достаточно многим программистам осилить технику управления устройствами через SCSI-интерфейс так и не удается, поэтому имеет смысл рассмотреть эту проблему поподробнее.

Для решения поставленной задачи нам понадобится:

а) Описание SCSI-интерфейса (рекомендую "The Linux SCSI programming HOWTO", который можно найти здесь: http://www.ibiblio.org/pub/Linux/docs/HOWTO/other-formats/pdf/SCSI-Programming-HOWTO.pdf);

б) Описание ATAPI-интерфейса для CD-ROM/DVD накопителей (см. "ATA Packet Interface for CD-ROMs" и "Specification for ATAPI DVD Devices", причем спецификации на DVD гораздо лучше и полнее описывают CD-ROM, чем их родная документация; не самые свежие, но вполне подходящие ревизии можно найти здесь: www.stanford.edu/~csapuntz/specs/INF-8020.PDF и ftp.seagate.com/sff/INF-8090.PDF);

в) Описание форматов хранения данных на лазерных дисках (см. standard ECMA-130 "Data interchange on read-only 120 mm optical data disks", который можно найти здесь: http://www.ecma-international.org/publications/files/ecma-st/Ecma-130.pdf);

г) Помимо этого годится любая литература, так или иначе затрагивающая вопросы программирования CD-ROM; нелишним будет почитать "ATAPI(IDE) CD Информация к размышлению" от Константина Норватова и "Особенности программирования CD-ROM'а на Спектруме" от Влада Сотникова.

Итак, что же такое SCSI? Это - стандартизованный платформенно-независимый интерфейс, обеспечивающий согласованное взаимодействие различных устройств и высокоуровневых приложений. Собственно, аббревиатура SCSI именно так и расшифровывается - Small Computer System Interface (Системный Интерфейс Малых Компьютеров). Благодаря SCSI для низкоуровневого управления устройствами совершенно необязательно прибегать к написанию собственных драйверов (писать драйвер только для того, чтобы прорваться сквозь ограничения API - чистейший маразм) и эту задачу можно решить и на прикладном уровне, посылая устройству специальные CDB-блоки, содержащие стандартные или специфичные для данного устройства команды управления вместе со всеми необходимыми им параметрами. Собственно, "CDB" так и расшифровывается - Command Descriptor Block. Пример одного из таких блоков приведен ниже:

Cмещение, байт

Содержимое

0x0

0x28

Код команды "read sector"

0x1

0x00

Зарезервировано

0x2

0x00

Номер сектора - 0х69

0x3

0x00

0x4

0х00

0x5

0x69

0x6

0x00

Кол-во секторов

0x7

0x01

0x8

0x00

Зарезервировано

0x9

0x00

Зарезервировано

0xA

0x00

Зарезервировано

Таблица 1. Пример CDB блока, который будучи переданным SCSI-устройству, заставит его прочитать 0x69-й сектор.

Первый байт блока представляет собой команду операции (в нашем случае: 0x28 - чтение одного или нескольких секторов), а все остальные байты блока - параметры данной команды. Причем обратите внимание на тот факт, что младший байт слова располагается по большему адресу, то есть все происходит не так, как в привычном нам IBM PC! Поэтому если передать в качестве номера первого сектора последовательность 0x69 0x00 0x00 0х00, то прочитается 0x6900000 сектор, а вовсе не 0x90000069, как можно было того ожидать!

Краткое описание стандартных SCSI-команд можно найти в том же "The Linux SCSI programming HOWTO", однако для наших целей их навряд ли окажется достаточно и команды, специфичные для CD-ROM дисков, мы рассмотрим отдельно. Однако это произойдет не раньше, чем мы разберемся, как CDB-блоки упаковываются в SRB-конверт (SCSI Request Block), без которого операционная система просто не поймет, что же мы хотим сделать (как известно, машинная программа выполняет то, что ей приказали сделать - иногда это совпадает с тем, что от нее хотели, иногда нет).

Структура SRB-блока подробно описана в NT DDK, поэтому не будем подробно на ней останавливаться и пробежимся по основным полям лишь вкратце.

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 (0xCh)
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;

Заполнив поля структуры SCSI_REQUEST_BLOCK подобающим образом, мы можем передать SRB-блок выбранному нами устройству посредством функции DeviceIoControl, просто задав соответствующий код IOCTL. Вот, собственно, и все! Заглотив наживку, операционная система передаст CDB-блок соответствующему устройству и оно выполнит (или не выполнит) содержащиеся в нем (СDB-блоке) команды. Обратите внимание: CDB-блок обрабатывается не драйвером устройства, но самим устройством, а потому мы имеем практически неограниченные возможности по управлению последним. И все это - с прикладного уровня!

Теперь о грустном. Процедура управлениями устройствами довольно капризна и одно-единственное неправильно заполненное поле может обернуться полным нежеланием устройства выполнять передаваемые ему команды. Вместо этого будет возвращаться код ошибки или вовсе не возвратится ничего. К тому же, малейшая неаккуратность может запросто испортить данные на всех жестких дисках, а потому с выбором значений TargetID и lun вы должны быть особенно внимательными! (Для автоматического определения физического адреса CD-ROM'а можно использовать SCSI-команду SCSI_INQUIRY - см. демонстрационный пример \NTDDK\src\win_me\block\wnaspi32 из DDK). Однако довольно говорить об опасностях (без них жизнь была бы слишком скучной), переходим к самому интересному - поиску того самого IOCTL-кода, который этот SRB-блок, собственно, и передает.

Оказывается, напрямую это сделать не так-то просто, точнее - легальными средствами вообще невозможно! Создатели Windows по ряду соображений решили предоставить полный доступ к полям структуры SCSI_REQUEST_BLOCK только писателям драйверов, а прикладных программистов оставили наедине с структурами SCSI_PASS_THROUGH и SCSI_PASS_THROUGH_DIRECT, схожими по назначению с SRB, но несколько ограниченными в своей функциональности. К счастью, на содержимое CDB-блоков не было наложено никаких ограничений, а потому возможности для низкоуровневых операций с железом у нас все-таки остались. Подробнее обо всем этом можно прочитать в разделе "9.2 SCSI Port I/O Control Codes" из NT DDK, а также из исходного текста демонстрационного примера "\NTDDK\src\storage\class\spti" из того же DDK (обратите внимание на файл spti.htm, лежащий в этом же каталоге, который достаточно подробно описывает суть управления устройством через SСSI-интерфейс).

Согласно наименованию каталога с демонстрационным примером, данный способ взаимодействия с устройством носит название SPTI и расшифровывается как SCSI Pass Through IOCTLs - т.е. SCSI, проходящий через IOCTL. Кратко перечислим основные особенности и ограничения SPTI-интерфейса. Во-первых, для передачи CDB-блоков устройству вы должны обладать привилегиями администратора, что не всегда удобно. Во-вторых, использование многоцелевых команд запрещено (т.е. мы не можем отдать команду копирования данных с устройства А на устройство Б в обход процессора, хотя такие команды у современных приводов есть и было бы очень здорово копировать лазерные диски, совершенно не загружая процессор). В-третьих, реверсивное (то бишь, двунаправленное) перемещение данных не поддерживается и в каждый момент времени данные могут перемещаться либо от устройства к компьютеру, либо от компьютера к устройству, но не то и другое одновременно!). В-четвертых, при установленном class-драйвере для целевого устройства, мы должны направлять CDB-блоки именно class-драйверу, но не самому SCSI-устройству. То есть, для управления CD-ROM'ом вы должны взаимодействовать с ним через устройство \\.\X:, где X - буква привода, попытка же обращения к "\\.\Scsi0:" возвратит ошибку (и это, как показывает практика, основной камень преткновения неопытных программистов, начинающих программировать раньше, чем читать документацию [3]). Наконец, в пятых, сама структура SCSI_PASS_THROUGH_DIRECT содержит значительно меньше полей, причем значение полей PathId, TargetId и Lun игнорируются! Физический адрес устройства на шине определяется непосредственно самой операционной системой по символьному имени дескриптора устройства, которому, собственно, и посылается SCSI_PASS_THROUGH_DIRECT запрос.

typedef struct _SCSI_PASS_THROUGH_DIRECT {
   USHORT Length;
   UCHAR ScsiStatus;
   UCHAR PathId;
   UCHAR TargetId;
   UCHAR Lun;
   UCHAR CdbLength;
   UCHAR SenseInfoLength;
   UCHAR DataIn;
   ULONG DataTransferLength;
   ULONG TimeOutValue;
   PVOID DataBuffer;
   ULONG SenseInfoOffset;
   UCHAR Cdb[16];
}SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT;

Листинг 5. Формат структуры 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);

Листинг 6. Открытие привода для получения дескриптора, использующегося для его уприавления.

Убедившись, что hCD не равно INVALID_HANDLE_VALUE, передаем полученный дескриптор вместе с самой структурой IOCTL_SCSI_PASS_THROUGHT_DIRECT функции DeviceIoControl, вызывая ее следующим образом:

DeviceIoControl(hCD, IOCTL_SCSI_PASS_THROUGH, &srb, sizeof(SCSI_PASS_THROUGH),
         sense_buf, MAX_SENSE_SZ, &returned, FALSE);

Листинг 7. Передача структуры IOCTL_SCSI_PASS_THROUGH.

Где srb - и есть заполненный экземпляр структуры IOCTRL_SCSI_PASS_THROUGHT, а returned - переменная, в которую будет записано количество байт, возращенных устройством. В свою очередь, sense_buf - это тот самый буфер, в котором заполненный нами экземпляр IOCTL_SCSI_PASS_THROUGHT_DIRECT возвращается назад, да не один, а вместе с sense info - кодом ошибки завершения операции. Если же операция завершилась без ошибок, то sense info не возвращается и sense_buf содержит только IOCTL_SCSI_PASS_THROUGHT. Позиция размещения sense info в буфере определяется содержимым поля SenseInfoOffset, значение которого должно быть подобрано так, чтобы не "наступать на пятку" структуре IOCTRL_SCSI_PASS_THROUGHT, т.е., попросту говоря, минимально возможное смещение Sense Info равно: srb.SenseInfoOffset = sizeof(SCSI_PASS_THROUGH_DIRECT). Обратите внимание, SenseInfoOffset - это не указатель на Sense Info, но индекс первого байта Sense Info в возвращаемом буфере!

Для определения факта наличия ошибки необходимо проанализировать количество байт, возращенных функцией DeviceIoControl в переменной returned. Если оно превышает размер структуры IOCTL_SCSI_PASS_THROUGHT, то в буфере находится sense info, а раз есть sense info, то есть и ошибка! Формат sense info приведен на рисунке ниже:

Формат SENSE INFO

Рисунок 1. Формат SENSE INFO, возвращаемой устройством в случае возникновения ошибки.

Первый байт указывает на тип ошибки и обычно принимает значение 70h (текущая ошибка - current error) или 71h (отсроченная ошибка - deferred error). Коды ошибок с 72h по 7Eh зарезервированы, причем ошибки с кодом 7Eh указывают на нестандартный (vendor-specific) sense info формат. Коды ошибок с 00h по 6Fh в спецификации CD-ROM ATAPI не определены и потому их использование нежелательно (данное предостережение, разумеется, адресовано не программистам, а разработчикам аппаратуры).

Описание ошибки кодируется тройкой чисел: Sense Key, Additional Sense Code (дополнительный смысловой код, сокращенно - ASC) и Additional Sense Code Qualifier (ASCQ). Вершину этой иерархической пирамиды возглавляет Sense Key, содержащее общую категорию ошибки (genetic categories), затем идет дополнительный смысловой код, более детально описывающий ошибку и, наконец, на самом низу иерархии находится квалификатор дополнительного смыслового кода, уточняющий непосредственно сам дополнительный смысловой код. Если ошибка исчерпывающее описывается одним лишь Sense Key и ASC, то ASCQ в таком случае отсутствует (точнее, находится в неопределенном состоянии).

Расшифровка основных кодов ошибок описывается в двух таблицах, приведенных ниже. Стоит сказать, что для анализа ошибки значение Sense Key, в общем-то, некритично, т.к. гарантируется, что каждый ASC принадлежит только одному Sense Key и напротив - один и тот же ASCQ может принадлежать нескольким различным ASC и потому в отрыве от последнего он бессмысленнен.

Sense Key

Описание

00h

NO SENSE. Нет дополнительной sense info. Операция выполнена успешно.

01h

RECOVERED ERROR (восстановленная ошибка). Операция выполнена успешно, но в процессе ее выполнения возникли некоторые проблемы, устраненные непосредственно самим приводом. За дополнительной информацией обращайтесь к ключам ASC и ASCQ.

02h

NOT READY (не готов). Устройство не готово.

03h

MEDIUM ERROR (ошибка носителя). В процессе выполнения операция произошла неустранимая ошибка, вызванная, по всей видимости, дефектами носителя или ошибкой записи данных. Данный sense key может возвращаться и в тех случаях, когда привод оказывается не в состоянии отличить дефект носителя от аппаратного сбоя самого привода.

04h

HARDWARE ERROR (аппаратная ошибка). Неустранимая аппаратная ошибка (например, отказ контроллера).

05h

ILLEGAL REQEST (неверный запрос). Неверные параметры, переданные приводу в CDB-пакете (например, начальный адрес больше конечного).

06h

UNIT ATTENTION (модуль требует внимания). Носитель заменен или выполнен сброс контроллера привода.

07h

DATA PROTECT (защищенные данные). Попытка чтения защищенных данных.

8h -0Ah

Зарезервировано

0Bh

ABORTED COMMAND (команда прервана). По тем или иным причинам выполнение команды было прервано.

0Eh

MISCOMPARE (ошибка сравнения). Исходные данные не соответствуют данным, прочитанным с носителя.

0Fh

Зарезервировано

Таблица 2. Основные Sense Key (категории ошибок) и их описания.

ASC ASCQ DROM Описание
00 00 DROM NO ADDITIONAL SENSE INFORMATION
00 11 R PLAY OPERATION IN PROGRESS
00 12 R PLAY OPERATION PAUSED
00 13 R PLAY OPERATION SUCCESSFULLY COMPLETED
00 14 R PLAY OPERATION STOPPED DUE TO ERROR
00 15 R NO CURRENT AUDIO STATUS TO RETURN
01 00 R MECHANICAL POSITIONING OR CHANGER ERROR
02 00 DROM NO SEEK COMPLETE
04 00 DROM LOGICAL DRIVE NOT READY - CAUSE NOT REPORTABLE
04 01 DROM LOGICAL DRIVE NOT READY - IN PROGRESS OF BECOMING READY
04 02 DROM LOGICAL DRIVE NOT READY - INITIALIZING COMMAND REQUIRED
04 03 DROM LOGICAL DRIVE NOT READY - MANUAL INTERVENTION REQUIRED
05 01 DROM MEDIA LOAD - EJECT FAILED
06 00 DROM NO REFERENCE POSITION FOUND
09 00 DRO TRACK FOLLOWING ERROR
09 01 RO TRACKING SERVO FAILURE
09 02 RO FOCUS SERVO FAILURE
09 03 RO SPINDLE SERVO FAILURE
11 00 DRO UNRECOVERED READ ERROR
11 06 RO CIRC UNRECOVERED ERROR
15 00 DROM RANDOM POSITIONING ERROR
15 01 DROM MECHANICAL POSITIONING OR CHANGER ERROR
15 02 DRO POSITIONING ERROR DETECTED BY READ OF MEDIUM
17 00 DRO RECOVERED DATA WITH NO ERROR CORRECTION APPLIED
17 01 DRO RECOVERED DATA WITH RETRIES
17 02 DRO RECOVERED DATA WITH POSITIVE HEAD OFFSET
17 03 DRO RECOVERED DATA WITH NEGATIVE HEAD OFFSET
17 04 RO RECOVERED DATA WITH RETRIES AND/OR CIRC APPLIED
17 05 DRO RECOVERED DATA USING PREVIOUS SECTOR ID
18 00 DRO RECOVERED DATA WITH ERROR CORRECTION APPLIED
18 01 DRO RECOVERED DATA WITH ERROR CORRECTION & RETRIES APPLIED
18 02 DRO RECOVERED DATA - THE DATA WAS AUTO-REALLOCATED
18 03 R RECOVERED DATA WITH CIRC
18 04 R RECOVERED DATA WITH L-EC
1A 00 DROM PARAMETER LIST LENGTH ERROR
20 00 DROM INVALID COMMAND OPERATION CODE
21 00 DROM LOGICAL BLOCK ADDRESS OUT OF RANGE
24 00 DROM INVALID FIELD IN COMMAND PACKET
26 00 DROM INVALID FIELD IN PARAMETER LIST
26 01 DROM PARAMETER NOT SUPPORTED
26 02 DROM PARAMETER VALUE INVALID
28 00 ROM NOT READY TO READY TRANSITION, MEDIUM MAY HAVE CHANGED
29 00 ROM POWER ON, RESET OR BUS DEVICE RESET OCCURRED
2A 00 ROM PARAMETERS CHANGED
2A 01 ROM MODE PARAMETERS CHANGED
30 00 ROM INCOMPATIBLE MEDIUM INSTALLED
30 01 RO CANNOT READ MEDIUM - UNKNOWN FORMAT
30 02 RO CANNOT READ MEDIUM - INCOMPATIBLE FORMAT
39 00 ROM SAVING PARAMETERS NOT SUPPORTED
3A 00 ROM MEDIUM NOT PRESENT
3F 00 ROM ATAPI CD-ROM DRIVE OPERATING CONDITIONS HAVE CHANGED
3F 01 ROM MICROCODE HAS BEEN CHANGED
40 NN ROM DIAGNOSTIC FAILURE ON COMPONENT NN (80H-FFH)
44 00 ROM INTERNAL ATAPI CD-ROM DRIVE FAILURE
4E 00 ROM OVERLAPPED COMMANDS ATTEMPTED
53 00 ROM MEDIA LOAD OR EJECT FAILED
53 02 ROM MEDIUM REMOVAL PREVENTED
57 00 R UNABLE TO RECOVER TABLE OF CONTENTS
5A 00 DROM OPERATOR REQUEST OR STATE CHANGE INPUT (UNSPECIFIED)
5A 01 DROM OPERATOR MEDIUM REMOVAL REQUEST
63 00 R END OF USER AREA ENCOUNTERED ON THIS TRACK
64 00 R ILLEGAL MODE FOR THIS TRACK
B9 00 R PLAY OPERATION OBORTED
BF 00 R LOSS OF STREAMING

Таблица 3. Основные ASC и ASCQ-коды.

Как видите - все просто! Единственное, с чем мы еще не разобрались - это ATAPI. Поскольку мы не собираемся взаимодействовать с ATAPI-интерфейсом напрямую (этой возможности "благодаря" архитекторам Windows мы, увы, лишены), промчимся галопом лишь по ключевым аспектам и особенностям. Как пишет Михаил Гук в своей книге "Интерфейсы персональных компьютеров" - "Для устройств, логически отличающихся от жестких дисков - оптических, магнитооптических, ленточных и любых других - в 1996 г. была принята спецификация ATAPI. Это пакетное расширение интерфейса, которое позволяет передавать по шине ATA-устройству блоки командной информации, структура которых была позаимствована из SCSI". Теперь, по крайней мере, становится понятно, почему Windows так лихо "превращает" ATAPI-устройства в SCSI. Если отбросить аппаратные различия интерфейсов, которые с программного уровня все равно не видны, то ATAPI-интерфейс будет очень напоминать SCSI. Во всяком случае, управление ATAPI-устройствами осуществляется посредством тех самых CDB-блоков, которые мы уже рассматривали выше.

Естественно, чтобы управлять устройством, необходимо знать - какими именно командами оно управляется. Для получения этой информации нам понадобится "ATAPI Packet Commands for CD-ROM devices". Откроем его на описании команды READ CD command (код 0xBEh) и обнаружим таблицу следующего содержания:

Описание команды READ CD

Рисунок 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. Взаимосвязь рода запрошенных данных и длины возвращаемого сектора.

Внутренний мир Windows NT

Рисунок 4. Внутренний мир Windows NT. IDE-устройства с прикладного уровня видятся как SCSI. Разумеется, на физическом уровне с приводом не происходит никаких изменений и CD-ROM привод с IDE-интерфейсом так IDE-приводом и остается со всеми присущими ему достоинствами и недостатками. Однако IRP-запросы к этому драйверу, проходя через Storage Class Driver, транслируются в SRB (SCSI request block). Затем SRB-запросы попадают в Storage port driver (т.е. непосредственно в сам драйвер привода), где они заново транслируются в конкретные физические команды данного устройства (см. рис. 3). Подробности этого увлекательного процессора можно почерпнуть из NT DDK (см. "1.1 Storage Driver Architecture"), здесь же достаточно указать на тот немаловажный факт, что кроме команд семейства IRP_MJ_ххх мы также можем посылать устройству и SRB-запросы, которые обладают значительно большей свободой и гибкостью. Однако такое взаимодействие невозможно осуществить непосредственно с прикладного уровня, поскольку IRP-команды относятся к числу приватных команд, в то время как API-функция DeviceIoControl передает лишь публичные команды, явно обрабатываемые драйвером в диспетчере IRP_MJ_DEVICE_CONTROL.

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

#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 // длина одного сектора

//-[DWORD READ_RAW_SECTOR_FROM_CD]---------------------------------------------
// функция читает один или несколько секторов с CDROM в сыром (RAW) виде,
// согласно переданным флагам
//
// ARG:
// driver       - что открывать (типа "\\.\X:")
// adapter_id   - номер шины (0 - primary, 1 - secondary)
// read_id      - номер устройства на шине (0 - master, 1 - slayer)
// buf          - буфер, куда читать
// buf_len      - размер буфера в байтах
// StartSector  - с какого сектора читать, считая от нуля
// N_SECTO      - сколько секторов читать \
// flags        - что читать (см. спецификацию на ATAPI)
//
// RET:
// !=0          - функция завершилась успешно
// ==0          - функция завершилась с ошибкой
//
// NOTE:
// работает только под NT/W2K/XP и требует прав администратора
//-----------------------------------------------------------------------------
DWORD READ_RAW_SECTOR_FROM_CD(char *driver, char *buf, int buf_len, DWORD StartSector, DWORD N_SECTOR, BYTE flags)
{
   HANDLE hCD;
   SCSI_PASS_THROUGH srb;
   DWORD returned, length, status;
   // ОТКРЫВАЕМ УСТРОЙСТВО
   //------------------------------------------------------------------------
   // внимание! не надо делать так "\\\\.\\SCSI0" или так "\\\\.\\CdRom0"
   // все равно не сработает! (это, кстати, частая ошибка начинающих)

   hCD = CreateFile(driver, GENERIC_WRITE | GENERIC_READ,
               FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
   if (hCD == INVALID_HANDLE_VALUE) return 0;
   
   // ФОРМИРУЕМ SRB
   //------------------------------------------------------------------------
   // ОПЦИИ

   srb.Length = sizeof(SCSI_PASS_THROUGH);
   srb.PathId = 0; // SCSI controller ID (игнор.)
   srb.TargetId = 0; // target device ID (игнор.)
   srb.Lun = 0; // logical unit device ID (игнор.)
   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] = LOBYTE(LOWORD(N_SECTOR));
   srb.Cdb[8] = HIBYTE(LOWORD(N_SECTOR));
   
   srb.Cdb[9] = flags; // что читать
   srb.Cdb[10] = 0; // Sub-Channel Data Bits
   srb.Cdb[11] = 0; // reserved
   
   // ОТПРАВЛЯЕМ SRB-блок ATAPI-устройству
   status = DeviceIoControl(hCD, IOCTL_SCSI_PASS_THROUGH,
               &srb, sizeof(SCSI_PASS_THROUGH), &srb, 0, &returned, FALSE);
   
   return 1;
}

Листинг 8. Функция, читающая сектора в сыром виде через SPTI.

Остается только сказать, что защитные механизмы, взаимодействующие с диском через SPTI, элементарно ломаются установкой точки останова на функции CreateFile/DeviceIoControl. Для предотвращения "лишних" всплытий отладчика фильтр точки останова должен реагировать только на те вызовы CreateFile, чей первый слева аргумент равен "\\.\X:" или "\\.\CdRomN". Соответственно, второй слева аргумент функции DeviceIoControl должен представлять собой либо IOCTL_SCSI_PASS_THROUGHT, либо IOCTL_SCSI_PASS_THROUGHT_DIRECT, шестнадцатеричные значения кодов которых 0x4D004 и 0x4D014, соответственно.

Доступ через ASPI

Вот два основных недостатка интерфейса SPTI (только что описанного выше): для взаимодействия с устройством он требует наличия прав администратора и, что еще хуже, SPTI поддерживается только операционными системами семейства NT и отсутствует на Windows 9x/ME. Единственный легальный способ дотянуться до CD-ROM'а под Windows 9x - воспользоваться 16-разрядным шлюзом, напрямую обращающемуся к MS-DOS драйверу MSCDEX, который обеспечивает значительно большую функциональность, нежели Windows-драйвер. Естественно, параллельная поддержка двух семейств операционных систем требует от программиста значительных усилий, что существенно повышает себестоимость программного продукта.

Для упрощения разработки кросс-платформенных приложений фирма Adaptec разработала специальный системно-независимый интерфейс, позволяющий управлять различными SCSI-устройствами с прикладного уровня, и назвала его ASPI - Advanced SCSI Programming Interface (хотя неофициально его расшифровывают как Adaptec SCSI Programming Interface, поскольку это больше соответствует истине).

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

Рассмотрим, как осуществляется внедрение ASPI-интерфейса в операционную систему на примере Windows Me. На самом высоком уровне иерархии находятся прикладные библиотеки WNASPI32.DLL и WINASPI.DLL для 32- и 16-разрядных приложений, соответственно. Они экспортируют три базовых ASPI-функции: GetASPI32DLLVersion, GetASPI32SupportInfo и SendASPI32Command (причем, последняя - самая важная) и три вспомогательных: GetASPI32Buffer, FreeASPI32Buffer, TranslateASPI32Address (последняя - только в 32-разрядной версии библиотеки).

Посредством функции DeviceIoControl они взаимодействуют с ASPI-драйвером, расположенном "ниже" и в зависимости от версии операционной системы называющимся либо APIX.VXD (Windows 9x), либо ASPI.SYS (Windows NT) [4] и создающим в процессе своей инициализации устройство с непроизносимым названием MbMmDp32. Только не спрашивайте меня, как это абракадабра расшифровывается - ответ похоронен в застенках компании Adaptec.

В принципе, ничего не мешает взаимодействовать с ASPI-драйвером и напрямую - в обход библиотеки WNASPI32.dll. Собственно, многие разработчики защитных механизмов именно так и поступают. Достаточно лишь дизассемблировать WNASPI32.dll и разобраться каким ASPI-командам какие IOCTL-коды соответствуют (ASPI-протокол по понятным соображениям не документирован). Действительно, на SendASPI32Command очень легко поставить бряк и тогда хакер мгновенно локализует защитный код. С вызовами же DeviceIoControl в силу их многочисленности взломщикам справиться намного труднее. К тому же начинающие ломатели защит (а таких среди хакеров - большинство) весьма смутно представляют себе архитектуру ввода-вывода и уж тем более не разбираются в ASPI-протоколе. Впрочем, для опытных хакеров такая защита - не преграда (подробнее см. "Способы разоблачения защитных механизмов").

Сам же ASPI-драйвер "подключен" к SCSI и IDE/ATAPI портам, за счет чего он позволяет управлять всеми этими устройствами (и приводами CD-ROM, в том числе).

Архитектура подсистемы ввода-вывода

Рисунок 5. Архитектура подсистемы ввода-вывода Windows 98. Клиентские модули (на данной схеме они обозначены цифрами 1, 2 и 3) посылают свои запросы драйверу файловой системы - Instable File System (обозначенному цифрой 6). В распоряжении клиентских модулей также имеются библиотеки ASPI для 32- и 16-разрядных приложений соответственно (они обозначены цифрами 4 и 6). От всей системы они стоят особняком, поскольку разработаны независимой компанией Adaptec и представляют собой факультативные компоненты. Драйвер файловой системы перенаправляет полученный им запрос на один их следующих специализированных драйверов, среди которых присутствует и драйвер привода CD-ROM - CDFS.VxD, обозначенный цифрой 8. В его задачи входит поддержка файловых систем лазерных дисков, как то - ISO 9660, High Sierra или другие файловые системы. Уровнем ниже лежит Volume Tracker (цифра 14), отслеживающий смену диска в накопителе, а еще ниже находится непосредственно сам драйвер, поддерживающий данную модель CD-ROM - так называемый CD type specific driver, реализуемый драйвером CDVSD.VxD и среди прочих обязанностей отвечающий за назначение буквы приводу. Это и есть секторный уровень взаимодействия с диском, никаких файловых систем здесь нет и в помине. Несмотря на то, что данный драйвер специфичен для конкретной модели привода CD-ROM, он совершенно независим от его физического интерфейса, поскольку опирается на CD-ROM device SCSI'zer (цифра 21), преобразующий IOP-запросы, поступающие от вышележащих драйверов, в SRB-пакеты, направляемые нижележащим драйверам (подробнее об этом см. раздел "Доступ через SCSI-порт"). Еще ниже находится SCSI CD-ROM helper (цифра 23), обеспечивающий стыковку SCSI'zer-а с SCSI-портом. Сам же SCSI-port, создаваемый менеджером SCSI-портов (цифра 26) представляет собой унифицированное системно-независимое средство взаимодействия драйверов среднего уровня с физическим (или виртуальным) оборудованием. К одному из таких SCSI-портов и подключается ASPI-драйвер (цифра 18), реализованный в файле APIX.VxD и восходящий к своим "оберткам" - WNASPI32.DLL и WNASPI.DLL (цифры 11 и 12 соответственно). Ниже SCSI-менеджера расположены драйвера мини-портов, переводящие SCSI-запросы в язык конкретной интерфейсной шины. В частности, драйвер, обеспечивающий поддержку IDE-устройств, реализован в файле ESDI_506.PDR (цифра 29). Естественно, при желании мы можем общаться с IDE-устройствами и через IDE/ATAPI-порты (цифра 25), реализованные все тем же драйвером ESDI_506.PDR (ASPI-драйвер по соображениям производительности именно так, собственно, и поступает). Левую часть блок-схемы, изображающую иерархию драйверов прочих дисковых устройств мы не рассматриваем, т.к. она не имеет никакого отношения к теме нашего обсуждения.

Для программирования под ASPI требуется как минимум две вещи: ASPI-драйвер и ASPI-SDK. Драйвер можно бесплатно скачать с сервера самой Adaptec (ею разработаны драйвера для следующих операционных систем: MS-DOS, Novell, Windows 9x, Windows NT/W2KXP), а вот SDK с некоторого момента распространяется за деньги. И хотя его стоимость чисто символическая (что-то около 10$, если мне не изменяет память), неразвитость платежных систем в России превращает процесс покупки в довольно затруднительное дело. Однако все необходимое для работы (документация, заголовочные файлы, библиотеки) можно позаимствовать из... Windows Me DDK (кстати, входящего в состав DDK для Windows 2000). Так что, если у вас уже есть W2K DDK, вам не о чем беспокоиться. В противном случае попробуйте обратиться к MSDN, распространяемым вместе с Microsoft Visual Studio 6.0. Здесь вы найдете документацию и заголовочные файлы, ну а недостающие библиотеки из соответствующих DLL можно получить и самостоятельно (lib.exe с ключом /DEF), либо же вовсе обойтись без них, загружая все необходимые функции через LoadLibrary/GetProcAddress.

Поскольку ASPI-интерфейс хорошо документирован (руководство по программированию насчитывает порядка 35 листов), то его освоение не должно вызвать никаких непреодолимых проблем (во всяком случае, после знакомства с SPTI). К тому же, в Windows Me DDK входит один законченный демонстрационный пример использования ASPI, найти который можно в папке "\src\win_me\block\wnaspi32\". Несмотря на досадный суффикс "Me", он отлично уживается и с другими операционными системами, как-то: Windows 98, Windows 2000, Windows XP и т.д.

Впрочем, реализован этот пример на редкость криво и с большим количеством ошибок, а его наглядность такова, что менее наглядного примера для демонстрации ASPI пожалуй и не подобрать! Уж лучше исследовать исходные тексты программы CD slow, которые можно легко найти в Интернете (однако она написана на ассемблере, а с ассемблером знаком не всякий).

Кратко перечислим основные недочеты демонстрационного примера aspi32ln.c: во-первых, это не консольная программа, а GUI'ная, а потому большая часть ее кода к ASPI вообще никакого отношения не имеет. Во-вторых, используется единая функция для получения уведомлений сразу от выполнения двух команд: SCSI_INQUIRY и SCSI_READ10, причем последняя в половине случаев заменена своей константой 0x28, что тоже не способствует ее пониманию. В-третьих, накопители на CD-ROM программой поддерживаются лишь частично. Плохо спроектированная архитектура программы не позволила разработчикам осилить поставленную перед ними задачу. Поэтому ветка, отвечающая за чтение с CD-ROM в функции ASPI32Post специальным образом закомментирована. Если же наложенную блокировку убрать, то при чтении станет происходить ошибка, поскольку программа ориентирована лишь на те накопители, чей размер сектора составляет 0x200 байт. Приводы CD-ROM дисков, чей сектор вчетверо больше, очевидно к этой категории не относятся и, чтобы не переписывать всю программу целиком, единственное, что можно сделать - это увеличить размер запрашиваемого блока данных до 0х800 байт (с жестких дисков будет считываться по четыре сектора за раз, что вполне допустимо). Наконец, в-пятых, инкремент (т.е. вычисление адреса следующего считываемого блока) реализован через одно место и поэтому вообще неработоспособен.

Ладно, не будет увлекаться критикой сопроводительных примеров (даже плохой программный код все же лучше, чем совсем ничего) и перейдем непосредственно к изучению ASPI-интерфейса, а точнее - его важнейшей команды SendASPI32Command, обеспечивающей передачу SRB-блоков устройству (со всеми остальными командами вы без труда справитесь и самостоятельно).

Структура SRB_ExecSCSICmd, в которую, собственно, и упаковывается SRB-запрос, как две капли воды похожа на SCSI_PASS_THROUGH_DIRECT. Во всяком случае, между ними больше сходства, чем различий. Вот, взгляните сами:

typedef struct
{
        BYTE SRB_Cmd;                   // ASPI command code = SC_EXEC_SCSI_CMD

        BYTE SRB_Status                 // ASPI command status byte
        BYTE SRB_HaId;                  // ASPI host adapter number
        BYTE SRB_Flags;                 // ASPI request flags
        DWORD SRB_Hdr_Rsvd;             // Reserved, MUST = 0
        BYTE SRB_Target;                // Target's SCSI ID
        BYTE SRB_Lun;                   // Target's LUN number
        WORD SRB_Rsvd1;                 // Reserved for Alignment
        DWORD SRB_BufLen;               // Data Allocation Length
        LPBYTE SRB_BufPointer;          // Data Buffer Pointer
        BYTE SRB_SenseLen;              // Sense Allocation Length
        BYTE SRB_CDBLen;                // CDB Length
        BYTE SRB_HaStat;                // Host Adapter Status
        BYTE SRB_TargStat;              // Target Status
        LPVOID SRB_PostProc;            // Post routine
        BYTE SRB_Rsvd2[20];             // Reserved, MUST = 0
        BYTE CDBByte[16];               // SCSI CDB
        BYTE SenseArea[SENSE_LEN+2];    // Request Sense buffer
}
SRB_ExecSCSICmd, *PSRB_ExecSCSICmd;

Листинг 9. Структура SRB_ExecSCSICmd.

Обратите внимание: для взаимодействия с устройством вам совершенно незачем знать его дескриптор! Достаточно указать его физический адрес на шине (т.е. правильно заполнить поля SRB_HaId и SRB_Target).... а как их узнать? Да очень просто: достаточно разослать по всем физическим адресам команду INQUIRY (код 12h). Устройства, реально (и/или виртуально) подключенные к данному порту, вернут идентификационную информацию (среди прочих полезных данных содержащую и свое имя), а несуществующие устройства не вернут ничего и операционная система отрапортует об ошибке.

Простейшая программа опроса устройств может выглядеть, например, так:

#define MAX_ID 8
#define MAX_INFO_LEN 48

SEND_SCSI_INQUITY()
{
   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 < AdapterCount; adapterid++)
   {
         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);
               }
         }
   }
}

Листинг 10. Последовательный опрос портов на предмет наличия подключенных к ним устройств.

Результат работы программы на компьютере автора выглядит так:
0.0 <-- IBM-DTLA-307015         TX2O
1.0 <-- IBM-DTTA-371440         T71O
1.1 <-- PHILIPS CDRW2412A       P1.55VO1214DM10574
2.0 <-- AXV     CD/DVD-ROM      2.2a5VO1214DM10574

Листинг 11. Устройства, подключенные к компьютеру автора. Первая слева цифра - 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]---------------------------------------------
// функция читает один или несколько секторов с CDROM в сыром (RAW) виде,
// согласно переданным флагам
//
// ARG:
// adapter_id - номер шины (0 - primary, 1 - secondary)
// read_id - номер устройства на шине (0 - master, 1 - slayer)
// 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;
}

Листинг 12. Демонстрационный пример программы, осуществляющий сырое чтение сектора с CD-диска.

Откомпилировав этот пример и запустив его на выполнение, убедитесь, что он успешно работает как под Windows 9x, так и под Windows NT, причем не требуя у вас наличия прав администратора! С одной стороны это, бесспорно хорошо, но с другой... наличие ASPI-драйвера создает огромную дыру в системе безопасности, позволяя зловредным программам вытворять с вашим оборудованием все, что угодно. Заразить MBR/boot-сектора? Пожалуйста! Уничтожить информацию со всего диска целиком - да проще этого ничего нет! Поэтому, если вы заботитесь о собственной безопасности - удалите ASPI32-драйвер со своего компьютера (для этого достаточно удалить файл ASPI.SYS из каталога WINNT\System32\Drivers). Разумеется, сказанное относиться только к NT, поскольку в операционных системах Windows 9x прямой доступ к оборудованию можно заполучить и без этого.

Доступ через SCSI-порт

Как уже говорилось выше (см. "Доступ через 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

Листинг 13. Взаимосвязь логических SCSI-устройств с native-NT устройствами.

Оказывается, устройства SCSI0: и SCSI1: представляют собой ни что иное, как символические ссылки на IDE-порты с номерами 0- и 1- соответственно. Впрочем, устройства с именами IdePort0 и IdePort1 не являются IDE-портами в физическом смысле этого слова. Это виртуальные SCSI-порты, создаваемые драйвером ATAPI.SYS в процессе его инициализации. Он же создает символические связи "\DosDevices\SCSI0:" и "\DosDevices\SCSI1:" к ним, а также ярлыки "\Device\ScsiPort0" и "\Device\ScsiPort1", недоступные подсистеме win32, но предназначенные для внутреннего использования исключительно на уровне драйверов. Разумеется, ATAPI.SYS не только создает все вышеперечисленные устройства, но и обслуживает их, предоставляя драйверам более высоких уровней унифицированный интерфейс для взаимодействия с установленным оборудованием.

А вот устройство с именем "SCSI2:" ни с какими физическими шинами вообще не связано и к соответствующему ему SCSI-порту подключен виртуальный привод CD-ROM, создаваемый программой Alcohol 120%, а точнее ее драйвером - AXSAKI.SYS! Драйвера высокого уровня (в частности драйвер CDROM.SYS), не заподозрив никакого подвоха, будут работать с виртуальным диском точно также, как и с настоящим, что, собственно, и не удивительно, т.к. концепция SCSI-порта обеспечивает независимость драйверов верхнего уровня от особенностей оборудования, с которым они, с позволения сказать, "работают". Именно поэтому под Windows NT так легко реализуются эмуляторы физических устройств!

Управлять SCSI-устройствами можно и с прикладного уровня через STPI-интерфейс, однако вместо буквенного имени привода следует задавать имя SCSI-порта, к которому этот привод подключен. Основное достоинство такого способа управления заключается в том, что для взаимодействия с приводом совершенно необязательно обладать правами администратора! Привилегий простого смертного пользователя будет более чем достаточно. К тому же, прямая работа со SCSI-портом несколько производительнее взаимодействия с устройством через длинную цепочку драйверов верхнего уровня многочисленных фильтров, окружающих их.

Однако все попытки передачи SRB-блока через SCSI-порт заканчиваются неизменной ошибкой. Следующий код наотрез отказывается работать. Почему?

// получаем дескриптор 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);

Листинг 15. Пример неправильной работы с виртуальным SCSI-портом.

Зарубежные телеконференции буквально кишат вопросами на этот счет - у одних этот код исправно работает, у других - нет (и их большинство). А ответ, между тем, находится в DDK (если, конечно, читать его сверху вниз, а не наискосок по диагонали). Вот, пожалуйста, цитата из раздела 9.2 SCSI Port I/O Control Codes: "If a class driver for the target type of device exists, the request must be sent to that class driver. Thus, an application can send this request directly to the system port driver for a target logical unit only if there is no class driver for the type of device connected to that LU [5]" ("Если класс-драйвер для целевого устройства установлен, управляющие запросы должны посылаться класс-драйверу, но не самому порту устройства. Таким образом, приложения могут посылать непосредственные запросы драйверу системного порта для целевых логических устройств, только если класс-драйвер для соответствующего типа устройств, подключенных к данному LU, не установлен"). В переводе на нетехнический язык: непосредственное управление портом с прикладного уровня возможно для тех и только тех устройств, чей класс-драйвер не установлен. Скажем, если вы подключили к компьютеру какую-то нестандартную железяку, то управлять ей напрямую через SCSI-порт вполне возможно (ведь класс-драйвера для нее нет!) Но приводы CD-ROM, про которые мы собственно и говорим, совсем иное дело! Класс-драйвер для них всегда установлен и потому операционная система всячески препятствует прямому взаимодействую с оборудованием через SCSI-порт, поскольку это единственный надежный путь избежать конфликтов.

Выходит, доступ к приводам через SCSI-порт невозможен? И так, и не так! Прямой доступ к SCSI-порту действительно блокируется системой, но та же самая система предоставляет возможность управления устройством через SCSI мини-порт. Мини-порт? Что это такое?! А вот об мы сейчас и расскажем!

Архитектура подсистемы ввода-вывода

Рисунок 6. Архитектура подсистемы ввода/вывода в Windows NT.

Доступ через SCSI мини-порт

Драйвер SCSI мини-порта и есть тот самый драйвер, за счет которого системе удается абстрагироваться от особенностей физических интерфейсов конкретного оборудования. Условимся для краткости называть его просто "мини-драйвером", хотя это будет и не совсем верно, поскольку помимо 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);

Листинг 16. Открытие SCSI-порта для управления драйвером мини-порта. Причем, обратите внимание: имя порта должно выглядеть как "SCSIx:", но не как "ScsiPortx", причем, в его конце обязательно должен присутствовать символ двоеточия, иначе ничего не получится.

Здесь мы открываем первый, считая от нуля, SCSI-порт, который, как мы уже знаем, соответствует первому каналу IDE или, говоря другими словами, Secondary IDE-контроллеру (на компьютере автора привод CD-ROM висит именно на нем). Для определения расположения приводов на неизвестном нам компьютере можно воспользоваться IOCTL-кодом IOCTL_SCSI_GET_INQUIRY_DATA, который заставит драйвер мини-порта перечислить все имеющиеся в его наличии оборудования, после чего нам останется только определить его тип (подробнее см. "NTDDK\SRC\STORAGE\CLASS\SPTI").

Однако управление мини-портом осуществляется совсем не так, как SCSI-портом! На этом уровне никаких стандартных команд уже не существует и мы вынуждены работать с учетом специфики и особенностей реализации конкретного оборудования. Вместо SRB-запросов мини-драйверу передается структура SRB_IO_CONTROL, определенная следующим образом:

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;

Листинг 17. Назначение полей структуры SRB_IO_CONTROL, обеспечивающей управление драйвером мини-порта.

Ну, с полем HeaderLength все более или менее ясно, но вот что это за сигнатура такая?! Дело в том, что коды управления драйверами мини-порта не стандартизованы и определяются непосредственно самим разработчиком данного драйвера, а потому коды команд одного драйвера навряд ли подойдут к другому. Вот во избежание междоусобных конфликтов каждый драйвер мини-порта и имеет уникальную сигнатуру, которую тщательно сверяет с сигнатурой переданной приложением в поле Signature структуры SRB_IO_CONTROL. И если эти сигнатуры не совпадают, драйвер отвечает: SRB_STATUS_INVALID_REQUEST (типа, отвали, моя черешня). К сожалению, интерфейс штатных мини-драйверов ATAPI.SYS и DISK.SYS абсолютно незадокументирован и если вы не умеете дизассемблировать, то вам остается лишь посочувствовать. Дизассемблер же сразу показывает, что сигнатуры обоих драйверов выглядят как "SCSIDISK", а сигнатура мини-драйвера от Alcohol 120% - "Alcoholx" (впрочем, последний в силу своей внештатности не представляет для нас особенного интереса).

С кодами команды разобраться сложнее. Правда, специалисты постоянно читающие MSDN и потому неплохо в нем ориентирующиеся, вероятно, смогут вспомнить, что: "...this specification describes the API for an application to issue SMART commands to an IDE drive under Microsoft Windows 95 and Windows NT. Under Windows 95, the API is implemented in a Vendor Specific Driver (VSD), Smartvsd.vxd. SMART functionality is implemented as a "pass through" mechanism whereby the application sets up the IDE registers in a structure and passes it to the driver through the DeviceIoControl API" ("...эта спецификация описывает API для приложений, передающих SMART-команды жестким дискам с IDE-интерфейсов под Microsoft Windows 95 и Windows NT. Под Windows 95 API реализовано в драйвере, специфичном для конкретного производителя (VSD - Vendor Specific Driver), называемом Smartvsd.vxd. SMART-функциональность реализована как "pass through"-механизм, посредством которого приложения устанавливают IDE-регистры, передавая их драйверу через специальную структуру, помещаемую во входной буфер функции DeviceIoControl ")

Ага! Один из драйверов позволяет нам манипулировать регистрами IDE-контроллера по своему усмотрению, то есть фактически предоставляет низкоуровневый доступ к диску! Очень хорошо! Интерфейс со SMART-драйвером достаточно хорошо документирован (см. "MSDN -> Specifications -> Platforms -> SMART IOCTL API Specification"), правда, раздражает гробовое молчание насчет Windows NT. То, что в NT никакх VxD нет - это и ежу ясно. Но, в то же время, заявляется, что SMART API в ней как будто бы реализован... Если напрячь свою голову и проявить чудеса интуиции, можно догадаться, что поддержка SMART в NT обеспечивается штатными средствами! Весь вопрос в том: какими именно средствами, и как? Ни SDK, ни DDK не содержат никакой информации на этот счет, но вот копание в заголовочных файлов из комплекта NT DDK может кое-что дать! Смотрите, что обнаруживается в файле scsi.h при тщательном его просмотре:

//
// 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

Листинг 18. Команды управления SMART в Windows NT, которые мы можем передавать драйверу мини-порта через поле ControlCode структуры SRB_IO_CONTROL.

Оторви Тигре хвост, если в Windows NT функциональность SMART реализуется не в драйвере мини-порта! И дизассемблирование ATAPI.SYS действительно подтверждает это! Вот вам и качество документации от Microsoft - уродство сплошное в стиле "маразм крепчает". Какой смысл включать в заголовочный файл IOCTL-команды, но не документировать их?! Причем, согласно лицензии, дизассемблирование любых компонентов операционной системы запрещено. Ладно, не будет скулить по поводу и без, а лучше еще раз перечитаем "SMART IOCTL API Specification", откуда поймем, что для управления драйвером мини-порта под Windows NT в поле ControlCode структуры SRB_IO_CONTROL мы должны передать код одной из приведенных выше команд. Пусть это будет, например, IOCTL_SCSI_MINIPORT_IDENTIFY.

Сразу же за концом структуры SRB_IO_CONTROL должна быть расположена структура SENDCMDINPARAMS, определенная следующим образом:

typedef struct _SENDCMDINPARAMS
{
    DWORD               cBufferSize;       // размер буфера в байтах или нуль
    IDEREGS             irDriveRegs;       // структура, содержащая значение IDE-регистров
    BYTE                bDriveNumber;      // физический номер диска, считая от нуля
    BYTE                bReserved[3];      // зарезервировано
    DWORD               dwReserved[4];     // зарезервировано
    BYTE                bBuffer[1];        // отсюда начинается входной буфер
} SENDCMDINPARAMS, *PSENDCMDINPARAMS, *LPSENDCMDINPARAMS;

Листинг 19. Структура SENDCMDINPARAMS, дающая прямой доступ к IDE-регистрам.

То есть входной буфер функции DeviceIoControl должен выглядеть так:

Структура входного буфера

Рисунок 7. Структура входного буфера функции DeviceIoControl для управления драйвером мини порта под Windows 9x/NT.

Первый элемент структуры - cBufferSize, содержащий размер bBuffer'a слишком очевиден и неинтересен. А вот структура IDREGS представляет собой настоящий клад, вот взгляните сами (только не упадите со стула, ибо потрясение будет столь же острым, сколько