Откомпилированная программа обычно подвергается упаковке. Цель здесь совсем не в уменьшении размера, а в затруднении анализа. Упаковщик набивает программу антиотладочными приемами и прочей бодягой, которая затрудняет пошаговую трассировку или даже делает ее невозможной. На самом деле хакер и не собирается ничего трассировать, а только снимает с работающего приложения дамп и дизассемблирует его (реконструировать exe-файл для этого необязательно). Надежно противостоять снятию дампа на прикладном уровне невозможно, а спускаться на уровень драйверов как-то не хочется. Некоторые защиты искажают PE-заголовок, гробят таблицу импорта и используют другие грязные трюки, с помощью которых затрудняют дампинг, но не предотвращают его в принципе.
глобальные инициализированные переменные
Предлагаю альтернативный путь — не препятствовать снятию дампа, а сделать полученный образ бесполезным, для чего достаточно использовать глобальные инициализированные переменные, «перебивая» их новыми значениями.
Листинг
защитный механизм, предотвращающий снятие дампа с работающей программы путем использования инициализированных глобальных переменных
char *p = 0; // глобальная переменная 1
DWORD my_icon = MY_ICON_ID; // глобальная переменная 2
…
if (!p) p = (char*) malloc(MEM_SIZE);
my_icon = (DWORD) LoadIcon (hInstance, my_icon);
Задумайся, что произойдет, если сбросить дамп с работающей программы. В переменной p окажется указатель на когда-то выделенный блок памяти, условие (!p) обломится и новая память не будет (!) выделена, а при обращении по старому указателю произойдет исключение. Другими словами, хакер уже не сможет изготовить исполняемый файл из дампа! Как минимум, придется восстановить значения всех глобальных переменных — геморрой :(. Ладно, изготовить исполняемый файл из дампа нельзя, но, может, дизассемблировать его? А вот и нельзя…
После выполнения функции LoadIcon переменная my_icon будет содержать не идентификатор иконки, а ее обработчик. Хакер не сможет установить, что это за иконка (строка, битмап или другой ресурс), и ему придется обращаться к отладчику, противостоять которому намного проще, чем дизассемблеру. Кстати, такой прием экономит память и широко используется во многих программах. Например, в стандартном «Блокноте» — попробуй снять с него дамп и обломайся :).
стартовый код
Единственная надежда хакера — отловить момент завершения распаковки и тут же сбросить дамп, пока защита не успела нагадить в глобальные переменные. Пошаговая трассировка исключается (противостоять ей очень легко), и остается… стартовый код, который варьируется от компилятора к компилятору.
Листинг
типичный представитель стартового кода (в случае с Microsoft Visual C++ MFC)
.text:00402A82 push ebp
.text:00402A83 mov ebp, esp
.text:00402A85 push 0FFFFFFFFh
.text:00402A87 push offset unk_403748
.text:00402A8C push offset loc_402C06
.text:00402A91 mov eax, large fs:0
.text:00402A97 push eax
.text:00402A98 mov large fs:0, esp
.text:00402A9F sub esp, 68h
…
.text:00402BAA call ds:GetModuleHandleA
.text:00402BB0 push eax
.text:00402BB1 call _WinMain@16 ; WinMain(x,x,x,x)
точка останова на GetModuleHandleA
Вызов API-функции GetModuleHandleA сразу же бросается в глаза. Если хакер установит сюда точку останова, отладчик/дампер «всплывет» в start-up-коде еще до передачи управления WinMain (также можно поставить точки останова на GetVesion/GetVersionEx, GetCommandLine, GetStartupInfo и т.д.). Если точка останова программная, распаковщик может обнаружить ее по наличию ССh в начале API-функции и, с некоторой долей риска, снять ее. Если второй байт функции равен 8Bh, то перед нами, очевидно, предстает стандартный пролог, первый (оригинальный) байт которого равен 55h. Получаешь права на запись через VirtualAlloc, меняешь CCh на 8Bh и продолжаешь распаковку в обычном режиме. Пусть хакер крякнет! Правда, в последующих версиях Windows пролог API-функций может быть модифицирован, и тогда этот трюк не сработает.
отладочные регистры
Аппаратную точку останова можно обнаружить чтением регистров Drx. Команда mov eax,DrX на прикладном уровне приводит к исключению, кроме того, отладчик (теоретически) может отслеживать обращение к отладочным регистрам, чтобы маскировать свое присутствие, — x86 процессоры предоставляют все необходимое для этого. Но если распаковщик прочитает свой контекст, он сможет дотянуться и до Drx, причем не только на чтение, но и на запись! Получается, что можно не только обнаружить точки останова, но и обезвредить их. Весь вопрос в том, как получить контекст. Чтение SDK выявляет API-функцию GetThreadContext, которая как раз для этого и предназначена, однако пользоваться ей нельзя, иначе хакер установит сюда точку останова и защита проиграет войну.
Нужно действовать так. Регистрируешь из распаковщика собственный обработчик SEH, возбуждаешь исключение (делишь на ноль, обращаешься по недействительному указателю) и получаешь контекст в одном из аргументов структурного обработчика. Остается установить точку останова на fs:0, где и хранится указатель на SEH-обработчик (но до этого додумается не каждый хакер).
структурные исключения
Кстати о fs:0. Первое, что делает стартовый код, — это регистрация собственного SEH-обработчика, поэтому установка точки останова на fs:0 позволяет хакеру всплыть сразу же после завершения распаковки, следовательно, распаковщик должен обращаться к этой ячейке как можно чаще. Десятки или даже тысячи раз, причем следует класть туда не что угодно, а именно ESP, иначе хакер установит условную точку останова (soft-ice это позволяет) и легко обойдет защиту.
поиск по сигнатуре
Есть такой козырный хакерский трюк. Взломщик снимает дамп с работающей программы, находит там стартовый код по сигнатуре, определяет его адрес и ставит на его начало аппаратную точку останова (программную ставить нельзя, она будет затерта при распаковке). Разработчику защиты необходимо либо распознавать аппаратные точки останова и снимать их, действуя по методике, описанной выше, либо использовать модифицированный стартовый код, который не сможет распознать хакер.
контроль $pc
Еще один трюк. Большинство распаковщиков располагаются в стороне после распакованного кода и при передаче управления на оригинальную точку входа прыгают куда-то далеко. Хакер может использовать этот факт как сигнал о том, что распаковка уже завершена. Конечно, установить аппаратную точку останова на это условие уже не удастся, и придется прибегнуть к пошаговой трассировке (ей легко противостоять), но ради подобного случая хакер может написать и трейсер нулевого кольца. Несложно. Сложно определить, когда же заканчивается распаковка. Контроль на $pc (в терминологии x86 — «eip») — единственный универсальный способ, который позволяет сделать это без особых измен и, чтобы обломать хакера, распаковщик должен как бы «размазывать» себя вдоль программы. Тогда он победит!