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

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

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




Борьба с хакерами на высоком сишном уровне (без ассемблерных вставок!) продолжается. Сегодня мы рассмотрим технику обфускации указателей на данные и функции, продемонстрировав системно-независимые подходы — легкие в реализации, но устойчивые к взлому.

Обфускация указателей на данные

Дизассемблеры и отладчики поддерживают мощные механизмы реконструкции перекрестных ссылок, опутывающих всю программу и образующих своеобразный несущий каркас, на который уже навешивается все остальное. Перекрестные ссылки — это артерии, автомагистрали и нити, связывающие крошечные лоскуты кода воедино. Допустим, у нас есть строка «wrong serial mumber» или «trial expired».

Достаточно всего одного щелчка мыши, чтобы найти код, выводящий ее на экран. Следующий щелчок перенесет нас в материнскую функцию, осуществляющую проверку серийного номера/срока действия программы. Чтобы воспрепятствовать анализу алгоритма, достаточно ослепить механизм реконструкции перекрестных ссылок. Тогда программа распадется на ряд крошечных лоскутов, неизвестно каким образом связанных друг с другом. Возьмем, к примеру, вариацию на тему «hello, world»:

Исходный текст незащищенной программы
char s1[] = "j.a.n.g.a.n b.e.r.u.m.a.h d.i t.e.p.i
p.a.n.t.a.i j.i.k.a";
char s2[] = "do not bulild a house on near the beach
if afraid of being hit by waves";
main() { MessageBox(0, s1, s2, MB_OK); }

А теперь посмотрим, как выглядит ее дизассемблерный листинг, сгенерированный IDA Pro:

Дизассемблерный листинг незащищенной программы
.text:00401000 _main:
.text:00401000 push 0
.text:00401002 push offset Caption ; "do not build a house on near
the"...
.text:00401007 push offset Text ; "j.a.n.g.a.n b.e.r.u.m.a.h
d.i"...
.text:0040100C push 0
.text:0040100E call ds:MessageBoxA
.text:00401014 retn
...
.data:00405030 ; char Text[]
; DATA XREF: .text:00401007
.data:00405030 Text db 'j.a.n.g.a.n b.e.r.u.m.a.h d.i t.e.p.i
p.a.n.t.a.i'
.data:00405030
.data:0040508C ; char Caption[]
; DATA XREF: .text:00401002?o
.data:0040508C Caption db 'do not bulild a house
on near the beach if afraid of'

Как мы видим, IDA Pro автоматически реконструировала перекрестные ссылки на строки, упростив анализ программы до предела. Как этому помешать? Во-первых, мы должны предотвратить попадание незашифрованных указателей в код, сгенерированный компилятором. А во-вторых, — расшифровать указатели в манере, которую не поддерживают ни IDA Pro, ни популярные отладчики. Первая фаза решается тривиально. Оптимизирующие компиляторы поддерживают ряд математических операций (типа сложения и вычитания), вычисляя их еще на стадии трансляции. В результате, в код попадают зашифрованные указатели. Демонстрируется это в следующем примере:

Очевидное, но неправильное решение
#define _KEY_ 0x666999
main()
{
char* p1 = s1 + _KEY_;
char* p2 = s2 + _KEY_;
MessageBox(0, p1 — _KEY_, p2 — _KEY_, MB_OK);
}

Компилируем файл, загружаем его в IDA Pro и видим:

Оптимизирующие компиляторы стремятся выполнить автоматическую деобфускацию указателей всегда, когда только возможно
.text:00401000 _main:
; CODE XREF: start+AF
.text:00401000 push 0
.text:00401002 push offset Caption
; "do not bulild a house on near the"...
.text:00401007 push offset Text ; "j.a.n.g.a.n b.e.r.u.m.a.h
d.i"...
.text:0040100C push 0
.text:0040100E call ds:MessageBoxA
.text:00401014 retn

Вот так сюрприз! А где же наши зашифрованные указатели?! Программа осталась в первозданном виде. Оказывается, оптимизирующий компилятор, вычисливший значение «s1 + _KEY_» на стадии трансляции, вычислил и значение «s1 - _KEY_», автоматически расшифровав указатель s1. Как запретить это компилятору — причем, не какому-то одному отдельно взятому, а всем оптимизаторам сразу? Очень просто: достаточно раскрыть ANSI C и прочитать, что трансляторы не оптимизируют статические и глобальные переменные. Следовательно, для достижения полученного результата, первый проход шифрования надо осуществлять с константой, а второй — с глобальной/статической переменной. Законченный (в смысле, окончательный) пример реализации приведен ниже:

