Краеугольным камнем проектирования систем защиты является разработка средств, препятствующих изучению алгоритмов работы систем. Если в системе защиты будут отсутствовать такие средства, то все усилия на ее создание не будут иметь никакого смысла, поскольку квалифицированный системный программист сможет разобраться в логике работы любого модуля системы и, следовательно, преодолеть любую систему защиты.
Изучение логики работы программы можно выполнить в двух режимах: статическом и динамическом. Суть статического режима заключается в дисассемблировании программы (преобразовании выполняемого программного модуля в исходный текст или листинг исходного текста) и изучении результатов дисассемблирования. Суть динамического режима состоит в трассировке программы. Под трассировкой программы понимается выполнение программы на вычислительной машине в некоторой среде, позволяющей осуществить доступ к регистрам, областям памяти, произвести останов программы по некоторым адресам и т. п. В динамическом режиме изучение логики работы программы осуществляется либо в процессе трассировки, либо по сохраненным результатам трассировки.
Заметим, что программы, защищенные только от дисассемблирования, могут легко трассироваться, и наоборот — программы, защищенные только от трассировки, могут быть дисассемблированы. В следствие этого для противодействия изучению алгоритмов систем защиты необходимо иметь средства, препятствующие как дисассемблированию, так и трассировке.
Принципы работы отладчиков. Трассировку программы легче всего выполнить с помощью программных продуктов, которые называются отладчиками. Основное назначение отладчиков — обнаружение ошибок в неправильно работающей программе. В настоящее время на рынке программного обеспечения предлагается много разнообразных отладчиков, среди которых есть просто уникальные по возможностям, предоставляемым для отладки программы. Вместе с тем принципы работы этих отладчиков одинаковые. Базисом для построения любого отладчика служат методы покомандного выполнения программы и контрольной точки.
Покомандное выполнение осуществляется с помощью средств процессора ПЭВМ. Для установки режима покомандного выполнения в процессоре имеется специальный флажок TF. Если установлен флажок 'TF, то процессор после выполнения каждой команды генерирует прерывание типа 1 и в соответствии с принципами операций подключается соответствующая процедура обработки прерывания. Когда процедура обработки прерывания получает управление, флажок TF сбрасывается и процессор начинает работать обычным образом. После завершения процедуры флажок TF восстанавливается и процессор снова переводится в пошаговый режим работы.
Контрольной точкой называется любое место в программе, где нормальное ее выполнение приостанавливается и предоставляется возможность некоторой специальной обработки. Обычно для реализации метода контрольной точки в код программы динамически вставляют команду INT 3 (прерывание 3 специально зарезервировано для этих целей). Процедура обработки прерывания 3 (процедура контрольной точки) после выполнения своих функций восстанавливает код, который был замещен командой INT 3 для останова по контрольной точке, и выполнение программы продолжается.
Следует обратить внимание на то, что для останова по контрольной точке не обязательно использовать команду INT 3, хотя это и самый удобный способ (команда INT 3 имеет длину 1 байт, остальные команды INT n — двухбайтные). Некоторые отладчики позволяют использовать и другие команды INT.
Кроме того, в процессорах INTEL 80386/80486 предусмотрены специальные аппаратные средства контрольной точки. В этих процессорах существуют специальные регистры средств отладки, используя которые можно устанавливать до четырех контрольных точек одновременно. Следующие программные события вызывают срабатывание любой из контрольных точек:
- обращение к участку памяти;
- изменение участка памяти;
- выборка команды по определенному адресу.
В процессорах INTEL 80386/80486 предусмотрена выработка прерывания при обращении к заранее определенным портам ввода-вывода. Более того, возможен режим работы, при котором команды, изменяющие или считывающие состояние признаков процессора, вызывают прерывание (при выполнении задачи в режиме виртуального процессора 8086 с параметром IOPL < 3).
Нужно быть виртуозом программирования, чтобы заметить
самомодифицирующуюся ловушку. Ведь при анализе листинга и с явными алгоритмами разбираться сложно (автор комментарии не оставляет), так что уж говорить о потайном смысле вроде бы расшифрованного участка.
Впрочем, и новичок-разработчик создать такие ловушки не сможет.
Но коль скоро он взялся за написание защитных механизмов, то ему придется осваивать изощренный стиль программирования, который запутывает дизассемблер нестандартной интерпретацией некоторых команд и нарушает общепринятые соглашения. Например, использование необычной структуры программы (совмещение стекового и кодового сегментов). Интеллектуальные дизассемблеры, как правило, это плохо воспринимают. А перекрестные вызовы процедур, многократные переходы из модуля в модуль и увеличение количества точек входа в них - не позволяют выявить блочную структуру программы.
Замена команд переходов, вызовов подпрограмм и прерываний направляет дизассемблер по ложному следу. Здесь вместо стандартного оператора вставляется группа других, в конечном счете выполняющих то же самое. Для неискушенных программистов на рисунке 4.1 (а-д) приведены такие варианты. Впрочем, для аналогичных эффектов достаточно в команде перехода изменить значение операнда (примеры е-ж). И еще проще модифицировать косвенные переходы (з-и).
????????????????????????
? СОКРЫТИЕ АДРЕСОВ ?
?а) безусловного перехода ?
? 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
Кстати, и саму команду можно модифицировать. Например, на рис. 4.1 (к) дизассемблер по адресу m: покажет PUSH AX (запись регистра AX в стек), поскольку этот байт имеет код 50h (01010000b), но перед ее выполнением один бит (4-й) меняется, в результате получается INC AX с кодом 40h (01000000b, увеличение содержимого регистра AX на 1) - то есть совсем другая команда, не отраженная в листинге.
Кропотливая работа преобразует участок до неузнаваемости. А "умный" дизассемблер (например, Sourcer), отслеживая "явную" передачу управления (в другое место), не найдет спрятанный блок и, следовательно, не будет дизассемблировать его.
ЗАМЕНА НА ЭКВИВАЛЕНТЫ
Самый простой - периодически заменять одну последовательнось команд на другую, внешне не похожую, но, в конечном итоге, выполняющую то же самое действие. Для этого подбираем эквиваленты. Например, команда MOV AX,BX и последовательность PUSH BX и POP AX - выполняют одно действие (пересылка содержимого регистра BX в регистр AX), команда CALL adr заменяется на последовательность PUSH IP+3 и JMP adr. Примерные варианты взаимозамены для основных команд ассемблера приведены в таблице 5.1 (естественно, для конкретных программ ее нужно дополнить и расширить).
Таблица 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 - адрес в командах перехода и вызова подпрограмм.
В тексте программы организуется участок, где будут храниться цепочки команд, с указанием адресов эквивалентных им участков. При очередной работе программа случайным образом меняет местами отдельные части из собственного тела и "хранилища". В результате после каждого прохода исполняемый код будет случайным образом изменен до неузнаваемости, однако функции программы не нарушаются. Единственный недостаток этого способа - новый вариант исполняемого кода часто не может быть адекватен предыдущему по скорости работы.
Разумеется, конкретная таблица может иметь несколько альтернативных вариантов для каждой последовательности. А для выравнивания их длин можно использовать команду NOP или ее аналоги (пара PUSH - POP или MOV AX,AX).