Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

comparaisons en "temps constant"

Aucune réponse
Avatar
Manuel P
Bonjour,

Je m'interroge sur certaines des techniques proposées dans [1]. Un peu
de contexte : il s'agit d'éviter les fuites d'information par mesure du
temps d'exécution dans du code cryptographique, et ce qui me pose
particulièrement question est le fait de n'utiliser que des opérations
bit à bit plutôt que les opérateurs '==' et '!=' quand ils seraient
naturels. (Je n'ai par contre bien sûr aucun doute concernant le fait
d'éviter les sorties de boucles dépendant des données comme dans
l'exemple initial de memcmp().)

[1]: https://cryptocoding.net/index.php/Coding_rules#Solution

Pour commencer, j'ai l'impression qu'il y a des soucis potentiels/
théoriques de portabilité, par exemple :

/* Return 1 if condition is true, 0 otherwise */
int ct_isnonzero_u32(uint32_t x)
{
return (x|-x)>>31;
}

On va dire qu'on se limite aux plateformes où uint32_t existe, de toutes
façons si c'est pas le cas ça se verra vite à la compilation :)
Imaginons par contre que le compilo ait choisit des int de 64 bits. Sauf
erreur de ma part, x va être promu en int et, dans le cas x != 0, on se
retrouve à appliquer un décalage sur un négatif, ce qui est défini par
l'implémentation, et au moins une des options raisonnables pour une
implémentation (propager le bit de signe) ne donne pas le résultat
attendu (-1 au lieu de 1 si x != 0). (Par contre je ne vois pas d'UB.)

Il me semble que ce problème (tout théorique à ma connaissance) peut
être résolu en ajoutant un cast:

return (x | (uint32_t) -x) >> 31;

Est-ce que vous confirmez cette analyse ou j'ai manqué quelque chose ?

Par ailleurs, la raison souvent évoquée pour préférer ce genre de
construction à un simple return x != 0, est que certains compilos
risquent de générer un saut conditionnel, ce qu'on souhaite éviter. Bon.
En même temps, comme noté un peu plus bas sur la page citée, le compilo
fait bien ce qu'il veut, et il peut aussi bien générer une branche si on
écrit des opérations de bits, que ne pas en générer pour un '!='. De
fait, j'ai examiné l'assembleur produit par GCC et Clang (récents) avec
différents niveaux d'optimisation pour x86 et ARM (les deux seuls
assembleurs que je lis un peu) avec "return x != 0;" et il m'a paru
exempt de branches ou autre construction en temps non constant. Ce qui
bien sûr ne prouve rien, et je n'ai pas trouvé de résultats de test à
une échelle un peu plus grande.

Après, il semble en effet raisonnable de supposer que des opérations sur
les bits sont moins susceptibles d'être traduites en branches que des
opérateurs de comparaison, vu que pour ça il faut (1) que le compilo
"comprenne" ce qu'on fait et (2) qu'il décide qu'une branche est lq
meilleure façon d'exprimer ça. Si on utilise des opérateurs de
comparaison, la condition (1) est automatiquement rempile.

En même temps, en face de ce bénéfice potentiel, on a un coût qui lui
n'est pas du tout hypothétique en termes de lisibilité du code (voire de
risque de se planter en écrivant du code trop astucieux).

Bien sûr la seule solution sûre, c'est d'écrire les parties critiques en
assembleur directement, mais ça pose d'autres questions en termes du
nombre de plateformes à supporter et/ou de sécurité potentiellement
inégale suivant les plateformes.

Bref, vous en pensez quoi ? Dans les cas où l'assembleur n'est pas une
option, vous l'écririez comment, cette fonction isnonzero() ?

Merci d'avance pour vos remarques et conseils,
Manuel.

8 réponses

1 2
Avatar
Antoine Leca
Le 13/03/2015 23:48, Samuel DEVULDER écrivit :
Cependant la question peut se poser de savoir quelle est la sémantique
formelle de -x quand x est unsigned.



Le résultat habituel en complément à 2, y compris si la machine utilise
une autre arithmétique -- si le compilateur est conforme à la norme !

Pour moi ca fait un overflow.



Non. C'est une des propriétés de base des unsigned de ne jamais créer de
situations de débordement et de toujours opérer sur l'ensemble Z/nZ
(avec n=UINT_MAX+1), et ce devrait être la raison pour utiliser des
unsigned dans du code (et non pas comme certains semble penser, le fait
que la quantité à représenter soit toujours positive).


Antoine
Avatar
espie
In article <me1ddn$177f$,
Manuel Pégourié-Gonnard wrote:
Le Sat, 14 Mar 2015 09:55:37 +0000, Marc Espie a écrit :

