ECW 2020 Final - ECDSA nonce reuse

img

ECW 2020 Final - ECDSA nonce reuse (crypto)

The final of the ECW 2020 took place today. My team grabbed the first place thanks to my monstrous team mates. This final was quite different from the one I did in 2018 as, this time, we could reach the others teams servers.

This means that when you flagged a chall you have to run your payload or script against 11 others servers to get 12 different flags with the same amount of points. This was quite insane.

From the statement of the challenge we have to find a flaw in a voting machine designed by an ex engineer from SONY. The only other information we had was that the server was running on port 1337.

/! DISCLAIMER /!

I’m very bad at crypto and will probably tell mistakes. Do NOT trust me.

crypto, ahhh crypto ..

playing around

Once a bit of scanning done we found some servers with the port 1337 TCP open.

λ 130 ackira ~/nextcloud/CTF/ecwfinal2020  » rlwrap nc 192.168.8.36 1337
Help Yves Attrovite to won the election, 1995 style !
Type "help" to get a list of available commands
Public key:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL0TaPvZ5gz4EPjkn6v5IIXAZzclp
J6wffo9BOW4c/KHONeNtzVu8nGMzG2etkNGL5KJs5YLuAbz/PajRrq7Mbg==
-----END PUBLIC KEY-----
Presidentielles> help                                                                       
Documented commands (type help <topic>):  
========================================
EOF  candidates  help  manual_resign  quit  results  vote

Presidentielles> candidates
Inès Timable        : Count(n=3725 , dgst='2yf4CngFudtg0ovJ7lOlGPcQ15JZiDVbuZzeru5zG08=', sig='RzgUDX71qOzWpCSuZ1/9bA0ulz2uDyBgiQdNDHefbYOH2U4jSbdHlcRzHETTJLQBPynZ6IGHbt5idHBocACOxw==')
Mario Naitiste      : Count(n=2886 , dgst='fO8i8HctF/u5AGd5WpoFwhtVKnLJQTEfzca3aK37/5Y=', sig='BtAtQuUnBAA+nIaGjbsS8tL0uvZfp94FVbtXlTQ6WkpocrhGVONnlgikv670YniRyhQYtPxVHOE+FrdPWocV7w==')
Ella Traifin        : Count(n=2623 , dgst='CVxaDvjOGok9WMgorId+ead4GKeHU/nrSymBgeAIT40=', sig='KJm5ganTBhkNiXF1XnHmqOLB6sh1m9k8zON+pHykNLJZTjxDUpoMboNGOF1TQor6c4yPzi1G0I74z1TyfDVHlQ==')
Teddy Fissile       : Count(n=2099 , dgst='4E5l0RuEbF5Q3JU/SEizhSsn7wv5aJOegB0hxw7j99s=', sig='h9fF6EzjbdxE42Fgq2TcW/vxfARigUYLonjhoq6CMA8V1F6oQSS/vKisomgeHRbsYEM3gs+FKgrH+9qlgyTVGA==')
Alain Thérieur      : Count(n=2059 , dgst='O47DUTwaUKnvA41cHVn1dx1KCkTspWQNmDaVVaHttXE=', sig='QeoZK1BjHkFydGJNBRef/wOnND8WcajIdWdXg8iV8wSZi44GbadCq+6lDPSC1RahDm3vlJdVTp5TV6BoQlEXAg==')
Annick Tarasse      : Count(n=2052 , dgst='RAn+3g8vrmRGC4/DiE0asD+iosCwPPn7G4sAxPqTVFM=', sig='lltqPXeVKKbSEOb0g06wOOy481cQNnfzLcymmgpm7EPMfzcDisp180Sjd8ylgmW5JJXWMjM5+bNvzKmVXQLMlQ==')
Jacques Septe       : Count(n=1955 , dgst='7yZLhcY6GXCWsWzxApjdAN16Lro6MA+Xd4k3NHnJC/M=', sig='WorPM/JxaueLKYFfQPf1q/RvEyIAThizEJfWfkGrvycAzB85reV9XYvomqszqHG3c3xtdiIzoc32MHPbAvmRvg==')
Gilles Léparbal     : Count(n=1952 , dgst='Fl3MR0snw+LpZ+v9EyDuFpnZNgZohnDqlz5eOcef1Ug=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTL6ptPQ0QdHIgxprcpEQ1dYNIoLefvkm/+OJNu2QuhTEA==')
Anne Estézi         : Count(n=1863 , dgst='+SuKpKu6C+Cvpisex8r0kN2/zgChCtqlDpGsbjWwP6M=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTLcocQGxzU1hMgArYJFmGPpuNoOfbup707etRHZ86wCVw==')
Kimberley Tartine   : Count(n=1561 , dgst='gUOJOnoRHEuiCb1Gq5+kKLdmKW/CBwPhT7sZJjTdwLY=', sig='AnMj+CizA5P8qxtpVAIFIz5T7y9ZHo6ADWGrH/npjIDUzL2k1QrV3X82TobjA+4TdXr1fSml6c5aGt84YxMzyw==')
Jessica Pote        : Count(n=1245 , dgst='WhdVxnhQhXU/cqWrU+4Yag+iV4VvNkUq6etEUP5AW0A=', sig='FUxKe+XQm5vKmgebohMERrlgOpYmtjl11WHWRsmVyDgkTNW/RqKPgDagg7uOacqyGZHeLOQGb+Qk7B4X99l3hA==')
Marc Assin          : Count(n=1214 , dgst='N/drFOwdzlQYBc3KILv55M4evcHzUEJB0vocptSs3zc=', sig='zAMy/ZZqOizgzA52SiYFvKBY0qLP7mjJpKFZ7QVBGQvHTum8QHy9a/GPvRbjp2r7b8ei+uIWTvtIq7eyYrh1oA==')
Iris Keurien        : Count(n=1113 , dgst='XexPpBvrPddiRK4M+bfFDcawwmLozucidSAZPD7FnzY=', sig='OBYIvdFft6FKBEDcgrqiTyA0/MsEQd2NVbHfL4VuJAOAXfGWqlt64RA4Dofs97c9IuXVezTbh2RRg94XBuzyrg==')
Jean Bonneau        : Count(n=1033 , dgst='LXj8K5OQxYI7IpwFKsYorTRR2tNQfyWtTgXRAfXzzGE=', sig='lK8iZL8Yp1pyx45Nuv4c5oAXwte7EcOtsxsaaBc+WD4VltyddJ40wK6PvZIJa5+bxFHFLywBjBqbP2HT5UCU/A==')
Kelly Diote         : Count(n=911  , dgst='etzVh2U7lD3sFhzVBb/QFOd4s83NQVC1PTJex+PyAmo=', sig='PuUndQ72dPYoY5tsfpKtmvOHjXR0miCr9qyG9hDxU29OEfWjKeSttf3/sym0M5ZsUP0908cXHk4DiODEOU5Cjg==')
Yves Attrovite      : Count(n=0    , dgst='4tcmDz9tFJ1dG3GqLV7zbV/qY/dpOAfQRYi/lkuCzAI=', sig='OrX9BJXTa8o12H4q5Cv2836KWqiFwAbL76mhZNxldD0gio6AoGYZY6I7Xa/nhRGgFd+SffOQ1drteiUJdV8bXA==')
Presidentielles> 

From the statement we understand that we need to help Yves Attrovite to win the election. In order to win the election he must have the highest vote count.

There are only 4 interesting commands :

  • candidates : list the candidates (see above)
  • manual_resign : update the vote count of a given candidate
  • results : make the elections happen and elect the candidate with the highest vote count
  • vote : give our vote to one candidate

My first try was to call the vote function multiples times but unfortunately we could use it only once.

The second way to achieve the goal, was to use the manual_resign function to set the vote count from one candidate.


Presidentielles> manual_resign 
[2020-11-18 11:06:34,390] __main__ - ERROR - Usage: manual_resign CANDIDATE COUNT B64_SIGNATURE 

Presidentielles>  manual_resign "Yves Attrovite" 11 "switch"
Presidentielles> candidates
Marc Assin          : Count(n=3443 , dgst='Isa1fNhckxex5tB6SxtMURegBnuN4mOG9Zt8o+Ff+jM=', sig='j85nDNrYv5tBcIs0pvk7/U7wpangN5L2CxLamIyCfmIBVm3Pe/Bjqgy78gXtieCQQUk0ojJfgHOmF0RtPRr8sQ==')
Jean Bonneau        : Count(n=3223 , dgst='h5ZPPj6vk+5nyYXbOy211n2qnoalA/sKv1Ome0ezva8=', sig='s45bP25ZtSdlbD4culmWji4NpNVCbNNi+HGoEXQEnN8F/EuQSetE4pCmY3z/VcKdce3uw2X6k+lXnDV2V+qiUQ==')
Jessica Pote        : Count(n=3007 , dgst='AeVwMCGgtswJ6ryPdGFNWe4ABtegu9UneMMv4PlPPws=', sig='EBgAhPjB/IVCrL6u5E0Q+BUZ72SqiztXmxGih6jl955C723Kfm99FfYHMqcWFyidQN5s1iJBCMpXRk9J/uGIEg==')
Alain Thérieur      : Count(n=2803 , dgst='33BTcuE8JPf3zKNxsJI/iR/MSp9dp2s3EYww/VqSLnM=', sig='+lNkYImtXzI/c+KOCx9GK6g9mRh3F76dPqF164RWoNXNSNiFELCiaThmG+2Xr10FHypV1VhNU/aJ/RUoc9QDaA==')
Kimberley Tartine   : Count(n=2769 , dgst='5/nGygcMkzlbCdVrwNpBjYK5FrBGt/ngmFE8gc1uUdA=', sig='5jzgfLdWeq7Fdp4vN200sY5KnRB67pvAZjszcLGsKujfYia14URyNtFpShpeGUDjdqnk6KYvKdd4JrVzSlnoyw==')
Ella Traifin        : Count(n=2622 , dgst='qRWhpDFAgk1IGnIDAotouOW++AjZXnaCRr7TdA2QWq8=', sig='IQBQ+uDa7N0FEEzg6vbXkbEN/ZPiL4VJW36lPKUDndTNHc/SYUO19uKBdzY5amqTCUuy2fr6LXH90+FFyMkxEA==')
Anne Estézi         : Count(n=2553 , dgst='w+tB9JV6HTEowJtsDEArieL4Aol9xCHY5gkLsYI0BiE=', sig='HwNeOtA+v1RJT25ooqC2i5sowlgibuoIF2PlIzOGDQ2CvhsltJiB2VyTHj6Ds5xRY66auM2yw+ZVJrKV0OHNvA==')
Annick Tarasse      : Count(n=2363 , dgst='nyq/DPn3bLty3OVxggpZagWpuFoABAYXfRLrEVynqoE=', sig='B0KMbtkSgjdSrlWVePII0khb3bIu9hXbvZhRhRz7HaldQWm9nLdVVFExrfZ4Q3codJ831dbA/JhtwmDh0aGSdw==')
Gilles Léparbal     : Count(n=2060 , dgst='QPsDO1R/paxV5qweBTa+BmzTqlYMKAk6c+vS335Tsh4=', sig='lofCSVxD8Zi/r3rMnig45pzpRviEFH0j3W43g3muzqdWEpZ8C+C2XndW3MnCKFGRwyHu0ql4kPMOmHDeW2LpkQ==')
Jacques Septe       : Count(n=2036 , dgst='79xUnnVH4pHXG2MmVjTbYiOhZX/iU4tEJDo1w5I2C/8=', sig='U+jHDFPnEBR8gxgxdxxu6Bw4sAzTnUW9OVgjJyyta8r3O3hyI0iDU2F29ojbuV8x4ZC5IIInP/7ox+teWlG/Bw==')
Teddy Fissile       : Count(n=1903 , dgst='5sYAgIKhhmb2imbEALBJXPI9zG1IXjoPeQhpMkT1vtU=', sig='3M1dlTPPgyw1l3inGj853v86/PCSooJMGKgnO4OCVC9BQC1iWjpuM+qDlATuR5y1yAL7HfWMMM2P12lFA23GHQ==')
Inès Timable        : Count(n=1524 , dgst='y45dRpchLIiqNMI5D/IFuYQPw8Gh/c7kFO8txbVzA9o=', sig='j85nDNrYv5tBcIs0pvk7/U7wpangN5L2CxLamIyCfmKcSm0+K6wq/sAZtwokicLJmgmXwq7VZ8H2Mlzu80uglw==')
Mario Naitiste      : Count(n=1342 , dgst='2bhu4yYEV2yqUBMO6Ttgw27Y2vE/qhpTdErAkx5bhvo=', sig='87lTyRw+upUsP0a1z/siggy8gjkWuhnoS2IGk+J6iSmbIH/rPChYiZQBJAYGqxexiZUkHo/EEJ5MwgWH5glf+Q==')
Iris Keurien        : Count(n=1260 , dgst='+coQM/QFNsNCUkGewNQ18R1aRRu05RUvliZunkyMdTs=', sig='4iSGjT2ZZDwlxLmasSPm3+p7hDnTXcrqiTXMp/D42LWLSJtqSdRAf9fqaxRHt/eKmSOnjZBbz5asVxZ3MPuVRA==')
Kelly Diote         : Count(n=1043 , dgst='qpw2mpD//8nJFQJDXuvsMfhZSqbB906mVN+Tr8/pPMU=', sig='FVh8aXP09XAI+EaqwHlMTKwEonb5+pyL0lyMuzAW76+Q3Cf+BjppkvbW1lp9Szgfn6Yq1wZIit+f8bvMNEJ3lQ==')
Yves Attrovite      : Count(n=11   , dgst='EpWEdZbASekXCUVFwJjN7wvFxvo2cWo+8dxTMdj9Jpc=', sig='switch')

