c-tricks-1Eh

сишные трюки\\ (1Eh выпуск)

крис касперски ака мыщъх, a.k.a. souriz, a.k.a. nezumi, no-email

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

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

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

Возьмем, к примеру, вариацию на тему «hello, world» (см. листинг 1):

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); }

Листинг 1 исходный текст незащищенной программы

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

.text:00401000 _main:; CODE XREF: start+AF↓p

.text:00401000push0

.text:00401002pushoffset Caption; «do not build a house on near the»…

.text:00401007pushoffset Text; «j.a.n.g.a.n b.e.r.u.m.a.h d.i»…

.text:0040100Cpush0

.text:0040100Ecallds:MessageBoxA

.text:00401014retn

.data:00405030 ; char Text[]; DATA XREF: .text:00401007↑o

.data:00405030 Textdb '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 Captiondb 'do not bulild a house on near the beach if afraid of'

Листинг 2 дизассемблерный листинг незащищенной программы

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

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

#define _KEY_ 0x666999

main()

{

char* p1 = s1 + _KEY_;

char* p2 = s2 + _KEY_;

MessageBox(0, p1 - _KEY_, p2 - _KEY_, MB_OK);

}

Листинг 3 очевидное, но не правильное решение

Компилируем файл, загружаем его в IDA-Pro и видим (см. листинг 4):

.text:00401000 _main:; CODE XREF: start+AF↓p

.text:00401000push0

.text:00401002pushoffset Caption; «do not bulild a house on near the»…

.text:00401007pushoffset Text; «j.a.n.g.a.n b.e.r.u.m.a.h d.i»…

.text:0040100Cpush0

.text:0040100Ecallds:MessageBoxA

.text:00401014retn

Листинг 4 оптимизирующие компиляторы стремятся выполнить автоматическую де-обфускацию указателей всегда, когда это только возможно

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

Очень просто! Достаточно раскрыть ANSI C и прочитать, что трансляторы не оптимизируют статические и глобальные переменные, следовательно, для достижения полученного результата, первый проход шифрования следует осуществлять с константой, а второй — с глобальной/статической переменной.

Законченный (в смысле окончательный) пример реализации приведен ниже (см. листинг 5):

main()

{

char* p1 = s1 + _KEY_;

char* p2 = s2 + _KEY_;

static _key_ = _KEY_;

MessageBox(0, p1 - _key_, p2 - _key_, MB_OK);

}

Листинг 5 реально работающая обфускация указателей

Программа усложнилась незначительно, зато результат превзошел все ожидания:

.text:00401000 _mainproc near; CODE XREF: start+AF↓p

.text:00401000moveax, dword_4050D8; _key_

.text:00401005movecx, 0A6BA25h; указатель на s1 (зашифрованный)

.text:0040100Amovedx, 0A6B9C9h; указатель на s2 (зашифрованный)

.text:0040100Fsubecx, eax

.text:00401011push0; uType

.text:00401013subedx, eax

.text:00401015pushecx; lpCaption

.text:00401016pushedx; lpText

.text:00401017push0; hWnd

.text:00401019callds:MessageBoxA

.text:0040101Fretn

.text:0040101F _mainendp

.data:00405030db 6Ah ; j; начало строки s1

.data:00405031db 2Eh ; .

.data:00405032db 61h ; a

.data:00405033db 2Eh ; .

.data:00405034db 6Eh ; n

.data:00405035db 2Eh ; .

.data:00405036db 67h ; g

.data:0040508Cdb 64h ; d; начало строки s2

.data:0040508Ddb 6Fh ; o

.data:0040508Edb 20h

.data:0040508Fdb 6Eh ; n

.data:00405090db 6Fh ; o

.data:00405091db 74h ; t

Листинг 6 дизассемблерный код программы, шифрующей указатели на данные

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

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

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

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

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

Но довольно слов, побольше дела, в смысле кода наглядных примеров (см. листинг 7):

#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);

}

Листинг 7 обфускация указателей на функции

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

.text:00401020 _mainproc near; CODE XREF: start+AF↓p

.text:00401020moveax, dword_405050; _key_

.text:00401025movedx, 66A6B696h; указатель на s1 (зашифрованный)

.text:0040102Asubedx, eax

.text:0040102Cleaecx, [eax-6626162Eh]; указатель на s2 (зашифрованный)

.text:00401032pushecx

.text:00401033movecx, dword_40504C; ff

.text:00401039pushedx

.text:0040103Axoreax, ecx

.text:0040103Ccalleax; foo

.text:0040103Eaddesp, 8

.text:00401041retn

.data:0040504C dword_40504Cdd66267666h; зашифрованный указатель на foo

Листинг 8 убийственный результат обфускации указателей на код и данные — полный хаос!

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