Salut à tous, aujourd’hui on va continuer le cycle d’article sur les mots de passes, et plus particulièrement : comment stocker un mot de passe. Pour info, je me pompe sans honte sur cet article de sophos, qui est au top sur le sujet.
Comment on faisait ? ou plutôt comment ne pas stocker un mot de passe.
En clair !
La plus ancienne méthode de stockage consistait à stocker les mots de passe… en clair ! Elle pose quelques petits problèmes, assez évidents : en cas d’attaque réussie, en cas de réutilisation du mots de passe, etc. Cette méthode là, on ne la rencontre plus trop chez les professionnels. Mais parfois, on tombe encore sur un utilisateur qui garde un joli fichier Excel avec tout ses mots de passe dedans.
R1- Ne pas stocker les mots de passe en clair, donc.
Chiffré ?
Puisqu’on ne peut pas les stocker en clair, on va les chiffrer, non ? bein non plus, déjà cela signifie que les administrateurs accèdent encore au clair de vos mots de passe. En fait, en chiffrant votre base de mot de passe vous ne faites que transférer la sécurité sur le secret de la clé de chiffrement. Résultat l’attaquant essaiera plutôt d’attaquer le service détenant cette clé avant de péter votre base chiffré et de revenir au premier cas, en clair.
R2- Ne chiffrer la base de mots de passe.
Hashé alors ??
Bon alors qu’est ce qu’on doit faire ? On va les hasher, non ? La méthode n’est pas réversible normalement, les admins n’auront plus la connaissance des mots de passe des utilisateurs et cela permet de vérifier que le mot de passe saisi par l’utilisateur est correct en recalculant le hash.
Et bein… non : déjà les algo de hash vieillissent et finissent tous par devenir plus ou mois réversibles, dans un temps raisonnable, comme MD5 ou SHA1 plus récemment. Mais aussi parcequ’un algo de hash est mathématiquement une surjection (et une fonction de hash « parfaite » serai injective), ce qui signifie, d’une part, qu’il peut y avoir des collisions (i.e deux mots de passe donnant le même hash). Et, d’autre part, que deux utilisateurs ayant le même mots de passe auront le même hash stocké dans la base. Déjà cela donne une information (ces utilisateurs ont le même mots de passe), mais surtout cela permet des attaques par rainbow table en pré-calculant pleins de hashs probables à partir d’un dictionnaire par exemple.
R3- Ne pas utiliser le hash des mots passes non plus.
Hashé et salé ???
Bon… on bah va les saler nos hash alors ? Vu que le problème avec le hash vient de la surjection, si on mélange l’entrée avec un élément aléatoire fixe, aléatoire et unique à chaque utilisateur, deux utilisateurs avec le même mot de passe n’auront plus le même hash en base (en dehors de quelques collisions très improbables). L’attaquant devra alors précalculer autant de rainbow table qu’il y a de valeurs de sel différente dans la base. On est bon là, notre stockage de mots de passe :
- n’est pas réversible ;
- présente des hash uniques par utilisateur ; et
- ne donne pas d’indication sur le mots de passe.
Bah non, toujours pas, en fait les capacités de calcul des machines actuelles sont devenues tellement violentes que, même ça, c’est encore trop facile. Quelques exemples :
- crack.sh : ou comment péter du DES (2^56 clé) en 26h pour 20$.
- onlinehashcrack.com : propose un service semi-gratuit pour péter du mot passe.
- Sophos : présente, ici, une machine capable de taper les 400 Milliard de MD4 par seconde, qu’ils annoncent à 100 milliard de SHA-256.
- Une rapide recherche Google vous remontera pas mal de Geeks qui, avec quelques nvidia 1080Ti, arrivent aussi à taper aussi les 100 Milliards de hash par seconde sur du SHA1!
Et au-dessus, c’est que sur des gens qui n’utilisent gentiment que les moyens « grand public ». Je vous laisse imaginer ce que la NSA et nos barbouzes devraient savoir faire avec un supercalculateur du TOP500.
Tout cela signifie qu’à ce point, le problème n’est pas « théorique » mais « pratique » : notre calcul de hash est trop simple donc trop rapide. Et même si c’est déjà mieux que toutes les autres méthodes qu’on a vu, ce n’est pas assez.
R4 – Hasher et saler les mots de passe ce n’est plus suffisant…
Alors comment on le stocke ce foutu mot de passe ???
Hashé, salé et étiré !
Je l’ai dit, on doit rendre plus longue et plus dur (non, tu sors) le calcul du hash stocker pour notre mot de passe. Il faut que ce soit trop long pour un attaquant, mais pas pour un utilisateur qui s’authentifie. Un bon moyen pour faire ça, c’est de boucler sur une fonction HMAC-SHA (j’en parle dans mon article du YubiKey et KeePass).
Pour expliquer ça simplement,
- Prenez votre mot de passe.
- Concaténez son sel.
- Calculez R, le HMAC-SHA256 du sel et du mot de passe..
- Puis « n » fois :
- Calculer S, le HMAC-SH256 du mot de passe avec le HMAC-SHA256 précédent (R).
- faite le XOR S et R dans une variable H
- R devient S ;
- et on recommence la boucle.
- H est la valeur du mot de passe hashé, salé et streché à stocker.
A partir de là, utilisez un nombre n suffisamment élevé pour que cela soit assez rapide à calculer pour votre serveur, pas sensible pour l’utilisateur, mais bien trop long pour qu’un attaquant puisse calculer rapidement plusieurs valeurs de hash à la suite sans avoir envi de se pendre.
C’est d’ailleurs pour cette raison que KeePass vous propose l’option ci-dessous quand vous créez une base de mot de passe. Cela vous permet d’ajuster ce « n » pour que votre machine fasse le calcul en une seconde et de rendre toute attaque par bruteforce très, très, très, lente.
Et voilà, alors la méthode que j’ai expliqué ici c’est PBKDF2, mais quand vous entendrez parler de Bcrypt ou Argon2, c’est exactement le même principe derrière. C’est simplement l’implémentation qui bouge un peu. Il y a des subtilités d’experts possibles entre ces technos, mais déjà si vous êtes sur ces familles d’algo au moment au je rédige ces lignes, un attaquant tombant sur votre base de mots de passe ne pas passer un moment agréable.
Bonus, on implémente ça en PowerShell ?
Ouais, parce que pourquoi pas, donc vite fait ça donne le bout de code ci-dessous :
function Get-HMACSHA256 {
Param(
[Parameter(Mandatory=$true)] [Byte[]] $message,
[Parameter(Mandatory=$true)] [Byte[]] $cle
)
$hmacsha = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha.key = $cle
$signature = $hmacsha.ComputeHash($message)
return $signature
}
function Get-RandomSalt{
Param(
[Parameter(Mandatory=$false)] [int] $size=16
)
$MyBuffer = New-Object byte[] $size
for($i=0;$i -lt $size;$i++){
$MyBuffer[$i] = [byte] (Get-Random -Minimum ([byte]::MinValue) -Maximum ([byte]::MaxValue))
}
return $MyBuffer
}
function Get-bxorArray{
Param(
[Parameter(Mandatory=$true)] [Byte[]] $a,
[Parameter(Mandatory=$true)] [Byte[]] $b
)
if($a.length -ne $b.length){
Write-Debug 'Invalid parameters size, Arrays must have the same size'
Throw ParameterArgumentValidationError
}
[Byte[]] $c = New-Object byte[] $a.length
for($i=0; $i -lt $a.length ; $i++)
{
$c[$i] = $a[$i] -bxor $b[$i]
}
return $c
}
function Get-PBKDF2Password{
Param(
[Parameter(Mandatory=$true)] [Byte[]] $password,
[Parameter(Mandatory=$true)] [Byte[]] $salt,
[Parameter(Mandatory=$true)] [int32] $iteration=10
)
$hash = Get-HMACSHA256 -message $password -cle $Salt
for($i=0 ; $i -lt $iteration; $i++){
$hash2 = Get-HMACSHA256 -message $password -cle $hash
$hash = Get-bxorArray $hash $hash2
}
return $hash
}
$Password = [Text.Encoding]::ASCII.GetBytes("Th1s_IsMys3cr3t!")
$Salt = Get-RandomSalt
$b64Salt = [Convert]::ToBase64String($Salt)
$nb = 1000
$b64stretchedhash = [Convert]::ToBase64String((Get-PBKDF2Password -password $Password -salt $salt -iteration $nb))
$stored_info_password = "logininfos;$nb;$b64Salt;$b64stretchedhash"
Write-host $stored_info_password
#Checking
# $inpasswd="logininfos;10;gR7KmmHBVvbn2CehGvG9gQ==;Ugq8UuNFUpoLXzml5ThJWi40Kbxdgpzqf8xP/WuCczQ="
$storedb64salt = $stored_info_password.Split(';')[2]
$storedSalt = [Byte[]] [Convert]::FromBase64String($storedb64salt)
$storedIter = [int] $stored_info_password.Split(';')[1]
$computedHash = [Convert]::ToBase64String((Get-PBKDF2Password -password $Password -salt $storedSalt -iteration $storedIter))
$toprocess = "logininfos;$storedIter;$storedb64salt;$computedHash"
Write-host $toprocess
if($computedHash -eq $b64stretchedhash){
Write-Host "success"
}else{
Write-Host "failure"
}
Conclusion
Comme d’habitude, j’espère que ça vous aura intéressé. Pour finir je vous invite vraiment à passer, quand c’est possible, sur ce type d’algorithme (Bcrypt like) pour le stockage de vos mots de passe. Et pour l’avoir vécu, je vous assure que c’est très frustrant d’arriver à dumper la base de mots de passe pour tomber sur du Bcrypt, et voir John arriver péniblement à quelques essais par seconde.
Bref, obligez l’attaquant à réfléchir et geekez bien !
@+