OSEE study materials
OSEE study resources

Undersized allocation with a delimiter-terminated copy

OSEE · Put your knowledge to work in context.

All case studies

Undersized allocation with a delimiter-terminated copy

A 32-bit Windows service accepts a request consisting of a 2-byte declared-length field, a flags byte, and data, terminated by a 0xFF delimiter. The handler allocates a buffer using the declared length, then, before copying, allocates a 0x100-byte reply context whose callback function pointer sits at offset 0x80 of its user data. Only after both allocations does it copy the data field into the request buffer, byte by byte, stopping at the delimiter with no bound check against the declared length. The declared length is never compared with the actual data length. The handler is single threaded per connection and the heap is fresh at connection start. A typical request declares 0x40 and delivers more, and the process dies with a heap access violation. All modules are rebased, DEP is enforced, and the reply context is a heap object rather than a stack one.

1. Compute exactly how many data bytes you send to land on the reply context's callback pointer, and what you must not disturb on the way there.

Read the worked answer

Walk the heap geometry from the request buffer's user address.

The buffer is 0x40 bytes of user data, so its user region spans offsets 0x00 to 0x3F. Immediately above it is the header of the next chunk, 8 bytes on x86, spanning 0x40 to 0x47: a 4-byte size field and a 4-byte flags/previous-size field. The reply context begins at 0x48 and its callback is 0x80 bytes further in, so the callback dword starts at 0x48 + 0x80 = 0xC8.

To overwrite that dword you must deliver 0xC8 + 4 = 0xCC data bytes before the delimiter. Sending exactly 0xCC bytes and then 0xFF leaves the callback holding your four bytes and leaves everything past it untouched.

The critical constraint is the 8 bytes at 0x40 to 0x47. A 0x100-byte allocation has a chunk size of 0x108, so the size field must contain 0x00000108 and the second dword must remain a plausible value (zero is safe for a chunk that is not preceded by a free neighbour). If you overwrite those with 0x41414141, the very next heap operation walks a bogus size and you get an access violation long before the callback is ever invoked. The payload is therefore mostly padding plus two carefully chosen dwords, not a uniform pattern, and the exact byte count matters more than the content.

2. Why does sending far more data than the minimum not simply improve your odds?

Read the worked answer

Because everything past the callback pointer is writing into memory you do not control or understand, and the process has to survive long enough to invoke the callback.

Concretely, every byte past 0xCC lands in the reply context's tail or in whatever chunk follows it. Those chunks have headers, and those headers are read on the next allocation or free. Overwriting them typically produces an immediate heap corruption fault inside the allocator, which pre-empts the call path you were aiming at. Even when the process survives, the extra writes may corrupt the reply context fields that the caller uses before invoking the callback, so the callback never gets reached.

There is also a framing consequence. The copy stops at the delimiter, so the number of bytes written is exactly the distance to your 0xFF. You have byte-exact control by construction and no reason to overflow more deeply than necessary. Precision here is cheap: it costs nothing to count to 0xCC.

A good discipline is to make the first attempt minimal and confirm the callback holds your value, with a breakpoint on the indirect call. Only then expand. Sending 0x1000 bytes and hoping is how you end up unable to tell heap corruption from an address problem, and those two failures look identical in a crash dump if you have overwritten enough of the heap.

3. The callback now holds your four bytes, but every module is rebased. What are your realistic options, and which would you test first?

Read the worked answer

You have a single controlled dword written to a heap object and full control of the request bytes, which in practice means you also control the reply context's user data if you extend the write carefully. That is a stronger primitive than it looks, because the object is heap-resident and the heap is sprayed by your own requests.

The hard problem is the address. With every module rebased you cannot hardcode a gadget. Options, in the order worth testing:

  1. Leak first. Look for a path that returns heap or image contents to the client before the callback fires. The reply context exists to build a response, and if any field of it is echoed, you have both a heap address and often an image pointer. This is the cheapest and most reliable route and should be checked before anything else.
  2. Point the callback at a heap address you have filled with a ROP chain. This requires knowing the heap address, which requires the leak, so it collapses into option 1.
  3. If the service exposes any unrebased mapping, use it. Check the module list rather than assuming; a statically linked CRT or an old third-party DLL at a default base is common and instantly removes the address problem.

What you should not do is assume a fixed heap base. The heap base is randomized per process, and a spray that lands at a predictable address on Windows 7 SP1 will not hold on a machine with different allocation history.