For this challenge, we were given a binary named "mute". The reason for this name becomes apparent in a minute.
$ file mute
mute: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3c37c5241fad4af47c79288b1f0aea4b63418e86, not stripped
I find this challenge to be good for two reasons. First, the premise is interesting/novel. And second, this is an example of a challenge that can be simply stated and does not attempt to make itself harder through obfuscation of the vulnerability. I came up with my solution right as the contest was ending, so I do not have the flag nor did I run it against the server. However, this solution worked well for me and is interesting.
Give it a quick run to see what happens:
SILENCE, FOUL DAEMON!
So no output. Likely expected given the name mute. However, the program didn't finish execution. Running it in ltrace, we see some interesting calls:
[pid 55233] __libc_start_main(0x400aa7, 1, 0x7fff989503e8, 0x400b50 <unfinished ...>
[pid 55233] mmap(0, 4096, 7, 34 <unfinished ...>
[pid 55233] SYS_mmap(0, 4096, 7, 34) = 0x7f59c2e2e000
[pid 55233] <... mmap resumed> ) = 0x7f59c2e2e000
[pid 55233] puts("SILENCE, FOUL DAEMON!" <unfinished ...>
[pid 55233] SYS_fstat(1, 0x7fff98950190) = 0
[pid 55233] SYS_brk(0) = 0x16be000
[pid 55233] SYS_brk(0x16df000) = 0x16df000
[pid 55233] SYS_write(1, "SILENCE, FOUL DAEMON!\n", 22SILENCE, FOUL DAEMON!
) = 22
[pid 55233] <... puts resumed> ) = 22
[pid 55233] fflush(0x7f59c29c9620) = 0
[pid 55233] seccomp_init(0, 0x7f59c29ca780, 0, 0x7f59c26fb6e0) = 0x16be420
[pid 55233] seccomp_arch_add(0x16be420, 0xc000003e, 1, 1) = 0xffffffef
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 0, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 2, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 3, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 4, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 5, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 6, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 7, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 8, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 9, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 10, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 11, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 12, 0) = 0
[pid 55233] seccomp_rule_add(0x16be420, 0x7fff0000, 59, 0) = 0
[pid 55233] seccomp_load(0x16be420, 0, 0, 7 <unfinished ...>
[pid 55233] SYS_prctl(38, 1, 0, 0) = 0
[pid 55233] SYS_prctl(22, 2, 0x16be490, 0x7f59c270c06a) = 0
[pid 55233] <... seccomp_load resumed> ) = 0
[pid 55233] read(0 <unfinished ...>
[pid 55233] SYS_read(0
Looks like 4096 bytes are being mapped into memory. Not only that, the permissions for it is 7, which means that it has Read/Write/Execute. This is generally an indication that something will be gaining execution that is not in the binary itself. Pausing on that for a second, there are also calls to seccomp. Calls to these functions were found in multiple places at this years DEFCON and are new in my experience. I have not seen them used in previous CTFs, though it's possible that they have been.
The concept behind the seccomp library is basically to allow the programmer a means of filtering system calls. This can be helpful to mitigate possible attacks against your system since you can tell the Linux kernel that you don't actually want a set of syscalls to be able to execute. For example, if you're running a program that should not be executing other programs, you might want to pre-emptively disable the execve system call. The effect of this would be that if your program was exploited and a malicious user told it to spawn a shell, they would be unable to do so.
The first call to seccomp_init basically creates a filtering context, and configures it so that only those calls that are specified will be allowed. If syscalls are attempted to be executed that are not a part of the allowed list, they will cause the program to terminate. The meat of the rest of that trace tells us what system calls will be allowed. Specifically, they are:
Any system call not on that list will cause the program to segfault. We'll get back to the importance of this, but let's return to the question of why this program never terminated. Opening it up in radare2, all of main actually fits inside a single window.
radare2 is being a little quirky, but the obj._DYNAMIC is actually the value 0x1000 (otherwise known as 4096, coincidence?). We can see a call to dropSyscalls which performs the seccomp calls. Finally, the program enters a loop, reading input until it gets 4096 bytes worth. This check is why we never got any response.
Finally, at 0x400b3a, you can see that we load up the buffer address, and call it. What this means is that we're simply giving this program our shellcode we want to execute, and it's executing it. So what's the catch? The catch is syscalls being filtered above.
Taking a closer look at what syscalls are not on the allowed list, you should notice that the write system call is not on that list. The write system call is how you get any data back from a program (among other uses). The program has to write to a file descriptor before it makes its way to you. To be fair, I initially created shellcode to execute /bin/sh, before realizing the futility of attempting to spawn a shell in that way.
So that leaves us with the question of how to get data back. We were given quite a few syscalls to work with. One thought was to remove the filter by calling seccomp_reset. Sadly, I couldn't get this method to actually remove the filter.
The solution I ended up with was to utilize a similar concept that is used in SQL web exploitation. There's a technique called Blind SQL Injection that is similar in nature to this challenge. Basically, you can provide input to the program, but you cannot directly get output. The solution in the web world is to use other byproducts, such timing attacks. Timing attacks work because you can cause behaviors that will take a noticeably different amount of time. In our case, we do have full control over the binary, we're just not allowed to write data back. Thus, if we can cause a behavior change such as that, we can actually leak data.
At this point, we get down to some low level assembly programming. A good portion of the shellcoding programming tasks is eased by the pwntools shellcraft module. This module allows you to generate shellcode on-the-fly using both canned shellcode as well as writing your own.
The approach I took for the timing attack was to open the guessed flag file, read it onto the stack, then guess a single character. If I guessed correctly, I keep the connection open. If I guessed incorrectly, I closed the connection. Thus, I'm able to ask a binary question of "is this character the right one" and reduce the problem to brute forcing a string.
One trick I utilized was to use an assembly instruction called cmov. cmov is an intel instruction that tells the CPU to conditionally move a value from one place to another based on flags. In our situation, this was super helpful because it allowed me to not have to worry about coding jump statements. Instead, I could conditionally move the different syscall into the register if the values were not equal, causing the fail behavior instead of the success behavior.
The final code is here: https://raw.githubusercontent.com/Owlz/CTF/master/2017/DEFCON/mute/win.py
In my example case, this was able to successfully leak the flag back to me. In the event that the name of the flag file could not be guessed, the same technique could be used to leak directory names first.