Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #100, АПРЕЛЬ 2007 г.

Трюки от Крыса

Крис Касперски

Хакер, номер #100, стр. 130

Сегодняшний выпуск «Трюков» всецело посвящен хитроумным приемам программирования, затрудняющим дизассемблирование и отладку откомпилированной программы и, следовательно, увеличивающим ее сопротивляемость взлому, причем все это — без всякого ассемблера и других шаманских ритуалов!

Сладкая парочка setjump и longjump

Трассировка программы без захода в функции (step-over) — основной способ хакерской навигации. На его пути его реализации разработчики защитных механизмов стремятся расположить всякие подводные рифы и другие неожиданные ловушки, типа функций, никогда не возвращающих управление в точку возврата, что приводит к потере контроля над отлаживаемой программой и сильно напрягает хакера, заставляя его входить в каждую функцию, а также во все вызываемые ею функции. При большом уровне вложенности взлом растягивается на многие часы, дни, недели, месяцы, годы…

Самый простой (и легальный) способ обхода точки возврата основан на использовании стандартных Си-функций setjump и longjump. Первая запоминает состояние стека функции в переменной типа jmp_buf (включая адрес текущей выполняющейся инструкции). Вторая передает управление по этому адресу вместе с аргументом типа int, что создает безграничный простор для всякого рода трюкачества, наглядный пример которого приведен ниже:

Обход точки возврата через longjmp

#include <stdio.h>

#include <setjmp.h>

#include <stdlib.h>

jmp_buf stack_state;

A(){printf("func A()n");longjmp(stack_state, -1 );}

B(){printf("func B()n");longjmp(stack_state, -2 );}

C(){printf("func C()n");longjmp(stack_state, -3 );}

main()

{

int jmpret;

// __asm{int 03}; // для отладки

// запоминаем состояние стека

jmpret = setjmp(stack_state);

// выполняем C(), из которой мы возвращаемся в точку jmpret

if (jmpret==-3) retu 0;

// выполняем C(), из которой мы возвращаемся в точку jmpret

if (jmpret==-2) C();

// выполняем B(), из которой мы возвращаемся в точку jmpret

if (jmpret==-1) B();

// выполняем A(), из которой мы возвращаемся в точку jmpret

if (jmpret==00) A();

// эта функция никогда не получает управление

printf("good bye, world!n");

}

Откомпилируем программу и убедимся, что она последовательно вызывает функции A(), B(), C(), после чего раскомментируем строку «_asm{int 03}», откомпилируем еще раз и запустим полученный exe-файл под отладчиком OllyDbg (или любым другим), нажав <F9> (run) для достижения строки «int 03h». Начинаем трассировать программу по <F8> (step over) и… Не доходя до строки «printf("good bye, world!n");» и не успев выполнить функцию A(), отладчик неожиданно теряет контроль за подопытной программой, и она, вырвавшись из-под трассировки, благополучно завершается по retu 0, что OllyDbg и констатирует. Сказанное относится не только к OllyDbg, но также к Soft-Ice и всем остальным отладчикам.

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

Подмена адреса возврата

При трассировке step-over отладчики устанавливают программную (реже аппаратную) точку возврата за концом команды CALL func_A, куда func_A возвращает управление посредством оператора retu, стягивающего со стека адрес возврата, положенный туда процессором перед вызовом func_A. Таким образом, чтобы вырваться из-под трассировки, нам достаточно заменить подлинный адрес возврата адресом какой-нибудь другой функции (назовем ее функцией func_B), куда и будет передано управление.

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

Таким образом, нам надо просто получить адрес самого левого аргумента (что можно сделать оператором «&»). Преобразовав его к указателю на машинное слово (на x86 составляющее 32 бита и совпадающее по размеру с указателем на int), уменьшить его на единицу и… записать по этому адресу указатель на функцию func_B, куда и будет передано управление по завершении func_A. Следует помнить, что при выходе из функции func_B управление будет передано… обратно на саму функцию func_B! Почему? Да потому, что она вызвана «нечестным» способом и в стек не занесен адрес возврата. Тем не менее, func_B может спокойно вызывать остальные функции «честным» путем, ничем не рискуя.

Законченный пример приведен ниже:

Демонстрация вызова функции с подменой адреса возврата

// функция B(), которой функция A()

// скрытно передает управление

B(){printf("func B();n");exit(0);}

// явно объявляем функцию как _cdecl,

// чтобы оптимизатор «случайно» не реализовал ее как fastcall

_cdecl A(int x)

{

// подмена адреса возврата

*((((int*)&x)-1))=x;

printf("func A();n");

}

main()

{

// __asm{int 03}; // для отладки

// функция A() подменяет свой адрес возврата на B()

A((int) B);

// эта функция никогда не получает управление

printf("good bye, world!n");

}

Компилируем программу, убеждаемся, что она работает, затем раскомментируем строку «_asm{int 03}», перекомпилируем и запускаем под отладчиком Microsoft Visual Studio Debugger (или любым другим). Нажимаем <F5> (run) и затем несколько раз <F10> (step over). Отладчик входит в функцию A(), но обратно уже не возвращается, поскольку отлаживаемая программа вырывается из лап трассировщика!

Маскировка указателей

Описанный выше прием эффективен в борьбе против отладчиков, но бессилен перед дизассемблерами, поскольку при первом же взгляде на вызов функции func_A становится заметно, что ей в качестве аргумента передается адрес функции 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

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

Самое простое, что можно сделать, — перед передачей указателя на func_B наложить на него «магическое слово» операцией XOR, а перед подменой адреса возврата наложить XOR еще раз, получая исходный указатель:

Доработанный вариант, маскирующий указатель на func_B

#define MAGIC_WORLD 0x666999

*((((int*)&x)-1))=x ^ MAGIC_WORLD;

A(((int) B) ^ MAGIC_WORLD);

Компилируем программу, не забыв задействовать оптимизацию, чтобы компилятор зашифровал указатель еще на стадии компиляции; в 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

Теперь указатель на функцию func_B превратился в безликую константу 267999h, в которой даже самые проницательные хакеры навряд ли смогут распознать ее истинную сущность! Кстати говоря, описанный трюк не только полезен в контексте подмены адреса возврата, но применим ко всем видам указателей — как на функции, так и на данные, в том числе и текстовые строки, перекрестные ссылки к которым автоматически генерируются IDA Pro и другими дизассемблерами. А по перекрестным ссылкам найти код, выводящий сообщение о неверном ключе регистрации или истечении демонстрационного строка использования, — минутное дело! Если, конечно, указатели не будут зашифрованы магическим словом!

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