Nice we can update the amount of votes for our candidate, lets win the election

Presidentielles> results

[2020-11-18 11:09:35,525] __main__ - ERROR - ('Malformed formatting of signature', MalformedSignature('Invalid length of signature, expected 64 bytes long, provided string is 5 bytes long'))
[2020-11-18 11:09:35,525] __main__ - ERROR - 'Yves Attrovite' cheated !!!

So the signature is wrong, I tried to set the same as before but get onto this

Presidentielles> results                                                                                  
[2020-11-18 14:23:17,989] __main__ - ERROR - Signature verification failed                   
[2020-11-18 14:23:17,990] __main__ - ERROR - 'Yves Attrovite' cheated !!!

So, it won’t be this easy.

If you paid attention, you have noticed the public key sent when opening the connection

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL0TaPvZ5gz4EPjkn6v5IIXAZzclp
J6wffo9BOW4c/KHONeNtzVu8nGMzG2etkNGL5KJs5YLuAbz/PajRrq7Mbg==
-----END PUBLIC KEY-----

It looks so small, my first (and wrong assumption) was that it was a small RSA exponent and we could found its 2 factors and generate the private key.

attacking ECDSA

Let’s find it exponent with openssl

$ openssl rsa -in pub.pem -pubin -text -modulus

140069342045568:error:0607907F:digital envelope routines:EVP_PKEY_get0_RSA:expecting an rsa key:crypto/evp/p_lib.c:469:

