Неявный самоконтроль как средство создания не ломаемых защит [Крис Касперски] (pdf) читать онлайн

-  Неявный самоконтроль как средство создания не ломаемых защит  175 Кб, 19с. скачать: (pdf) - (pdf+fbd)  читать: (полностью) - (постранично) - Крис Касперски

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Крис Касперски

íÍåÿâíûé ñàìîêîíòðîëü
êàê ñðåäñòâî ñîçäàíèÿ
íå ëîìàåìûõ çàùèò

kk@sendmail.ru

Крис Касперски
kk@sendmail.ru

Неявный самоконтроль как средство
создания не ломаемых защит

Введение
сновная ошибка подавляющего большинства разработчиков защитных
механизмов состоит в том, что они дают явно понять хакеру, что защита
еще не взломана. Если защита сообщает "неверный ключевой файл (пароль)", то
хакер ищет тот код, который ее выводит и анализирует условия, которые приводят
к передаче управления на данную ветку программы. Если защита в случае неудач
ной аутентификации блокирует некоторые элементы управления и/или пункты ме
ню, – хакер либо снимает такую блокировку в "лоб", либо устанавливает точки
останова (в просторечии называемые бряками) на APIфункции, посредством ко
торых такое блокирование может быть осуществлено (как правило это EnableWin
dows), после чего он опятьтаки оказывается в непосредственной близости от за
щитного механизма, который ничего не стоит проанализировать и взломать. Даже
если защита не выводит никаких ругательств на экран, а просто тихо "кончает",
молчаливо выходя из программы, то хакер либо ставит точку останова на функцию
exit, либо тупо трассирует программу и, дождавшись момента передачи управления
на exit, анализирует один или несколько последующих условных переходов в цепи
управления, – какойто из них непосредственно связан с защитой!
В некоторых защитных механизмах используется контроль целостности
программного кода на предмет выявления его изменений. Теперь, если хакер под
правит несколько байтиков в программе, защита немедленно обнаружит это
и взбунтуется. Святая простота! – воскликнет хакер, – и отключит самоконтроль
защиты, действуя тем же самым способом, что описан выше. По наблюдениям ав
тора, типичный самоконтроль выявляется и нейтрализуется за несколько минут.
Наиболее сильный алгоритм защиты: использовать контрольную сумму критиче
ских участков защитного механизма для динамической расшифровки некоторых
веток программы ломаются уже не за минуты, а за часы (в редчайших случаях –
дни). Алгоритм взлома выглядит приблизительно так: а) подсмотрев контрольную
сумму в оригинальной программе, хакер переписывает код функции CalculateCRC,
заставляя ее всегда возвращать это значение, не выполняя реальной проверки;
б) если защита осуществляет подсчет контрольной суммы различных участков

О

Неявный самоконтроль как средство создания не ломаемых защит
программы и/или разработчик использовал запутанный самомодифицирующийся
код, трудно предсказуемым способом меняющий свою контрольную сумму, то ха
кер может изменить защиту так, чтобы она автоматически само, восстанавливалась
после того, как все критические участки будут пройдены; в) отследив все вызовы
CalculateCRC, хакер может просто снять динамическую шифровку, расшифровав
ее вручную, после чего надобность в CalculateCRC пропадает.
Стоит отметить, что независимо от способа своей реализации любой само
контроль элементарно обнаруживается установкой точек останова на те участки за
щитного механизма, которые были изменены. Остальное – дело техники. Можно
сколь угодно усложнять алгоритм подсчета контрольной суммы, – напичкивать его
антиатладочными приемами, реализовать его на базе собственных виртуальных ма
шин (то есть интерпретаторов), некоторые из которых, такие, например, как
Стрелка Пирса, достаточно трудно проанализировать. Но… если такие меры
и остановят взломщика, то ненадолго.

Техника неявного контроля
шибка традиционного подхода заключается в его предсказуемости.
Любая явная проверка чего бы то ни было, независимо от ее алгорит
ма – это зацепка! Если хакер локализует защитный код, то все – пиши пропало.
Единственный надежный способ отвадить его от взлома – "размазать" защитный
код по всей программе с таким расчетом, чтобы нейтрализовать защиту без полно
го анализа всей программы целиком – было заведомо невозможным. К сожалению,
существующие методики "размазывания" либо многократно усложняют реализа
цию программы, либо крайне неэффективны. Некоторые программисты вставляют
в программу большое количество вызовов одной и той же защитной функции, иду
щих из различных мест, наивно полагая тем самым, что хакер будет искать и анали
зировать их все. Да как бы не так! Хакер ищет ту самую защитную функцию и пра
вит ее. К тому же, зная смещение вызываемой функции, найти, отследить ее вызо
вы можно без труда! Даже если встраивать защитную функцию непосредственно
в место ее вызова, – хакер сможет найти все такие места тупым поиском по сигна
туре. Пускай, оптимизирующие компиляторы, несколько меняют тело inlineфунк
ций с учетом контекста конкретного вызова – эти изменения не принципиальны.
Реализовать же несколько десятков различных защитных функций – слишком на
кладно, да и фантазии у разработчика не хватит и хакер, обнаружив и проанализи
ровав пару – тройку защитных функций, настолько проникнется "духом" и ходом
мысли разработчика, что все остальные найдет без труда.
Между тем существует и другая возможность – неявная проверка целостно
сти своего кода. Рассмотрим следующий алгоритм защиты: пусть у нас имеется за
шифрованная (а еще лучше упакованная) программа. Мы, предварительно скопи
ровав ее в стековый буфер, расшифровываем (распаковываем) ее и… используем
освободившийся буфер под локальные переменные защищенной программы.
С точки зрения хакера, анализирующего дизассемблерный код, равно как

О

Крис Касперски

3

Неявный самоконтроль как средство создания не ломаемых защит
и гуляющего по защите отладчиком, все выглядит типично и "законно". Обнару
жив защитный механизм (пусть для определенности это будет тривиальная па
рольная проверка), хакер правит соответствующий условный переход и с удовле
творением убеждается, что защита больше не ругается и программа работает.
Как будто бы работает! – через некоторое время выясняется, что после взлома ра
бота программы стала неустойчивой, – то она неожиданно виснет, то делает из чи
сел "винегрет", то… Почесав репу, хакер озадаченно думает: а как это вообще ло
мать? На что ставить точки останова? Ведь не анализировать же весь код целиком!
Весь фокус в том, что некоторые из ячеек буфера, ранее занятого зашифро
ванной (упакованной) программой при передаче их локальным переменным не
были проинициализированы! Точнее, они были проинициализированы теми зна
чениями, что находились в соответствующих ячейках оригинальной программы.
Как нетрудно догадаться, именно эти ячейки и хранили критичный к изменениям
защитный код, а потому и неявно контролируемый нашей программой. Теперь
я готов объяснить зачем вся эта котовасия с шифровкой (упаковкой) нам вообще
понадобилась: если бы мы просто скопировали часть кода программы в буфер,
а затем "наложили" на него наши локальные переменные, то хакер сразу бы заин
тересовался происходящим и, бормоча под нос "чтото здесь не так", вышел бы
непосредственно на след защиты. Расшифровка нам понадобилась лишь для усы
пления бдительности хакера. Вот он видит, что код программы копируется в бу
фер. Спрашивает себя "а зачем?" и сам же себе отвечает: "для расшифровки!".
Затем, дождавшись освобождения буфера с последующим затиранием его содер
жимого локальными переменными, хакер (даже проницательный!) теряет к этому
буферу всякий интерес. Далее – если хакер поставит контрольную точку останова
на модифицированный им защитный код, то он вообще не обнаружит к ней обра
щения, т. к. защита контролирует именно зашифрованный (упакованный) код,
содержащийся в нашем буфере. Даже если хакер поставит точку останова на
буфер, он быстро выяснит, что: а) ни до, ни в процессе, ни после расшифровки
(распаковки) программы содержимое модифицированных им ячеек не контроли
руется (что подтверждает анализ кода расшифровщика/распаковщика – проверок
целостности там действительно нет); б) обращение к точке останова происходит
лишь тогда, когда буфер затерт локальными переменными и (по идее!) содержит
другие данные.
Правда, ушлый хакер может обратить внимание, что после "затирания" зна
чение этих ячеек осталось неизменным. Совпадение? Проанализировав код, он
сможет убедиться, что они вообще не были инициализированы и тогда защита па
дет! Однако, мы можем усилить свои позиции: достаточно лишь добиться, чтобы
контролируемые байты попали в "дырки", образующиеся при выравнивании струк
туры (этим мы отвечает хакеру на вопрос: а чего это они не инициализированы?),
а затем скопировать эту структуру целиком (вместе с контролируемыми "дырка
ми"!) в десятокдругой буферов, живописно разбросанных по всей программе.
Следить за всеми окажется не такто просто: вопервых, не хватит контрольных то
чек, а, вовторых, это просто не придет в голову.

Крис Касперски

4

Неявный самоконтроль как средство создания не ломаемых защит

Рисунок 1.

Практическая реализация
равила хорошего тона обязывают нас проектировать защитные меха
низмы так, чтобы они никогда, ни при каких обстоятельствах не могли
нанести какой бы то ни было вред легальному пользователю. Даже если вам очень
очень хочется наказать хакера, ломающего вашу программу, форматировать диск
в случае обнаружения модификации защитного кода, категорически недопустимо!
Вопервых, это просто незаконно и попадает под статью о умышленном создании
деструктивных программ, а вовторых… задумайтесь, что произойдет, если искаже
ние файла произойдет в результате действий вируса или некоторого сбоя? Если вы
не хотите, чтобы пострадали невинные, вам придется отказаться от всех форм вре
да, в том числе и преднамеренном нарушении стабильности работы самой защи
щенной программы.

П

Крис Касперски

5

Неявный самоконтроль как средство создания не ломаемых защит
Стоп! Ведь выше мы говорили как раз об обратном. Единственный путь сде
лать защиту трудно ломаемой, – не выдавая никаких ругательных сообщений, по
которым нас можно засечь, молчаливо делать "винегрет" из обрабатываемых дан
ных. А теперь, выясняется, что делать этого по этическим (и юридическим!) сооб
ражением нельзя. На самом деле, если хорошо подумать, то все эти ограничения
легко обойти. Что нам мешает оснастить защиту явной проверкой целостности
своего кода? Хакер найдет и нейтрализует ее без труда, но это и не страшно, по
скольку истинная защита находится совершенно в другом месте, а вся эта бутафо
рия нужна лишь затем, чтобы предотвратить последствия непредумышленного ис
кажения кода программы и поставить пользователя в известность, что все данные
нами гарантии (как явные, так и предполагаемые) ввиду нарушения целостности
оригинального кода, аннулируются. Правда, при обсуждении защиты данного ти
па, некоторые коллеги мне резонно возразили, а что, если в результате случайного
сбоя окажутся изменены и контролируемые ячейки, и сама контрольная сумма?
Защита сработает у легального пользователя!!! Ну что мне на это ответить? Случай
но таких "волшебных" искажений просто не бывает, их вероятность настолько
близка к нулю, что… К тому же, в случае срабатывания защиты мы ведь не форма
тируем легальному пользователю диск, а просто нарушаем нормальную работу про
граммы. Путь и предумышленно, все равно, если в результате того или иного сбоя
был искажен исполняемый файл, то о корректности его работы более говорить не
приходится. Ну хорошо, если вы так боитесь сбоев, можно встроить в защиту хоть
десяток явных проверок, – трудно нам что ли?!
Ладно, оставим этические проблемы на откуп тем самым пользователям, ко
торые приобретают титул "лицензионных" исключительно через крак, и перейдем
к чисто конкретным вещам. Простейший пример реализации данной защиты приве
ден в листинге 1. Для упрощения понимания и абстрагирования от всех технических
деталей, здесь используется простейшая схема аутентификации, "ломать" которую
совершенно необязательно: достаточно лишь подсмотреть оригинальный пароль,
хранящийся в защищенном файле прямым текстом. Для демонстрационного приме
ра такой прием с некоторой натяжкой допустим, но в реальной жизни, вам следует
быть более изощренными. По крайней мере следует добиться того, чтобы ваша защи
та не ломалась изменением одного единственного байта, поскольку в этом случае да
же неявный контроль будет легко выявить. Следует так же отметить, что контроли
ровать все критические байты защиты – не оченьто хорошая идея, т. к. хакер сможет
это легко обнаружить. Если защита требует для своего снятия хотя бы десяти моди
фикаций в различных местах, три из которых контролируются, то с вероятностью
~70% факт контроля не будет обнаружен. Действительно, среднестатистический ха
кер следить за всеми модифицированными им байтами просто не будет. Вместо это
го он, в надежде что тупая защита контролирует целостность своего кода целиком,
будет следить за обращениями к одной, ну максимум двумтрем, измененным им
ячейкам и… с удивлением обнаружит, что защита их вообще не контролирует.
После того, как контрольные точки выбраны, вы должны определить их сме
щение в откомпилированном файле. К сожалению, языки высокого уровня не по
зволяют определять адреса отдельных машинных инструкций и, если только вы не
пишите защиту на ассемблерных вставках, то у вас остается одинединственный
путь – воспользоваться каким ни будь дизассемблером (например, IDA).
Крис Касперски

6

Неявный самоконтроль как средство создания не ломаемых защит
Допустим, критическая часть защиты выглядит так и нам необходимо про
контролировать целостность условного оператора if, выделенного жирным синим
шрифтом:
int my_func()
{
if (check_user())
{
fprintf(stderr, "passwd ok\n");
}
else
{
fprintf(stderr, "wrong passwd\n");
exit(1);
}
return 0;
}

Загрузив откомпилированный файл в дизассемблер, мы получим следую
щий код (чтобы быстро узнать которая из всех процедур и есть my_func, опирайтесь
на тот факт, что большинство компиляторов располагает функции в памяти в по
рядке их объявления, т. е. my_func будет вторая по счету функция):
.text:00401060
.text:00401060
.text:00401065
.text:00401067
.text:00401069
.text:0040106E
.text:00401073
.text:00401078
.text:0040107B
.text:0040107D
.text:0040107E
.text:0040107E
.text:0040107E
.text:0040107E
.text:00401083
.text:00401088
.text:0040108D
.text:0040108F
.text:0040108F

sub_401060

proc near
; CODE XREF: sub_4010A0+AFvp
call
sub_401000
test
eax, eax
jz
short loc_40107E
push
offset aPasswdOk
; "passwd ok\n"
push
offset unk_407110
call
_fprintf
add
esp, 8
xor
eax, eax
retn
; 

loc_40107E:

sub_401060

push
push
call
push
call
endp

; CODE XREF: sub_401060+7^j
offset aWrongPasswd ; "wrong passwd\n"
offset unk_407110
_fprintf
0FFFFFFFFh
; int
_exit

Как нетрудно сообразить, условный переход, расположенный по адресу
0x401067 и есть тот самый "if", который нам нужен. Однако, это не весь if, а только
малая чего часть. Хакер может и не трогать условного перехода, а заменить инструк
цию test eax, eax на любую другую инструкцию, сбрасывающую флаг нуля. Так же
он может модифицировать защитную функцию sub_401000, которая и осуществля
ет проверку пароля. Словом, тут много разных вариантов и на этом несчастном
условном переходе свет клином не сошелся, а потому для надежного распознавания
взлома нам потребуются дополнительные проверки. Впрочем, это уже детали. Глав
ное, что мы определили смещение контролируемого байта. Кстати, а почему имен
но байта?! Ведь мы можем контролировать хоть целое двойное слово, расположен
ное по данному смещению! Особого смысла в этом нет, просто так проще.
Крис Касперски

7

Неявный самоконтроль как средство создания не ломаемых защит
Чтобы не работать с непосредственными смещениями (это неудобно и вооб
ще некрасиво), давайте загоним их в специально на то предназначенную структуру
следующего вида:
union anti_hack
{
// буфер, содержащий оригинальный код программы
char buf[MAX_CODE_SIZE];
// локальные переменные программы
struct local_var
{
int
local_var_1;
int
local_var_2;
};
// неявно контролируемые переменные программы
struct code_control
{
char
gag_1[OFFSET_1];
int
x_val_1;
char
gag_2[OFFSET_2  OFFSET_1  sizeof(int)];
int
x_val_2;
};
};

Массив buf – это тот самый буфер, в который загружается оригинальный
код программы для его последующей расшифровки (распаковки). Поверх массива
накладываются две структуры: local_val, содержащая в себе локальные перемен
ные, которые в процессе своей инициализации затирают соответствующие им
ячейки buf'a и тем самым создают впечатление, что прежнее содержимое буфера
стало теперь ненужным и более уже не используется. Количество локальных пере
менных может быть любым, главное – следить за тем, чтобы они не перекрывали
контрольные точки программы, изменять содержимое которых нельзя. В приве
денном выше примере, по соображениям наглядности, контрольные точки выне
сены в отдельную структуру code_control, два массива которой gag_1 и gag_2 ис
пользуются лишь для того, чтобы переменные x_val_1 и x_val_2 были размещены
компилятором по необходимым нам адресам. Как нетрудно долгаться: константа
OFFSET_1 задает смещение первой контрольной точки, а OFFSET_2 – второй.
Достоинство такой схемы заключается в том, что при добавлении или удалении
локальных переменных в структуру local_var, структура code_contorl остается не
изменной. Напротив, если объединить локальные переменные и контрольные
точки одной общей крышей, то размеры массивов gag_1 и gag_2 станут зависеть от
количества и размера используемых нами локальных переменных:
union anti_hack
{
char buf[MAX_CODE_SIZE];
struct code_control
{
int
local_var_1;
int
local_var_2;

Крис Касперски

8

Неявный самоконтроль как средство создания не ломаемых защит
char
int
char
int

gag_1[OFFSET_1sizeof(int)*2];
x_val_1;
gag_2[OFFSET_2  OFFSET_1  sizeof(int)];
x_val_2;

};
};

Код, выделенный жирным шрифтом, как раз и отвечает за то, чтобы размер
массивапустышки gag_1, компенсировал пространство, занятое локальными пе
ременными. Такая ручная "синхронизация" крайне ненадежна и служит источни
ком потенциальных ошибок. С другой стороны, теперь мы можем не беспокоить
ся, что локальные переменные случайно затрут контрольные точки, т. к. если такое
произойдет, длина массива gag_1 станет отрицательной и компилятор тут же выска
жет нам все, что он о нас думает. Поэтому, окончательный выбор используемой
конструкции остается за вами.
Теперь – пару слов о расшифровке (распаковке) нашей программы. Вопер
вых, нет нужды расшифровывать всю программу целиком, – достаточно расши
фровать лишь сам защитный механизм, а то и его критическую часть. Причем, са
ма процедура расшифровки должна быть написано максимально просто и незамы
словато. Поверьте, лишние уровни защиты здесь совершенно ни к чему. Хакер все
равно их вскроет за очень короткое время, и, самое главное, чем круче окажется за
щита, тем внимательнее будет вести себя хакер. Мы же, напротив, должны убедить
его, что шифровка это – так, защита от детишек, и "настоящая" защита спрятана
гдето совсем в другом месте (пусть ищет то, чего нет!).
Правда, тут есть одна проблема. По умолчанию Windows запрещает модифи
кацию кодовой секции PEфайла и потому непосредственная расшифровка кода
невозможна! Первая же попытка записи ячейки, принадлежащей секции .text, вы
зовет аварийное завершение программы. Можно, конечно, обхитрить Windows,
создав свою собственную секцию, разрешающую операции чтения, исполнения
и записи одновременно, или, как еще более изощренный вариант, исполнять рас
шифрованный код непосредственно в стеке, однако, мы пойдем другим путем
и просто отключим защиту кодового сегмента от его непредумышленной модифи
кации. Достоинство этого приема в том, что он очень просто реализуется, а недо
статок – ослабление контроля за поведением программы. Если в результате тех или
иных ошибок, наша программа пойдет в разнос и начнет затирать свой собствен
ный код, операционная система будет бессильна ее остановить, поскольку мы сами
отключили защиту! С другой стороны, в тщательно протестированной программе
вероятность возникновения подобной ситуации исчезающе мала и в ряде случаев
ею можно смело пренебречь. Во всяком случае, в примере, приведенном ниже, мы
поступим именно так (речь ведь все равно идет не о технике расшифровке, а о не
явном контроле за модификацией кода).
Остается лишь обмолвится парой слов о способах определения диапазона
адресов, принадлежащих защитному коду. Поскольку, большинство компиляторов
размешают функции в памяти в порядке их объявления в программе, адрес начала
защитного кода совпадает с адресом первой, относящейся к нему функции, а адрес
конца равен адресу первой не принадлежащей к нему функции (т. е. первой функ
ции, расположенной на его "хвостом").
Крис Касперски

9

Неявный самоконтроль как средство создания не ломаемых защит
Теперь, разобравшись с расшифровкой, переходим к самому интересному –
неявному контролю за критическими точками нашего защитного механизма. Пусть
у нас имеется контрольная точка x_val_1, содержащая значение x_original_1, тогда
для его неявной проверки можно "обвязать" некоторые вычислительные выраже
ния следующим кодом: some_var = some_var + (x_val_1 – x_original_1). Если кон
трольная ячейка x_val_1 действительно содержит свое эталонное значение x_origi
nal_1, то разность двух этих чисел равна нулю, а добавление нуля к чему бы то ни
было, никак не изменяет его значения. Грубо говоря, x_val_1 уравновешивается
противоположным ему по знаку x_origial_1 и за это данный алгоритм называют "ал
горитмом коромысла" или "алгоритмом весов". Можно ли быстро обнаружить та
кие "весы" беглым просмотром листинга программы? Не спешите отвечать "нет",
поскольку правильный ответ – "да". Давайте рассуждать не как разработчики за
щитного механизма, а как хакеры: вот в процессе взлома мы изменили такието
и такието ячейки программы, после чего она отказала в работе. Существует два
"тупых" способа контроля своей целостности: контроль по адресам и контроль по
содержимому. Для выявления первого хакер просто ищет адрес "хакнутой" им ячей
ки в коде программы. Если его нет (а в данном случае его и нет!), он предпринима
ет попытку обнаружить ее содержимое! А вот содержимое контролируемой ячейки
в точности равно x_original_1 и тривиальный контекстный поиск за доли секунды
выявит все вхождения! Чтобы этого не произошло и наша защита так просто не сда
лась, следует либо уменьшить протяженность контролируемых точек до байта
(байт – слишком короткая сигнатура для контекстного поиска), либо не хранить
x_original_1 в прямом виде, а получать его на основе некоторых математических
вычислений. Только не забываете, что оптимизирующие компиляторы все кон
стантные вычисления выполняют еще на стадии компиляции и: (#define x_orgi
nal_1 0xBBBBBA; some_var += (x_val_1 – 1 – x_original_1);) на самом деле не уси
лит защиту! Поэтому, лучше вообще отказаться от алгоритма "весов", тем более, что
он элементарно "вырезается" в случае его обнаружения. Надежнее изначально
спроектировать алгоритм программы так, чтобы она осмысленно использовала
x_original, а не уравновешивала его "противовесом". Приведенный ниже пример
умышленно ослаблен в целях демонстрации как можно использовать эту уязви
мость для облегчения взлома.

Исходный текст
#include
#define
#define
#define
#define
#define

PASSWD
"+++"
MAX_LEN
1023
MAX_CODE_SIZE (0x10*1024)
OFFSET_1
0x42
OFFSET_2
0x67

#define x_original_1
#define x_original_2

Крис Касперски

0xc01b0574
0x44681574

10

Неявный самоконтроль как средство создания не ломаемых защит
#define x_original_all 0x13D4C04B
#define x_crypt

0x66

int check_user()
{
char passwd[MAX_LEN];
fprintf(stderr,"enter password:");
fgets(passwd, MAX_LEN, stdin);
return ~strcmp(passwd, PASSWD);
}

int my_func()
{
if (check_user())
{
fprintf(stderr, "passwd ok\n");
}
else
{
fprintf(stderr, "wrong passwd\n");
exit(1);
}
return 0;
}
main()
{
int a, b = 0;
#pragma pack(1)
union anti_hack
{
char buf[MAX_CODE_SIZE];
struct code_control
{
int
local_var_1;
int
local_var_2;
char
gag_1[OFFSET_1sizeof(int)*2];
int
x_val_1;
char
gag_2[OFFSET_2  OFFSET_1  sizeof(int)];
int
x_val_2;
};
};
union anti_hack ZZZ;
// TITLE
fprintf(stderr, "crackeme.0xh by Kris Kaspersky\n");
// расшифровка кода
// =======================================================================
// копируем расшифровываемый код в буфер
memcpy(&ZZZ, &check_user, (int) &main  (int) &check_user);

Крис Касперски

11

Неявный самоконтроль как средство создания не ломаемых защит
// расшифровываем в буфере
for (a = 0; a < (int) &main  (int) &check_user;
{
(*(char *) ((int) &ZZZ + a)) ^= x_crypt;
}

a++)

// копируем обратно
memcpy(&check_user, &ZZZ, (int) &main  (int) &check_user);

// явная проверка изменения кода
// =======================================================================
for (a = 0; a < (int) &main  (int) &check_user; a++)
{
b += *(int *) ((int) &check_user + a);
}
if (b != x_original_all)
{
fprintf(stderr, "ERR: invalid CRC (%x) hello, hacker\n", b);
return 0;
}
// явная проверка "валидности" пользователя
// =======================================================================
my_func();
// нормальное выполнение программы
// =======================================================================
// скрытый контроль
ZZZ.local_var_1 = 2;
ZZZ.local_var_2 = 2;x_original_2;
sprintf(ZZZ.gag_1, "%d * %d = %d\n", ZZZ.local_var_1,
ZZZ.local_var_2,
ZZZ.local_var_1*ZZZ.local_var_2+((x_original_1^ZZZ.x_val_1)+(x_original_2^ZZZ.x_val_2)));
printf("DEBUG: %x %x\n", ZZZ.x_val_1, ZZZ.x_val_2);
fprintf(stderr, "%s",ZZZ.gag_1);
}

Как это ломают?
сли все сделано правильно, то полученный исполняемый файл не ру
шиться при его запуске, а победоносно выводит на экран: "crackeme.0xh
by Kris Kaspersky\nenter password:" и ждет ввода пароля. Договоримся не обращать
внимание на пароль, прямым текстом хранящийся в программе и попробуем взло
мать защиту другим, более универсальным путем, а именно: изучением алгоритма
ее работы под дизассемблером. Запускаем нашу любимую ИДУ и, дождавшись

Е

Крис Касперски

12

Неявный самоконтроль как средство создания не ломаемых защит
окончания процесса дизассемблирования, смотрим что у нас там. Ага, текстовые
строки "passwd ok" и "wrong passwd" в сегменте данных действительно есть, но вот
перекрестных ссылок, ведущих к коду, выводящему их, чтото не видно. Странно,
ну да лиха беда начало! Запускам любой отладчик (например, WDB) и устанавлива
ем на адрес строки "wrong passwd" точку останова: "BA r4 407054". Даем команду
"GO" для продолжения выполнения программы, вводим любой, пришедший нам
на ум пароль, и… отладчик тут же всплывает, показывая адрес машинной команды,
обращающейся к первому символу строки. Но что нам это дает? Ведь мы, судя по
всему, находится в теле библиотечной функции out, осуществляющей вывод на
консоль и в ее коде для нас нет ничего интересного. С другой стороны – эту функ
цию ктото вызывает! Кто именно? Ну мало ли! Функция printf к примеру, код ко
торой для нас ничуть не более интересен… Конечно, поднимаясь по цепочке вызо
вов вверх (окно call stack вам в помощь!), мы рано или поздно достигнем защитно
го кода, вызвавшего эту функцию, но вот как нам быстро определить где защитный
код, а где библиотечные функции? Да очень просто! Та функция, один из аргумен
тов которой представляет собой непосредственное смещение нашей строки, оче
видно, и есть функция защитного кода! Последовательно щелкая мышкой по адре
сам возврата, перечисленных в окне "call stack", мы наконец находим:
0040106E
00401073
00401078
0040107D

6854704000
6810714000
E88A010000
6AFF

push
push
call
push

407054h
407110h
00401207
0FFh

Смещение, выделенное жирным шрифтом, – есть ни что иное, как смеще
ние искомой строки, соответственно, адрес 0x40106E (так же выделенный жирным
шрифтом) лежит гдето в гуще защитного кода. А нука, глянем сюда дизассембле
ром – чего это вдруг ИДА не создала перекрестную ссылку к строке?
.text:00401000 dword_401000
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401050 dword_401050
.text:00401050
.text:00401050
.text:00401050
.text:00401050

dd 62668AE7h, 31306666h, 2616560Eh, 17760E66h, 968E6626h
; DATA XREF: sub_401090+23vo
; sub_401090+28vo ...
dd 0E666667h, 662616B6h, 724222EBh, 6665990Eh, 0E38E3666h
dd 0E5666667h, 26D972A2h, 0EB662616h, 0DF6E4212h, 66666663h
dd 0C095B455h, 6939A4EDh, 0E738A6F2h, 666266A2h, 0F6F6A566h
dd 9999CD8Eh, 12A6E399h, 162E0E73h, 760E6626h, 8E662617h
; CODE XREF: sub_401090+AFvp
dd 666667F9h, 556EA2E5h, 320EA5A6h, 0E662616h, 66261776h
dd 6667EC8Eh, 8E990C66h, 666664FDh, 556AA2E5h, 0F6F6A5A6h
dd 0F6F6F6F6h

Вот это номер! Она вообще не посчитала это за код, объявив его массивом!
Хорошо, заставим ее дизассемблировать этот фрагмент вручную. Переместив кур
сор к самому началу массива, нажимаем для его удаления, а затем для
превращения байтовой цепочки в код.
text:00401000
text:00401000
text:00401000
text:00401002
text:00401006
text:00401008
text:00401008 loc_401008:

Крис Касперски

out

8Ah, eax

bound
xor

sp, [esi+66h]
[ecx], dh

; sub_401090+28vo ...
; DMA page register 74LS612:
; Channel 7 (address bits 1723)

; CODE XREF: .text:0040102Dvj

13

Неявный самоконтроль как средство создания не ломаемых защит
text:00401008
text:00401009
text:0040100A
text:0040100B
text:0040100B
text:0040100E
text:00401010
text:00401010
text:00401018
text:0040101A
text:0040101A

push
push
push
db
push
jbe
db
mov
mov
db
jmp

cs
esi
ss
26h, 66h
cs
short loc_401027
66h
ss, es:[esi+0E666667h]
dh, 16h
26h, 66h
short small near ptr unk_401040

Хм! Что за ерунда у нас получается?! Вновь переключившись на отладчик,
мы убеждаемся, что тот же самый код в нем выглядит вполне нормально:
00401000
00401006
00401007
00401008
0040100D
00401012

81EC00040000
56
57
6830704000
6810714000
E8F0010000

sub
push
push
push
push
call

esp,400h
esi
edi
407030h
407110h
00401207

Такое впечатление, что защитный механизм зашифрован… А почему бы в са
мом деле и нет? Возвращаясь к дизассемблеру, щелкаем по перекрестной ссылке
и видим:
.text:004010AE
.text:004010AE
.text:004010AE
.text:004010AE
.text:004010AE
.text:004010AE
.text:004010B3
.text:004010B3
.text:004010B3
.text:004010B8
.text:004010B8
.text:004010B8
.text:004010BD
.text:004010BD
.text:004010BD
.text:004010C1
.text:004010C3
.text:004010C6
.text:004010C8
.text:004010CB
.text:004010CD
.text:004010CF
.text:004010D2
.text:004010D2
.text:004010D2
.text:004010D4
.text:004010D6
.text:004010D8
.text:004010DA
.text:004010DA
.text:004010DA

Крис Касперски

mov
eax, offset sub_401090
загружаем в регистр EAX непосредственное смещение процедуры sub_401090,
чем и выдаем наш бесхитростный расшифровщик с головой если бы целевой
адрес вычислялся бы на основе некоторых математических операций, то вы
явить расшифровщик было бы сложнее (но по аппаратным контрольным точкам –
все равно возможно)
mov
esi, offset loc_401000
; загружаем в регистр esi непосредственное смещение процедуры loc_401000
;
sub
eax, offset loc_401000
; вычисляем длину зашифрованного фрагмента
;
lea
edi, [esp+14h]
; устанавливаем EDI на локальный буфер esp+14h
;
mov
ecx, eax
add
esp, 8
mov
edx, ecx
shr
ecx, 2
repe movsd
mov
ecx, edx
and
ecx, 3
repe movsb
; копируем фрагмент [0х40100 – 0x401090) в локальный буфер
;
xor
ecx, ecx
test
eax, eax
jle
short loc_4010EA
; есть что расшифровывать? ;)
;
loc_4010DA:
; CODE XREF: sub_401090+58vj

