Антиотладочные приемы. Положи SoftIce на лопатки

TanaT

Xakep, номер #050, стр. 054-057

tanat@hotmail.ru

Наш журнал часто рассказывает о взломах программ, детально разбирая технику самого процесса и используемый при этом инструментарий. Пришло время встать по другую сторону баррикад. Сегодня мы узнаем, как защитить свою программу и усложнить жизнь крякеру. Мы разберемся во внутреннем устройстве отладчика и в принципах его работы, вместе поищем дыры в его механизмах и, наконец, ответим на вопрос: "Всесилен ли SoftIce?". Ведь не секрет, что с его помощью многие защиты снимаются в течение часа. Да какое там часа, 10 минут (при условии, что ты слышишь о нем не в первый раз). В общем, читай дальше, не пожалеешь!

Многие считают, что защищать программы не нужно. В этом действительно есть рациональное зерно. Когда я, например, слышу что-нибудь об антиотладочных приемах и дизассемблерных ловушках, на ум сразу приходит высказывание: Everything that can run, can be cracked. Защищать программы бесполезно: отбить охоту копаться в твоем коде можно лишь у начинающего крякера, опытного профи не остановить ничем. Взлом программы - дело времени. В твоих силах увеличить это время настолько, что сам крякер пожалеет, что взялся за твою программу.

Однако такая точка зрения грешит своей необъективностью: разумный компромисс заключается в том, чтобы защитить свою программу и сделать ее регистрацию выгодной. Пока защита будет взламываться, можно успеть продать какое-то количество продукта, что само по себе уже достижение. А обеспечив качественную поддержку со стороны разработчика, можно привлечь к себе в клиенты фирмы и корпорации. Ведь нельзя представить, что, например, Yahoo или Yandex будут использовать пиратские версии соответствующих антивирусов и брэндмауэров (firewalls по-русски).

Вообще отладчиков существует тьма-тьмущая, все не ограничивается одним SoftIce, хотя у последнего много преимуществ. Итак, отладчик может работать как обычная программа или в нулевом кольце защиты. К первым можно отнести Turbo Debugger и Code View, а ко вторым - уважаемый SI. Не стоит удивляться, что мы вспомнили такую, казалось бы, древность, как TD (тормознутый дебаггер) и CV (продукт всеми любимого БГ). На их примерах проще всего понять принцип работы любого отладчика.

Inside Debugger

Сейчас мы попытаемся понять "образ мышления отладчика" :). Итак, почти все отладчики используют так называемый режим трассировки (для этого нужно установить в единичку флаг трассировки TF). В этом режиме после выполнения любой команды генерируется отладочное исключение (или прерывание, кому как больше нравится) int 01h (h мы поставили в конце по привычке, хотя на самом деле число 01 одинаково и в десятичной и в шестнадцатеричной системах). Исключение составляют команды, модифицирующие сегментные регистры. Следовательно, при выполнении каких-либо операций с этими регистрами происходит "потеря трассировочного прерывания". Далее: начиная с Intel 80386 появились новые возможности отладки (специальные отладочные регистры DR0-DR7), модифицировалась и потеря трассировочного прерывания. Теперь оно теряется только при операциях с регистром SS. Что же нового дают регистры DR0-DR7? Они позволяют ловить: исполнение команды, запись данных, чтение/запись в порт и запись/чтение данных, но не исполнение. Таким образом, любой современный отладчик либо использует TF (что очень маловероятно - слишком старо), либо 8 отладочных регистров (почти всегда).

Теперь разберем подробнее. Регистры DR0-DR3 служат для установки брейкпоинтов (то есть их может быть одновременно только четыре - это будет очень важно в дальнейшем), DR4 и DR5 зарезервированы, DR6 частично зарезервирован, а частично используется (нам не важно, как) и DR7 - самый главный, он задает режим работы других регистров, содержащих точки останова.

Таким образом, вся отладка происходит через эти регистры. В принципе, теперь ты и сам можешь написать свой отладчик, правда, придется досконально изучить каждый бит трассировочных регистров, но ведь это мелочи :).

Один на один с отладчиком

Ну что, пришло время и нам показать, на что мы способны? Пойдем по порядку. Первыми идут старики: TD и CV. У них есть дыры? Если по существу, то это одна сплошная дыра. Обойти эти отладчики очень просто (поэтому ими никто не пользуется). Дело в том, что они являются отладчиками реального времени, следовательно, имеют с отлаживаемой программой общий стек, позволяют программе влиять на свой собственный код, находятся в одном адресном пространстве с прогой-пациентом. Что из этого можно извлечь?

Начну с садистского приема. Все-таки пусть эта тварь тоже почувствует себя жертвой :). Этот прием очень прост: мы отключим клавиатуру! Представь, Ламер запустит твою программулину под отладчиком, а у него раз - и клавиатура отключилась. Что ему делать? Да ничего, только перезагружаться. А отключить клаву можно с помощью махинаций с портами (3 способа):

