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

Системный шпионаж в *nix часть 1

Крис Касперски ака мыщъх

Хакер, номер #087, стр. 087-134-5


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

Маленький нюанс: под FreeBSD 4.5 (более свежие версии не проверял) read() всегда возвращает позитивный результат, даже если /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, то содержимое функции можно определить с помощью нашей программы get_addr.c. На мыщъхином компьютере первые 10h байт функции gets выглядят так: 55h 89h E5h 57h 56h 53h 83h ECh 2Ch 8Bh 75h 08h E8h ACh 4Dh FBh.

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

Нажмем <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 и ловить исключение из ядра.

Назад на стр. 087-134-4  Содержание  Вперед на стр. 087-134-6