Faonsa
Catégorie: medium
Points: 180
Résolutions: 21
Énoncé
Deploying the fault attack in real life is hard, we deployed it artificially!
from Crypto.Util.number import *
from math import gcd
import sys
from flag import flag
def diff(a, b):
assert a.bit_length() == b.bit_length()
w, l = 0, a.bit_length()
for _ in range(l):
if bin(a)[2:][_] != bin(b)[2:][_]: w += 1
return w
def sign_esa(pubkey, x, m):
g, p, y = pubkey
while True:
k = getRandomRange(2, p-1)
if gcd(k, p-1) == 1:
break
u = pow(g, k, p)
v = (m - x * u) * inverse(k, p - 1) % (p - 1)
return (u, v)
def verify_esa(pubkey, sgn, m):
g, p, y = pubkey
u, v = sgn
return pow(y, u, p) * pow(u, v, p) % p == pow(g, m, p)
def die(*args):
pr(*args)
quit()
def pr(*args):
s = " ".join(map(str, args))
sys.stdout.write(s + "\n")
sys.stdout.flush()
def sc():
return sys.stdin.readline().strip()
def main():
border = "|"
pr(border*72)
pr(border, "Hello guys! This is a another challenge on fault attack too, again ", border)
pr(border, "our storage could apply at most `l' bit fault on ElGamal modulus, p,", border)
pr(border, "try to sign the given message and get the flag! Have fun and enjoy!!", border)
pr(border*72)
pr(border, "Generating the parameters, it's time consuming ...")
nbit = 256
while True:
_p = getPrime(255)
p = 2 * _p + 1
if isPrime(p):
g = 2
if pow(g, _p, p) != 1: break
else: g += 1
x = getRandomRange(2, p // 2)
y = pow(g, x, p)
B, l = [int(b) for b in bin(p)[2:]], 30
MSG = "4lL crypt0sy5t3ms suck5 fr0m faul7 atTaCk5 :P"
m = bytes_to_long(MSG.encode('utf-8'))
while True:
pr("| Options: \n|\t[A]pply fault \n|\t[G]et the parameters \n|\t[S]ign the message \n|\t[V]erify the signature \n|\t[Q]uit")
ans = sc().lower()
if ans == 'a':
_B = B
pr(border, f"please send at most {l}-tuple array from indices of bits of ElGamal modulus, like: 5, 12, ...")
ar = sc()
try:
ar = [int(_) for _ in ar.split(',')]
if len(ar) <= l:
for i in range(len(ar)): _B[ar[i]] = (_B[ar[i]] + 1) % 2
P = int(''.join([str(b) for b in _B]), 2)
Y = pow(g, x, P)
else: raise Exception('Invalid length!')
except: pr(border, "Something went wrong!!")
elif ans == 'g':
pr(border, f'p = {p}')
pr(border, f'g = {g}')
pr(border, f'y = {y}')
elif ans == "v":
pr(border, "please send signature to verify: ")
_flag, signature = False, sc()
try:
signature = [int(_) for _ in signature.split(',')]
if verify_esa((g, P, Y), signature, m): _flag = True
else: pr(border, "Your signature is not valid!!")
except:
pr(border, "Something went wrong!!")
if _flag: die(border, "Congrats! your got the flag: " + flag)
elif ans == 's':
pr(border, "Please send your message to sign: ")
msg = sc().encode('utf-8')
if msg != MSG.encode('utf-8'):
_m = bytes_to_long(msg)
try:
sgn = sign_esa((g, P, Y), x, _m)
pr(border, f'sign = {sgn}')
except:
pr(border, "Something went wrong!!")
else:
pr(border, "Kidding me!? Really?")
elif ans == 'q': die("Quitting ...")
else: die("Bye bye ...")
if __name__ == "__main__": main()
Analyse
Le serveur implémente un schéma de signature de type El Gamal
Voir Wikipédia
On se donne ainsi:
Un nombre premier p public
Un générateur multiplicatif g (public)
Un exposant secret x
La valeur publique de g^x
Pour un message m, une signature est un couple (u,v)
tel que y^u * u^v = g^m
.
Le serveur choisit p de taille 256 bits avec la propriété classique p-1 = 2q
où q est premier, pour éviter d’avoir un logarithme discret rapide.
On nous donne la possibilité de basculer 30 bits du nombre p
, puis on doit envoyer au serveur une signature du message m
connu.
Après l’attaque par fautes, il est aussi possible de demander au serveur la signature d’un message différent de m
.
Stratégie
Le chemin le plus naturel pour fabriquer une signature est de trouver la valeur de l’exposant secret x
.
Malheureusement, lorsqu’on attaque le nombre premier p
pour le modifié, la valeur de y = g^x mod p
est recalculée et n’est plus accessible. En revanche, le serveur permet de fabriquer des signatures (pour des messages différents de m).
Dans un premier temps, on cherche à modifier p
pour que p-1
soit lisse. Comme on peut basculer 30 bits:
-
on met à zéro les bits de poids faible (en moyenne, la moitié des bits est à 1) de sorte que p-1 soit divisible par
2^k
aveck > 50
-
on met à zéro des bits de poids plutôt fort, en les choississant de sorte que
p-1
soit le plus lisse possible
Comme p est de taille 256 bits, un bon compromis est de modifier 26 bits de poids faible (alors p-1 se termine par au moins 50 zéros), et d’utiliser les 4 autres pour rechercher un nombre premier lisse. Avec un espace de recherche de taille au moins 200^4
on espère trouver facilement des candidats.
De plus, grâce au facteur 2^50
la factorisation de p-1
se ramène à des nombres de taille plus petite (environ 200 bits).
Expérimentalement, il suffit d’une minute pour trouver p
tel que les facteurs possèdent au plus 17 chiffres.
Une fois un tel p
trouvé, on doit extraire de l’information: en réalité il n’est pas absolument nécessaire de connaître x
ou y
, on peut fabriquer une signature en utilisant une astuce de masquage.
Étant donné une signature:
y^u * u^v = g^m
La connaissance du logarithme discret g = u^z
permet de fabriquer d’autres signatures:
y^u * u^(v+kz) = g^(m+k)
Il suffit donc par exemple de signer le message m-1
(en remplaçant le ‘P’ final par la lettre ‘O’), puis de renvoyer (u, v+z)
Solution
from sage.all import factor, is_prime, Zmod
from telnetlib import Telnet
import itertools
import ast
g = 2
c = Telnet("06.cr.yp.toc.tf", 31117)
print(c.read_until(b"uit\n"))
# Lecture des params
c.write(b"G\n")
while True:
line = c.read_until(b"\n").decode()
if "uit" in line:
break
if "p =" in line:
p = int(line.split()[-1])
if "y =" in line:
y = int(line.split()[-1])
print("got params")
print("g = 2")
print("p =", p)
print("y =", y)
length = p.bit_length()
# Cherche un flip sympa
bits = [i for i in range(1, 200) if (p>>i) & 1]
for choice in itertools.combinations(bits[26:], 4):
faults = bits[:26] + list(choice)
#print(faults)
pflip = p - sum(1<<i for i in faults)
if not is_prime(pflip):
continue
print("prime", faults)
factors = factor(pflip-1)
print(factors)
if all(f < 1e17 for f, _ in factors):
print("lisse => gagné")
break
print("faults", faults)
print("flipped P", pflip)
# On envoie les fautes
c.write(b"A\n")
print(c.read_until(b"\n"))
faults_str = ",".join(str(length-1-b) for b in faults)
c.write((faults_str + "\n").encode())
print(c.read_until(b"uit\n"))
# On signe m-1
MSG_MINUS_ONE = "4lL crypt0sy5t3ms suck5 fr0m faul7 atTaCk5 :O"
c.write(b"S\n")
print(c.read_until(b"\n"))
c.write((MSG_MINUS_ONE + "\n").encode())
sig_str = c.read_until(b"\n").decode()
u, v = ast.literal_eval(sig_str.split("=")[-1])
print(f"signature(MSG-1) = ({u}, {v})")
print(c.read_until(b"uit\n"))
dlog = Zmod(pflip)(g).log(u)
v += dlog
c.write(b"V\n")
c.write(("%s,%s\n" % (u,v)).encode())
print(c.read_until(b"\n"))
print(c.read_until(b"\n"))
Exemple de sortie:
g = 2
p = 63558542919681679657827509812151026228657575786278721651088204705887423094019
y = 56305067584657944939138280017722781136893024438679815887320125089936809511003
...
prime [1, 8, 10, 11, 12, 14, 15, 19, 23, 24, 25, 28, 32, 33, 34, 35, 36, 37, 38, 39, 45, 48, 52, 53, 56, 57, 59, 70, 83, 131]
2^72 * 3 * 7^2 * 101 * 113177 * 127681 * 209263 * 3157188179 * 88811450443 * 1069126771864087
lisse => gagné
faults [1, 8, 10, 11, 12, 14, 15, 19, 23, 24, 25, 28, 32, 33, 34, 35, 36, 37, 38, 39, 45, 48, 52, 53, 56, 57, 59, 70, 83, 131]
flipped P 63558542919681679657827509812151026225935316850911204270793252846594275934209
b'| please send at most 30-tuple array from indices of bits of ElGamal modulus, like: 5, 12, ...\n'
b'| Options: \n|\t[A]pply fault \n|\t[G]et the parameters \n|\t[S]ign the message \n|\t[V]erify the signature \n|\t[Q]uit\n'
b'| Please send your message to sign: \n'
signature(MSG-1) = (39168832821243646424877677228052654160940445777491337832834889770336825592500, 9638551011955447360951672204231232505283569332270235659820346627565410439715)
(une ou deux minutes de calcul)
b'| Options: \n|\t[A]pply fault \n|\t[G]et the parameters \n|\t[S]ign the message \n|\t[V]erify the signature \n|\t[Q]uit\n'
b'| please send signature to verify: \n'
b'| Congrats! your got the flag: CCTF{n3W_4t7aCk_8y_fAuL7_!nJ3cT10N_oN_p!!!}\n'
Note: des participants ont signalé que le serveur permettait d’accumuler plus de fautes que prévu (à caause de _B = B
, comme dans le défi ‘Fiercest’).