;
;
;
;
;

14

Неявный самоконтроль как средство создания не ломаемых защит
.text:004010DA
.text:004010DA
.text:004010DE
.text:004010E1
.text:004010E1
.text:004010E1
.text:004010E5
.text:004010E5
.text:004010E5
.text:004010E6
.text:004010E8
.text:004010E8
.text:004010EA
.text:004010EA
.text:004010EA
.text:004010EC
.text:004010F0
.text:004010F2
.text:004010F7
.text:004010FA
.text:004010FC
.text:004010FE
.text:00401101
.text:00401101
.text:00401101
.text:00401101
.text:00401101
.text:00401101
.text:00401101

do{
mov
dl, [esp+ecx+0Ch]
xor
dl, 66h
mov
[esp+ecx+0Ch], dl
; производим над каждым байтом зашифрованного кода операцию XOR 0x66
;
inc
ecx
; берем следующий байт
;
cmp
ecx, eax
jl
short loc_4010DA
; } while (ecx < eax)
loc_4010EA:

;
;
;
;
;
;

; CODE XREF: sub_401090+48^j
mov
ecx, eax
lea
esi, [esp+0Ch]
mov
edx, ecx
mov
edi, offset loc_401000
shr
ecx, 2
repe movsd
mov
ecx, edx
and
ecx, 3
repe movsb
записываем расшифрованные данные обратно постой, как записываем обратно?!
ведь модификация секции .text обычно запрещена?! но ведь "обычно" еще
не "всегда", верно? смотрим атрибуты секции: Flags E0000020: Text
Executable Readable Writable. ага! защита от записи была вручную от
ключена разработчиком! поэтому перезапись расшифрованного фрагмента
происходит без ошибок и препирательств со стороны Windows

Теперь, когда алгоритм расшифровки установлен (см. выделенную жирным
шрифтом строку), мы можем самостоятельно расшифровать его. Для этого нажи
маем в окне IDA и вводим следующий скрипт:
auto a;
for (a=0x401000; a < 0x401090; a++)
{
PatchByte(a, Byte(a) ^ 0x66);
}

Нажав для его выполнения, мы становится свидетелями успеш
ной расшифровки кода защитного механизма. Теперь с ним можно беспрепят
ственно работать без всяких преград. Кстати, посмотрим, создала ли IDA пере
крестные ссылки к строкам "passwd ok" и "wrong passwd"…
.text:00401050 sub_401050
proc near
; CODE XREF: sub_401090+AFvp
.text:00401050
call
sub_401000
.text:00401055
test
eax, eax
.text:00401057
jz
short loc_40106E
.text:00401059
push
offset aPasswdOk
; "passwd ok\n"
.text:0040105E
push
offset unk_407110
.text:00401063
call
_fprintf
.text:00401068
add
esp, 8
.text:0040106B
xor
eax, eax
.text:0040106D
retn
.text:0040106E ; 

Крис Касперски

15

Неявный самоконтроль как средство создания не ломаемых защит
.text:0040106E loc_40106E:
.text:0040106E
.text:00401073
.text:00401078
.text:0040107D
.text:0040107F
.text:0040107F sub_401050
.text:0040107F

