TL;DR
The "House of Water" is a sophisticated heap exploitation technique that targets the t-cache (thread cache) system in the GNU C Library's (glibc) heap implementation. This technique specifically manipulates the metadata of t-cache entries to take advantage of Use-After-Free (UAF) vulnerabilities.
The idea and logic are similar to many House attacks. We manipulate to create a fake chunk inside a legitimate chunk area. But the tricky part of House of Water is that, our fake chunk occupies across from the starting of the heap where stores the data of the Tache Perthread Struct, to the chunk where will be free'd into the unsorted bin!
Prerequisites
- Use-After-Free (UAF) primitive.
- We are able to malloc/free a relatively large chunk (still belongs to small request though).
⚠ We do NOT need an overflow or memory leak to complete the attack!
Overview
When a program starts to run and meets its first malloc, the heap (main arena) will be initialized. By default, a memory size of 0x290 will then be allocated to store metadata. This memory area involves the Tcache Perthread Struct by storing relative parameters and the addresses of various tcache entries (aka pointers to the tcache-bin chunks).
In current versions of glibc, when we try to execute malloc(0x3d8)
, malloc(0x3e8)
, two chunks in size of 0x3e0 & 0x3f0 (992 & 1008 in decimal) will be allocated respectively. After that we free them into the corresponding tcache bins, the location at offset +0x88 from the heapbase will turn into 0x10001.
It's important to understand the image of the whole memory now. And the location at +0x88 offset from heapbase is also the end where the heap stores relative parameters for the Tcache Perthread Struct—starting from +0x90 offset, the heap will store tcache-entry pointers from the smallest tcache bin, which keeps the free'd chunks in size of 0x20.
Further, the memory from +0x88 offset to +0x290 offset of the heapbase, sits all pointers for the tcache entries. It exactly covers the smallest tcache-bin-chunk size of 0x20 to the biggest of 0x410.
Interestingly, 0x410 happens to be the smallest size of large-bin chunks, which exists an overlapping situation.
Here I draw a picture to show the memory map for above allocation and illustrate how we are going to perform the House of Water attack:
After deploying the fake chunk size to 0x10001, we will then request 3 chunks as the unsorted_start, unsorted_middle, unosrted_end, with padding chunks separated (avoid consolidation). We are going to free all of them into the unsorted bin, and modify their fd & bk to values that are related to tcache-bin chunks of 0x20, 0x30 classes. NB. we don't need to leak the tcache addresses here because we will leverage the pointers generated when we free the faked tcache-bin chunks.
This fake heap chunk header (with the size field 0x10001 on it) happens to be positioned above the 0x20 and 0x30 t-cache linked address entries, enabling the creation of a fully functional fake unsorted-bin entry. With the unsorted-bin chunks we created before, the correct size should be set for the chunk, and the next chunk's prev-in-use bit must be 0. Therefore, from the fake tcache metadata chunk+0x10000, the appropriate values should be written.
This is a magnificent plan to return an arbitrary write chunk at the offset +0x80 from the heapbase, meaning we are able to control the tcache metadata. Moreover, we get pointers as an added bonus which will be saved into the tcache entries 0x20, 0x30.
Attack Demo
we use a demo code from how2heap with our analysis.
Step 1: Allocate a 0x3d8 and a 0x3e8 to set their respective t-cache counts to 1, effectively inserting 0x10001 in to the tcache above the 0x20 and 0x30 tcache entries:
void *fake_size_lsb = malloc(0x3d8);
void *fake_size_msb = malloc(0x3e8);
free(fake_size_lsb);
free(fake_size_msb);
Using GDB, we see two allocated chunks are free'd into the corresponding tcache bins.
We can extract the information for better analysis:
- The 2 dummy chunks:
* Entry 0x3e0 @ 0x55555555b2a0
* Entry 0x3f0 @ 0x55555555b680
The t-cache metadata will now have the following entry counts:
0x000055555555b070 0x0000000000000000 0x0000000000000000
0x000055555555b080 0x0000000000000000 0x0000000000010001
0x000055555555b090 0x0000000000000000 0x0000000000000000
And let's define the heapbase as a variable so that we can follow the memory later. We just need to mask the lowest bits off the tcache entry we retrieve above:
void *metadata = (void *)((long)(fake_size_lsb) & ~(0xfff));
It looks like the exact situation as we introduced in the Overview part:
Then we allocate 7 0x88 chunks needed to fill out the 0x90 tcache bin at a later time:
void *x[7];
for (int i = 0; i < 7; i++) {
x[i] = malloc(0x88);
}
Step 2: Here we will allocate three 0x90 chunks with guard/padding chunks in between to avoid consolidation. And they will be the target for the House of Water attack in the following steps:
void *unsorted_start = malloc(0x88);
_ = malloc(0x18); // Guard chunk
void *unsorted_middle = malloc(0x88);
_ = malloc(0x18); // Guard chunk
void *unsorted_end = malloc(0x88);
_ = malloc(0x18); // Guard chunk
The memory map look in GDB:
They are unsorted_start, unsorted_middle, unsorted_end:
- User-data entries:
* unsorted_start @ 0x55555555be60
* /guard/
* unsorted_middle @ 0x55555555bf10
* /guard/
* unsorted_end @ 0x55555555bfc0
* /guard/
Step 3: In this stage, we will want to trick the Allocator to consider the fake chunk (size of 0x10000) as a free'd chunk. To satisfy the conditions being free'd, namely having the correct size at the end of the fake chunk, and a size field next to it having its prev-in-use bit set to 0.
First we need to add a 0xf000 padding to make the fake chunk exactly fit the size of 0x10000, and to prepare the end_of_fake as a writable metadata chunk to modify the metadata we need to satisfy the free'd fake chunk:
_ = malloc(0xf000); // Padding
void *end_of_fake = malloc(0x18); // Metadata chunk
The memory map look in GDB:
The metadata chunk (aka end_of_fake) is prepared for us to write/modify the meta data of the large fake chunk above:
- metadata chunk :
* end of fake @ 0x55555556b080
Then we write the desired data to cheat the Allocator, preventing libc from failing check:
*(long *)end_of_fake = 0x10000;
*(long *)(end_of_fake+0x8) = 0x20;
Two parts are needed to modify inside the metadata chunk (end_of_fake), including the size of current free'd chunk & the prev_inuse bit of the next chunk:
Now we have:
*0x55555556b080 = 0x10000
*0x55555556b088 = 0x20
Step 4: Fill up the 0x90 tcache bin with the dummy chunks allocated from earlier by freeing them. By doing so, when next time a 0x88 chunk is free'd, it ends up in the unsorted-bin rather than the full tcache bin.
for (int i = 0; i < 7; i++) {
free(x[i]);
}
Step 5: Create a 0x20 and a 0x30 tcache entry which overlaps the unsorted_start and unsorted_end. By doing this, we can blindly fake a fd and bk pointer in the tcache metadata without the needing to leak addresses!
Here comes the trickiest part of the House of Water attack.
We essentially want a pointer in the 0x20 tcache entry (heapbase + 0x90) to act as a fd pointer and a pointer in the 0x30 tcache entry (heapbase +0x98) to act as a bk pointer.
To be notice, we want them to point to the chunk header of our unsorted bin entries (the user-data field), but not at the chunk head itself, which is special for the tcache (not like fast, small, unsorted, large bins).
Using a technique like House of Botcake, UAF, or a stronger arb-free primitive, free a chunk that exactly overlaps with the header of the unsorted_start and unsorte_end chunks.
Before the attack, the memory map should look like:
And we plan to trick the 0x20 tcache entry (tcachebins[0x20][0/1]) overlaps the unsorted_end chunk, the 0x30 tcache entry (tcachebins[0x30][0/1]) overlaps the unsorted_start chunk:
unsorted_start:
0x000055555555be50 0x0000000000000000 0x0000000000000091 <-- tcachebins[0x30][0/1], unsortedbin[all][0]
0x000055555555be60 0x0000000000000000 0x0000000000000000
0x000055555555be70 0x0000000000000000 0x0000000000000000
unsorted_end:
0x000055555555bfb0 0x0000000000000000 0x0000000000000091 <-- tcachebins[0x20][0/1], unsortedbin[all][2]
0x000055555555bfc0 0x0000000000000000 0x0000000000000000
0x000055555555bfd0 0x0000000000000000 0x0000000000000000
Usually, a challenge won't allow us to free an arbitrary address. We can leverage a double-free-like attack to achieve this goal. There is an example chanllenge linked here. But for the sake of simplicity in this case, we will simulate an arbitrary free primitive to explain how the House of Water works.
Step 5.1: Write 0x31 above unsorted_start to allow the chunk to be free'd into the 0x30 tcache entry:
*(long*)(unsorted_start-0x18) = 0x31;
This creates a 0x31 entry just above unsorted_start, which looks like the following:
Then we free the faked 0x31 chunk (where we need the arb-free technique):
free(unsorted_start-0x10); // Create a fake fd pointer for the fake chunk
⚠ This step is essential. Not for the tcache next pointer, but to make the tcache entry become the chunk head for the unsorted_start (and for unsorted_end later). Here we cleverly leverage the difference between tcache and other bins, namely the tcache entry stores the user-data field of a chunk, while other bins store the chunk head. And we will consider the fake chunk a whole large chunk at the end, with its fd pointer points to unsorted_end and its bk pointer points to unsorted_start.
Because of the metadata (next pointer & key) created by free'ing the 0x31 fake chunk, the metadata of the unsorted_start chunk is corrupted. We then need to reover the original header to make the size field 0x91:
*(long*)(unsorted_start-0x8) = 0x91;
Finally, the desired look will be:
Step 5.2: We will then do the exact same thing at the unsorted_end chunk area. The only difference is to overlap the 0x20 tcache entry on the unsorted_end:
*(long*)(unsorted_end-0x18) = 0x21; // Write 0x21 above unsorted_end
free(unsorted_end-0x10); // Create a fake bk for the fake chunk
*(long*)(unsorted_end-0x8) = 0x91; //recover the original header
And now we have:
Step 6: Let's free all the unsorted chunks to create a unsorted bin list:
free(unsorted_end);
free(unsorted_middle);
free(unsorted_start);
After freeing them in order, we have such a bin-list looking:
Now the unsorted_start, unsorted_middle, unsorted_end looks like:
We can now observe that the 0x30 tcache entry points to unsorted_start, and the 0x20 tcache entry points to the unsorted_end, which is what we need to fake an unsorted-bin entry and hijack the unsorted_middle in the following attack:
The large fake chunk in the tcache will look like:
Step 7: Overwrite LSB of the unsorted_start's fd pointer, and unsorted_end's bk pointer. Make them point to the large fake chunk which begins at the tcache metadata:
/* VULNERABILITY */
*(unsigned long *)unsorted_start = (unsigned long)(metadata+0x80);
*(unsigned long *)(unsorted_end+0x8) = (unsigned long)(metadata+0x80);
Here, we will need to apply vulnerability like a UAF primitive to complete this task. The good news is that we don't need to leak the heap address in advance. Because we just need to overwrite the LSB! In this case, we only need to rewrite 0x55555555bf00 to 0x55555555b080 inside the unsorted_start, and 0x55555555be50 to 0x55555555b080 inside the unsorted_end:
After this attack, between the unsorted_start and unsorted_end, there sites no longer the previous unsorted_middle, but the fake chunk! Now we can check the unsorted bin list in GDB:
Step 8: Finally, simply allocate a chunk that's within the 0x10000 range, we will get a returned memory chunk splitted from the large fake chunk, meaning we can now write anything on the tcache metadata!
As an example, we will allocate a 0x288:
void *meta_chunk = malloc(0x288);
And the allocation will be our faked chunk (size 0x290). We can check the meta_chunk in GDB:
Bonus: The primary goal of this house is to provide a leakless way to gain tcache control by overwriting LSB. And we have a nice bonus for the free LIBC pointers as we can see in the above image. Notice now both the 0x20 and 0x30 tcache entries contain the libc pointers which point to the main_arena.
Comments | NOTHING