OSED study materials
OSED study resources

Building an Arbitrary Read Primitive from a Format String

OSED · Put your knowledge to work in context.

All case studies

Building an Arbitrary Read Primitive from a Format String

A 32-bit Windows service has a format string bug in a response builder: sprintf(response, user_input) followed by send(client, response, strlen(response), 0). user_input is a 128-byte buffer read from the client and is null-terminated. The response buffer is 512 bytes. You need to read the 4-byte value at an arbitrary address to defeat ASLR; for example, the address of MessageBoxA in user32.dll. You can send multiple requests. The service is 32-bit and runs on Windows 10 x86. The format string is on the stack, and you do not know its address initially. The input cannot contain null bytes because it is read with recv into a zeroed buffer and then treated as a C string. You have confirmed the format string is reachable and that the response is sent back verbatim, but the service may truncate at 512 bytes.

1. Explain how to use the format string to read from an arbitrary address when you do not know the address of your own buffer. What role do positional arguments play?

Read the worked answer

First, leak stack addresses with %p to find a pointer to your own buffer. The format string is passed on the stack, so some positional argument (e.g., %8$p) will print a value that points into the stack near your buffer. By varying the number of %p specifiers, you can identify which argument index corresponds to a pointer to the buffer. Once you know that index, you can place the target address at the end of the format string and reference it with %<index>$s. For example, if %8$p leaks the address of the buffer, then the 8th argument is a pointer to the buffer; if you append the target address at offset N in the buffer, you use %<8+offset/4>$s to dereference it. This works because the format string's arguments are read from the stack, and the buffer contents are on the stack. Positional arguments let you skip the earlier stack slots and directly reach the address you control.

2. How do you handle the 128-byte input limit and the null-byte restriction when placing the target address?

Read the worked answer

The input limit means you must keep the format string short. Use a minimal leak like %8$s and place the 4-byte target address immediately after it. The null-byte restriction is the main obstacle: if the target address contains 0x00, you cannot include it directly because sprintf will stop at the null. Choose a target whose address has no null bytes. For a DLL function like MessageBoxA in user32.dll, the address is typically in the 0x7xxxxxxx range, so all four bytes are nonzero. If you must read an address with a null byte, you can use a two-stage approach: first use %n to write the address bytes into a stack location that you can reference, or use a partial address with %s on a pointer that is already on the stack. Another option is to use %hhn to write the null byte into a controlled location after the format string has been parsed, but that is more complex. For ASLR bypass, prefer targets without null bytes.

3. How do you turn the leaked bytes into a reliable ASLR bypass for the target? What checks would you perform?

Read the worked answer

The %s specifier will print bytes until it hits a null, so the 4-byte pointer may be truncated if any byte is zero. Since you chose a target with no null bytes, you should receive all four bytes, but they may be non-printable. Read the raw response, not a text-decoded version. Compare the leaked value across multiple requests; it should be stable for a running process. Identify which module the address belongs to by checking its high bits and known module ranges. To defeat ASLR for the exploit, you need the base of the module containing your ROP gadgets or the function you plan to call. If you read MessageBoxA, you know user32.dll's base by subtracting the known offset of MessageBoxA in that specific DLL version. Verify the offset against the target's DLL (same patch level). If the service restarts and ASLR re-randomizes, you must re-leak. Finally, use the leaked base to compute gadget addresses and build the ROP chain; test the chain in a debugger with the same DLL version to confirm the offsets.