push
push
call
push
call
endp

; CODE XREF: sub_401050+7^j
offset aWrongPasswd ; "wrong passwd\n"
offset unk_407110
_fprintf
0FFFFFFFFh
; int
_exit

Держи нас за хвост! Перекрестные ссылки действительно созданы и ведут
к приведенному выше коду, который слишком прост, чтобы его комментировать.
Смотрите: подпрограмма loc_40106E, выводящая надпись "wrong passwd" на экран
и прерывающая выполнение программы вызовом функции _exit, имеет перекре
стную ссылку, sub_401050+7, ведущую к условному переходу jz short loc_401064
(в листинге он выделен жирным шрифтом), который, судя по всему, и есть тот са
мый условный переход, что нам нужен! Забив его машинными командами NOP,
мы, очевидно, добьемся того, что защита перестанет "ругаться" на неверные паро
ли и любой введенный пароль воспримет как правильный.
Ну что, запустим HIEW и запишем по адресу .401057 последовательность
"90h 90h"? Не спешите, не все так просто! Ведь исходная программа зашифрована
и записанные нами команды NOP после расшифровки превратятся неизвестно во
что. Какой из этого выход? Да очень простой: записав последовательность 90h 90h
в HIEW'е мы тем же самым HIEW'ом ее и зашифруем! ОК, приступаем. Итак,
для перевода HIEW'a в hexрежим, и ".401057" для перехода по требу
емому адресу, для входа в режим редактирования, 90, 90 – забивает условный
переход, (четыре раза) для перемещения курсора на начало редакти
руемого фрагмента, , и еще раз для шифровки. Наконец,
для сохранения внесенных изменений.
Победно запускаем взломанный файл и…
crackeme.0xh by Kris Kaspersky
ERR: invalid CRC (d7988417) hello, hacker

