Use-after-free in a 0x38-byte LFH bucket
A 32-bit Windows 7 SP1 document viewer parses a proprietary .acm format. Each node record causes the parser to malloc 0x38 bytes, copy up to 8 attacker bytes into the name field at offset 0x08, and link the object into a global list. A DEL record frees the node but leaves the list entry dangling. A later RENDER opcode walks the list and calls the function pointer at offset 0x14 of every entry. The file format lets you emit node, DEL and RENDER records in any order and in any quantity, up to a 1 MB file cap. DEP is on across the process, ASLR is on everywhere with no non-ASLR module, and the low-fragmentation heap is active for this process. The status bar displays the name of the last rendered node using a %s conversion, and the name field is not guaranteed to be null-terminated. You can reproduce a clean crash at an address you fully control.
1. Describe the allocation/free sequence you would use to get attacker-controlled bytes over the freed 0x38 chunk, and explain why simply sending a second node record usually fails.
Read the worked answer
The naive sequence is: node, DEL, node, RENDER. It fails for a few compounding reasons. The freed 0x38 chunk is not returned to the same place a fresh 0x38 request is served from once the bucket has history: the LFH serves from per-bucket free lists whose order is influenced by how many chunks of that size were freed and how many were cached, and the front-end lookaside list also holds recently freed chunks. A single free followed by a single allocation often does land in the right slot, but the timing is sensitive to parser-internal allocations (the list node, the file buffer, the string copies) that also consume the bucket.
A robust sequence:
- Prime the bucket with a batch of node creates so the LFH has a populated, stable bucket rather than falling back to the backend allocator.
- Free a contiguous run of victims (DEL on many records) rather than one.
- Immediately re-allocate the same number of 0x38 objects whose name field carries your controlled bytes at offset 0x08, and whose offset 0x14 you also control if the record format lets you write past the name.
- RENDER. With many freed slots and many refills, at least one dangling list entry will point at a chunk you overwrote.
The 8-byte name is the usual limitation: offsets 0x08 through 0x0F are yours, but the function pointer at 0x14 is not reachable through the name field alone unless the refill object is a different structure with attacker-controlled content at that offset. If no structure writes at 0x14, the primitive you actually have is a controlled 8-byte overwrite of whatever follows the name field, and the whole plan has to be re-evaluated around that.
2. Assume you can overwrite the function pointer at offset 0x14 with four bytes of your choosing. What do you point it at, given that ASLR is fully enabled and every module is rebased?
Read the worked answer
With no non-ASLR module anywhere, a hardcoded gadget address is worthless, so the overwrite by itself only produces the crash you already have. You need an address whose value you can learn before you send the final file.
The status bar is the leak. The %s conversion on a name field that is not null-terminated reads past offset 0x10 into whatever follows on the heap, which for a freed or adjacent chunk includes heap pointers and, when the chunk overlaps a structure holding a module pointer, a mapped-image address. Sending a node, rendering it, and reading the displayed string gives a heap address and often an image address. That converts the bug into two stages: a leak file, then an exploit file.
With the leak in hand, the function pointer should point at a stack pivot or directly at a payload you have already placed. Two viable targets:
- A pivot such as
push esp; retorxchg eax, espin the now-known image, landing on a ROP chain you have sprayed into the heap. Because the call is made with the object pointer in hand, registers at call time matter; verify in the debugger which register holds what rather than assuming. - A sprayed heap address containing an executable region, which only works if you have also defeated DEP (VirtualAlloc via ROP, or a JIT region if the viewer has a script engine).
What you should not do is lean on a 0x0c0c0c0c-style spray. On 32-bit with LFH and heap randomization, a spray of that shape is unreliable and will look like it works in the lab and fail on the third run.
3. After a failed attempt the process faults reading address 0x41414145. Explain what that tells you about how far your exploit got.
Read the worked answer
0x41414145 is the little-endian dword AAAA followed by 0x45, which is the byte immediately after a four-byte run of your filler. That pattern says the chunk was successfully reused with your data, and that the dword consumed by the indirect call is a combination of the 8-byte name field plus one byte of whatever object you refilled with. In other words, the grooming worked and the control-flow hijack fired against a value of your choosing.
So the failure is not in heap layout. It is in two other places. First, offset arithmetic: the call reads offset 0x14, and if the refill structure put your name at 0x08, then 0x14 falls inside your 8-byte name only if your copy overran its own bounds. The value 0x41414145 means you are off by a byte somewhere. Second, ASLR: even a perfectly chosen 0x41414141 shows you had no valid code address to supply.
The useful conclusion is that the whole heap-side problem is solved and only the address problem remains, which is exactly what the leak is for. That is a very different state from a fault at 0x00000000 or at a lookaside-list pointer, both of which would mean the freed chunk was recycled by the allocator instead of by you. Distinguish those two cases before you change anything, because they lead to opposite fixes.