# sidhe (crypto, 300p, 29 solved)

In the task we get [server
code](https://raw.githubusercontent.com/TFNS/writeups/master/2020-04-17-PlaidCTF/sidhe/server.sage)
and we can connect to the server.

## Task analysis

The server code impelements Supersingular isogeny Diffie–Hellman key exchange
protocol.  
The code is simple to follow:

1. Server randomly selects a `private secret`  
2. We can perform 300 interactions with the server  
3. Each time we can provide a new public key  
4. Each time we can send payload encrypted using derived shared secret  
5. If we send encrypted `Hello World` we get back confirmation of valid message  
6. If we send encrypted `private secret` we get back a flag  
7. If we send anything else, we get confirmation of invalid message

## Vulnerability

The vulnerability lies in the fact that `private secret` is selected only once
per connection, and we can perform 300 key exchanges, with the same secret in
place.

## Solution

The general idea behind the attack is pretty simple to understand.  
The private secret (sk3) is used in:

```python  
def isoex3(sk3, pk2):  
   Ei, P, Q = pk2  
   S = P+sk3*Q  
   for i in range(e3):  
       R = (3^(e3-i-1))*S  
       phi = Ei.isogeny(R)  
       Ei = phi.codomain()  
       S = phi(S)  
   return Ei.j_invariant()  
```

Where `P` and `Q` values are provided as part of our public key.  
Let's imagine for a moment that those are just some numbers.  
Server starts off by doing `S = P+sk3*Q`.  
Let's perform one exchange with some values P and Q and derive a shared
secret.  
Now let's modify `Q` by flipping the least significant bit, and perform
another exchange.  
There are 2 things which could happen:  
- If `sk3` has it's own LSB as `0`, then both derived shared secrets will be identical  
- If `sk3` has it's own LSB a `1`, then we effectively changed `S`, and thus rest fo calculations will differ

Of course we're not dealing with numbers, but much more complex structures,
but general idea stays the same.  
We want to modify `P` and `Q` in such a way, that the resulting shared secret
either stays the same, or changes, depending on bits of private secret.

We can detect this by sending `Hello World` payload and checking if server is
able to decrypt it (so shared secrets match) or not.

For the solution we're closely following the paper `On the security of
supersingular isogeny cryptosystems` by SD Galbraith, C Petit, B Shani, YB Ti:

https://ora.ox.ac.uk/objects/uuid:840faec4-382f-44ec-
aeac-76bd5962f7cb/download_file?file_format=pdf&safe_filename=859.pdf

The paper provides clear description of recovery process in case server is the
one using `2^e2`, while in our case server is using `3^e3`, so we need to
adapt the solution a bit for that.  
We also have to use the slightly harder version, because server is performing
some `Weil Pairing` checks.

First off we need to implement the missing counterpart of operations for `2^e`
for our own client, based on those for `3^e` provided with server:

```python  
def isogen2(sk2):  
   Ei = E  
   P = Pb  
   Q = Qb  
   S = Pa+sk2*Qa  
   for i in range(e2):  
       phi = Ei.isogeny((2^(e2-i-1))*S)  
       Ei = phi.codomain()  
       S = phi(S)  
       P = phi(P)  
       Q = phi(Q)  
   return (Ei,P,Q)

def isoex2(sk2, pk3):  
   Ei, P, Q = pk3  
   S = P+sk2*Q  
   for i in range(e2):  
       R = (2^(e2-i-1))*S  
       phi = Ei.isogeny(R)  
       Ei = phi.codomain()  
       S = phi(S)  
   return Ei.j_invariant()  
```

### Recovery for 2^n PoC

We start off by implementing the simpler case for `2^e`:

```python  
def two():  
   Sa = randint(0, 2^e2-1)  
   print('sa',bin(Sa))  
   Ea, phiPb, phiQb = isogen2(Sa)  
   Sb = randint(0, 3^e3-1)  
   Eb, phiPa, phiQa = isogen3(Sb)  
   #phiQa = phiQa+2^(e2-1)*phiPa # simple case for lSB  
   R = phiPa  
   S = phiQa

   Ki = 0  
   for i in range(len(bin(Sa)[2:])-2):  
       ZE = Zmod(2^e2)  
       theta = 1/ZE(1+2^(e2-1-i))  
       theta = theta.nth_root(2)  
       phiPa = (int(theta)*R-int(theta*2^(e2-i-1)*Ki)*S)  
       phiQa = int(theta*(1+2^(e2-i-1)))*S

       # simpler case wihout scaling theta, but won't pass the assertions below  
       #phiPa = R-(2^(e2-i-1)*Ki)*S  
       #phiQa = (1+2^(e2-i-1))*S

       P = phiPa  
       Q = phiQa  
       assert(P*(2^e2) == Eb(0) and P*(2^(e2-1)) != Eb(0))  
       assert(Q*(2^e2) == Eb(0) and Q*(2^(e2-1)) != Eb(0))  
       assert(P.weil_pairing(Q, 2^e2) == (Pa.weil_pairing(Qa, 2^e2))^(3^e3))

       JA = isoex2(Sa,(Eb, phiPa, phiQa))  
       JB = isoex3(Sb,(Ea, phiPb, phiQb))  
       if (JA == JB):  
           print('even')  
       else:  
           Ki+=2**i  
           print('odd')  
   print(bin(Ki))  
```

The simplest option just to recover LSB is changing `phiQa` to `phiQa =
phiQa+2^(e2-1)*phiPa`.  
For full recovery we could use the option:  
```python  
phiPa = R-(2^(e2-i-1)*Ki)*S  
phiQa = (1+2^(e2-i-1))*S  
```

Where `Ki` is the recovered known part of the private secret, but this won't
pass we `Weil Pairing` checks.  
To do that we need to include scaling factor theta metioned in the paper.

### Recovery for 3^n PoC

Now that we have a working PoC for 2, we can modify it to fit our actual case:

```python  
def three():  
   Sb = randint(0, 3^e3-1)  
   tmp = Sb  
   coeffs = []  
   while tmp != 0:  
       coeffs.append(tmp % 3)  
       tmp //= 3;  
   print(coeffs)  
   print("Sb",Sb)  
   Eb, phiPa, phiQa = isogen3(Sb)

   Sa = randint(0, 2^e2-1)  
   Ea, phiPb, phiQb = isogen2(Sa)

   R = phiPb  
   S = phiQb

   x = 0  
   for i in range(5):  
       ZE = Zmod(3^e3)  
       theta = 1/ZE(1+3^(e3-1-i))  
       theta = theta.nth_root(2)  
       found_coeff = 0  
       for case in range(1,3):  
           phiPb = (int(theta)*R-int(theta*3^(e3-i-1)*(x+case*3**i))*S)  
           phiQb = int(theta*(1+3^(e3-i-1)))*S

           # simpler case wihout scaling theta, but won't pass the assertions below  
           #phiPb = R-(3^(e3-i-1)*(x+case*3**i))*S  
           #phiQb = (1+3^(e3-i-1))*S

           P = phiPb  
           Q = phiQb  
           assert(P*(3^e3) == Ea(0) and P*(3^(e3-1)) != Ea(0))  
           assert(Q*(3^e3) == Ea(0) and Q*(3^(e3-1)) != Ea(0))  
           assert(P.weil_pairing(Q, 3^e3) == (Pb.weil_pairing(Qb, 3^e3))^(2^e2))

           JA = isoex2(Sa,(Eb, phiPa, phiQa))  
           JB = isoex3(Sb,(Ea, phiPb, phiQb))  
           if (JA == JB):  
               found_coeff = case  
               break;  
       print('coefficient', found_coeff, 'for power 3^'+str(i))  
       x+=found_coeff*3**i  
   print(x)

three()  
```

Here `x` denotes the known part of private secret and `case` is the new
coefficient we're testing.

The only major difference is the fact that we're working now in base3.  
This means that we can recover coefficients for consecutive powers of `3`, and
that we may need to perform 2 exchanges to recover one coefficient, because we
need to check coefficient `1` and if it doesn't work we need to check `2` and
only then we can assume that original coefficient was `0`.

### Solver

Now that we have a working PoC for 3^n recover, we can finally implement full
client to grab the flag.  
Note that according to the paper, this method will not work for the highest 2
coefficients, so we need to brute-force those at the very end.

Core of the solver is:

```python  
def oracle(Ei, P, Q, ciphertext, s):  
   pk2 = Ei,P,Q  
   send_key(s,pk2)  
   receive_until(s, ":")  
   send(s, ciphertext.encode("hex"))  
   result = receive_until(s, [".","!"])  
   return "Good" in result  

def recover_coefficients(Ea,R,S,oracle, ciphertext, s):  
   x = 0  
   ZE = Zmod(3^e3)  
   for i in range(e3-2):  
       print("Recovering 3^%d coefficient" % i)  
       theta = 1/ZE(1+3^(e3-1-i))  
       theta = theta.nth_root(2)  
       found_coeff = 0  
       for case in range(1,3):  
           phiPb = (int(theta)*R-int(theta*3^(e3-i-1)*(x+case*3**i))*S)  
           phiQb = int(theta*(1+3^(e3-i-1)))*S  
           if oracle(Ea, phiPb, phiQb, ciphertext, s):  
               found_coeff = case  
               break;  
       print('coefficient', found_coeff, 'for power 3^'+str(i))  
       x+=found_coeff*3**i  
   return x  
```

This allows us to recover all but last 2 coefficients, then we can do:

```python  
limit = e3  
for a in range(3):  
   for b in range(3):  
       secret = res+a*3**(limit-1)+b*3**(limit-2)  
       super_secret_hash = hashlib.sha256(str(secret).encode('ascii')).digest()[:16]  
       ciphertext = cipher.encrypt(super_secret_hash)  
       send_key(s, pk2)  
       send(s,ciphertext.encode("hex"))  
       response = s.recv(9999)  
       print(response)  
```

And one of the responses where we guessed `a` and `b` correctly will contain
the flag.  
After a while we manage to recover:
`PCTF{if_you_wish_for_postquantum_crypto_ask_a_supersingular_isogenie}`

Complete solver
[here](https://raw.githubusercontent.com/TFNS/writeups/master/2020-04-17-PlaidCTF/sidhe/client.sage)

Original writeup
(https://github.com/TFNS/writeups/blob/master/2020-04-17-PlaidCTF/sidhe/README.md).# sidhe (crypto, 300p, 29 solved)

In the task we get [server
code](https://raw.githubusercontent.com/TFNS/writeups/master/2020-04-17-PlaidCTF/sidhe/server.sage)
and we can connect to the server.

## Task analysis

The server code impelements Supersingular isogeny Diffie–Hellman key exchange
protocol.  
The code is simple to follow:

1\. Server randomly selects a `private secret`  
2\. We can perform 300 interactions with the server  
3\. Each time we can provide a new public key  
4\. Each time we can send payload encrypted using derived shared secret  
5\. If we send encrypted `Hello World` we get back confirmation of valid
message  
6\. If we send encrypted `private secret` we get back a flag  
7\. If we send anything else, we get confirmation of invalid message

## Vulnerability

The vulnerability lies in the fact that `private secret` is selected only once
per connection, and we can perform 300 key exchanges, with the same secret in
place.

## Solution

The general idea behind the attack is pretty simple to understand.  
The private secret (sk3) is used in:

```python  
def isoex3(sk3, pk2):  
Ei, P, Q = pk2  
S = P+sk3*Q  
for i in range(e3):  
R = (3^(e3-i-1))*S  
phi = Ei.isogeny(R)  
Ei = phi.codomain()  
S = phi(S)  
return Ei.j_invariant()  
```

Where `P` and `Q` values are provided as part of our public key.  
Let's imagine for a moment that those are just some numbers.  
Server starts off by doing `S = P+sk3*Q`.  
Let's perform one exchange with some values P and Q and derive a shared
secret.  
Now let's modify `Q` by flipping the least significant bit, and perform
another exchange.  
There are 2 things which could happen:  
\- If `sk3` has it's own LSB as `0`, then both derived shared secrets will be
identical  
\- If `sk3` has it's own LSB a `1`, then we effectively changed `S`, and thus
rest fo calculations will differ

Of course we're not dealing with numbers, but much more complex structures,
but general idea stays the same.  
We want to modify `P` and `Q` in such a way, that the resulting shared secret
either stays the same, or changes, depending on bits of private secret.

We can detect this by sending `Hello World` payload and checking if server is
able to decrypt it (so shared secrets match) or not.

For the solution we're closely following the paper `On the security of
supersingular isogeny cryptosystems` by SD Galbraith, C Petit, B Shani, YB Ti:

https://ora.ox.ac.uk/objects/uuid:840faec4-382f-44ec-
aeac-76bd5962f7cb/download_file?file_format=pdf&safe_filename=859.pdf

The paper provides clear description of recovery process in case server is the
one using `2^e2`, while in our case server is using `3^e3`, so we need to
adapt the solution a bit for that.  
We also have to use the slightly harder version, because server is performing
some `Weil Pairing` checks.

First off we need to implement the missing counterpart of operations for `2^e`
for our own client, based on those for `3^e` provided with server:

```python  
def isogen2(sk2):  
Ei = E  
P = Pb  
Q = Qb  
S = Pa+sk2*Qa  
for i in range(e2):  
phi = Ei.isogeny((2^(e2-i-1))*S)  
Ei = phi.codomain()  
S = phi(S)  
P = phi(P)  
Q = phi(Q)  
return (Ei,P,Q)

def isoex2(sk2, pk3):  
Ei, P, Q = pk3  
S = P+sk2*Q  
for i in range(e2):  
R = (2^(e2-i-1))*S  
phi = Ei.isogeny(R)  
Ei = phi.codomain()  
S = phi(S)  
return Ei.j_invariant()  
```

### Recovery for 2^n PoC

We start off by implementing the simpler case for `2^e`:

```python  
def two():  
Sa = randint(0, 2^e2-1)  
print('sa',bin(Sa))  
Ea, phiPb, phiQb = isogen2(Sa)  
Sb = randint(0, 3^e3-1)  
Eb, phiPa, phiQa = isogen3(Sb)  
#phiQa = phiQa+2^(e2-1)*phiPa # simple case for lSB  
R = phiPa  
S = phiQa

Ki = 0  
for i in range(len(bin(Sa)[2:])-2):  
ZE = Zmod(2^e2)  
theta = 1/ZE(1+2^(e2-1-i))  
theta = theta.nth_root(2)  
phiPa = (int(theta)*R-int(theta*2^(e2-i-1)*Ki)*S)  
phiQa = int(theta*(1+2^(e2-i-1)))*S

# simpler case wihout scaling theta, but won't pass the assertions below  
#phiPa = R-(2^(e2-i-1)*Ki)*S  
#phiQa = (1+2^(e2-i-1))*S

P = phiPa  
Q = phiQa  
assert(P*(2^e2) == Eb(0) and P*(2^(e2-1)) != Eb(0))  
assert(Q*(2^e2) == Eb(0) and Q*(2^(e2-1)) != Eb(0))  
assert(P.weil_pairing(Q, 2^e2) == (Pa.weil_pairing(Qa, 2^e2))^(3^e3))

JA = isoex2(Sa,(Eb, phiPa, phiQa))  
JB = isoex3(Sb,(Ea, phiPb, phiQb))  
if (JA == JB):  
print('even')  
else:  
Ki+=2**i  
print('odd')  
print(bin(Ki))  
```

The simplest option just to recover LSB is changing `phiQa` to `phiQa =
phiQa+2^(e2-1)*phiPa`.  
For full recovery we could use the option:  
```python  
phiPa = R-(2^(e2-i-1)*Ki)*S  
phiQa = (1+2^(e2-i-1))*S  
```

Where `Ki` is the recovered known part of the private secret, but this won't
pass we `Weil Pairing` checks.  
To do that we need to include scaling factor theta metioned in the paper.

### Recovery for 3^n PoC

Now that we have a working PoC for 2, we can modify it to fit our actual case:

```python  
def three():  
Sb = randint(0, 3^e3-1)  
tmp = Sb  
coeffs = []  
while tmp != 0:  
coeffs.append(tmp % 3)  
tmp //= 3;  
print(coeffs)  
print("Sb",Sb)  
Eb, phiPa, phiQa = isogen3(Sb)

Sa = randint(0, 2^e2-1)  
Ea, phiPb, phiQb = isogen2(Sa)

R = phiPb  
S = phiQb

x = 0  
for i in range(5):  
ZE = Zmod(3^e3)  
theta = 1/ZE(1+3^(e3-1-i))  
theta = theta.nth_root(2)  
found_coeff = 0  
for case in range(1,3):  
phiPb = (int(theta)*R-int(theta*3^(e3-i-1)*(x+case*3**i))*S)  
phiQb = int(theta*(1+3^(e3-i-1)))*S

# simpler case wihout scaling theta, but won't pass the assertions below  
#phiPb = R-(3^(e3-i-1)*(x+case*3**i))*S  
#phiQb = (1+3^(e3-i-1))*S

P = phiPb  
Q = phiQb  
assert(P*(3^e3) == Ea(0) and P*(3^(e3-1)) != Ea(0))  
assert(Q*(3^e3) == Ea(0) and Q*(3^(e3-1)) != Ea(0))  
assert(P.weil_pairing(Q, 3^e3) == (Pb.weil_pairing(Qb, 3^e3))^(2^e2))

JA = isoex2(Sa,(Eb, phiPa, phiQa))  
JB = isoex3(Sb,(Ea, phiPb, phiQb))  
if (JA == JB):  
found_coeff = case  
break;  
print('coefficient', found_coeff, 'for power 3^'+str(i))  
x+=found_coeff*3**i  
print(x)

three()  
```

Here `x` denotes the known part of private secret and `case` is the new
coefficient we're testing.

The only major difference is the fact that we're working now in base3.  
This means that we can recover coefficients for consecutive powers of `3`, and
that we may need to perform 2 exchanges to recover one coefficient, because we
need to check coefficient `1` and if it doesn't work we need to check `2` and
only then we can assume that original coefficient was `0`.

### Solver

Now that we have a working PoC for 3^n recover, we can finally implement full
client to grab the flag.  
Note that according to the paper, this method will not work for the highest 2
coefficients, so we need to brute-force those at the very end.

Core of the solver is:

```python  
def oracle(Ei, P, Q, ciphertext, s):  
pk2 = Ei,P,Q  
send_key(s,pk2)  
receive_until(s, ":")  
send(s, ciphertext.encode("hex"))  
result = receive_until(s, [".","!"])  
return "Good" in result  

def recover_coefficients(Ea,R,S,oracle, ciphertext, s):  
x = 0  
ZE = Zmod(3^e3)  
for i in range(e3-2):  
print("Recovering 3^%d coefficient" % i)  
theta = 1/ZE(1+3^(e3-1-i))  
theta = theta.nth_root(2)  
found_coeff = 0  
for case in range(1,3):  
phiPb = (int(theta)*R-int(theta*3^(e3-i-1)*(x+case*3**i))*S)  
phiQb = int(theta*(1+3^(e3-i-1)))*S  
if oracle(Ea, phiPb, phiQb, ciphertext, s):  
found_coeff = case  
break;  
print('coefficient', found_coeff, 'for power 3^'+str(i))  
x+=found_coeff*3**i  
return x  
```

This allows us to recover all but last 2 coefficients, then we can do:

```python  
limit = e3  
for a in range(3):  
for b in range(3):  
secret = res+a*3**(limit-1)+b*3**(limit-2)  
super_secret_hash = hashlib.sha256(str(secret).encode('ascii')).digest()[:16]  
ciphertext = cipher.encrypt(super_secret_hash)  
send_key(s, pk2)  
send(s,ciphertext.encode("hex"))  
response = s.recv(9999)  
print(response)  
```

And one of the responses where we guessed `a` and `b` correctly will contain
the flag.  
After a while we manage to recover:
`PCTF{if_you_wish_for_postquantum_crypto_ask_a_supersingular_isogenie}`

Complete solver
[here](https://raw.githubusercontent.com/TFNS/writeups/master/2020-04-17-PlaidCTF/sidhe/client.sage)

Original writeup
(https://github.com/TFNS/writeups/blob/master/2020-04-17-PlaidCTF/sidhe/README.md).