in al,21h
or al,00000010b ; irq 1 клавиатурное irq
out 21h,al

или

in al,61h
or al,10000000b ; бит 7 - отключает клаву
out 61h,al

или

mov al, 0ADh ; отключение клавиатуры
out 64h,al

К таким же садистским приемам можно отнести и переход в нестандартный графический режим: в таком случае экран исказится, земля задрожит... И отладка станет невозможной! Но тут многое зависит от этой самой нестандартности: клавы-то у всех почти одинаковые, а вот нестандартности еще и поискать надо. Но мы еще не закончили с клавиатурой. Следующий код вызывает переполнение порта клавиатуры, он безотказно действует под MS-DOS, но и только :(.

in al,064
push ax
mov al,0FE
out 064,al
pop ax
out 064,al

Далее. Помнишь, мы говорили о потере трассировочного прерывания? Хоть этот прием и старый (все о нем давно знают), но тебе стоит на это посмотреть:

...
pop ss ; в режиме трассировки после этой команды прерывание int 1 не будет вызвано
pushf
pop ax ; Записать флаги в ax
test ax,0100h ; Проверка: установлен ли флаг TF в единичку?
jnz OPS
...
OPS: ; работа в пошаговом режиме! Тут ты можешь попытаться повесить комп, выйти из программы и стереть command.com
...

Потеря процессором трассировочного прерывания после выполнения команды POP SS приведет к тому, что отладчик "не заметит" команду PUSHF, и в стек будет занесено реальное состояние регистра флагов (с установленным битом TF).

Идем дальше. Помнишь, мы говорили об отладочном исключении (за номером 1)? Так вот, можно поменять вектор этого прерывания (перегрузить его). То есть теперь оно будет указывать на что-нибудь, нужное тебе (что ты и будешь использовать в программе), но отнюдь не на отладку. Тут уж раздолье для твоей фантазии. Примеров приводить не буду, так как перегружать прерывания можно только под DOS, ибо мастдай такую штуку не пропустит.

Еще на старые версии SI действовал такой прием: есть прерывание int 03h, оно является как бы API SI, то есть SI через это прерывание управляет и может быть управляем. Соответственно ничто тебе не мешает самому управлять SI через свою программу и вызвать зависание или перезагрузку компа :). НО! Это действует только для DOS (WINDOWS вместо прерываний предоставляет WIN API) и только на старые версии SI (потом вышел патч, и дыра исчезла).

Всесилен ли SoftIce?

Нет. Не всесилен - его можно побороть. Правда, с одной оговоркой - можно побороть программу, но не крякера-профи. Чтобы охладить твою радость, скажу, что хотя можно побороть SI, нельзя побороть IDA (крутейший дизассемблер). Но против последнего существуют свои приемы: самомодифицирующийся и зашифрованный код. К тому же исследовать мегабайты дизассемблированного кода - занятие не из легких.

Но вернемся к SI. Как же положить его на лопатки? Да не простой SI, а последний, пропатченный. Ну что ж, давай разберемся.

Если обобщить, то существует три способа: первый - убивает SI (он просто вылетает), второй - очень сильно затрудняет отладку, третий - очень перспективный способ, делающий отладку по большому счету вообще безнадежной. Давай по порядку.

Первый способ состоит в эмуляции исключительной ситуации. Например, деления на ноль. Ты же знаешь, что на ноль делить нельзя?

А что сделает компьютер в этом случае? Он сгенерирует исключение! (Исключение - это что-то типа ошибки, обработку которой выполняет специальная функция - обработчик исключения). Но языки высокого уровня позволяют перехватывать исключения и обрабатывать их по-своему! А что делает отладчик? Следует учесть, что SI (реально крутая прога) пытается полностью эмулировать процессор и выполняет те же действия, что и прога-пациент. Значит, он тоже будет делить на ноль! А он-то, в отличие от твоей проги, делить на ноль не умеет! Осталась сущая ерунда: научить твою прогу делить на ноль. Вот пример на языке C:

