simple_calc offered a binary that expects us to make some calculations.
It will ask for a number of calculations (say N) to perform and will
malloc()Nx4 bytes in the heap. If we decompile with
IDA, it’ll look something like this:
Then a loop of N iterations will commence,
each iteration offering to perform one of the possible arithmetic operations,
ADD/SUB/MUL/DIV, or exit. Those operations perform pretty much what you expect of them,
which take in 2 DWORD operands, and apply the function. What is worth noticing is that
both operands and result are stored in the .bss (therefore at predictable
By exiting, simple_calc performs a memcpy() of the malloc-ed buffer (whose
length is controlled by us) into a stack buffer (of length 0x28 bytes) located
It is then easy to spot the trivial stack buffer overflow.
When an operation is finished, the resulting DWORD is stored inside the malloc-ed
buffer at the offset corresponding of the main loop iteration.
So the game here is to play with those (basic) arithmetic operations to
write arbitrary data in the malloc-ed buffer: for example, if we want to write
0x10001000 | 0x20002000 in our malloc-ed buffer, we would create 2 operations,
an ADD with op1=0x10000000 and op2=0x00001000
an ADD with op1=0x20000000 and op2=0x00002000
and so on
By calling successively the same arithmetic operation, say ADD (or any other),
we have a predictable way to populate the malloc-ed buffer.
To corrupt the memory we must fill the stack buffer entirely (40 bytes), so make
at least 10 operations. The stack buffer is followed (in the memory layout) by
variables, so we add 24 bytes of junk (3 QWORD), another QWORD for
overwriting the SFP, and a last to overwrite RIP.
We execute and a SIGSEGV was well caught (as seen with
However, the faulty instruction is in the free() following the memcpy() and
yet not in the return from the main function.
free() is trying to remove the chunk pointed by the value stored in $rdi (here
0x4444444444444444). However, a quick look in the man page (man 3 free) and we
find our solution:
The free() function frees the memory space pointed to by ptr,[…] If ptr is
NULL, no operation is performed.
So let’s rebuild our stack accordingly:
We try again, and we hit the SIGSEGV in the RET. Perfect, time to bypass NX.
We want to have a shell (what else, right?) so we need all the gadgets to
syscall execve(‘/bin/sh’, 0, 0).
Bypassing NX is not that hard, all we need are the right gadgets. We choose a
writable address, and write ‘/bin//sh’ (we arbitrarily chose 0x6c3110 in the
.bss). Using ropgadget makes
it easier than ever:
At this stage, we have /bin//sh written @0x6c3110 and this address inside the
EDI register. Then we can use the gadget 0x437aa9: pop rdx ; pop rsi ; ret to populate RSI and
RDX with 0. Because it embeds a libc, the binary is full of syscall
instructions, we’ll use the one at 0x435675.