The binary allows us to flip 5 bits anywhere. Obviously not enough to do something useful, so we should use this “first round” to get unlimited flips.
But first, some quick reversing of the bianry to know what we can work with
This looks a bit different, from what we would have expected, the “welcome” message is missing here, but buf gets printed.
Something seems to be initializing buf before we enter main.
__frame_dummy_init_array_entry contains a pointer to initialize
This makes more sense. initialize will initialize buf with the welcome message, which then gets printed in main.
Ok, after we flipped 5 bits, main will call exit to end the program, so exit.got would make a good target for flipping.
To flip exit.got to main, we would need 6 bit flips, which we don’t have. But we can flip exit.got to _start (only needs 3 flips), which will also get us back into main (though executing initialize again).
So, we’re back in main and can still flip some more bits (since exit.got still points to _start, the binary will now loop infinitely.)
Time to get some leaks…
buf gets filled in initialize via sprintf and the format string in welcome_str. If we can point welcome_str somewhere else, we’ll control, how buf is initialized. A got entry would be handy…
To flip 0x400b51 into 0x601060 we need 8 flips, but can only do 5 in one go. Thus we have to make sure, that welcome_str points to something valid after 5 flips, so initialize doesn’t crash…
welcome_str will now point to 0x601051, which doesn’t contain anything useful, but is a valid pointer, so we can continue…
welcome_str now points to 0x601060, thus buf gets filled with the content of setvbuf.got, which can be leaked now and used to calculate libc base.
And we’re still able to flip bits. But we won’t be able to overwrite any got in one go with a useful address or gadget and every usable got entry will be called via initialize => main.
But
We can flip the _start pointer in exit to main in one go now (this needs exactly 5 bit flips). By doing this, the call to exit will then skip initialize and jump directly to main again. We can then flip a got, which is only used in initialize to something useful, and then flip exit back to _start after that. By this we have unlimited “rounds” for this.
I overwrote localtime with a one gadget because the constraints were easy to fulfill.
Since we have a libc leak, we can calculate the current value of localtime.got and also the target value we want to store there. We then just have to flip every bit in localtime which doesn’t match the one in our one_gadget.
Now that we have one_gadget in localtime, we’ll just flip exit back again to _start, so initialize will be called again, triggering one_gadget, giving us a shell :)