// Защита
__try //Это начало блока, где может возникнуть исключительная ситуация
{
int Number1=5, Number2=5; // Думаю, это не нуждается в комментариях
double Result=Result/(Number1-Number2); // Вот она изюминка: пусть SI делит результат на ноль
}
__except(EXCEPTION_EXECUTE_HANDLER) // А вот тут мы ловим исключение и не даем программе вылететь с ошибкой
{
// Здесь находится твой дальнейший код
}
А вот как выглядит тот же прием на Delphi:
var Number1, Number2: integer;
Result: real;
begin
try
Number1:=5;
Number2:=5;
Result:=Result/(Number1-Number2);
except
...... /* Твой код
end

"В чем сила брат?" А сила в том, что крякеру надо будет пытаться найти адрес, где начинается твой дальнейший код, а это может быть не просто, так как реализация такого механизма сильно зависит от конкретного компилятора! Ради полноты картины следует сказать, что и IDA раньше вылетал после такого трюка. Однако его последние версии дизассемблируют все корректно. Чтобы затруднить анализ такого кода, нужно воспользоваться несколькими дополнительными приемами: вставить несколько таких блоков (размазать защиту по всей программе) в полезные процедуры, сделать специальные большие (много кода) функции, которые после многочисленных операций возвращают какое-нибудь число, которое и нужно присваивать числам a и b (для деления на ноль). В общем, сделать такой прием не столь очевидным для обнаружения.

Что еще мы можем предложить отладчику? А вот что. Почти любое руководство по SI говорит: "Найди функцию, с помощью которой осуществляется считывание пароля из окошка. Это может быть либо GetWindowTextA, либо GetDlgItem, либо еще что-нибудь..."

Вот тут-то и можно подловить мальчиша-кибальчиша: узнать самому, с помощью какой функции осуществляется ввод пароля, дизассемблировать ее :) (почти любая функция начинается с команд PUSH EBP или MOV EBP,ESP - которые твоя прога может выполнить и самостоятельно). Потом выполнить какую-то часть этой функции самому и передать управление на оставшийся кусок (обычной командой jmp, а для получения адреса, куда передавать управление, можно воспользоваться API-функцией GetProcAddr()). Таким образом неопытный взломщик так никогда и не узнает, каким способом ты получаешь свои данные из окна. Конечно, опытный профи догадается, но не сразу. Потом ему придется потрудиться, чтобы сломать твою программулину.

Последний и самый действенный способ - это использовать несколько потоков (одновременно работающие функции), которые будут преобразовывать полученные данные. Вот представь: пользователь ввел пароль, твоя программа его получила, SI это засек, один поток преобразует пароль (например, меняет все буквы на маленькие), второй меняет его еще как-нибудь (например, после каждой буквы вставляет число, генерируемое по специальной формуле). Что сделает SI? Он может вообще не засечь второго потока. А тогда вообще кранты! Но опытный крякер догадается, что где-то происходит изменение данных. Правда, ему придется потратить на это время. А теперь подумаем: сколько точек останова можно поставить с использованием отладочных регистров? Правильно - четыре. Помнишь, я обращал на это твое внимание? А что тебе мешает организовать больше четырех потоков? Ничего! И вот тут даже опытный крякер приплывет. Ты спросишь: "А что мешает крякеру установить программную точку останова?" То есть, почему взломщик не сможет после каждой команды вызвать int 01h без использования трассировочных регистров (это и есть программная точка останова)? А то, что программная точка останова МОДИФИЦИРУЕТ код (она его изменяет, этим грешат еще отладчики реального режима). А то, что код изменился, можно запросто узнать, подсчитав его контрольную сумму CRC. Таких алгоритмов очень много в сети на всех языках, так что я не буду повторяться.

Некролог

Напоследок я хочу раскрыть тебе несколько тонкостей, на которых не стал останавливаться в статье. Я не стал приводить примеры реализации многопотоковых приложений, так как они сильно зависят от языка программирования и требуют глубоких знаний. Реализацию многопотоковых приложений на Delphi можно найти на странице Horrific'а: www.cydsoft.com/vr-online/3_2001/delphi1.htm.

Теперь о том, как еще можно усложнить жизнь взломщику. Надо размазывать защиту по программе, используя потоки, где только можно, например, сделать поток, считающий CRC твоего кода и сигнализирующий об отладке.

Старайся придумывать свои приемы. Основа каждого приема - оригинальность. Например, можно взять какую-нибудь известную функцию (к которой отладчик или дизассемблер привык), поменять местами пару команд или добавить простых NOP (команда ассемблера, которая ничего не делает) и ассемблировать заново. В результате ты получишь нечто новое, что и следует использовать в своих творениях.

Теперь несколько слов, почему нельзя напрямую обращаться к регистрам DR0-DR7. Ведь постоянно их модифицируя, можно помешать отладчику использовать их в своих целях. Дело в том, что обращение к ним возможно лишь в защищенном, реальном и SMM-режимах (SMM - это System Managment Mode, но нам это сейчас неважно). Легально перейти в эти режимы обычной программе под WiN невозможно. То есть win дает переходить в эти режимы, но только через дырки, оставленные для себя. А что касается NT/2000 (наших любимцев), то там такой переход вообще очень сложное дело, и каждая новая дырка зашивается очень быстро. Так что напрямую помешать отладке мы не сможем.

И помни: ты сможешь обмануть SI (он не всесилен), да и IDA тоже, но чтобы облапошить крякера, надо придумать что-нибудь более оригинальное. Самым эффективным способом является многопотоковость. Каким бы гениальным ни был крякер, ресурсы его инструментария просто истощатся, а писать собственный отладчик для твоей проги - слишком велика честь. За это время ты распродашь продукта столько, что его взломанная версия никому не будет нужна.

Содержание
ttfb: 6.8190097808838 ms