1. О программах-отмычках, взломанных файлах и несанкционированном копировании.
2. Идентификация пользователя: "СВОЙ" - "ЧУЖОЙ"?
3. Может ли компьютер стать графологом?
4. Как защититься от "размножения".
6. Самомодификация программ - эффектно и полезно.
7. Исполняемый модуль - что можно сделать без исходных текстов?
8. Как очистить прогармму от вирусов и пристыкованных защит.
Приложение 1. Словарь терминов.
В редакцию журнала "ТЕХНИКА-МОЛОДЕЖИ" нередко приходили письма с просьбой публиковать компьютерные программы. Однако массовый научно-художественный журнал не мог давать специфические материалы. Поэтому было решено издать приложение, укомплектованное дискетой: прочитал, и тут же запустил программу с дискеты. А тематика самая разная - от алгоритмов игр и борьбы с вирусами до справочных материалов по операционным системам, распространенным программам и периферийному оборудованию (например, как подключить принтер и сконструировать собственные шрифты). Ну а первые выпуски посвятили защите информации. Все алгоритмы, рассчитанные на непосредственное применение в работе, можно использовать и как пособие в практических занятиях. Это актуально, ведь учебников по способам пресечения информационного воровства до сих пор не существовало.
Компьютеры стремительно проникают во все сферы нашей жизни. Но с ростом автоматизации перед рядовым пользователем возникает и целый букет проблем, о существовании которых он даже не подозревал.
Начнем с "защиты информации" - тема выбрана не случайно. Несмотря на недавно принятый Закон о защите программ и баз данных, нелегальное копирование, компьютерные диверсии (вирусы, "бомбы", "троянские кони"), а также количество финансовых преступлений с использованием вычислительной техники - не уменьшаются. Поэтому сохранность информации является проблемой номер один не только у нас, но и во всем мире. Ее усугубляют отсутствие элементарных знаний у большинства пользователей, владеющих лишь простейшими навыками копирования файлов, выработанная с годами привычка никогда не покупать программы и кажущаяся легкость бесплатного добывания дорогостоящего программного обеспечения.
Впрочем, для любителей головоломок и нестандартных решений тема защиты информации интересна сама по себе. Динамичность и сложность, в сочетании с краткостью применяемых алгоритмов, возможность предугадывать действия потенциальных хакеров ("взломщиков" программ) роднят ее с увлекательными играми, основанными на интеллектуальном поединке двух противников.
Как предупредить покушения на информацию? Можно ограничить доступ к ней посторонним, встроить в компьютер замок, отключающий клавиатуру, поставить сейф для дискет, вставить в один из разъемов ПК парольную "заглушку"... Из всего арсенала организационных, технических и программно-аппаратных средств выделим чисто программные. Они просты в тиражировании, технологичны в изготовлении и применении, не требуют каких-либо производстводственных мощностей, в то же время обеспечивают достаточный уровень защищенности. Впрочем, надежность в этом случае определяется знанием последних достижений общей теории программирования и умением разработчика использовать специальные приемы. Естественно, что новинки систем охраны влекут и совершенствование способов "взлома" - извечное противоборство "щита и меча".
Вот некоторые из наиболее популярных приемов защиты.
Преобразование ФОРМАТА ДИСКЕТЫ - наиболее простой путь предотвращения ее копирования средствами DOS. Даже незначительные изменения в структуре или расположении системных таблиц или каталога приводят к тому, что дискета становится непонятной "операционке". Можно "перепутать" адреса секторов, на которых расположены защищаемые файлы; пометить отдельные кластеры, занятые данными, как сбойные; переделав запись в BOOT-секторе, изъять из доступного системе пространства несколько треков (дорожек); в конце-концов - просто применить иную структуру, взяв за основу аналог из других операционных систем (например, RT-11 от ДВК). Разумеется, работать с такой дискетой сможет только специальная программа, полностью заменяющая стандартные функции ввода-вывода.
Одним из способов ШИФРАЦИИ данных является их архивация по специальным алгоритмам, что позволяет к тому же и сэкономить место на магнитном носителе. Правда, сами кодирующие блоки программ оказываются слабым местом. Их исследование под отладчиком или дизассемблером позволяет хакеру понять алгоритм шифрации и повторить его. Поэтому особенно актуальна ЗАЩИТА ОТ ИССЛЕДОВАНИЯ. Важную роль здесь играет стиль программирования. В отличие от общепринятых "наглядности" и "структурности", для охранных механизмов следует применять "изощренность", то есть такой стиль, который позволит получить сложный и запутанный исполняемый модуль. Еще лучше - если он будет саморазворачивающимся в процессе работы (программа "дописывает" свои части, отсутствующие на винчестере). Очень полезными могут оказаться приемы, о которых журнал "ТМ" рассказывал в 1986 - 1988 годах в разделе "Клуб электронных игр" (использование кода оператора в качестве операнда, набора констант по прямому назначению и как подпрограммы, передача управления в середину сложной двух-трехбайтовой команды, проход "своим ходом" через данные и другие хитрости, не только экономящие память, но и запутывающие алгоритм).
Но это пассивная защита, а в качестве активной - рекомендуем ПРЕСЕКАТЬ ПОПЫТКИ ИССЛЕДОВАНИЯ ИЛИ НЕСАНКЦИОНИРОВАННОГО "РАЗМНОЖЕНИЯ": периодически определять контрольную сумму всех кодов образа задачи в процессе работы (не "отрезан" ли какой-либо блок); сравнивать свободную память с тем объемом ОЗУ, к которому программа привыкла или приучена (не запущены ли паралельно резидентные "отмычки"); проверять вектора прерываний (нет ли их перехватов); используя компьютерный таймер, контролировать время прохождения отдельных частей (выявление "остановов" и "потактового режима" отладчика).
Изучение операционных систем, аппаратных особенностей ЭВМ позволяет выделить индивидуальные отличия и использовать их для НАСТРОЙКИ НА КОНКРЕТНУЮ ПЭВМ, СИСТЕМУ или ДИСКЕТУ, что делает программное обеспечение непереносимым без санкции разработчика. Правда, в архитектуре большинства компьютеров не существует аппаратной особенности, анализ которой помог бы выделить одну машину из серии таких же. Зато динамические характеристики различных частей (вращение винчестера и дисководов, скорость обращения к оперативной памяти и реакции клавиатуры) и их соотношения между собой индивидуальны, хотя и не очень устойчивы. Для повышения надежности рекомендуется использовать аппарат математической статистики. Вы не знакомы с ней? Мы снабдим вас необходимыми алгоритмами. Кстати, передаваемые в комплекте с лицензионными программами специальные электронные устройства, искусственно создающие аппаратную уникальность конкретной машины (так называемые "заглушки", которые подключаются к одному из разъемов ПЭВМ и по предусмотренному автором ПО запросу выдают парольную комбинацию байт), не всегда эффективны. Хакеры, используя принцип спаривания компьютеров, отслеживают на втором все передаваемые "заглушкой" сигналы, чтобы затем для несанкционированных копий повторить их программой-"псевдозаглушкой".
Простой царапиной можно пометить дискету - сделать точно такую же на другой дискете невозможно, а охранный механизм по характеру и местоположению сбойных блоков (или их отсутствию) легко определит подмену. (Но и с этим хакеры успешно справляются: особый драйвер дисковода перехватывает поступающую информацию и заменяет ее на выявленную предварительным тестом с ключевой дискеты.)
Надежную ИДЕНТИФИКАЦИЮ ПОЛЬЗОВАТЕЛЯ можно провести по почерку (скорость, привычки в использовании основных или вспомогательных частей клавиатуры, "любимые" комбинации клавиш при альтернативных вариантах, выполнение "сдвоенных" и "строенных" нажатий одной или двумя руками и т.д.), по росписи с использованием мышки, с помощью психологических тестов и паролей.
ПОИСК И УНИЧТОЖЕНИЕ ПОХИЩЕННОЙ ИНФОРМАЦИИ осуществляется специальными резидентными драйверами (находящимися в памяти даже после выгрузки пакета), которые постоянно контролируют операции ввода-вывода, "просматривают" другие директории винчестера и дискет, вставленных в дисководы. По сути это практическое применение вирусных механизмов для защиты. Отдельных читателей может шокировать такой способ охраны. Однако, если речь идет о важной информации и вирус настроен исключительно на уничтожение украденных данных, то криминала нет. Даже сторож яблоневого сада вооружается ружьем, так что же говорить о допустимых средствах борьбы с утечкой интеллектуальных ценностей?
Это далеко не полный перечень различных вариантов "предохранения". Однако следует учесть: их применение нередко мешает работе легальных пользователей, поэтому включение того или иного способа в защитный механизм требует тщательной оценки всех положительных и отрицательных сторон.
Каждый алгоритм обладает своей степенью НАДЕЖНОСТИ. Как показывает опыт - 98 процентов "любознательных" программистов отступятся от защиты, если она гарантирует: невозможность копирования дискет с двух - трех попыток распространенными средствами типа COPY II-PC, COPYWRITE или UNLOCK; невозможность разобраться в алгоритме стартовых блоков программы с помощью известных отладчиков и дизассемблеров (удача же на первых шагах, наоборот, подхлестнет "спортивный" интерес); уникальность применяемых систем (если на один и тот же "замок" закрыты программы разных авторов, то с ним стоит и повозиться). А если к тому же пакет решает специальные задачи (не интересен широкому кругу пользователей) и обходится дешевле, чем оплата профессионального хакера, - то можно быть спокойным, "краж" не будет.
Но, предположим, перед неким хакером все же поставили такую цель. К сожалению, пока не существует объективных критериев оценки надежности того или иного алгоритма либо их комбинаций, зато из субъективных можно взять за основу - время, которое понадобится для "взлома". Шансы того, что защита целиком будет вскрыта, могут быть подсчитаны на основании следующих параметров: вероятность наличия у хакера спецсредств копирования и анализа защищенных программ; объем и степень популярности примененной системы охраны; насколько интересна сама программа другим пользователям; стоимость программы и системы защиты.
Обеспечение БЕЗОПАСНОСТИ В ЛОКАЛЬНЫХ СЕТЯХ, пожалуй, самая сложная проблема. Она не решается автоматически, даже если на каждой ПЭВМ все обстоит благополучно, но системе в целом должного внимания не уделялось. Да и сам подход к построению защиты иной. Сначала необходимо разобраться: во-первых, к каким блокам имеющейся информации доступ должен быть максимально открыт (общая часть), ограничен (дополнительные сведения) или полностью закрыт (управление сетью, списки абонентов и их паролей); во-вторых, какие способы проникновения в базы данных возможны для легальных корреспондентов, включая сетевые серверы, и для хакеров. Теоретически всегда допустима ситуация, когда два защитившиеся друг от друга абонента контактируют с третьим через общие устройства - тут-то и возникают любопытные варианты похищения или порчи данных. Поистине неоценим зарубежный опыт борьбы и с сетевыми вирусами, автономно живущими в узловых компьютерах и накапливающими передаваемую информацию. Но поскольку сети в нашей стране только начинают развиваться, теория и практика их безопасности разработаны пока недостаточно.
Решение некоторых из этих проблем - вы найдете в этом выпуске приложения "ТМ", а остальные - в последующих.
Почерк уникален, это знают все. Но немногие догадываются, что в общении с компьютером индивидуальность пользователя проявляется также: скорость, привычка использовать основную или дополнительную часть клавиатуры, характер "сдвоенных" и "строенных" нажатий клавиш, излюбленные приемы управления компьютером..., с помощью которых можно выделить конкретного человека среди всех работавших на данной машине. И ничего удивительного, - это сродни способности меломанов различать на слух пианистов, исполняющих одно произведение.
Как же выявить индивидуальные особенности клавиатурного почерка? Также, как и при графологической экспертизе: нужны эталонный и исследуемый образцы текста. Лучше, если их содержание будет одинаковым (так называемая, парольная или ключевая фраза). Разумеется, по двум-трем, даже по десяти нажатым клавишам отличить пользователя невозможно, нужна статистика.
При наборе ключевой фразы компьютер позволяет зафиксировать много различных параметров, но для идентификации наиболее удобно использовать время, затраченное на ввод отдельных букв. А повторив ввод фразы несколько раз, в результате будем иметь множество временных интервалов для каждого символа. На базе полученных значений всегда можно рассчитать среднее время ввода каждого символа, допустимое отклонение от среднего, и хранить эти результате в качесте эталонов для каждого пользователя.
Уникальные особенности клавиатурного почерка выявляются двумя методами: по набору ключевой фразы или по "свободному" тексту. Каждый обязательно имеет режимы настройки и идентификации. При настройке определяются и запоминаются эталонные характеристики ввода пользователем ключевых фраз, например, время, затраченное на отдельные буквы. А в режиме идентификации, после исключения грубых ошибок, эталонное и полученное множества сопоставляются (проверяется гипотеза о равенстве их центров распределения).
Обе методики различаются лишь выбором парольной фразы. В первом случае это всегда одно и то же, а во втором - самый разнообразный текст, что имеет свои преимущества, позволяя получать те же характеристики незаметно, не акцентируя внимание пользователя на парольной фразе. Впрочем, на выбор схемы проверки влияет тематика защищаемого ПО. Предположим, некий владелец фирмы, желая узнать текущий финансовый оборот, запустил программу бухгалтерского учета, а компьютер, вместо короткой справки с коммерческой и потому секретной информацией, предлагает набрать 2 - 3 странички "свободного текста", дабы убедиться, что перед ним действительно директор или главбух. То есть, здесь лучше применить метод "парольной фразы". С другой стороны, лицо, имеющее допуск к секретам, может работать с такой программой целый день, время от времени отлучаясь от компьютера, а чтобы в этот момент злоумышленники не воспользовались раскрытой системой, желательно периодически проводить "негласную проверку" (просьба перенабрать "пароль" через каждые полчаса будет слишком назойлива).
Правильной идентификации помогает также рисунок почерка. Под этим понимается ряд значений, представляющих собой разность между соседними временными интервалами - своего рода "производная" по почерку, показывающая относительные замедления или ускорения при работе с клавиатурой. Характеристика достаточно индивидуальна, что подтверждается рядом экспериментов.
Для определения эталонных характеристик пользователя необходимо выбрать ключевую фразу. Желательно, чтобы буквы были равномерно распределены по клавиатуре, например: "Внимание - идентификация пользователя по клавиатурному почерку". Затем раз десять набрать ее на клавиатуре, определить время, затраченное на ввод каждой буквы, и исключить грубые ошибки (те значения, которые резко выделяются из каждой десятки имеющихся). Рассчитать и запомнить величины математического ожидания (M), дисперсии (S) и число наблюдений (n). Эти значения и называются эталонными (на блок-схеме - с индексом "э"). Блок-схема алгоритма режима настройки приведена на рис. 1.1.
Определение математических параметров пользователя (на блок-схеме: M, S и n с индексом "и") при его идентификации проводится аналогично, как и в режиме настройки. Единственное отличие - множества по каждому символу будут состоять из меньшего количества значений (если фразу набирают несколько раз) или даже из единичных величин (при однократном наборе). Затем сравниваются дисперсии двух множеств (эталонного и только что расчитанного) и величины математического ожидания - равны ли, то есть совпадают ли центры распределения этих двух совокупностей. Разумеется, полного равенства не будет, потому алгоритм заканчивается оценкой вероятности того, что пользователь - тот же (если она больше 50%, то все несоответствия можно отнести за счет случайных факторов). Алгоритмы обоих вариантов набора ключевой фразы приведены на блок-схемах - рис. 1.2 (неоднократный) и рис. 1.3 (однократный).
В отличие от первого метода, здесь получаемый ряд значений сильно отличается от эталона (любой символ "ключа" даже если и встретится, то окажется не на "своем" месте). Поэтому при составлении множеств в качестве базисных используются величины, которые можно подобрать и в ключевой, и в случайной фразах, например, - время между нажатием двух клавиш в одинаковых сочетаниях (если слово эталона "Внимание", то в свободном тексте ищем "Вн", "ни", "им" и т.д. и определяем размер паузы, прошедшей с момента нажатия "В" до нажатия "н"), считая, что пользователь будет переносить руку от одной клавиши к другой одинаково в обоих случаях (при настройке и идентификации).
А сравнение математического ожидания и дисперсии с эталонными такое же, как и раньше (если базисные величины двух множеств выбраны правильно, то они хорошо коррелируют), но прежде необходимо исключить грубые ошибки, которых в данном случае будет больше.
Приведенные методы достаточно просты и опираются на известные разделы математической статистики [1-3], в различных вариациях они используются во многих системах. Разумеется, можно воспользоваться дисперсионным, регрессионным и другими видами анализа и усложнить решения, доведя их до совершенства. Но это усложнит и жизнь пользователя, ведь затруднительно каждый раз перед началом работы вводить солидные куски парольных текстов.
Вполне естественно, что с течением времени характеристики пользователя меняются. Поэтому рекомендуется после каждой успешной идентификации корректировать эталоны по формуле Mи=(n*Mэ+X)/(n+1), где Mи, Mэ - характеристики исправленного и эталонного множеств, X - величина, полученная в ходе идентификации, n - количество опытов, вошедших в эталонное множество.
Люди по разному воспринимают происходящие события. Предложи за короткое время прикинуть количество точек или гласных букв в длинных словах, размеры горизонтальных и вертикальных линий, - сколько испытуемых, столько и мнений.
Эти особенности человеческой психики также подходят для идентификации. Правда, в зависимости от состояния и самочувствия человека полученные значения будут "плавать", поэтому в практике разумнее положиться на интегральный подход, когда итог подводится по нескольким проверкам, учитывая и работу с клавиатурой. Результирующий тест мог бы быть таким: на экране, на несколько секунд, появляются вертикальные линии. Их размер и количество случайны. Пользователь набирает соответствующие, на его взгляд, цифры. Таким образом, выясняем: характеристики клавиатурного почерка, оцениваем память (насколько указанные длина и число линий близки к действительности), внимание и точность подсчета (насколько длина одной линии правильно сопоставлена с соседней). Сравниваем результаты с эталоном. В этом методе не так важны ошибки в определении размеров, главное - чтобы они повторялись и при настройке, и при идентификации.
Таблица 1.1. Значения t-распределения Стьюдента P. | |||||
P/n | 0.95 | 0.99 | P/n | 0.95 | 0.99 |
4 | 2.78 | 4.60 | 16 | 2.12 | 2.92 |
5 | 2.57 | 4.03 | 17 | 2.11 | 2.90 |
6 | 2.45 | 3.71 | 18 | 2.10 | 2.88 |
7 | 2.37 | 3.50 | 19 | 2.09 | 2.861 |
8 | 2.31 | 3.36 | 20 | 2.086 | 2.845 |
9 | 2.26 | 3.25 | 25 | 2.064 | 2.797 |
10 | 2.23 | 3.17 | 30 | 2.045 | 2.756 |
11 | 2.20 | 3.11 | 40 | 2.023 | 2.708 |
12 | 2.18 | 3.06 | 50 | 2.009 | 2.679 |
13 | 2.16 | 3.01 | 70 | 1.996 | 2.649 |
14 | 2.15 | 2.98 | 80 | 1.991 | 2.640 |
15 | 2.13 | 2.95 | 100 | 1.984 | 2.627 |
Таблица 1.2. Значения t-распределения Стьюдента P. | |||||
P/n | 0.95 | 0.99 | P/n | 0.95 | 0.99 |
5 | 3.04 | 5.04 | 16 | 2.20 | 3.04 |
6 | 2.78 | 4.36 | 17 | 2.18 | 3.01 |
7 | 2.62 | 3.96 | 18 | 2.17 | 2.98 |
8 | 2.51 | 3.71 | 20 | 2.145 | 2.932 |
9 | 2.43 | 3.54 | 25 | 2.105 | 2.852 |
10 | 2.37 | 3.41 | 30 | 2.079 | 2.802 |
11 | 2.33 | 3.31 | 40 | 2.048 | 2.742 |
12 | 2.29 | 3.23 | 50 | 2.030 | 2.707 |
13 | 2.26 | 3.17 | 70 | 2.009 | 2.667 |
14 | 2.24 | 3.12 | 80 | 2.003 | 2.655 |
15 | 2.22 | 3.08 | 100 | 1.994 | 2.639 |
Таблица 1.3. F-распределение для уровня значимости a=0.05. | ||||||||||
k1=k2= | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Результат | 161 | 19.0 | 9.28 | 6.39 | 5.05 | 4.28 | 3.79 | 3.44 | 3.18 | 2.97 |
k1=k2= | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 |
Результат | 2.97 | 2.12 | 1.84 | 1.69 | 1.60 | 1.55 | 1.50 | 1.46 | 1.42 | 1.39 |
Таблица 1.4. Значение функции Ф(y). | |||||||||
y=0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 |
0 | 0.079 | 0.158 | 0.235 | 0.31 | 0.38 | 0.45 | 0.516 | 0.576 | 0.632 |
y=1 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 | 1.7 | 1.8 | 1.9 |
0.68 | 0.728 | 0.77 | 0.806 | 0.838 | 0.866 | 0.89 | 0.91 | 0.93 | 0.94 |
y=2 | 2.1 | 2.2 | 2.3 | 2.4 | 2.5 | 2.6 | 2.7 | 2.8 | 2.9 |
0.95 | 0.96 | 0.97 | 0.978 | 0.983 | 0.987 | 0.99 | 0.993 | 0.995 | 0.996 |
y=3 | 3.1 | 3.2 | 3.3 | 3.4 | 3.5 | 3.6 | 3.7 | 3.8 | 3.9 |
0.997 | 0.998 | 0.9986 | 0.999 | 0.9993 | 0.9995 | 0.9997 | 0.9998 | 0.9999 | 0.9999 |
y >=4.0 | |||||||||
0.9999 |
Таблица 1.5. Значения для вероятности p = 0.01 и числа степеней свободы 1. | |||||||||
Число степ. свободы | Результат | Число степ. свободы | Результ. | ||||||
1 | 6.6 | 8 | 20.1 | ||||||
2 | 9.2 | 9 | 21.7 | ||||||
3 | 11.3 | 10 | 23.2 | ||||||
4 | 13.3 | 11 | 24.7 | ||||||
5 | 15.1 | 12 | 26.2 | ||||||
6 | 16.8 | 13 | 27.7 | ||||||
7 | 18.5 |
Рукопись в своеобразии начертания букв доносит до нас что-то личностное. Графологи, продравшись сквозь частокол завитушек, многое расскажут об их авторе. А что можно узнать с помощью компьютера о человеке, "долбящему" по клавишам? Попробуем поискать аналогии.
Д.М.Зуев-Инсаров, автор фундаментальных работ по графологии, не только убедительно демонстрирует методы определения пола, возраста, образования, рода занятий писавшего, но и достаточное внимание уделяет экспериментальным основаниям этого научного направления. Его классификация содержит такие формальные признаки почерка, как: сила нажима, динамичность и напряженность движения, вытянутость, наклон и степень связанности букв, направление строки, расположение и содержательность текста, способ держания орудия письма, равномерность и соразмерность букв и слов, ритм и выразительность письма.
Что же из этого может позаимствовать "клавиатуровед"? Аналогии есть, это несомненно. Например, временные интервалы между вводом символов с клавиатуры по информативности ничем не уступают связанности букв в словах на бумаге. Действительно, если не новичок при хорошем темпе набора вдруг начал ошибаться, то скорее всего он охвачен внутренними переживаниями, не относящимися к выполняемой работе. А ровный темп, взятый в начале и сохраненный до конца, не хуже графологического признака "равномерность письма" свидетельствует о пунктуальности и аккуратности тестируемого.
Конечно, многие особенности рукописного почерка при работе на компьютере сопоставлять бесполезно, ведь клавиатура и драйверы стандартизируют написание букв. Зато здесь возможен анализ новых признаков: зависимость скорости ввода слов от их смысла, относительное время нажатия клавиш различных полей клавиатуры и другие. Причем они в некоторых случаях даже более информативны - например, реакция тестируемого на различные термины укажет сферу его интересов. Действительно, химик быстрее наберет "водород", "соединение", которыми он постоянно оперирует, чем "программа", "экскаватор". А модельеру будут привычнее - "манекен", "выкройка". (Кстати, это свойство "мышечной" памяти, не контролируемой сознанием, можно использовать для программы "Детектор лжи".) В данном случае появляется возможность отдельно анализировать левое и правое полушария мозга (отвечающие за образное и абстрактное мышление), поскольку они связаны с правой и левой руками человека, а письмо представляет только "одностороннюю" информацию.
Формулы для получения формальных признаков клавиатурного почерка приведены на рис. 2.1, а их соответствие графологической классификации - в таблице 2.1. Смысл, вложенный в некоторые термины, отличается от принятого Зуевым-Инсаровым, потому что похожие особенности в том и другом случаях выявляются разнымии методами, однако это не противоречит его работе [1].
Графологические исследования способствуют также диагностированию больного, так как почерк меняется при заболевании, возвращаясь к нормальному виду по мере выздоровления. Кто знает, не подойдет ли подобный анализ и при постановке предварительного диагноза работающего на компьютере.
Можно пойти и дальше - построить Личностный вектор и на его основе описать внешность, характер и судьбу человека [2]. Заключение хотя и кажется слишком категоричным, тем не менее основано на известных теориях и гипотезах из психологии. Впрочем, взаимосвязь внешнего и внутреннего облика очевидна: закомплексованность из-за физического изъяна отражается на поведении, которое, в свою очередь, влияет на способ общения с компьютером. Поэтому решение обратной задачи - определение по клавиатурному почерку характера и, через него, внешности - хотя и трудно, но допустимо, пусть пока теретически. Например, на нештатную ситуацию (якобы "сбой" компьютера, а на самом деле - предусмотренный автором программы психологический тест) вспыльчивый холерик и "вечно комплексующий" меланхолик отреагируют по разному. А кто меньше отвлекается при длительной работе? Флегматик, сангвиник или холерик? Вероятно, наибольшее возражение вызывает утверждение о возможности прогнозирования судьбы. Хотя и в его защиту можно привести немало аргументов. В данном случае даже нет необходимости опираться на астрологов, которые считают, что жизнь человека зависит от даты рождения. Возможно и другое объяснение. Выбор Судьбой для человека поступка/события из альтернативных вариантов - продиктованы его деятельностью в далеком и недалеком прошлом. То есть, свобода выбора - всего лишь иллюзия разума. Таким образом, выяснив главные вехи в биографии и определив основные черты характера, можно с достаточной степенью вероятности смоделировать линию поведения человека в той или иной ситуации. В конце концов даже по капле воды можно судить об океане, а по поступку о жизни. Поэтому возможность предсказания судьбы становится, пусть спорной, как почти все в этом мире, но понятной. И в принципе реализуемой - что стоит для хорошего компьютера учесть несколько тысяч величин? Лишь бы были тщательно проработаны математические формулы и введены числовые значения всех факторов, влияющих на человека.
1) Темп набора тестового материала в целом: Tm = T/n где T - время набора, n - число символов. 2) Темп набора каждого слова: Ts[i] = T[i]/k[i] где T[i] - время набора i-того слова, k[i] - число символов в нем. 3) Средняя пауза между словами: i=L Tp = (T - (T[i]) ) / L i=1 где L - число слов в тексте. 4) Степень связности набора (вычисляется после исключения грубых ошибок): -----------------¬ S = (t[j] - M) /(n-1) где t[j] - время между набором j и j+1 символа в слове (пробелы исключаются), i=L M = (T[i])/n. i=1 5) Общий рисунок почерка: dX[i] = t[i] - t[i+1] для всех i. |
Рис. 2.1. ОПРЕДЕЛЕНИЕ ОСОБЕННОСТЕЙ КЛАВИАТУРНОГО ПОЧЕРКА.
Таблица 2.1. СООТВЕТСТВИЕ КЛАВИАТУРНЫХ И РУКОПИСНЫХ ХАРАКТЕРИСТИК ПОЧЕРКА | |||||||||
На клавиатуре | В рукописи | Особенность характера | |||||||
Длительный поиск клавиши. | Отсутствие связи между буквами слова. | Душевный разлад (если за ПЭВМ не новичок). | |||||||
Одинаковые паузы между словами. | Равномерные интервалы между словами. | Уверенность в себе, отсутствие отрицательных переживаний. | |||||||
Длительность пауз между словами изменяется в широких пределах. | Неравномерность интервалов между словами. | Неустойчивость характера, несогласованность чувств, активность проявляется только в результате отдельных вспышек волевой энергии. | |||||||
Ровный темп набора каждого слова при вводе всего тестового материала. | Равномерность написания как первых так и последних строк рукописного текста. | Способность к регулярной систематической работе. Привычки к порядку, пунктуальности и аккуратности. | |||||||
Пауза между словами много кратно превышает паузы между отдельными буквами. | Плотное написание слов с чрезмерными интервалами между ними. | Истеричность. | |||||||
Высокий темп в начале теста, заметно снижаюшийся к его завершению. | Более правильное и четкое написание начала текста по сравнению с заключением. | Быстрая утомляемость. | |||||||
Темп ввода отдельных слов | Тестируемый ранее неоднократно набирал их на клавиатуре. | ||||||||
Отдельные слова вводятся гораздо медленнее остальных. | Тестируемый никогда не встречался с данными словами, возможно, даже не знает их правописания. |
С развитием отечественного рынка программ возрастает значение их защиты от несанкционированного копирования (НСК). Разработчики просто опускают руки перед фатальной неизбежностью пиратского распространения своего детища, ведь кража этой интеллектуальной собственности проста, занимает считанные минуты и остается незаметной, хотя доходы приносит вполне приличные. Правда, корпорации-монстры все же предпочитают не защищать собственную продукцию, ибо общий оборот их средств намного превышает убытки от воровства, которые, кстати, частично покрываются такой "бесплатной рекламой". Однако, для подавляющего большинства фирм и организаций, занимающихся разработкой ПО, продажа копий (зачастую с единственной "товарной" программы) - основное средство существования. "Пираты" же изменяют не только название продукта, но и фирму-изготовителя, тем самым добавляя к моральным потерям появление конкурирующего товара с аналогичными потребительскими свойствами. Поэтому каждый разработчик, несомненно, рано или поздно сталкивается с проблемой защиты ПО от НСК.
Первая дилемма, возникающая на этом пути: придумывать ли защиту самому или же приобрести существующий пакет "контрдействий", которых, к слову сказать, появилось у нас великое множество. Обычно выбор определяется важностью и стоимостью защищаемой информации, но, пожалуй, решающее значение играет квалификация программиста. Профессионал чаще приступает к созданию оригинальной защиты, ибо большинство ныне существующих представляют собой так называемый пристыкованный блок, который, чего греха таить, является чистым надувательством. Ведь опытные хакеры снимают его за считанные минуты, даже не удостаивая изучением (например, с помощью утилиты CATCHER, рекламируемой центром САПР Ассоциации "Наука"). Кроме того, профессионал никогда не отдаст на откуп другому такую интересную задачу, как создание защиты. А любитель выберет уже существующие охранные программы. Бывают еще случаи привлечения программистов "со стороны", но, поскольку передача исходных текстов сопряжена с утечкой информации (от которой как раз и стремятся застраховаться), это не типично для нашего рынка.
Сейчас, выражаясь на жаргоне программистов, существуют три типа пресечения НСК: "по дискете", "по машине" и "по пользователю". Первый - проверяет характеристики и информацию ключевой дискеты, которая служит, как правило, и носителем защищаемого ПО. Второй - следит за совпадением некоторых заранее определенных характеристик допущенного для работы компьютера. О третьем и, пожалуй, самом интересном способе охраны речь шла в двух предыдущих главах.
Наиболее эффективен второй путь, так как проверка компьютера проста и непродолжительна, может часто повторяться по ходу работы, не снижая общего быстродействия. Если же режим реального времени не обязателен и возможны многократные обращение к "медленным устройствам" типа гибких дискет или проверка клавиатурного почерка пользователя (конечно, когда он участвует в работе программы), то и остальные типы охраны могут быть встроены в блок защиты.
Разберем алгоритмы проверки компьютера. Программу удобнее привязать к следующим характеристиками машины:
1) быстродействие: процессор, память, контроллеры и т.д.; скорость вращения двигателей дисководов; скорость реакции на внешние воздействия;
2) конфигурация: тип микропроцессора (МП) и разрядность шины данных; наличие и тип контроллеров для внешних устройств и самих устройств (жесткие и гибкие диски, сопроцессор);
3) особенности: контрольная сумма BIOS; содержимое CMOS памяти; длина конвейера шины данных; аномальные явления в ходе его работы.
Их разделение на группы обусловлено лишь похожими алгоритмами определения и применения, а вообщем-то, оно весьма условно.
Легко заметить, что быстродействие (первая группа) в наибольшей степени отражает индивидуальные отличия машин - они, пожалуй, эффективнее других привяжут ПО к компьютеру. Правда, из-за нестабильности электронных элементов схемы, нестабильны и показатели его узлов. Незнание способов корректировки таких накладок чаще всего и отпугивает программистов.
Один из методов определения быстродействия компьютера представлен в процедуре "Sample" (рис. 3.1), который фиксирует отсчет таймера за время выполнения фрагмента 16 - 20. В регистр AX заносится для дальнейшего использования результат. Напомним, непосредственное его сравнение с некоторой эталонной величиной может привести к ошибке из-за нестабильности задающего генератора ПЭВМ. Отметим также, что реализация аналогичного алгоритма на языке BASIC даст еще больший разброс значений - это общий недостаток почти всех языков высокого уровня. Для исключения грубых ошибок и повышения точности необходимо провести многократные измерения и воспользоваться алгоритмом на рис. 1.1 (глава "Идентификация пользователя: "свой"-"чужой?").
Время выполнения одного и того же участка программы на разных компьютерах скорее всего будет отличаться, что и позволит выделить определенную ПЭВМ из ряда аналогичных.
Тот же пример (рис. 3.1) годится и для измерения скорости работы оперативного запоминающего устройства (ОЗУ). Его электрическая схема набирается из микросхем (МС), объемом по 16, 32 или 64 Кб (килобит). В зависимости от типа, 8 - 16 таких МС составляют 1 блок (электронщики называют их "блоком", а программисты "страницей"), емкостью 64 КБ (килобайт). Значит, в стандартном адресном пространстве IBM PC/XT в 640 КБ - 10 блоков МС, причем каждый имеет собственные характеристики, чуть-чуть отличающиеся от других. То есть быстродействие страниц ОЗУ у каждого компьютера различно. На участке 16 - 20 осуществляется простая перезапись содержимого области памяти (ее регенерация). Время регенерации всех страниц составит ряд из 10 чисел, значения и последовательность которых характерны только для данной ПЭВМ.
Скорость вращения двигателей дисководов определяется аналогично (например, замер по таймеру операции чтения/записи некоторого сектора или дорожки дискеты, как это делает COPYLOCK).
Под скоростью реакции на внешние воздействия понимается время, необходимое компьютеру на отклик после получения команды внешнего устройства, или размер паузы между выдачей команды, например, в контроллер накопителя гибких магнитных дисков (НГМД), и приходом сигнала готовности (аппаратное прерывание IRQ7 для НГМД). Измерения проводятся по отсчетам таймера, но можно использовать и свой программный счетчик, как это делает BIOS при дисковых операциях.
Если речь идет о компьютерах одной партии, то информация о типе и конфигурации не уникальна, хотя, как дополнительный элемент привязки к характеристикам ПЭВМ, не повредит. Ее можно получить через прерывание BIOS 11h (или по адресу 40h:0010h ОЗУ), а более исчерпывающую - из CMOS-памяти. Гораздо сложнее определить программным путем тип МП.
Характеристики третьей группы, очевидно, не всегда индивидуальны у конкретной машины. Общеизвестно, что в одной партии конрольная сумма BIOS одинакова (если, конечно, это не так называемая "белая" сборка). Энергонезависимая память CMOS, содержащая часы реального времени и информацию о конфигурации, также зачастую одинакова. К сожалению, формат CMOS различен на разных типах машин, а значит единого способа привязки нет. Если же расчет строится на определенный тип компьютера, то доступ к CMOS вполне возможен (с условием, что конфигурация не будет меняться в процессе эксплуатации программы).
Алгоритм чтения/записи байтов в ячейки CMOS приведен на рис. 3.2. Кстати, процедуру подсчета контрольных сумм можно усложнить, если арифметическую операцию "сложение" заменить на логическую или добавить к ней еще что-нибудь.
А на рис. 3.3 более интересный алгоритм, шифрующий и одновременно привязывающий программу к конкретному компьютеру. На определенные участки защищаемой программы накладывают некоторую область CMOS (например, операцией XOR). При запуске программа сама осуществит обратную процедуру, дешифрируя свой код (удобнее использовать область, защищенную контрольной суммой, байты 10h - 20h). Естественно, что на другом компьютере константа "CMOS" будет отличаться и, значит, процесс дешифровки испортит программу. Кстати, здесь годится и собственная информация, записанная в резервные ячейки энергонезависимой памяти, если, конечно, ее формат позволяет это делать, как, например, у компьютеров фирмы WIPRO Information Technology Limited (допускающих достаточно вольное обращение с CMOS: в том числе и с ячейками, защищенными контрольной суммой, с последующей корректировкой по адресам 2Eh,2Fh). А компьютеры SystemPro фирмы Compaq, хотя и не дают программисту возможности воспользоваться этим методом защиты, но обладают (как и некоторые другие на базе МП 80386 и выше) таким интересным свойством, как пользовательский пароль, который занесен в байты 38h - 3Fh и защищен от чтения и записи.
Последние две особенности третьей группы характеристик ПЭВМ, на наш взгляд, наиболее перспективны для защиты ПО от исследования и поэтому рассматриваются в следующей главе ("Защита от исследований").
Независимо от выбранного способа идентификации, больше всего вопросов вызывает сама передача программ покупателю. Ведь в этот момент они уже должны содержать в себе эталонные характеристики ПЭВМ пользователя, чтобы проводить соответствующие проверки. Хорошо, если автор может прийти и лично настроить свои программы, а как быть с иногородними покупателями?
Действия по настройке лучше всего поручить специальной инстолирующей программе, которая может храниться на одной дискете с передаваемым пакетом. Инстолятору поручают разархивацию продукции (расшифровку и приведение в рабочее состояние), перезапись с дискеты на винчестер, определение используемых характеристик компьютера и вписывание их в известные одному автору места программ (конкретные адреса для этих значений должны быть предусмотрены заранее). Попутно проверяется - не является ли ключевая дискета незаконной копией и соответствует ли название организации-покупателя упоминаемому в договоре купли-продажи. По завершению работы инстолятор может стереть сам себя с дискеты или сделать отметку об успешно проведенной инстоляции (чтобы самоликвидироваться после исчерпания оговоренного договором лимита).
Такую схему передачи можно применять даже при пересылке программных продуктов по почте - после самоуничтожения инстолятора владение ключевой дискетой становится бесполезным.
Ассемблер: ---------- 1 0000 code segment para public 2 assume cs:code,ds:code 3 4 0000 sample proc 5 6 0000 FA cli 7 0001 BA 0043 mov dx,43h ; включить 0-й канал 8 0004 B0 34 mov al,34h ; таймера 9 0006 EE out dx,al 10 0007 B2 40 mov dl,40h ; задать начальное 11 0009 8A C6 mov al,dh ; значение счетчика 12 000B EE out dx,al 13 000C EE out dx,al 15 ;======================================= 16 000D 33 F6 xor si,si ; 17 000F 8B FE mov di,si ; Здесь может быть 18 0011 B9 8000 mov cx,8000h ; расположен любой 19 0014 FC cld ; ваш текст 20 0015 F3> A5 rep movsw ; 21 ;======================================= 23 0017 BA 0043 mov dx,43h ; зафиксировать текущее 24 001A B0 04 mov al,4 ; значение счетчика 25 001C EE out dx,al 26 001D B2 40 mov dl,40h ; считать значение счетчика 27 001F EC in al,dx ; в AX 28 0020 8A E0 mov ah,al 29 0022 EC in al,dx 30 0023 86 E0 xchg ah,al 31 0025 FB sti 32 33 ; здесь должна быть проверка считанного значения 34 35 0026 B8 4C00 mov ax,4c00h 36 0029 CD 21 int 21h 37 38 002B sample endp 39 002B code ends 40 end sample BASIC: ------ 100 OUT &H43, &H34 110 OUT &H40, 0 120 OUT &H40, 0 . . . . . . . . . . . . . . . . . . . ' Текст замеряемого участка программы . . . . . . . . . . . . . . . . . . . 300 OUT &H43, 4 310 X% = INP ( &H40 ) + INP ( &H40 ) * 256 320 PRINT X% |
рис. 3.1 |
1) чтение: Ассемблер: BASIC: ---------- ------ . . . . . . mov al,номер байта CMOS 300 OUT &H70, номер байта CMOS out 70h,al 310 X% = INP ( &H71 ) jmp $+2 in al,71h . . . . . . 2) запись: Ассемблер: BASIC: ---------- ------ . . . . . . mov al,номер байта CMOS 300 OUT &H70, номер байта CMOS out 70h,al 310 OUT &H71, новое значение jmp $+2 mov al,новое значение . . . out 71h,al . . . |
рис. 3.2 |
Ассемблер: BASIC: ---------- ----- . . . . . . mov ax,0f800h 500 DEF SEG = &Hf800 mov es,ax 510 S = 0 xor ax,ax 520 FOR I = 0 TO 32766 STEP 2 mov bx,ax 530 S = S + PEEK(I) + PEEK(I+1)*256 mov cx,4000h 540 NEXT rpt: 550 PRINT S add ax,es:[bx] inc bx . . . inc bx loop rpt . . . |
рис. 3.3 |
Чтобы модернизировать механическое изделие, электронное устройство или интеллектуальный продукт - программу - надо понять принцип работы, определить основные и вспомогательные части. Для программ существуют специальные инструменты, позволяющие разбирать их "до винтика". Самые универсальные называются дизассемблеры и отладчики. Первые преобразуют непонятный машинный код в удобочитаемый текст на языке низкого уровня - ассемблере. Вторые - информируют обо всех процессах, протекающих в недрах компьютера, после выполнения отдельного участка или даже каждого шага программ, то есть помогают понять суть выполняемых операций. Поэтому следущая "линия обороны" любого блока защиты проходит именно здесь.
Четкой грани между дизассемблированием и анализом полученного текста под отладчиком - нет (часто эти функции совмещены в единой программе), что и позволило объединить методы борьбы с обоими способами "взлома" в рамках одной статьи. Но приемы против каждого из них - свои. Ведь противодействовать декодировке записанного на дискете файла нельзя (допустима лишь пассивная защита - запутывание алгоритма или шифрация самих кодов), зато при работе защищенных модулей на компьютере ( пусть даже в пошаговом режиме отладки ) активное сопротивление вполне возможно.
Еще одно замечание. Искусственное усложнение исполняемого модуля затрудняет исследование алгоритмов. Надежность защиты, в данном случае, зависит от того, насколько программист отождествит себя со "взломщиком", угадает логику его мышления и представит проблемы, с которыми тот сталкивается. А для этого ему самому нужно побывать в роли хакера, почти по системе Станиславского.Проще всего разбирать чужую программу, если она уже распечатана на бумаге на любом языке высокого уровня (Паскаль, Си и т.д.), но в крайнем случае сгодится и ассемблер (машинные коды заменены их мнемоническим изображением). Кстати, в качестве универсального инструмента рекомендуем отладчик Turbo Debugger (TD), имеющий широкий сервис и удобный интерфейс.
Естественно, автор программы, предусмотрев это, применил шифрование или ее разновидность - архивацию. А потому непосредственное дизассемблирование уже не даст верных результатов, если вообще что-то даст. Опытный хакер быстро поймет, что вместо текста идет "мусор" и сразу же начнет поиск средств для снятия шифра. Обычно он достигает цели, так как рано или поздно программа сама производит нужную операцию (она ведь не подозревает, что ее запустил хакер). Определив момент завершения дешифрации, можно "снять" в файл содержимое памяти, занимаемой уже "нормальной" программой, и, прогнав его дизассеблером, получить желаемый результат.
Даже если используется поэтапная дешифровка (то есть она разнесена по времени), полной гарантии защиты нет - дизассемблирование лишь несколько затянется. Впрочем, когда дешифрацией занимается несколько подпрограмм и каждая является результатом работы предыдущей, хакеру предстоит очень нудная и кропотливая работа по их анализу.
Отметим, что шифрование, хотя и не гарантирует полной безопасности программы, но вынуждает хакера запускать ее отдельные участки (без алгоритмов, разбираясь "на ходу"), таким образом, разработчику предоставляется возможность активно вмешиваться в процесс "взлома" (точнее, поручить это своей программе) и, в первую очередь, отобрать у хакера самый мощный инструмент - пошаговый режим отладки.
В режиме отладки больше всего забот доставляет стек: его расположение, размер, варианты применения. Достаточно тонкое его использование зачастую делает невозможным даже запуск стандартных отладочных средств. Например, назначение в тело выполняемой задачи: стековый сегмент совпадает с кодовым, а указатель вершины стека SP указывает на саму программу. Тогда отработка отладчиком хотя бы одного прерывания (трассировочного) обязательно сотрет участок размером не менее 3-х слов. Тем более, что популярные отладчики (TD, CodView и другие) применяют только пользовательский стек, затирая в нашем случае коды на большую глубину. Кроме того, старые версии TD имеют принципиальную ошибку - при начальной загрузке совершенно произвольно уменьшают стартовое значение указателя стека на 2. Более умеренно работают со стеком отладчики AFD и PERISCOPE. И наиболее выгодно себя проявляет обычный DEBUG, поставляемый вместе с DOS.
Переназначение стека в свободную область памяти как средство борьбы с назначением его в тело программы не каждому "по плечу", тем более если через него передаются массивы данных из модуля в модуль, да и он сам активно участвует в работе (как, например, в пакете CONVOY фирмы "Элиас", осуществляющем через стек разархивацию защищенного файла). В этом случае корректный проход программы возможен только без трассировки (то есть пошаговый режим исключен).
Не менее важная и такая же сложная проблема, стоящая перед хакером, - отслеживание прерываний, перехватываемых исследуемой программой. Суть в следующем. Все стандартные отладчики для нормальной работы "забирают" первое и третье из них. Первое (трассировочное) используется для пошагового режима. Третье необходимо для точек останова программы по заданным адресам. Защитный механизм обязательно должен их перехватывать, чтобы предотвратить анализ под отладчиком. Хакер, если он разобрался с замыслом автора, может либо обойти данный участок (с не малой долей риска, если прерывание выполняет некоторую "полезную" функцию), либо изменить подпрограмму обработки прерывания таким образом, чтобы после ее отработки управление передавалось отладчику (но неумелое ее исправление тоже чревато...).
Вот далеко не полный перечень того, с чем сталкивается "взломщик" в своем нелегком труде, только при добывании текста.
Сформулируем приемы, мешающие анализу: шифрование и архивирование (как его разновидность); использование самогенерируемых кодов; изощренный стиль программирования и многое другое, что сможет придумать автор.
Шифрование исполняемых файлов - наиболее простое средство для реализации. Достаточно, например, к каждому байту модуля добавить некоторую константу, чтобы дизассемблер ничего "не понял".
Предварительное архивирование также не представляет особых затруднений для хакера, но является более эффективным по сравнению с шифрованием, так как решает сразу две задачи: уменьшает размер защищаемого модуля и скрывает код от дизассемблера. Как это делается? Возьмем файл рисунка. Он содержит поточечное описание всех строк экрана (цифры - это точки определенного цвета, ноль - ее отсутствие). Например, рисунок красной линии на экране, состоящей из 200 точек, записывается в файл в виде 200 байт, а значение каждого байта равно 4 (код для красного цвета). Архиватор заменяет перечисление одинаковых цифр (эти 200 четверок) элементарным шифром - двумя байтами (первый - количество повторов, второй - значение цвета), и размер файла резко уменьшается (в примере - с 200 до 2 байт). Для других типов записей существуют свои методы сжатия. Большинство из них описано в литературе, поэтому не будем останавливаться.
Метод самогенерируемых кодов наиболее сложен в реализации, но очень эффективен. Суть его такова. В программу вложен массив данных, который сам по себе может быть исполняемым кодом (реально получающим управление) или смысловым текстом, но после некоторых арифметических или логических операций преобразуемый в участок программы, выполняющий иные, не менее важные функции.
Нужно быть виртуозом программирования, чтобы заметить самомодифицирующуюся ловушку. Ведь при анализе листинга и с явными алгоритмами разбираться сложно (автор комментарии не оставляет), так что уж говорить о потайном смысле вроде бы расшифрованного участка.
Впрочем, и новичок-разработчик создать такие ловушки не сможет. Но коль скоро он взялся за написание защитных механизмов, то ему придется осваивать изощренный стиль программирования, который запутывает дизассемблер нестандартной интерпретацией некоторых команд и нарушает общепринятые соглашения. Например, использование необычной структуры программы (совмещение стекового и кодового сегментов ). Интеллектуальные дизассемблеры, как правило, это плохо воспринимают. А перекрестные вызовы процедур, многократные переходы из модуля в модуль и увеличение количества точек входа в них - не позволяют выявить блочную структуру программы.
Замена команд переходов, вызовов подпрограмм и прерываний направляет дизассемблер по ложному следу. Здесь вместо стандартного оператора вставляется группа других, в конечном счете выполняющих то же самое. Для неискушенных программистов на рисунке 4.1 (а-д) приведены такие варианты. Впрочем, для аналогичных эффектов достаточно в команде перехода изменить значение операнда (примеры е-ж). И еще проще модифицировать косвенные переходы (з-и).
Кстати, и саму команду можно модифицировать. Например, на рис. 4.1 (к) дизассемблер по адресу m: покажет PUSH AX (запись регистра AX в стек), поскольку этот байт имеет код 50h (01010000b), но перед ее выполнением один бит (4-й) меняется, в результате получается INC AX с кодом 40h (01000000b, увеличение содержимого регистра AX на 1) - то есть совсем другая команда, не отраженная в листинге.
Кропотливая работа преобразует участок до неузнаваемости. А "умный" дизассемблер (например, Sourcer), отслеживая "явную" передачу управления (в другое место), не найдет спрятанный блок и, следовательно, не будет дизассемблировать его.
Опытный хакер, если ему не надоест разбираться с защитой, будет неоднократно прогонять непонятые куски программы отладчиком. Некоторые приемы борьбы с этим уже рассматривались.
Очень эффективное средство от пошагового выполнения программы - назначение стека в ее тело. А если там находятся данные для работы, то режим отладки усложняется. К тому же частое изменение местоположения стека, поверьте на слово, измотает хакера окончательно.
Напомним, что при пошаговом режиме хакеру рекомендовалось обходить участки, перехватывающие 1-е прерывание. Так вот, чтобы он не смог воспользоваться этим советом, нужно поручить подпрограмме обработки прерывания некоторую полезную функцию. Например, генерацию кодов или дешифрацию.
Напомним также традиционные методы защиты [1].
Программа должна подсчитывать и проверять контрольные суммы своих участков для определения "контрольных точек" или "точек останова", расставленных хакером. Дело в том, что стандартное применение 3-го прерывания предусматривает запись кода его вызова вместо байта программы, а это меняет контрольную сумму. Периодическая же проверка вовремя проинформирует программу о замене "родных" байт "чужими", то есть о попытке исследования программы под отладчиком.
Известно, что выполнение программы в режиме трассировки значительно уменьшает ее быстродействие. Поэтому по времени "прохода" отдельных кусков защищаемого ПО можно определить работу под отладчиком. По таймеру (например, запустив его 2-й канал) заранее просчитывают скорость выполнения некоторого участка и сравнивают его со значением, полученным в ходе работы программы. Если есть существенное расхождение в полученных результатах, то можно сделать вывод о выполнении данного участка под контролем. Из недостатков метода отметим лишь разное быстродействие процессоров на разных ПЭВМ, которое следует учитывать.
Интересный способ выматывания "исследователя" - применение достаточно больших процедур, производящих некоторую сложную и, на первый взгляд, важную работу, но на самом деле, не имеющих никакого отношения к логике работы программы, так называемых "пустышек". Для лучшей имитации их важности можно включать в них перехват 13h, 21h, 25h и 26h прерываний (обслуживающих ввод-вывод информации на внешние устройства), что, безусловно, заинтересует хакера.
Оригинальный способ защиты ПО от исследования, примененный в пакете COPYLOCK, использует конвейер шины данных микропроцессора. На рис. 4.2 изображен его фрагмент. (Надеемся, что не навлечем на себя гнев цивилизованных пользователей, афишируя некоторые тонкости программы: все равно она безнадежно устарела, да и не вскрыта лишь самыми ленивыми. А начинающим специалистам рекомендуем ознакомиться с ее работой. Полный листинг опубликован в электронном журнале "НСК", N1, 1992 г.). Фрагмент дан со значением смещений относительно кодового сегмента, чтобы читатель смог увидеть, как команда REP STOSW в подпрограмме SUBR затирает значением из регистра AX область ОЗУ, в которой находится и сама подпрограмма. Тем не менее, SUBR нормально отрабатывает и возвращает управление в точку вызова (но только не в пошаговом режиме).
Трюк очень прост: так как длина конвейера не менее 4-х байт, то, очевидно, команды, расположенные за REP STOSW, уже находятся в нем до ее выполнения, что и обеспечивает нормальную работу подпрограммы даже после затирания ее кода в ОЗУ. Выполнение же по одному шагу (то есть по трассировочному прерыванию) нарушает очередность засылки кодов в МП и приводит к непредсказуемому результату.
Пример на рис. 4.3 демонстрирует более изящное использование конвейера. Он определяет - идет ли выполнение программы с трассировкой или нет, и осуществляет ветвление (команда JMP с меткой m:) в зависимости от этого. Здесь ветвление служит лишь для индикации работы под отладчиком, но вы можете применить его по своему усмотрению.
На рис. 4.4 использование конвейера шины данных в иной интерпретации и в более завуалированном виде. По существу, это вариация на ту же тему и демонстрирует лишь разнообразие способов работы с конвейером.
Аномальные явления, с которыми приходится сталкиваться при программировании МП Intel 80x86, не менее интересная тема при рассмотрении построения защитных механизмов. Информацию о них программист чаще всего получает экспериментальным путем (что становится его "ноу-хау"). Отступлений от стандарта обычно немного (исключение составляют машины фирмы Compaq с длинным перечнем особенностей). Об одном упоминалось в печати [2] - это потеря трассировочного прерывания после команд, связанных с пересылкой сегментных регистров типа MOV SEG.REG,R/M и POP SEG.REG. К сожалению, в статье результаты исследований описаны неполно. Во-первых, для МП 8086/8088 (а точнее, японского аналога V20) существует еще один тип команд, заставляющий пропускать трассировочное прерывание: MOV R/M,SEG.REG. Во-вторых, для МП с более высоким номером также идет потеря трассировки, но только для стекового сегментного регистра. Это свойство с успехом можно применить для определения трассировки и типа машин.
Известно, что отладчики при обработке 1-го прерывания анализируют текущую команду на PUSHF (код 9Ch) и сбрасывают Т-бит. Поэтому последовательность команд PUSHF, POP AX под отладчиком не позволит получить установленный 8-й бит в регистре AX. На рис. 4.5 представлен текст короткой программы, использующей эту особенность. Команда POP SS заставляет отладчик пропустить следующую за ней команду PUSHF из-за потери трассировки, и, благодаря этому, выявляется факт работы под отладчиком.
Образцом знания особенностей работы МП и наиболее лаконичным вариантом распознавания его типа мы считаем подпрограмму, текст которой приведен в статье "Intel insight on specific instructions" [3]. Вот два примера из нее:
1) Для определения типа МП, начиная с 80186 и выше, используется тот факт, что для них в счетчиках сдвигов (линейных и циклических) маскируются все биты, кроме 5-и младших, ограничивая тем самым величину сдвига 31 битом.
2) Начиная с МП 80286, характерна следующая особенность: команда PUSH SP заносит в стек значение SP с учетом его декремента, а более низшие типы МП - без.
Для пытливого ума выявление особенностей микропроцессоров сослужит хорошую службу как при составлении защитных алгоритмов, так и для идентификации ПЭВМ, а именно на таких программистов и рассчитана эта статья.
В качестве примера, реализующего некоторые приемы, была разработана утилита DLOCK. Правила пользования ею приведены в приложении 2.
1. Дмитриевский Н.Н., Расторгуев С.П. " Искусство защиты и "раздевания" программ", "СОВМАРКЕТ", 1991.
2. статья "Особенности работы МП 8086/8088 в пошаговом режиме", "Журнал д-ра Добба", N 2, 1991.
3. "Intel insight on specific instructions", "Personal Computer World", April, 1990.
СОКРЫТИЕ АДРЕСОВ а) безусловного перехода jmp m mov ax,offset m ; занести в стек . . . push ax ; адрес метки. ret ; перейти на метку. . . . m:. . . m: . . . . . . . . . б) вызов подпрограммы call subr mov ax,offset m ; занести в стек . . . push ax ; адрес возврата. . . . jmp subr ; перейти на под- . . . m: . . . ; программу. subr: subr: . . . в) прерывание int 21h pushf ; занести в стек флаги. . . . xor si,si mov es,si call dword ptr es:[21h*4] . . . г) возврат из подпрограммы . . . . . . ; взять из стека ret pop bx ; адрес возврата и jmp bx ; перейти на него. . . . . . . д) выход из прерывания iret mov bp,sp ; переход на точку jmp dword ptr [bp] ; возврата из пре- . . . . . . ; рывания. add sp,4 ; точка возврата. popf . . . МОДИФИКАЦИЯ е) перехода mov word ptr cs:m+1,1234h ; адрес 1234h вписать вместо . . . ; 0000 у оператора безуслов- m: jmp 0000h ; ного перехода . . . ж) вызываемой подпрограммы mov word ptr cs:m+1,es ; изменить сегмент п/п mov word ptr cs:m+3,5678h ; и адрес 0000 на 5678h . . . m: call far 0000h . . . з) косвенного перехода mov bx,1234h jmp dword ptr cs:[bx] . . . и) косвенного вызова подпрограммы les si,dword ptr cs:subr call word ptr es:[si] . . . к) команды and byte ptr cs:m,0EFh ; обнулить 4-й бит по адресу m . . . m: push ax ; команда преобразуется в INC AX . . . |
рис. 4.1 |
Фрагмент пакета COPYLOCK (использование конвейера шины данных микропроцессора) . . . . . cs:07FF mov cx,40Eh cs:0802 mov di,08A6h cs:0805 call subr . . . . . ;===================================== ; Затирание участка памяти. subr proc near cs:0D63 pushf cs:0D64 cld cs:0D65 mov ax,ds cs:0D67 mov es,ax cs:0D69 rep stosw cs:0D6B popf cs:0D6C retn subr endp |
рис. 4.2 |
ОПРЕДЕЛЕНИЕ РЕЖИМА ТРАССИРОВКИ (1 вариант) 1 0000 code segment para public 2 assume cs:code,ds:code 3 4 0000 sample1 proc 5 6 0000 0E push cs 7 0001 1F pop ds 8 0002 C6 06 0008r 00 mov byte ptr m+1,0 ; изменение ; смещения в команде JMP 9 0007 EB 06 m: jmp short norm_ex 10 0009 BA 001Br mov dx,offset trace ; выполнение с ; трассировкой 11 000C EB 04 90 jmp exit 12 000F norm_ex: ; выполнение без ; трассировки 13 000F BA 0026r mov dx,offset norm 14 0012 exit: 15 0012 B4 09 mov ah,9 16 0014 CD 21 int 21h 17 0016 B8 4C00 mov ax,4C00h 18 0019 CD 21 int 21h 19 20 001B trace db 'Tracing!',0Ah,0Dh,'$' 21 22 0026 norm db 'Normal exit.',0Ah,0Dh,'$' 23 24 25 0035 sample1 endp 26 0035 code ends 27 end sample |
рис. 4.3 |
ОПРЕДЕЛЕНИЕ РЕЖИМА ТРАССИРОВКИ (2 вариант) 1 0000 code segment para public 2 assume cs:code,ds:code 3 4 0000 sample2 proc 5 6 0000 0E push cs 7 0001 0E push cs 8 0002 07 pop es 9 0003 1F pop ds 10 0004 BF 000Cr mov di,offset m 11 0007 F9 stc 12 0008 FC cld 13 0009 B0 88 mov al,88h ; заменить код операции следующей ; команды на mov byte ptr m, al. 14 000B AA stosb 15 000C 3A 06 000Cr m: cmp al,byte ptr m 16 0010 73 06 jnc norm_ex 17 0012 BA 0024r mov dx,offset trace ; выполнение с трассировкой, ; если бит CF остался 18 0015 EB 04 90 jmp exit ; установленным. 19 0018 norm_ex: 20 0018 BA 002Fr mov dx,offset norm 21 001B exit: 22 001B B4 09 mov ah,9 23 001D CD 21 int 21h 24 001F B8 4C00 mov ax,4C00h 25 0022 CD 21 int 21h 26 27 0024 trace db 'Tracing!',0Ah,0Dh,'$' 28 0024 29 002F norm db 'Normal exit.',0Ah,0Dh,'$' 30 31 32 003E sample2 endp 33 003E code ends 34 end sample2 |
рис. 4.4 |
ОПРЕДЕЛЕНИЕ РЕЖИМА ТРАССИРОВКИ (3 вариант) 1 0000 code segment para public 2 assume cs:code,ds:code 3 4 0000 sample3 proc 5 6 0000 0E push cs 7 0001 0E push cs 8 0002 1F pop ds 9 0003 17 pop ss 10 0004 9C pushf 11 0005 58 pop ax 12 0006 F6 C4 01 test ah,1 ; бит TF установлен ? 13 0009 74 06 jz norm_ex ; нет - уйти на norm_ex ; да - вып-ие с трассировкой 14 000B BA 001Dr mov dx,offset trace 15 000E EB 04 90 jmp exit 16 001 norm_ex: 17 0011 BA 0028r mov dx,offset norm 18 0014 exit: 19 0014 B4 09 mov ah,9 20 0016 CD 21 int 21h 21 0018 B8 4C00 mov ax,4C00h 22 001B CD 21 int 21h 23 24 001D trace db 'Tracing!',0Ah,0Dh,'$' 25 26 0028 norm db 'Normal exit.',0Ah,0Dh,'$' 27 28 29 0037 sample3 endp 30 0037 code ends 31 end sample3 |
рис. 4.5 |
DLOCK ver 2.0
DLOCK.EXE - программа встраивания в .EXE файлы модуля защиты от отладчиков и дизассемблеров, которая сама может использоваться как часть защиты программного обеспечения. Ее достоинством является то, что пользователь выбирает место расположения встраиваемых защитных модулей в своей программе.
Формат запуска программы:
DLOCK <имя файла> <смещение> где:
<имя файла> - полное имя защищаемого .EXE файла.
<смещение> - смещение от начала файла, заданное в любой системе счисления до начала буфера , в который будет размещен модуль защиты (не менее 300h байт).
Для нормальной работы, при программировании необходимо зарезервировать в своей программе область памяти не менее 300h байт. Упростить поиск этой области в оттранслированном .EXE файле поможет какое-нибудь оригинальное ключевое слово. Например:
Ассемблер: DB 'figtebe',300h DUP (?)
C: char buf[0x300] = "figtebe" ;
Средствами поиска любой оболочки DOS (XTREE, Norton Commander, PCTOOLS и др.) находим ключевое слово и определяем по нему смещение, требуемое в качестве 2-го аргумента формата запуска программы. Например, если при просмотре командой View (в режиме Hex) в пакете XTREE .EXE файл выглядел так:
. . . . . . . . . . . . . . . . . . . 000240 kstackstackstack 000250 .3.P..........!. 000260 ....figtebe..... 000270 ................ . . . . . . . . . . . . . . . . . . . то смещение равно 264 и формат запуска программы такой: DLOCK YOURFILE.EXE 0x264 |
рис. 4.6 |
С самомодифицирующейся программой, т.е. такой программой, которая постоянно меняет свой исполняемый код, разбираться очень трудно. Представьте, в распечатке с дизассемблированным текстом хакер не понял какой-то участок. Естественно, что он загрузит отладчик и попробует "прогнать" непонятный блок в пошаговом режиме. Каково же будет его удивление, когда он не сможет найти эту часть - по тем же адресам записано совсем другое. Он удивится еще больше, после того как сравнит имеющийся листинг с тем, что выводится на экран компьютера. "Ведь это посторонняя программа! А где же та, на анализ которой я потратил столько времени?" - воскликнет он. И при последующих попытках происходит тоже самое - каждый раз предыдущий текст бесследно исчезает, а на его месте возникает нечто новое, требующее повторного анализа.
Впрочем, полностью самомодифицирующиеся модули - большая редкость. В профессиональных системах защиты чаще применяется частичная переработка кодов. Легче всего модифицировать EXE и COM-файлы при загрузке в ОЗУ, выбирая куски для переработки случайным образом, и при этом можно сразу же вносить изменения в исходный файл программы на магнитном носителе. Как именно? Познакомьтесь с несколькими способами.
Самый простой - периодически заменять одну последовательнось команд на другую, внешне не похожую, но, в конечном итоге, выполняющую то же самое действие. Для этого подбираем эквиваленты. Например, команда MOV AX,BX и последовательность PUSH BX и POP AX - выполняют одно действие (пересылка содержимого регистра BX в регистр AX), команда CALL adr заменяется на последовательность PUSH IP+3 и JMP adr. Примерные варианты взаимозамены для основных команд ассемблера приведены в таблице 5.1 (естественно, для конкретных программ ее нужно дополнить и расширить).
В тексте программы организуется участок, где будут храниться цепочки команд, с указанием адресов эквивалентных им участков. При очередной работе программа случайным образом меняет местами отдельные части из собственного тела и "хранилища". В результате после каждого прохода исполняемый код будет случайным образом изменен до неузнаваемости, однако функции программы не нарушаются. Единственный недостаток этого способа - новый вариант исполняемого кода часто не может быть адекватен предыдущему по скорости работы.
Разумеется, конкретная таблица может иметь несколько альтернативных вариантов для каждой последовательности. А для выравнивания их длин можно использовать команду NOP или ее аналоги (пара PUSH - POP или MOV AX,AX).
Включение в алгоритм элементов случайности делает "внешность" задачи непредсказуемой. Пример этого способа приведен на прилагаемой дискете.
Более сложный способ - модификация кодов команд с изменением характера выполняемых операций (рис. 5.1). Делается это так. Написанный на ассемблере и уже отлаженный модуль транслируется в объектный код с получением листинга. На листинге, не обращая внимания на мнемонику, ищем участки с похожими закономерностями изменения величин кодов команд (вот где пригодится опыт решения математических задач, типа "найди закономерность", из популярных журналов). Затем выделяем найденные участки в отдельную подпрограмму и, используя подмеченную закономерность, составляем алгоритм ее преобразования в коды первого участка, второго, третьего... Этот алгоритм встраиваем вместо участков и по завершению преобразования подпрограммы - передаем ей управление. Как правило, с первой попытки полной аналогии с изъятым блоком не получается, поэтому нужно поманипулировать командами, переставить их местами, может быть - добавить лишние (тем не менее не нарушающими общего алгоритма), что-то заменить эквивалентной последовательностью.
Несмотря на кажущуюся простоту, одновременное использование некоторых байт как операторов и операндов является "высшим пилотажем" в программировании. Способ пришел от программистов для 8-разрядных процессоров типа Z80, К580 и др. Дело в том, что у них основные коды однобайтовых пересылок и букв совпадают, к тому же у компьютеров с МП Z80 маловато ОЗУ (приходится его экономить). Вот и используют участки осмысленного текста - как для появляющихся на экране сообщений, так и для загрузки нужных регистров. Разумеется, некоторые буквы оказываются лишними при прогоне участка, как кода программы. Но, с другой стороны, вслед за "сообщением" можно поставить несколько команд, корректирующих результаты ненужных операций. Пример приведен в распечатке листинга игры "Jetpac" фирмы "Ultimate" для компьютера "Spectrum-48" на рис. 5.2 (для МП Z80). По ходу космической игры в верхней части экрана появляются надписи: "1UP" (результат 1 игрока), "HI" (лучший результат за всю игру) и "2UP" (2 игрок). При анализе текста программы выяснилось, что эти надписи образуют отдельную подпрограмму, осуществляющую пересылки некоторого значения из ячейки ОЗУ (адрес 0D055h) в стек и из аккумулятора в эту же ячейку (то есть сложный обмен значениями между регистром A, стеком и ОЗУ).
Впрочем, то что легко программируется для 8-разрядных процессоров, вызывает определенные трудности на 16-разрядных: и характер операций у "буквенных" кодов другой, и однобайтовых команд мало (с двух - трехбайтовыми разбираться еще сложнее). Тем не менее, после некоторой тренировки можно и этот способ взять на вооружение.
Традиционный подход при использовании ветвления заключается в построении 2-х участков с разным набором команд. При выполнении запрограммированного условия - выполняется первый, в противном случае - управление передается на второй. В самомодифицирующихся модулях лучше отойти от общепринятой схемы, и применить переброску того или иного блока на один и тот же участок программы. Сработало условие - на следующие за ним адреса записывается первый набор команд, не сработало - другой, затирая предыдущие (тем более, что у МП 286 и выше - блочные пересылки выполняются легко и быстро).
Определить в распечатке такой "оверлейный" блок - очень сложно, особенно, если его куски хранятся в других сегментах.
БЛОК ДЕКОДИРОВАНИЯ (присланный Ерко В.Д., автор неизвестен): ; 1 модуль программы n: mov HL, nn ; адрес начала блока mov BC, ll ; длина блока mov AX, kk ; ввод ключа декодера xor (HL) ; декодирование mov (HL), AX ; замена закодирован. значения на раскодир. inc HL ; переход к очередному байту dec BC ; уменьшение счетчика jnz n ; повтор декодирования, если BC > 0 .......... ; продолжение модуля ; 2 модуль программы nn: db ....................... ; (Закодированный блок.) db ........ ; конец блока, адрес = nn + ll мм: db ........ ; 3 модуль (и так далее) После декодирования 1 блока и исполнения остальных команд управление передается на декодированный блок, который начинается с такой же процедуры, декодирующей следующий участок. И таких вложений - 156. |
рис. 5.1 |
ИСПОЛЬЗОВАНИЕ ТЕКСТА КАК КОДА ПРОГРАММЫ (для микропроцессора Z80) 6E1E C3 6C 71 jmp 716Ch ; завершение предыдущего ; участка 6E21 47 mov B,A ; начало подпрограммы 6E22 31 55 D0 mov SP,0D055h ;1UP - текст первого ; сообщения 6E25 47 mov B,A ; 6E26 32 55 D0 mov (0D055h),A ;2UP - текст второго ; сообщения 6E29 45 mov B,L ; 6E2A 48 mov C,B ;H - третье сообщение 6E2B C9 ret ;I (оно же завершает 6E2C 21 00 00 mov HL,0000 ; подпрограмму) |
рис. 5.2 |
Таблица 5.1. ВЗАИМОЗАМЕНЯЕМЫЕ КОМАНДЫ. | |||||||||
Первичный код | Альтернативный код | ||||||||
Команды пересылки: | |||||||||
mov op1,op2 | push op2 pop op1 |
||||||||
xchg op1,op2 | push op1 push op2 pop op1 pop op2 |
||||||||
lds r,dword ptr op | mov r,word ptr op mov ds,word ptr op+2 |
||||||||
les r,dword ptr op | mov r,word ptr op mov es,word ptr op+2 |
||||||||
Высокий темп в начале теста, заметно снижаюшийся к его завершению. | Более правильное и четкое написание начала текста по сравнению с заключением. | ||||||||
Арифметические команды: | |||||||||
add op1,op2 | xchg op2,ax add op1,ax xchg op2,ax |
||||||||
adc,sub,sbb и др. | аналогично add | ||||||||
inc op | add op,1 | ||||||||
dec op | sub op,1 | ||||||||
Логические команды: | |||||||||
and,or,xor и др. | аналогично add | ||||||||
not op | xor op,0ff(ff)h | ||||||||
Цепочечные команды: | |||||||||
rep movsb | push ax m: mov al,[si] mov es:[di],al inc si inc di loop m pop ax |
||||||||
repe(repne) cmpsb | push ax m: mov al,[si] cmp al,es:[di] jne(je) m1 inc si inc di loop m m1: pop ax |
||||||||
lodsb | mov al,[si] inc si |
||||||||
stosb | mov es:[di],al inc di |
||||||||
shift op,cnt | push cx mov cx,cnt m: shift op,1 loop m pop cx |
||||||||
Команды передачи управления: | |||||||||
j(условие) loc | jn(условие) loc1 jmp loc loc1: . . . |
||||||||
loop loc | dec cx jne loc |
||||||||
jmp addr | push addr ret |
||||||||
jmp dword ptr addr | push addr+2 push addr retf |
||||||||
call addr | push m jmp addr m: . . . |
Cокращения: op, op1, op2 - операнды команд; r - операнд-регистр; shift - код команды сдвига; cnt - счетчик в командах сдвига; loc - метка в командах перехода и цикла; addr - адрес в командах перехода и вызова подпрограмм.
Одной из самых сложных работ в программировании является модификация исполняемых модулей при отсутствии исходных текстов. Надо признать, что программисту не часто приходится заниматься подобными работами, но, однако, никто не застрахован от потери собственных исходных текстов программ. В частности, мы обратились к решению названной задачи для того, чтобы исправить найденную в нашем пакете ошибку. "Исходники" к тому времени были случайно уничтожены при борьбе с вирусом. Бесполезно вспоминать и переписывать весь пакет - за тот срок, в течении которого гарантировалось устранение любых замечаний, мы в любом случае не успевали этого сделать. Поэтому стали искать другие пути. В результате разработали соответствующий метод и даже набор инструментальных средств, который с успехом применяем по сей день.
Разумеется, приемы корректировки исполняемых модулей предназначены профессиональным разработчикам ПО для IBM PC в среде DOS. Поэтому опустим некоторые детали, которые профессионал должен знать, а в качестве примера возьмем лишь EXE-файлы - более сложные для модификации, поскольку все сказанное может быть отнесено и к COM-файлам.
Напомним, что EXE-файл состоит из заголовка, таблицы перемещения и собственно исполняемого кода. Информация заголовка используется операционной системой для загрузки модуля в оперативную память. Устанавливаются значения основных регистров, обрабатывается таблица перемещения, а затем управление передается задаче. В ходе работы все основные функции ввода/вывода, захвата или освобождения ОЗУ и т.п. обрабатываются операционной системой через прерывание 21h, а значит, всегда могут быть изменены с помощью специального драйвера, который либо резидентно находится в памяти, либо непосредственно включен в модифицируемый EXE-файл. Второй подход более удобен.
Для корректировки функций EXE-файла необходимо: внедрить в него этот драйвер; осуществить передачу управления на драйвер.
Вариантов включения собственного блока в существующий EXE-файл несколько: можно добавить его в конец или в начало файла; встроить в свободное место внутри программы или "склеить" им оба EXE-файла (свой и модифицируемый).
Легче всего добавить дополнительные команды в конец EXE-файла и исправить в заголовке размер загружаемой части (с учетом добавленных байт). Однако предварительно необходимо проверить, возможна ли их загрузка в ОЗУ. Дело в том, что задача может быть оверлейной и DOS загрузит только ее корневой сегмент. Например, в BORLAND C++ размер BCC.EXE более 800 кб, а объем ОЗУ всего 640 кб. Ясно, что загрузкой и выгрузкой отдельных частей гиганта занимается специальный блок в корневом сегменте программы, который ничего не знает о "прилепленном" в конце модуле, и поэтому тот никогда не попадет в оперативную память. А в заголовке имеется информация только о корневом блоке, небольшом по размеру. Так что если к загружаемой части не относится "хвост", значит данный метод здесь не годится.
В этом случае можно поискать "пустые" места в загружаемой части EXE-файла. Например, цепочку любых повторяющихся кодов. Если размер найденного пространства больше модуля, то все в порядке и следует вписать туда свои команды.
Если же и этого нет, то пробуем включить свой модуль в начало программы, сразу за таблицей перемещения, конечно, при наличии там свободного места. Впрочем, в отличие от предыдущего способа, здесь его всегда можно создать. Достаточно "раздвинуть" файл и изменить таблицу перемещения с учетом сдвига, затем подправить заголовок (адрес первой исполняемой команды и размер загружаемой в память части файла). Кстати, пример такой программы с комментариями приводится на дискете.
В принципе, можно и не раздвигать файл, а создать модуль в виде такого же COM или EXE-файла. Затем изменить список распределения кластеров в FAT-таблице каталога, не трогая их самих.
FAT (File allocation table) - это таблица в начальных секторах дискеты, которая содержит связный список цепочек кластеров, занимаемых тем или иным файлом на диске или относящимися к свободному пространству [2]. Ее дополняет оглавление файлов (называемое также директорий или каталог). Два байта по смещению 1Ah в элементе оглавления каждого файла содержат номер начального кластера цепочки, а каждый элемент цепочки в FAT-таблице указывает на следующее звено или информирует о конце файла.
Для включения модуля в EXE-файл необходимо найти в FAT-таблице цепочку, отмечающую занятые модулем кластеры диска и заменить в ней метку "конец модуля" ссылкой на начальный кластер встраиваемого исполняемого файла. Затем в элементе оглавления файла исправить номер начального кластера (вместо него вписать стартовый номер кластера нашего модуля), а сам модуль пометить как удаленный.
В результате при запуске EXE-файла на выполнение операционная система загрузит в память содержимое всей цепочки кластеров с диска, в том числе и кластеров нашего файла, который первым получит управление. Теперь уже его команды должны отвечать за запуск исполняемой части EXE-файла, в который он был внедрен.
Основная сложность при "склейке" двух программ заключается в том, чтобы после первой корректно запустить вторую. Если первая - ваша, вопрос решается просто: при ее завершении нужно передать управление по адресам, взятым из заголовка второй задачи. Если же обе из "склеиваемых" - "чужие", причем завершаются в неизвестном месте и неизвестным способом, то выход один - необходимо изменить часть DOS, обрабатывающую функции завершения процессов.
Это можно сделать двумя способами. Первый. Создается специальный драйвер перехватывающий все функции завершения работы программ (назовем его монитор). Затем, "склеиваются" вместе все выбранные EXE или COM-файлы, причем сначала в полученном конгламерате размещается монитор. При запуске он принимает на себя обработку прерываний, а получив управление, анализирует - какая из задач уже отработала, освобождает ресурсы, выбирает и размещает в ОЗУ следующую задачу, и передает ей управление. После последней задачи монитор восстанавливает исходную подпрограмму DOS и завершает свою работу.
Второй вариант является модификацией первого. Разница заключается только в том, что монитор не обладает суверенитетом отдельной задачи, а прикрепляется к первой из них одним из уже описанных способов. После этого, к "зараженной" монитором задаче можно командой COPY добавлять любое количество EXE-файлов.
Управление автоматически перейдет на модуль, внедренный в тело EXE-файла, если изменены начальные значения регистров CS и IP в заголовке файла (байты 20 - 23 заголовка) или первый оператор, на который указывают эти регистры (вместо него ставят команду перехода JMP либо CALL по адресу своего модуля). Но можно перехватить управление исправив номер прерывания в двухбайтовой команде INT. Рассмотрим эти методы подробнее.
Для изменения начальных значений регистров CS и IP в заголовке EXE-файла после внедрения блока команд в его тело нужно скопировать с 20 по 23-й байт заголовка (CS:IP) в специальный буфер, предусмотренный в модуле. Затем вписать в заголовок файла адрес первой команды модуля, а по его завершении передать управление на сохраненный в буфере адрес начала выполнения EXE-файла.
Впрочем, можно измененить первую команду (указанную регистрами CS:IP), не меняя сам заголовок. После внедрения блока команд в тело файла скопируем первые байты в заранее предусмотренный буфер, а вместо них впишем JMP или CALL с адресом модуля. По завершении его работы выполним сохраненные в буфере команды EXE-файла и передадим управление ему самому (на адрес из заголовка файла с учетом уже выполненных команд).
Если же недалеко от начала EXE-файла встретится собственный JMP или CALL с атрибутом far , то достаточно изменить адрес перехода у существующей команды (запомнив первоначальное значение) на адрес встроенного модуля. Удостовериться в том, что обнаруженный вами байт ( EAh или 9Ah ) действительно является первым байтом команды JMP или CALL с аттрибутом FAR можно используя таблицу перемещения: один из ее элементов должен указывать на адрес на 3 больший, чем адрес найденного байта (это сегментная часть адреса в команде).
Изменение номера прерывания в двухбайтной команде INT является самым простым из всех перечисленных способов перехвата управления. Метод не требует внедрения своего модуля в тело "чужого" файла. Достаточно лишь изготовить резидентный драйвер, отрабатывающий любое свободное прерывание DOS. А в EXE-файле ищем первое попавшееся прерывание и заменяем его тем, на котором "висит" наш драйвер. Естественно, что он должен обрабатывать и функцию исправленной команды INT в ЕХЕ-файле.
Допустим, надо отстранить резидентную задачу от обработки какого-либо прерывания. В этом случае необходимо выяснить, какая из загруженных в ОЗУ программ получает, словно эстафетную палочку, обработку данного прерывания.
Доступ к диску осуществляется, в основном, через 13h и 21h прерывания. Системы защиты обычно "забирают" их на себя и проверяют, разрешен ли доступ по ним. Если разрешен, то остальной процесс возвращается обратно к операционной системе. Порой, чтобы снять защиту, достаточно восстановить переустановленный вектор прерывания, направив его на DOS. А найти старый адрес поможет пошаговый режим (трассировка) отладчика. Но как определить, что "процесс дошел" именно до защитного механизма? Это можно сделать автоматически. Текст такой программы (на языке Си) находится на дискете в директории FTRACE.
Заметим, что трассировкой пользуются не только отладчики, но и вирусы. Метод их работы демонстрируется программой RV (файлы rv.c, ftrace.asm, ftrace.h). Кстати, "вирусный" подход можно использовать и для борьбы с вирусами же. Практика показала его эффективность: например, приведенная на дискете программа protect.asm мешает "хорошо себя чувствовать" некоторым вирусам, но и, правда, некоторым антивирусам тоже.
В прерыдущей главе описано, как внедрить защитный механизм в тело программы. В этой, напротив, расскажем о способе его удаления с помощью программы EXEB, имеющейся на дискете.
Программа EXEB.EXE предназначена для анализа исполняемых модулей и удаления из них пристыковочных частей - вирусов, защит и т.п. Она корректно работает на ПЭВМ типа IBM PC/XT/AT в среде MS DOS версии 3.1 и выше.
Поставляемая дискета содержит следующие файлы:
README - описание;
EXEB.EXE - основная программа;
CHSIZE - утилита для уменьшения размера результирующей задачи.
Перед началом работы необходимо скопировать дискету на жесткий диск в любой директорий.
Формат команды запуска:
EXEB [!] <IntNumber> <FuncNumber> <Counter> <TaskName> [<CommandLine>]
где:
! - признак "холостого" срабатывания;
<IntNumber> ::= номер перехватываемого прерывания;
<FuncNumber> ::= номер функции (AH) указанного прерывания;
<Counter> ::= счетчик появлений функции FuncNumber;
<TaskName> ::= имя исследуемой задачи;
<CommandLine>::= аргументы командной строки исследуемой задачи.
Примечание:
В командной строке в качестве аргумента FuncNumber разрешен символ "*".
Числовые аргументы задаются в 16-ричной системе счисления.
Например, "EXEB 21 30 1 a.exe" - означает: перенести код исследуемой задачи a.exe в результирующий EXE-файл при первом обращении к 30h функции 21h прерывания после запуска a.exe.
Аргументы желательно выбирать таким образом, чтобы они соответствовали первому после пристыкованного блока прерыванию в теле исследуемой задачи.
Если программа была создана компиляторами с языков высокого уровня (С, PASCAL, BASIC и т.п.), то рекомендуются следующие значения аргументов:
<IntNumber> = 21 <IntNumber> = 21 <FuncNumber> = 30 или <FuncNumber> = 35 <Counter> = 1 <Counter> = 1.
EXEB позволяет исследовать исполняемый EXE-модуль на команды вызова прерываний (int). Их номера фиксируются программой EXEB, если в качестве аргумента FuncNumber использован символ "?". Например: при строке запуска программы "EXEB 10 ? 1 a.exe" в крайнем правом столбце экрана будут выcвечиваться текущие номера функций 10-го прерывания.
Первый аргумент командной строки "!" - признак "холостого" срабатывания - необязателен. Его функциональное назначение - выдача звукового сигнала в момент обнаружения указанной вами в командной строке ситуации (без реальной ее обработки программой EXEB, т.е. без создания результирующего EXE-файла).
Итогом работы EXEB является файл с именем TASK.EXE, который можно запускать на выполнение (в той же среде, в которой он был создан: версия DOS, конфигурация ПЭВМ).
Будучи запущенной, EXEB захватывает указанное в командной строке прерывание, запускает исследуемую задачу и начинает отсчитывать обращения к указанной в командной строке функции захваченного прерывания. Когда счетчик обращений станет равен 0, EXEB переносит образ исследуемой задачи из оперативной памяти на диск. При этом первой исполняемой командой задачи станет вызов указанного в командной строке EXEB прерывания (int ??). Схематично последовательность работы EXEB представлена на рис. 7.1 - 7.3.
После запуска задача EXEB выдает на экран сообщение о готовности и ожидает нажатия любой клавиши. В этот момент пользователь при желании может вставить дискету в дисковод (при исследовании задачи, имеющей защиту по ключевой дискете).
Результирующая задача TASK.EXE не всегда способна воспринять командную строку, указанную в аргументе <CommandLine> при ее создании.
TASK.EXE должна запускаться примерно в той же среде, где она была создана (объем свободной оперативной памяти). В случае какого-либо несоответствия на экран выдается сообщение об этом и TASK.EXE прекращает работу.
Так как EXEB "снимает" в файл TASK.EXE практически всю оперативную память, то его объем может быть очень большим. Для уменьшения размера TASK.EXE можно использовать утилиту CHSIZE, формат запуска которой вы можете выяснить запустив ее без аргументов.
Если вы внимательно прочитали весь предшествующий материал, то справиться с двумя головоломками будет несложно. Если же задания оказались вам не под силу, вновь проштудируйте статьи.
На рис. 8.1 приведена программа, которая не работает под отладчиком в пошаговом режиме. Проверьте это, оттранслировав и запустив ее, например, под Turbo Debugger. Нажимая клавишу F8 (без захода в п/п), благополучно доберетесь до ее конца. Если же попробуете пройти программу в потактовом режиме (клавиша F7), с заходом в подпрограмму "DEL", то после выполнения оператора STOSW продолжение будет невозможно. Почему?
Программа на рис. 8.2 также не работает в пошаговом режиме, но причина здесь иная. В чем тут дело?
cseg segment para public 'code' assume cs:cseg exam proc start: jmp frwd del: pushf cld mov ax,cs mov es,ax rep stosw popf ret frwd: push cs pop ds mov cx,12 mov di,offset start call del mov dx,offset msg mov ah,9 int 21h mov ax,4c00h int 21h msg db 'Hellow, boys!',0ah,0dh,'$' exam endp cseg ends end start |
рис. 8.1 ГОЛОВОЛОМКА 1 |
TEXT: segment byte public 'CODE' assume cs:TEXT org 100h start: mov dx,offset prv mov ah,9 int 21h db 2eh pushf pop ax sahf mov ah,9 jb Glk mov dx,offset mes1 int 21h mov ax,4c00h int 21h Glk: mov dx,offset mes2 int 21h mov ax,4c01h int 21h prv label byte db 'dbtest',0ah,0dh,024h mes1 label byte db 'Ok',0ah,0dh,24h mes2 label byte db 'Trace',0ah,0dh,24h TEXT ends end start |
рис. 8.2 ГОЛОВОЛОМКА 2 |
Второй номер компьютерного приложения к журналу "Техника-молодежи" продолжит тему защиты информации. В нем будут даны ответы на некоторые вопросы по вирусологии, возникающие у неискушенных пользователей, состоится знакомство с программами-отладчиками и детально описаны способы защиты исполняемых модулей от исследования:
- вирусы и их функции;
- отладчики: состояние, перспективы; как "доработать" имеющийся отладчик;
- библиотека подпрограмм для защиты исполняемого кода от исследования;
- распределенные программные модули; генерация исполняемого кода;
- программный замок с псевдослучайным ключом;
- DLOCK ver 3.0;
- рекомендации по защите программ для ПЭВМ типа "Enterprisse" и "Spectrum" ("Синклер");
- квалификационная тест-программа.
Дискета, как всегда, содержит исполняемые модули и исходные тексты программ на Ассемблере, Си, Бейсике.
Подписаться на оптовую поставку компьютерного приложения можно в редакции журнала "Техника-молодежи" (125015, Москва, Новодмитровская ул., д. 5а, тел. 285-16-87, 285-89-80), приобрести единичные экземпляры - там же или у наших диллеров (у которых Вы купили первый выпуск).
Аномалии МП - специфические особенности выполнения некоторых команд микропроцессора.
Байт - единица информации, в системе MS DOS байт может принимать значения кодов от 0 до 255, 2 байта составляют машинное слово (значение от 0 до 65536).
Блок (подпрограмма, процедура) - часть программы (хранящаяся на диске внутри .EXE файла), имеющая собственный набор команд и данных, и предназначенная для выполнения некоторых действий с последующей передачей управления тому блоку основной программы программы, который ее вызвал.
Дизассемблер - программа, позволяющая получить текст других программ на языке ассемблер.
Дисперсия - мера рассеяния значений случайной величины около ее математического ожидания.
Драйвер - (англ. "водитель") резидентная программа, постоянно находящаяся в оперативной памяти и активизирующаяся от соответствующего прерывания, обработка которого за ней закреплена. В отличие от других резидентных программ, драйвер, как правило, создается для сопровождения одного устройства или поддержки одной программы, формируя необходимую для их нормальной работы среду в оперативной памяти.
Заголовок EXE-файла - начальная часть файла, в которой, в частности, содержится информация о стартовом значении регистров МП, размере всего файла, таблица перемещения и т.д.
Защитный механизм - часть исполняемого модуля, реализующая защитные функций: идентификацию пользователя, компьютера, магнитного носителя, защиту программы от исследования и т.п.
Идентификация - сопоставление предъявленных характеристик с эталонными.
Исполняемый модуль - программа или ее законченная часть (хранящаяся на диске отдельно), имеющая набор команд и данных, имя и предназначенная для загрузки в оперативную память ПЭВМ с последующуй передачей ему управления.
Исходный текст (исходник) - текст программы на одном из языков программирования и введенный в ПЭВМ непосредственно программистом (до какой-либо обработки его компьютером).
Кластер - минимальная единица дисковой памяти, выделяемая DOS-ом для хранения файла.
Ключевая фраза - (она же - парольная) последовательность символов, вводимая пользователем с клавиатуры для его идентификации.
Конвейер шины данных - аппаратно реализованная память ПЭВМ (обычно 4 - 6 байт), предназначенная для хранения очереди процессорных команд, ожидающих выполнения.
Контроллер - аппаратное устройство, реализующее обмен информации между ПЭВМ и периферийным оборудованием.
Контрольная точка - команда, после или до выполнения которой может быть зафиксировано состояние вычислительного процесса.
Листинг - текст программы написанный на одном из языков программирования, в котором, как правило, указана информация по выделенным для программы ресурсам компьютера (распределение памяти и др.) и приведены коды команд. В отличие от исходных текстов, которые вводит программист, листинг конечный продукт специальных программ (трансляторов, компиляторов, дизассемблеров и т.д.).
Математическое ожидание - характеристика случайной величины, ее определение связано с понятием о среднем значении множества (область центра математического множества).
МП - микропроцессор.
НСК - несанкционированное копирование.
НСД - несанкционированный доступ.
Отладчик - программа, позволяющая исследовать процесс выполнения других программ.
ПО - программное обеспечение.
Пошаговый режим - процесс выполнения программы по одной команде. После выполнения каждого шага управление возвращается программе, осуществляющей пошаговый режим (для возможности просмотра состояния ОЗУ и содержимого регистров МП).
Прерывание - программно или аппаратно инициированная передача управления по адресу, который находится в таблице векторов прерываний:
- аппаратное - инициируется контроллерами периферийного оборудования;
- программное - инициируется программой;
- трассировочное - аппаратное прерывание по вектору 1, выполняемое после каждой команды процессора, если флаг трассировки TF слова состояния процессора установлен;
- 21h - программное прерывание по вектору 21h, реализующее все основные функции DOS (файловые операции, управление памятью, справочная информация и т.д.);
- 13h - программное прерывание по вектору 13h, реализующее все основные операции по работе с диском;
- функции - выполняемые тем или иным прерыванием в зависимости от значений входных аргументов.
Пристыкованный блок - блок программы (модуль), который работает только один раз, как правило, сразу после запуска программы, и после передачи управления основной задаче (к которой он пристыкован) в дальнейшем вычислительном процессе не участвует.
Псевдослучайная последовательность - последовательность чисел, подчиняющаяся заданному закону распределения.
Резидентная программа - программа, постоянно находящаяся в оперативной памяти и активизирующаяся от соответствующего прерывания, обработка которого за ней закреплена.
Самомодифицирующийся модуль - исполняемый блок программы, изменяющий собственные команды в процессе выполнения.
Сегмент - страница оперативной памяти (блок ОЗУ), отведенная либо для кодов программы (кодовый сегмент), либо под стек (стековый), либо под данные (сегмент данных).
Сегментные: регистры - аппаратные регистры микропроцессора, предназначенные для хранения адресов соответствующих сегментов: CS - кодового, DS - данных, SS - стека;
пересылки - пересылки данных из сегмента в сегмент, в которых участвуют сегментные регистры.
Стек - часть оперативной памяти, выделяемая программе для хранения промежуточных результатов вычислений и данных.
Таблица перемещения - часть заголовка EXE-файла, содержащая адреса команд и данных в его теле, относительно начала файла на диске. После выделения DOS-ом свободных сегментов для загрузки в ОЗУ, операционная система, пользуясь таблицей перемещений, пересчитает относительные адреса на абсолютные (те, на которых программа реально будет располагаться в оперативной памяти).
Точка останова - адрес команды, перед выполнением которой управление передается отладчику.
Трассировка - процесс пошагового выполнения программы под отладчиком.
Утилита - программа, дополняющая операционную систему, выполняющая одну из функций обслуживания компьютера или его периферии.
Элемент оглавления - структура, описывающая дисковый файл.
Эмулятор - устройство (программа), позволяющее анализировать результаты выполнения каждой команды исследуемой программы без реальной передачи ей управления.
BIOS - Base Input Output System (базовая система ввода/вывода).
CMOS - энергонезависимая память ПЭВМ, имеет встроенные часы реального времени с календарем, содержит информацию о конфигурации машины.
COM-файл - исполняемый модуль, содержащий только бинарный образ задачи без какой-либо управляющей информации; этот тип программ всегда загружается в ОЗУ по одним и тем же адресам (указанным при написании программы), величина COM-файла не может превышать размер одного сегмента (64 КБ).
EXE-файл - программный файл (коды модуля, программы), хранящийся на диске, имеющий заголовок и таблицу перемещения. После загрузки модуля в оперативную память, DOS вводит в регистры МП начальные значения (из заголовка) и настраивает программу на выделенные сегменты памяти.
FAT - системная таблица диска, указывающая физическое раположение файлов и свободную область на диске.
SP (указатель вершины стека) - аппаратный регистр МП, содержащий смещение текущего адреса стека относительно начала стекового сегмента (адрес в регистре SS).