Взлом через покрытие

Алгоритмы определения покрытия


Существует по меньшей мере два алгоритма определения покрытия: трассировка и точки останова. Исполнение программы в пошаговом режиме дает нам полную трассу прогона, "разоблачая" адреса всех выполняемых инструкций. К сожалению, защищенные программы как правило не позволяют себя трассировать, к тому же это медленно и непроизводительно.

Другой алгоритм заключается во внедрении программных точек останова в начало _всех_ машинных инструкций. Каждая, однажды сработавшая точка останова, снимается, а соответствующий ей код объявляется покрытым. Именно так большинство профилировщиков и работает (с той лишь разницей, что они внедряют точки останова в начало _группы_ инструкций, представляющей ту или иную строку исходного кода — вот зачем им нужна информация о строках!). Быстродействие (за счет снятия точек останова) намного выше, чем при трассировке, особенно в циклах, но… программную точку останова _очень_ легко обнаружить, поскольку она представляет собой байт CCh, записываемый поверх инструкции и выявляемый простым подсчетом контрольной суммы, что делает его малопригодным для анализа защитных механизмов.

К сожалению, аппаратных точек останова всего четыре и потому единственным надежным способом определения покрытия остается полная эмуляция процессора (чтобы не писать эмулятор с нуля можно воспользоваться уже готовым, например, BOCHS, распространяемом в исходных текстах — легким взмахом "напильника" мы переделаем его во что угодно!). Как вариант, можно использовать грубый метод покрытия по страницам — помечаем все страницы процесса как недоступные, а потом ловим исключения и определяем покрытие. При условии, что защитный код расположен в отдельной процедуре (как чаще всего и происходит), мы с определенной степенью вероятности ее "запеленгуем", а, быть может, и нет. Все зависит от того сколько места защитный код занимает. Во всяком случае, одиночные jx'ы данный метод не обнаруживает в принципе, но при желании его можно доработать.


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


Содержание раздела