…и тут выясняется, что защита отнюдь не так непроходима тупа, как нам это
показалось вначале! Судя по надписи она както контролирует целостность своего
кода и прекращает работу в случае его изменения. Что ж! Открываем очередное пи
во и продолжаем взлом. Можно поступить двояко: или поискать перекрестную
ссылку на строку "ERR: invalid CRC" или же установить контрольную точку на мо
дифицированный нами условный переход. Кинем монетку, если выпадет орел –
ищем перекрестную ссылку, ну а если монета упадет решкой – используем кон
трольную точку. Так, а где у нас монетка? Нету монетки?! Ну тогда, как истинные
хакеры, мы быстренько пишем собственный генератор случайных чисел и… решка!
(Если у вас выпал орел, значит, нам с вами не по пути).
> BA r4 0x407054
> G
Hard coded breakpoint hit

Отладчик WDB сообщает, что сработала контрольная точка останова. Про
пускаем ее, – это защита копирует код программы в локальный буфер для его
последующей расшифровки (это следует из того, что мы всплыли на инструкции
Крис Касперски

16

Неявный самоконтроль как средство создания не ломаемых защит
movs). Следующее всплытие отладчика соответствует обратной операции – копи
рованию уже расшифрованного кода на место постоянного проживания. А вот
третье по счету всплытие уже интересно:
0401109
0040110E
00401111
00401113
00401114
00401116
00401118
0040111E