The fuck. OpenSSL does not recognize the key as RSA.

How can I get the type of the key ?

But hopefully there is an openssl commands for that !

Zestfully.

$ openssl asn1parse -in pub.pem

0:d=0  hl=2 l=  89 cons: SEQUENCE
2:d=1  hl=2 l=  19 cons: SEQUENCE
4:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
13:d=2  hl=2 l=   8 prim: OBJECT            :prime256v1
23:d=1  hl=2 l=  66 prim: BIT STRING

ECDSA public key of course, I was suspecting this kind of key a bit but sadly I never really looked into it as much as I should have.

From here we have to find a vulnerability for this ECDSA implementation.

SONY mistakes

From the statement the voting machine has been developed by an SONY ex employee, but why this indication. I just googled SONY ECDSA vulnerability and came across this post https://ropnroll.co.uk/2017/05/breaking-ecdsa/

more about sony mistakes in PS3

I don’t want to get into the crypto part too deep. The vulnerability resides in the reuse of the r number which should have been random for every signature.

This mistakes is easy spotable as the signature of 2 different values starts with some common bytes.

Gilles Léparbal     : Count(n=1952 , dgst='Fl3MR0snw+LpZ+v9EyDuFpnZNgZohnDqlz5eOcef1Ug=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTL6ptPQ0QdHIgxprcpEQ1dYNIoLefvkm/+OJNu2QuhTEA==')
Anne Estézi         : Count(n=1863 , dgst='+SuKpKu6C+Cvpisex8r0kN2/zgChCtqlDpGsbjWwP6M=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTLcocQGxzU1hMgArYJFmGPpuNoOfbup707etRHZ86wCVw==')

As you see these 2 candidates signatures start with ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTL. This indicate the reuse of a nonce.

Here is our vulnerability, this one allow us to retrieve the private key thanks to m1, m2, sig1 , sig2 and the public key. Where mx and sigx are the plain texts and the associated signatures.

With the private key it will possible to sign whatever we want.

The only thing that we don’t know was m1 and m2. I struggled here for so long and I’m still very mad toward myself as I could get the information way sooner.

Presidentielles> help manual_resign

        manual_resign CANDIDATE COUNT B64_SIGNATURE
        B64_SIGNATURE = ECDSA.sign_digest(hash(f"{CANDIDATE}-{COUNT}".encode("utf-8")))

I think I have tried help vote but as nothing showed up I didn’t test for the rest ..

So for the example above m1 is Gilles Léparbal-1952, and m2 is Anne Estézi=1863.

recovering the private key

First we get the order of the given curve. Moreover the standard used is NIST256p thanks to the public key information, so we will need sha256 instead of sha1.

from ecdsa.curves import NIST256p as curves
from ecdsa.util import sigencode_string, sigdecode_string
from ecdsa import VerifyingKey, SigningKey
from base64 import b64encode, b64decode
from hashlib import sha256
import gmpy