Реально работающая обфускация указателей
main()
{
char* p1 = s1 + _KEY_;
char* p2 = s2 + _KEY_;
static _key_ = _KEY_;
MessageBox(0, p1 — _key_, p2 — _key_, MB_OK);
}

Программа незначительно усложнилась, зато результат превзошел все ожидания (дизассемблерный листинг ты можешь посмотреть в исходнике на диске). Получается, IDA Pro не только не реконструировала перекрестные ссылки, но и распознала указатели на s1 и s2, оставив их в зашифрованном виде. И хотя расшифровать значение указателя вполне возможно (достаточно проанализировать дизассемблерный код), — на это уходит время и, кроме того, все средства для постройки графов тушатся на корню. Достигается все — без применения ассемблерных вставок и прочих нестандартных извращений!

Обфускация указателей на функции

Зашифровать указатели на функции намного сложнее, поскольку оптимизаторы не поддерживают математических преобразований над ними. Почему? А потому, что их не поддерживает стандарт. Тот самый, позволяющий законными средствами получить указатель на функцию, но сужающий меню доступных действий только до присвоения нуля — естественно, это не входит в наши планы. Запрет на математические преобразования легко обходится кастингом. В частности, 32?битные операционные системы (Windows 9x/NT, Linux, FreeBSD) используют плоскую модель адресного пространства и 32?битные указатели на код, которыми можно оперировать так же, как и целочисленным типом DWORD (unsigned int).

В других случаях разрядность указателя может отличаться от обозначенной. Более того, он вообще может представлять собой сложную структуру, состоящую из селектора и смещения, а потому кастинг — уже хак. Но этот хак работает! Главное, вынести физический тип указателя на код в отдельный define, зависящий от платформы. Кастинг снимает защиту на математические операции с указателями на функции, но все преобразования выполняются на стадии выполнения программы, а вовсе не на стадии компиляции. В итоге, указатели «благополучно» переживают оптимизацию, попадая в машинный код целевого файла, где их распознает IDA Pro вместе с отладчиками. Проблема кажется неразрешимой, но… кто нам мешает доработать откомпилированный код уже после трансляции, зашифровав указатели непосредственно в двоичном файле и выполняя расшифровку уже в самой программе? Перед нами встает проблема поиска указателей в откомпилированном коде.

Каково же будет наше решение? Самое простое — загнать указатели в структуру, предваренную специальным маркером — текстовой строкой или константой с уникальным содержимым. После чего нам остается только найти этот маркер в программе и зашифровать следующие за ним указатели. Это можно сделать как вручную в HIEW’е, так и автоматически, с помощью несложной программы. Но довольно слов, больше дела:

Обфускация указателей на функции
#define p DWORD
#define _KEY_ 0x66666666
baz(char* s1, char* s2){ MessageBox(0, s1, s2, MB_OK); }
struct FF
{
p am; // <<-- marker
p f1; // <<-- list of func. pointers
} ff = { 0xEFBEADDE, (p) &baz};
main()
{
char* p1 = s1 + _KEY_;
char* p2 = s2 + _KEY_;
static _key_ = _KEY_;
int (*foo)(char*, char*);
foo = (int (*)(char*, char*)) (ff.f1 ^ pk);
foo((char*) p1 — pk, (char*) p2 + pk);
}

После компиляции программы мы должны найти в исполняемом файле «магическую» последовательность 0xDEADBEEF, наложив на следующее за ней двойное слово ключ шифрования 0x66666666 по XOR. Убедившись, что все выполнено правильно и программа работает, а не падает, загружаем ее в дизассемблер (смотри листинг из исходника на диске) и видим убийственный результат обфускации указателей на код и данные.

Полный хаос и теперь сам черт не разберет, что это за код и какого он там делает! Да, конечно, при прогоне программы под отладчиком (или плагином-эмулятором для IDA Pro) хакер узнает значение регистра EAX, определив, какая функция тут вызывается. Но… наглядность дизассемблерного листинга необратимо утеряна. Механизмы реконструкции потока управления тихо курят в сторонке, высаживая хакера на измену и увеличивая время анализа программы на порядок-другой.

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