BA00104000
8B3C0A
03DF
41
3BC8
7CF1
81FB80EC0040
741F

mov
mov
add
inc
cmp
jl
cmp
je

edx,401000h
edi,dword ptr [edx+ecx]
ebx,edi
ecx
ecx,eax
00401109
ebx,4000EC80h
0040113F

Тривиальный алгоритм подсчета контрольной суммы буквально сам броса
ется в глаза. "Или автор защиты полный идиот, или же он специально хотел быть
обнаруженным" – ворчим мы себе под нос, попутно размышляя, что лучше: скор
ректировать контрольную сумму или же просто заменить условный переход в стро
ке 0x40111E на безусловный, так чтобы он вообще не контролировал свою целост
ность? Ладно, будем приучать себя к аккуратности. Подгоняем курсор к строке
0x401118 и даем команду "Run to cursor", не забыв предварительно заблокировать
установленную точку останова (иначе отладчик просто зациклиться) и смотрим:
какое значение содержит в себе регистр EBX. Как следует из окна "Registers" оно
равно 0xd7988417, в то время как оригинальная контрольная сумма защищенного
файла была – 0x4000EC80 (см. строку 0x401118 приведенного выше листинга).
Запускаем HIEW и переписываем ее по живому, меняя "cmp ebx, 4000EC80h" на
"cmp ebx, 0xd7988417". Проверяем! wow! Это работает! Выломанный файл успешно
запускается и, молчаливо проглотив любой введенный пароль, смиренно сообщает
"passwd ok" и продолжает нормальное выполнение программы. Обмыв это дело на
радостях двойным количеством пива, хакер раздает выломанную программу всем,
нуждающимся в ней пользователям и…
…в процессе эксплуатации взломанной программы выясняется, что ведет
она себя мягко выражаясь не совсем адекватно. В частности, в нашем случае она
выводит на экран: "2 * 2 = 34280". Вот это номер! Поскольку доверять такому взло
му со всей очевидностью нельзя, лучше всего не испытывать судьбу, а приобрести
легальную копию программы (особенно, если дело касается бухгалтерского ПО,
ошибки которого зачастую несопоставимы с его стоимостью). Но всетаки, хотя бы
в плане спортивного интереса, – можно ли взломать такую программу или нет?
Условимся, что мы не будем анализировать код, вычисляющий дважды два, по
скольку в реальном, полновесном приложении очень легко добиться, чтобы ошиб
ка проявлялась не в месте ее возникновения, а в совсем другой ветке программы,
делая тем самым обратную трассировку невозможной.
Первое, что попытается сделать любой здравомыслящий хакер – поискать
смещение и/или содержимое модифицированных им ячеек, надеясь что они хра
нятся в программе прямым текстом. Причем, следует помнить о том, что некото
рые защиты контролируют не сам модифицированный байт, а некоторую протяж
ную область к которой он принадлежит. В частности, если контролируется целост
ность первого байта условного перехода, то разработчик защиты может схитрить,
обратившись к двойному слову, расположенному на три байта "выше". Что ж!
Крис Касперски

