Sploitus

Exploit for CVE-2012-0056

kitploit · 2026-08-25

Exploit Code

MARKDOWN239 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-PYTHONONE-CVE-2012-0056
# Linux Local Privilege Escalation via SUID /proc/pid/mem Write

## Mempodipper

Представляем Mempodipper, эксплойт для CVE-2012-0056. `/proc/_pid_ /mem` — это интерфейс для прямого чтения и записи памяти процесса путем перемещения по тем же адресам, что и виртуальное адресное пространство процесса. В версии 2.6.39 защита от несанкционированного доступа к `/proc/_pid_ /mem` была признана достаточной, и предыдущая директива `#ifdef`, запрещавшая запись в произвольную память процесса, была удалена. Любой, имеющий соответствующие разрешения, мог записывать в память процесса. Оказалось, конечно, что проверка разрешений была выполнена плохо. _Это означает, что все ядра Linux >=2.6.39 уязвимы_, вплоть до коммита с исправлением, выпущенного пару дней назад. Давайте шаг за шагом рассмотрим старый код ядра и разберемся, в чем проблема.

Когда `/proc/_pid_ /mem` открывается, вызывается этот код ядра:

root@kitploit:~
    
    
    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` процесса, с которым был открыт, и сохраняет его для последующей проверки при чтении и записи.

Однако запись (и чтение) имеют ограничения проверки разрешений. Давайте взглянем на функцию записи:

root@kitploit:~
    
    
    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`, так что вот код последней:

root@kitploit:~
    
    
    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. Взгляните на это:

root@kitploit:~
    
    
    $ 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`:

root@kitploit:~
    
    
    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 рандомизирует адресные пространства процессов, что делает невозможным узнать, куда писать. Стоит ли тратить время на изобретение более хитрых способов чтения памяти процесса с последующим поиском? Нет. Обратите внимание:

root@kitploit:~
    
    
    $ readelf -h /bin/su | grep Type
       Type:                              EXEC (Executable file) 
    

Это означает, что `su` не имеет перемещаемой секции .text (иначе выводилось бы "DYN" вместо "EXEC"). Оказывается, что `su` в подавляющем большинстве дистрибутивов _не скомпилирован сPIE_, что отключает ASLR для секции .text двоичного файла! Так что мы выбрали `su` мудро. Смещения в памяти всегда одинаковы. Чтобы найти правильное место для записи, давайте посмотрим на ассемблерный код вокруг вывода сообщения об ошибке "Unknown id: blabla".

Он получает строку ошибки здесь:

root@kitploit:~
    
    
      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:

root@kitploit:~
    
    
      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)
    

Закрывает лог:

root@kitploit:~
    
    
      4036a3:       e8 f0 eb ff ff          callq  402298 (closelog@plt)
    

И завершает программу:

root@kitploit:~
    
    
      4036a8:       bf 01 00 00 00          mov    $0x1,%edi
      4036ad:       e8 c6 ea ff ff          callq  402178 (exit@plt)
    

Поэтому мы хотим использовать `0x402178`, адрес вызываемой функции exit. В эксплойте можно автоматизировать поиск символа `exit@plt` с помощью простого однострочника bash:

root@kitploit:~
    
    
    $ 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.

В итоге эксплойт работает как часы с полной надежностью:

root@kitploit:~
    
    
    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.

_{Как всегда, эта работа носит исключительно академический характер и не предназначена для использования вне исследований и образования.}_