Перехват библиотечных функций в linux и bsd

/Dev/mem


Чтение документации ("man mem") показывает, что файл /dev/mem имеется практически на всех UNIX-подобных системах, а если его вдруг нет, он может быть создан в любой момент следующими командами:

mknod -m 660 /dev/mem c 1 1

chown root:kmem /dev/mem

Листинг 2 создание файла-устройства /dev/mem, предоставляющего доступ к физической памяти компьютера с прикладного уровня

Здесь: 660 – права доступа, /dev/mem – имя файла (может быть любым, например /home/kpnc/nezumi), "c" – тип устройства (символьное устройство), "1 1" – устройство (физическая память). Файл /dev/mem (или как вы его назовете) свободно доступен с прикладного уровня, но только для root, что есть саксь.

Структура файла предельно проста — линейные смещения соответствуют физическим адресам. Допустим, нам известно, что по адресу FFFFh:FFF0h во всех BIOS'ах хранится команда перехода на загрузочный код, а за ним (как правило) лежит дата создания прошивки. Переводим "сладкую парочку" сегмент:смещение в линейный вид (linear == seg*10h + offset), получаем FFFF0h. Это и будет смещение в файле.

Рисунок 2 чтение содержимого BIOS'а через файл /dev/mem

Основной камень преткновения в том, что оперативная память компьютера используется UNIX'ом как кэш и потому один и те же физические страницы в различное время могут соответствовать различным виртуальным адресам. Но это не проблема. Можно найти каталог страниц и выполнить трансляцию вручную. Указатель на текущий каталог храниться в регистре CR3, попытка доступа к которому с прикладного уровня возбуждает исключение, но поскольку каталог имеет довольно характерную структуру (описанную в документации на процессор), его легко найти простым сканированием физической памяти.

Часть виртуальных страниц, принадлежащих процессу, может быть выгружена на диск и тогда в файле /dev/mem ее не окажется. При хроническом недостатке оперативной памяти, значительный процент адресного пространства процесса попадает на диск и хотя совместно используемые библиотеки вытесняются в последнюю очередь, они все-таки вытесняются (особенно редко используемые функции). Это значит, что прежде чем ковыряться в /dev/mem необходимо загрузить соответствующую функцию в память. А как это сделать? Ну, например, просто вызывать ее. Вызов функции не гарантирует загрузки всех принадлежащих ей страниц, но нам этого и не надо! Достаточно, чтобы загрузилась первая страница (а она наверняка загрузится!), чтобы воткнуть в начало функции jump на свой thunk.


Для нейтрализации побочных эффектов обычно функцию вызывают с инвалидными аргументами, чтобы она завершилась без выполнения, однако, это грязный трюк, на который ведутся далеко не все функции. В частности, gets упорно ожидает ввода с клавиатуры даже если в качестве указателя ей передать нуль. Если память, принадлежащая функции, доступна на чтение (а в linux/BSD она доступна), нам достаточно просто прочитать несколько байт от начала функции — это гарантированно загрузит принадлежащую ей страницу в физическую память. Собственно говоря, для поиска перехватываемой функции в /dev/mem нам все равно потребуется ее сигнатура, так что без чтения здесь не обойтись.

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



#include <dlfcn.h>

int a; unsigned char *x;

x = (unsigned char*) gets;

printf("\nx = gets:%08Xh",x);

for(a=0;a<0x60;a++) printf("%02X ",x[a],(a%0x10)?"":printf("\n%08Xh:",(a+x)));

x = dlopen("libc.so.6",RTLD_NOW);

printf("\n\nbase libc.so.6:%08Xh",x);

for(a=0;a<0x60;a++) printf("%02X ",x[a],(a%0x10)?"":printf("\n%08Xh:",(a+x)));

x= dlsym(x,"gets");

printf("\n\nglsym(,\"gets\"):%08Xh",x);

for(a=0;a<0x60;a++) printf("%02X ",x[a],(a%0x10)?"":printf("\n%08Xh:",(a+x)));

printf("\n");

Листинг 3 программа get_addr.c, определяющая адрес функции gets

Компилируем ("gcc get_addr.c -o get_addr -ldl") и запускаем полученный файл на выполнение (ключ -ldl подключает библиотеку dl, экспортирующую функции dlopen и dlsym). На мыщъхином компьютере результат выглядит так:

x = gets:08048364h

08048364h:FF 25 A8 98 04 08 68 08 00 00 00 E9 D0 FF FF FF



08048374h: FF 25 AC 98 04 08 68 10 00 00 00 E9 C0 FF FF FF

08048384h:FF 25 B0 98 04 08 68 18 00 00 00 E9 B0 FF FF FF

08048394h:FF 25 B4 98 04 08 68 20 00 00 00 E9 A0 FF FF FF

080483A4h:00 00 00 00 00 00 00 00 00 00 00 00 31 ED 5E 89

080483B4h:E1 83 E4 F0 50 54 52 68 90 86 04 08 68 30 86 04

base libc.so.6:400179E8h

400179E8h:00 C0 02 40 D8 79 01 40 30 B5 15 40 6C 66 01 40

400179F8h:88 77 01 40 10 7C 01 40 00 00 00 00 30 B5 15 40

40017A08h:80 B5 15 40 78 B5 15 40 50 B5 15 40 58 B5 15 40

40017A18h:60 B5 15 40 00 00 00 00 00 00 00 00 00 00 00 00

40017A28h:68 B5 15 40 70 B5 15 40 40 B5 15 40 48 B5 15 40

40017A38h:38 B5 15 40 00 00 00 00 00 00 00 00 98 B5 15 40

glsym(,"gets"):4008CE60h

4008CE60h:55 89 E5 57 56 53 83 EC 2C 8B 75 08 E8 AC 4D FB

4008CE70h:FF 81 C3 AF E7 0C 00 C7 45 E0 00 00 00 00 8B 93

4008CE80h:5C 9C FF FF 0F B7 02 25 00 80 FF FF 66 85 C0 75

4008CE90h:0E 8B 83 34 02 00 00 85 C0 0F 85 07 01 00 00 0F

4008CEA0h:B7 02 25 00 80 FF FF 66 85 C0 0F 84 E3 00 00 00

4008CEB0h:8B 42 04 3B 42 08 0F 83 C8 00 00 00 0F B6 08 40

Листинг 4 результат работы программы get_addr

Указатель на функцию gets, судя по адресу (08048364h), смотрит на секцию .plt, то есть находится во "владениях" текущего процесса. Первые байты функции равны FFh 25h A8h 98h 04h 08h, что соответствует команде JMP [080498A8h]. Выходит, это еще не сама функция, а только переходник к ней! Адрес 080498A8h хранится в двойном слове лежащим в глобальной таблице смещений (got). Модификация plt/got обеспечивает перехват функции лишь в пределах текущего процесса, что с одной стороны очень даже хорошо, но с другой — весьма хреново.

Базовый адрес библиотеки libc (400179E8h) лежит в непосредственной близости от истинного адреса функции gets (4009CE60h), возвращаемым dlsym. В глаза сразу же бросается классический пролог 55h/89h E5h (PUSH EBP/MOV EBP,ESP), который мы и будем патчить для перехвата, но сперва разберемся как работать с /dev/mem.





Рисунок 3  Midnight Commander не может просмотреть файл /dev/mem

/dev/mem это необычный файл. Если в Midnight Commander'е подвести к нему курсор и нажать <F3> — ни хрена не выйдет (и не войдет)! Тоже самое произойдет если попытаться просмотреть другой файл, представляющий устройство /dev/mem (например, ранее созданный нами /home/kpnc/nezumi). Некоторые источники утверждают, что функция fopen обламывается с открытием /dev/mem и нужно юзать низкоуровневые функции операционной системы: open/read/write. Мыщъх проверил: на KNOPPIX (основан на Debian) и FreeBSD функции fopen/fread/frwite работают нормально, но, возможно, на других системах они ведут себя не так, поэтому не будем высаживаться и сделаем как говорят.

Маленький нюанс — под 4.5 BSD (более свежие версии не проверял) read всегда возвращает позитивный результат даже если море, тьфу, /dev/mem уже кончился, поэтому закладываться на возвращаемое ее значение нельзя.

#include <fcntl.h>

#define PAGE_SIZE    0x1000

int fd;

char buf_page[PAGE_SIZE];

// открываем /dev/mem на чтение и запись

// (подробнее см. "man 2 open")

