Category: Blockchain (Vyper / EVM) · launched as dormant-signer.zip
(sha256 9397090f…)
Artifact
chain/-genesis.json,transactions.jsonl(the whole custody-desk history),replay.sh,HEADcontracts/-Authorizer.vy,VoucherDesk.vy,Notary.vy
Authorizer.vy pays withdrawals against a co-signature from an off-chain
"automated signer". When the desk winds an authorizer down it files a recovery
escrow and self-destructs:
filed = escrow ^ escrow_mask(liveness signature) ^ running_digest
where
running_digest = keccak256(concat(prev, keccak256(abi_encode(authorizer, request_id, amount, recipient))))folded over every settled request - recomputable by replaying the chain;escrow_mask(v,r,s) = keccak256(ESCROW_DOMAIN ++ r ++ s ++ v)over the signer's liveness signature -ecrecover(LIVENESS_DIGEST, v, r, s), i.e. only the signer can produce it.
Method
Recover the signer private key - ECDSA nonce reuse. The
authorizeco-signatures on the two authorizers leak the samerfor two different messages (the RFC6979 nonce collided across deployments):output k = (z1 - z2) * (s1 - s2)^-1 mod n d = (s1 * k - z1) * r^-1 mod nrecover.py→ recovered signer keys, e.g. authorizer Ac0ecef7be0815db43cf992c33dce2b92739576089c31e30ce0c30ac2e2919e52.Recompute the liveness signature deterministically (RFC6979, low-s,
v ∈ {27,28}, rawLIVENESS_DIGEST, no message prefix) and derivemask = keccak(ESCROW_DOMAIN || r || s || v).Replay the chain into a local anvil, read
storage[14] = masked_secreton the sealed authorizer0x5ebb…, and strip the mask:output escrow = masked_secret ^ mask (running_digest already folded into the filed blob ⇒ it cancels)
Repro
1cd chain && DETACH=1 bash replay.sh # local anvil + replay of transactions.jsonl
2python3 ds_open.py # recovers key, reads storage[14], unmasks
ds_open.py prints the escrow plaintext:
hiCTF{layout_undead_borrowed_betrays}
Verification
- Key recovery is confirmed by
ecrecoverover everyauthorizeco-signature on the destroyed authorizer returning the recovered address (100% match - seefinal.py). - The unmasked escrow is ASCII and a well-formed
hiCTF{…}token.
Tooling notes
- Authorizer filed
masked_secretat storage slot 14;running_digestandsettledfollow it - read them witheth_getStorageAt. - Deterministic nonces (RFC6979) meant the "unrecoverable" key was actually
leaked by a single
rcollision between the two authorizers.