Обзор возможных методов
Условимся рассматривать универсальные методики перехвата, не требующие модификации ни подопытного файла, ни ядра и работающие под любой UNIX-подобной системой (возможно, с небольшими переделками).
Начнем с классики, то есть издалека. Один из самых популярных методов перехвата, активно используемый под windows, и называемый "методом модификации [таблицы] импорта" выглядит так:
q создаем отладочный процесс вызовом fork()/exec()/ptrace(PTRACE_TRACEME [в BSD — PT_TRACE_ME, в дальнейшем BSD-объявления будет приводится через слеш]) или подключаемся к уже запущенному процессу через ptrace(PTRACE_ATTACH/PT_ATTACH, pid, 0, 0);
q через функцию ptrace(PTRACE_PEEKTEXT/PT_READ_I, pid, addr, 0) читаем глобальную таблицу смещений (global offset table или got) — аналог таблицы импорта;
q посредством функции ptrace(PTRACE_POKETEXT/PT_WRITE_I, pid, addr, data) модифицируем указатели на нужные нам функции, заменяя их на offset thunk, где thunk — наш обработчик, внедренный в адресное пространство процесса тем или иным путем (например, при помощи той же PTRACE_POKETEXT/PT_WRITE_I);
o контроль за динамически загружаемыми библиотеками осуществляется путем перехвата функций dlopen/dlsym, экспортируемыми libdl.so.x, но фактически реализованных в libc.so.x (там они называются _dl_open/_dl_sym соответственно);
o непубличные функции самой программы и статических библиотек перехватываются путем внедрения команды jump thunk в их начало (естественно, оригинальное содержимое нужно где-то предварительно сохранить) с поиском по сигнатурам или с привязкой к фиксированным адресам;
q отсоединяемся от процесса через ptrace (PTRACE_DETACH/PT_DEATACH, pid, 0, 0), позволяя ему продолжить нормальное выполнение, но теперь уже с исправленной" глобальной таблицей смещений, вызывающей функции через наш хакерский thunk, который может протоколировать вызовы, мухлевать с аргументами или даже передавать управление подложным функциям;
Метод " модификации импорта" легко реализуется, надежен, но… не свободен от недостатков. В windows функции ReadProcessMemory/WriteProcessMemory не требуют от процесса, чтобы он находится под отладкой и подопытному приложению очень трудно им противостоять. Их linux-аналоги являются частью библиотеки ptrace, обломать которую очень легко (см. мою статью "методология защиты в мире UNIX", опубликованную в Хакере с год назад). К тому же подопытный процесс может вырваться из лап шпиона. Для этого ему достаточно породить дочерний процесс или сделать себе exec(), чтобы перезапуститься. В этом случае системный загрузчик перечитает исходный образ elf-файла с диска и все изменения в got'е будут потеряны. Чтобы этого не произошло, наш шпион должен следить за всеми потенциально опасными функциями, пускай и ценой усложнения реализации. И последнее (но самое главное) ограничение, — шпионаж носит сугубо локальный характер и может контролировать только дочерние процессы или процессы, явно переданные ему на "съедение".
А вот другой популярный прием, называемый методом "подмены библиотеки", так же позаимствованный из мира windows:
q создаем "обертку" (wrapper) вокруг интересующей нас библиотеки, экспортирующей те же самые функции, что и она;
q оригинальную библиотеку переименовываем или ложим на в другое место;
q функции-обертки определяют идентификатор вызывающего их процесса и если это действительно "их" процесс, совершают заранее запланированные действия (пишут вызов в log, подменяют аргументы или код возврата и т. д). Как определить id процесса? Это легко — ведь функции-обертки вызываются из контекста используемого их процесса и решение задачи сводится к выяснению идентификатора текущего процесса, возвращаемого функцией getpid;
q функция-обертка передает управление оригинальной функции основной библиотеки или своей собственной подложной функции;
За внешней простотой реализации такого подхода кроется целый ворох проблем. Создать обертки вокруг всех "системных" библиотек вручную практически нереально и эту работу необходимо автоматизировать, написав несложный парсер so-файлов и кодогенератор. Не обязательно генерировать готовый elf-файл — проще создать Си-программу и откомпилировать ее gcc.
"Глобальность" перехвата воздействует на все процессы и кривая "обертка" рушит ось так, что только дампы летят. Давайте не будем трогать системные библиотеки, а вместо этого изменим переменную LD_LIBRARY_PATH в окружении "подопытного" процесса. Она специально предусмотрена на тот случай, если отдельно взятому приложению требуется предоставить свой набор библиотек (статический и динамический загрузчики сначала ищут библиотеку в путях, указанных LD_LIBRARY_PATH, и только если там ее не оказывается переходят к файлу /etc/ld.so.conf, а затем к путям — /lib и /usr/lib), но если "подопытный" процесс попытается загрузить библиотеку по абсолютному пути, он сможет вырваться из-под нашего контроля!
Перспективнее всего осуществлять перехват путем прямой модификации памяти подопытного без обращения к ptrace. Как это можно сделать? Первым в голову приходит файл /proc/<pid>/mem, однако, в большинстве систем он недоступен даже root'у и приходится спускаться на уровень ядра, что сильно напрягает. Хакерские источники упоминают о двух других интересных файлах: /dev/mem
(образ физической памяти компьютера до трансляции виртуальных адресов) и /dev/kmem
(образ виртуальной памяти ядра). Файл /dev/kmem обычно с прикладного уровня недоступен и никаких библиотек прикладного уровня здесь нет, поэтому нам он совершенно неинтересен, а вот /dev/mem мы рассмотрим поподробнее.