We developed a new device named CYDF :) Ubuntu 16.04 latest nc q-escape.pwn.seccon.jp 1337

494 Pts, 2 solved, pwn. q-escape.tar.gz

We’re given a qemu binary that has been compiled with a new device called cydf which looks like it is a vga device. After a bit of digging it seems to be a copy and paste of an existing qemu device cirrus which is also missing from the binary, a great place to start as it hopefully means that we have most of the source and just need to find what was added or changed.

Looking at the CydfVGAState structure we can see that there is a field called VulnState vs[0x10]; that has been added, and it’s name is a pretty big hint that we should focus on it. There is also a global which looks suspicious 0x10C94E0 ; uint64_t vulncnt

The xrefs for vulncnt show that it is used in cydf_vga_mem_write and after a bit of RE we see that there are a couple of operations that we can perform based on the value of s->vga.sr[0xCC] if we use an addr greated than 0x10000 and less than 0x18000.

I didn’t know too much about how to even access vga memory or the ioports, so some research was required. The source for the cirrus device had a nice header for cirrus_vga_mem_read saying memory access between 0xa0000-0xbffff which aligns with what wikipedia says for vga mappings. The other hint in the original source is in cirrus_init_common where is says Register ioport 0x3b0 - 0x3df and looking at the same functions in our binary the values line up.

An easy way of accessing the physical memory is to map /dev/mem into our process and then we can read/write to it like normal. The base image we are provided with does not have this mounted, but we can mount it with mknod -m 660 /dev/mem c 1 1. This allows us to do the following to access 0xa0000 directly:

int fd = open("/dev/mem", O_RDWR | O_SYNC);
uint8_t *mem = mmap(0, 0x20000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0xa0000);
mem[0] = 0x41;

We can test this by setting a breakpoint on cydf_vga_mem_write to check that it gets called: breakpoint

The next issue is the function immediately returns due to the following check:

if (!(s->vga.sr[7] & 1)) {
  vga_mem_writeb(&s->vga, addr, mem_value);
  return;
}

So we need to figure out how to set the value of s->vga.sr[7] to 1. There is a method called cirrus_vga_write_sr which looks like it’s what we want, it allows you to do s->vga.sr[s->vga.sr_index] = val; so now we need to find out how to set s->vga.sr_index. This is done in cirrus_vga_ioport_write using ioport 0x3c4, and we see directly below that ioport 0x3c5 is used to call cirrus_vga_write_sr.

To use the ioports we can use the following code:

ioperm(0x3C4, 2, 1); // set port access
outb(7, 0x3C4);	// set the index
outb(1, 0x3C5); //set the sr value

By setting a breakpoint at 0x68F7C0 we can confirm that we are hitting the expected path: breakpoint

We should now be able to hit the section of code that deals with VulnState by setting s->vga.sr[7] and accessing addr at 0x10000, and by setting a breakpoint at 0x68F27B we can confirm this works!

int fd = open("/dev/mem", O_RDWR | O_SYNC);
uint8_t *mem = mmap(0, 0x20000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0xa0000);
ioperm(0x3C4, 2, 1); // set port access
outb(7, 0x3C4);	// set the index
outb(1, 0x3C5); //set the sr value
mem[0x10000] = 0;

The disassembly for cydf_vga_mem_write looks like there are 5 different paths we can take based on the value of s->vga.sr[0xCC] (which we know how to set now) and they are create, write, print, update size, and another update. Later on I found that the last path had been removed from the server as they were running a different binary than the supplied one.

There is an obvious heap overflow as we can update the max_size and no realloc occurs, and the is also an off-by-one error that allows us to access vs[0x10] which actually points to a field called latch. If we can set this value then we have an easy read/write primative!

The latch gets set in cydf_vga_mem_read by either setting the lower 16 bits if they are 0, otherwise setting the upper 16 bits and clearing the lower 16.

uint32_t latch = s->latch[0];
if (!(latch & 0xffff)) {
  s->latch[0] = addr | latch;
} else {
  s->latch[0] = addr << 16;
}

This is fine for us, we can set the latch in two goes, then read and write to it using the vs[0x10] bug. I choose to set it to the __printf_chk GOT at 0xEE7028 as it’s easy to call.

When calling the print method for a VulnState it will print from the qemu process, so we need to read it with our wrapper (eg pwntools script) and then send the leak back into our exploit to set it.

Another catch was that stdout was line buffered, so to get any output you needed to print a newline to flush it.

All this worked locally and managed to use a magic gadget to get a shell, party time! But as is always the case, it didn’t work remotely :( After some investigation I found that the remote binary wasn’t just slightly different, the offsets were totally different and 0xEE7028 wasn’t even mapped! I could still leak ELF from 0x400001 so hopefully it was just a matter of locating the GOT and the __printf_chk entry.

I leaked the PHT entry at 0x400130 to get the offset for the _DYNAMIC section, which let me know that the GOT started at 0xDEA000. I then iterated through the entries until I found an address that would give me a libc base that ended in 000 which was found at 0xDEA2D8 and the rest of the exploit worked as intended!

SECCON{6767ac011b200bde1249d241b1cd5480}

Full exploit