public_key_pem = '''
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL0TaPvZ5gz4EPjkn6v5IIXAZzclp
J6wffo9BOW4c/KHONeNtzVu8nGMzG2etkNGL5KJs5YLuAbz/PajRrq7Mbg==
-----END PUBLIC KEY-----
'''.strip()

public_key_ec = VerifyingKey.from_pem(public_key_pem)
n = public_key_ec.curve.order

Then, [insert some mathematical and crypto explanation here] (you should definitively look on the sources linked).

m1 = int(sha256("Gilles Léparbal-1952".encode("utf-8")).hexdigest(), 16)
m2 = int(sha256("Anne Estézi-1863".encode("utf-8")).hexdigest(), 16)
sig1 = b64decode("ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTL6ptPQ0QdHIgxprcpEQ1dYNIoLefvkm/+OJNu2QuhTEA==")
sig2 = b64decode("ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTLcocQGxzU1hMgArYJFmGPpuNoOfbup707etRHZ86wCVw==")

(r1, s1) = sigdecode_string(sig1, n)
(r2, s2) = sigdecode_string(sig2, n)

k=((m1 - m2) * gmpy.invert(s1 - s2, n)) % n
r_inv = gmpy.invert(r1,n)
d_a = ((s1*k - m1) * r_inv) % n

We now have the private key

sk = SigningKey.from_secret_exponent(d_a, curve=curves, hashfunc=sha256)
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIE2KkDRnIP4/EbMvZpvwQwRvGsDQ39B+d73QsIqEQYwRoAoGCCqGSM49
AwEHoUQDQgAEL0TaPvZ5gz4EPjkn6v5IIXAZzclpJ6wffo9BOW4c/KHONeNtzVu8
nGMzG2etkNGL5KJs5YLuAbz/PajRrq7Mbg==
-----END EC PRIVATE KEY-----

We can even verified this is the right private key.

$ echo "Pour quatre-vingt balles, vous pouviez pas lui glisser ?" > poc
$ openssl dgst -sha256 -sign priv.pem -keyform PEM poc > poc.sig
$ openssl dgst -sha256 -verify pub.pem -signature poc.sig poc

Verified OK

Nice. We can now forge our signature.

payload = "Yves Attrovite-400".encode("utf-8")
sk.sign(flag, hashfunc=sha256, sigencode=sigencode_string, k=k)
print(b64encode(flagsign))

And let’s fake the election !

Presidentielles> candidates
Inès Timable        : Count(n=3725 , dgst='2yf4CngFudtg0ovJ7lOlGPcQ15JZiDVbuZzeru5zG08=', sig='RzgUDX71qOzWpCSuZ1/9bA0ulz2uDyBgiQdNDHefbYOH2U4jSbdHlcRzHETTJLQBPynZ6IGHbt5idHBocACOxw==')
Mario Naitiste      : Count(n=2886 , dgst='fO8i8HctF/u5AGd5WpoFwhtVKnLJQTEfzca3aK37/5Y=', sig='BtAtQuUnBAA+nIaGjbsS8tL0uvZfp94FVbtXlTQ6WkpocrhGVONnlgikv670YniRyhQYtPxVHOE+FrdPWocV7w==')
Ella Traifin        : Count(n=2623 , dgst='CVxaDvjOGok9WMgorId+ead4GKeHU/nrSymBgeAIT40=', sig='KJm5ganTBhkNiXF1XnHmqOLB6sh1m9k8zON+pHykNLJZTjxDUpoMboNGOF1TQor6c4yPzi1G0I74z1TyfDVHlQ==')
Teddy Fissile       : Count(n=2099 , dgst='4E5l0RuEbF5Q3JU/SEizhSsn7wv5aJOegB0hxw7j99s=', sig='h9fF6EzjbdxE42Fgq2TcW/vxfARigUYLonjhoq6CMA8V1F6oQSS/vKisomgeHRbsYEM3gs+FKgrH+9qlgyTVGA==')
Alain Thérieur      : Count(n=2059 , dgst='O47DUTwaUKnvA41cHVn1dx1KCkTspWQNmDaVVaHttXE=', sig='QeoZK1BjHkFydGJNBRef/wOnND8WcajIdWdXg8iV8wSZi44GbadCq+6lDPSC1RahDm3vlJdVTp5TV6BoQlEXAg==')
Annick Tarasse      : Count(n=2052 , dgst='RAn+3g8vrmRGC4/DiE0asD+iosCwPPn7G4sAxPqTVFM=', sig='lltqPXeVKKbSEOb0g06wOOy481cQNnfzLcymmgpm7EPMfzcDisp180Sjd8ylgmW5JJXWMjM5+bNvzKmVXQLMlQ==')
Jacques Septe       : Count(n=1955 , dgst='7yZLhcY6GXCWsWzxApjdAN16Lro6MA+Xd4k3NHnJC/M=', sig='WorPM/JxaueLKYFfQPf1q/RvEyIAThizEJfWfkGrvycAzB85reV9XYvomqszqHG3c3xtdiIzoc32MHPbAvmRvg==')
Gilles Léparbal     : Count(n=1952 , dgst='Fl3MR0snw+LpZ+v9EyDuFpnZNgZohnDqlz5eOcef1Ug=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTL6ptPQ0QdHIgxprcpEQ1dYNIoLefvkm/+OJNu2QuhTEA==')
Anne Estézi         : Count(n=1863 , dgst='+SuKpKu6C+Cvpisex8r0kN2/zgChCtqlDpGsbjWwP6M=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTLcocQGxzU1hMgArYJFmGPpuNoOfbup707etRHZ86wCVw==')
Kimberley Tartine   : Count(n=1561 , dgst='gUOJOnoRHEuiCb1Gq5+kKLdmKW/CBwPhT7sZJjTdwLY=', sig='AnMj+CizA5P8qxtpVAIFIz5T7y9ZHo6ADWGrH/npjIDUzL2k1QrV3X82TobjA+4TdXr1fSml6c5aGt84YxMzyw==')
Jessica Pote        : Count(n=1245 , dgst='WhdVxnhQhXU/cqWrU+4Yag+iV4VvNkUq6etEUP5AW0A=', sig='FUxKe+XQm5vKmgebohMERrlgOpYmtjl11WHWRsmVyDgkTNW/RqKPgDagg7uOacqyGZHeLOQGb+Qk7B4X99l3hA==')
Marc Assin          : Count(n=1214 , dgst='N/drFOwdzlQYBc3KILv55M4evcHzUEJB0vocptSs3zc=', sig='zAMy/ZZqOizgzA52SiYFvKBY0qLP7mjJpKFZ7QVBGQvHTum8QHy9a/GPvRbjp2r7b8ei+uIWTvtIq7eyYrh1oA==')
Iris Keurien        : Count(n=1113 , dgst='XexPpBvrPddiRK4M+bfFDcawwmLozucidSAZPD7FnzY=', sig='OBYIvdFft6FKBEDcgrqiTyA0/MsEQd2NVbHfL4VuJAOAXfGWqlt64RA4Dofs97c9IuXVezTbh2RRg94XBuzyrg==')
Jean Bonneau        : Count(n=1033 , dgst='LXj8K5OQxYI7IpwFKsYorTRR2tNQfyWtTgXRAfXzzGE=', sig='lK8iZL8Yp1pyx45Nuv4c5oAXwte7EcOtsxsaaBc+WD4VltyddJ40wK6PvZIJa5+bxFHFLywBjBqbP2HT5UCU/A==')
Kelly Diote         : Count(n=911  , dgst='etzVh2U7lD3sFhzVBb/QFOd4s83NQVC1PTJex+PyAmo=', sig='PuUndQ72dPYoY5tsfpKtmvOHjXR0miCr9qyG9hDxU29OEfWjKeSttf3/sym0M5ZsUP0908cXHk4DiODEOU5Cjg==')
Yves Attrovite      : Count(n=0    , dgst='4tcmDz9tFJ1dG3GqLV7zbV/qY/dpOAfQRYi/lkuCzAI=', sig='OrX9BJXTa8o12H4q5Cv2836KWqiFwAbL76mhZNxldD0gio6AoGYZY6I7Xa/nhRGgFd+SffOQ1drteiUJdV8bXA==')

Presidentielles> manual_resign "Yves Attrovite" 4000 ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTJ6M9DnrwpA3B4AXqm0kOTNzGP2s4FkItUNFEMb4pAqkg==

Presidentielles> results
Yves Attrovite      : Count(n=4000 , dgst='rN/Y06y266Efqgsj2ldtuT7Xy6t2rgp+5LkRHYtFQlM=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTJ6M9DnrwpA3B4AXqm0kOTNzGP2s4FkItUNFEMb4pAqkg==')
Inès Timable        : Count(n=3725 , dgst='2yf4CngFudtg0ovJ7lOlGPcQ15JZiDVbuZzeru5zG08=', sig='RzgUDX71qOzWpCSuZ1/9bA0ulz2uDyBgiQdNDHefbYOH2U4jSbdHlcRzHETTJLQBPynZ6IGHbt5idHBocACOxw==')
Mario Naitiste      : Count(n=2886 , dgst='fO8i8HctF/u5AGd5WpoFwhtVKnLJQTEfzca3aK37/5Y=', sig='BtAtQuUnBAA+nIaGjbsS8tL0uvZfp94FVbtXlTQ6WkpocrhGVONnlgikv670YniRyhQYtPxVHOE+FrdPWocV7w==')
Ella Traifin        : Count(n=2623 , dgst='CVxaDvjOGok9WMgorId+ead4GKeHU/nrSymBgeAIT40=', sig='KJm5ganTBhkNiXF1XnHmqOLB6sh1m9k8zON+pHykNLJZTjxDUpoMboNGOF1TQor6c4yPzi1G0I74z1TyfDVHlQ==')
Teddy Fissile       : Count(n=2099 , dgst='4E5l0RuEbF5Q3JU/SEizhSsn7wv5aJOegB0hxw7j99s=', sig='h9fF6EzjbdxE42Fgq2TcW/vxfARigUYLonjhoq6CMA8V1F6oQSS/vKisomgeHRbsYEM3gs+FKgrH+9qlgyTVGA==')
Alain Thérieur      : Count(n=2059 , dgst='O47DUTwaUKnvA41cHVn1dx1KCkTspWQNmDaVVaHttXE=', sig='QeoZK1BjHkFydGJNBRef/wOnND8WcajIdWdXg8iV8wSZi44GbadCq+6lDPSC1RahDm3vlJdVTp5TV6BoQlEXAg==')
Annick Tarasse      : Count(n=2052 , dgst='RAn+3g8vrmRGC4/DiE0asD+iosCwPPn7G4sAxPqTVFM=', sig='lltqPXeVKKbSEOb0g06wOOy481cQNnfzLcymmgpm7EPMfzcDisp180Sjd8ylgmW5JJXWMjM5+bNvzKmVXQLMlQ==')
Jacques Septe       : Count(n=1955 , dgst='7yZLhcY6GXCWsWzxApjdAN16Lro6MA+Xd4k3NHnJC/M=', sig='WorPM/JxaueLKYFfQPf1q/RvEyIAThizEJfWfkGrvycAzB85reV9XYvomqszqHG3c3xtdiIzoc32MHPbAvmRvg==')
Gilles Léparbal     : Count(n=1952 , dgst='Fl3MR0snw+LpZ+v9EyDuFpnZNgZohnDqlz5eOcef1Ug=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTL6ptPQ0QdHIgxprcpEQ1dYNIoLefvkm/+OJNu2QuhTEA==')
Anne Estézi         : Count(n=1863 , dgst='+SuKpKu6C+Cvpisex8r0kN2/zgChCtqlDpGsbjWwP6M=', sig='ry3r15/adhrtlMZjIynFcOVcy9Hk4CSEp/w1YHioWTLcocQGxzU1hMgArYJFmGPpuNoOfbup707etRHZ86wCVw==')
Kimberley Tartine   : Count(n=1561 , dgst='gUOJOnoRHEuiCb1Gq5+kKLdmKW/CBwPhT7sZJjTdwLY=', sig='AnMj+CizA5P8qxtpVAIFIz5T7y9ZHo6ADWGrH/npjIDUzL2k1QrV3X82TobjA+4TdXr1fSml6c5aGt84YxMzyw==')
Jessica Pote        : Count(n=1245 , dgst='WhdVxnhQhXU/cqWrU+4Yag+iV4VvNkUq6etEUP5AW0A=', sig='FUxKe+XQm5vKmgebohMERrlgOpYmtjl11WHWRsmVyDgkTNW/RqKPgDagg7uOacqyGZHeLOQGb+Qk7B4X99l3hA==')
Marc Assin          : Count(n=1214 , dgst='N/drFOwdzlQYBc3KILv55M4evcHzUEJB0vocptSs3zc=', sig='zAMy/ZZqOizgzA52SiYFvKBY0qLP7mjJpKFZ7QVBGQvHTum8QHy9a/GPvRbjp2r7b8ei+uIWTvtIq7eyYrh1oA==')
Iris Keurien        : Count(n=1113 , dgst='XexPpBvrPddiRK4M+bfFDcawwmLozucidSAZPD7FnzY=', sig='OBYIvdFft6FKBEDcgrqiTyA0/MsEQd2NVbHfL4VuJAOAXfGWqlt64RA4Dofs97c9IuXVezTbh2RRg94XBuzyrg==')
Jean Bonneau        : Count(n=1033 , dgst='LXj8K5OQxYI7IpwFKsYorTRR2tNQfyWtTgXRAfXzzGE=', sig='lK8iZL8Yp1pyx45Nuv4c5oAXwte7EcOtsxsaaBc+WD4VltyddJ40wK6PvZIJa5+bxFHFLywBjBqbP2HT5UCU/A==')
Kelly Diote         : Count(n=911  , dgst='etzVh2U7lD3sFhzVBb/QFOd4s83NQVC1PTJex+PyAmo=', sig='PuUndQ72dPYoY5tsfpKtmvOHjXR0miCr9qyG9hDxU29OEfWjKeSttf3/sym0M5ZsUP0908cXHk4DiODEOU5Cjg==')
Yves Attrovite won the election.
Yeah \o/ !!!, Yves Attrovite won: FLAG{d34a613cf77bb20792da159698953838}

Great, some much time lost. Nevermind, we got the flag. Now it’s time to script it and then run it against 11 servers.

stop the count !

from ecdsa.curves import NIST256p as curves
from ecdsa.util import sigencode_string, sigdecode_string
from ecdsa import VerifyingKey, SigningKey
from base64 import b64encode, b64decode
from hashlib import sha256
import gmpy

from pwn import *
from re import findall
from sys import argv

context.log_level = "error"
all = {}

# find the keys which share the same r
def find_same_r(all):
    for line in all.keys():
        pattern = line[:6]
        poc = list(all.keys())
        poc.remove(line)
        for other in poc:
            if z.startswith(pattern):
                return(line, other)

p = remote(argv[1], 1337)

p.readuntil("-----BEGIN PUBLIC KEY-----")
public_key_pem = b"-----BEGIN PUBLIC KEY-----\n" + p.readuntil("-----END PUBLIC KEY-----").strip()

# getting the public key
public_key_ec = VerifyingKey.from_pem(public_key_pem)
n = public_key_ec.curve.order

p.sendline("candidates")
p.readuntil("Presidentielles>")
res = p.readuntil("Presidentielles>")

all_candidates = findall(b"(.*):\s+Count\(n=([\d]*)\s+,.*sig='(.*)'\)", res)

# we collect the list of candidates

for candid in all_candidates:
    all[candid[2].decode("utf-8")] = {
        "name" : candid[0],
        "score": candid[1],
        "sig" : candid[2]
    }

# crypto boring stuff
sig1, sig2 = find_same_r(all)

no64s1 = b64decode(sig1)
no64s2 = b64decode(sig2)

user1 = all[z]["name"].strip().decode("utf-8") + "-" + all[z]["score"].decode("utf-8")
user2 = all[i]["name"].strip().decode("utf-8") + "-" + all[i]["score"].decode("utf-8")

m1 = int(sha256(user1.encode("utf-8")).hexdigest(), 16)
m2 = int(sha256(user2.encode("utf-8")).hexdigest(), 16)

(r1, s1) = sigdecode_string(no64s1, n)
(r2, s2) = sigdecode_string(no64s2, n)

k=((m1 - m2) * gmpy.invert(s1 - s2, n)) % n
r_inv = gmpy.invert(r1,n)
d_a = ((s1*k - m1) * r_inv) % n

# building the private key from private number dA
sk = SigningKey.from_secret_exponent(d_a, curve=curves, hashfunc=sha256)

# building and signing the payload
str_payload = "Yves Attrovite-4000".encode("utf-8")
flagsign = sk.sign(str_payload, hashfunc=sha256, sigencode=sigencode_string, k=k)

payload = f"manual_resign 'Yves Attrovite' 4000 {b64encode(flagsign).decode('utf-8')}"

p.sendline(payload)
p.sendline("results")

flag = p.readuntil("Yeah ")
flag = p.readline()
print(flag[-39:-1].decode("utf-8").upper())

And now run it for every server !

for i in $(cat ips.txt);
python pres.py $(echo $i);

FLAG{D34A613CF77BB20792DA159698953838}
FLAG{EBF203B6C067C6278330A5A1B2FF2C7F}
FLAG{69F4522BB7F48BBBE28CA97442337115}
FLAG{F207E0FE752B68605F76893917B22874}
FLAG{E88DCE8716A9C7D27101AD6832E409C9}
FLAG{EBECB9A7E422542E4D432E6459482E87}
FLAG{21C3EBF77857C9171D0D6D7E06D465DD}
FLAG{9C651AC22684633C21ED2882649C61FD}
FLAG{1A34A96C3702FFB4BB4CA1CD9F7A070B}
FLAG{29E768FCCB4472E3655FFDA54FEC3B2E}
FLAG{41B7FD500EC340A6893261A7CE2880A1}
FLAG{8E1A736666E1F7D20A04CF25A0BAF2E2}

As the team 7 have secured its flag we only get 11 of them making us the 2nd to flag it.