17

Неявный самоконтроль как средство создания не ломаемых защит
Сказано – сделано. Ищем… Быстро выясняется, что ничего похожего на смещение
модифицированного нами перехода в защищенной программе нет, но вот его ори
гинальное содержимое на наше удивление всетаки обнаруживается:
.text:00401090 arg_3F
.text:00401090 arg_53
.text:00401144
.text:00401148
.text:0040114C
.text:00401152
.text:00401158

= dword
= dword
mov
mov
xor
xor
mov

ptr
ptr
ecx,
edx,
ecx,
edx,
eax,

43h
57h
[esp+0Ch+arg_53]
[esp+0Ch+arg_3F]
48681574h
5EC0940Fh
2

Мало того! Рядом с ним валяется указатель 0x57, который "волшебным" об
разом совпадает с относительным смещением модифицированного нами байта, от
считываемого от начала тела первой зашифрованной процедуры (развитие зри
тельной памяти невероятно ускоряет взлом программ). Так вот ты какой, северный
олень! Буквально за однудве секунды мы вышли на след защитного кода, который
по замыслу автора мы ни за что не должны были обнаружить! А обнаружили мы его
только "благодаря" тому обстоятельству, что и смещение, и содержимое контроль
ной точки хранилось в программе в открытом виде. Вот если бы оно вычислялось
на лету на основе запутанных математических операций… впрочем, не будет повто
ряться, мы об этом уже говорили.
Хорошо, условимся считать, что поиск по содержимому не дал результатов
и хакер остался с защитой один на один. Что он еще может предпринять? А вот
что – аппаратная точка останова на модифицированный байт! Да, конечно, мы уже
устанавливали ее, но ранее слишком быстро отсекали "лишние" срабатывания.
Теперь же настало время заняться этим вопросом вплотную. Вновь запустив поряд
ком затосковавший за это время WDB, мы даем ему уже знакомую команду
"ba r4 0x401057" (не обязательно набивать ее на клавиатуре, достаточно лишь на
жать стрелку вверх и отладчик сам извлечет ее из истории команд). Первое сраба
тывание приходится на следующий код:
04010C8 C1E902
004010CB F3A5
004010CD 8BCA

shr
rep movs
mov

ecx,2
dword ptr [edi],dword ptr [esi]
ecx,edx

Узнаете? Ну да, были мы здесь недавно и все тщательно проанализировали,
так и не обнаружив ничего интересного. Идем дальше? Стоп! А точку останова на
буферприемник кто будет ставить? ОК, отдаем отладчику следующую команду:
"ba r4 (edi4)". Почему (edi4)? Так ведь точки останова срабатывают после выпол
нения соответствующей им команды, т. е. на момент всплытия отладчика, регистр
EDI указывает на следующее двойное слово, а совсем не на то, которые содержит
только что скопированный в буфер код.
Очередное всплытие отладчика приводит нас к коду расшифровщика, уже
знакомому нам и не содержащему абсолютно ничего интересного. Не тратя на не
го понапрасну свое драгоценное время мы отдаем команду "G" и… через серию по
следовательных всплытий отладчика, отождествляем расшифровку защитного ко
да, его обратное копирование, явную проверку контрольной суммы и, наконец,
сталкивается с малопонятным на первый взгляд кодом, про который можно сказать
лишь одно: он использует значение тех самых ячеек защитного кода, которые мы
варварски "модернизировали":
Крис Касперски

18

Неявный самоконтроль как средство создания не ломаемых защит
0040113F
0401144
00401148
0040114C
00401152
00401158
0040115D
00401161

E80CFFFFFF
8B4C2463
8B54244F
81F174156848
81F20F94C05E
B802000000
8D4C1104
8D54240C

call
mov
mov
xor
xor
mov
lea
lea

00401050
ecx,dword ptr [esp+63h]
edx,dword ptr [esp+4Fh]
ecx,48681574h
edx,5EC0940Fh
eax,2
ecx,[ecx+edx+4]
edx,[esp+0Ch]

Конечно, в данном демонстрационном примере алгоритм "балансировки"
распознается без особого труда и серьезных умственных усилий, но как бы там ни
было, аппаратные точки останова позволили выявить тот самый код, что осущест
вляет неявный контроль целостности защиты. Кстати, аппаратных контрольных
точек всего четыре, а количество буферов, в которые можно запихать"клоны" ко
пий оригинального кода программы – не ограничено много. Словом, если чуть
чуть постараться, можно очень сильно умерять хакерский пыл – за всеми буфера
ми так просто не уследить, придется анализировать огромное количество кода,
лишь часть из которого непосредственно относится к защитному механизму, а все
остальное – мусор. Чтобы еще больше запутать хакера можно осуществлять неяв
ный контроль целостности не при каждом запуске программы, а, скажем, на осно
ве датчика случайных чисел, – один раз, эдак, из десяти. "Плавающая" защита –
что может быть хуже?! Да, теоретически можно и ее сломать, но: вопервых, даже
трудно себе представить сколько на нее придется угробить времени, а, вовторых,
никто не даст и кончика хвоста на отсечение, что выявлены и нейтрализованы все
уровни защиты. Ведь аппаратные точки срабатывают лишь в момент обращения
к ним, а дизассемблирование бессильно выявить адреса, получаемые на основе
сложных математических манипуляций с указателями.
Но всетаки давайте доломаем нашу защиту. В данном конкретном случае
мы можем нейтрализовать защитный механизм, просто заменив команду
xor ecx, 48681574h на xor ecx, 48689090h, т. е. просто скорректировав "балансир".
Однако, при взломе реальной программы хакер должен убедиться, что корректиру
емый им балансир и не балансирует чтото еще.

Крис Касперски

19