Grosso-modo, la plupart de ces trucs reviennent a remplacer des trucs
clairs par des equivalents obfuscated qui font la meme chose.



Ouais, totalement.

En tant qu'auteur de compilo, je verrais ca comme un challenge:
qu'est-ce qu'il faut que je rajoute dans mon compilateur pour
optimiser ces trucs quand meme ? :p



:D J'espère quand même qu'ils le feront pas exprès pour embêter les
utilisateurs, mais effectivement je m'attends à ce que ce genre
d'obfuscation marche de moins en moins.



Ils l'ont deja fait expres en optimisant memset ce qui fait que nettoyer
une cle apres usage ne sert plus a rien.

Ils l'ont deja fait expres en optimisant tout le code d'un programme
et en virant donc certains appels de fonction/optimisant a travers les
fonctions.

Note: non, bien sur, c'est pas expres. Mais optimiser, c'est surtout
comprendre mieux ce que fait un code, et mieux on comprend plus on est
a meme de reagir en consequence...

Apres, de toutes facons, un fabricant de processeur est deja en train
de faire des trucs qui vont limiter ce genre de choses.



Tu penses à un truc en particulier ?



Le fait que d'ores et deja, pour calculer le temps que va prendre une
sequence d'instructions, c'est extremement complexe et un poil non
previsible... si tu es fabricant de processeur, si tu as une sequence
d'instructions qui peut prendre 3 cycles de moins si elle tombe sur un
zero a un endroit dans les donnees qu'elle traite, en general, tu VAS
prendre 3 cycles de moins. Et donc le timing te dira qu'il y avait un
zero.

Principe general, pas besoin de penser a un truc en particulier.

