As I am only interested in the "free" chunks at this point, I have removed all lines from the output that correspond with a "busy" chunk and/or are part of a LFH subsegment for a different bucket size. (in case you’re wondering how to recognize those: simply look for lines that are preceded by an asterisk (*), indicating a large busy/internal chunk, and followed by a list a smaller chunks of similar sizes within the address range of this larger busy/internal chunk. This larger one is the subsegment, the smaller ones are the LFH managed chunks inside that subsegment).
In other words, I’m only listing the chunks that are managed by the BEA and could potentially be used to satisfy the allocation requests caused by my test application.
As you can see, there are no free chunks of exactly 0x300 bytes, however the last line in the output shows a free chunk of 0xba0 bytes (01561440 – 01561FE0).
This is quite normal, as this is the remaining space in the segment that has not been allocated yet. It shows up as a free chunk (because that’s exactly what it is) and will be split to satisfy new allocation requests ( = that’s pretty much how the BEA works when it has to use a larger chunk to satisfy an allocation request).
Since there are no free chunks of 0x300 bytes, a larger free chunk should be split (providing that the one in the list above is not being used by LFH… but as the flag is set to "free" and as it is not part of a larger subsegment, it shouldn’t be related with LFH at all. After all, a subsegment typically shows up as a larger chunk that is marked as "busy" and "internal").
The "flags" indicate that this is not an LFH managed chunk. (Otherwise, the output would have mentioned the word "LFH" as well)
Based on the addresses returned by HeapAlloc, it looks like the final free chunk of 0xba0 bytes (at 0x1561440) was indeed split into pieces:
Both allocations sit next to each other, because allocations are taken from the start of a free chunk when splitting. As both allocations are taken from the same larger chunk, it is expected from them to sit together. What is left of the original 0xba0 byte chunk is 0x590 bytes (as expected, showing up as a free chunk which starts at 01561a50 and ends at 01561FE0)
01561438 0061 0201 [00] 01561440 00300 - (busy)
01561740 0061 0061 [00] 01561748 00300 - (busy)
01561a48 00b3 0061 [00] 01561a50 00590 - (free)
Great, but we have not been really using the FreeList mechanisms of the BEA at this point. All we have been doing is allocate chunks from within a normal segment, consuming the free space that was there all the time. To trigger the BEA FreeList mechanism and see how it behaves, we’ll have to "free" some chunks ourselves first.
In this second example, we’ll create a series of allocations of 0x300 bytes. We’ll free the last one and then cause 2 allocations of 0x100 bytes.
The purpose of the exercise is to check where these 2 allocations will be placed.
As we intend to evaluate the BEA mechanism, we have to avoid that the LFH gets triggered. We know that the heap manager in Windows 7 will trigger the LFH when it sees 18 consecutive allocations of a size in the same bucket (i.e. the next request will be allocated from within a LFH managed subsegment).
For that reason, we’ll only cause 10 allocations of 0x300 bytes, hopefully avoiding that the LFH will be used.
After causing the 10 allocations, let’s examine the last one and see if LFH is on or off.
App:
C:\Users\corelan\Desktop\vc++\win10\BEA_Alloc2\Release>BEA_Alloc2.exe Default process heap found at 0x009B0000 Press a key to start... Allocated chunk of 0x300 bytes at 0x009BF1D0 Allocated chunk of 0x300 bytes at 0x009BF4D8 Allocated chunk of 0x300 bytes at 0x009BF7E0 Allocated chunk of 0x300 bytes at 0x009BFAE8 Allocated chunk of 0x300 bytes at 0x009BFDF0 Allocated chunk of 0x300 bytes at 0x009C00F8 Allocated chunk of 0x300 bytes at 0x009C0400 Allocated chunk of 0x300 bytes at 0x009C0708 Allocated chunk of 0x300 bytes at 0x009C0A10 Allocated chunk of 0x300 bytes at 0x009C0D18
Press return to continue
WinDBG:
0:003> !heap -x 0x009C0D18
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
009c0d10 009c0d18 009b0000 009b0000 308 308 8 busy
So far so good, no sign of LFH at this point. Next, the application will free this last chunk:
App:
Free chunk at 0x009C0D18 Press return to continue
WinDBG:
0:003> !heap -x 0x009C0D18
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
009c0d10 009c0d18 009b0000 009b0000 12d0 308 0 free
Before running the rest of the application, let’s look at the current state of the heap, looking for "free" chunks of 0x300 bytes or larger. The output below only contains the relevant lines:
WinDBG:
0:003> !heap -p -h 0x009B0000
_HEAP @ 9b0000
_LFH_HEAP @ 610000
_HEAP_SEGMENT @ 9b0000
CommittedRange @ 9b0498
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
009c0d10 025a 0061 [00] 009c0d18 012c8 - (free)
As we can see in the output, as the last chunk of 0x300 bytes was allocated right before the remaining free space in the segment, this now freed chunk got merged with the adjacent free space in the segment, and has now become a 0x12c8 byte free chunk. This larger free chunk starts at 009c0d10, which is also the start address of the 0x300 byte chunk that was freed.
Our test application will now cause 2 allocations of 0x100 bytes each. Sure enough, this is large enough to satisfy 2 requests for 0x100 bytes, but in order to predict what exactly will happen, we need to look at the heap state again first.
The output of !heap -p -h
WinDBG:
0:003> !heap -p -h 0x009B0000
_HEAP @ 9b0000
_LFH_HEAP @ 610000
_HEAP_SEGMENT @ 9b0000
CommittedRange @ 9b0498
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
009b2b00 0021 0012 [00] 009b2b08 00100 - (free)
009bc330 0034 0089 [00] 009bc338 00198 - (free)
009c0d10 025a 0061 [00] 009c0d18 012c8 - (free)
Based on the output, we can see
Let’s see what logic is applied by the BEA to reuse those freed chunks:
App:
Allocated chunk of 0x100 bytes at 0x009B2B08
Allocated chunk of 0x100 bytes at 0x009BC338
Done...
This makes sense. The chunk that is exactly the size of the allocation request gets reused first. Next, the next one that is larger gets split. The remaining space near the end of the segment, which is now a combination of the original free space and the 0x300 byte chunk that was freed, does not get used.
The 2 chunks are not adjacent (despite the allocations happened right after each other). None of the 2 chunks were able to take memory space back from one the 0x300 byte chunk that was freed earlier.
The behavior is absolutely normal, but this means that we cannot simply use the BEA to take a previously-allocated-and-now-freed chunk back… at least not without doing some "massaging".
On the other hand, because the BEA does not keep chunks together based on buckets, and if we can improve control, we may be able to use a smaller or a larger allocation size to take a position back in memory that has been freed, regardless of the original size of that freed chunk.
Let’s try to increase control over what happens, increasing our chances that we can take a freed chunk back.
As demonstrated in the previous exercise, the fact that the BEA is managing freed chunks means that, by default, we may not be able to predict where a new allocation will be placed relative to another chunk or in relation with taking the space of a freed chunk (which would be useful in a Use After Free scenario).
Let’s start with the latter scenario first. What does it take to take a freed chunk back?
The answer is quite simple: it depends on what else is free.
A possible approach to control what else is free, is to simply reduce what is free at the time the free happens that you would like to take back. The idea is to exhaust the freelist as much as possible… and to do that, we need to cause allocations.
Additionally, we may want to check that the freed chunk (the one we’re trying to take back… we’ll call this one the ‘vulnerable’ freed chunk) does not sit after an already freed chunk. This would cause both chunks to be merged, and subsequent allocation requests would start consuming the start of the new (bigger chunk). Depending on the size of the first part, it may be difficult to get perfect control over the contents. On the other hand, if you can control the size of that first freed chunk (which will eventually get merged with the ‘vulnerable’ freed chunk), it might actually allow you to improve control over what bytes exactly you will manage if you don’t control all bytes of the new allocation.
Again, it all depends on the layout that you create, and your understanding of what is inside the free list at the time of the allocation that is supposed to take the place of the "vulnerable" freed chunk.
Anyway, let’s look at an example. BEA_Alloc3 contains the following scenario: we’ll free a 0x58 byte chunk, and the goal is to use a 0x58 byte allocation that will take its position back
Obviously we have to make sure that LFH is never activated (as we are playing with the BEA), and that we can avoid that the 0x58 byte chunk gets merged with an adjacent free chunk. This means that we could try to surround the 0x58 byte chunk by 2 allocations that we created as well, but won’t allow to be freed.
In order for all 3 allocations to be adjacent, we have to make sure there is nothing on the free list already that will satisfy any of the 3 allocation requests. In the example application, I am using 10 allocations for both sizes, but of course in reality, you’ll need to check how many allocations you need to truly remove all free chunks; while being careful about avoiding LFH at the same time.
As the size of the 1st and 3rd allocation (the ones before and after the 0x58 byte chunk) is not so important, we can choose a size that is not on the freelist yet. (Sizes are not important, because we don’t have to take LFH bucket sizes into account. Again, we’re playing with the BEA). Anyway, I’ll use 0x100 byte chunk to surround the 0x58 chunk.
Run the application, and check the heap after causing a series of allocations for 0x58 and 0x100 bytes, to exhaust the freelist for those sizes.
App:
C:\Users\corelan\Desktop\vc++\win10\BEA_Alloc3\Release>BEA_Alloc3.exe
Default process heap found at 0x016C0000
Press a key to start...Allocated chunk of 0x58 bytes at 0x016C96D0
Allocated chunk of 0x100 bytes at 0x016C2B08
Allocated chunk of 0x58 bytes at 0x016CDF50
Allocated chunk of 0x100 bytes at 0x016CD328
Allocated chunk of 0x58 bytes at 0x016CD430
Allocated chunk of 0x100 bytes at 0x016CFFF8
Allocated chunk of 0x58 bytes at 0x016C2CB8
Allocated chunk of 0x100 bytes at 0x016D0100
Allocated chunk of 0x58 bytes at 0x016CDC18
Allocated chunk of 0x100 bytes at 0x016D0208
Allocated chunk of 0x58 bytes at 0x016CDC78
Allocated chunk of 0x100 bytes at 0x016D0310
Allocated chunk of 0x58 bytes at 0x016D0418
Allocated chunk of 0x100 bytes at 0x016D0478
Allocated chunk of 0x58 bytes at 0x016D0580
Allocated chunk of 0x100 bytes at 0x016D05E0
Allocated chunk of 0x58 bytes at 0x016D06E8
Allocated chunk of 0x100 bytes at 0x016D0748
Allocated chunk of 0x58 bytes at 0x016D0850
Allocated chunk of 0x100 bytes at 0x016D08B0
Press return to continue
WinDBG shows that the last 2 allocations are not part of LFH (which means LFH is not active yet). We can also see that these allocations are taken from the large free chunk at the end of the segment (thus no longer taking already freed chunks that might have been part of the freelist)
0:003> !heap -x 0x016D08B0
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
016d08a8 016d08b0 016c0000 016c0000 108 60 8 busy0:003> !heap -x 0x016D0850
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
016d0848 016d0850 016c0000 016c0000 60 108 8 busy
Next, the application will cause the necessary layout. As a segment doesn’t necessarily want to keep chunks of the same bucket size together, and operates in a first ask, first serve manner (unlike the LFH), the 3 allocations should be placed right next to each other:
App:
Allocated chunk of 0x100 bytes at 0x016D09B8
Allocated chunk of 0x58 bytes at 0x016D0AC0, filled with 'A'
Allocated chunk of 0x100 bytes at 0x016D0B20
Press return to continue
Great! We can use WinDBG to confirm that the 3 chunks are adjacent indeed, and that the middle chunk (0x58) is filled with A’s
(from output of !heap -p -h 0x016C0000)
016d09b0 0021 0021 [00] 016d09b8 00100 - (busy)
016d0ab8 000c 0021 [00] 016d0ac0 00058 - (busy)
016d0b18 0021 000c [00] 016d0b20 00100 - (busy)0:001> dd 0x016d0ac0 L 0x58/4
016d0ac0 41414141 41414141 41414141 41414141
016d0ad0 41414141 41414141 41414141 41414141
016d0ae0 41414141 41414141 41414141 41414141
016d0af0 41414141 41414141 41414141 41414141
016d0b00 41414141 41414141 41414141 41414141
016d0b10 41414141 41414141
Next, the 0x58 byte chunk will be freed (which means it should end up on the freelist, and not merged with an adjacent free chunk as long as we don’t free the adjacent 0x100 byte chunks ourselves). Finally, a new allocation request for 0x58 is supposed to take that position back. The example application will populate it with B’s:
App:
Free chunk of 0x58 bytes at 0x016D0AC0
Allocated chunk of 0x58 bytes at 0x016D0AC0, filled with 'B'
Done...
WinDBG:
0:001> dd 0x016d0ac0 L 0x58/4
016d0ac0 42424242 42424242 42424242 42424242
016d0ad0 42424242 42424242 42424242 42424242
016d0ae0 42424242 42424242 42424242 42424242
016d0af0 42424242 42424242 42424242 42424242
016d0b00 42424242 42424242 42424242 42424242
016d0b10 42424242 42424242
Bingo.
Let’s take it one step further.
This time we’ll free a 0x58 byte chunk, and we’ll try to use a 0x80 byte allocation (where we only control the last 4 dwords), to control the exact 4 first dwords in the original 0x58 byte chunk. (I know, replacing one object with another object of a different size sounds kinky… but hey, who knows this could be useful one time … or not)
The approach is pretty much the same: we’ll start by (trying) to exhaust the freelist for the sizes that we are going to use, to make sure we have better control over what will be returned when we ask for a chunk of those sizes.
What is different in this case, is the layout we need to allow magic to happen – i.e. to take the first 4 dwords of the 0x58 byte chunk using the last 4 dwords of the 0x80 byte allocation.
The idea is to try to force 2 freed chunks to merge (by placing them next to each other and then freeing both). The so-called "vulnerable" object that gets freed in our imaginary "use after free" scenario is 0x58 bytes, and the goal is to to control the first 4 dwords. The object we can use to replace it with is 0x80 bytes, and we’ll pretend we can only control the last 4 dwords of that one. In other words, we have to make sure that we position those 4 dwords with surgical precision, making sure that the "overlap" will happen in the right place.
I generally suck at math, but after spending some time on the abacus, finger counting and scratching my head, I came up with the complex formula that results in trying to position a 0x80-4-4-4-4-8 = 0x68 byte object before the 0x58 byte object. The 4 times 4 bytes are needed to cause overlap, and the additional 8 bytes are there to compensate for the chunk header of the 0x58 byte chunk that sits before the contents of the 0x58 byte chunk, and is thus also consuming space in memory.
This means that we also have to make sure to remove 0x68 byte chunks from the free list in order to increase control over allocations of that size.
We’ll create a layout that consists of 4 allocations: our ‘magic’ 0x68 and 0x58 objects in the middle, surrounded by 2 other objects, to avoid that a merge of one of the 2 in the middle will cause us to lose control when things start to merge with the wrong objects. We’ll also cause a little bit more allocations to try to exhaust the heap, as some of these sizes may be popular sizes.
(Again, the number of allocations you need to properly clear out the free list depends on the applicaiton, the context, basically on what is already free at that time)
Let’s see what it looks like:
App:
C:\Users\corelan\Desktop\vc++\win10\BEA_Alloc4\Release>BEA_Alloc4.exe
Default process heap found at 0x010D0000
Press a key to start...
<...snip...>
Allocated chunk of 0x68 bytes at 0x010E1D68
Allocated chunk of 0x58 bytes at 0x010E1DD8
Allocated chunk of 0x80 bytes at 0x010E1E38
Allocated chunk of 0x68 bytes at 0x010E1EC0
Allocated chunk of 0x58 bytes at 0x010E1F30
Allocated chunk of 0x80 bytes at 0x010E1F90
Allocated chunk of 0x68 bytes at 0x010E2018
Press return to continue
(Check the last 3 chunks in WinDBG (using !heap -x
) to make sure that none of them is LFH managed at this point.)Next, the application creates the initial layout (0x80 // 0x68 // 0x58 // 0x80). The first and last one are just there to prevent the 0x68 and 0x58 sized chunks to accidentally merge with the wrong one.
App:
Allocated start chunk (0x80 bytes) at 0x010E2088
Allocated first chunk (0x68 bytes) at 0x010E2110
Allocated second 'vulnerable' chunk (0x58 bytes) at 0x010E2180, filled with 'A'
Allocated end chunk (0x80 bytes) at 0x010E21E0
Press return to continue
WinDBG (output from !heap -p -h
):010e2080 0011 000e [00] 010e2088 00080 - (busy)
010e2108 000e 0011 [00] 010e2110 00068 - (busy)
010e2178 000c 000e [00] 010e2180 00058 - (busy)
010e21d8 0011 000c [00] 010e21e0 00080 - (busy)
Sure enough, the 4 chunks are in the right place. When the ‘vulnerable’ 0x58 byte chunk gets freed, we’ll also cause the first one to free ourselves. This should merge the 2 together.
App:
Free chunk of 0x58 bytes at 0x010E2180
Free first chunk of 0x68 bytes at 0x010E2110
Press return to continue
WinDBG shows that we now have one big free chunk at 0x010E2110, of 0xc8 bytes. If we dump it, we see the original 0x68 chunk followed by the vulnerable 0x58 byte chunk.
0:003> !heap -p -a 0x010E2110
address 010e2110 found in
_HEAP @ 10d0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
010e2108 001a 0000 [00] 010e2110 000c8 - (free)
0:003> dd 0x010E2110 L 0xc8/4
010e2110 010e2268 010de858 00000000 00000000
010e2120 00000000 00000000 00000000 00000000
010e2130 00000000 00000000 00000000 00000000
010e2140 00000000 00000000 00000000 00000000
010e2150 00000000 00000000 00000000 00000000
010e2160 00000000 00000000 00000000 00000000
010e2170 00000000 00000000 0c00000c 00008812
010e2180 010e2268 010de858 41414141 41414141
010e2190 41414141 41414141 41414141 41414141
010e21a0 41414141 41414141 41414141 41414141
010e21b0 41414141 41414141 41414141 41414141
010e21c0 41414141 41414141 41414141 41414141
010e21d0 41414141 41414141
Finally, our 0x80 byte allocation is now supposed to take the first 0x80 bytes of this free chunk, overwriting the first 4 dwords inside the chunk with A’s with the last 4 dwords of the 0x80 byte chunk.
App:
Allocated chunk of 0x80 bytes at 0x010E2110, filled with 'B'
Done...
WinDBG:
0:001> !heap -p -a 0x010E2110
address 010e2110 found in
_HEAP @ 10d0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
010e2108 0011 0000 [00] 010e2110 00080 - (busy)
0:001> dd 0x010E2110 L 0xc8/4
010e2110 42424242 42424242 42424242 42424242
010e2120 42424242 42424242 42424242 42424242
010e2130 42424242 42424242 42424242 42424242
010e2140 42424242 42424242 42424242 42424242
010e2150 42424242 42424242 42424242 42424242
010e2160 42424242 42424242 42424242 42424242
010e2170 42424242 42424242 42424242 42424242
010e2180 42424242 42424242 42424242 42424242
010e2190 6a309389 0000880d 010dee10 010d1db8
010e21a0 41414141 41414141 41414141 41414141
010e21b0 41414141 41414141 41414141 41414141
010e21c0 41414141 41414141 41414141 41414141
010e21d0 41414141 41414141
Damn, it looks like we missed our target with 8 bytes (as I said, I suck at math… but perhaps I should have used a 0x60 byte chunk instead of 0x68… ) anyway I’m sure you get the point…
Nice – but all of this was only possible because none of the sizes involved were managed by the LFH already, and because we were able to avoid the LFH from being triggered.
Perhaps this is the right time to look at the LFH under WIndows 10 and see how it behaves. After all, we may not always have the luxury of avoiding the LFH in the first place.
In this first exercise, we’ll examine if it still takes 0x12 (18) consecutive allocations for a size in the same bucket before the LFH will start taking care of allocations and frees of those sizes.
In order to avoid any influencing or assumptions, I’ll use chunksizes that has not been allocated yet. (0x1500 bytes, 0x2100 bytes, 0x3000 bytes, 0x800 bytes)
I’ll combine a couple of tests in this test:
Step1: how many allocations are needed to trigger LFH
App:
C:\Users\corelan\Desktop\vc++\win10\LFH_Alloc1\Release>LFH_Alloc1.exe Default process heap found at 0x00D30000 Press a key to start... [1] Allocated chunk of 0x1500 bytes at 0x00D4CF68 [2] Allocated chunk of 0x1500 bytes at 0x00D4E470 [3] Allocated chunk of 0x1500 bytes at 0x00D4F978 [4] Allocated chunk of 0x1500 bytes at 0x00D50E80 [5] Allocated chunk of 0x1500 bytes at 0x00D52388 [6] Allocated chunk of 0x1500 bytes at 0x00D53890 [7] Allocated chunk of 0x1500 bytes at 0x00D54D98 [8] Allocated chunk of 0x1500 bytes at 0x00D562A0 [9] Allocated chunk of 0x1500 bytes at 0x00D577A8 [10] Allocated chunk of 0x1500 bytes at 0x00D58CB0 [11] Allocated chunk of 0x1500 bytes at 0x00D5A1B8 [12] Allocated chunk of 0x1500 bytes at 0x00D5B6C0 [13] Allocated chunk of 0x1500 bytes at 0x00D5CBC8 [14] Allocated chunk of 0x1500 bytes at 0x00D5E0D0 [15] Allocated chunk of 0x1500 bytes at 0x00D5F5D8 [16] Allocated chunk of 0x1500 bytes at 0x00D60AE0 [17] Allocated chunk of 0x1500 bytes at 0x00D61FE8 [18] Allocated chunk of 0x1500 bytes at 0x00D6B348 [19] Allocated chunk of 0x1500 bytes at 0x00D64A20 [20] Allocated chunk of 0x1500 bytes at 0x00D69E40 [21] Allocated chunk of 0x1500 bytes at 0x00D6C850 [22] Allocated chunk of 0x1500 bytes at 0x00D63518 [23] Allocated chunk of 0x1500 bytes at 0x00D6DD58 [24] Allocated chunk of 0x1500 bytes at 0x00D67430 [25] Allocated chunk of 0x1500 bytes at 0x00D6F260 [26] Allocated chunk of 0x1500 bytes at 0x00D65F28 [27] Allocated chunk of 0x1500 bytes at 0x00D70768 [28] Allocated chunk of 0x1500 bytes at 0x00D68938 [29] Allocated chunk of 0x1500 bytes at 0x00D71C70 [30] Allocated chunk of 0x1500 bytes at 0x00D77438 Press return to continue
If you pay close attention to the addresses, you can see a bigger gap between allocations 17 and 18. This is a good indication that allocations are no longer individual chunks inside a normal segment, but are now being consumed from an LFH subsegment. To be sure, let’s validate the findings in WinDBG.
Simply run !heap -x on all addresses (starting from the first one in the list), until you find the first one that has the "LFH" marker.
Allocation 17:
0:001> !heap -x 0x00D61FE8
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
00d61fe0 00d61fe8 00d30000 00d30000 1508 1508 8 busy
Allocation 18:
0:001> !heap -x 0x00D6B348
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
00d6b340 00d6b348 00d30000 00d3ae28 1508 - 8 LFH;busy
So – it looks like the LFH takes over with allocation 18.
Step 2: will the LFH trigger be influenced if another allocation (of a size in a different bucket) occurs in the series of 18 allocations.
App:
[1] Allocated chunk of 0x2100 bytes at 0x00D93500 [2] Allocated chunk of 0x2100 bytes at 0x00D95608 [3] Allocated chunk of 0x2100 bytes at 0x00D97710 [4] Allocated chunk of 0x2100 bytes at 0x00D99818 [5] Allocated chunk of 0x2100 bytes at 0x00D9B920 [6] Allocated chunk of 0x2100 bytes at 0x00D9DA28 [7] Allocated chunk of 0x2100 bytes at 0x00D9FB30 [8] Allocated chunk of 0x2100 bytes at 0x00DA1C38 [9] Allocated chunk of 0x2100 bytes at 0x00DA3D40 [10] Allocated chunk of 0x2100 bytes at 0x00DA5E48 Allocated chunk of 0x300 bytes at 0x00D49450 [11] Allocated chunk of 0x2100 bytes at 0x00DA7F50 [12] Allocated chunk of 0x2100 bytes at 0x00DAA058 [13] Allocated chunk of 0x2100 bytes at 0x00DAC160 [14] Allocated chunk of 0x2100 bytes at 0x00DAE268 [15] Allocated chunk of 0x2100 bytes at 0x00DB0370 [16] Allocated chunk of 0x2100 bytes at 0x00DB2478 [17] Allocated chunk of 0x2100 bytes at 0x00DB4580 [18] Allocated chunk of 0x2100 bytes at 0x00DC10D8 [19] Allocated chunk of 0x2100 bytes at 0x00DBAAC0 [20] Allocated chunk of 0x2100 bytes at 0x00DC32E0 Press return to continue
Focusing on allocation 17 and 18 (of 0x2100 bytes), we can see that alloc 17 was not LFH yet, but the 18th one did it again, despite the allocation of a 0x300 byte chunk in the middle of the series.
0:001> !heap -x 0x00DB4580
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00db4578 00db4580 00d30000 00d30000 2108 2108 8 busy
0:001> !heap -x 0x00DC10D8
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00dc10d0 00dc10d8 00d30000 00d3ae78 2208 - 3f LFH;busy
Interesting indeed :)
Step 3: will a free (of a different chunk size) influence the LFH trigger ?
App:
[1] Allocated chunk of 0x3000 bytes at 0x00DD6690
[2] Allocated chunk of 0x3000 bytes at 0x00DD9698 [3] Allocated chunk of 0x3000 bytes at 0x00DDC6A0 [4] Allocated chunk of 0x3000 bytes at 0x00DDF6A8 [5] Allocated chunk of 0x3000 bytes at 0x00DE26B0 [6] Allocated chunk of 0x3000 bytes at 0x00DE56B8 [7] Allocated chunk of 0x3000 bytes at 0x00DE86C0 [8] Allocated chunk of 0x3000 bytes at 0x00DEB6C8 [9] Allocated chunk of 0x3000 bytes at 0x00DEE6D0 [10] Allocated chunk of 0x3000 bytes at 0x00DF16D8 Freed chunk at 0x00D49C80 Freed chunk at 0x00D4A188 [11] Allocated chunk of 0x3000 bytes at 0x00DF46E0 [12] Allocated chunk of 0x3000 bytes at 0x00DF76E8 [13] Allocated chunk of 0x3000 bytes at 0x00DFA6F0 [14] Allocated chunk of 0x3000 bytes at 0x00DFD6F8 [15] Allocated chunk of 0x3000 bytes at 0x00E00700 [16] Allocated chunk of 0x3000 bytes at 0x00E03708 [17] Allocated chunk of 0x3000 bytes at 0x00E06710 [18] Allocated chunk of 0x3000 bytes at 0x00E0C748 [19] Allocated chunk of 0x3000 bytes at 0x00E15760 [20] Allocated chunk of 0x3000 bytes at 0x00E1B770 Done...
WinDBG (focus on allocation 17 and 18 again):
0:001> !heap -x 0x00E06710
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00e06708 00e06710 00d30000 00d30000 3008 3008 8 busy
0:001> !heap -x 0x00E0C748
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00e0c740 00e0c748 00d30000 00d3aea0 3008 - 8 LFH;busy
Interesting once more:)
Step 4: free a chunk from the same bucket during the allocation series.
[1] Allocated chunk of 0x800 bytes at 0x004EAE70
[2] Allocated chunk of 0x800 bytes at 0x0053E910 [3] Allocated chunk of 0x800 bytes at 0x0053F118 [4] Allocated chunk of 0x800 bytes at 0x0053F920 [5] Allocated chunk of 0x800 bytes at 0x00540128 [6] Allocated chunk of 0x800 bytes at 0x00540930 [7] Allocated chunk of 0x800 bytes at 0x00541138 [8] Allocated chunk of 0x800 bytes at 0x00541940 [9] Allocated chunk of 0x800 bytes at 0x00542148 [10] Allocated chunk of 0x800 bytes at 0x00542950 Freed chunk at 0x00542950 [11] Allocated chunk of 0x800 bytes at 0x00542950 [12] Allocated chunk of 0x800 bytes at 0x00543158 [13] Allocated chunk of 0x800 bytes at 0x00543960 [14] Allocated chunk of 0x800 bytes at 0x00544168 [15] Allocated chunk of 0x800 bytes at 0x00544970 [16] Allocated chunk of 0x800 bytes at 0x00545178 [17] Allocated chunk of 0x800 bytes at 0x00545980 [18] Allocated chunk of 0x800 bytes at 0x009F0070 [19] Allocated chunk of 0x800 bytes at 0x009F2090 [20] Allocated chunk of 0x800 bytes at 0x009F68D8 Done...
WinDBG (looking again at allocation 17 and 18)
0:004> !heap -x 0x00545980
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00545978 00545980 00450000 00450000 808 808 8 busy
0:004> !heap -x 0x009F0070
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 009f0068 009f0070 00450000 00458c60 808 - 8 LFH;busy
Interestingly enough, the free did not really impact the LFH trigger at all. 18 allocations still did the trick.
I guess more exhaustive testing is needed to confirm this behaviour, including checking what happens if there are more frees etc… but at least we are able to see some kind of pattern that indicates it may be more difficult to avoid that the LFH will be get enabled eventually, especially if you have no other option that to cause a certain number of allocations (more than 18) of chunks in the same bucket.
If you know of a way to prevent this from happening, please let me know :)
In the second exercise, we’ll see if the LFH still behaves the same way as under Windows 7 – i.e. returning freed chunks in a LIFO manner. We’ll activate the LFH using 20 allocations of 0x500 bytes. The last one will be freed, and then another allocation of 0x500 bytes will happen.
The goal is to see if the last one to be freed will be the first one to be returned again. (LIFO).
C:\Users\corelan\Desktop\vc++\win10\LFH_Alloc2\Release>LFH_Alloc2.exe
Default process heap found at 0x008D0000 Press a key to start...
[1] Allocated chunk of 0x500 bytes at 0x008E0440
[2] Allocated chunk of 0x500 bytes at 0x008E0948 [3] Allocated chunk of 0x500 bytes at 0x008E0E50 [4] Allocated chunk of 0x500 bytes at 0x008E1358 [5] Allocated chunk of 0x500 bytes at 0x008E1860 [6] Allocated chunk of 0x500 bytes at 0x008E1D68 [7] Allocated chunk of 0x500 bytes at 0x008E2270 [8] Allocated chunk of 0x500 bytes at 0x008E2778 [9] Allocated chunk of 0x500 bytes at 0x008E2C80 [10] Allocated chunk of 0x500 bytes at 0x008E3188 [11] Allocated chunk of 0x500 bytes at 0x008E3690 [12] Allocated chunk of 0x500 bytes at 0x008E3B98 [13] Allocated chunk of 0x500 bytes at 0x008E40A0 [14] Allocated chunk of 0x500 bytes at 0x008E45A8 [15] Allocated chunk of 0x500 bytes at 0x008E4AB0 [16] Allocated chunk of 0x500 bytes at 0x008E4FB8 [17] Allocated chunk of 0x500 bytes at 0x008E54C0 [18] Allocated chunk of 0x500 bytes at 0x008E7D28 [19] Allocated chunk of 0x500 bytes at 0x008E5EF8 [20] Allocated chunk of 0x500 bytes at 0x008E6E10 Press return to continue
Freed chunk at 0x008E6E10
Press return to continue
Allocated chunk of 0x500 bytes at 0x008E9148
Press return to continue
In WinDBG, we can see that the LFH was indeed activated, but it also looks like the LIFO is behavior from Windows 7 is no longer there.
0:003> !heap -x 0x008E6E10
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
008e6e08 008e6e10 008d0000 008d8d90 508 - 0 LFH;free0:003> !heap -x 0x008E9148
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
008e9140 008e9148 008d0000 008d8d90 508 - 8 LFH;busy
Perhaps causing a series of allocations of 0x500 byte will allow us to take the freed chunk back. Press return to cause another 20 allocations of 0x500 bytes and see what happens:
Allocated chunk of 0x500 bytes at 0x008E59F0
Allocated chunk of 0x500 bytes at 0x008E7318 Allocated chunk of 0x500 bytes at 0x008E8738 Allocated chunk of 0x500 bytes at 0x008E6400 Allocated chunk of 0x500 bytes at 0x008E8C40 Allocated chunk of 0x500 bytes at 0x008E6908 Allocated chunk of 0x500 bytes at 0x008E7820 Allocated chunk of 0x500 bytes at 0x008E8230 Allocated chunk of 0x500 bytes at 0x008E6E10 --- got it back --- Allocated chunk of 0x500 bytes at 0x008EC740 Allocated chunk of 0x500 bytes at 0x008EEA78 Allocated chunk of 0x500 bytes at 0x008EBD30 Allocated chunk of 0x500 bytes at 0x008EC238 Allocated chunk of 0x500 bytes at 0x008EDB60 Allocated chunk of 0x500 bytes at 0x008EF488 Allocated chunk of 0x500 bytes at 0x008ECC48 Allocated chunk of 0x500 bytes at 0x008F0DB0 Allocated chunk of 0x500 bytes at 0x008E99F8 Allocated chunk of 0x500 bytes at 0x008EEF80 Allocated chunk of 0x500 bytes at 0x008EB828 Press return to continue
In this case, it took another 9 allocations to get the freed chunk back. In fact, if you’d run the same application a couple of times, you’ll see that the number of times it takes to get the freed chunk back, varies largely between 0 (sometimes you’ll get it back LIFO style) and never (at least, not in the first 20 allocations or so)… but in most cases I got it back within the first 10 allocations. (A lot more structured testing would be needed to find the sweet spot that would provide some sort of predictability. It’ll probably never be 100% reliable, but it may not be too messy either.)
Update (7/7/2016) – I added "LFH_TakeBack" to the github repository, which will automate some statistic gathering. For each chunksize between 8 and 0x4000, it will enable LFH, alloc a chunk and free it again, and then measure how many allocations are needed to take it back. The application calculates an average, a minimum and maximum number of tries, and also keeps track how many times the object was not taken back within the first 2000 allocations.
After running the app, it looks like the maximum number of allocations needed sits around 50.
Anyway, looking at the addresses of the allocations, it also looks like the chunks are no longer adjacent (as they were in Windows 7, at least as long as the chunks are inside the same subsegment).
This will certainly make it more complex to create a specific layout/sequence of objects when the LFH is enabled.
Is LFH still limited to 0x4000 byte chunks (max)?
App:
C:\Users\corelan\Desktop\vc++\win10\LFH_Alloc3\Release>LFH_Alloc3.exe
Default process heap found at 0x00930000 Press a key to start...
[1] Allocated chunk of 0x4000 bytes at 0x00940FF8
[2] Allocated chunk of 0x4000 bytes at 0x00945000 [3] Allocated chunk of 0x4000 bytes at 0x00949008 [4] Allocated chunk of 0x4000 bytes at 0x0094D010 [5] Allocated chunk of 0x4000 bytes at 0x00951018 [6] Allocated chunk of 0x4000 bytes at 0x00955020 [7] Allocated chunk of 0x4000 bytes at 0x00959028 [8] Allocated chunk of 0x4000 bytes at 0x0095D030 [9] Allocated chunk of 0x4000 bytes at 0x00961038 [10] Allocated chunk of 0x4000 bytes at 0x00965040 [11] Allocated chunk of 0x4000 bytes at 0x00969048 [12] Allocated chunk of 0x4000 bytes at 0x0096D050 [13] Allocated chunk of 0x4000 bytes at 0x00971058 [14] Allocated chunk of 0x4000 bytes at 0x00975060 [15] Allocated chunk of 0x4000 bytes at 0x00979068 [16] Allocated chunk of 0x4000 bytes at 0x0097D070 [17] Allocated chunk of 0x4000 bytes at 0x00981078 [18] Allocated chunk of 0x4000 bytes at 0x0098D0B8 [19] Allocated chunk of 0x4000 bytes at 0x009950C8 [20] Allocated chunk of 0x4000 bytes at 0x009910C0 Press return to continue
[1] Allocated chunk of 0x4008 bytes at 0x009C7008
[2] Allocated chunk of 0x4008 bytes at 0x009CB018 [3] Allocated chunk of 0x4008 bytes at 0x009CF028 [4] Allocated chunk of 0x4008 bytes at 0x009D3038 [5] Allocated chunk of 0x4008 bytes at 0x009D7048 [6] Allocated chunk of 0x4008 bytes at 0x009DB058 [7] Allocated chunk of 0x4008 bytes at 0x009DF068 [8] Allocated chunk of 0x4008 bytes at 0x009E3078 [9] Allocated chunk of 0x4008 bytes at 0x009E7088 [10] Allocated chunk of 0x4008 bytes at 0x009EB098 [11] Allocated chunk of 0x4008 bytes at 0x009EF0A8 [12] Allocated chunk of 0x4008 bytes at 0x009F30B8 [13] Allocated chunk of 0x4008 bytes at 0x009F70C8 [14] Allocated chunk of 0x4008 bytes at 0x009FB0D8 [15] Allocated chunk of 0x4008 bytes at 0x009FF0E8 [16] Allocated chunk of 0x4008 bytes at 0x00A030F8 [17] Allocated chunk of 0x4008 bytes at 0x00A07108 [18] Allocated chunk of 0x4008 bytes at 0x00A0B118 [19] Allocated chunk of 0x4008 bytes at 0x00A0F128 [20] Allocated chunk of 0x4008 bytes at 0x00A13138 Press return to continue
WinDBG: 0x4000 bytes
0:001> !heap -x 0x00981078
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00981070 00981078 00930000 00930000 4008 4008 8 busy
0:001> !heap -x 0x0098D0B8
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 0098d0b0 0098d0b8 00930000 00938c48 4008 - 8 LFH;busy
WinDBG: 0x4008 bytes
0:001> !heap -x 0x00A07108
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00a07100 00a07108 00930000 00930000 4010 4010 8 busy
0:001> !heap -x 0x00A0B118
Entry User Heap Segment Size PrevSize Unused Flags ----------------------------------------------------------------------------- 00a0b110 00a0b118 00930000 00930000 4010 4010 8 busy
Answer: YES, 0x4000 seems to be the maximum size (just like on Windows 7)
The LFH_Alloc* exercises demonstrate that chunks are no longer allocated in an consecutive manner inside a LFH subsegment. Of course, this complicates creating a specific layout within the subsegment. I still wanted to know if it would be possible to replace the memory space used by a LFH chunk by an LFH allocation of a size from a different bucket. As an example, can I take the space of a 0x58 byte chunk using a 0x88 byte allocation, within the LFH.
As the LFH subsegments are used to keep chunks of the same bucket size together, this would require clearing out the entire subsegment used for storing the vulnerable object, and hopefully the Heap Manager will reuse those pages for another subsegment (allocations for a different bucket size).
LFH_TakeBack2 demonstrates if it works or not. The idea is to try to place the "vulnerable" object in a subsegment where you control all the other chunks. As soon as the vulnerable object gets freed, you cause all the other chunks to be freed as well. Hopefully, this will also release the entire subsegment and its pages, so they can be reused again (even for a subsegment associated with chunksizes that fall in a different bucket).
App:
C:\Users\corelan\Desktop\vc++\win10_heap\LFH_TakeBack2\Release>LFH_TakeBack2.exe
Vulnerable object of 0x00000058 bytes at 0x01136B98, filled with 'A'
Allocations done. Press return to start free process
WinDBG:
0:003> dd 0x01136B98
01136b98 41414141 41414141 41414141 41414141
01136ba8 41414141 41414141 41414141 41414141
01136bb8 41414141 41414141 41414141 41414141
01136bc8 41414141 41414141 41414141 41414141
01136bd8 41414141 41414141 41414141 41414141
01136be8 41414141 41414141 07fe9621 88011d00
01136bf8 00000000 00000000 00000000 00000000
01136c08 00000000 00000000 00000000 00000000
0:003> !heap -p -a 0x01136B98
address 01136b98 found in
_HEAP @ e30000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
01136b90 000c 0000 [00] 01136b98 00058 - (busy)
0:003> !heap -x 0x01136B98
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
01136b90 01136b98 00e30000 00e45638 60 - 8 LFH;busy
Continue with App:
Vulnerable object at 0x01136B98 was freed
Free done. Press return to start new allocations (size 0x00000088)Allocations done. Check if 0x01136B98 contains 'B' now
WinDBG:
0:001> dd 0x01136B98
01136b98 42424242 42424242 42424242 42424242
01136ba8 42424242 42424242 42424242 42424242
01136bb8 42424242 42424242 42424242 42424242
01136bc8 42424242 42424242 42424242 42424242
01136bd8 42424242 42424242 42424242 42424242
01136be8 42424242 42424242 42424242 42424242
01136bf8 42424242 42424242 42424242 42424242
01136c08 42424242 42424242 07f296dd 8800bf00
0:001> !heap -x 0x01136B98
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
01136b80 01136b88 00e30000 00e38ca0 90 - 8 LFH;busy
Size is now 0x90. So, the mechanism still works (just like in older Windows versions). Of course, because the sequence of chunks inside a segment is not fully under our control, it will be difficult to control specific bytes of the freed object when you replace it with a different sized chunk.
What do virtualallocdblock chunks/allocations look like under WIndows 10?
In order to create such chunks, we need to cause HeapAlloc/RtlAllocateHeap allocations of a size that is larger than the VirtualMemoryThreshold value in the heap header. In the example application, I am triggering 20 allocations of 0x7ffb0 bytes (which is larger than the 7ff00 byte threshold).
App:
C:\Users\corelan\Desktop\vc++\win10\Large_Alloc1\Release>Large_Alloc1.exe
Default process heap found at 0x00E20000 Press a key to start...
[1] Allocated chunk of 0x7ffb0 bytes at 0x00C48020
[2] Allocated chunk of 0x7ffb0 bytes at 0x01110020 [3] Allocated chunk of 0x7ffb0 bytes at 0x011A1020 [4] Allocated chunk of 0x7ffb0 bytes at 0x0123F020 [5] Allocated chunk of 0x7ffb0 bytes at 0x01326020 [6] Allocated chunk of 0x7ffb0 bytes at 0x013BA020 [7] Allocated chunk of 0x7ffb0 bytes at 0x0144C020 [8] Allocated chunk of 0x7ffb0 bytes at 0x014D2020 [9] Allocated chunk of 0x7ffb0 bytes at 0x0156D020 [10] Allocated chunk of 0x7ffb0 bytes at 0x015F4020 [11] Allocated chunk of 0x7ffb0 bytes at 0x01680020 [12] Allocated chunk of 0x7ffb0 bytes at 0x01712020 [13] Allocated chunk of 0x7ffb0 bytes at 0x017AB020 [14] Allocated chunk of 0x7ffb0 bytes at 0x0183B020 [15] Allocated chunk of 0x7ffb0 bytes at 0x018C9020 [16] Allocated chunk of 0x7ffb0 bytes at 0x01957020 [17] Allocated chunk of 0x7ffb0 bytes at 0x019EA020 [18] Allocated chunk of 0x7ffb0 bytes at 0x01A75020 [19] Allocated chunk of 0x7ffb0 bytes at 0x01B09020 [20] Allocated chunk of 0x7ffb0 bytes at 0x01B9D020 Press return to continue
WinDBG: (output of !heap -p -h
, limited to information related with VirtualAllocdBlocks)VirtualAllocdBlocks @ e2009c
00c48018 10000 0004 [00] 00c48020 7ffb0 - (busy VirtualAlloc) 01110018 10000 0000 [00] 01110020 7ffb0 - (busy VirtualAlloc) 011a1018 10000 0000 [00] 011a1020 7ffb0 - (busy VirtualAlloc) 0123f018 10000 0000 [00] 0123f020 7ffb0 - (busy VirtualAlloc) 01326018 10000 0000 [00] 01326020 7ffb0 - (busy VirtualAlloc) 013ba018 10000 0000 [00] 013ba020 7ffb0 - (busy VirtualAlloc) 0144c018 10000 0000 [00] 0144c020 7ffb0 - (busy VirtualAlloc) 014d2018 10000 0000 [00] 014d2020 7ffb0 - (busy VirtualAlloc) 0156d018 10000 0000 [00] 0156d020 7ffb0 - (busy VirtualAlloc) 015f4018 10000 0000 [00] 015f4020 7ffb0 - (busy VirtualAlloc) 01680018 10000 0000 [00] 01680020 7ffb0 - (busy VirtualAlloc) 01712018 10000 0000 [00] 01712020 7ffb0 - (busy VirtualAlloc) 017ab018 10000 0000 [00] 017ab020 7ffb0 - (busy VirtualAlloc) 0183b018 10000 0000 [00] 0183b020 7ffb0 - (busy VirtualAlloc) 018c9018 10000 0000 [00] 018c9020 7ffb0 - (busy VirtualAlloc) 01957018 10000 0000 [00] 01957020 7ffb0 - (busy VirtualAlloc) 019ea018 10000 0000 [00] 019ea020 7ffb0 - (busy VirtualAlloc) 01a75018 10000 0000 [00] 01a75020 7ffb0 - (busy VirtualAlloc) 01b09018 10000 0000 [00] 01b09020 7ffb0 - (busy VirtualAlloc) 01b9d018 10000 0000 [00] 01b9d020 7ffb0 - (busy VirtualAlloc)
Yes, we can still trigger this kind of allocations. Due to the nature of this type of allocations they are still placed at the start of a fresh new page (which is why we’re seeing the start address alignments). On the other hand, the gaps between 2 allocations seems to be larger than under Windows 7.
This means that it will become harder to use this type of allocations to fill up a larger memory region as part of a heap spray. There will be much bigger holes in between allocations, and the locations of the holes are also non-predictable, which means we may not be able to rely on absolute heap spray addresses as much as we could in Windows 7.
Perhaps a larger series of allocations is needed, and a larger number of runs, to find "sweet spots", addresses that are allocated more often than others. Not sure what kind of percentage of predictability we may be able to obtain, but it might be worth the try.
So… can we do a precise heap spray under Windows 10?
Well…. yes. The key is to avoid LFH, and to avoid virtualallocdblocks as well.
Use a "sweet" size and a "sweet" number of allocs to get aligned consecutive allocations (starting at ????0048) as a normal chunk, inside a normal segment. Perhaps the first few allocations won’t start at that aligned address (because there are already some smaller allocations in the segment), but as soon as the allocations trigger the creation of another segment, and you manage to take the first spot, your allocations should be aligned.
App:
C:\Users\corelan\Desktop\vc++\win10\Large_Alloc2\Release>Large_Alloc2.exe
Default process heap found at 0x012B0000 Press a key to start...
[1] Allocated chunk at 0x012C0FF8
[2] Allocated chunk at 0x01300FF8 [3] Allocated chunk at 0x01340FF8 [4] Allocated chunk at 0x015B0048 [5] Allocated chunk at 0x015F0048 [6] Allocated chunk at 0x01630048 [7] Allocated chunk at 0x016B0048 [8] Allocated chunk at 0x016F0048 [9] Allocated chunk at 0x01730048 [10] Allocated chunk at 0x01770048 [11] Allocated chunk at 0x017B0048 [12] Allocated chunk at 0x017F0048 [13] Allocated chunk at 0x01830048 [14] Allocated chunk at 0x018B0048 [15] Allocated chunk at 0x018F0048 [16] Allocated chunk at 0x01930048 [17] Allocated chunk at 0x01970048 [18] Allocated chunk at 0x019B0048 [19] Allocated chunk at 0x019F0048 [20] Allocated chunk at 0x01A30048 [21] Allocated chunk at 0x01A70048 [22] Allocated chunk at 0x01AB0048
...
WinDBG:
0:003> d 0c0c0c0c
0c0c0c0c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c1c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c2c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c3c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c4c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c5c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c6c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0c0c0c7c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
To turn this into a precise heap spray, we need to overcome the fact that we don’t know the exact start address of the first one. As we know that the start addresses will be aligned eventually to the start of a page, and we can control the size of the allocations, we simply have to repeat the same structure (junk + ROP + shellcode + junk) every 0x1000 bytes inside each allocation (as explained in the heap spray tutorials on this site). This should allow you to put/find your content at a predictable address. The same logic applies if you need to put specific values at specific places… simply repeat the layout every 0x1000 bytes and you should be fine.
In "Precise_Spray", the goal is to put marker "$$$$" (\x24\x24\x24\x24) at 0x0c0c0c0c:
C:\Users\corelan\Desktop\vc++\win10_heap\Precise_Spray\Release>Precise_Spray.exe Default process heap found at 0x00550000 Press a key to start...
Spray done, check 0x0c0c0c0c >> Contents at 0x0c0c0c0c: 24242424
0:003> db 0c0c0c0c 0c0c0c0c 24 24 24 24 20 20 20 20-20 20 20 20 20 20 20 20 $$$$ 0c0c0c1c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20 0c0c0c2c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20 0c0c0c3c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20 0c0c0c4c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20 0c0c0c5c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20 0c0c0c6c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20 0c0c0c7c 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20
Of course, in a complex/multithreaded application, there will be ‘noise’ (other allocations and frees) at the same time your heapspray is running, and which could affect the placement of your allocations within the segment. A possible approach could be to cause some large allocations first (50 allocations of 0x1ff00 bytes or so… any big size, smaller than the chunk size you’re using for the actual spray), each time followed by a small allocation (which we will keep allocated, to avoid that the big ones get coalesced), and then free the large ones. That way, the application can use those freed chunks, split them, consume them, without bothering your aligned spray at all.
I’ve had good results with spraying using chunk sizes of 0x20000-8 bytes, and 0x40000-8 bytes, but I guess any similar aligned size that is a multiple of a page size will work.
Good luck y’all. <3
Peter
Oh yeah, before I forget, please check out:
https://facebook.com/demandglobalchange // https://bit.ly/demandglobalchange_full // https://bit.ly/demandglobalchange
Read & share. Give people reasons to live, not to die for. thank you.
© 2016 – 2021, Peter Van Eeckhoutte (corelanc0d3r). All rights reserved.