if ((fd=open("/dev/mem", O_RDWR, 0))==-1) return printf("/dev/mem open error\n");

// перемещаемся в начало (необяз.)

if (lseek(fd, 0, SEEK_SET) == -1) return printf("/dev/mem seek error\n");

// читаем 0x1000 байт в буфер

if (read(fd, buf_page, 0x1000) != 0x1000) return printf("/dev/mem read error\n");

Листинг 5 фрагмент программы, демонстрирующей работу с файлом /dev/mem

Кстати говоря, консольный шестнадцатеричный редактор hexedit из комплекта поставки KNOPPIX показывает /dev/mem вполне нормально, а вот графическая версия khexedit, позаимствованная оттуда же, не показывает ни хрена.



Рисунок 4 редактор khexedit не может просмотреть файл /dev/mem

А давайте проведем небольшой эксперимент! Экспериментирование — это основное занятие мыщъх'ей (после ганжа, конечно). Возьмем какую-нибудь редко используемую библиотечную функцию (например, gets) и отпатчим ее по полной программе, внедрив в начало байт C3h, соответствующей машинной инструкции RETN, а потом вызовем ее и посмотрим получилось ли у нас или нет.



Запускаем IDA PRO, загружаем libc.so.6, переходим к функции gets (<Ctrl-G>, "gets", <ENTER>) и смотрим какие байты расположены в начале функции (чтобы IDA PRO отображала машинный код рядом с инструкциями необходимо в меню Options выбрать пункт "Text representation" и в поле "Number of opcode bytes" поставить "7"). Если нет IDA PRO, содержимое функции можно определить с помощью нашей программы, приведенной в листинге 3. На мыщъхином компьютере первые 10h байт функции gets выглядят так: 55h 89h E5h 57h 56h 53h 83h ECh 2Ch 8Bh 75h 08h E8h ACh 4Dh FBh.



Рисунок 5  функция gets в дизассемблере IDA PRO

Открываем /dev/mem в hexeditor'е ("$hexedit /dev/mem"), давим <Ctrl-S> (search) и вводим эту последовательность без суффикса 'h' и без пробелов: "5589E557565383EC2C8B7508E8AC4DFB". Редактор подумает немного и выдаст результат. У мыщъх'а функция gets обнажилась в памяти по адресу 6BA8E60h. Это физический адрес и он непостоянен. Данная страница может многократно вытесняться из памяти и загружаться по совершенно другим адресам.



Рисунок 6 редактор hexedit нашел функцию gets по ее сигнатуре

Нажмем <Ctrl-S> еще раз, чтобы убедиться, что данное вхождение — единственное. Если искомая последовательность присутствует в памяти по нескольким адресам, это значит, что либо произошла коллизия (совпадение с другой функций) и тогда искомую последовательность необходимо удлинить еще на несколько байт, либо в память загружено несколько библиотек, содержащих одну и ту же реализацию функции gets (или библиотека, экспортирующая gets, попала в дисковый кэш) и тогда нам нужно обратить внимание на младшие 3 байта: у нашей функции физические и виртуальные адреса будут равны, поскольку, адрес начала страницы всегда кратен 1000h. Если же ни одного вхождения не найдено — функция gets отсутствует в памяти (не загружена библиотека или страница вытеснена на диск) и тогда мы будем должны ее загрузить. Другая причина — искомая последовательность пересекла границу страницы памяти, а, как мы уже говорили, порядок следования физических страниц не совпадает с виртуальным. В данном случае, все хорошо: между началом gets и концом физической страницы расположено PAGE_SIZE ? (addres_of_func % PAGE_SIZE) = 1000h – (4008CE60h%0x1000) == 1A0h байт, что более, чем достаточно для поиска, но чтобы мы стали делать, если бы эта дистанция равнялась всего нескольким байтам?! А ничего — просто искали бы функцию в памяти не с начала самой функции, а с начала принадлежащей ей страницы, то есть: if (!memcmp(dlsym(lib_name, func_name) & 0xFFFFF000, buf_page)). В этом случае нам достаточно, чтобы между началом функции и концом страницы было всего 5 байт, необходимых для внедрения команды jump thunk. А если этих байт нет? Тогда необходимо либо искать следующую страницу и внедряться в середину функции (но это самый тяжкий вариант и здесь он не рассматривается), либо ставить в начало функции CCh и ловить исключение из ядра (см. врезку "проблемы стабильности").



