It wouldn't be fun if there were no protections, right?
Using Xenial Xerus, try running:
Notice that it throws an error.
Is the chunk at the top of the bin the same as the chunk being inserted?
For example, the following code still works:
When removing the chunk from a fastbin, make sure the size falls into the fastbin's range
The previous protection could be bypassed by freeing another chunk in between the double-free and just doing a bit more work that way, but then you fall into this trap.
Namely, if you overwrite fd
with something like 0x08041234
, you have to make sure the metadata fits - i.e. the size ahead of the data is completely correct - and that makes it harder, because you can't just write into the GOT, unless you get lucky.
A double-free can take a bit of time to understand, but ultimately it is very simple.
Firstly, remember that for fast chunks in the fastbin, the location of the next chunk in the bin is specified by the fd
pointer. This means if chunk a
points to chunk b
, once chunk a
is freed the next chunk in the bin is chunk b
.
In a double-free, we attempt to control fd
. By overwriting it with an arbitrary memory address, we can tell malloc()
where the next chunk is to be allocated. For example, say we overwrote a->fd
to point at 0x12345678
; once a
is free, the next chunk on the list will be 0x12345678
.
As it sounds, we have to free the chunk twice. But how does that help?
Let's watch the progress of the fastbin if we free an arbitrary chunk a
twice:
Fairly logical.
But what happens if we called malloc()
again for the same size?
Well, strange things would happen. a
is both allocated (in the form of b
) and free at the same time.
If you remember, the heap attempts to save as much space as possible and when the chunk is free the fd
pointer is written where the user data used to be.
But what does this mean?
When we write into the use data of b
, we're writing into the fd
of a
at the same time.
And remember - controlling fd
means we can control where the next chunk gets allocated!
So we can write an address into the data of b
, and that's where the next chunk gets placed.
Now, the next alloc will return a
again. This doesn't matter, we want the one afterwards.
Boom - an arbitrary write.
Still on Xenial Xerus, means both mentioned checks are still relevant. The bypass for the second check (malloc() memory corruption) is given to you in the form of fake metadata already set to a suitable size. Let's check the (relevant parts of) the source.
The fakemetadata
variable is the fake size of 0x30
, so you can focus on the double-free itself rather than the protection bypass. Directly after this is the admin
variable, meaning if you pull the exploit off into the location of that fake metadata, you can just overwrite that as proof.
users
is a list of strings for the usernames, and userCount
keeps track of the length of the array.
Prompts for input, takes in input. Note that main()
itself prints out the location of fakemetadata
, so we don't have to mess around with that at all.
createUser()
allocates a chunk of size 0x20
on the heap (real size is 0x30
including metadata, hence the fakemetadata
being 0x30
) then sets the array entry as a pointer to that chunk. Input then gets written there.
Get index, print out the details and free()
it. Easy peasy.
Checks you overwrote admin
with admin
, if you did, mission accomplished!
There's literally no checks in place so we have a plethora of options available, but this tutorial is about using a double-free, so we'll use that.
First let's make a skeleton of a script, along with some helper functions:
As we know with the fasttop
protection, we can't allocate once then free twice - we'll have to free once inbetween.
Let's check the progression of the fastbin by adding a pause()
after every delete()
. We'll hook on with radare2 using
Due to its size, the chunk will go into Fastbin 2, which we can check the contents of using dmhf 2
(dmhf
analyses fastbins, and we can specify number 2).
Looks like the first chunk is located at 0xd58000
. Let's keep going.
The next chunk (Chunk 1) has been added to the top of the fastbin, this chunk being located at 0xd58030
.
Boom - we free Chunk 0 again, adding it to the fastbin for the second time. radare2 is nice enough to point out there's a double-free.
Now we have a double-free, let's allocate Chunk 0 again and put some random data. Because it's also considered free, the data we write is seen as being in the fd
pointer of the chunk. Remember, the heap saves space, so fd
when free is located exactly where data is when allocated (probably explained better here).
So let's write to fd
, and see what happens to the fastbin. Remove all the pause()
instructions.
Run, debug, and dmhf 2
.
The last free()
gets reused, and our "fake" fastbin location is in the list. Beautiful.
Let's push it to the top of the list by creating two more irrelevant users. We can also parse the fakemetadata
location at the beginning of the exploit chain.
The reason we have to subtract 8 off fakemetadata
is that the only thing we faked in the souce is the size
field, but prev_size
is at the very front of the chunk metadata. If we point the fastbin freelist at the fakemetadata
variable it'll interpret it as prev_size
and the 8 bytes afterwards as size
, so we shift it all back 8 to align it correctly.
Now we can control where we write, and we know where to write to.
First, let's replace the location we write to with where we want to:
Now let's finish it off by creating another user. Since we control the fastbin, this user gets written to the location of our fake metadata, giving us an almost arbitrary write.
The 8 null bytes are padding. If you read the source, you notice the metadata string is 16 bytes long rather than 8, so we need 8 more padding.
Awesome - we completed the level!
Mixing it up a bit - you can try the 32-bit version yourself. Same principle, offsets a bit different and stuff. I'll upload the binary when I can, but just compile it as 32-bit and try it yourself :)