DiceCTF 2026 - Plane-or-Exchange [ Cryptography ]
Writeup for the DiceCTF cryptography challenge "plane-or-exchange".
This post explains how to solve the DiceCTF cryptography challenge plane-or-exchange.
The challenge involves analyzing a custom cryptographic protocol built around permutations and polynomial invariants.
Challenge Description
The challenge description:
Alice and Bob had a brief exchange, and now they know something which I do not. Would you please help me to drop some Eves?
We are given:
protocol.py(implementation of the protocol)public.txtcontaining:- Alice’s public key
- Bob’s public key
- Public parameters
- Ciphertext
Our goal is to recover the shared secret and decrypt the ciphertext.
Understanding the Protocol
Inside protocol.py, the key functions are:
connect()calculate()normalize()
The important transformation is:
1
2
3
F(X) = normalize(calculate(X))
This converts a grid diagram structure into a polynomial invariant.
The key mathematical property is:
1
2
3
F(connect(A,B)) = F(A) × F(B)
So connecting two objects corresponds to multiplying their polynomials.
Key Exchange Mechanism
The protocol builds public keys like this:
1
2
3
4
PubA = connect(P, PrivA)
PubB = connect(P, PrivB)
Applying the invariant:
1
2
3
4
F(PubA) = F(P) × F(PrivA)
F(PubB) = F(P) × F(PrivB)
The shared secret computed by Alice is:
1
2
3
Shared = F(PrivA) × F(PubB)
The Cryptographic Weakness
Since we know:
1
2
3
F(PubA) = F(P) × F(PrivA)
We can isolate the private component:
1
2
3
F(PrivA) = F(PubA) / F(P)
Now substitute into the shared secret equation:
1
2
3
Shared = (F(PubA) / F(P)) × F(PubB)
All values on the right side are public, so we can compute the shared secret directly.
This means the protocol leaks a multiplicative invariant, which completely breaks the key exchange.
Computing the Shared Secret
First compute the polynomials:
Alice Public Polynomial
1
2
3
4
5
6
t^14 - 11t^13 + 61t^12 - 217t^11 + 548t^10 - 1032t^9
* 1494t^8 - 1687t^7 + 1494t^6 - 1032t^5
* 548t^4 - 217t^3 + 61t^2 - 11t + 1
Bob Public Polynomial
1
2
3
4
5
6
2t^14 - 19t^13 + 84t^12 - 226t^11 + 405t^10
* 523t^9 + 540t^8 - 527t^7 + 540t^6
* 523t^5 + 405t^4 - 226t^3 + 84t^2 - 19t + 2
Public Parameter Polynomial
1
2
3
t^6 - 5t^5 + 13t^4 - 17t^3 + 13t^2 - 5t + 1
Recover the private component:
1
2
3
PrivA = PubA / P
Then compute:
1
2
3
Shared = PrivA × PubB
Decryption
The shared polynomial is converted into a key by:
- Converting polynomial to string
- Hashing with SHA-256
- XOR with the ciphertext
Solve Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import hashlib
import sympy as sp
t = sp.Symbol("t")
pubA = t**14 - 11*t**13 + 61*t**12 - 217*t**11 + 548*t**10 - 1032*t**9 + 1494*t**8 - 1687*t**7 + 1494*t**6 - 1032*t**5 + 548*t**4 - 217*t**3 + 61*t**2 - 11*t + 1
pubB = 2*t**14 - 19*t**13 + 84*t**12 - 226*t**11 + 405*t**10 - 523*t**9 + 540*t**8 - 527*t**7 + 540*t**6 - 523*t**5 + 405*t**4 - 226*t**3 + 84*t**2 - 19*t + 2
P = t**6 - 5*t**5 + 13*t**4 - 17*t**3 + 13*t**2 - 5*t + 1
ciphertext = bytes.fromhex(
"288cdf5ecf3eb860e2cb6790bff63baceaebb6ed511cd94dd0753bac59962ef0"
"cd171231dc406ac3cdc2ff299d78390ff3"
)
privA, _ = sp.div(pubA, P)
shared = sp.expand(privA * pubB)
key = hashlib.sha256(str(shared).encode()).digest()
while len(key) < len(ciphertext):
key += hashlib.sha256(key).digest()
plaintext = bytes(c ^ k for c, k in zip(ciphertext, key))
print(plaintext.decode())
Final Flag
1
dice{plane_or_planar_my_w0rds_4r3_411_knotted_up}
Key Takeaway
This challenge demonstrates a common cryptographic mistake:
Exposing a multiplicative invariant in a key exchange scheme allows attackers to algebraically isolate private components.
Because the protocol allowed:
1
F(connect(A,B)) = F(A) × F(B)
an attacker could simply divide out the public parameter and recover the secret.
Always ensure cryptographic constructions do not leak algebraic invariants that allow separation of secret components.