Автор: (c)Крис Касперски ака мыщъх
Сегодняшний выпуск трюков всецело посвящен хитроумным приемам программирования, затрудняющим дизассемблирование и отладку откомпилированной программы, то есть увеличивающих ее сопротивляемость взлому, причем все это - безо всякого ассемблера и других шаманских ритуалов!
Трассировка программы без захода в функции (step-over) - основной способ хакерской навигации, на пути которого разработчики защитных механизмов стремятся расположить всякие подводные рифы и другие неожиданные ловушки типа функций, никогда не возвращающих управление в точку возврата, что приводит к потере контроля над отлаживаемой программой и сильно напрягает хакера, заставляя входить в каждую функцию, а также во все вызываемые ею функции. При большом уровне вложенности взлом растягивается на многие часы, дни, недели, месяцы, годы...
Самый простой (и легальный) способ "обхода" точки возврата основан на использовании стандартных Си-функций setjump/longjump. Первая запоминает состояние стека функции в переменной типа jmp_buf (включая адрес текущей выполняющейся инструкции), вторая - передает управление по этому адресу вместе с аргументом типа int, что создает безграничный простор для всякого рода "трюкачества", наглядный пример которого приведен ниже:
Листинг 1. Обход точки возврата через longjmp.
Откомпилируем программу и убедимся, что она последовательно вызывает функции A(), B(), C(), после чего раскомментируем строку _asm{int 03}, откомпилируем еще раз и запустим полученный exe-файл под отладчиком OllyDbg (или любым другим), нажав <F9> (run) для достижения строки int 03h (см. рис. 1). Начинаем трассировать программу по <F8> (step over) и... не доходя до строки "printf("good bye, world!\n");" и не успев выполнить функцию A(), отладчик неожиданно теряет контроль за подопытной программой, и она, вырвавшись из-под трассировки, благополучно завершается по return 0, что OllyDbg и констатирует. Сказанное относится не только к OllyDbg, но также к Soft-Ice и всем остальным отладчикам.
К сожалению, в дизассемблере типа IDA Pro ловушка становится слишком очевидной, и установив точку останова на функцию setjmp, хакер без труда сможет отладить защищенную программу, разобрав защитный механизм на составные части, выкинув из него все лишние детали.
Рисунок 1. При входе в следующую функцию отладчик безвозвратно теряет контроль над отлаживаемой программой.
При step-over трассировке отладчики устанавливают программную (реже аппаратную) точку возврата за концом команды CALL func_A, куда func_A возвращает управление посредством оператора return, стягивающего со стека адрес возврата, положенный туда процессором перед вызовом func_A. Таким образом, чтобы вырваться из-под трассировки, нам достаточно заменить подлинный адрес возврата на адрес какой-нибудь другой функции (назовем ее функцией func_B), куда и будет передано управление.
Проблема в том, что положение адреса возврата в стеке нельзя узнать штатными средствами языка Си, а если задействовать нелегальные средства, то это уже будет не трюк, а хак, то есть грязный прием программирования, работающий не на всех платформах и зависящий от компилятора. Тем не менее, кое-какие зацепки у нас есть. Мы знаем, что стек растет снизу вверх (т.е. из области старших адресов в область младших), также мы знаем, что по Си-соглашению аргументы функции заносятся в стек справа налево, после чего туда заносится адрес возврата и между последним аргументом и адресом возвратом компилятор не имеет права класть ничего другого, в противном случае функция просто не сможет найти свои аргументы, а поскольку программист имеет право использовать функции, откомпилированные разными компиляторами, то компилятору ничего не остается, кроме как следовать соглашениям.
Таким образом, нам надо просто получить адрес самого левого аргумента (что можно сделать оператором &) и, преобразовав его к указателю на машинное слово (на x86 составляющее 32 бита и совпадающее по размеру с указателем на int), уменьшить его на единицу и... записать по данному адресу указатель на функцию func_B, куда и будет передано управление по завершению func_A. При этом случает помнить, что при выходе из функции func_B управление будет передано... обратно на саму функцию func_B! Почему? Да потому, что она вызвана "нечестным" способом и в стек не занесен адрес возврата. Тем не менее, func_B может спокойно вызывать остальные функции "честным" путем, ничем не рискуя.
Законченный пример приведен ниже:
Листинг 2. Демонстрация вызова функции с подменой адреса возврата.
Компилируем программу, убеждаемся что она работает, затем раскомментируем строку _asm{int 03}, перекомпилируем обратно и запускаем под отладчиком Microsoft Visual Studio Debugger (или любым другим). Нажимаем <F5> (run) и затем несколько раз <F10> (step over). Отладчик входит в функцию A(), но обратно уже не возвращается, поскольку отлаживаемая программа вырывается из лап трассировщика!
Рисунок 2. Подмена адреса возврата вырывает отлаживаемую программу из лап трассировщика.
Описанный выше прием успешно борется с отладчиками, но бессилен перед дизассемблерами, поскольку при первом же взгляде на вызов функции func_A, становится заметно (см. листинг 3), что ей в качестве аргумента передается адрес функции func_B. "Это же явно неспроста" - бормочет хакер себе под нос, устанавливая точку останова на func_B, о которую спотыкается защитный механизм в бессильной попытке освободится от гнета отладчика.
.text:0040103F int 3 ; Trap to Debugger .text:00401040 push offset func_B .text:00401045 call func_A .text:0040104A add esp, 4 .text:0040104D push offset aGoodByeWorld ; "good bye, world!\n" .text:00401052 call _printf
Листинг 3. Так выглядит откомпилированный листинг 2 в дизассемблере.
Проблема решается легкой ретушью защитного механизма. Достаточно слегка зашифровать указатель на func_B, чтобы он не так бросался в глаза и... хакер ни за что не догадается, где зарыта собака, пока не проанализирует весь код целиком, а анализ всего кода программы - дело непростое и отнимающее уйму времени.
Самое просто, что только можно сделать - перед передачей указателя на func_B наложить на него "магическое слово" операцией XOR, а перед подменой адреса возврата наложить XOR еще раз, получая исходный указатель:
Листинг 4. Доработанный вариант листинга 2, маскирующий указатель на func_B.
Компилируем программу (не забыв задействовать оптимизацию, чтобы компилятор зашифровал указатель еще на стадии компиляции; в Microsoft Visual C++ это достигается путем указания ключа /Ox, в других компиляторах это может быть ключ -O2 или что-то другое, описанное в справочном руководстве).
text:00401040 _main proc near text:00401040 push 267999h text:00401045 call sub_401020 text:0040104A push offset aGoodByeWorld ; "good bye, world!\n" text:0040104F call _printf text:00401054 add esp, 8 text:00401057 retn text:00401057 _main endp
Листинг 5. Доработанный листинг 2 в дизассемблере.
Теперь указатель на функцию func_B превратился в безликую константу 267999h, в которой даже самые проницательные хакеры вряд ли смогут распознать указатель! Кстати говоря, описанный трюк полезен не только в контексте подмены адреса возврата, но применим ко всем видам указателям - как на функции, так и на данные, в том числе и текстовые строки, перекрестные ссылки, которые автоматически генерируются IDA Pro и другими дизассемблерами, а по перекрестным ссылкам найти код, выводящий сообщение о неверном ключе регистрации или истечении демонстрационного строка использования - минутное дело! Если, конечно, указатели не будут зашифрованы магическим словом!