2026 Hacktheon Sejong CTF ๋ํ
0. ๋ฌธ์ ๊ฐ์
์ด๋ฒ ๋ฌธ์ ๋ Voice Over Challenge๋ผ๋ ์ด๋ฆ์ ์์ฑ ๊ธฐ๋ฐ CTF ๋ฌธ์ ์๋ค.
์ ๊ณต๋ ํ์ผ์ ๋ค์๊ณผ ๊ฐ์๋ค.
1
2
3
4
5
sample_001.wav
sample_002.wav
sample_003.wav
sample_004.wav
sample_005.wav
์์ค์ฝ๋๋ ์ ๊ณต๋์ง ์์๊ณ , ๋ฌธ์ ํ์ด์ง ์ฃผ์๋ง ์ฃผ์ด์ก๋ค.
http://3.37.31.209:8000
์ฒ์์๋ ์ ๊ณต๋ WAV ํ์ผ ์ค ํ๋๊ฐ ์ ๋ต ์์ฑ์ผ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์ง๋ง, ์ค์ ์๋ฒ์ ์ ์ถํด ๋ณด๋ ๊ทธ๋ ์ง ์์๋ค.
1. ์ ๊ณต ํ์ผ ๋ถ์
์์ถ์ ํด์ ํ ๋ค WAV ํ์ผ๋ค์ ํ์ธํด ๋ณด๋ ๋ชจ๋ ๊ฐ์ ํ์์ด์๋ค.
1
2
3
mono
16kHz
16-bit PCM
๊ฐ ์์ฑ ํ์ผ์ ์๋ฒ์ ์ ์ถํด ๋ณธ ๊ฒฐ๊ณผ, ๊ณตํต์ ์ผ๋ก speaker_similarity๋ ๋๊ฒ ๋์์ง๋ง text_similarity๋ ๋ฎ๊ฒ ๋์๋ค.
์๋ฅผ ๋ค์ด sample_001.wav๋ฅผ ์ ์ถํ์ ๋ ์๋ฒ๋ ํ์ ์ ์ฌ๋๋ฅผ ์ฝ 0.957 ์ ๋๋ก ํ๋จํ๋ค.
ํ์ง๋ง transcript๋ target sentence์ ๋ค๋ฅธ ๋ฌธ์ฅ์ผ๋ก ์ธ์๋์๊ณ , ๊ฒฐ๊ตญ ํ
์คํธ ์ ์ฌ๋ ์กฐ๊ฑด์ ๋ง์กฑํ์ง ๋ชปํด ์คํจํ๋ค.
์ด๋ฅผ ํตํด ์ ๊ณต๋ ์์ฑ ํ์ผ์ ์ ๋ต ์์ฑ์ด ์๋๋ผ, ๋ชฉํ ํ์์ ๋ชฉ์๋ฆฌ๋ฅผ ๋ด์ ๋ ํผ๋ฐ์ค ์ํ์ด๋ผ๋ ๊ฒ์ ์ ์ ์์๋ค.
์ฆ ๋ฌธ์ ์ ๋ชฉํ๋ ๋ค์๊ณผ ๊ฐ์๋ค.
- ์ ๊ณต๋ ์ํ ์์ฑ์ ์ด์ฉํด
target sentence๋ฅผ ๊ฐ์ ํ์์ ๋ชฉ์๋ฆฌ๋ก ์ฝ๊ฒ ๋ง๋ ๋ค ์ ์ถํ๊ธฐ
2. ์๋ฒ API ํ์ธ
์น ํ์ด์ง์ API๋ฅผ ํ์ธํด ๋ณด๋ ์ฃผ์ ์๋ํฌ์ธํธ๋ ๋ ๊ฐ์๋ค.
1
2
GET /api/challenge
POST /api/verify
/api/challenge๋ ๋งค๋ฒ ์๋ก์ด ๊ฐ์ ๋ฐํํ๋ค.
{
"token": "...",
"target_sentence": "..."
}
/api/verify๋ ์ ๋ก๋ํ WAV ํ์ผ์ ๊ฒ์ฌํด ๋ค์ ๊ฐ์ ๋ฐํํ๋ค.
1
2
3
4
5
speaker_similarity
text_similarity
transcript
success
flag
์ฆ ์๋ฒ๋ ๋จ์ํ ์์ฑ ํ์ผ ํ๋๋ง ๋ณด๋ ๊ฒ์ด ์๋๋ผ,
- ๋ชฉ์๋ฆฌ๊ฐ ๋ชฉํ ํ์์ ๋น์ทํ๊ฐ?
- ๋งํ ๋ฌธ์ฅ์ด target_sentence์ ์ผ์นํ๋๊ฐ?
๋ ์กฐ๊ฑด์ ๋์์ ๊ฒ์ฌํ๊ณ ์์๋ค.
3. ๋จ์ TTS ์๋
์ฒ์์๋ Windows TTS๋ก target sentence๋ฅผ ์ฝ๊ฒ ๋ง๋ ๋ค ์ ์ถํด ๋ณด์๋ค.
์ด ๊ฒฝ์ฐ text_similarity๋ ์ด๋ ์ ๋ ์ฌ๋ผ๊ฐ์ง๋ง, speaker_similarity๊ฐ ์ฝ 0.53 ์์ค์ผ๋ก ๋ฎ๊ฒ ๋์ ์คํจํ๋ค.
์ฆ ๋จ์ TTS๋ก๋ ๋ฌธ์ ๋ฅผ ํ ์ ์์๋ค.
text_similarity์กฐ๊ฑด์ ๋ง์กฑ ๊ฐ๋ฅspeaker_similarity์กฐ๊ฑด์ ๋ง์กฑ ๋ถ๊ฐ
๋ฐ๋ผ์ ํ์ํ ๊ฒ์ ๋จ์ ์์ฑ ํฉ์ฑ์ด ์๋๋ผ, ์ ๊ณต๋ ์ํ ์์ฑ์ ํ์์ ๋ณต์ ํ๋ zero-shot voice cloning ์ด์๋ค.
4. Voice Cloning ์ ๊ทผ
๊ณต๊ฐ๋ Hugging Face voice cloning ๋ฐ๋ชจ๋ฅผ ์ฐพ์๋ณธ ๊ฒฐ๊ณผ, ๋ค์ Space๋ฅผ ์ฌ์ฉํ ์ ์์๋ค.
- Kikirilkov/Voice_Cloning
์ด Space๋ ๋ค์๊ณผ ๊ฐ์ ํํ๋ก ๋์ํ๋ค.
1
predict(text, audio, language)
์ฆ,
1
2
3
text โ ์๋ฒ๊ฐ ์๊ตฌํ target_sentence
audio โ ์ ๊ณต๋ sample WAV
language โ en
๋ฅผ ์
๋ ฅํ๋ฉด, ๋ ํผ๋ฐ์ค ์์ฑ๊ณผ ๋น์ทํ ๋ชฉ์๋ฆฌ๋ก target sentence๋ฅผ ์ฝ์ WAV ํ์ผ์ ์์ฑํ ์ ์์๋ค.
๋ ํผ๋ฐ์ค๋ก๋ ๊ฐ์ฅ ๊ธธ๊ณ ์์ ์ ์ธ ์ํ์ธ sample_005.wav๋ฅผ ์ฌ์ฉํ๋ค.
5. Solver ์ฝ๋
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
import argparse
from pathlib import Path
import requests
try:
from gradio_client import Client, handle_file
except ImportError as exc:
raise SystemExit(
"Missing dependency: gradio_client\n"
"Install it with: python -m pip install gradio_client"
) from exc
CHALLENGE_URL = "http://3.37.31.209:8000"
DEFAULT_SPACE = "Kikirilkov/Voice_Cloning"
def get_challenge(base_url: str) -> dict:
response = requests.get(f"{base_url}/api/challenge", timeout=30)
response.raise_for_status()
return response.json()
def clone_voice(space_name: str, sample_path: Path, text: str, language: str) -> Path:
client = Client(space_name)
output_path = client.predict(
text,
handle_file(str(sample_path)),
language,
api_name="/predict",
)
return Path(output_path)
def submit_audio(base_url: str, token: str, audio_path: Path) -> dict:
with audio_path.open("rb") as audio_file:
response = requests.post(
f"{base_url}/api/verify",
files={"audio": (audio_path.name, audio_file, "audio/wav")},
data={"token": token},
timeout=180,
)
response.raise_for_status()
return response.json()
def main() -> None:
parser = argparse.ArgumentParser(description="Solve the Voice Over Challenge")
parser.add_argument(
"--sample",
default=str(Path(__file__).with_name("sample_005.wav")),
help="Reference WAV file for the target speaker",
)
parser.add_argument(
"--space",
default=DEFAULT_SPACE,
help="Public Hugging Face Space used for voice cloning",
)
parser.add_argument(
"--language",
default="en",
help="Language parameter for the voice cloning API",
)
parser.add_argument(
"--base-url",
default=CHALLENGE_URL,
help="Challenge base URL",
)
args = parser.parse_args()
sample_path = Path(args.sample).resolve()
if not sample_path.exists():
raise SystemExit(f"Sample file not found: {sample_path}")
print(f"[+] Reference sample : {sample_path}")
print(f"[+] Voice clone space: {args.space}")
challenge = get_challenge(args.base_url)
print(f"[+] Token : {challenge['token']}")
print(f"[+] Target sentence : {challenge['target_sentence']}")
cloned_audio = clone_voice(
args.space,
sample_path,
challenge["target_sentence"],
args.language,
)
print(f"[+] Cloned audio : {cloned_audio}")
result = submit_audio(args.base_url, challenge["token"], cloned_audio)
print(f"[+] Speaker score : {result['speaker_similarity']}")
print(f"[+] Text score : {result['text_similarity']}")
print(f"[+] Transcript : {result['transcript']}")
if result.get("success") and result.get("flag"):
print(f"[+] FLAG : {result['flag']}")
else:
print("[-] Solve failed")
print(result)
if __name__ == "__main__":
main()
6. ์คํ ๊ฒฐ๊ณผ
์์ฑ๋ ์์ฑ์ /api/verify์ ์ ์ถํ์ ์๋ฒ๊ฐ target sentence๋ฅผ ์ ํํ ์ธ์ํ๋ค.
์ฑ๊ณต ์๋ต์ ๋ค์๊ณผ ๊ฐ์๋ค.
1
2
3
4
5
6
7
{
"text_similarity": 1.0,
"text_threshold": 0.8,
"speaker_similarity": 0.8727,
"success": true,
"flag": "hacktheon2026{b7d30e21e4106a6ca4d451a218f15a97}"
}
text_similarity๋ 1.0์ผ๋ก ์์ ํ ์ผ์นํ๊ณ , speaker_similarity๋ ์๊ณ๊ฐ์ธ 0.8์ ๋์ด ์ฑ๊ณตํ๋ค.
7. ์ต์ข ํ๋๊ทธ
1
hacktheon2026{b7d30e21e4106a6ca4d451a218f15a97}
8. ์ ๋ฆฌ
์ด ๋ฌธ์ ๋ ์ผ๋ฐ์ ์ธ ์น ์ทจ์ฝ์ ์ด๋ API ์ฐํ ๋ฌธ์ ๊ฐ ์๋๋ผ, ์๋ฒ๊ฐ ์๊ตฌํ๋ ๊ฒ์ฆ ์กฐ๊ฑด์ ์ ํํ ์ดํดํ๋ ๊ฒ์ด ํต์ฌ์ด์๋ค.
์ฒ์์๋ ์ ๊ณต๋ WAV ํ์ผ์ ๊ทธ๋๋ก ์ ์ถํ๊ฑฐ๋ ๋จ์ TTS๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉํฅ์ผ๋ก ์ ๊ทผํ ์ ์์ง๋ง,
์ค์ ๋ก๋ ๋ ์กฐ๊ฑด์ ๋์์ ๋ง์กฑํด์ผ ํ๋ค.
๋ชฉ์๋ฆฌ ์ ์ฌ๋๋ฌธ์ฅ ์ผ์น๋
๊ฒฐ๊ตญ ์ ๊ณต๋ ์ํ์ ์ ๋ต์ด ์๋๋ผ ๋ชฉํ ํ์์ ๋ ํผ๋ฐ์ค์๊ณ ,
๋ฌธ์ ์ ์๋๋ ์ด๋ฅผ ์ด์ฉํด target sentence๋ฅผ ๊ฐ์ ๋ชฉ์๋ฆฌ๋ก ํฉ์ฑํ๋ ๊ฒ์ด์๋ค.
Voice cloning์ ํ์ฉํด ๋ ์กฐ๊ฑด์ ๋ชจ๋ ๋ง์กฑ์ํค๋ฉด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์๋ค.