Как противостоять трассировке
Принципиальная возможность создания подлинно "невидимых" отладчиков большей частью просто возможностью и остается – большинство из них позволяют обнаружить себя даже непривилегированному коду.
Наибольшие нарекания вызывает использование однобайтового кода 0xCC для создания точки останова вместо поручения той же задачи специально на то предназначенным отладочным регистрам. Так поступают SoftIce, Turbo Debugger, Code Viewer и отладчик, интегрированный в Microsoft Visual Studio. Причем последний неявно использует точки останова при пошаговом прогоне программы – помещая в начало следующей инструкции этот пресловутый байт 0xCC.
Тривиальная проверка собственной целостности позволяет обнаружить факт установки точек останова, свидетельствующий об отладке. Не стоит использовать конструкции наподобие if (CalculateMyCRC()!=MyValidCRC) {printf("Hello, Hacker!\n");return;} их слишком легко обнаружить и нейтрализовать, подправив условный переход так, чтобы он всегда передавал управление нужной ветке программы. Лучше расшифровывать полученным значением контрольной суммы критические данные или некоторый код.
Простейшая защита может выглядеть, например, так:
int main(int argc, char* argv[])
{
// зашифрованная
строка Hello, Free World!
char s0[]="\x0C\x21\x28\x28\x2B\x68\x64\x02\x36\
\x21\x21\x64\x13\x2B\x36\x28\x20\x65\x49\x4E";
__asm
{
BeginCode: ; //начало контролируемого кода
pusha ; //сохранение всех регистров общего назначения
lea ebx,s0 ; // ebx=&s0[0]
GetNextChar: ; // do
XOR eax,eax ; // eax = 0;
LEA esi,BeginCode;// esi = &BeginCode
LEA ecx,EndCode ; // выислиление длины...
SUB ecx,esi ; // ...контролируемого кода
HarvestCRC: ; // do
LODSB ; // загрузка очередного байта в al
ADD eax,eax ; // выисление контрольной суммы
LOOP HarvestCRC ; // until(--cx>0)
xor [ebx],ah ; // расшифровка очередного символа s0
inc ebx ; // указатель на след. симв.
cmp [ebx],0 ; // until (пока не конец строки)
jnz GetNextChar ; // продолжить расшифровку
popa ; // восстановить все регистры
EndCode: ; // конец контролируемого кода
NOP ; // Safe BreakPoint here
}
printf(s0); // вывод строки на экран
return
0;
}
Листинг 221
При нормальном запуске на экране должна появиться строка "Hello, Free World!", но при прогоне под отладчиком при наличии хотя бы одной точки останова, установленной в пределах от BeginCode до EndCode на экране появится бессмысленный мусор наподобие: "Jgnnm."Dpgg"Umpnf#0"
Значительно усилить защиту можно, если поместить процедуру подсчета контрольной суммы в отдельный поток, занимающийся (для сокрытия свой деятельности) еще чем-нибудь полезным так, чтобы защитный механизм по возможности не бросался в глаза.
Потоки – вообще великая вещь, требующая к себе особого подхода. Человеку очень трудно смирится с тем, что программа может исполняться во множестве мест одновременно. Распространенные отладчики грешат тем, что отлаживают каждый поток по отдельности, но никогда два и более сразу. Приведенный ниже пример показывает, как это можно использовать для защиты.
// Эта функция будет выполняться в отдельном потоке
// ее назначение незаметно изменять регистр символов в строке,
// содержащей имя пользователя
void My(void *arg)
{
int p=1; // Указатель на шифруемый байт
// обратите внимание, шифровка выполняется
// не с первого байта, - это позволяет обойти
// контрольную точку, установленную на начало
// буфера
// выполнять до тех пор, пока не встретится перенос строки
while ( ((char *) arg)[p]!='\n')
{
// ожидать, пока очередной символ не будет инициализирован
while( ((char *) arg)[p]<0x20 );
// инвертировать пятый бит
// это приводит к изменению регистра латинских
// символов на противоположный
((char *) arg)[p] ^=0x20;
// указатель на следующий обрабатываемый байт
p++;
}
}
int main(int argc, char* argv[])
{
char name[100]; // буфер, содержащий имя пользователя
char buff[100]; // буфер, содержащий пароль
// забивка буфера имени пользователя нулями
// некоторые компиляторы это делают за нас, но не все!
memset(&name[0],0,100);
// выполнять процедуру My
в отдельном потоке
_beginthread(&My,NULL,(void *) &name[0]);
// запрос имени
пользователя
printf("Enter name:");fgets(&name[0],66,stdin);
// запрос пароля
// Важно: пока пользователь вводит пароль, второй поток
// получает достаточно квантов времени, чтобы изменить
// регистр всех символов имени пользователя
// Это обстоятельсво не так очевидно и не вытекает из
// беглого анализа программы, особенно при ее исследовании
// под отдадчиком, слабо показывающим взамного влияение
// отдельных компонентов программы друг на друга
printf("Enter password:");fgets(&buff[0],66,stdin);
// сравнение имени и пароля c
эталонными значениями
if (!(strcmp(&buff[0],"password\n")
// Важно: поскольку, введенное пользователем имя было
// преобразовано, фактически происходит сранение не
// strcmp(&name[0],"KPNC\n") а strcmp(&name[0],"Kpnc\n"),
// что далеко не очевидно на первый взгляд
|| strcmp(&name[0],"KPNC\n")))
// правильные имя и пароль
printf("USER OK\n");
else
// ошибка в вводе имени или пароля
printf("Wrong user or password!\n");
return
0;
}
Листинг 222
На первый взгляд программа ожидает "услышать" "KPNC:password" Но так ли это на самом деле? А вот и нет! Верный ответ – "Kpnc:password". В то время пока пользователь вводит свой пароль, второй поток обрабатывает буфер, содержащий его имя, меняет регистр всех символов, кроме первого, на противоположный. Весь фокус в том, что при пошаговой трассировке одного потока все остальные потоки выполняются независимо от него и могут произвольным образом вклиниваться в работу отлаживаемого потока, например, модифицировать его код.
Взять потоки под контроль можно введением в каждый из них точки останова, но если потоков окажется больше четырех (а что мешает разработчику защиты их создать?) отладочных регистров на всех не хватит и придется прибегать к использованию опкода 0xCC, который защитному механизму ничего не стоит обнаружить!
Ситуация усугубляется тем, что большинство отладчиков, в том числе и хваленый SoftIce очень плохо переносят программы со структурной обработкой исключений
(SEH). Инструкция, вызывающая обрабатываемое исключение, либо "срывает" отладчик, выходя из-под его контроля, либо передает управление на библиотечный фильтр исключений, который прежде чем передать управление прикладному обработку вызывает множество своих служебных функций, в которых взломщику немудрено и "утонуть".
Впрочем, по сравнению с ранними версиями SoftIce даже это большой прогресс, т.к. раньше он жестко держал некоторые прерывания, не позволяя программе самостоятельно обрабатывать, скажем, деление на нуль.
Если попытаться прогнать приведенный пример под SoftIce вплоть до версии 4.05 включительно (остальные не проверял, ввиду их отсутствия, но, скорее всего, они будут вести себя точно так же), он, достигнув строки int c=c/(a-b) внезапно "слетит", теряя контроль над отлаживаемым приложением. Теоретически исправить ситуацию можно заблаговременной установкой точки останова на первую команду блока __except, но, попробуй-ка вычислить, где расположен этот блок, не заглядывая в исходный текст, которого у хакера заведомо нет!
// Пример защиты, построенный на обработке структурных исключений
int main(int argc, char* argv[])
{
// Защищенный блок
__try{
int a=1; // Попытка деления на ноль
int b=1; // многословность объясняется тем,
// при выполнении следующей инструии отладчик SoftIce
//
теряет контроль над отлаживаемой программой и "слетает"
int c=c/(a-b); // что большинсвтво компиляторов
// выдают ошибку, встретив конструкцию
// наподобие
int a=a/0;
// некий код, который никогда не получит управления,
// но может быть вставлен для "отвода глаз". Если значение
// переменным a и b присваивается не непосредственно, а
// из результата, возращенного некими функциями, то при
// дизассемблировании программы их равенство будет не так
// очевидно. В результате взломщик может потратить много
// времени на анализ совершенно бесполезного кода
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// этот код получит управление при возникновении
// исключения "деление на ноль"
// но отладчик SoftIce не распознает такой ситации
// и требует ручной установки точки останова на первую
// инструкцию блока __except
// а, что бы определить, адрес блока __except
требуется
// разобраться каким именно образом реализованна поддержка
// SEH в конктерном компиляторе
}
}
Листинг 223
Прежде чем справиться с такой защитой, взломщику придется основательно изучить реализацию механизма обработки структурных исключений, как на уровне операционной системы, так и на уровне конкретного компилятора. В подавляющем большинстве существующей литературы этот вопрос обходится стороной. И не спроста – реализация SEH действительно очень сложна, громоздка, многословна. Все это приводит к тому, что большинство программистов и технических писателей совершенно не представляют, что находится у нее "под капотом".
Поскольку, SEH по-разному реализована в каждом компиляторе, нет ничего удивительно, что SoftIce отказывается ее поддерживать. Поэтому, предложенный вариант защиты очень стоек к взлому и, в то же время, крайне прост в реализации. А самое важное – он одинаково хорошо работает во всех операционных системах семейства Windows от 95 до 2000.
Как работает отладчик
…древним с их мыслящими машинами было куда легче.
Френк Херберт "Дюна"
Бороться с отладчиком, не представляя себе, как он работает, было бы по меньшей мере наивно, поэтому, ниже будут рассмотрены базовые принципы, лежащие в его основе. На всеобъемлимость это изложение не претендует, но позволяет читателю составить общее представление о вопросе. Технические подробности исчерпывающе изложены в главе "Debugging and Performance Monitoring" технического руководства "Intel Architecture Software Developer's Manual Volume 3: System Programming Guide", бесплатно распространяемого фирмой Intel.
Все существующие отладчики можно разделить на две категории – первые используют отладочные средства процессора, а вторые самостоятельно эмулируют процессор, полностью контролируя выполнение "подопытной" программы.
Качественный эмулирующий отладчик отлаживаемому коду ни обнаружить, ни обойти невозможно, но полноценных эмуляторов Pentium-процессоров на сегодняшний день нет, и вряд ли они появятся в обозримом будущем.
Да и есть ли смысл их создавать? Микропроцессоры Pentium предоставляют в распоряжение разработчика богатейшие отладочные возможности, позволяющие контролировать даже привилегированный код! Они поддерживают пошаговое исполнение
программы, отслеживают выполнения инструкции по заданному адресу, контролируют обращения к заданным ячейкам памяти (или портам ввода-вывода), сигнализируют о переключениях задач и т.д.
Если бит трассировки регистра флагов установлен, то после выполнения каждой машинной инструкции автоматически генерируется отладочное исключение INT 1, передавая управление отладчику. Отлаживаемый код может обнаружить трассировку анализом регистра флагов, поэтому, для обеспечения собственной невидимости отладчик должен распознавать команды чтения регистра флагов и эмулировать их выполнение, возвращая нулевое значение флага трассировки.
Следует обратить внимание на одно важное обстоятельство: после выполнения команды, модифицирующей значение регистра SS, отладочное исключение не генерируется! Отладчик должен уметь распознавать такую ситуацию и самостоятельно устанавливать точку останова на следующую инструкцию.
В противном случае, войти в процедуру, предваренную инструкцией POP SS (например, так: PUSH SS; POP SS; CALL MySecretProc), автоматический трассировщик не сможет. Не все современные отладчики учитывают эту тонкость, и такой прием, несмотря на свою архаичность, может оказаться далеко не бесполезным.
Четыре отладочных регистра DR0-DR3 хранят линейные адреса четырех контрольных точек, а управляющий регистр DR7 содержит для каждой из них условие, при выполнении которого процессор генерирует исключение INT 0x1, передавая управление отладчику. Всего существует четыре различных условия: прерывание при выполнении команды, прерывание при модификации ячейки памяти, прерывание при чтении
или модификации, но не исполнении ячейки памяти и прерывание при обращении к порту ввода-вывода.
Установкой специального бита можно добиться генерации отладочного исключения при всяком обращении к отладочным регистрам, которое возникает даже в том случае, если их пытается прочесть (модифицировать) привилегированный код. Грамотно спроектированный отладчик может скрыть факт своего присутствия, не позволяя отлаживаемому коду себя обнаружить, какие бы ни были у него привилегии (правда, если "подопытный" код отлаживает сам себя, задействовав все четыре контрольные точки, отладчик не сможет работать).
Если бит Т в TSS отлаживаемой задачи установлен, то при каждом переключении на нее будет генерироваться отладочное исключение до выполнения первой команды задачи. Чтобы предотвратить собственное обнаружение, отладчик может отслеживать всякие обращения к TSS и возвращать программе подложные данные. Необходимо заметить – Windows NT по соображениям производительности не использует TSS, (точнее использует, но всего один) и эта отладочная возможность для нее совершенно бесполезна.
Программная точка останова – единственное, что нельзя замаскировать, не прибегая к написанию полноценного эмулятора процессора. Она представляет собой однобайтовый код 0xCC, который, будучи помещенным в начало инструкции, вызывает исключение INT 0x3 при попытке ее выполнения.Отлаживаемой программе достаточно подсчитать свою контрольную сумму, чтобы выяснить: была ли установлена хоть одна точка останова или нет. Для достижения этой цели она может воспользоваться командами MOV, MOVS, LODS, POP, CMP, CMPS или любыми другими, - никакому отладчику невозможно их всех отследить и проэмулировать.
Настоятельно рекомендуется использовать программные точки останова в тех, и только в тех случаях, когда аппаратных уже не хватает. Однако, практически все современные отладчики (в том числе и SoftIce) всегда устанавливают программные точки останова, а не аппаратные. Это обстоятельство может быть с успехом использовано в защитных механизмах, примеры реализаций которых приведены в разделе "Как противостоять трассировке".
Как защитить свои программы
Откуда бы ни бралась ключевая информация – из реестра, файла или клавиатуры, взломщик может практически мгновенно локализовать ее местоположение в памяти и установить на него контрольную точку. Помешать этому нельзя, но не составит труда подложить хакеру неожиданный сюрприз, - путь ключевая информация анализируется не сразу же после получения, а передается в качестве аргумента множеству функций, которые что-то с ней делают и затем передают другим функциям, а те в свою очередь – следующим.
Защитный механизм может быть встроен во что угодно – хоть в процедуру открытия файла или расчета зарплаты. Не стоит делать явных проверок, пусть лучше в случае вызова функции с неверной ключевой информацией она возвратит неправильный результат, но не сигнализирует об ошибке. Взломанная программа на первый взгляд будет исправно работать, и далеко не сразу выяснится, что работает она неправильно (например, выводит на экран одни числа, а на принтер – совсем другие). А чтобы обезопасить легального пользователя от ошибочного ввода пароля, достаточно в одном месте явно проверить его контрольную сумму, которая не дает взломщику никакой информации об истинном значении пароля.
Таким образом, защита как бы "размазывается" по всей программе, буфера с ключевыми данными многократно дублируются и на отслеживание обращений у взломщика не хватит ни контрольных точек, ни терпения для анализа огромного объема манипулирующими с ними кода. Будет еще лучше, если после выполнения проверки ключевой информации, эти же самые буфера использовать для хранения служебных данных, обращение к котором происходит по возможности максимального часто. Это не позволит взломщику быстро отделить защитный механизм от прочего прикладного кода.
Попутно: поскольку большинство взломщиков ставит контрольную точку на начало контрольного буфера, имеет смысл поместить в первые четыре байта ключа "заглушку", обращение к которой либо не происходит вовсе, либо с ней манипулирует имитатор защиты, направляя хакера по ложному пути.
В такой ситуации взломщику ничего не останется как, затарившись пивом, плотно засесть за кропотливое изучение всего
кода программы, прямо или косвенно манипулирующего с ключевой информацией (а это многие мегабайты дизассемблерного листинга!). Если критическая часть кода зашифрована, причем ни в какой момент работы программы не расшифровывается полностью (при выходе в каждую функцию она расшифровывается, а при выходе зашифровывается вновь), хакер не сможет получить готовый к дизассемблированию дамп, и будет вынужден прибегнуть к трассировке. А вот тут его будет жать второй сюрприз!
Классификация защит
"Стать хакером очень просто. Достаточно выучить и понять: математические дисциплины (математический анализ, теорию функций комплексного переменного, алгебру, геометрию, теорию вероятностей, математическую статистику, математическую логику и дискретную математику...)".
Борис Леонтьев "Хакеры & Internet".
Проверка аутентичности
(от греческого "authentikos" – подлинный) – "сердце" подавляющего большинства защитных механизмов. Проверка аутентичности необходима,Должны же мы чтобы удостовериться,:
то ли лицо работает с программой, за которое оно себя выдает, и разрешено ли этому лицу работать с программой вообще.! В зависимости от результатов проверки защита либо передает управление основной ветке программы, либо "ругается" и блокирует работу.
В качестве "лица" может выступать не только пользователь, но и его компьютер или носитель информации, хранящий лицензионную копию программы. Таким образом, все защитные механизмы можно разделить на две основных категории:
· защиты, основанные на знании (пароля, серийного номера)
· защиты, основанные на обладании (ключевой диск, документация)
Защиты, основанные на знании, бесполезны, если обладатель защищенной с их помощью программы, не заинтересован в сохранении ее секретности. Он может сообщить пароль (серийный номер) кому угодно, после чего любой сможет запустить такую программу на своем компьютере.
Поэтому, парольные защиты для предотвращения пиратского копирования программ непригодны. Почему же тогда практически все крупные производители в обязательном порядке используют серийные номера? Ответ прост – для защиты своей интеллектуальной собственности грубой физической силой. Происходит это приблизительно так: …рабочая тишина такой-то фирмы внезапно нарушается топотом сапог парней в камуфляже, сверяющих лицензионные номера Windows (Microsoft Office, Microsoft Visual Studio) с лицензионными соглашениями, и стоит обнаружиться хотя бы одной "левой" копии, как появившийся, словно из-под земли, сотрудник фирмы начинает радостно потираеть
руки в предвкушении дождя вечнозеленых… В лучшем случае – заставят купить все "левые" копии, в худшем же…
К домашним пользователям в квартиру, понятное дело, никто не врывается – частная собственность и все такое, да к этому никто собственно и не стремится. Что с домашнего пользователя возьмешь-то? К тому же, самим фирмам выгодно массовое распространение их продукции, а кто его обеспечит лучше пиратов? Но и здесь серийные номера не лишние – они разгружают службу технической поддержки от "левых" звонков незарегистрированных пользователей, одновременно с этим склоняя последних к покупке легальной версии.
Такая схема защиты идеальна для корпораций -гигантов, но она не подходит для мелких программистских коллективов и индивидуальных разработчиков, особенно если они зарабатывают на жизнь написанием узкоспециализированных программ с ограниченным рынком назначения сбыта (скажем, анализаторов звездных спектров или системы моделирования ядерных реакций). Не имея достаточного влияния, "раскачать" сотрудников известных органов на облаву по проверки лицензионности своего ПО нереально, а "выбить" деньги из нелегальных пользователей можно разве что с помощью криминальных структур, да и то навряд ли. Вот и приходится рассчитывать только лишь на собственныесобственную
силыу и смекалку.
Тут лучше подходит тип защит, основанных на обладании некоторым уникальным предметом, скопировать который очень чрезвычайно тяжело, а в идеале – вообще невозможно. Первые ласточки этой серии – ключевые дискеты, записанные с таким расчетом, чтобы при их копировании копия чем-нибудь да отличалась от оригинала. Самое простое (но не самое лучше) слегка изуродовать дискету гвоздем (шилом, перочинным ножом), а затем, определив местоположение дефекта относительно сектора (это можно сделать записью-чтением некоторой тестовой информации – до какого-то момента чтение будет идти нормально, а потом начнется "мусор"), жестко прописать его в программе и при каждом запуске проверять – на том же самом месте дефект или нет? Когда же дискеты вышли из употребления, эта же техника была адоптирована и для компакт-дисков.
Кто побогаче уродует их лазером, кто победнее – все тем же шилом или гвоздем.
Таким образом, программа жестко привязана к диску (дискете) и требует ее присутствия для своей работы, а, поскольку скопировать такой диск нереально (попробуй-ка, добиться идентичных дефектов на копиях), пираты "отдыхают".
Защитные механизмы, основанные на обладании, часто модифицирует предмет обладания в процессе работы программы, ограничивая количество запусков программы или время ее использования. Особенно часто такая "фишка" используется в инсталляторах – чтобы не нервировать пользователя, ключ запрашивается лишь однажды – на стадии установки программы, а работать с ней можно и без него. Если количество инсталляций ограничено, ущербом от несанкционированных установок одной копии программы на несколько компьютеров можно пренебречь.
Единственная проблема – все это ущемляет права легального пользователя. Кому понравится ограничение на количество инсталляций? (А ведь некоторые люди переустанавливают систему и все ПО буквально каждый месяц, а то и несколько раз на дню). Ключевые диски распознаются не всеми типами приводов, зачастую "не видимы" по сети, а, если защитный механизм для увеличения стойкости к взлому, обращается к оборудованию напрямую, в обход драйверов, такая программа наверняка не будет функционировать под Windows NT\2000 и весьма вероятно откажет в работе под Windows 9x (если, конечно, она не была заранее спроектирована соответствующим образом, но если так – это хуже, ибо некорректно работающая защита, исполняющаяся с наивысшими привидениями, может причинить немалый урон системе). Помимо этого,
ключевой предмет можно потерять, его могут украсть, да и сам он может выйти из строя (дискеты склонны сыпаться и размагничиваться, диски – царапаться, а электронные ключи – "сгорать").
Конечно, эти претензии относится к качеству реализации, а не к идее ключей вообще, но конечным пользователям от этого ничуть не легче! Если же защита создает неудобства, у пользователей появляется очень сильная мотивация к посещению ближайшего доступного пирата на предмет приобретения у него контрфактного программного обеспечения.
И никакие разговоры о морали, этике, добропорядочности и т.д. не подействуют – своя рубашка ближе к телу, а о добропорядочности нужно в первую очередь задуматься разработчикам таких защит. Тов…, тьфу, господа, не отравляйте жизнь пользователям! Пользователи – тоже люди!
В последнее время наибольшую популярность обрели защиты, основанные на регистрационных номерах – удачно сочетающие защиты обоих типов. при первом запуске программа привязывается к компьютеру и включает "счетчик" (вариант – блокирует некоторые функциональные возможности). А чтобы ее "освободить" необходимо ввести пароль, сообщаемый разработчиком за некоторое материальное вознаграждение. Часто для предотвращения пиратского копирования пароль представляет собой некоторую производную от ключевых параметров компьютера (или производную от имени пользователя в простейшем случае).
Разумеется, этот краткий обзор типов защит очень много оставил за кадром, но подробный разговор о классификации защит выходит за рамки этой книги, так что отложим его до второго тома.
Рисунок 1 0x026 Основные типы защит
Кто такие хакеры
…Назови ты меня вчера быком, я был бы быком. Назвал бы ты меня лошадью -- и я был бы лошадью. Если люди дают имя какой-то сущности, то, не приняв этого имени, навлечешь на себя беду.
Приписывается китайскому мудрецу Лао-цзы
Прежде чем подавать на стол блюда хакерской кухни, неплохо бы разобраться кто, собственно, эти хакеры и что они едят? Заглянув в толковый словарь английского языка, например в "The American Heritage Dictionary", мы убедимся, что глагол "hack" возник в английском лексиконе задолго до появления компьютеров и в прямом смысле обозначал "бить, рубить, кромсать" (но не уродовать!) топором, мотыгой или молотом. Т.е. делать физически тяжелую, монотонную, занудную, интеллектуально непритязательную работу – удел батраков, неудачников и бездарей. Неудивительно, что производные от глагола "хак" обозначали "бить баклуши", "халтурить", "выполнять работу наспех" – ведь наемные рабочие испокон веков "фунциклировали" из-под палки! Термин считался пренебрежительным, если не ругательным: "хак" стало даже синонимом нашего "кляча"! Словом, в докомпьютерную эпоху титулом "хакера" ни один здравомыслящий человек ни возгордился бы…
Сегодня же "хакер" звучит практически так же как "национальный герой", пускай и преступный, но все же крутой малый, которому не грех подражать. Чем же объясняется такая метаморфоза?
По одной из гипотез в щелчке, издаваемом реле, американцам слышалось "хак - хак". Динозавры машинной эры состояли из многих тысяч реле и "хакали" во всю, особенно когда оператор ЭВМ запускал очередную программу на выполнение. Возможно, именно за это операторов и прозвали "хакерами". Или, говоря по-русски "клацальщиками". По другой гипотезе звук "хак" приписывается перфоратору, кромсающему перфоленту на мелкие куски, так что щепки (такие аккуратненькие круглые "щепочки") во все стороны летят!
На ассоциативном уровне обе гипотезы вполне правдоподобны. И реле, и перфоратор издают повторяющиеся монотонные удары, чем-то напоминающие кашель, а выражение "кашлять сухим кашлем" - одно из значений слова "hack". К тому же, программировали "динозавров" исключительно в машинных кодах, подчас с помощью переключателей или перетыкивания разъемов, - физически тяжелая, нудная, неблагодарная работа, достающаяся наименее привилегированной части персонала. Какой там романтизм? Какое изящество решений или полет мысли? Халтура сплошная… Редкая программа обходится без ошибок, а программа, составленная в машинных кодах – тем более. При желании любого оператора было можно назвать халтурщиком – "хакером" в ругательном смысле этого слова. "Вот, наделал кучу ошибок, хакер ты наш!"
Обыватели же, далекие от вычислительной техники, и знакомые с ней исключительно по фантастическим романам, испытывали перед ЭВМ благоговейное уважение, подогреваемое гордостью за научно-технические достижения всего рода homo sapiens в целом и американской нации в частности. "Белые воротнички" – цвет нации, управляющие махиной размером с супермаркет и стоящей дороже тысячи таких супермаркетов, вызывали у рядового американца смесь восторга, зависти и стремления к подражанию. Вроде как "я тоже хочу быть космонавтом", не задумываясь о том, что космонавтика это только с виду романтика, а в действительности – каторжная работа.
Но, если желание побывать в космосе до сих пор смогли реализовать лишь единицы, то ЭВМ стали широко доступными уже в начале шестидесятых. К тому времени их было можно встретить и в подвалах университетов, и в стенах крупных корпораций, и практически во всех исследовательских учреждениях. Очутиться за пультом ЭВМ в создании студента означало практически то же самое, что и "сесть за штурвал реактивного бомбардировщика". Программирование ассоциировалось отнюдь не с "батрачеством", а с интеллектуальной игрой.
И "старшие наставники" студентов – операторы ЭВМ были не только их руководителями, но и кумирами. Студенты, одержимые вычислительной техникой, стремились во всем копировать персонал, обслуживающий большие ЭВМ, часто без понимания сути происходящего. Прознав жаргонное прозвище операторов, студенты, не догадываясь о его иронично – оскорбительном оттенке, с достоинством стали называть хакерами и себя и своих товарищей, и даже свою работу окрестили "хакерством". Но в их устах слово "хакер" звучало отнюдь не насмешкой, а расценивалось как титул. Ты – хакер, значит, ты такой же мастер, как и настоящий оператор ЭВМ. Значит, ты крутой парень и перед тобой не стыдно снять шляпу.
Так "хакеры" из работяг превратились в программистов – энтузиастов, помешенных на компьютерах и выделывающих на них такое… такое, что другим и не снилось. Термин продолжал видоизменяться, мигрируя своими значениями в сторону "крутого трюка", "забавного эффекта", "выполненного со вкусом розыгрыша". Этот дух подхватили и другие факультеты, порой и вовсе не связанные ни с электроникой, ни с вычислительной техникой, ни даже с точными науками вообще. "Хаком" стали называть любой классный розыгрыш или нестандартное решение знакомой задачи, – жаргонный термин технического языка превратился в модное словечко, употребляемое всеми кому не лень.
Тем временем мутация "хакера" продолжалась… Чтобы понять ее причины мысленно перенесемся в конец шестидесятых – начало семидесятых, а, может, даже чуточку позже. В те годы среди западной молодежи витал дух борьбы. Борьбы с кем? Да разве это важно! Протестовали против войны во Вьетнаме (кто не хотел служить в армии – жгли повестки), ломали пуританские устои старого мира, провозглашая свободу любви, презирали деньги (или только делали вид, что презирали, завистливо поглядывая в сторону того, у кого они есть). По большому счету вся борьба сводилась к суете в песочнице и власть имущих в общем-то ничуть не раздражала.
Молодежные лидеры не имели в руках никакого оружия – ни политического, ни экономического, ни идеологического, не говоря уже об огнестрельном. К тому же, через десяток лет дух борьбы покинул Америку и весь шум закончился.
"Счастливое исключение" составили программисты. В те дни компьютерные системы еще не успели обзавестись достойной защитой, но уже управляли стратегически и экономическими важными объектами. Власть над компьютерами позволяла дать хорошего пинка и правительственным организациям, и финансовым магнатам, и корпорациям, и другим сильным мира сего, причем, оставаясь безнаказанным. Не существовало ни соответствующих законов, ни компьютерной полиции, способной "вычислить" преступника…
Словом, дикий запад времен разбоя, романтики и беспредела, когда человек с кольтом мог заставить шерифа мирного уездного городка "слушать Шопена лежа". У американцев надо сказать, по поводу освоения Америки очень сильный комплекс – одних вестернов они сняли больше, чем мы фильмов про Великую Отечественную Войну. Понятно дело, каждый юный американец в душе мнит себя полноправным ковбоем!
Компьютеры же позволили воплотить эту мечту в жизнь. Освой ЭВМ и носись по электронным сетям, как "неуловимый Джо", отстреливающий индейцев (банкиров, ЦРУ-шников и т.д.). Да и как не носиться, когда на книжных лотках как грибы появлялись фантастические романы, главными героями которых были компьютерные взломщики – хакеры. Писатели, никогда в жизни не видевшие ЭВМ, плохо разбирались в техническом жаргоне и употребляли его на интуитивно-бессознательном уровне безо всякого понимания. Достаточно перелистать "The Shockware Rider" Джона Бруннера (John Brunner) 1975 года, "The Adolescence of P?1" Томаса Риана (Thomas Ryan) 1977 года или "Necromancer" Вильяма Гибсона (Wilam Gibson), опубликованный в 1984 году, чтобы убедиться насколько их авторы были далеки от вычислительной техники. Впрочем, литературных достоинств произведений это ничуть не ущемляло, а читатели в своей массе были от вычислительной техники еще более далеки, чем писатели, и у них сложился устойчивый образ "ЭВМ – это круто", а "хак – это вообще круто".
Нейроматик, кстати, был самой любимой книгой Роберта Тапплана Морриса, создавшего своей знаменитый вирус – червь, надо полагать, не без влияния Вильяма Гибсона.
Журналисты, не обременение ни знаниями ЭВМ, ни лингвистическим образованием, из всего этого поняли только одно: некто, называющие себя хакерами, ломают компьютеры по всей стране, причем ломают весьма круто с убытками в особо крупных размерах.
Слово "хакер" вырвалось на страницы газет, но в широких массах глагол "хак" по-прежнему означал все те же "бить--кромсать", и американцы, вполне естественно, заключили, что хакер -- это тот, кто вламывается в чужие системы и раздалбывает их в пух и прах.
Вот, собственно, и все… Кольцо замкнулось, - термин "хакер" вернул свое "историческое" значение, но не прекратил эволюцию! Хакерам прошлого поколения (т.е. энтузиастам программирования) очень не понравилось, что их титул смешали, мягко выражаясь, с дерьмом, и при его упоминании от них все стали шарахаться как от огня. Стремясь реабилитировать себя в глазах общественности, хакеры предприняли попытку разделить всех своих на "хороших" и "плохих", оставив за "хорошими" парнями право называться "хакерами", для "плохих" придумав специальный термин "кракер" – от слова "crack" – ломать (кстати, почему не "брейкер" от слова "break"?), в буквальном смысле обозначающий "ломатель". Затея с треском провалилось, - далеко не каждый взломщик был готов нацепить на себя ярлык плохого паря. Называться хакером по-прежнему считалось и модно, и престижно, пускай все "хакерство" ограничилось "wannabe" (в дословном русском переводе "хочубытькак", т.е. подражанием). Предметы хакерской культуры обожествлялись, становясь предметом поклонения, догматом, иконой на стене.
Эта ветка генеалогического древа "хакеров" не имеет будущего и обречена на медленное, но неотвратимое вымирание.
Уже сегодня, в начале первого десятилетия двадцать первого века, термин "хакер" стал всеобъемлющим и утратил всякий смысл. Кто пишет вирусы? Хакеры! Кто ломает программы? Хакеры! Кто крадет деньги из банков? Хакеры! Кто пакостит в Сети? Хакеры! Кто программирует на ассемблере? Хакеры! Кто знает все тонкости операционной системы и железа? Хакеры! Сказать собеседнику, что ты хакер, не уточив, что конкретно ты имеешь под этим ввиду, все равно, что ничего не сказать.
Термин "хакер" умер, но ведь хакеры – остались! Остались и работяги-кодеры, пускай уже не клацающие реле, но зато шумящие пропеллерами вентиляторов, остались и энтузиасты программирования, упоенно программирующие и на древних, и на современных языках, остались и исследователи защит, и умельцы по их взлому… Люди есть, а термина, определяющего их принадлежность, уже нет.
Почему бы не назвать определенную категорию компьютерщиков "кодокопателями"? Этот термин, впервые употребленный Безруковым, на мой взгляд, очень удачен и интуитивно понятен без дополнительный объяснений. Любой, кто любит копаться в коде (не обязательно машинном) по праву может считать себя кодокопателем.
Таким людям, собственно и посвящена эта книга…
Немного истории
- Историческая система взаимных грабежей и вымогательств остановится здесь, на Арраки. Нельзя с годами преодолеть расхищение того, в чем нуждаешься, не принимая во внимание интересы тех, кто придет после тебя.
Френк Херберт "Дюна"
Раньше всех появился debug.com – пародия, отдаленно напоминающая отладчик, зато входящая в штатную поставку MS-DOS. Сегодня этот инструмент годился разве что для забавы и изучения ассемблера. Впрочем, и тогда от него мало кто был в восторге, и новые отладчики росли как грибы после дождя. Правда, большинство из них недалеко ушло от своего прототипа, отличаясь от оригинала разве что интерфейсом.
Это было золотое время разработчиков защит. Стоило лишь "запереть" клавиатуру, запретить прерывания, сбросить флаг трассировки, и отладка программы становилась невозможной.
Первые мало-мальски пригодные для взлома отладчики появились только после оснащения компьютеров 80286 процессором. В памяти хакеров навсегда останутся AFD PRO, написанный в 1987 году 'AdTec GmbH', знаменитый Turbo Debugger, созданный годом позже двумя братьями Chris'ом и Rich'ем Williams'ом, первый эмулирующий отладчик Сергея Пачковки, написанным, правда, с большим опозданием: в 1991 году. Разработчики защит крякнули, но выдержали – эти отладчики по-прежнему позволяли отлаживаемой программе захватить над собой контроль и очень плохо переносили "извращения" со стеком, экраном, клавиатурой…
Ситуация изменилась с выходом 80386 процессора – резкое усложнение программного обеспечения (и как следствие – огромные сложности с его отладкой) диктовало необходимость наличие развитых отладочных средств в самом процессоре. И в 386 они появились! С этого момента разработчикам защит стали наступать на пятки.
Масла в огонь подлила NuMega, выпустившая в конце восьмидесятых свой замечательный Soft-Ice, пользовавшийся у хакеров огромной популярностью, а ныне портированный на Windows 9x и Window NT/2000 и до сих пор остающийся бесспорным фаворитом (хотя, не без конкуренции).
Впрочем, неверно было бы считать, что NuMega – криминальная фирма, а Soft-Ice – исключительно хакерский продукт. Этот отладчик предназначен в первую очередь для разработчиков драйверов и легальных исследователей операционной системы (не разбираясь во внутренностях ОС, с драйверами особо не разгонишься).
Но, так или иначе, Soft-Ice задал копоти всем защитам и их разработчикам. Пускай он не был (да и сегодня не стал) полностью Stealth-отладчиком, невидимых для отлаживаемых программ, имел и имеет ряд ошибок, позволяющих: обнаружить отладчик, завестись его и/или вырваться защите из под контроля, но… в умелых руках отладчик справлялся со всеми этими ограничениями и обходил заботливо расставленные "капканы". И с каждой версией Айса противостоять ему становилось все труднее и труднее (старые ошибки устранялись быстрее, чем вносились новые).
Постепенно мода на антиотладочные приемы сошла на нет и уж совсем заглохла под победное шествие Windows. Распространилось совершенно нелепое убеждение, что под Windows на прикладном уровне дернуть хвост человеку с отладчиком – невозможно. Это вызывает ухмылку профессионалов, эпизодически встраивающих разные ловушки в свои программы – так, больше для разминки (дабы мозги жиром не заплыли), чем для серьезной борьбы с хакерами.
Бороться с хакерами при современном уровне продвинутости средств анализа приложений несколько наивно – те и от Тигра хвост оторвут, но сегодня кроме хакеров серьезную угрозу представляют и вчерашние желторотые пользователи, начитавшиеся различных faq "как ломать программы" (благо сейчас они доступны всем кому не попадя) и теперь только и ищущие на чем испытать свою богатырскую силу.
Неточности, недоговорки
1) "Почти все функции, создающие объекты ярда, принимают указатель на структуру SECURITY_ATTRIBUTES" как аргумент… Большинство приложений вместо этого аргумента передают NULL и создают объект с защитой по умолчанию. Такая защита подразумевает, что администратор и создатель объекта получают к нему полный доступ, а все прочие к объекту не допускаются" стр. 9.
Гм-гм, выходит, если к объекту необходимо допускать всех остальных, как часто и бывает, придется явно инициализировать SECURITY_ATTRIBUTES? Конечно же, нет! По умолчанию допускаются все пользователи со всеми полномочиями – будь то запись, чтение или еще что. Проверьте – создайте новый файл вызовом CreateFile, передав вместо атрибутов секретности NULL, и попытайтесь открыть его, войдя в систему под другим пользователем. Открывается? Вот и славненько!
2) "…если Вы создаете диалоговое окно, какой смысл формировать список одним потоком, а кнопку другим" стр. 53.
Смысл есть – пусть один (или несколько) потоков, занятых, скажем, поиском файлов на диске, создают один (или несколько) элементов списка для вывода результатов своей работы, а кнопка "Стоп" их всех "срубает".
3) "Как узнать, например, чьим объектом – User или ядра – является данный значок? ...проанализировать Win32 функцию, создающую объект. Практически у всех функций, создающих объекты ядра, есть параметр, позволяющий указать атрибуты защиты". стр. 9
Не очень-то надежный способ! Вот, у функции HINSTANCE LoadLibrary(LPCTSTR lpLibFileName) нет никаких атрибутов секретности, но описатель HINSTANCE принадлежит ядру. Почему? Да хотя бы уже потому, что ядро ее и экспортирует, о чем и рассказывается в SDK. Если под рукой нет SDK, на помощь приходит тот факт, что функция содержится в библиотеке kernel32.lib и, стало быть, – "ядреная".
4) "…по завершении процесса операционная система гарантированно освобождает все ресурсы, принадлежащие эту процессу" стр. 12.
…если только процесс не вызвал исключение, вызывающие его аварийное завершение.
Именно поэтому приходится перегружать машину после очередного "зависания" того же Word-а, – иначе при попытке открытия последнего редактируемого файла, будет выдано сообщение – файл уже открыт другим процессом и работать с ним невозможно. Хороший программист должен предусмотреть такую ситуацию и принять адекватные меры по ее устранению.
5) "Имейте ввиду: описатели
объектов наследуются, но сами объекты нет (курсив Рихтера)" там же
Ух, ты! килограмм не длиннее литра, да еще курсивом! Объекты ядра принадлежат ядру ОС, но не породившему их процессу, которому остается довольствоваться только описателями (дескрипторами) этих объектов. Поэтому о наследовании объектов ядра другими процессами говорить просто некорректно.
6) "Первый и третий параметр функции DuplicateHandle представляют собой описатели объектов ярда, специфичные для вызывающего процесса" стр. 18
Брр… ничего не понял! А вы, читатель? На самом деле, эти параметры описатели процессов – процесса-источника и процесса-приемника (точнее, выражаясь терминологией самого же Рихтера – псвевдоописатели)
7) "Граница между двумя типами приложений [консольных и графических –KK] весьма условна. Можно, например, создать консольное приложение, способное отображать диалоговые окна…" стр. 25
Тип приложения указывается в заголовке исполняемого фала и однозначно определяет механизм его загрузки и инициализации. Тот факт, что консольные приложения имеют доступ к GDI-функциям, а графические приложения могут создать консоли не позволяет делать вывод об "условности" границ между обоими типами приложений.
8) "Завершение потока" стр. 60.
К трем перечисленным Рихтером способам завершения потока (ExitThread; TerminateThread; завершение процесса, породившего поток) необходимо добавить и четвертый (кстати, самый популярный и простой из всех) – return. Т.е. возврат управления главной функции потока.
9) "В Windows 95 все четыре описанные функции не предусмотрены.
В ней не удается даже загрузить программу, вызывающую любую из этих функций" стр. 72
Да, но только если она загружает экспортирующую их DLL неявной компоновкой. Поэтому, очень важно объяснить читателю, что API функции, отсутствующие в Windows 95, настоятельно рекомендуется вызывать, явно загружая соответствующие им библиотеки и самостоятельно обрабатывая ситуации с отсутствием функций.
Вообще же, в отношении функциональности Windows 95, Рихтер очень туманен и выражается то "приложение, использующее такие-то функции не будет работать в Windows 95", то "приложение, использующее такие-то функции вообще не удастся загрузить в Windows 95". Очень важно отличать отсутствие функций и отсутствие их реализаций. Первых – в Windows 95 вообще нет, вторые как будто-то есть, но при попытке вызова всегда возвращают ошибку. Рихтер, увы, различает эти два случая не всегда.
10) "Любой поток может вызвать эту функцию [SuspendThread – KK] и приостановить выполнение другого потока. Хоть об этом нигде и не говорится (но я все равно скажу!), приостановить свое выполнение поток способен сам, а возобновить без посторонней помощи – нет… Поток допустимо задерживать не более чем M " стр. 72
В этом маленьком абзаце сразу три ошибки. Первое – для приостановки другого потока его надо открыть с флагом THREAD_SUSPEND_RESUME, на что не у всех остальных потоков хватит прав, так приостановить выполнение системных потоков очень проблематично (точнее, не прибегая к недокументированным секретам – невозможно).
Второе – приостановка осуществляется не только SuspendThread, но и массой функций таких как: Sleep, WaitFor…
Третье – эти функции, в частности Sleep, позволяют потоку самостоятельно контролировать свое "засыпание" – "пробуждение". Ну, во всяком случае, без явного вызова ResumeThread другим потоком.
11) "Если система почему-либо не свяжет EXE-файл с необходимыми ему DLL-модулями, на экране появится соответствующее сообщение, а адресное пространство процесса и объект "процесс" освобождаются" стр. 164
Не могу удержаться, чтобы не заметить, что в Windows 2000 при запуске процесса из консольного приложения сообщение о неудачной загрузке DLL не появляется и процесс тихо "кончает", оставляя пользователя в недоумении – почему он не работает?!
Поэтому, теперь программисту недопустимо игнорировать результат успешности завершения CreateProcess и необходимо самостоятельно вызывать GetLastError для донесения до пользователя причины ошибки. Не стоит надеяться на операционную систему – отныне она это уже не делает.
12) "Семейство Interlocked функций" стр. 312
Описывая эти синхронизующие функции, Рихтер упустил одно немаловажное обстоятельство – большинство компиляторов в большинстве случав для приращения (уменьшения) значения переменной на единицу, используют ассемблерные команды inc [var] и dec[var] соответственно. Они не могут быть прерванными на середине операции и заботится об их синхронизации незачем.
Не прерываются и операции сложения (вычитания) 32-разрядной переменной с 32-разрядной константой, а так же все аналогичные битовые операции[4].
Исключения:
а) 64-разрядные переменные;
б) известная "болезнь" ранних компиляторов от Borland – выполнение всех операций с переменными как минимум в три этапа: mov reg,[var]\ ops reg,const\ mov [var], reg;
в) сложные случаи адресации, разбиваемые компилятором на несколько стадий – вычисление эффективного адреса и приращение (уменьшение) переменной, расположенной по этому адресу.
13) "Если бы этот код выполнялся в Win32 приложении без блока try-finally и оно завершилось бы из-за неправильного доступа к памяти в Funcinator, семафор остался бы занят и не освободился – соответственно и ожидающие его потоки не получили бы процессорного времени" стр. 522
Постой, постой. Какие потоки? Если потоки самого процесса – так ведь они тихо скончались вместе с самим приложением, а если потоки других процессов – так ведь после завершения процесса семафор будет освобожден операционной системой.
Так что принудительное освобождение семафора в этом случае – очевидное излишество.
14) "Это простейший способ внедрения DLL [добавления внедряемой DLL в ключ реестра HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\ CurrentVersion\Windows\APPINIT_DLLS – KK].. Однако здесь есть ряд недостатков… Ваша DLL будет спроецирована лишь тех процессов, на которые отображен и USER32. А последнее делается только в GUI-приложениях, т.е. данные способ не подходит для программ консольного типа, - например, компиляторов или компоновщиков." стр. 602
В документации от Microsoft и в технических статьях сторонних авторов, содержащихся в том же MSDN, утверждается, что этот способ срабатывает для всех
процессов системы. Сейчас проверил на Windows 2000 – действительно, внедряемая DLL послушно проецируется даже на консольные приложения.
Потом, неверно утверждение, что консольные приложения не используют USER32. Используют, да еще как! Чаще всего он им необходим для подачи сигналов вызовом функции MessageBeep, экспортируемой USER32.
Обработка исключений в реальном и защищенном режимах
Когда возникает отладочное исключение (как, впрочем, и любое другое исключение вообще), процессор заносит в стек регистр флагов, адрес следующей (или текущей – в зависимости от рода исключения) выполняемой инструкции и лишь затем передает управление отладчику.
В реальном режиме флаги с адресом возврата заносятся в стек отлаживаемой программы, поэтому, факт отладки обнаружить очень просто – достаточно контролировать целостность содержимого, лежащего выше указателя стека. Или, как вариант, установить указатель на его вершину, - тогда добавление новых данных в стек окажется невозможным и отладчик не сможет функционировать.
Иная ситуация складывается при работе в защищенном режиме – обработчик исключения может находиться в своем собственном адресном пространстве и не использовать никаких ресурсов отлаживаемого приложения, в том числе и стека. Грамотно спроектированный отладчик защищенного режима ни обнаружить, ни блокировать принципиально невозможно, даже привилегированному коду, исполняющемуся в нулевом кольце.
Сказанное справедливо для Windows NT, но неприменимо к Windows 9x – эта операционная система не использует должным образом всех преимуществ защищенного режима и всегда "замусоривает" стек отлаживаемой задачи, независимо от того находится ли она под отладкой или нет.
Ошибки Джефри Рихтера
"И на солнце есть пятна"
Народная китайская мудрость
Монография Джефри Рихтера "Windows для профессионалов" – один из лучших (а, может быть, и самый лучший) учебник по программированию, настольная книга многих Windows-разработчиков (в том числе и меня). Это самое полное, проработанное и систематизированное описание Win32 API, написанное живым, легко доступным языком – без излишнего занудства и воды.
Дифирамбы – дифирамбами, но, каким бы непререкаемым авторитетом Рихтер ни был, а ошибки есть и у него. Не то, чтобы они сильно портили книгу, но все же…Словом, в этой главе речь пойдет именно о них. Не берусь утверждать, что выловил все ошибки, но вот, взгляните на то, что мне удалось обнаружить при вдумчивом чтении книги.
Номера страниц указаны по третьему изданию от 1997 года – самому последнему, которое мне удалось приобрести. Возможно, даже наверняка, какая-то часть ошибок принадлежит не самому Рихтеру, а сотрудникам "Русской редакции", выполнивших ее перевод на русский язык.
К сожалению, я не смог раздобыть оригинал и совершенно не представляю себе как это сделать. Да я ли один? Ведь и подавляющее большинство читателей этого самого оригинала и в глаза не видело! К тому же, цель статьи – не очернить Рихтера, а не дать ошибкам закрепиться в умах молодых программистов (запоминаются-то ошибки легко, а вот забываются куда труднее).
Итак…
Пара слов в заключении
Многие считают использование самомодифицирующегося кода "дурным" примером программирования, обвиняя его в отсутствии переносимости, плохой совместимости с различными операционными системами, необходимости обязательных обращений к ассемблеру и т.д. С появлением Windows 95/Windows NT этот список пополнился еще одним умозаключением, дескать "самомодифицирующийся код – только для MS-DOS, в нормальных же операционных системах он невозможен (и поделом!)".
Как показывает настоящая глава, все эти притязания, мягко выражаясь, неверны. Другой вопрос – так ли необходим самомодифицирующийся код, и можно ли без него обойтись? Низкая эффективность существующих защит (обычно программы ломаются быстрее, чем успевают дойти до легального потребителя) и огромное количество программистов, стремящихся "топтанием клавиш" заработать себе на хлеб, свидетельствует в пользу необходимости усиления защитных механизмов любыми доступными средствами, в то числе и рассмотренным выше самомодифицирующимся кодом.
"Подводные камни" перемещаемого кода
При разработке кода, выполняющегося в стеке, следует учитывать, что в операционных системах Windows 9x, Windows NT и Windows 2000 местоположение стека различно, и, чтобы сохранить работоспособность при переходе от одной системы к другой, код должен быть безразличен к адресу, по которому он будет загружен. Такой код называют перемещаемым, и в его создании нет ничего сложного, достаточно следовать нескольким простым соглашениям – вот и все.
Замечательно, что у микропроцессоров серии Intel 80x86 все короткие переходы (short jump) и близкие вызовы (near call) относительны, т.е. содержат не линейный целевой адрес, а разницу целевого адреса и адреса следующей выполняемой инструкции. Это значительно упрощает создание перемещаемого кода, но вместе с этим накладывает на него некоторые ограничения.
Что произойдет, если следующую функцию "void Demo() { printf("Demo\n");}" скопировать в стек и передать ей управление? Поскольку, инструкция call, вызывающая функцию pritnf, "переехала" на новое место, разница адресов вызываемой функции и следующей за call
инструкции станет совсем иной, и управление получит отнюдь не printf, а не имеющий к ней никакого отношения код! Вероятнее всего им окажется "мусор", порождающий исключение с последующим аварийным закрытием приложения.
Программируя на ассемблере, такое ограничение можно легко обойти, используя регистровую адресацию. Перемещаемый вызов функции printf упрощенно может выглядеть, например, так:"lea eax, printf\ncall eax." В регистр eax (или любой другой регистр общего назначения) заносится абсолютный линейный, а не относительный адрес и, независимо от положения инструкции call, управление будет передано функции printf, а не чему-то еще.
Однако такой подход требует значения ассемблера, поддержки компилятором ассемблерных вставок, и не очень-то нравится прикладным программистам, не интересующихся командами и устройством микропроцессора.
Для решения данной задачи исключительно средствами языка высокого уровня, - необходимо передать стековой функции указатели на вызываемые ее функции как аргументы. Это несколько неудобно, но более короткого пути, по-видимому, не существует. Простейшая программа, иллюстрирующая копирование и выполнение функций в стеке, приведена в листинге 2.
void Demo(int (*_printf) (const char *,...) )
{
_printf("Hello, Word!\n");
return;
}
int main(int argc, char* argv[])
{
char buff[1000];
int (*_printf) (const char *,...);
int (*_main) (int, char **);
void (*_Demo) (int (*) (const char *,...));
_printf=printf;
int func_len = (unsigned int) _main - (unsigned int) _Demo;
for (int a=0;a<func_len;a++)
buff[a]= ((char *) _Demo)[a];
_Demo = (void (*) (int (*) (const char *,...))) &buff[0];
_Demo(_printf);
return
0;
}
Листинг 228 Программа, иллюстрирующая копирование и выполнение функции в стеке
Три ключа были необходимы для отказа от явной проверки значения аргументов, которую легко обнаружить анализирующему лицу. Например, пусть событие KEY (key_code) генерируется при каждом нажатии на клавиатуру. Тогда обработчик, считывающий входную информацию, должен привязываться только к коду события (KEY) и получать введенный символ в виде аргумента.
Если одна из клавиш (или комбинаций клавиш) зарезервирована для специальной цели (например, задействует некоторые дополнительные функции в программе), то ее обработчик может привязываться одновременно к коду события (KEY) и коду клавиши (key_code), не опасаясь за свое раскрытие, т.к. правильный ключ дает лишь единственная комбинация KEY и key_code, а явная проверка на соответствие нажатого символа секретному коду отсутствует.
Привязка к аргументам позволяет отлавливать искомые последовательности в потоке данных независимо от того, каким образом они получены. Например, процедура аутентификации, ожидающая пароля "MyGoodPassword", не интересуется - введен ли он с клавиатуры, получен ли с удаленного терминала, загружен ли из файла и т.д.
Такой подход значительно упрощает программирование и уменьшает зависимость одних модулей от других. Программа представляет собой совокупность обработчиков, автоматически коммутируемых возникающими событиями. Никакого детерминизма! Это чем-то напоминает взаимодействие биологической клетки с окружающей средой и в скором будущем может стать довольно перспективным направлением.
The only secure computer is
" The only secure computer is one that's unplugged, locked in a safe, and buried 20 feets under the ground in a secret location... and I'm not even too sure about that one…"
Дэннис Хьюжз (Dennis Huges),
ФБР США
Эпиграф выбран неслучайно. Информационная безопасность сегодня представляет одну из весьма “горячих” тем. Ее актуальность весьма велика, и каждое пособие связанное с этой темой подвергается анализу со стороны обычно весьма скептически настроенных специалистов. Исследование программ связано с вопросами информационной безопасности напрямую. Когда автор этой книги пригласил меня, как специалиста, стать ее научным редактором, я отнесся к этой затее с большим интересом.
Сама мысль о возможности опубликования подобных материалов допускает для многих некоторую крамолу, как некогда было, к примеру, с криптографией и некоторыми областями теории чисел. Более того, тематика данной книги до некоторого времени расценивалась как близкая к широко обсуждаемым криминальным темам и лишь в последнее время вернулась в свое естественное научное русло.
На мой взгляд, эта книга будет интересна весьма широкому кругу читателей. Наверняка ею заинтересуются и те, кто лишь начинает свой восход к Олимпу знаний, и уже “матерые” специалисты в области программирования и исследования программ (или на иностранный манер “reverse engineering”). Хочется особенно отметить, что материалы книги устроены таким образом, что будут полезны и обычному программисту (как пособие по оптимизации программ для современных интеллектуальных компиляторов), и специалистам различных направлений (например, специалистам информационной защиты ? в качестве пособия по поиску так называемых “закладок”). Стиль изложения “от простого к сложному” позволяет говорить также и о том, что данная книга послужит также и учебным пособием для начинающих исследователей и “кодокопателей”.
Книга содержит бесценное количество уникального по своей сути практического материала. Множество поверхностных работ за рубежом представляет очень мало практического интереса для тех, кто с интересом изучает прикладную математику, программирование и устройство компьютеров. Да и среди публикаций современного российского научного сообщества читатель не найдет лучшего пособия по изучению техники исследования программ.
Однако, я все-таки рекомендую читателю подвергнуть сомнению все вышесказанное и убедиться во всем самостоятельно, прочитав данную книгу.
С уважением,
Хади Р.А.
Приглашение к дискуссии или новые приемы защиты
"многочисленные критические нападки -- неизбежный удел всякой новой концепции"
Ганс Селье. "От мечты к открытию"
В заключении книги мне хотелось бы поделиться собственным опытом создания защит, сломать которые принципиально невозможно. Точнее, их взлом потребовал бы многих тысяч, а то и миллионов лет на типичном бытовом компьютере (во всяком случае, очень хочется на это надеяться).
Гарантированно воспрепятствовать анализу кода позволяет только шифрование программы. Но сам процессор не может непосредственно исполнять зашифрованный код, поэтому перед передачей управления его необходимо расшифровать. Если ключ содержится внутри программы, стойкость такой защиты близка к нулю. Все, чего может добиться разработчик, - затруднить поиск и получение этого ключа, тем или иным способом препятствуя отладке и дизассемблированию программы.
Другое дело, если ключ содержится вне программы. Тогда стойкость защиты определяется стойкостью используемого криптоалгоритма (при условии, что ключ перехватить невозможно). В настоящее время опубликованы и детально описаны многие криптостойкие шифры, взлом которых заведомо недоступен рядовым злоумышленникам.
В общих чертах идея защиты заключается в описании алгоритма с помощью некой математической модели, одновременно с этим используемой для генерации ключа. Разные ветви программы зашифрованы различными ключами, и чтобы вычислить этот ключ, необходимо знать состояние модели на момент передачи управления соответствующей ветви программы. Код динамически расшифровывается в процессе его выполнения, а чтобы расшифровать его целиком, нужно последовательно перебрать все возможные состояния модели. Если их число будет очень велико (чего нетрудно добиться), восстановить весь код станет практически невозможно!
Для реализации этой идеи автором был создан специальный событийно-ориентированный язык программирования. События в нем представляют собой единственное средство вызова подпрограммы. Каждое событие имеет свой код и один (или несколько) аргументов.
Событие может иметь какое угодно количество обработчиков, а может не иметь ни одного (в этом случае вызываемому коду возвращается ошибка).
На основе кода события и значения аргументов менеджер событий генерирует три ключа - первый только на основе кода события, второй - только на основе аргументов, и третий на основе кода и аргументов (см. пояснение 1). Затем он пытается полученными ключами последовательно расшифровать всех обработчиков событий. Если расшифровка происходит успешно, это означает, что данный обработчик готов обработать данное событие, и тогда ему передается управление.
Алгоритм шифрования должен быть выбран так, чтобы обратная операция была невозможна. При этом установить, какое событие данный обработчик обрабатывает, можно только полным перебором. Для блокирования возможности перебора в язык была введена контекстная зависимость - генерация дополнительной серии ключей, учитывающих некоторое количество предыдущих событий. Это позволило устанавливать обработчики на любые последовательности действий пользователя, например, на открытие файла с именем "Мой файл", запись в него строки "Моя строка" и переименование его в "Не мой файл".
Очевидно, что перебор комбинаций всех событий со всеми возможными аргументами займет бесконечное время и принципиально невозможен. Восстановить исходный код программы, защищенной таким образом, удастся не раньше, чем все ее ветви хотя бы однократно получат управление. Но частота вызова различных ветвей не одинакова, и у некоторых из них очень мала. Например, можно установить на слово "сосна", введенное в текстовом редакторе, свой обработчик, выполняющий некоторые дополнительные проверки на целостность кода программы или на лицензионную чистоту используемого ПО.
Взломщик не сможет быстро выяснить - до конца ли взломана программа или нет. Ему придется провести тщательное и кропотливое тестирование, но даже после этого он не будет в этом уверен!
Таким же точно образом осуществляется ограничение срока службы демонстрационных версий.
Разумеется, обращаться к часам реального времени бесполезно, их очень легко перевести назад, вводя защиту в заблуждение. Гораздо надежнее опираться на даты открываемых файлов - даже если часы переведены, созданные другими пользователями файлы в большинстве случаев имеют правильное время. Но взломщик не сможет узнать ни алгоритм определения даты, ни саму дату окончания использования продукта! Впрочем, дату в принципе можно найти и полным перебором, но что это дает? Модификации кода воспрепятствовать очень легко - достаточно, чтобы длина зашифрованного текста была чувствительна к любым изменениям исходного. В этом случае взломщик не сможет подправить "нужный" байт в защитном обработчике и вновь зашифровать его. Придется расшифровывать и вносить изменения во все остальные обработчики (при условии, что они контролируют смещение, по которому расположены), а это невозможно, т.к. соответствующие им ключи заранее неизвестны.
Существенными недостатками предлагаемого решения являются низкая производительность и высокая сложность реализации. Если со сложностью реализации можно смириться, то производительность налагает серьезные ограничения на сферу его применения. Впрочем, можно значительно оптимизировать алгоритм или оставить все критичные к быстродействию модули незашифрованными (или расшифровывать каждый обработчик только один раз), словом, дорогу осилит идущий! Интересно другое - действительно ли эта технология позволяет создавать принципиально неизучаемые приложения или в приведенные рассуждения вкралась ошибка? Было бы очень интересно выслушать мнения коллег, специализирующихся на защите информации.
Самомодифицирующийся код как средство защиты приложений
И вот после стольких мытарств и ухищрений злополучный пример запущен и победно выводит на экран "Hello, World!". Резонный вопрос – а зачем, собственно, все это нужно? Какая выгода оттого, что функция будет исполнена в стеке? Ответ:– код функции, исполняющееся в стеке, можно прямо "на лету" изменять, например, расшифровывать ее.
Шифрованный код чрезвычайно затрудняет дизассемблирование и усиливает стойкость защиты, а какой разработчик не хочет уберечь свою программу от хакеров? Разумеется, одна лишь шифровка кода – не очень-то серьезное препятствие для взломщика, снабженного отладчиком или продвинутым дизассемблером, наподобие IDA Pro, но антиотладочные приемы (а они существуют и притом в изобилии) – тема отдельного разговора, выходящего за рамки настоящей статьи.
Простейший алгоритм шифрования заключается в последовательной обработке каждого элемента исходного текста операцией "ИЛИ-исключающее-И" (XOR). Повторное применение XOR к шифротексту позволяет вновь получить исходный текст.
Следующий пример (см. листинг 3) читает содержимое функции Demo, зашифровывает его и записывает полученный результат в файл.
void _bild()
{
FILE *f;
char buff[1000];
void (*_Demo) (int (*) (const char *,...));
void (*_Bild) ();
_Demo=Demo;
_Bild=_bild;
int func_len = (unsigned int) _Bild - (unsigned int) _Demo;
f=fopen("Demo32.bin","wb");
for (int a=0;a<func_len;a++)
fputc(((int) buff[a]) ^ 0x77,f);
fclose(f);
}
Листинг 229 Шифрование функции Demo
Теперь из исходного текста программы функцию Demo
можно удалить, взамен этого, разместив ее зашифрованное содержимое в строковой переменной (впрочем, не обязательно именно строковой). В нужный момент оно может быть расшифровано, скопировано в локальный буфер и вызвано для выполнения. Один из вариантов реализации приведен в листинге 4.
Обратите внимание, как функция printf
в листинге 2 выводит приветствие на экран. На первый взгляд ничего необычного, но, задумайтесь, где
размещена строка "Hello, World!". Разумеется, не в сегменте кода – там ей не место ( хотя некоторые компиляторы фирмы Borland помещают ее именно туда). Выходит, в сегменте данных, там, где ей и положено быть? Но если так, то одного лишь копирования тела функции окажется явно недостаточно – придется скопировать и саму строковую константу. А это – утомительно. Но существует и другой способ – создать локальный буфер и инициализировать его по ходу выполнения программы, например, так: …buf[666]; buff[0]='H'; buff[1]='e'; buff[2]='l'; buff[3]='l';buff[4]='o',… - не самый короткий, но, ввиду своей простоты, широко распространенный путь.
int main(int argc, char* argv[])
{
char buff[1000];
int (*_printf) (const char *,...);
void (*_Demo) (int (*) (const char *,...));
char code[]="\x22\xFC\x9B\xF4\x9B\x67\xB1\x32\x87\
\x3F\xB1\x32\x86\x12\xB1\x32\x85\x1B\xB1\
\x32\x84\x1B\xB1\x32\x83\x18\xB1\x32\x82\
\x5B\xB1\x32\x81\x57\xB1\x32\x80\x20\xB1\
\x32\x8F\x18\xB1\x32\x8E\x05\xB1\x32\x8D\
\x1B\xB1\x32\x8C\x13\xB1\x32\x8B\x56\xB1\
\x32\x8A\x7D\xB1\x32\x89\x77\xFA\x32\x87\
\x27\x88\x22\x7F\xF4\xB3\x73\xFC\x92\x2A\
\xB4";
_printf=printf;
int code_size=strlen(&code[0]);
strcpy(&buff[0],&code[0]);
for (int a=0;a<code_size;a++)
buff[a] = buff[a] ^ 0x77;
_Demo = (void (*) (int (*) (const char *,...))) &buff[0];
_Demo(_printf);
return
0;
}
Листинг 230 Зашифрованная программа
Теперь (см. листинг 4) даже при наличии исходных текстов алгоритм работы функции Demo будет представлять загадку! Этим обстоятельством можно воспользоваться для сокрытия некоторой критической информации, например, процедуры генерации ключа или проверки серийного номера.
Проверку серийного номера желательно организовать так, чтобы даже после расшифровки кода, ее алгоритм представлял бы головоломку для хакера. Один из примеров такого алгоритма предложен ниже.
Суть его заключается в том, что инструкция, отвечающая за преобразование бит, динамически изменяется в ходе выполнения программы, а вместе с нею, соответственно, изменяется и сам результат вычислений.
Поскольку при создании самомодифицирующегося кода требуется точно знать в какой ячейке памяти какой байт расположен, приходится отказываться от языков высокого уровня и прибегать к ассемблеру.
С этим связана одна проблема – чтобы модифицировать такой-то байт, инструкции mov требуется передать его абсолютный линейный адрес, а он, как было показано выше, заранее неизвестен. Однако его можно узнать непосредственно в ходе выполнения программы. Наибольшую популярность получила конструкция "CALL $+5\POP reg\mov [reg+relative_addres], xx" – т.е. вызова следующей инструкцией call
команды и извлечению из стека адреса возврата – абсолютного адреса этой команды, который в дальнейшем используется в качестве базы для адресации кода стековой функции. Вот, пожалуй, и все премудрости.
MyFunc:
push esi ; сохранение регистра esi
в стеке
mov esi, [esp+8] ; ESI = &username[0]
push ebx ; сохранение прочих регистров в стеке
push ecx
push edx
xor eax, eax ; обнуление рабочих регистров
xor edx, edx
RepeatString: ; цикл обработки строки байт-за-байтом
lodsb ; читаем очередной байт в AL
test al, al ; ?достигнут конец строки
jz short Exit
; Значение счетчика для обработки одного байта строки.
; Значение счетчика следует выбирать так, чтобы с одной стороны все биты
; полностью перемешались, а с другой - была обеспечена четность (нечтность)
; преобразований операции xor
mov ecx, 21h
RepeatChar:
xor edx, eax
; циклически меняется с xor
на adc
ror eax, 3
rol edx, 5
call $+5 ; ebx = eip
pop ebx ; /
xor byte ptr [ebx-0Dh], 26h; Эта команда обеспечивает цикл.
; изменение инструкции xor
на adc
loop RepeatChar
jmp short RepeatString
Exit:
xchg eax, edx ; результат
работы (ser.num) в eax
pop edx ; восстановление регистров
pop ecx
pop ebx
pop esi
retn ; возврат из функции
Листинг 231 Процедура генерации серийного номера, предназначенная для выполнения в стеке
Приведенный алгоритм интересен тем, что повторный вызов функции с передачей тех же самых аргументов может возвращать либо той же самый, либо совершенно другой результат – если длина имени пользователя нечетна, то при выходе из функции XOR меняется на ADC с очевидными последствиями. Если же длина имени четна – ничего подобного не происходит.
Разумеется, стойкость предложенной защиты относительно невелика. Однако она может быть значительно усилена. На то существует масса хитрых приемов программирования – динамическая асинхронная расшифровка, подстановка результатов сравнения вместо коэффициентов в различных вычислениях, помещение критической части кода непосредственно в ключ и т.д.
Но назначение статьи состоит не в том, чтобы предложить готовую к употреблению защиту (да и, зачем? чтобы хакерам ее было бы легче изучать?), а доказать (и показать!) принципиальную возможность создания самомодифицирующегося кода под управлением Windows 95/Windows NT/Windows 2000. Как именно предоставленной возможностью можно воспользоваться – надлежит решать читателю.