Техника снятия дампа с защищенных приложений

         

Механизмы динамической расшифровки


Алгоритм динамической расшифровки, реализованный в протекторе Armadillo и известный под именем CopyMem, в общих чертах выглядит так: защита порождает отладочный процесс, передавая функции CreateProcess в качестве имени нулевой аргумент командной строки, "благодаря" чему в диспетчере задач отображается две копии запущенной программы. Одна из них сервер (условно), другая — клиент. Сервер посредством функции VirtualProtectEx делает все страницы клиента недоступными (атрибут PAGE_NOACCESS) и передает ему управление, ожидая отладочных событий с помощью функции WaitForDebugEvent, а события долго ждать себя не заставляют и при первой же попытке выполнения кода в недоступной странице возбуждается исключение, передающее серверу бразды правления. Сервер расшифровывает текущую страницу, взаимодействуя с клиентов посредством API-функций ReadProcessMemory/WriteProcessMemory, устанавливает необходимые атрибуты доступа и возвращает клиенту управление.

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

Потребность в отладочном процессе-сервере объясняется тем, что по другому ловить исключения на прикладном уровне просто не получается. А как же механизм структурных исключений или, сокращенно, SEH? Регистрируем свой собственный обработчик и ловим исключения, что называется по месту возникновения. Это избавляет нас от API-вызовов, обеспечивающих межпроцессорного взаимодействия, которые элементарно перехватываются хакером. Увы! Если защищаемое приложение использует SEH (а подавляющее большинство приложений его используют), наш обработчик окажется перекрыт другим.
Столкнувшись с "нашим" исключением, он попросту не будет знать, что с ним делать и с вероятностью близкой к единице, просто завершит приложение в аварийном режиме.

Теоретически, установку нового обработчика легко отследить, установив аппаратную точку останова по доступу к памяти на адрес FS:[00000000h]. Операционные системы семейства NT позволяют прикладным приложениям манипулировать с отладочными регистрами через контекст, причем, отладочный регистр действует лишь в рамках "своего" процесса, не мешая работать всем остальным, но 9x "забывает" сохранять отладочные регистры в контексте "своего" процесса и они приобретают глобальный характер, воздействующий на все процессы! Так что в ней этот трюк не проходит.

А вот другой способ: устанавливаем драйвер, перехватывающий исключения на уровне IDT и взаимодействующий со своим процессом либо через DeviceIoControl, либо через NtReadVirtualMemory/NtWriteVirtualMemory/KeDeattachProcess/KeAttachProcess. Это вполне надежно, однако, написание драйверов — занятие утомительное и совсем небезопасное в плане "голубых экранов смерти". К тому же, разработчику защиты придется либо наотрез отказываться от поддержки 9x (которая все еще жива!), либо реализовывать сразу два драйвера! Тем не менее, защиты такого типа все-таки встречаются.

Независимо от того, как происходит обработка исключения, сломать такую защиту очень просто! Читаем первую страницу, дожидаемся завершения расшифровки, сохраняем ее на диск, обращаемся к следующей странице и... действуем так до тех пор, пока в наших руках не окажется весь образ целиком. Стоит только внедрить код дампера в адресное пространство защищенного процесса и… протектору будет очень сложно отличить обращения самой программы от обращений дампера.

Последние версии протектора Armadillo, недавно переименованного в Software Passport, реализуют намного более надежный, хотя и чрезвычайно низко производительный механизм трассирующей расшифровки, при котором весь код программы зашифрован целиком.


Сервер трассирует клиента, расшифровывая по одной инструкции за раз (предыдущая инструкция при этом зашифровывается). Снять дамп тупым обращением к памяти уже не получается, поскольку защиту интересуют только исключения, возникающие при исполнении. Все, что мы можем, это "вклиниться" между зашифрованным приложением и расшифровщиком, "коллекционируя" расшифрованные инструкции, образующие трассу потока выполнения. Поскольку, достичь 100% покрытия кода практически невозможно, полученный дамп будет неполноценным, но... тут есть один маленький нюанс. Покомандная расшифровка не может использовать ни блочные, ни контекстно-зависимые криптоалгоритмы, поскольку трассирующий расшифровщик никогда не знает наперед какая инструкция будет выполнена следующей. Остаются только потоковые алгоритмы типа XOR или RC4, которые очень легко расшифровать — стоит только найти гамму, которую протектор несмотря ни на какие усилия, слишком глубоко запрятать все равно не сможет! Естественно, полностью автоматизировать процесс снятия дампа в этом случае уже не удастся и придется прибегнуть к дизассемблированию, а, быть может, даже отладке. К счастью, подобные схемы защиты не получили широкого распространения, и навряд ли получат его в обозримом будущем. Трассировка замедляет скорость работы приложения в десятки раз, в результате чего оно становится неконкурентоспособным.


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