So how does a technique from the year 2000 hold up against modern versions of GLIBC?
We're about to find out in the Safe Unlink technique.
Navigate to the 'safe_unlink' directory.
We're already familiar with the malloc internals required to leverage this technique
so we'll crack straight on with the vulnerable
'safe_unlink' binary,
starting with our usual drills.
checksec shows us that we're firmly back in the present day with NX enabled alongside the usual
stack canaries and full RELRO. AMD introduced hardware NX support to their consumer desktop
processors in 2003, followed by Intel in 2004
although they called it XD for eXecute Disable.
We also see that the binary is linked against GLIBC version 2.30
compiled without tcache support.
We'll continue our usual investigation of the binary by loading it into GDB and running it.
This time we have a libc leak, but no heap leak. Other than that the menu looks familiar,
in fact, with the exception of the 'target' option, this program behaves identically to the unsafe_
unlink binary.
The only differences are those
we've already noted; that NX is enabled
and the GLIBC version it's linked against is more modern.
We're still only able to request up to two chunks with option 1, in the small chunk range only, excluding
fast sizes.
We're able to write to those chunks' user data with option 2 and free them by index with option 3.
The 'target' option works as expected and as usual, the target data resides in the data section of the
binary, which is non-position-independent.
If I request a 0x90-sized chunk and attempt to write too much data into it,
inspecting the heap reveals that the bug is also identical to the unsafe_unlink binary, an overflow
into the succeeding chunk's size field.
We'll start, as we usually do, by building an arbitrary write primitive which we'll use to overwrite
the target data.
And we are, of course going to leverage the unlinking process to do this.
There are just two mitigations that stand in our way,
the first is of course NX. In the unsafe unlink technique
we were able to redirect execution to our shellcode stored on the heap.
The presence of NX means that we can no longer do this because the heap, along with all other data
regions, will be marked as non-executable.
This greatly restricts the number of viable targets for the reflected write primitive we get from hijacking
the unlinking process.
Recall that one prerequisite of the reflected write is that both addresses we supply must point to writable
memory.
For example, if we write the address of system() to the free hook, the reflected write will also attempt
to copy the free hook address into the system() function, which is marked as non-writable.
So we'll need to rethink our forged fd and bk pointers.
The second mitigation we had to deal with is in the unlink macro itself.
Note that in later versions of GLIBC the unlink macro became a function called unlink_chunk(),
although it still does the same job.
I'll use the term unlink macro/function interchangeably.
Shown in blue is the unlink macro as it existed in the year 2000,
this is what we leveraged in the unsafe unlink technique.
All it does is copy a chunk's fd and bk in that reflected write pattern.
Shown in red is how the unlink function exists today, calls to the malloc_printerr() function, highlighted
in bright red, are part of malloc's exploit mitigations and what cause a process to abort when their preceding
check fails.
Of interest to us is the second check known as the "safe unlinking" check.
It's there to ensure that the chunk being unlinked is really part of a doubly linked list.
Let's break it down.
'p' represents the victim chunk. fd and bk
are its forward and backward pointers respectively.
The process will abort if either of the following are true: on following the victim's fd, the bk
at that location does not point back to the victim chunk.
Or on following the victim's, bk the fd
at that location doesn't point back to the victim chunk.
You may have noticed that we could pass the safe unlinking check by simply setting both our forged
fd and bk to the address of the chunk being unlinked.
However, this would not only require a heap leak, which we don't have in this scenario, but would
render the reflected write useless since we would just be overwriting our own data with values that already
exist.
We need to come up with a different target for our reflected write,
one that passes the safe unlinking checks and yields a useful primitive.
So let's learn a bit about how this binary works.
One of malloc's many small optimizations is that it doesn't keep track of allocated chunks.
It maintains pointers to its heaps' top chunks and free chunks in their respective arenas,
but when a chunk is allocated to a thread, that thread is expected to keep a reference to the chunk
until it returns it to malloc via functions like free().
This means that a program must store pointers to every chunk it's been allocated somewhere, whether
it keeps them on the stack,
its data section or even on the heap itself.
The vulnerable binaries we've dealt with so far
keep their chunk pointers on the stack where we are unable to tamper with them through lack of a stack
leak.
This binary, however, keeps its chunk pointers in its data section in an array labeled m_array,
which is just an arbitrary name
I've given it.
This is by no means uncommon, even if a program keeps most of its chunk references on the heap itself
it still needs to store the pointer to that initial heap memory on the stack or if it wants that space
to be more widely accessible, its own data or bss sections.
Because this binary is non-position-independent
we know the address of m_array at runtime. Printing the array shows that it's made up of pointers to chunks
followed by the size of the request that chunk was allocated for.
These pointers are used by the program when the 'edit' and 'free' options are selected.
Earlier, we noted that one way to pass the safe unlinking checks was to use pointers to the chunk
being unlinked.
And here we have a pointer to that chunk.
If we forged our fd and bk such that this pointer in m_array became both the bk of the chunk
pointed to by our fd and the fd of the chunk pointed to by our bk, that would pass the safe
unlinking checks.
Furthermore, the reflected write would have the effect of overwriting the targeted m_array entry with our
forged fd, which is the address of itself minus 24.
Once that's happened, the 'edit' option could be used to overwrite m_array entries with more control, leading
to an arbitrary write.
Now, there's clearly a lot of moving parts to this technique
so we're going to take it one step at a time.
Close GDB,
make a copy of the pwntools template and open it in your editor.
Scroll to the bottom and you'll see that we're starting more or less where the unsafe unlink left off.
We begin by requesting two 0x90-sized chunks, A and B, then we fill chunk A with forged chunk
metadata and use the overflow bug to clear chunk B's prev_inuse flag.
There's also an example of how the address of m_array can be accessed via elf.sym.m_array.
Let's clear out the first few lines, leaving just the important stuff.
Now we can point out fd at m_array minus 24, so that the bk of the chunk malloc will think is
there overlaps the first m_array entry.
We'll give our bk the same treatment, pointing it at m_array minus 16 to overlap
the first entry with the imaginary chunk's fd.
Run your script with the usual options,
'vis' and dump quadwords at m_array minus 2.
We see the first entry in m_array here pointing to chunk A's user data.
Our forged fd points here, making that first entry that imaginary chunk's backward pointer.
So now the bk of the chunk pointed to by the victim's fd points back to the victim.
That's the first half of the safe unlinking check.
Our forged bk points here, making the first m_array entry that chunk's fd. Now the fd of the chunk pointed
to by the victim's
bk also points back to the victim, which completes our safe unlinking checks.
But we're forgetting something important.
malloc uses metadata pointers when dealing with chunks, not pointers to user data,
so the fd and bk of those imaginary chunks we've overlapped with m_array are pointing sixteen bytes
after where they need to be to pass the checks.
We can confirm this by freeing chunk B, at which point we receive the error message we saw in the
unlink function.
And we can see the last call made from that function was right after failing the safe unlinking check.
So how do we solve this problem?
Well, recall how we directed malloc to unlink chunk A in the first place.
We forged a prev_size field that led malloc to the start of chunk A.
So what's stopping us from subtracting 16 bytes from our forged prev_size field to convince malloc that
the chunk it needs to unlink starts at chunk A's user data instead?
Return to your editor and let's make that change.
Decrease your fake prev_size field by 16 bytes and we'll need to add some more metadata into chunk A
since we're now forging an entire fake chunk inside it. Add two quadwords to your payload,
the first is an unused prev_size field,
the second is the size field of our fake chunk.
We actually need to set this correctly to 0x80 as the version of GLIBC we're dealing with
will abort if it doesn't match the prev_size field of the chunk that led malloc to our fake chunk
in the first place.
We take a closer look at GLIBC's so-called "size versus prev_size" checks
in part 2 of the HeapLAB course.
Lastly, we need to decrease the amount of garbage between our bk and prev_size field to account
for the new fields.
Let's run our exploit script with the usual debugging options and see if we've set up everything correctly.
Inspect the heap, then with a little GDB-fu
we can see what malloc sees when it follows our forged fd.
This line simply casts our fake chunk as a 'malloc_chunk' struct and follows its forward pointer.
It shows us that the bk of the chunk at that address points back to our fake chunk, this time to
the start of the chunk rather than its user data.
And this is enough to pass the first half of the safe unlinking check.
Following our forged bk shows that the fd of the chunk at that address also points back to our fake
chunk, which is enough to pass the second half of the safe unlinking check.
This time when we free chunk B, GLIBC doesn't abort and we can return to GDB to see the damage we've
done.
Avoid using the 'vis_heap_chunks' command at this point,
we've corrupted the heap and it will get confused.
Instead, use 'dq' and the
'sbrk_base' trick to dump some memory at the start of the default heap.
The 'top_chunk' command reveals that we've consolidated chunk B not only with our fake chunk, but with
the top chunk as well.
And because our fake chunk was consolidated, it was also unlinked,
meaning
that our reflected
write
must have worked.
Dumping the memory surrounding m_array shows the outcome of this.
We can see the first entry in m_array pointing not to the heap anymore, but to 24 bytes before itself.
This is the result of the second half of the unlinking process in which our forged bk was followed
and our forged fd was copied over the fd at the destination.
Notice how our fd and the first m_array entry are identical.
Now we can use the 'edit' option to write to the chunk at index zero of m_array, which allows us to overwrite
m_array itself a second time with our own controlled data.
Rather than doing that manually, let's add it to our exploit script.
We'll need to start by freeing chunk B, triggering the consolidation with our fake chunk and unlinking
it.
At this point, the address at index zero of m_array points near itself
thanks to our reflected
write,
so we'll use the 'edit' option with an index of zero to overwrite that address a second time.
We'll need to send three quadwords of garbage because that m_array entry points three quadwords
behind itself, followed by the address
we want to replace it with, in this case, our target data.
Now, the first entry in m_array points to our target and we can use the 'edit' function again, select index
zero and overwrite the target data.
Running our script under production conditions and selecting option 4 confirms that we've successfully
turned the safe unlink technique into an arbitrary write.
In summary, we requested two chunks, A and B, and leveraged an overflow bug in this binary to clear
the prev_inuse flag on the allocated chunk B.
We also prepared fake chunk metadata in its previous chunk, chunk A, including a size field, forward
and backward pointers and a prev_size field.
When we freed chunk B, its clear prev_inuse flag indicated to malloc that the previous chunk was
free, even though it wasn't and therefore a candidate for consolidation.
malloc used our forged prev_size field to find the start of the previous chunk so it could unlink
it prior to consolidation.
We set our prev_size field to the actual size of the previous chunk minus 16, making it appear
as though the previous chunk started at the first quadword of chunk A's user data, where we'd prepared
the rest of our fake chunk metadata.
malloc then performed its safe unlinking checks on our fake chunk.
First, it followed our fd and checked that the bk at the destination pointed back to our fake chunk.
We passed this check by aligning our fd address such that the bk at its destination overlapped a
pointer back to our fake chunk.
Next, malloc followed our bk and performed the same check on the fd at its destination.
We passed this check using the same trick of aligning our bk address such that the fd at its destination
also overlapped the same pointer to our fake chunk.
Once our fake chunk passed the safe unlinking checks, malloc unlinked it.
It followed our fd and copied our bk over the bk at its destination.
Then it followed our bk and copied our fd over the fd at its destination.
This had the effect of overwriting the pointer to our fake chunk with its own address minus 24.
Because the program was using that pointer for write operations that we controlled,
we were able to leverage it to overwrite that pointer again, this time with controlled data.
We overwrote the pointer with the address of our target data, allowing us to finally overwrite the target.
Now, this technique can be difficult to understand the first time, especially the part where we
pass the safe unlinking checks,
so don't hesitate to replay this video if you need to.
There's also a commented solution script available from this lecture's resources.
Now that we've leveraged the safe unlink technique for an arbitrary write, let's wrap things up by
dropping a shell. The arbitrary write primitive we've developed doesn't have any strings attached
so this should be straightforward.
If you're confident you can finish this exploit on your own,
now is a good opportunity to pause the video and do so.
If we swap the address of our target for the address of the free hook when overwriting the first m_array
entry, our next attempt to edit the chunk at index zero will allow us to overwrite the free hook,
which we can replace with the address of the system() function.
The only problem with that is when we free the only chunk we can at index zero,
that pointer will be the address of the free hook,
so we can't write a "/bin
/sh"
string there.
But instead of using a one-gadget,
why don't we just point this m_array entry eight bytes before the free hook?
That way, we can write our "/bin/sh" string before the free hook, and it conveniently takes up exactly
one quadword,
then continue writing, clobbering the free hook itself.
Let's test our exploit under GDB to see if we've got everything in place.
It looks like the free hook has been overwritten with the address of the system() function and the first
entry in m_array, which has yet to be freed, points to the string
"/bin/sh".
All that's left to do is free the chunk at index zero. The free hook will redirect free() to system()
and the argument passed to it will be a pointer to the string
"/bin/sh".
Let's return to our editor and add this final line to our exploit.
Now when we run our exploit under production conditions, we get a shell.
If you need to clarify anything we've learned in this video or just refresh your memory on this technique
later, there's a second commented exploit script that drops a shell again available from this lecture's
resources.
And don't forget to browse the PDF if you need a reminder of any malloc internals.
And that's the Safe Unlink technique, the modern equivalent of the Unsafe Unlink.
To leverage it
we had to be able to clear the prev_inuse bit of an allocated chunk, for which we use an overflow bug.
And we had to know the address of a pointer to a chunk,
in this case we used one stored by the program in its data section.
Join me in the next video where we'll start our journey towards learning the House of Orange technique,
which is without a doubt one of the most beautiful and complex heap exploitation techniques out there.