В этой главе мы рассмотрим некоторые способы организации защиты информации от несанкционированного копирования и несанкционированного доступа. Практически все эти способы используют специальные методы работы с дисками, поэтому мы и расположили данный материал в книге, посвященной дисковой подсистеме.
Прежде чем начинать проектирование системы защиты от несанкционированного доступа, нужно совершенно четко себе представлять, что именно и от кого вы собираетесь защищать. Это необходимо для правильного выбора средств защиты.
Не существует никаких "абсолютно надежных" методов защиты информации, гарантирующих полную невозможность получения несанкционированного доступа (НСД). Можно утверждать, что достаточно квалифицированные системные программисты, пользующиеся современными средствами анализа работы программного обеспечения (отладчики, дизассемблеры, перехватчики прерываний и т.д.), располагающие достаточным временем, смогут преодолеть практически любую защиту от НСД. Этому способствует "открытость" операционной системы MS-DOS, которая хорошо документирована и предоставляет любой программе доступ к любым программным и аппаратным ресурсам компьютера.
Поэтому при проектировании системы защиты от НСД следует исходить из предположения, что рано или поздно эта защита окажется снятой. Целью проектирования, по мнению авторов книги, должен быть выбор такого способа защиты, который обеспечит невозможность получения НСД для заранее определенного круга лиц и в течение ограниченного времени.
Например, если вы собираетесь защитить от копирования коммерческую версию вашей программы, вам необязательно защищать эту версию от копирования "навсегда" - стоимость такой защиты может превысить стоимость самой программы. Вполне достаточно, чтобы способ защиты было невозможно "разгадать" к моменту появления следующей версии вашей программы, так как в новой версии вы сможете изменить этот способ.
Короче говоря, уровень защиты должен быть таким, чтобы было бы выгоднее купить программу, а не заниматься снятием защиты от копирования.
Иногда защищать программы от копирования вообще нецелесообразно. Фирма Microsoft в основном не защищает от копирования свои программные продукты. Для привлечения покупателей она устанавливает низкие цены на свои изделия и обеспечивает сопровождение на высоком уровне. Новые версии программ продаются зарегистрированным пользователям со значительными скидками. Эти версии появляются достаточно быстро, поэтому имеет смысл покупать новые версии, а не копировать старые.
Таким образом, для выбора способа организации защиты от НСД и от копирования необходим индивидуальный подход в каждом конкретном случае.
Все средства защиты от НСД можно разделить на аппаратные и программные.
Аппаратные средства защиты могут быть реализованы в виде специальных модулей, устанавливаемых в слоты расширения материнской платы компьютера, либо подключаемых к последовательному порту ввода/вывода. Эти модули могут содержать однокристальные микро-ЭВМ или специальные заказные микросхемы, выполняющие обмен кодовыми последовательностями с программой. Можно также использовать специальные версии BIOS.
В нашей книге мы будем рассматривать только программные средства защиты от НСД, так как они доступны, не требуют специального оборудования и вместе с тем дают неплохие результаты.
Мы рассмотрим программные средства, которые блокируют доступ к компьютеру для всех посторонних лиц еще на этапе загрузки операционной системы, опишем способы защиты от копирования дискет, защиты от копирования программ, находящихся на жестком диске.
Кроме того, небольшой раздел этой главы посвящен вопросам защиты программ от отладки. Те участки программ, которые отвечают за защиту от НСД, желательно составлять таким образом, чтобы они работали по-разному в зависимости от того, выполняются они в реальном времени, или под управлением какого-либо отладчика, например, CodeView.
Приведенный в этой главе перечень способов защиты от НСД не претендует на полноту, мы продемонстрируем лишь основные приемы защиты от НСД и приведем некоторые программы. Вы сможете использовать их в таком виде, как они есть или в измененном вами виде. Проблема защиты информации от НСД неисчерпаема, приступая к ее решению, помните о вечной борьбе брони и снаряда.
Операционная система MS-DOS не содержит каких-либо средств разграничения доступа. Однако если компьютером пользуется несколько человек, или если возможен доступ к вашему компьютеру посторонних лиц, желательно использовать программу, запрашивающую пароль при загрузке операционной системы.
Одним из примеров коммерческой системы разграничения доступа для MS-DOS может служить системный администратор ADM. Эта система обеспечивает разграничение доступа к отдельным логическим дискам для нескольких пользователей. Логический диск может быть предоставлен пользователю с правом на чтение, чтение/запись или полностью "спрятан" от пользователя. Для создания логических дисков используется нестандартная схема разделов диска, поэтому, загрузив операционную систему с дискеты, вы "увидите" только диск C:, остальные диски будут для вас недоступны.
Системный администратор ADM в целом удовлетворяет требованиям защиты от НСД, но, к сожалению, в настоящее время эта система полностью исчерпала запас "секретности" - доступна информация о способах получения статуса привилегированного пользователя, которому предоставлены все логические диски. Имеется даже программа для удобного и легкого взламывания этой системы защиты.
В ответственных случаях едва ли целесообразно использовать такие широко распространенные средства защиты от НСД, так как почти наверняка кто-то уже подобрал к ним ключи. В любом случае, эти ключи есть в руках фирмы-разработчика системы защиты.
Как же можно доработать MS-DOS, для того, чтобы она обеспечивала ограничение доступа при загрузке?
Для ответа на этот вопрос вспомним, как выполняется загрузка операционной системы.
После тестирования аппаратуры и инициализации векторов прерываний BIOS происходит попытка выполнить чтение самого первого сектора на нулевой дорожке и нулевой стороне диска А: или, при отсутствии дискеты в дисководе А:, первого сектора нулевой дорожки нулевой стороны накопителя на жестком диске С:. В любом случае, содержимое сектора записывается в оперативную память по адресу 7C00:0000, после чего по этому же адресу передается управление.
Если загрузка операционной системы выполняется с жесткого диска, считанный сектор содержит главную загрузочную запись - Master Boot Record и таблицу разделов диска.
Идея заключается в замене программы загрузки, находящейся в главной загрузочной записи на свою, разработанную специальным образом - по аналогии с печально известным вирусом Stone.
При установке системы защиты первый сектор, содержащий главную загрузочную запись, переписывается на другое место, например, во второй или третий сектор нулевой дорожки нулевой стороны. Это возможно, так как обычно на нулевой дорожке нулевой стороны используется только самый первый сектор.
Таблица разделов, находящаяся в этом секторе, зашифровывается специальным системным паролем, доступным только системному программисту.
В первый сектор (на место главной загрузочной записи) записывается специальная программа и таблица имен пользователей и их, разумеется зашифрованные, пароли.
После передачи управления эта программа переписывает себя в другое место оперативной памяти и запрашивает имя и пароль пользователя. Если имя и пароль указаны правильно, программа считывает главную загрузочную запись в память по адресу 7C00:0000 и там (в памяти, а не на диске) расшифровывает таблицу разделов. После этого управление передается главной загрузочной записи по адресу 7C00:0000 и загрузка продолжается обычным образом.
Теперь, если выполнить загрузку операционной системы с дискеты, разделы жесткого диска станут недоступны, так как, во-первых, таблица разделов находится в другом секторе диска, а во-вторых, она зашифрована.
Используя указанную выше схему защиты от НСД, не забывайте о такой программе, как Norton Disk Doctor. Эта программа сможет восстановить и таблицу разделов, и корневые каталоги, и все остальное...
Вы можете усложнить приведенную выше схему, добавив специальный драйвер. Этот драйвер, переназначив дисковые прерывания (INT 13h и INT 40h - используется MS-DOS вместо INT 13h) и прерывания для работы с каталогами, будет отслеживать все обращения к каталогам и расшифровывать их при необходимости. Однако при этом производительность системы может несколько снизиться.
Если все, что вам нужно, это ограничить доступ к загрузке операционной системы с жесткого диска, вы можете использовать драйвер, который запрашивает пароль на стадии инициализации. Если пароль введен правильно, драйвер завершает работу без установки себя резидентно в памяти, вслед за чем продолжается обычная загрузка операционной системы.
Приведем пример такого драйвера. Вы можете записать его на диск С: и первой строкой в файле CONFIG.SYS записать:
device=c:\stop.sys
Для продолжения загрузки введите пароль sys.
.MODEL tiny .CODE ; Драйвер состоит из одного ; сегмента кода org 0 ; Эта строка может отсутствовать include sysp.inc ;=========================================================== stop PROC far ;драйвер - это FAR-процедура ;=========================================================== E_O_P: ;Метка конца программы, ;устанавливаем ее в начало ;драйвера, т.к. нам не надо ;оставлять драйвер резидентно ;в памяти ; Заголовок драйвера dd 0ffffffffh ;адрес следующего драйвера dw 8000h ;байт атрибутов dw dev_strategy ;адрес процедуры стратегии dw dev_interrupt ;адрес процедуры прерывания db 'STOP ' ;имя устройства (дополненное пробелами) ;=========================================================== ; Программа стратегии dev_strategy: mov cs:req_seg,es mov cs:req_off,bx ret ; Здесь запоминается адрес заголовка запроса req_seg dw ? req_off dw ? ;=========================================================== ;Обработчик прерывания dev_interrupt: push es ;сохраняем регистры push ds push ax push bx push cx push dx push si push di push bp ; Устанавливаем ES:BX на заголовок запроса mov ax,cs:req_seg mov es,ax mov bx,cs:req_off ; Получаем код команды из заголовка запроса и умножаем ; его на два, чтобы использовать в качестве индекса ; таблицы адресов обработчиков команд mov al,es:[bx]+2 shl al,1 sub ah,ah ; Обнуляем AH lea di,functions ; DI указывает на смещение таблицы add di,ax ; Добавляем смещение в таблице jmp word ptr [di] ; Переходим на адрес из таблицы functions LABEL WORD ; Таблица функций dw initialize dw check_media dw make_bpb dw ioctl_in dw input_data dw nondestruct_in dw input_status dw clear_input dw output_data dw output_verify dw output_status dw clear_output dw ioctl_out dw Device_open dw Device_close dw Removable_media ; Выход из драйвера, если функция не поддерживается check_media: make_bpb: ioctl_in: nondestruct_in: input_status: clear_input: output_verify: output_status: clear_output: ioctl_out: Removable_media: Device_open: Device_close: output_data: input_data: or es:word ptr [bx]+3,8103h jmp quit ;=========================================================== quit: or es:word ptr [bx]+3,100h pop bp pop di pop si pop dx pop cx pop bx pop ax pop ds pop es ret ;=========================================================== ; Процедура выводит на экран строку ; символов в формате ASCIIZ dpc proc near push si dpc_loop: cmp ds:byte ptr [si],0 jz end_dpc mov al,ds:byte ptr [si] @@out_ch al inc si jmp dpc_loop end_dpc: pop si ret dpc endp ;=========================================================== hello db 13,10,'+--------------------------------+' db 13,10,'¦ *STOP* (C)Frolov A., 1990 ¦' db 13,10,'+--------------------------------+' db 13,10 db 13,10,'Enter password...' db 13,10,0 ;=========================================================== initialize: lea ax,E_O_P ;смещение конца программы в AX mov es:word ptr [bx]+14,ax ;помещаем его в заголовок mov es:word ptr [bx]+16,cs ; ; Стираем экран mov dh,18h mov dl,80h xor cx,cx mov bh,7 xor al,al mov ah,6 int 10h ; Устанавливаем курсор в левый верхний угол экрана mov bh,0 xor dx,dx mov ah,2 int 10h ; Выводим сообщение mov ax,cs mov ds,ax mov si,offset hello call dpc ; Ожидаем ввода правильного пароля loop_password: mov ax,0 int 16h cmp al,'s' jne loop_password mov ax,0 int 16h cmp al,'y' jne loop_password mov ax,0 int 16h cmp al,'s' jne loop_password jmp quit stop ENDP END stop
Участок программы, ответственный за ввод пароля, имеет смысл усложнить. Предоставляем это сделать вам самим.
Описанный выше драйвер эффективен в тех случаях, когда посторонний пользователь не может по тем или иным причинам загрузить операционную систему с дискеты.
Дополнительным средством защиты информации (возможно, наилучшим) может служить шифрование файлов данных. Для этого можно воспользоваться готовыми программами или написать собственные.
Одна из готовых программ, которая может быть использована для шифрования данных - архиватор PKZIP. Эта программа кроме шифрования файлов еще и уплотняет их, что полезно и само по себе. После образования зашифрованного архива программа PKZIP удаляет исходные файлы. К сожалению, эти файлы можно легко восстановить, например, утилитой Нортона QU. Для того, чтобы удалить эти файлы окончательно, можно в конце рабочего дня запускать утилиту Нортона WIPEINFO, которая прописывает сектора свободных кластеров нулями (при вызове с соответствующими параметрами).
Для удобства шифрования файлов программой PKZIP мы подготовили две программы (соответственно, для шифрования и дешифрования).
Первая программа запрашивает пароль, не отображая его на экране. Затем она ищет программу PKZIP в каталогах, описанных переменной среды PATH и запускает ее с соответствующими параметрами. Вторая программа выполняет аналогичные действия с программой PKUNZIP, использующейся для дешифрования.
Программа шифрования всех файлов текущего каталога:
#include <stdio.h> #include <conio.h> #include <process.h> void main(int argc, char *argv[]) { char passw[80], filebuf[80], parms[100], ch; _searchenv( "pkzip.exe", "PATH", filebuf ); if(!(*filebuf) ) { printf("PKZIP not found.\n"); exit(-2); } printf( "Enter password: " ); getpw(passw, 80); sprintf(parms,"-m -s%s %s",passw,argv[1]); execl( filebuf, parms, parms, NULL ); exit(-1); } int getpw( char *buf, int size) { int i; char ch; for(i=0; i<size; ) { ch = getch(); if(ch == 0) ch=getch(); putch('.'); *buf = ch; if(*buf == 0xd) break; i++; buf++; } *buf = 0; }
Программа дешифрования:
#include <stdio.h> #include <conio.h> #include <process.h> void main(int argc, char *argv[]) { char passw[80], filebuf[80], parms[100], ch; _searchenv( "pkunzip.exe", "PATH", filebuf ); if(!(*filebuf) ) { printf("PKUNZIP not found.\n"); exit(-2); } printf( "Enter password: " ); getpw(passw, 80); sprintf(parms,"-s%s %s",passw,argv[1]); execl( filebuf, parms, parms, NULL ); exit(-1); } int getpw( char *buf, int size) { int i; char ch; for(i=0; i<size; ) { ch = getch(); if(ch == 0) ch=getch(); putch('.'); *buf = ch; if(*buf == 0xd) break; i++; buf++; } *buf = 0; }
В качестве параметра программам надо задать имя образуемого архива (можно без расширения). Если это имя не указывать, используется имя (NULL) - на эту строку указывает неинициализированный параметр argv[1].
Дискета, предназначенная для установки защищенного от копирования программного обеспечения должна быть сама защищена от копирования.
Копирование дискет можно выполнить как по файлам (утилитами операционной системы COPY и XCOPY), так и по секторам (утилитой DISKCOPY, программами PCTOOLS, PCSHELL и аналогичными). Кроме того, существуют программы, специально предназначенные для копирования дискет, защищенных от копирования, например COPY2PC. Специальные программы могут копировать дискеты, содержащие только определенные защищенные программные пакеты, например, DBASE, или они могут повторять структуру дорожек диска с точностью до бита.
Наиболее просто обеспечить защиту от программ копирования дискет по секторам. Можно предложить следующие достаточно простые способы, использующие нестандартное форматирование отдельных дорожек дискеты:
Очевидно, что все эти способы непригодны для защиты от таких программ копирования, которые способны копировать битовую структуру дорожек диска. Что можно порекомендовать в этом случае?
Можно использовать специальную аппаратуру при записи инсталляционных дискет, которая позволяет записывать отдельные дорожки или сектора как бы с промежуточным уровнем записи. Эти участки дорожки будут читаться нестабильно, каждый раз будут получаться новые значения битов. Если такая дискета будет скопирована на обычной аппаратуре (с использованием обычных дисководов и программ побитового копирования) то все дорожки будут читаться стабильно. Если при многократном контрольном чтении указанных секторов или дорожек каждый раз будут получены разные данные - мы имеем дело с оригиналом, в противном случае - с незаконной копией.
Однако дискеты с промежуточным уровнем записи все-таки могут быть скопированы с использованием специальной аппаратуры, копирующей содержимое дорожек "аналоговым" способом (как в бытовом магнитофоне).
Для защиты от аналогового копирования можно использовать дискеты, на которых в некоторых местах искусственно созданы дефекты магнитного покрытия - выжженные лазером небольшие точки или просто царапины.
Проверка основывается на том, что в дефектные места невозможно произвести запись информации. Если мы имеем дело с копией, то на месте дефектных секторов окажутся хорошие - копируется только информация, но не дефекты дискеты!
Разумеется, что можно использовать комбинации различных методов защиты от копирования. При этом легко распознаваемые методы (нестандартный размер сектора и т.п.) можно использовать для маскировки какого-либо другого, более тонкого метода защиты.
Более подробно мы остановимся на нестандартном форматировании, как на наиболее простом методе защиты от копирования, для использования которого не требуется ни специальной аппаратуры, ни специально подготовленных дискет с дефектами. Используя сведения о работе с диском на физическом уровне, приведенные в этой книге, вы сможете самостоятельно использовать метод дефектных дискет или дискет с промежуточным уровнем записи.
Самое простое, что можно сделать - изменить размер секторов на дорожке. Приведем простую программу, которая форматирует двадцатую дорожку диска, создавая на ней сектора размером 256 байтов. После форматирования программа записывает в первый сектор нестандартной дорожки строку, введенную с клавиатуры. Затем для контроля содержимое этого сектора считывается и отображается на экране. Обратите внимание на изменения в таблице параметров дискеты - они необходимы для использования нестандартного размера сектора.
#include <stdio.h> #include <conio.h> #include <dos.h> #include <stdlib.h> #include <bios.h> #include "sysp.h" // Номер форматируемой дорожки #define TRK 20 // Код размера сектора - 256 байт #define SEC_SIZE 1 union REGS inregs, outregs; char _far diskbuf[512]; char _far diskbuf1[512]; char buf[80]; void main(void); void main(void) { struct diskinfo_t di; unsigned status; unsigned char old_sec_size, old_fill_char, old_eot; int i, j; DPT _far *dpt_ptr; printf("\nПрограмма уничтожит содержимое" "\n20-й дорожки диска А:." "\nЖелаете продолжить? (Y,N)\n"); // Ожидаем ответ оператора и анализируем его i = getch(); if((i != 'y') && (i != 'Y')) exit(-1); // Получаем адрес таблицы параметров дискеты dpt_ptr = get_dpt(); // Сохраняем старые значения из таблицы параметров old_sec_size = dpt_ptr->sec_size; old_fill_char = dpt_ptr->fill_char; old_eot = dpt_ptr->eot; // Устанавливаем в таблице параметров дискеты // код размера сектора, символ заполнения при // форматировании, количество секторов на дорожке dpt_ptr->sec_size = SEC_SIZE; dpt_ptr->fill_char = 0x77; dpt_ptr->eot = 15; // Устанавливаем тип диска inregs.h.ah = 0x17; inregs.h.al = 3; inregs.h.dl = 0; int86(0x13, &inregs, &outregs); // Устанавливаем среду для форматирования inregs.h.ah = 0x18; inregs.h.ch = TRK; inregs.h.cl = dpt_ptr->eot; inregs.h.dl = 0; int86(0x13, &inregs, &outregs); // Подготавливаем параметры для функции форматирования di.drive = 0; di.head = 0; di.track = TRK; di.sector = 1; di.nsectors = 15; di.buffer = diskbuf; // Подготавливаем буфер формата для 15-ти секторов for(i=0, j=1; j<16; i += 4, j++) { diskbuf[i] = TRK; diskbuf[i+1] = 0; diskbuf[i+2] = j; diskbuf[i+3] = SEC_SIZE; } // Вызываем функцию форматирования дорожки status = _bios_disk(_DISK_FORMAT, &di) >> 8; printf("\nФорматирование завершилось с кодом: %d",status); // Записываем информацию в нестандартный сектор printf("\nВведите строку для записи в нестандартный сектор," "\nдлина строки не должна превышать 80 байтов" "\n->"); gets(buf); strcpy(diskbuf,buf); di.drive = 0; di.head = 0; di.track = 20; di.sector = 1; di.nsectors = 1; di.buffer = diskbuf; status = _bios_disk(_DISK_WRITE, &di) >> 8; if(status) { printf("\nОшибка при записи в нестандартный сектор: %d", status); exit(-1); } di.drive = 0; di.head = 0; di.track = 20; di.sector = 1; di.nsectors = 1; di.buffer = diskbuf1; for(i = 0; i < 3; i++) { status = _bios_disk(_DISK_READ, &di) >> 8; if( !status ) break; } printf("\nПрочитано из нестандартного сектора:\n%s\n", diskbuf1); // Восстанавливаем старые значения в // таблице параметров дискеты dpt_ptr->sec_size = old_sec_size; dpt_ptr->fill_char = old_fill_char; dpt_ptr->eot = old_eot; exit(0); }
Какую информацию можно записать в нестандартный сектор?
Если вы делаете инсталляционную (установочную) дискету, которая рассчитана на ограниченное количество инсталляций, нестандартный сектор - самое подходящее место для хранения счетчика инсталляций. Даже такие программы, как Norton Utilities или Norton Disk Editor, не помогут прочитать или изменить значение этого счетчика. В этот же сектор можно записать и другую информацию, необходимую для правильной инсталляции защищенного программного обеспечения.
Другой пример - использование нестандартного номера дорожки. Программа форматирует дорожку (стандартным образом) с номером 81. Обычно считается, что дискеты могут содержать 40 или 80 дорожек, соответственно, с номерами 0...39 или 0...79, однако возможно использование и дорожек с большими номерами. Обычные программы копирования будут копировать только 40 или 80 дорожек, "не заметив" нашей лишней дорожки.
Этим мы и воспользуемся, записав на 81-ю дорожку контрольную информацию. Для разнообразия в примере используем функции общего управления вводом/выводом GENERIC IOCTL.
Первая программа предназначена для форматирования 81-й дорожки:
#include <dos.h> #include <stdio.h> #include <malloc.h> #include <errno.h> #include "sysp.h" void main(void); void main(void) { union REGS reg; struct SREGS segreg; DPB _far *dbp; DPB_FORMAT _far *dbp_f; int sectors, i; printf("\nПрограмма уничтожит содержимое" "\n81-й дорожки диска А:." "\nЖелаете продолжить? (Y,N)\n"); // Ожидаем ответ оператора и анализируем его i = getch(); if((i != 'y') && (i != 'Y')) exit(-1); // Заказываем память для блока параметров устройства dbp = malloc(sizeof(DPB)); // Заказываем память для блока параметров устройства, // который будет использован для форматирования dbp_f = malloc(sizeof(DPB_FORMAT)); if(dbp == NULL || dbp_f == NULL) { printf("\nМало оперативной памяти!"); exit(-1); } // Получаем текущие параметры диска А: dbp->spec = 0; // Вызываем подфункцию 0Dh для выполнения // операции чтения текущих параметров диска А: reg.x.ax = 0x440d; reg.h.bl = 1; reg.x.cx = 0x0860; reg.x.dx = FP_OFF(dbp); segreg.ds = FP_SEG(dbp); intdosx(®, ®, &segreg); // Проверяем флаг переноса if(reg.x.cflag != 0) { printf("\nОшибка: %d",reg.x.ax); exit(-1); } // Заполняем блок парметров для форматирования. dbp->spec = 5; // Считываем из BPB количество секторов на дорожке sectors = dbp->bpb.seccnt; // Подготавливаем таблицу, описывающую формат дорожки // Записываем количество секторов на дорожке dbp->trkcnt = sectors; // Для каждого сектора на дорожке в таблицу // записываем его номер и размер. for(i = 0; i < sectors; i++) { dbp->trk[i].no = i+1; dbp->trk[i].size = 512; } // Устанавливаем новые параметры для диска А: reg.x.ax = 0x440d; reg.h.bl = 1; reg.x.cx = 0x0840; reg.x.dx = FP_OFF(dbp); segreg.ds = FP_SEG(dbp); intdosx(®, ®, &segreg); // Проверяем флаг переноса if(reg.x.cflag != 0) { printf("\nОшибка: %d",reg.x.ax); exit(-1); } // Подготавливаем блок параметров устройства, // который будет использован при вызове // операции проверки возможности форматирования // дорожки dbp_f->spec = 1; dbp_f->head = 0; dbp_f->track = 81; reg.x.ax = 0x440d; reg.h.bl = 1; reg.x.cx = 0x0842; reg.x.dx = FP_OFF(dbp_f); segreg.ds = FP_SEG(dbp_f); intdosx(®, ®, &segreg); // Проверяем флаг переноса if(reg.x.cflag != 0) { printf("\nОшибка: %d",reg.x.ax); exit(-1); } // Если указанный формат дорожки поддерживается, // поле специальных функций будет содержать 0. // Проверяем это. if(dbp_f->spec != 0) { printf("\nФормат дорожки не поддерживается!"); exit(-1); } // Заполняем блок параметров для выполнения // операции форматирования dbp_f->spec = 0; dbp_f->head = 0; dbp_f->track = 81; // Форматируем дорожку с номером 81, головка 0 reg.x.ax = 0x440d; reg.h.bl = 1; reg.x.cx = 0x0842; reg.x.dx = FP_OFF(dbp_f); segreg.ds = FP_SEG(dbp_f); intdosx(®, ®, &segreg); // Проверяем флаг переноса if(reg.x.cflag != 0) { printf("\nОшибка: %d",reg.x.ax); exit(-1); } // Освобождаем буфера free(dbp); free(dbp_f); exit(0); }
Для записи и последующего чтения информации на дополнительную дорожку можно использовать следующую программу:
#include <dos.h> #include <stdio.h> #include <malloc.h> #include <errno.h> #include "sysp.h" void main(void); void main(void) { union REGS reg; struct SREGS segreg; DPB_WR _far *dbp_wr; char buf[1000]; char buf1[80]; int sectors, i; // Заказываем память для блока параметров устройства, // который будет использован для чтения/записи dbp_wr = malloc(sizeof(DPB_WR)); if(dbp_wr == NULL) { printf("\nМало оперативной памяти!"); exit(-1); } // Записываем информацию в нестандартный сектор printf("\nВведите строку для записи в нестандартный сектор," "\nдлина строки не должна превышать 80 байтов" "\n->"); gets(buf1); strcpy(buf,buf1); // Заполняем блок параметров для выполнения // операции записи. dbp_wr->spec = 0; dbp_wr->head = 0; dbp_wr->track = 81; dbp_wr->sector = 0; dbp_wr->sectcnt = 1; dbp_wr->buffer = buf; // Выполняем операцию записи reg.x.ax = 0x440d; reg.h.bl = 1; reg.x.cx = 0x0841; reg.x.dx = FP_OFF(dbp_wr); segreg.ds = FP_SEG(dbp_wr); intdosx(®, ®, &segreg); // Проверяем флаг переноса if(reg.x.cflag != 0) { printf("\nОшибка при записи: %d",reg.x.ax); exit(-1); } // Заполняем блок параметров для выполнения // операции чтения. dbp_wr->spec = 0; dbp_wr->head = 0; dbp_wr->track = 81; dbp_wr->sector = 0; dbp_wr->sectcnt = 1; dbp_wr->buffer = buf; // Выполняем операцию чтения дорожки reg.x.ax = 0x440d; reg.h.bl = 1; reg.x.cx = 0x0861; reg.x.dx = FP_OFF(dbp_wr); segreg.ds = FP_SEG(dbp_wr); intdosx(®, ®, &segreg); // Проверяем флаг переноса if(reg.x.cflag != 0) { printf("\nОшибка при чтении: %d",reg.x.ax); exit(-1); } printf("\nПрочитано из нестандартного сектора:\n%s\n", buf); // Освобождаем буфер free(dbp_wr); exit(0); }
Более интересный способ защиты дискет от копирования связан с использованием при форматировании нестандартного чередования секторов на дорожке. В приведенной ниже программе использовано "обратное" расположение секторов - вначале идет сектор с номером 15, затем 14 и т.д.
#include <stdio.h> #include <conio.h> #include <dos.h> #include <stdlib.h> #include <bios.h> #include "sysp.h" // Номер форматируемой дорожки #define TRK 20 // Код размера сектора - 512 байт #define SEC_SIZE 2 union REGS inregs, outregs; char _far diskbuf[512]; void main(void); void main(void) { struct diskinfo_t di; unsigned status; unsigned char old_sec_size, old_fill_char, old_eot; int i, j; DPT _far *dpt_ptr; printf("\nПрограмма уничтожит содержимое" "\n20-й дорожки диска А:." "\nЖелаете продолжить? (Y,N)\n"); // Ожидаем ответ оператора и анализируем его i = getch(); if((i != 'y') && (i != 'Y')) exit(-1); // Получаем адрес таблицы параметров дискеты dpt_ptr = get_dpt(); // Сохраняем старые значения из таблицы параметров old_sec_size = dpt_ptr->sec_size; old_fill_char = dpt_ptr->fill_char; old_eot = dpt_ptr->eot; // Устанавливаем в таблице параметров дискеты // код размера сектора, символ заполнения при // форматировании, количество секторов на дорожке dpt_ptr->sec_size = SEC_SIZE; dpt_ptr->fill_char = 0xf6; dpt_ptr->eot = 15; // Устанавливаем тип диска inregs.h.ah = 0x17; inregs.h.al = 3; inregs.h.dl = 0; int86(0x13, &inregs, &outregs); // Устанавливаем среду для форматирования inregs.h.ah = 0x18; inregs.h.ch = TRK; inregs.h.cl = dpt_ptr->eot; inregs.h.dl = 0; int86(0x13, &inregs, &outregs); // Подготавливаем параметры для функции форматирования di.drive = 0; di.head = 0; di.track = TRK; di.sector = 1; di.nsectors = 15; di.buffer = diskbuf; // Подготавливаем буфер формата для 15-ти секторов // Используем обратный порядок расположения секторов // на дорожке for(i=0, j=15; j>0; i += 4, j--) { diskbuf[i] = TRK; diskbuf[i+1] = 0; diskbuf[i+2] = j; diskbuf[i+3] = SEC_SIZE; } // Вызываем функцию форматирования дорожки status = _bios_disk(_DISK_FORMAT, &di) >> 8; printf("\nФорматирование завершилось с кодом: %d",status); // Восстанавливаем старые значения в // таблице параметров дискеты dpt_ptr->sec_size = old_sec_size; dpt_ptr->fill_char = old_fill_char; dpt_ptr->eot = old_eot; exit(0); }
Для анализа используемого чередования секторов можно использовать следующую программу, которая пытается прочитать подряд два расположенных рядом сектора с номерами 1 и 2. Если сектора на дорожке чередуются обычным образом, то сектора с номерами 1 и 2 находятся рядом. Если же дорожка отформатирована приведенной выше программой, то эти сектора находятся на максимальном удалении друг от друга.
Программа анализирует время, необходимое на то, чтобы 50 раз подряд прочитать эти два сектора на двадцатой дорожке. Вначале используется головка 0 - это нестандартная дорожка, затем - головка 1, для которой раньше было выполнено стандартное форматирование.
На экран выводятся не только времена, но и их отношение, которое и может служить критерием при определении того, с чем мы имеем дело - с оригинальной дискетой или с ее копией.
#include <stdio.h> #include <conio.h> #include <bios.h> #include <dos.h> #include <stdlib.h> #include <time.h> char _far diskbuf[1024]; void main(void); void main(void) { unsigned status = 0, i, j; struct diskinfo_t di; time_t start, end; float t1, t2; // Читаем первый сектор дорожки для синхронизации таймера di.drive = 0; di.head = 0; di.track = 20; di.sector = 1; di.nsectors = 1; di.buffer = diskbuf; for(i = 0; i < 3; i++) { status = _bios_disk(_DISK_READ, &di) >> 8; if( !status ) break; } // Отсчет времени начинаем сразу после чтения сектора, // это позволит компенсировать время, необходимое на разгон // мотора дисковода. start = clock(); // Повторяем 50 раз чтение секторов с номерами 1 и 2 for(j=0; j<50; j++) { di.drive = 0; di.head = 0; di.track = 20; di.sector = 1; di.nsectors = 2; di.buffer = diskbuf; for(i = 0; i < 3; i++) { status = _bios_disk(_DISK_READ, &di) >> 8; if( !status ) break; } } end = clock(); t1 = ((float)end - start) / CLK_TCK; printf("Время для головки 0: %5.1f\n",t1); // Выполняем аналогичную процедуру для дорожки, // которая была отформатирована обычным способом. di.drive = 0; di.head = 1; di.track = 20; di.sector = 1; di.nsectors = 1; di.buffer = diskbuf; for(i = 0; i < 3; i++) { status = _bios_disk(_DISK_READ, &di) >> 8; if( !status ) break; } start = clock(); for(j=0; j<50; j++) { di.drive = 0; di.head = 1; di.track = 20; di.sector = 1; di.nsectors = 2; di.buffer = diskbuf; for(i = 0; i < 3; i++) { status = _bios_disk(_DISK_READ, &di) >> 8; if( !status ) break; } } end = clock(); t2 = ((float)end - start) / CLK_TCK; printf("Время для головки 0: %5.1f\n",t2); printf("\nОтношение времен чтения для головок 0 и 1: %5.1f", t1/t2); }
Приведенный выше метод защиты дискет от копирования проверен на программе COPY2PC. Оказалось, что эта программа не копирует чередование секторов на дорожке.
Обычно процесс установки защищенного от копирования программного пакета выглядит следующим образом:
Последний шаг необходим для того, чтобы защищенный от копирования программный пакет стало невозможно перенести на другой компьютер, используя программы копирования файлов или разгрузки дисков на дискеты или магнитные ленты с последующим восстановлением на жестком диске другого компьютера.
В этом разделе книги мы приведем несколько методов настройки программного обеспечения на конкретный компьютер:
Первый способ предполагает исследование расположения какого-либо достаточно длинного файла, записанного на диск в процессе установки на предмет определения его расположения на диске.
Программа установки, пользуясь таблицей размещения файлов FAT, определяет список кластеров, распределенных файлу и записывает этот список в конец защищаемого файла или в отдельный файл. Можно использовать, например, файл конфигурации, предназначенный для хранения текущих параметров программного пакета. Список кластеров можно зашифровать, сложив его, например, с использованием логики "ИСКЛЮЧАЮЩЕЕ ИЛИ", с каким-либо числом.
После запуска программный пакет определяет расположение защищенного файла на диске и сравнивает его с записанным при установке. Если расположение изменилось - запущена незаконная копия программного пакета.
Какие недостатки у этого способа?
Прежде всего, невозможна оптимизация диска такими программами, которые могут изменить расположение файлов на диске, например, Norton Speed Disk.
Второй недостаток связан с тем, что можно взять вторую машину с таким же типом жесткого диска, и с помощью несложной программы переписать содержимое всех секторов с диска одной машины на диск другой.
Первый недостаток можно преодолеть, используя процедуру "деинсталляции", или съема программного пакета с диска компьютера. Эта процедура заключается в том, что с жесткого диска удаляются файлы программного продукта, после чего счетчик инсталляций на дистрибутивной дискете уменьшается на единицу. После выполнения всех необходимых операций с диском можно выполнить повторную установку программного пакета.
Таким образом, можно переносить программный пакет с одной машины на другую, но нельзя его размножить на несколько компьютеров.
Второй недостаток устранить сложнее, так как при идентичности структуры дисков расположение файлов на нем будет идентично и с этим ничего нельзя сделать.
Запись контрольной информации в неиспользуемый участок файла сделает невозможным копирование программного пакета утилитами разгрузки дисков, но по-прежнему остается возможность использования программ копирования по секторам содержимого диска.
Метод проверки версии или контрольных сумм программы BIOS пригоден для защиты от копирования между компьютерами, содержащими разные версии BIOS. Однако при покупке партии компьютеров все они почти наверняка будут иметь одинаковый BIOS, поэтому, хотя этот метод достаточно прост, его эффективность относительно невысока.
Последний метод - проверка производительности отдельных подсистем компьютера - кажется нам наиболее пригодным, так как такие характеристики, как быстродействие диска или процессора являются в достаточной степени уникальными для каждого конкретного компьютера.
Мы приведем несколько примеров программ, демонстрирующих использование некоторых из перечисленных выше методов.
Сначала покажем, как получить список кластеров, распределенных файлу. Вы уже знаете, как получить этот список, пользуясь таблицей размещения файлов и дескриптором файла в каталоге. Сложность здесь заключается в том, что операционная система не предоставляет никакого документированного способа получения номера первого кластера, распределенного файлу. Вам придется последовательно просматривать дерево каталогов до тех пор, пока вы не доберетесь до вашего файла. Для просмотра дерева каталогов вам придется использовать непосредственное чтение диска и таблицу размещения файлов. Лишь найдя нужный вам каталог (содержащий файл, для которого нужно получить список кластеров) и прочитав каталог как файл в память, вы сможете воспользоваться дескриптором файла для определения номера первого кластера, распределенного файлу.
К сожалению, непосредственное чтение диска - единственная документированная возможность получения списка кластеров, распределенных файлу.
Мы рассмотрим более простой, но, увы, недокументированный способ получения списка кластеров, использующий таблицу открытых файлов. Эта таблица была описана в разделах книги, посвященных векторной таблице связи.
Напомним, что для каждого открытого файла эта таблица содержит, кроме всего прочего, номер первого кластера, распределенного файлу, и номер кластера файла, к которому только что выполнялось обращение - last_clu.
Идея заключается в том, чтобы открыв файл и найдя его в таблице файлов, последовательно считывать его порциями, равными по размеру одному кластеру. В процессе считывания файла поле, содержащее номер кластера last_clu будет последовательно принимать значения, равные номерам всех распределенных файлу кластеров.
Размер кластера можно получить из блока параметров BIOS BPB, который находится в загрузочной записи диска.
Приведем пример программы, которая проделывает все это и выводит на экран содержимое таблицы файлов и список кластеров для файла, путь которого передается программе в качестве параметра:
#include <dos.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <io.h> #include "sysp.h" void main(int argc, char *argv[]); void show(DFCB far *); void main(int argc, char *argv[]) { CVT far *cvt; DFT far *dft; unsigned i,j,k; DFCB far *dfcb, far *file_dfcb; int handle, flag, disk; BOOT far *boot_rec; int status; char *buf; char drive[_MAX_DRIVE], dir[_MAX_DIR]; char fname[_MAX_FNAME], ext[_MAX_EXT]; char name[12]; printf("Информация о расположении файла\n" "Copyright Frolov A. (C),1990\n"); printf("Исследуем расположение файла '%s'\n", argv[1]); // Открываем файл, для которого будем получать список кластеров handle = open(argv[1], O_BINARY); if(handle == 0) { printf("Ошибка при открытии файла!\n"); exit(-1); } // Разбиваем путь файла на составляющие компоненты: // - буква диска; // - каталог; // - имя файла; // - расширение имени _splitpath(argv[1], drive, dir, fname, ext); // Комбинируем строку из имени и расширения strcpy(name,fname); for(i=0; i<8; i++) { if(name[i] == 0) break; } for(; i<8; i++) name[i] = ' '; name[8] = 0; strcat(name, &ext[1]); // Преобразуем строку имени в заглавные буквы strupr(name); // Вычисляем номер дисковода drive[0] = toupper(drive[0]); disk = drive[0] - 'A'; printf("%s - %s - %s - %s : %s\n", drive, dir, fname, ext, name); cvt=get_mcvt(); // Адрес векторной таблицы связи dft=get_fdft(cvt); // Адрес начала таблицы файлов // Сбрасываем флаг поиска файла flag = 0; for(;;) { if(dft == (DDCB far *)0) break; // Конец таблицы файлов i=dft->file_count; for(j=0;j<i;j++) { // Цикл по файловым управляющим блокам dfcb=(&(dft->dfcb))+j; // Адрес DFCB файла // Ищем файл в таблице открытых файлов k = memcmp(name,dfcb->filename,11); if(k == 0) { printf("Файл найден!\n"); printf("\nDFCB файла: %Fp\n\n",dfcb); // Запоминаем адрес таблицы для найденного файла file_dfcb = dfcb; // Показываем содержимое таблицы для найденного файла show(file_dfcb); flag = 1; break; } } dft=get_ndft(dft); if(flag == 1) break; } if(flag == 0) { printf("Файл не найден"); close(handle); exit(-1); } // Заказываем буфер для чтения BOOT-записи. // Адрес буфера присваиваем FAR-указателю. boot_rec = malloc(sizeof(*boot_rec)); // Читаем загрузочную запись в буфер status = getboot((BOOT far*)boot_rec, disk); // Вычисляем размер кластера в байтах i = boot_rec->bpb.clustsize * boot_rec->bpb.sectsize; printf("\nРазмер кластера, байтов : %d",i); // Если произошла ошибка (например, неправильно указано // обозначение диска), завершаем работу программы if(status) { printf("\nОшибка при чтении BOOT-сектора"); close(handle); exit(-1); } buf = malloc(i); printf("\nСписок кластеров файла:\n"); // Читаем файл по кластерам, выводим номер // последнего прочитанного кластера, который // берем из таблицы файлов for(;;) { read(handle, buf, i); if(eof(handle)) break; printf("%d ",file_dfcb->last_clu); } close(handle); free(boot_rec); free(buf); exit(0); } // Функция для отображения содержимого таблицы файлов void show(DFCB far *dfcb) { int k; printf("Имя файла: "); for(k=0;k<11;k++) { putchar(dfcb->filename[k]); } printf("\nКоличество file handles: %d\n" "Режим доступа: %d\n" "Поле reserv1: %04X\n" "Информация об устройстве: %04X\n" "Адрес драйвера: %Fp\n" "Начальный кластер: %d\n" "Время: %04X\n" "Дата: %04X\n" "Размер файла в байтах: %ld\n" "Текущее смещение в файле: %ld\n" "Поле reserv2: %04X\n" "Последний прочитанный кластер: %d\n" "Сегмент PSP владельца файла: %04X\n" "Поле reserv7: %d\n" "--------------------------------------\n\n", dfcb->handl_num, dfcb->access_mode, dfcb->reserv1, dfcb->dev_info, dfcb->driver, dfcb->first_clu, dfcb->time, dfcb->date, dfcb->fl_size, dfcb->offset, dfcb->reserv2, dfcb->last_clu, dfcb->ownr_psp, dfcb->reserv7); }
Получив список кластеров, распределенных защищаемому файлу, вы можете зашифровать его и записать, например, в конец защищаемого файла. Впоследствии, перед началом работы, защищенная программа может проверить свое расположение на диске и сравнить его с записанным в зашифрованном списке.
Рассмотрим теперь использование BIOS для защиты от копирования программ с жесткого диска.
Для защиты используем поля, уникальные для
конкретной версии BIOS. Это дата изготовления и код
типа компьютера. Вся эта информация находится в
BIOS, начиная с адреса F000:FFF5 и имеет следующий
формат:
F000:FFF5 (8) |
дата изготовления BIOSBIOS; |
F000:FFFC (2) |
не используется; |
F000:FFFE (1) |
код типа компьютера. |
Программа инсталляции в процессе установки защищенного программного обеспечения на жесткий диск может прочитать эти поля и записать их в зашифрованном виде, например, в один из файлов защищаемого программного пакета.
Для удобства работы с идентификатором BIOS BIOS файл sysp.h содержит определение типа BIOS_ID:
#pragma pack(1) typedef struct _BIOS_ID_ { char date[8]; unsigned reserve; char pc_type; } BIOS_ID; #pragma pack()
Мы подготовили функцию, которая возвращает адрес идентификатора BIOSBIOS:
/** *.Name getbiosi * *.Title Получить адрес идентификатора BIOS * *.Descr Функция возвращает адрес идентификатора BIOS, * имеющего следующую структуру: * * typedef struct _BIOS_ID_ { * * char date[8]; * unsigned reserve; * char pc_type; * * } BIOS_ID; * * В этой структуре: * * date - дата изготовления BIOS; * reserve - зарезервировано; * pc_type - код типа компьютера. * *.Params Нет * *.Return Адрес идентификатора BIOS **/ #include <stdio.h> #include "sysp.h" BIOS_ID _far *getbiosi(void) { return((BIOS_ID _far *)(FP_MAKE(0xf000, 0xfff5))); }
В качестве примера приведем текст программы, отображающей на экране содержимое идентификатора BIOS:
#include <stdio.h> #include <stdlib.h> #include "sysp.h" void main(void); void main(void) { BIOS_ID _far *id; int i; id = getbiosi(); printf("\nИдентификатор BIOS:" "\n" "\nДата изготовления: "); for(i=0; i<8; i++) { putch(id->date[i]); } printf("\nРезервное поле: %04.4X" "\nТип компьютера: %02.2X" "\n", id->reserve, (unsigned char)id->pc_type); exit(0); }
Защищая свои программы от несанкционированного копирования, не следует забывать о таких средствах "взлома", как пошаговые отладчики - CodeView, Advanced Fullscreen Debug, AT86 и другие.
Используя отладчики, искушенные в своем деле системные программисты рано или поздно смогут обнаружить и отключить (или обмануть) средства защиты от копирования.
В этом разделе книги мы расскажем о некоторых приемах, позволяющих затруднить обнаружение средств защиты.
Стандартным методом, использующимся для обнаружения средств защиты от копирования, является дизассемблирование программы установки программного пакета или выполнение его под управлением пошагового отладчика. Листинг, получаемый в процессе дизассемблирования, оказывает большую услугу при использовании отладчика, поэтому эти два средства - дизассемблирование и использование отладчика - обычно используются вместе.
Соответственно, требуются отдельные средства для борьбы с дизассемблером и для защиты от отладчиков.
Для затруднения дизассемблирования лучше всего подходит шифрование отдельных участков программ или всей программы целиком. Например, часть программы-инсталлятора можно оформить в виде отдельной COM-программы. После трансляции исходного текста этой программы ее можно зашифровать тем или иным способом, и в зашифрованном виде подгружать в память как программный оверлей. После загрузки программу следует расшифровать в оперативной памяти и передать ей управление.
Еще лучше выполнять динамическое расшифровывание программы по мере ее выполнения, когда участки программы расшифровываются непосредственно перед использованием и после использования сразу же уничтожаются.
При расшифровывании можно копировать участки программы в другое место оперативной памяти. Пусть, например, программа состоит из нескольких частей. После загрузки ее в оперативную память управление передается первой части программы. Эта часть предназначена для расшифровки второй части, располагающейся в памяти вслед за первой.
Задача второй части - перемещение третьей части программы на место уже использованной первой части и расшифровка ее там.
Третья часть, получив управление, может проверить свое расположение относительно префикса программного сегмента и, в случае правильного расположения (сразу вслед за PSP), начать загрузку сегментных регистров такими значениями, которые необходимы для выполнения четвертой - инсталляционной - части программы.
Если попытаться дизассемблировать программу, составленную подобным образом, то из этого ничего не получится.
Второй способ борьбы с дизассемблером является, по своей сути, борьбой с человеком, занимающимся дизассемблированием. Он заключается в увеличении размера загрузочного модуля программы до сотни-другой килобайтов и в усложнении структуры программы.
Объем листинга, получающегося при дизассемблировании программы размером в 30-40 килобайтов, достигает 1-1,5 мегабайтов. Поэтому большие размеры инсталляционной программы могут сильно увеличить время обнаружения средств защиты.
Что такое усложнение структуры программы, достаточно понятно само по себе. Авторам известна программа, использующая для обращении к одной и той же области памяти, содержащей многочисленные переменные, разные сегментные адреса. Поэтому очень трудно сразу догадаться, что на самом деле программа работает с одной и той же областью памяти.
Мы не будем дальше рассказывать о способах усложнения структуры программы, оставив простор для фантазии читателя. Вместо этого перейдем к борьбе с трассировкой программы пошаговыми отладчиками.
Можно предложить две группы средств защиты от трассировки. В первую группу входят средства блокировки работы самих отладчиков, делающие невозможным трассировку программы. Вторая группа средств направлена на определение факта работы программы под управлением отладчика.
К первой группе средств относится:
Напомним, что прерывание INT 1 и INT 3 (соответственно, прерывание для пошаговой работы и прерывание по однобайтовой команде INT) интенсивно используются отладчиками. Первое прерывание позволяет отладчику получать управление после выполнения каждой команды трассируемой программы. С помощью второго прерывания отладчик может устанавливать точки останова, заменяя байты программы на команду однобайтового прерывания.
Защищенная от трассировки программа должна подготовить свои собственные обработчики для этих прерываний и "незаметно" для человека, занимающегося трассировкой, записать их адреса в таблицу векторов прерываний.
Эти обработчики прерываний могут не делать ничего, т.е. состоять из одной команды IRET, или выполнять какие-либо действия, фиксирующие факт работы программы под контролем отладчика.
В ходе трассировки программы (при ее пошаговом выполнении) вам необходимо нажимать на клавиши - для перехода к очередной команде. Это можно использовать для блокировки отладчика.
Запретив прерывания командой CLI и переназначив клавиатурное прерывание на себя, программа установки может выполнять какие-либо действия, не требующие работы оператора, производящего установку, с клавиатурой. Обработчик клавиатурного прерывания защищенной от трассировки программы должен фиксировать прерывания, например, установкой флага.
Если программа работает не под контролем отладчика, прерывания во время этого участка программы невозможны (они запрещены командой CLI).
Если же используется режим пошагового выполнения программы под управлением отладчика, отладчик разрешает клавиатурные прерывания, невзирая на то, что была выдана команда CLI. Наш обработчик клавиатурного прерывания в этом случае зафиксирует работу в режиме трассировки.
Аналогично можно воспользоваться прерыванием таймера.
Защищенная программа устанавливает свой обработчик для прерывания таймера, который взводит флаг в случае прихода прерывания от таймера. В подходящий момент времени она запрещает прерывания и выполняет какую-либо работу, периодически проверяя флаг.
Если программа работает в пошаговом режиме, команда запрета прерываний CLI не работает, и флаг будет взведен, сигнализируя работу под контролем отладчика.
Последний метод, который мы рассмотрим, основан на использовании архитектурной особенности процессоров 8086, 80286, 80386 и 80486 - наличии конвейера команд.
Для увеличения производительности во всех этих процессорах используется предварительная выборка команд во внутреннюю очередь команд, располагающуюся физически на кристалле процессора.
Метод заключается в модификации команды программы, находящейся вблизи от той команды, которая выполняет эту модификацию.
Если программа работает не под управлением отладчика, к моменту выполнения команды модификации байт модифицируемой команды уже находится во внутренней очереди команд. Следовательно, никакой модификации не произойдет.
Если же программа работает под управлением отладчика, то изменение байта команды будет выполнено правильно, так как очередь команд будет заполнена командами самого отладчика.
Приведем программу, демонстрирующую описанный выше метод:
; Эта программа демонстрирует способ защиты от ; отладки, основанный на использовании архитектурной ; особенности процессоров 8086, 80286, 80386, 80486 - ; наличии внутреннего конвейера команд TITLE BUG .MODEL tiny .STACK 100h .DATA msg DB "Программа работает без отладчика!", 13, 10, "$" .CODE .STARTUP ; Запрещаем прерывания cli ; Вызываем программу проверки call _TEST ; Разрешаем прерывания sti .EXIT 0 ; ------------------------------- ; Программа проверки _TEST PROC near ; Выполняем модификацию команды с меткой next. ; Вместо команды ret записываем команду nop. mov BYTE PTR next, 90h next: ret ; Если модификация не произошла, это означает, что ; программа выполняется под управлением отладчика. ; Выводим сообщение о работе под контролем отладчика. mov ah, 9h mov dx, OFFSET msg int 21h ret _TEST ENDP END
Эта программа выводит сообщение о работе без отладчика, если происходит изменение команды, и не выводит ничего, если изменения команды не происходит (т.е. если программа работает под управлением отладчика).
Метод был проверен для отладчиков Code View, AFD, AT86,
утилиты отладки MS-DOS DEBUG и дал положительные
результаты.