一、概述

在如下示例程序print_banner中,调用了glibc动态库中的函数printf,在编译和链接阶段,链接器无法知道进程运行起来之后printf函数的加载地址,所以示例中call printf的地址只有在进程运行起来以后才能确定。

 080483cc <print_banner>:
 80483cc:    push %ebp
 80483cd:    mov  %esp, %ebp
 80483cf:    sub  $0x8, %esp
 80483d2:    sub  $0xc, %esp
 80483d5:    push $0x80484a8
 80483da:    call **<printf函数的地址>**
 80483df:    add $0x10, %esp
 80483e2:    nop
 80483e3:    leave
 80483e4:    ret

那么进程运行起来之后,glibc动态库也装载了,printf函数地址也确定了,上述call指令中的地址是如何获得的呢。call地址主要是在运行时/链接时进行重定位。运行时重定位和链接时重定位相关知识见背景知识模块。

二、堆溢出tcache利用方式及原理

​ Tcache的利用主要分为以下几种:

tcache poisoning
简单来说就是覆盖 tcache entry 结构体中的 next 域,不经过任何伪造 chunk 即可分配到另外地址
tcache dup
类似于 fastbin 的double free,就是多次释放同一个tcache,形成环状链表
tcache perthread corruption
控制tcache_perthread_struct结构体
tcache house of spirit
free 内存后,使得栈上的一块地址进入 tcache 链表,这样再次分配的时候就能把这块地址分配出来

tcache机制允许,将空闲的chunk以链表的形式缓存在线程各自tcache的bin中。下一次malloc时可以优先在tcache中寻找符合的chunk并提取出来。他缺少充分的安全检查,如果有机会构造内部chunk数据结构的特殊字段,我们可以有机会获得任意想要的地址。
https://blog.csdn.net/jazrynw...

typedef struct tcache_entry
{
    //指向chunk当中的用户内存区,第一个变量是一个指针,指向tcache的下一个chunk,
    //就是单链表访问
  struct tcache_entry *next;
  /* 这个字段是用来检测双重free释放的  */
  struct tcache_perthread_struct *key;
} tcache_entry;


在malloc中对tcache几乎没有什么检查,如果能给有机会覆写tcache中的next字段并且将next字段,覆写为任意指定的的地址,malloc就会直接返回这个地址的指针给应用程序,之后便可以恶意利用这个地址的内容,写入也好,读取也好。

tcache poisoning利用过程
当有机会覆写chunk中的fd字段时候,将fd覆盖为我们想要利用的地址,由于tcache_get只做了关于tcache bin中关于chunk大小的检查(malloc提供的参数,保证chunk可以在tcache bin范围内取出),没做关于地址位置的检查,之后的malloc如果使用tcache的话就会直接返回我们覆盖的地址块。

注意:这样的覆写,不会改变counts的计数,此后仍有可能添加chunk进入tcache,由于tcache_get也缺少检查,取出chunk的时候也会直接--counts,另外tcache的chunk链遵循的是LIFO策略,永远在tcache的chunk链表头部插入chunk,和直接取出chunk,取出的时候不会先检查计数,而是直接判断链表头部头部是不是NULL,有没有chunk,有就tcache_get直接取出。

漏洞利用示例

#include<stdio.h>
#include<malloc.h>

int main(int n,char **args){
char * buf1 = malloc(16);
char * buf2 = malloc(16);
char * buf3;
char mystr[16] = "normal var";
printf("1nd buf1:%p\n",buf1);
printf("1nd buf2:%p\n",buf2);
printf("1nd mystr:%p\n",mystr);
free(buf2);

scanf("%s",buf1);
printf("%ld\n",*(long long*)(buf1+24));
buf2 = malloc(16);

buf3 = malloc(16);

printf("2nd buf2:%p\n",buf2);
printf("2nd buf3:%p\n",buf3);

scanf("%s",buf3);

printf("%s\n",mystr);


return 0;
}

这个程序首先分配了两个16字节的缓冲区,buf1,buf2,另外一个栈上变量mystr保存要输出的字符串。

printf打印这三个变量在内存中的地址。这时free掉buf2,buf2的chunk会被添加到tcache中。

此时scanf接受输入,并没有限制字符串长度,可以造成缓冲区溢出覆盖下个chunk的特殊字段。下面这个printf只是打印了下prev_size字段的值,你可以不关注。

再次使用malloc分配,tcache上的chunk也就是前面释放的buf2又回到buf2中,但是注意了,如果我们之前将buf2的chunk的fd字段构造为我们的地址,比如mystr这个变量的地址,那么tcache中的情况实际是bu2d的chunk保存在tcache的头部,buf2的chunk的fd不为NULL有一个地址,就好像tcache中的chunk链有两块chunk一样(counts的计数还是一个),此时再次malloc(即buf3),malloc内部查看tcache不为NULL认为是一个有效的tcache chunk,直接取出(counts计数为-1)我们的buf3就获得了mystr的地址。

scanf向buf3写入字符串,此时如果buf3时mystr的地址,那他就会改变mystr的原有值。

输出mystr,正常应该输出"normal var"而我们利用了tcache后会输出上面向buf3写入的内容。

pwntools利用漏洞

code当中是我们构造的字符串nop填充 + chunk大小标记为in_use(chunk来自于主堆区,非mmaped) + mystr地址

发送’hack!’,程序最后输出’hack!’

利用总结
需要有机会覆写chunk
需要多次malloc分配来获得指定内存的地址
tcache机制追求速度缺少足够多的检查,利用比较容易
注意malloc分配的大小

背景知识

1、现代操作系统不允许修改代码段,只能修改数据段

2、编译阶段和运行阶段汇编的变化

参考文档:https://blog.csdn.net/linyt/a...
编译阶段是将.c源码翻译成汇编指令的中间件.o
查看使用objdump -d test.o ,可以看到printf的地址暂时使用fc ff ff ff替代,这个地址在链接/运行时会进行修正。

00000000 <print_banner>:
      0:  55                   push %ebp
      1:  89 e5                mov %esp, %ebp
      3:  83 ec 08             sub   $0x8, %esp
      6:  c7 04 24 00 00 00 00 movl  $0x0, (%esp)
      d:  e8 fc ff ff ff      call  e <print_banner+0xe>
     12:  c9                   leave
     13:  c3                   ret

链接阶段是将一个或多个中间文件(.o)通过链接器链接成一个可执行文件,链接主要完成

各个中间文件之间的同名section合并
对代码段,数据段以及各符号进行地址分配
链接时重定位修正

3、运行时重定位和链接时重定位

  • 链接时重定位:如果函数在其他.o文件中定义,则链接时printf地址即可确定,直接重定位
  • 运行时重定位:如果函数在动态库内(链接阶段是可以知道printf在哪定义的,只是如果定义在动态库内不知道它的地址而已),则会在运行时进行重定位。运行时重定位是无法修改代码段的,只能将printf重定位到数据段。那在编译阶段就已生成好的call指令,怎么感知这个已重定位好的数据段内容呢。
    答案是:链接器生成一段额外的小代码片段,通过这段代码支获取printf函数地址,并完成对它的调用。伪代码如下。
    链接阶段发现printf定义在动态库时,链接器生成一段小代码print_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接重定位,而运行时才对printf做运行时重定位。

    .text
    ...
    
    // 调用printf的call指令
    call printf_stub
    ...
    
    printf_stub:
      mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
      jmp rax // 跳过去执行printf函数
    
    .data
    ...
    printf函数的储存地址:
      这里储存printf函数重定位后的地址
    
  • PLT和GOT表
    动态链接主要有2个因素

    需要存放外部函数的数据段
    获取数据段存放函数地址的一小段额外代码

    如果可执行文件中调用多个动态库函数,那每个函数都需要这两样东西,这样每样东西就形成一个表,每个函数使用中的一项。
    总不能每次都叫这个表那个表,于是得正名。存放函数地址的数据表,称为重局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序链接表(PLT,Procedure Link Table)。它们两姐妹各司其职,联合出手上演这一出运行时重定位好戏。
    那么PLT和GOT长得什么样子呢?前面已有一些说明,下面以一个例子和简单的示意图来说明PLT/GOT是如何运行的。
    假设最开始的示例代码test.c增加一个write_file函数,在该函数里面调用glibc的write实现写文件操作。根据前面讨论的PLT和GOT原理,test在运行过程中,调用方(如print_banner和write_file)是如何通过PLT和GOT穿针引线之后,最终调用到glibc的printf和write函数的?
    下面是PLT和GOT雏形图,供参考。

4、tcache

tcache是glibc2.26之后引进的新机制,类似与fastbin,tcache为每个线程预留了一个特殊的bins,bin的数量是64个,每条bin链上最多有7个chunk,free的时候当tcache满了才放入fastbin等其他存储内容。tcache是默认开启的
tcache就是一个为了内存分配速度而存在的机制,当size不大(这个程度后面讲)堆块free后,不会直接进入各种bin,而是进入tcache,如果下次需要该大小内存,直接讲tcache分配出去,是不是感觉和fastbin蛮像的,但是其size的范围比fastbin大多了,他有64个bin链数组,也就是(64+1)*size_sz*2,在64位系统中就是0×410大小

5、safe-link

safe-link是glibc2.32引入的新的防御机制-safe-linking(异或加密),对于fastbin以及tcache的fd指针会被进行异或操作加密,用来异或的值随堆地址发生改变。其核心思想是:将指针的地址右移12位再和指针本身异或,如下,L为指针的地址,P为指针本身,该操作是可逆的,取指针时再做一次操作就可以还原得到原来的指针
该操作是在chunk被放入tcache bin和从tcache bin中取出时进行

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

2.33版本的glibc不同于以往,对于堆块地址的释放之后,对于同一大小的fastbin以及tcache有效的fd永远只有一个,剩余的bin照旧。

6、标准chunk结构

https://blog.csdn.net/weixin_...
堆的基本机制以及结构,目前主要使用的内存管理库是ptmalloc,而在ptmalloc中,用户请求的空间由名为chunk的数据结构表示


  • N,M,P字段
    P:PREV_INUSE 之前的chunk已经被分配则为1
    M: IS_MMAPED 当前chunk是mmap()得到的则为1
    N: NON_MAIN_ARENA 当前chunk在non_main_arena里则为1
  • TOP chunk

该chunk中,prev_size参数为前一chunk(如果未被使用)的大小,size参数为该chunk的大小,而P参数(pre_insue)为标志位,标志前一个chunk的使用情况。而上述的三个参数组成了chunk的header部分,该部分一般不会被用户直接访问

用户能够访问的空间为mem部分,如果一个chunk正被使用,则data部分为用户储存内容的空间,此时fd、bk指针并无实际意义。如果一个chunk未被使用,则mem部分的fd与bk储存的分别是上一个和下一个未被使用的chunk的地址。而这样的一个由未被使用的chunk组成的链表被称为bin。bin这个概念是与内存回收相关的,也就是堆管理器会根据用户已经申请到的内存空间大小进行释放,来决定放入哪类bins中。

一般而言,不同大小的free chunk会被分类到不同的bin中,而bin的类型可以被分为fast bin(32位下16-64字节,64位下小于等于0x80), small bin,large bin以及unsorted bin。其中,fast bin的操作效率最高,为单向链表,其他的都是双向链表。较高的操作效率意味着较低的安全性(传统艺能——牺牲安全换效率),所以fastbin机制产生的漏洞也是堆区漏洞的最重要的组成部分之一。

诸多的bin链由指针数组进行管理与保存,数组里头装的就是不同大小的bin链的头尾结点指针:

fastbinY数组:大小为10,为fastbin的专用数组。(最后三个链表保留未使用。
bins数组:大小为129,其中unsorted bin在bins[1],small bin在bins[2]~bins[63],large bin在bins[64]~bins[126]

而我们的fastbinY数组为了追求效率,直接舍弃了对bk指针的管理,使得fastbin形成了一个单链表结构(而非一般的双链表),在进行添加删除操作时使用的是LIFO原则,结构大致如图

而fastbin的高效体现在什么地方呢?

默认情况下,对于size_t为4B的平台, 小于64B的chunk分配请求;对于size_t为8B的平台,小于128B的chunk分配请求,程序会根据所需的size首先到fastbin中去寻找对应大小的bin中是否包含未被使用的chunk,如果有,则直接从bin中返回该chunk。而释放chunk时,也会根据chunk的size参数计算fastbin中对应的index,如果存在对应的大小,就将chunk直接插入对应的bin中。

32位平台 size_t 长度为 4 字节,64 位平台的 size_t 长度可能是 4 字节,也可能是 8 字节,64 位Linux平台 size_t 长度为 8 字节

而且为了追求效率,fastbin不仅使用单链表进行维护,由fastbin管理的chunk即使在被释放后chunk的p参数也不会被重置,而且在释放时只会对链表指针头部的chunk进行校验。

7、linux结构

linux进程虚拟地址空间


堆结构

8、内存分配释放流程


03-05 20:29