Короче говоря, все проблемы решаемы, так что не будет высаживаться, а лучше подготовим тестовую программу, которая будем вызывать функцию gets. Один из вариантов реализации выглядит так:

char buf[666];

while(strcmp(buf,"exit")) printf(".",gets(buf));

Листинг 6 тестовая программа demo.c, используемая для экспериментов с функций gets

Не выходя из hex-редактора, откомпилируем ее ("gcc demo.c -o demo") и запустим на выполнение ("./demo"). Программа выполняется как и положено — ожидает ввода с клавиатуры и выходит по "exit". А вот как мы ее хакнем! Изменяем первый байт функции на C3h, сохраняем изменения по <F2> и запускаем ./demo еще раз. На этот раз, функция gets немедленно возвращает управления не обращая никакого внимания на клавиатуру и экран заполняется стройными рядами точек. Ура! У нас получилось!



Рисунок 7 это не звездное небо и не матрица, это — результат успешного хака функции gets под FreeBSD

Модификация gets воздействует как на уже запущенные, так и на в последствии запускаемые процессы, причем процессу очень сложно обнаружить, что его хакнули! (примечание: если gets уже находится в ожидании ввода, то замена 55h на С3h не приводит к немедленному выходу из функции и результат будет заметен только при ее следующем вызове).

Возникает вопрос: насколько это надежно? Что произойдет, если модифицированная страница в результате нехватки памяти отправиться в изгнание или какой-нибудь процесс попытаться загрузить хакнутую библиотеку еще раз? Гарантирует ли операционная система непротиворечивость ситуации? Документация (см. "man mem") не дает ответа на поставленный вопрос и это правильно, поскольку модификация страниц отслеживается не самой операционной системой, а процессором. При любом записи в страниц (не важно каким путем она произошла — хоть инструкцией mov, хоть через /dev/mem), процессор устанавливает dirty-флаг, сообщая операционной системе, что при вытеснении страницы ее следует сбрасывать на диск, что операционная система и делает. Специального обработчика для поддержки "хакнутых" страниц нет, они обрабатываются так же как остальные (то есть, так как нам надо). Никакой процесс не может "перезагрузить" хакнутую библиотеку, поскольку операционная система не видит никакой необходимости считывать с медленного диска то, что уже находится в оперативной памяти. Теоретически, если все процессы выгрузят модифицированную библиотеку, спустя какое-то время операционная система действительно выбросит ее из памяти и при повторной загрузке начнет дрыгать диском. Тогда наш хак пойдет лесом, но это крайне маловероятная ситуация, которой к тому же можно противостоять путем перехвата dlclose.



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

#include <stdio.h>

#include <fcntl.h>

#include <dlfcn.h>

main(int c, char **v)

{

       #define PAGE_SIZE    0x1000        // размер страницы (не менять!)

       #define MIN_SG_SIZE  0x10          // размер сигнатуры для поиска

       #define LIB_NAME     "libc.so.6"   // перехватываемая библиотека по умолчан.

       #define FNC_NAME     "gets"        // перехватываемая функция по умолчанию

       #define MAX_MEM      (512*1024/4)  // макс. размер физ. памяти (для BSD)

      

       int a, fd;

       unsigned char *p;

       int f=0; char *p_lib, *p_fnc;

       int fuck_a; char fuck[]="-\\|/";

       unsigned char page_buf[PAGE_SIZE];

      

       // определяем

что падчить

       if (c<3) p_lib=LIB_NAME,p_fnc=FNC_NAME; else p_lib=v[1],p_fnc=v[2];

       printf("patch %s::%s\n",p_lib,p_fnc);

      

       // определяем адрес функции для патча

       p = dlopen(p_lib,RTLD_NOW);if (!p) return printf("%s not found\n",p_lib);

       p = dlsym(p,p_fnc); if (!p) return printf("%s not found\n",p_fnc);

      

       // вычисляем расстояние до конца страницы

       if (((unsigned int)p)%PAGE_SIZE < MIN_SG_SIZE)

              return printf("can't find func! too close to end of the page!\n"\

                           "decrease MIN_SG_SIZE and try again!\n");

      

       // открываем /dev/mem

       if ((fd=open("/dev/mem",O_RDWR,0))==-1) return printf("/dev/mem open error\n");

      

       // перемещаемся в начало (необяз.)

       if (lseek(fd, 0, SEEK_SET) == -1) return -1;

      

       // ищем в /dev/mem нашу функцию

       while(fuck_a<MAX_MEM)

       {

              // читаем по одной странице пока не дойдем до конца



              if (read(fd, page_buf, PAGE_SIZE) != PAGE_SIZE) break;

             

              // крутим жопой, создавая видимость бурной деятельности

              printf(":%c\r",fuck[(++fuck_a)&3]);

             

              // сравниваем содежимое физ. памяти с первыми MIN_SG_SIZE байтами ф-ции

              if (!memcmp(&page_buf[((unsigned int)p)%PAGE_SIZE],p,MIN_SG_SIZE))

                     printf("%s find at %08Xh\n",p_fnc,

                     (a=lseek(fd,0,SEEK_CUR))-((unsigned int)p)%PAGE_SIZE,f++);

       }

      

       // выход, если найдено 2 или более вхождений

       if (--f) return printf("-err:don't know which page i have to fix\n"\

                           "increse MIN_SG_SIZE and try again!\n");

      

       // выход не найдено ни одного вхождения

       if (f) return printf("-not find\nincrease physical memory and try again\n");

      

       // ПАТЧИМ

       //-------------------------------------------------------------------------

       printf("OK\n\nbefore patch:\n");

      

       // перемещаемся на странцу назад для чтения буфера

       if (lseek(fd,a-PAGE_SIZE,SEEK_SET)==-1) return -1;

       if (read(fd,page_buf,PAGE_SIZE) != PAGE_SIZE) return -1;

      

       // печатаем оригинальное содержимое

       for (f=0;f<0x10;f++) printf("%02X ",page_buf[((unsigned int)p)%PAGE_SIZE+f]);

      

       // ставим C3h (ret)

       if (page_buf[((unsigned int)p)%PAGE_SIZE]==0xC3)

              page_buf[((unsigned int)p)%PAGE_SIZE] = 0x55;

       else

              page_buf[((unsigned int)p)%PAGE_SIZE] = 0xC3;

      

       // перемещаемся на странцу назад для записи буфера

       if (lseek(fd,a-PAGE_SIZE,SEEK_SET)==-1) return -1;

       if (write(fd,page_buf,PAGE_SIZE) != PAGE_SIZE) return -1;

      

       // печатаем отпаченное содержимое

       printf("\n\nafter patch:\n");

       if (lseek(fd,a-PAGE_SIZE,SEEK_SET)==-1) return -1;



       if (read(fd,page_buf,PAGE_SIZE) != PAGE_SIZE) return -1;

      

       for (f=0;f<0x10;f++) printf("%02X ",page_buf[((unsigned int)p)%PAGE_SIZE+f]);

       printf("\n\n");

}

Листинг 7 автоматический перехватчик mem.c, внедряющий в начало функции gets команду retn

Несколько замечаний к программе: для уменьшения количества коллизий и ускорения поиска, сравнение ведется с привязкой к смещению внутри страницы (за это отвечает конструкция page_buf[((unsigned int)p)%PAGE_SIZE]). Загрузка страниц в память происходит автоматически за счет работы memcmp. Специально заботиться об этом не надо. Программа обрабатывает ситуации, когда искомая последовательность встречается в памяти более одного раза или "разрезается" страницей напополам. В этом случае, она советует увеличить (уменьшить) константу MIN_SG_SIZE, отвечающую за размер сигнатуры, и повторить попытку еще раз. Естественно, в автономном коде (например, коде червя) необходимо организовать дополнительный цикл, который здесь не показан, чтобы не захламлять листинг второстепенными деталями.

Компилируем программу ("gcc mem.c -O2 -o mem -ldl") и запускаем ее на выполнение. Первый ключ командой строки — имя библиотеки, второй — имя функции, которую нужно хакнуть. При запуске без аргументов хачится функция gets из библиотеки libc.so.6. Если в начале функции уже стоит C3h, программа пытается восстановить стандартный пролог и пишет 55h, то есть работает как триггер (попытка хака функции с нестандартным прологом закончиться плачевно).


Содержание раздела