Данная статья была написана в долгих и мучительных попытках реализовать подмену системного вызова sys_mkdir на ядре версии 2.6.23. Исходным шагом для нее стала работа dev0id-а о защите от исполнения в стеке ОС Linux (ссылку на статью можно найти в конце данной статьи). И как и говорил уважаемый dev0id, первоочередной задачей для нас стала подмена системных вызовов своими. Для примера (уж не знаю почему, возможно так сошлись звезды в созвездии стрельца) был выбран системный вызов sys_mkdir. Но для начала немного остановимся на том, как вообще писать модули.
Любой модуль должен обязательно состоять как минимум из двух функций: int init_module() и void cleanup_module(). Вообще данные функции можно заменить и своими, но в нашем случае мы не будем это рассматривать, как и то, что функция init_module может в себя принимать параметры как и функция main(). Я думаю, что из названия становится понятно, что функция init_module() вызывается в тот самый момент когда вы делаете /sbin/insmod, т.е. загружаете свой модуль в ядро. Ну и соответственно cleanup_module() происходит при выгрузке модуля из ядра.
Простейший же модуль выглядит так:
/*------------------------------------------------*/
#include <linux/module.h>
#include <linux/kernel.h>
/*------------------------------------------------*/
MODULE_LICENSE("GPL");
/*------------------------------------------------*/
int init_module(void)
{
printk("It`s our rules module :-)\n");
return 0;
}
/*------------------------------------------------*/
void cleanup_module(void)
{
printk("Our module was unload :-(\n");
}
В этом листинге вызывается макрос MODULE_LICENSE("GPL");, он вызывается для того, чтобы при загрузке модуля не было сообщения о неизвестной лицензии модуля. Теперь необходимо скомпилировать наш модуль, для этого создаем простой Makefile, содержащий всего одну строчку:
obj-m := simple_module.o, где simple_module нужно поменять на имя вашего исходника.
Теперь остается сделать
make -C путь_к_исходникам_ядра M=$PWD modules
У меня путь к исходникам /usr/src/kernels/2.6.23.15-80.fc7-x86_64, у вас он может быть другим.
Загрузим наш модуль в ядро командой /sbin/insmod название_модуля. Модули в ядрах 2.6.х имеют расширение .ko. На первый взгляд ничего не произошло, но это только на первый взгляд. Выполним команду dmesg | tail -n 1 для того, чтобы просмотреть сообщения ядра - как видите, наш модуль вывел «It`s our rules module :-)», а это значит у нас все получилось.
Теперь вернемся к основной нашей проблеме - когда динозавры были еще маленькие, а ядра имели версию 2.4.х., можно было без проблем вызывать любой системный вызов с помощью тогда еще экспортируемой Таблицы системных вызовов (sys_call_table). Однако в ядрах версии 2.6.х лафа закончилась и таблицу системных вызовов экспортировать перестали. Однако и ум человеческий не стоит на месте. И dev0id таки придумал, как убрать этот маленький недостаток. Дело в том, что вызов sys_close экспортируется, а так как адрес этого вызова находится как раз в таблице системных вызовов, то достаточно пробежаться в секции данных для того, чтобы найти необходимый нам адрес. Более подробно вы можете прочитать в
статье dev0da, замечу лишь, что нужно немножко переделать данную функцию для того, чтобы она заработала на х86-64, как это сделать вы можете посмотреть в прилагаемом листинге.
Однако же на этом все проблемы и душевные терзания не закончились - дело в том, что попытавшись в лоб поменять адрес системного вызова, мне надавали пощечин, сказали «Ошибка сегментирования» и ко всему прочему в ядре остался модуль, который будет продолжать висеть там до перезагрузки. В связи с этим напрашивалось всего два решения: или бросить это гиблое дело, или попробовать переписать вызов sys_mkdir на лету. Но так как времени было много, а пива еще больше, было решено проверить возможность перезаписи системных вызовов. Сохраним первый байт функции sys_mkdir, что бы восстановить его при выгрузке модуля. Как и все гениальное это очень просто.
long* sys_call_table = find_sys_call_table();
// нашли адрес sys_call_table
_sys_mkdir = *(sys_call_table + __NR_mkdir);
// взяли адрес sys_mkdir
char old_first_byte = *((char*)_sys_mkdir);
// а вот и первый байт
Итак это готово, в общем случае можно записать по этому байту что угодно, однако, чтобы не получить при вызове команды mkdir ошибку сегментирования, было решено записать туда опкод команды ret, т.е. 0хс3. Загрузив модуль ошибки сегментирования получено не было, значит писать в эту функцию можно, теперь попробуем вызвать команду mkdir. В лучшем случае мы получаем ответ о том, что пропущен аргумент, в худшем команда вроде бы исполняется, но естественно никакой директории не создает. Отлично, значит можно в эту функцию, дописать что угодно, но что угодно не лучшее решение, поэтому нужно было дописать код, который бы вызывал нашу функцию, а затем делал ret чтобы предотвратить исполнение
оставшейся части функции sys_mkdir. Сказано - сделано. Прежде всего пришлось разобраться с опкодами команд для процессоров Intel. В этом помогает статья об опкодах на wasm.ru. Значит дело осталось за малым - взять документацию по процессорам, которую Intel любезно предоставляет всем страждущим. На ассемблере код нашей новой функции выглядит так.
xor eax, eax ;(для х86-84 rax)
mov rax, адрес_нашей_функции
call rax ;вызвали нашу функцию
ret ;вернулись в функцию вызвавшую sys_mkdir
Вот и все, теперь смотрим опкоды инструкций для Интела. Для первой инструкции подходит опкод
31 /r XOR r/m32, r32 для 32 битной системы, и
REX.W + 31 /r XOR r/m64, r64 для 64 битной.
По суффиксу /r становится ясно, что необходимо задать следующим байтом оба операнда, байт этот получается 11 000 000, на wasm его называют ModR/M, не будем от этого отступать, все тонкости как он получается я не буду объяснять, так, как все это подробно описано в статье на wasm.ru. Для 64 битной же системы существует еще и префикс REX - как его получить вы можете также узнать из документации по процессорам Intel. Он просто должен быть равен 0х48. Значит для первой инструкции опкод получается 0х31 0хс0. Аналогично получаем опкод инструкции mov:
B8 +rd MOV r32, imm64 и
REX.W B8 +rd MOV r64, imm64
Так как мы используем регистр eax, то опкод инструкции так и есть 0x0b8 за которым следует адрес нашей функции. Если бы у нас был регистр ecx, то опкод был бы 0хb9, если edx то 0хba, ebx - 0xbb и т. д. Кстати, адрес нашей функции должен быть перевернут, так как в памяти байты хранятся в обратном порядке, т. е. если у вас был адрес 0х00a1b2c3d4e5f611 то он станет 0x11f6e5d4c3b2a100, от такие вот пироги.
Инструкция call:
FF /2 CALL r/m32 и
FF /2 CALL r/m64
Это говорит о том, что данная инструкция одинакова как для 32 так и для 64 битных платформ. Суффикс /2 говорит о том, что в байте ModR/M Reg должно всегда быть числом 2. Получаем Mod(11) Reg(010 = 2) R/M(000 = EAX). Получаем 0хd0. Значит опкод для обоих платформ будет 0xff 0xd0.
Ну и как я уже говорил остается ret. Её опкод 0хс3.
У нас получилось, что весь наш код занимает 16 байт для х86-64 и 10 для 32 битной платформы. Остается только сохранить эти байты для того, чтобы восстановить их после выгрузки модуля.
char old_commands[COMMANDS_COUNT];
memcpy(old_commands, (void *)sys_mkdir_ptr, COMMANDS_COUNT);
Вот вам в принципе и вся теория, позволяющая подменять системные вызовы в ядрах версии 2.6.х. Только не забывайте, что если вам понадобится использовать аргументы, которые передаются системному вызову, то взять их можно из регистров ebx — 1, ecx — 2 и т.д., а для этого функция должна быть как asmlinkage, в противном случае функция будет искать все аргументы в стеке. Ну и соответственно желательно бы, чтобы после отработки ваша функция таки вызывала оригинал, для этого достаточно перенести ее в другую область памяти. Вот и все, пора идти допивать оставшееся пиво :-).
Ссылки
Статья от dev0idа:
http://www.securitylab.ru/contest/212111.php
Статьи на wasm.ru:
http://www.wasm.ru/print.php?article=codech03
http://www.wasm.ru/print.php?article=codech04
http://www.wasm.ru/print.php?article=codech05
Документация от Intel: