В очередной раз столкнувшись c необходимостью отследить создание новых процессов, я подумал: а почему бы не посмотреть на то, как это делает DTrace? В отличие от стандартного и общеизвестного алгоритма с патчем таблицы системных вызовов, который работает только с системным вызовами, DTrace позволяет перехватить что угодно. На мой взгляд, подобный функционал можно было реализовать путем добавления переходов на свой обработчик в начало перехватываемой функции, но все оказалось куда более элегантно.
Изучение DTrace стоит начать с его “эпицентра” (как утверждают авторы фрэймворка) – функции dtrace_probe. В целом, код достаточно понятен и содержит в себе большое количество комментариев. После внимательного изучения этой функции становится ясно, что искомое находится чуть дальше, в функции fbt_provide_module, которую можно найти в файле fbt.c директории bsd/dev/dtrace исходных кодов XNU. Эта функция как раз и используется для патчинга перехватываемых функций.
Итак, что же он делает в случае с 64 битной архитектурой (для 32 битной или PPC используется тот-же принцип)? Сам процесс установки перехватчика выглядит следующим образом: у каждого модуля берется таблица символов (struct nlist_64) и каждый глобальный символ, имеющий имя, сверяется со списком требуемых “проб”. При этом DTrace отказывается перехватывать ряд системных функций, например, такие как собственные функции (dtrace_*), функции, отвечающие за отладку (kdp_*, kdbg_*, kernel_debug), работу таймеров (etimer_*, clock_*, absolutetime_to_*), управление питанием (acpi_*), и ряд других. Если найденную функцию необходимо перехватить, то делается проверка того, что функция начинается со следующей последовательности команд (адрес начала функции получается из той же таблицы символов):
mov %rsp, %rbp ; 0x48, 0x89, 0xE5
Затем ищется конец функции. Если все прошло успешно, то создается “проба”, в которой сохраняется внутренняя информация, включая информацию о найденных адресах. На этом создание пробы завершается и начинается обработка следующей функции из списка.
Ну, а теперь самое интересное. После того как проба становится активной (функция fbt_enable), второй байт от начала перехватываемой функции подменяется на 0xF0. Копирование происходит по физическим адресам в памяти при помощи функции ml_nofault_copy. В результате возникает гарантировано неверная команда и, как следствие, возникновение исключения при попытке ее обработки процессором. Ну, а дальше все довольно очевидно. Возникающее исключение перехватывается DTrace, и идет обработка сработавшей “пробы” с последующим вызовом оригинальной функции.
Лично мне механизм очень понравился, но, к сожалению, при всей своей элегантности алгоритм, используемый DTrace, не подходит для использования в сторонних приложениях, т.е. воспользоваться им, конечно, можно, но ни к чему хорошему это не приведет.