Now that the Simple Calc is done, let’s try the complex one!
Basic information
From the organizers:
The file has exactly the same attributes as the Simple Calc:
The difference
They are different in just a few bytes:
Let’s have a look at 0x000156e0 + 0x00400000 = 0x004156e0:
The code is located in the free function. These two NOPs have replaced a test
in the original binary:
The code tests if rdi is equal to zero. That register holds the first and
only argument passed to free which is the pointer to
free.
If we try to execute the same payload that grilled the simple calculator, free
will end up dereferencing a memory area area that is not allocated and
segfault:
The ROP chain used in the Simple Calculator should work on that binary too
because the gadgets are the same and they are located at the same addresses. We
just need to find a way to trick free into believing that the address we give
him is a valid chunk. For that we need a memory region that is not affected by
ASLR and that we can control.
Memory region of interest
Each operation done by the calculator saves both operands and the result in global
variables. For example for the subtraction:
0x6c4ab0 holds the first operand, say sub_x
0x6c4ab4 holds the second operand, say sub_y
0x6c4ab8 holds the result of the subtraction, say sub_r
They all holds 32-bit values as explained in the Simple Calc write-up which
means that we have a controllable area of 12 bytes. We could have more with the
adjacent areas used for the other operation but we don’t need them.
Analyzing free
That free is part of the GNU C library (glibc) which means that we
don’t have to reverse it because the source code is available. The
patch that has been applied just removes the code on lines 2939 and 2940:
The function returns if the test in the following if is true:
mem2chunk takes a pointer to a memory area and returns the pointer to the
start of the chunk (i.e its header).
chunk_is_mapped is a macro that only checks if the second bit in the size
header of the chunk is set:
The second if is not that important, let’s skip over to the call to
munmap_chunk and have a look at that function:
The macro chunksize removes the low bits from the sizefield in the chunk
headers. Therefore the second bit that has to be set, will not be present in
the variable size in the above code.
The only way to hit the return, is to pass that if statement:
Therefore block bitwise OR’ed with total_size must have all its lowest
significant bit not set (i.e. equal to zero), which means that none of block
and total_size can have there lower bits set.
dl_pagesize = 0x1000 4k pages
block = p - p->prev_size the address of the header of the chunk minus the
size of the previous chunk
total_size = p->prev_size + size the size of the previous chunk plus the
size of the chunk
We can only control 12 bytes in the memory area of interest with the subtract
operation, but this is not a problem because in the above condition, none of
the higher bits are checked. We will layout the memory as follow:
If we use the above addresses in the condition that we have to pass, we have:
block = p - p->prev_size = 0x6c4ab0 - p->prev_size: block must end with
0x000 therefore prev_size ends with 0xab0
total_size = p->prev_size + size: total_size must end with 0x000
therefore size must end with 0x1000 - 0xab0 = 0x550
There is is problem here because the second bit of size would not be set and
we would not enter the first if we mentioned at the beginning of this
section. We can add it without any problem because, as mentioned above, the
macro chunksizewill remove it, which gives us the following values for the
operation:
sub_r = 0x552 = 1362
sub_x = 0xab0 = 2736
sub_y = sub_x - sub_r = 1374
The pointer to pass to free is the address of the memory region as given by a
call to malloc, not the address of the chunk that we used lately. The correct
address is 0x6c4ac0 because the fields size and
prev_size are located respectively 8 bytes and 16 bytes before it as shown at
the beginning of the free function:
Final payload
Compared to the payload of the Simple Calc, we have to modify the 18 dwords of
padding with the address corresponding to the fake chunk we will create and add
a last operation before triggering the buffer overflow with the option 5: