【Pwn 笔记】堆利用总结

记载自己理解的东东

main_arena

1
2
3
4
5
6
7
8
9
10
11
64
main_arena = __malloc_hook + 0x10
= __realloc_hook + 0x18
= __memalign_hook + 0x20

32
main_arena = __malloc_hook + 0x18
= __realloc_hook + 0x1c
= __memalign_hook + 0x20

在这里 "__malloc_hook", "__realloc_hook", "__memalign_hook" 均可以用 libc.sym 来获取对应的地址

malloc_chunk

堆结构如下,其中0,1,2,3表示一个单位长度。

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

[p + 0] INTERNAL_SIZE_T prev_size; /* 前一个空闲chunk的大小*/
[p + 1] INTERNAL_SIZE_T size; /* 字节表示的chunk大小,包括chunk头 */

[p + 2] struct malloc_chunk* fd; /* 双向链表 -- 只有在被free后才存在 */
[p + 3] struct malloc_chunk* bk; /* fd:前一个空闲的块 bk:后一个空闲的块*/

struct malloc_chunk* fd_nextsize; /*块大小超过512字节后会有这两个指针*/
struct malloc_chunk* bk_nextsize;
};

补充说明:
1.prev_size :前一块被free的话则为空闲块的大小,前一块未被free的话则为0
2.size : 因为chunk是四字节对齐所以size的低三位一定是0,被用来做flag

正常unlink的函数主要代码如下

1
2
3
4
5
6
7
8
9
#define unlink(AV, P, BK, FD) {
FD = P->fd; // FD = P + 0x10;
BK = P->bk; // BK = P + 0x18;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK; // FD + 0x18 = BK;
BK->fd = FD; // BK + 0x10 = FD;
}

绕过方法

实际上,我们还是有办法绕过unlink的检查,不过需要有一些条件:

1.有一个指向heap内的指针
2.存放这个指针的地址已知(一般这个地址(&P)是全局变量)
3.可以对这个指针进行多次写入

Unlink的合并操作

在free一块堆chunk内存时,会查看该块前后相邻的两块是否空闲,如果空闲的话则把他们从原来的链表上卸载出来和当前块合并在一起。分为向前合并和向后合并。

调试方法:

call free处下断点,然后代码类似如下:

1
gdb.attach(p, "b *0x400B62\nc" + "\nsi" * 99)

在这只要再si一步就会完成 unlink

向前合并:

查看下一个块是不是空闲的 –— 下一个块是空闲的,如果下下个块(距离当前空闲块)的PREV_INUSE(P)位没有设置(值为0)。
为了访问下下个块,将当前块的大小加到它的块指针,再将下一个块的大小加到下一个块指针。
如果是空闲的,使用unlink操作合并它。将合并后的块添加到 unsorted bin 中。

向后合并:

查看前一个块是不是空闲的 –— 如果当前空闲块的 PREV_INUSE(P) 位为 0 ,则前一个块是空闲的,那么合并它。

这时 unlink 实际做了*P = P - 0x18

简单记忆法:假设要改的链表地址为P,那么修改堆*PfdP - 0x18,堆*PbkP - 0x10

64位向后合并实例

实际情况:

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
初始链表,这里先在 0x10b1270 的堆处布置向后合并的条件
然后 free 掉 0x10b1380 处的堆块进行 unlink
pwndbg> x/8gx 0x6020e0
0x6020e0: 0x00000000010b1060 0x0000000000000001
0x6020f0: 0x00000000010b1170 0x0000000000000001
0x602100: 0x00000000010b1280 0x0000000000000001
0x602110: 0x00000000010b1390 0x0000000000000000

初始 0x10b1280 堆
pwndbg> x/10gx 0x10b1270
0x10b1270: 0x0000000000000000 0x0000000000000211
0x10b1280: 0x0000000000000000 0x0000000000000101
0x10b1290: 0x00000000006020e8 0x00000000006020f0
0x10b12a0: 0x6161616161616161 0x6161616161616161
0x10b12b0: 0x6161616161616161 0x6161616161616161


伪造的 0x10b1390 堆
pwndbg> x/4gx 0x10b1380
0x10b1380: 0x0000000000000100 0x0000000000000100
0x10b1390: 0x6363636363636363 0x0000000000000000

删除掉 0x10b1390 堆之后

新链表
pwndbg> x/8gx 0x6020e0
0x6020e0: 0x00000000010b1060 0x0000000000000001
0x6020f0: 0x00000000010b1170 0x0000000000000001
0x602100: 0x00000000006020e8 0x0000000000000001
0x602110: 0x00000000010b1390 0x0000000000000000

新 0x10b1280 堆
pwndbg> x/10gx 0x10b1270
0x10b1270: 0x0000000000000000 0x0000000000000211
0x10b1280: 0x0000000000000000 0x0000000000020d81
0x10b1290: 0x00000000006020e8 0x00000000006020f0
0x10b12a0: 0x6161616161616161 0x6161616161616161
0x10b12b0: 0x6161616161616161 0x6161616161616161

不变的 0x10b1390 伪造堆
pwndbg> x/4gx 0x10b1380
0x10b1380: 0x0000000000000100 0x0000000000000100
0x10b1390: 0x6363636363636363 0x0000000000000000

原理演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第一步(注意 0x10b1280 是伪造的 prev_size 区域)
P = 0x602100
FD = P->fd; // FD = *(0x602100).fd <==> (0x10b1280).fd <==> 0x10b1290(important)
BK = P->bk; // BK = *(0x602100).bk <==> (0x10b1280).bk <==> 0x10b1298

第二步检测 if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
FD->bk == *(0x10b1290).bk <==> (0x6020e8).bk <==> 0x602100
BK->fd == *(0x10b1298).fd <==> (0x6020f0).fd <==> 0x602100(important)

第三步
FD->bk = BK; // 0x602100 = 0x10b1298
BK->fd = FD; // *(0x10b1298).fd <==> (0x6020f0).fd <==> 0x602100 = 0x6020e8

完毕

double-free

1.新建两个chunk分别为chunk0和chunk1

1
2
3
4
5
6
7
8
9
chunk0                malloc返回的ptr        chunk1        malloc返回的ptr
| | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+
| | | | | | | | | | |
| | | | | | prev | size&| | | |
| prev_size |size&Flag| | | | size | flag | | | |
| | | | | | | | | | |
| | | | | | | | | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+

2.free掉这两个块,第一块作为稍后unlink的块,另一块作为free的块

3.新建一个块,这个块要能覆盖到chunk1的头部,从而伪造两个chunk的头,chunk0的头部之前已经说过了,而chunk1需要欺骗操作系统chunk0是已经被free了的,所以其头部应该如下

1
2
pre_size     chunk0_size
size chunk1_size

然后整体实现如下:

1
2
3
4
5
6
7
8
9
10
chunk0                malloc返回的ptr           chunk1        malloc返回的ptr
| | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
| | |fake|fake|fake|fake| D | fake | fake | | | |
| | |prev|size| FD | BK | A | prev | size&| | | |
| prev_size |size&Flag|size| | | | T | size | flag | | | |
| | | | | | | A | | | | | |
| | | | | | | | | | | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|-------new_chunk0-------|

4.那么新建这个块就相当于是两个块了,然后我们free chunk1,就能实现unlink chunk0从而使得chunk0的地址存放的值变成了P-0x18
5.接下来修改chunk0的值,修改的值为0x18个padding,然后就能修改chunk0的地址的值了,这时修改成free的GOT表地址
6.接下来再修改一次chunk0, 这次的值修改成system那么就会使得free的GOT表指向system,接下来free的时候传入/bin/sh就能获取shell了

realloc

realloc的特性如下:

1
1.对 ptr 进行判断,如果 ptr 为 NULL,则函数相当于 malloc(new_size),试着分配一块大小为 new_size 的内存,如果成功将地址返回,否则返回 NULL。如果 ptr 不为 NULL,则进入 2
1
2
3
4
5
6
7
8
9
2.查看 ptr 是不是在堆中,如果不是的话会跑出异常错误,会发生 realloc invalid pointer。

如果 ptr 在堆中,则查看 new_size 大小,如果 new_size 大小为 0,则相当于 free(ptr),将 ptr 指针释放,返回 NULL

如果 new_size 小于原大小,则ptr中的数据可能会丢失,只有 new_size 大小的数据会保存(这里很重要)

如果 size 等于原大小,等于啥都没做,如果 size 大于原大小,则看 ptr 所在的位置还有没有足够的连续内存空间

如果有的话,分配更多的空间,返回的地址和 ptr 相同,如果没有的话,则会使用 malloc 分配更大的内存,将旧的内容拷贝到新的内存中,把旧的内存 free 掉,返回新地址,否则返回NULL

realloc(): invalid old size

1
2
3
4
if ((__builtin_expect ((uintptr_t) oldp > (uintptr_t) -oldsize, 0)
|| __builtin_expect (misaligned_chunk (oldp), 0))
&& !DUMPED_MAIN_ARENA_CHUNK (oldp))
malloc_printerr ("realloc(): invalid pointer");

在 realloc 的地址前面加上对应的 prev_size 和 size,之后申请相同大小 size 的 realloc 即可,如下:

1
2
0x9860038:	0x00000000	0x00000050
0x9860040: 0x0804b048 0x00000069

这里 0x0804b048 是 atoi 的 got 表地址,在前面加上p32(0) + p32(0x50)后申请一个 0x50 的 realloc 即可

分配的地址相同时,UAF 情况

1.new_size > old_size 时

(1).若后面无堆块,替换该地址上堆块的 size 为 new_size 的大小

(2).若后面有堆块,释放当前堆块,在后面新建一个 new_size 大小的堆块

2.new_size < old_size 时,释放原堆块,将新堆块放入原堆块地址

此时如果剩余堆块大小足够组成一个堆块,则使其剩余空间变成一个已经释放的堆块(常用手段,这个堆块会被放到相应的 bin 中)

分配的地址为 0 时 – tcache bin 情况

和 malloc 差不多

1.地址为 0 时,会去 tcache bin 寻找一块符合的 bin,将新块建立在那

这里只是将地址改为了所选择的 tcache bin 的大小,并没有更改它的 size,也就是说原有的 size 不变

2.如果没有符合的 bin,就在下一地址段创建一个新堆块

tcache bin

在申请一个 0x110 堆块时,如果 tcache bin 布局如下:

1
2
3
4
tcachebins
0x90 [ 1]: 0x5617b6d6d3900x0
0x110 [ 7]: 0x5617b6d6d280 —▸ 0x7f33b3dca760 (nore) ← 0x0
0x130 [ 1]: 0x5617b6d6d2600x0

如果我们能更改 0x5617b6d6d280 上堆块的 size 为其他大小,那么 tcache bin 布局会变成我们想要跳转到的地址:

1
2
3
0x90 [  1]: 0x5617b6d6d3900x0
0x110 [ 6]: 0x7f33b3dca760 (nore) ← 0x0
0x130 [ 1]: 0x5617b6d6d2600x0
文章目录
  1. 1. main_arena
  2. 2. malloc_chunk
  3. 3. Unlink
    1. 3.1. 绕过方法
    2. 3.2. Unlink的合并操作
      1. 3.2.1. 向前合并:
      2. 3.2.2. 向后合并:
  4. 4. double-free
  5. 5. realloc
    1. 5.1. realloc(): invalid old size
    2. 5.2. 分配的地址相同时,UAF 情况
    3. 5.3. 分配的地址为 0 时 – tcache bin 情况
  6. 6. tcache bin
|