codeblog code is freedom — patching my itch

January 22, 2012

fixing vulnerabilities with systemtap

Filed under: Blogging,Debian,Security,Ubuntu,Ubuntu-Server,Vulnerabilities — kees @ 3:22 pm

Recently the upstream Linux kernel released a fix for a serious security vulnerability (CVE-2012-0056) without coordinating with Linux distributions, leaving a window of vulnerability open for end users. Luckily:

  • it is only a serious issue in 2.6.39 and later (e.g. Ubuntu 11.10 Oneiric)
  • it is “only” local
  • it requires execute access to a setuid program that generates output

Still, it’s a cross-architecture local root escalation on most common installations. Don’t stop reading just because you don’t have a local user base — attackers can use this to elevate privileges from your user, or from the web server’s user, etc.

Since there is now a nearly-complete walk-through, the urgency for fixing this is higher. While you’re waiting for your distribution’s kernel update, you can use systemtap to change your kernel’s running behavior. RedHat suggested this, and here’s how to do it in Debian and Ubuntu:

  • Download the “am I vulnerable?” tool, either from RedHat (above), or a more correct version from Brad Spengler.
  • Check if you’re vulnerable:
    $ make correct_proc_mem_reproducer
    ...
    $ ./correct_proc_mem_reproducer
    vulnerable
    
  • Install the kernel debugging symbols (this is big — over 2G installed on Ubuntu) and systemtap:
    • Debian:
      # apt-get install -y systemtap linux-image-$(uname -r)-dbg
      
    • Ubuntu:
      • Add the debug package repository and key for your Ubuntu release:
        $ sudo apt-get install -y lsb-release
        $ echo "deb http://ddebs.ubuntu.com/ $(lsb_release -cs) main restricted universe multiverse" | \
              sudo tee -a /etc/apt/sources.list.d/ddebs.list
        $ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys ECDCAD72428D7C01
        $ sudo apt-get update
        
      • (This step does not work since the repository metadata isn’t updating correctly at the moment — see the next step for how to do this manually.) Install the debug symbols for the kernel and install systemtap:
        sudo apt-get install -y systemtap linux-image-$(uname -r)-dbgsym
        
      • (Manual version of the above, skip if the above works for you. Note that this has no integrity checking, etc.)
        $ sudo apt-get install -y systemtap dpkg-dev
        $ wget http://ddebs.ubuntu.com/pool/main/l/linux/$(dpkg -l linux-image-$(uname -r) | grep ^ii | awk '{print $2 "-dbgsym_" $3}' | tail -n1)_$(dpkg-architecture -qDEB_HOST_ARCH).ddeb
        $ sudo dpkg -i linux-image-$(uname -r)-dbgsym.ddeb
        
  • Create a systemtap script to block the mem_write function, and install it:
    $ cat > proc-pid-mem.stp <<'EOM'
    probe kernel.function("mem_write@fs/proc/base.c").call {
            $count = 0
    }
    EOM
    $ sudo stap -Fg proc-pid-mem.stp
    
  • Check that you’re no longer vulnerable (until the next reboot):
    $ ./correct_proc_mem_reproducer
    not vulnerable
    

In this case, the systemtap script is changing the argument containing the size of the write to zero bytes ($count = 0), which effectively closes this vulnerability.

UPDATE: here’s a systemtap script from Soren that doesn’t require the full debug symbols. Sneaky, put can be rather slow since it hooks all writes in the system. :)

© 2012, Kees Cook. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 License.
CC BY-SA 4.0

6 Comments

  1. That’s very neat.

    Comment by Vadim P. — January 22, 2012 @ 9:10 pm

  2. PoC is public right now…

    Comment by Luca Bruno — January 22, 2012 @ 11:08 pm

  3. http://hunger.hu/really_correct_proc_mem_reproducer.c

    modified since on 32-bit architectures both the original and spender’s
    reproducer had reported “not vulnerable” wrongly because of a bad cast
    and incorrect return value checking… ;)

    Comment by Hunger — January 23, 2012 @ 5:15 am

  4. A little addendum:
    Instead of compiling and running the systemtap-module each time, you can use staprun:
    1. Step: give a module a (persistent) name:
    stap -g -m cve_2012_0056 CVE-2012-0056.stp -p4
    (the options mean: -g Guru-mode, to allow writing to the count variable, -m cve_2012_0056 name of the module, -p4 stop after step 4 (creating the module, instead of step 5, running it)
    now a file cve_2012_0056.ko is created.
    2. Step: copy it to a proper place:
    mkdir /lib/modules/$(uname -r)/systemtap
    cp cve_2012_0056.ko /lib/modules/$(uname -r)/systemtap
    3. Step: run it:
    staprun -L cve_2012_0056
    (this can be done by any user in the group stapusr, or root)

    What is gained by using this? After creating the module in step 1, the debuginfos are no longer necessary:
    tim@host:~/src/CVE$ ./correct_proc_mem_reproducer
    vulnerable
    tim@host:~/src/CVE$ staprun -L cve_2012_0056

    Disconnecting from systemtap module.
    To reconnect, type “staprun -A cve_2012_0056”
    tim@host:~/src/CVE$ ./correct_proc_mem_reproducer
    not vulnerable
    tim@host:~/src/CVE$ dpkg-query -W -f=’${Package}\t${Status}\n’ linux-image-$(uname -r)-dbg
    linux-image-3.2.0-1-amd64-dbg unknown ok not-installed

    (of course you would normally NOT run the script with your normal user account, this is for demonstration only…..)
    If I understand systemtap right, you could also transfer the module to other hosts running the same kernel. So this module I created should work for all hosts running linux-image-3.2.0-1-amd64, version 3.2.1-1 (current unstable).

    Comment by Tim — January 23, 2012 @ 5:16 pm

  5. another remark:
    to let the call to write fail instead of returning “0 bytes written”, I added another probe to the systemtap-script:

    probe kernel.function(“mem_write@fs/proc/base.c”).return {
    $return = -1
    }

    With this the exploit-program exits/fails. Without the return-probe, the forked su enters an infinite loop ( “Please write these 88 bytes” -> “I successfully wrote 0 bytes” -> “okay, then write these remaining 88 bytes” -> and so on….)

    I found this, because I wanted to log these calls and added a “println” to the call-probe:

    root@host:~# cat logit.stp
    probe kernel.function(“mem_write@fs/proc/base.c”).call {
    println(“write attempt: ” . sprint($count) . ” bytes (“. user_string_quoted($buf) . “) at offset ” . sprint(kernel_long($ppos)))
    $count = 0
    }

    root@host:~# stap -g logit.stp
    […..]
    write attempt: 88 bytes (“Unbekannte ID: H1\377\260i1705H1\377\260j1705H1\366@\26717@\26602\260!”…) at offset 4202665
    write attempt: 88 bytes (“Unbekannte ID: H1\377\260i1705H1\377\260j1705H1\366@\26717@\26602\260!”…) at offset 4202665
    write attempt: 88 bytes (“Unbekannte ID: H1\377\260i1705H1\377\260j1705H1\366@\26717@\26602\260!”…) at offset 4202665
    write attempt: 88 bytes (“Unbekannte ID: H1\377\260i1705H1\377\260j1705H1\366@\26717@\26602\260!”…) at offset 4202665
    WARNING: Number of errors: 1, skipped probes: 0
    WARNING: There were 38056 transport failures.
    Warning: /usr/bin/staprun exited with status: 1
    Pass 5: run failed. Try again with another ‘–vp 00001’ option.

    so my “final” version of the systemtap-script has rate-limiting on the print-line:

    global last = 0;
    probe kernel.function(“mem_write@fs/proc/base.c”).call {
    now = jiffies()
    if((now HZ() )) {
    println(“write attempt: ” . sprint($count) . ” bytes (“. user_string_n_quoted($buf,$count) . “) at offset ” . sprint(kernel_long($ppos)))
    last = now
    }
    $count = 0
    }
    probe kernel.function(“mem_write@fs/proc/base.c”).return {
    $return = -1
    }

    result:
    tim@host:~/src/CVE$ ./correct_proc_mem_reproducer
    write: Operation not permitted
    not vulnerable

    this also shows ASLR at work (the offset differs from call to call):
    write attempt: 8 bytes (“33\b@”) at offset 140737430350128
    write attempt: 8 bytes (“33\b@”) at offset 140736850297152

    Comment by Tim — January 24, 2012 @ 11:49 am

  6. … okay the comment-function filters out “what-looks-like-html”.
    The if-line should read:
    if((now LESSTHAN last) || (( now – last ) GREATERTHAN HZ() )) {

    (I hope you don’t mind me spamming your blog ;-) )

    Comment by Tim — January 24, 2012 @ 11:58 am

Powered by WordPress