Il n'y a qu'a voir les bugs etranges qu'on a pu avoir sur loongson,
ou certaines generations avaient quelques soucis avec la prediction
de code (trop de sauts d'affilee == debordement de la pile de
prediction; test de pointeur nul == on prend les deux branches et on
collapse quand on a le resultat du test. Dommage quand ca provoque
des panic en mode noyaux => necessiter d'inserer assez de NOP pour
temporiser entre le test et l'utilisation du pointeur...)

De fait, tu as une micro-machine dans ton cpu, et tu n'as AUCUN
CONTROLE sur les optimisations qu'elle fait et sur le niveau
d'optimisation que tu voudrais.
Avatar
Antoine Leca
Le 16/03/2015 11:05, Marc Espie écrivit :
In article <me1ddn$177f$,
Manuel Pégourié-Gonnard wrote:
Le Sat, 14 Mar 2015 09:55:37 +0000, Marc Espie a écrit :
Apres, de toutes facons, un fabricant de processeur est deja en train
de faire des trucs qui vont limiter ce genre de choses.



Tu penses à un truc en particulier ?



Le fait que d'ores et deja, pour calculer le temps que va prendre une
sequence d'instructions, c'est extremement complexe et un poil non
previsible. [...]
Principe general, pas besoin de penser a un truc en particulier.



En fait, on touche là à une limite intrinsèque de l'exercice : le but du
code « à temps constant » est exactement l'inverse du but poursuivi par
tout le monde : en conséquence, à chaque fois que des techniques du
genre de celles proposées sur les manipulations de bits vont se
répandre, les "autres", alliant fabricants de processeurs et écrivains
de compilateur, vont repérer (volontairement ou pas) ces séquences et
vont les remplacer par les « meilleures » séquences (dont on est certain
qu'elles existent !) C'est ce qu'expliquait Marc auparavant, il
«verrai[t] ca comme un challenge».


De fait, tu as une micro-machine dans ton cpu, et tu n'as AUCUN
CONTROLE sur les optimisations qu'elle fait et sur le niveau
d'optimisation que tu voudrais.



Dans la première génération des Pentium prédisant les sauts, il y avait
un mécanisme (utilisant des préfixes de segment) pour indiquer au
micro-code une préférence, dans un sens ou l'autre, pour les sauts
conditionnels. Un effet de système a été de remplir le code source de
Linux de « jolies » macros pour indiquer le sens supposé des sauts,
histoire de gagner quelques µs. J'ai l'impression que à peine une
génération de processeur plus tard, le processeur ne faisait plus cas
des dites indications (soit pour des question de taille de pipeline, de
sécurité, de sous-optimisation ou autre) ; en tous cas, ce truc a
quasiment disparu des manuels de l'architecture IA-32, et le fait est
que plus personne ne s'en préoccupe.
Un peu comme le mot-clé register en C (qui lui a encore une utilité,
interdire d'utiliser & sur un objet, mais elle n'est pas intuitive.)


Antoine
Avatar
Jean-Marc Bourguet
Manuel Pégourié-Gonnard writes:

Apres, de toutes facons, un fabricant de processeur est deja en train
de faire des trucs qui vont limiter ce genre de choses.



Tu penses à un truc en particulier ?



- les caches: un accès à la mémoire va prendre plus ou moins de temps
suivant les accès qui ont eu lieu avant. C'est considéré en sécurité
comme un canal caché (on arrive à connaître des choses sur l'OS de cette
manière). Petite anectode d'il y a 30 ou 40 ans. Un programme est
arrivé à craquer des mots de passe de la manière suivante. L'OS qui les
vérifiait retournait une erreur dès qu'il se rendait compte que le mot de
passe n'était pas celui attendu. En placant le mot de passe à la
frontière entre deux pages, le programme savait la partie du mot de passe
placée sur la première était correcte ou pas (il s'arrangeait pour que la
seconde soit swappée, si l'OS y accédait, c'était facile de s'en rendre
compte). De nos jours on hache (du moins, ceux qui sont consciencieux et
qui arrivent à faire prendre conscience des problèmes aux décideurs) les
mots de passe, ça rend la technique inutilisable.

- plus généralement, la source des perfs, c'est le parallélisme. Une
source auxilliaire, c'est la capacité de deviner ce qui peut-être utile.
Les processeurs font des merveilles pour extraire tout le parallélisme
qu'ils peuvent du flux d'instruction et pour essayer de deviner ce qui
peut-être utile quand ils ne sont pas sûr. Parallélisme détecté
statiquement et dynamiquement (l'échec de l'Itanic^um, c'est l'échec de
vouloir tout faire statiquement à la compilation). Un processeur haut de
gamme de nos jours a de l'ordre de la centaine d'instructions en cours
d'exécution en même temps. Par coeur. Il y a les pipelines bien connus.
Mais aussi il est capable de commencer l'exécution de plusieurs
instructions en même temps (ce qu'on appelle superscalaire, c'est pas
récent, ça date des années 60; en fait toutes les techniques datent des
années 60, depuis d'un point de vue conceptuel on n'a guère fait que de
les paufiner, le progrès très rapide des années 90, c'est quand on a pu
enfin mettre toutes ces techniques ensembles sur une seule puce). Pour
essayer d'avoir plusieurs instructions à exécuter, il va le faire dans un
ordre différent de celui du flux (exécution dans le désordre, OOO).
Parce qu'il y a trop peu de registres, il détecte les usage indépendant
et utilise des registres physiques différents (renommage de registres,
pour les 16 registres logiques d'un x86-64, il y en a physiquement plus
d'une centaine). Parce que pour trouver quelque chose à faire, il faut
regarder au delà des sauts, un processeur va prédire quels sauts seront
pris (prédiction) et commencer à exécuter ce qui suit (quite à jeter les
résultats si le choix n'est pas bon, les perfs, ça coûte en puissance).
Pour alimenter tout ça, il faut un flux d'instructions. Les caches ne se
contente plus depuis longtemps de garder ce qui a été utilisé il n'y a
pas longtemps, ils essayent de deviner ce qui va l'être et de le charger
à l'avance. Tout ça fait que les perfs, c'est très variable, très
dépendant du contexte précis, très difficile à prédire, et très
révélateur des données effectivement traitées si on est prêt à payer le
coût de l'analyse. D'autres choses qui sont révélatrices à qui veut bien
payer ce coût, c'est la puissance (en Watt) consommée et les rayonnements
électro-magnétiques émis.

- à ma connaissance, ceux qui sont réellement sensibles à ces problèmes
de sécurité font leur propre hard.

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Jean-Marc Bourguet
Antoine Leca writes:

J'ai l'impression que à peine une génération de processeur plus tard, le
processeur ne faisait plus cas des dites indications (soit pour des
question de taille de pipeline, de sécurité, de sous-optimisation ou
autre) ; en tous cas, ce truc a quasiment disparu des manuels de
l'architecture IA-32, et le fait est que plus personne ne s'en préoccupe.



L'état de l'art dans les prédicteurs de saut fait que ces indications ne
servent plus guère que comme source d'initialisation du prédicteur. Les
annotations servent aussi au chaîne de compilation pour réordonner le code
pour mieux profiter des caches.

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Manuel P
Le Mon, 16 Mar 2015 10:05:09 +0000, Marc Espie a écrit :

Note: non, bien sur, c'est pas expres. Mais optimiser, c'est surtout
comprendre mieux ce que fait un code, et mieux on comprend plus on est
a meme de reagir en consequence...



Oui, en fait on est d'accord : meme si c'est pas fait expres pour
embeter, le resultat est le meme...

Apres, de toutes facons, un fabricant de processeur est deja en train
de faire des trucs qui vont limiter ce genre de choses.



Tu penses à un truc en particulier ?


[...]
Principe general, pas besoin de penser a un truc en particulier.



Oui, je vois bien de quel genre de principe on parle, c'est juste que
comme tu avais dit "un fabricant" je me demandais si c'etait un en
particulier, qui faisait des trucs qui allaient rendre la situation
encore plus complexe, ou si c'etait un "un" general.

Manuel.
Avatar
Manuel P
Le Mon, 16 Mar 2015 09:13:09 +0100, Antoine Leca a écrit :

Le 14/03/2015 15:07, Samuel DEVULDER écrivit :
En effet l'expression ternaire (sur des entiers)

(a ? b : c) (1)

qui s’exécute en temps variable peut être remplacée par l'expression en
temps constant:

(b & (a|-a)) | (c & ~(a|-a)) (2)



Comme disait Marc, on remplace du code clair par l'IOCCC.




Vu le contexte, il me semble que Sam suggerait que ca soit fait en interne
par le compilo, pas dans le code source, du coup si j'interprete bien son
message, on est tous les trois d'accord.

Les avantages de ce genre de solutions sont ÀMHA nombreux :
- les compilateurs sont plus à même de pouvoir suivre de près les
améliorations techniques des processeurs ;
- le code à protéger n'a pas besoin d'être modifié, il suffit juste de
recompiler : c'est l'idée force du langage C, et probablement la
meilleure raison qui fait que l'on s'en sert encore aujourd'hui ;
- on peut protéger des attaques sur le temps d'exécution, mais la même
idée fonctionne aussi pour les attaques sur la consommation électrique
ou la production électro-magnétique ;
- on arrête la course aux armements entre les optimisations du
compilateur et le code qui cherche à ne PAS en profiter, qui est en fait
le problème de base que nous commentons tous depuis le début ;
- les bogues introduits par la protection sont circonscrits au
compilateur, et donc en partie indépendants du code à protéger (par
contre cela donne plus d'importance aux effets des « 0-day » et autres
portes secrètes dans les compilateurs.)



Plus j'y pense, plus je trouve qu'effectivement ca serait la solution ideale.

En plus ca me parait pas tire par les cheveux : dans un sens c'est deja le
boulot du compilo de choisir, parmi differentes facons de traduire le C en
assembleur (en respectant la semantique definie par la norme), une qui soit
"optimale" suivant certains criteres (souvent le temps d'execution, parfois un
melange du temps d'execution et de la taille du code genere). Il s'agirait
juste de pouvoir lui demander de faire le meme genre de transformation, mais
en respectant un nouveau critere : pas de branches dependant de donnes
secretes.

Si j'avais la moindre competence sur le sujet *et* du temps libre, je me
lancerais la-dedans immediatement. A la place, je vais peut-etre essayer de
convaincre des gens qui ont les competences.

Manuel.

PS : Il y a juste un point ou je serais moins optimiste que toi :

- on peut protéger des attaques sur le temps d'exécution, mais la même
idée fonctionne aussi pour les attaques sur la consommation électrique
ou la production électro-magnétique ;



Pour le coup, si l'attaquant peut obtenir des traces suffisament precises, on
entre dans un tout autre domaine, ou chaque instruction fuit un peu
d'information sur les donnees sur lequelles elle s'execute, donc juste avoir
un flux d'instructions (et d'acces memoire) constant n'est plus du tout
suffisant. On doit utiliser des contre-mesures autres, essentiellement du
blinding.
Avatar
Jean-Marc Bourguet
Manuel Pégourié-Gonnard writes:

Bonjour,

Je m'interroge sur certaines des techniques proposées dans [1]. Un p eu
de contexte : il s'agit d'éviter les fuites d'information par mesure du
temps d'exécution dans du code cryptographique, et ce qui me pose
particulièrement question est le fait de n'utiliser que des opé rations
bit à bit plutôt que les opérateurs '==' et '!=' qua nd ils seraient
naturels. (Je n'ai par contre bien sûr aucun doute concernant le fait
d'éviter les sorties de boucles dépendant des données comm e dans
l'exemple initial de memcmp().)

[1]: https://cryptocoding.net/index.php/Coding_rules#Solution



Lié:

http://blog.erratasec.com/2015/03/x86-is-high-level-language.html

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
1 2