Пример 9.7.
: add word ptr [100], 01
: B406 mov ah,06 – прямой ввод через консоль
: B207 mov dl,07
0000010A: CD21 int 21h
0000010C: C3 Ret
В данном примере первая команда модифицирует сама себя и при этом становится на байт короче. Защита основана на том, что при выполнении первой команды, несколько следующих уже загружены в конвейер, и поэтому выполнение первой команды на реальном процессоре их никак не затронет.
В режиме же отладки, достаточно часто адрес следующей команды вычисляется после выполнения предыдущей, и, как правило, отладчики сбрасывают очередь команд на каждом шаге, вызывая трассировочное прерывание.
Если команда cmd1 изменит «образ» команды cmd2, стоящей в очереди в конвейере, на «образ» cmd2`, то это никак не отразится на ходе выполнения программы на реальном процессоре, поскольку процессор перейдет к исполнению следующей команды из очереди (cmd2). В режиме же трассировки после выполнения команды cmd1 произойдет вызов трассировочного прерывания. При возврате из прерывания очередь команд сбрасывается и из памяти выбирается модифицированная команда cmd2', что изменяет логику работы программы.
Отладчик «увидит» пример 9.7 следующим образом:
0Add word ptr [100], 01
0105 00B406B2 Add [si-4DFA],Dh
0pop es
010A CD21 int 21h
010C C3 ret
Аналогичный эффект демонстрировался в примере 8.8.
Пример 9.8.
...
mov byte ptr _cmd2,0F9h ; 0F9h -- код операции stc
_cmd2:
clc ; clc выполнится без отладчика, stc – под отладчиком
jc tracing
... ; код "нормального" режима
... ; выполнения программы
tracing:
... ; работа под отладчиком!
В данном примере команда mov byte ptr _cmd2, 0F9h (cmd1) осуществляет замену следующей команды clc (cmd2) на stc (cmd2'). При работе на реальном процессоре вместо команды stc будет выполнена «старая» команда clc, которая к этому моменту уже находится в очереди команд. При выполнении этого фрагмента под отладкой очередь команд будет сброшена и выполнится команда stc, что вызовет переход по метке tracing.
9.4.7. Использование недокументированных инструкций
Один из подходов, используемый для затруднения отладки и дизассемблировании программ, заключается в привлечении редко используемых инструкций центрального процессора (ЦП), недокументированных инструкций, или инструкций, имеющих скрытый результат [Error! Reference source not found.]. Не все злоумышленники хорошо знакомы с подобными инструкциями и скрытыми возможностями ЦП, что затрудняет исследование ими программ. Недостаток данных методов – жесткая привязка к процессору. Кроме этого, не гарантируется поддержка недокументированных инструкций в будущих процессорах, а значит совместимость с ними разработанных защит.
Достаточно часто для защиты ПО от исследования используют недокументированное использование префиксов инструкций.
Префиксы в кодах инструкций делятся на следующие типы:
1. Префиксы блокировки и повторения – говорят о том, что код инструкции относится к действиям блокировки или повторения.
2. Префиксы переопределения сегмента - указывают на то, с каким сегментом должна осуществляться работа команды. Соответствие между значениями префиксов и сегментами приведено в таблице 9.3.
Табл. 9.3. Соответствия префиксов и кодируемых ими сегментов
Значение префикса | Сегмент |
2Eh | CS |
36h | SS |
3Eh | DS |
26h | ES |
64h | FS |
65h | GS |
3. Префиксы переопределения размеров операндов (префикс 66h). Данный префикс используется в 16-разрядном режиме для манипулирования с 32-битными операндами и наоборот.
4. Префиксы переопределения размеров адреса (префикс 67h).
Если в процессорной инструкции используется более одного префикса из одной группы, то ее действие документально не определено. Приведем наиболее типичные приемы использования недокументированных команд и недокументированных возможностей процессора INTEL.
1. Недокументированное использование префикса переопределения размеров операндов. Согласно стандарту он используется только при наличии в команде каких-либо операндов. Однако на практике для реального процессора данный префикс может быть поставлен перед любой командой, и это будет работать на реальном процессоре. Например, реальный процессор примет инструкцию 0x66 CLI (использование префикса перед оператором запрещения прерываний). В связи с тем, что данная возможность не документирована, то, как правило, отладчики и дизассемблеры не принимают подобные инструкции и отказываются корректно интерпретировать подобный код.
2. То же самое следует сказать и про префиксы переопределения размеров адреса. Данные префиксы согласно документации используются только в командах, оперирующих с адресами памяти. Их использование с другими командами не документировано, но процессор будет выполнять эти команды. В качестве примера такой инструкции можно привести инструкцию 0x67 STI.
3. Префиксы переопределения сегментов также могут встречаться перед любой командой, в том числе и в командах, не обращающихся к памяти. Например, команда CS:NOP корректно выполняется, а многие дизассемблеры сбиваются при ее интерпретации.
4. Использование дублирующих префиксов, то есть запись префиксов вида 0x66, 0x66 непосредственно перед командой. Хотя фирма INTEL не гарантирует корректную работу своих процессоров при обнаружении такого рода инструкций, но фактически все процессоры правильно интерпретируют данные ситуации. Иное дело – отладчики и дизассемблеры, которые спотыкаются и начинают некорректно вести себя.
Следует отметить также, что процессором INTEL корректно выполняются и инструкции вида DS:FS:CS:Mov ax, [100] (последний префикс перекрывает все остальные), а многие отладчики и дизассемблеры сбиваются при их анализе.
5. Обращение к недокументированным регистрам. В процессорах INTEL регистры в настоящее время кодируются тремя битами следующим образом (таблица 9.4).
Табл. 9.4. Кодирование регистров в инструкциях
Код | Инструкция |
000 | ES |
001 | CS |
010 | SS |
011 | DS |
100 | FS |
101 | GS |
110 | Зарезервирован |
111 | Зарезервирован |
Две последние кодовые комбинации (110 и 111) в настоящее время зарезервированы и не используются. При попытке их использования вызывается исключительная ситуация 06h, которую можно перехватить. Отладчики же и дизассемблеры при встрече с подобными инструкциями начинают вести себя странно и непредсказуемо. Одни не генерируют при этом прерывания, чем и выдают себя, другие начинают некорректно работать. Поведение же дизассемблеров в этом случае тоже разнообразно. Ниже приведен пример того, как различные дизассемблера воспринимают подобные инструкции.
HIEW 8E??? F8 clc C3 retn | Qview 8EF8 mov! s, ax C3 ret | IDA Pro Db 8E Db 0F8 DB C3 |
Несуществующие регистры можно эмулировать в обработчике прерывания int 06h.
9.4.8. Шифрование кода программы
Рассмотренные выше приемы противодействия отладке и дизассемблированию имеют один серьезный недостаток – они привязываются либо к особенностям процессора, либо к особенностям отладчиков (дизассемблеров). Квалифицированные злоумышленники рано или поздно их обойдут.
Наиболее предпочтительным способом защиты ПО от отладки и дизассемблирования является способ, основанный на шифровании кода программы на некотором секретном ключе. При этом предъявляется требование того, чтобы секретность ключа не могла быть нарушена путем исследования кода программы и дискового пространства ПК. Таким образом, ключ не должен фигурировать ни в коде программы, а также не должен храниться ни в файле, ни в реестре, где он может быть обнаружен путем исследования работы программы различными средствами мониторинга.
Допустим, например, что секретный ключ расшифровывает рабочий код программы, а берется, из электронного ключа, либо представляет собой вводимую пользователем последовательность. В данном случае взлом становится очень трудоемким, а иногда и невозможным в приемлемые сроки. Затягивание времени взлома позволит все это время поддерживать объемы продаж.
Выделяют два способа шифрования кода программы – статическое и динамическое.
В первом случае модуль дешифровки отрабатывает только один раз, после чего исходный код будет полностью восстановлен. Такой подход имеет простую реализацию, но крайне неэффективен. Злоумышленник может снять дамб памяти в момент окончания работы модуля дешифровки. Это трудно сделать при динамической дешифровке, когда ни в какой момент времени код не будет расшифрован полностью. При вызове процедуры он расшифровывается, а при выходе – зашифровывается вновь.
Реализация простейшей процедуры шифрования / дешифрования кода программы может выглядеть следующим образом:
LEA SI, beginCrypt; | начало зашифрованного блока | |
Repeat: | Xor byte ptr [SI], 077h | |
INC SI | ||
CMP SI, offset endCrypt | ||
JNA Repeat | ||
beginCrypt | ||
…… | ||
endCrypt |
При анализе зашифрованных блоков программы, дизассемблер выдаст неверные результаты.
При динамической расшифровке не следует зашифрованные процедуры располагать следом одну за другой или передавать параметры в процедуры шифрования и дешифрования единообразно, иначе злоумышленник может достаточно быстро дешифровать данные процедуры с помощью специально написанных скриптов в дизассемблере IDA PRO, либо аналогичных ему. Рекомендуется располагать между зашифрованными процедурами случайное число незначащих байтов.
При атаке на шифр считается, что криптоалгоритм известен злоумышленнику с точностью до реализации и требуется найти только его ключ.
Уязвимости при реализации данного подхода появляются, когда код ПО шифруется и дешифруется не на ключе, а на его свертке. В данном случае при взломе злоумышленник перебирает не пароли, а свертки. При достаточно короткой свертке можно повысить скорость перебора на несколько порядков.
Желательно, чтобы расшифровка кода велась на достаточно длинном ключе, а контроль корректности введенного ключа производился путем сравнения свертки данного ключа со сверткой эталонного ключа. При этом желательно, чтобы свертка не удовлетворяла свойству рассеивания. В этом случае злоумышленник, зная свертку, получает множество удовлетворяющих ей ключей. При этом нет никаких достаточно строгих критериев, позволяющих автоматически отличить ложные варианты ключей. Как правило, злоумышленник должен брать ключ, запускать программу, она «зависает», брать другой ключ и т. д. В этом случае нахождение верного ключа значительно усложняется. Таким образом, данный метод является достаточно стойким и ставит множество проблем перед злоумышленником
10. Лабораторный практикум
10.1. Помехоустойчивые коды
Введем пространство сообщений в виде E(n, Um), где Um - алфавит, m - размерность алфавита, n - число символов из алфавита, образующих сообщение (слово) [17]. Такое пространство сообщений можно рассматривать как метрическое пространство, в котором расстояние между двумя сообщениями x и y, обозначаемое d (x, y) есть число различающихся символов в сообщениях x и y.
Пример 10.1. Пусть имеем алфавит U2 = {0,1} и пространство сообщений E(6, U2), в котором формируются сообщения: x =, y =Расстояние по Хеммингу между этими сообщениями будет равно 3, то есть d (x, y) = 3.
Приведенное определение расстояния d(x, y) в пространстве сообщений удовлетворяет всем свойствам расстояния. В процессе передачи информации по каналам связи возможно искажение передаваемого сообщения. При использовании двоичного алфавита искажение может привести к тому, что в принятом сообщении какая-нибудь переданная единица сменит свое значение на ноль или наоборот ноль исказится и примет значение единица. Возникает вопрос, как построить коды, позволяющие обнаружить такие сбои при передаче информации и позволяющие восстановить значения искаженных разрядов. Назовем коды, обладающие свойством обнаружения сбоев и восстановления искаженных разрядов при передаче информации помехоустойчивыми.
Рассмотрим случай, когда в процессе передачи сообщения оно может исказиться не более чем в k разрядах. В пространстве сообщений E(n, U2) выделим подмножество Hk Í E( n, U2), обладающее тем свойством, что для " x, y Î Hk выполняется неравенство
d (x, y) > k | (10.1) | |
Множество Hk назовем множеством осмысленных слов. Тогда любое x1 Ï Hk будет бессмысленным словом. Предположим, что при передаче слова x Î Hk оно исказилось и перешло в x1, но поскольку по условию искажение может произойти не более чем в k разрядах, то расстояние d(x, x1)£ k, следовательно, x1 Ï Hk и x1 есть бессмысленное слово. Таким образом, коды, удовлетворяющие условию (6.1), есть коды с обнаружением ошибки.
Пример 10.2. В пространстве сообщений E(3, U2 ) сформировать множество осмысленных слов.
Решение. Предлагается в качестве множества осмысленных слов H1 рассматривать множество { 000, 011, 110,101} c расстоянием между кодами 2. Тогда при k = 1 при передаче сообщения искажение в любом одном разряде превратит слово в бессмысленное.
Пример 10.3. В пространстве сообщений E(n-1, U2) сформировать множество осмысленных слов.
Коды с избыточностью - это коды, у которых количество осмысленных слов меньше общего числа возможных слов. Наличие избыточности является необходимым условием построения помехоустойчивых кодов. Рассмотрим построение кодов с обнаружением и исправлением ошибок, возникших при передаче сообщений. Предположим, что в процессе передачи информации может исказиться не более k разрядов кода. Множество осмысленных слов Hk Í E(n, U2) назначим таким образом, чтобы расстояние между его кодами подчинялось условию
d (x, y) > 2k | (10.2) | |
для " x, y Î Hk. Пусть в результате искажения код x перешел в код x1, тогда d(x, x1)£ k. Запишем неравенство треугольника d(x, y) £ d (x, x1) + d (x1, y). Усилим неравенство: 2k < k + d(x1, y) , что равносильно неравенству d(x1,y) > k. Последнее неравенство показывает, что расстояние от ошибочного слова x1 до слова x, подвергшегося искажению, меньше чем до любого другого осмысленного слова, тем самым позволяет восстановить правильное сообщение x. Коды, удовлетворяющие условию (6.2) называются кодами с исправлением ошибок.
Пример 10.4. В пространстве сообщений Е(4, U2) назначить множество кодов с исправлением ошибок.
Пример 10.5. В пространстве сообщений Е(6, U2) построить коды с обнаружением и исправлением ошибки.
Помехоустойчивые коды применяются при передаче информации на канальном уровне в Гигабит Ethernet (1000 Мбит/с).
10.2. Алгоритм кодирования и декодирования Хаффмена
Алгоритм Хаффмена [18] применяется при передаче информации по сети при удаленном доступе (протокол Тelnet ) . Предположим, что у нас есть алфавит из n символов. Нужно закодировать сообщение в виде строки бит. Присвоим каждому символу алфавита определенную последовательность бит (код). Затем соединим отдельные коды символов, составляющих сообщение, и получим кодировку сообщения. Пусть символам назначены следующие коды: A = 010; B = 100; C = 000; D = 111. Сообщение ABACCDA кодируется как . Такая кодировка считается неэффективной. Более эффективным считается подход, который использует частоту повторения кода, а именно: чаще всего повторяющемуся символу присваивается более короткий код, а менее частому более длинный. Введем в рассмотрение таблицу следующего вида:
Символ | Частота | Код |
А | 3 | 0 |
B | 1 | 110 |
C | 2 | 10 |
D | 1 | 111 |
При использовании этого кода сообщение ABACCDA кодируется как , что требует лишь 13 бит. В очень длинных сообщениях, которые содержат символы, встречающиеся чрезвычайно редко, экономия существенна. Обычно коды создаются не на основе частоты вхождения символов в отдельно взятом сообщении, а на основе их частоты во всем множестве сообщений.
Отметим, что если используются коды переменной длины, то код одного символа не должен совпадать с началом кода другого символа. Такое условие должно соблюдаться, если декодирование происходит слева направо. Такой подход к кодированию на основе учета частоты повторения символов в сообщениях позволяет реализовывать оптимальное кодирование информации. Операция предполагает использование структуры бинарного дерева. Каждый лист представляет символ исходного алфавита. Каждый узел представляет соединение символов из потомков данного узла.
На рис.10.1 приведено бинарное дерево, построенное с использованием рассмотренного выше примера. Каждый узел содержит символ и его частоту. Бинарное дерево строится снизу вверх, учитывая тот факт, что число предшественника узла со стороны левого поддерева меньше числа узла, а число предшественника узла со стороны правого поддерева больше или равно числу левого предшественника узла. Здесь листья есть символы сообщения, через запятую от символа обозначена его частота.
ACBD,7

A,3 CBD,4
![]()
C,2 BD,2
B,1 D,1
Рис. 10.1.
Такие деревья называются деревьями Хаффмена, по имени изобретателя этого метода кодирования. Как только дерево построено, код любого символа алфавита может быть определен просмотром дерева снизу вверх, начиная с листа, представляющего этот символ. Начальное значение кода - пустая строка. Каждый раз, когда мы поднимаемся по левой ветви, к коду слева приписывается 0; каждый раз при подъеме по правой ветви к коду слева приписывается 1, т. е. А имеет код 0; B имеет код 110 и т. д.
Алгоритм декодирования работает в обратном порядке. Проход по дереву начинается с корня дерева. Каждый раз, когда встречается 0, двигаемся по левой ветви, и каждый раз, когда встречается 1 , двигаемся по правой ветви, отнимая от кода слева соответствующий символ 0 или 1. Дойдя до листа, все откинутые от кода сообщения цифры образуют код расшифрованного символа. Повторяем этот процесс опять от корня к листу, но уже с меньшим кодом. Процесс повторяется до тех пор, пока в коде сообщения не останется цифр.
Пример. 10.6. Пусть известна таблица частот повторения символов вида
Символ | A | B | C | D | E | F | G | H | I |
Частота | 15 | 6 | 7 | 12 | 25 | 4 | 6 | 1 | 15 |
Используя таблицу, построить бинарное дерево для кодирования и декодирования сообщения IHFBDEGCA.
Решение.

IHFBDEGCA,91

![]()
![]()
IHFBD,38 EGCA,53

![]()
![]()
I,15 HFBD,23 E,25 GCA,28
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |



