mardi 10 novembre 2015

CTF Writeup : Hack.lu 2015 - checkcheckcheck

Inputs

For this problem, we are given the source code of an authenticated service. We have to login by providing a password, which is then concatenated to a salt, hashed with SHA256, and truncated to 48 bits. The server checks if the hash is correct, using a timing-proof algorithm (the two values are XORed, then all the bits of the result are combined with bitwise OR.

Countrary to usual attack scenarios, the actual salt is hidden from us. However, if the login fails, a stale debugging statement informs us of the difference between the stored hash and the computed hash (though because we do not know the salt, we cannot a priori make use of this information, as we do not know how to compute the hash.)

Vulnerability

The XOR implementation used for the timing-safe comparison is destructive (the first operand is replaced with the result of the XOR.) Furthermore, the order of arguments is reversed when testing the password: instead of overwriting the temporary computed hash, it overwrites the stored hash, which persists across login attempts from the same session (but not across connections - the connection handler loads the stored hash from a file before starting its main loop.)

Formally, a connection starts with a stored hash S=S0, and for every new submitted password pi, the stored hash gets replaced by a new value Si=Si1H(pi)

Solution

The first step is to recover the stored hash. If we submit the same password p twice, we get to observe S2=S1H(p)=S0H(p)H(p)=S0

Once the original hash is observed, the problem reduces to finding a sequence of n passwords pi such that Sn=0

Because XOR is a linear operation, we know that we can solve this problem for any S0 by finding an orthogonal basis for our vector space. That is, as the hash is 48 bits long, we need to find, for every bit position, a sequence of passwords that hashes to all zeros except in this specific position. Once this is determined, we can get to any hash value by combining the basis vectors for the bits we want.

There is an easier way, though. It is enough for us to find a basis such as the i-th vector has a 1 in the i-th bit position, and a 0 in all the previous bit positions. This corresponds to reducing a matrix to upper triangular form. Once we have this basis, we can apply (or not) each of the basis vectors in turn, dynamically adapting to the new result.

We need 48 basis vectors to spawn our 48-dimension output hash. We will use more random vectors, to lower the probability that our random vector set would accidentally be linearily dependant (and thus not a basis)

The solution is thus as such:

  • Connect to the service 100 times, submitting different passwords like dummy00, dummy01, dummy02, etc...
  • For each password, observe the hash difference, and deduce the actual computed hash (because the original stored hash is known)
  • Generate a matrix of bits containing the hashes outputs. This matrix has 100 lines (one per hash) and 48 columns (one per hash bit)
  • Adjunct an identity matrix to this one, then make the left part into an upper triangular matrix, by doing the following for each row:
    • For every bit before the i-th, check if it is 0. If not, xor with the i-th matrix row to make it 0.
    • Ensure that the row has a 1 in its i-th position (if not, drop the row or exchange it with a lower one)
  • Select all the rows corresponding to the 1 bits of the target hash, and xor them together. The 100 rightmost bits (where the identity matrix used to live) indicate which passwords from the random set, xored together, generate the target hash
It only remains to connect to the service, and automatically submit the set of all chosen passwords.

jeudi 14 mai 2015

Un conte d'horreur pour sysadmins

Journal d'un sysadmin bénévole - mercredi 13 mai, 20h

Je suis enfin rentré chez moi après une fructueuse journée de travail. Je m'installe dans mon canapé avec ma tablette, lance mon client ssh, attache mon tmux, dans lequel tourne mon client IRC.

Immédiatement, un détail m'interpelle, un enigmatique message dans le log de mes messages privés: "c'est quoi ce bordel ?"

Je lance /map dans mon client. Urgh. Sur la vingtaine de serveurs, réels ou virtuels, de notre petit réseau IRC privé, seule une poignée subsiste. Le site faisait tourner les services est hors circuit. Les admins responsables sont injoignables. Je peste intérieurement. La dernière fois que c'était arrivé, j'avais hurlé pour qu'un backup soit prêt a prendre le relais, ça n'a pas été fait. Tant pis.

En attendant, et ne sachant pas combien de temps durera la panne, je décide de préparer mon site a prendre le relais; je lance la compilation d'Atheme, puis joue un peu avec ma tablette en attendant.

mercredi 13 mai, 21h

Je retourne sur mon client SSH pour vérifier la progression de la compilation. Tiens, il s'est déconnecté. Je le relance. Il refuse de se connecter... plus de réseau ? je visite un site web au hasard, ça fonctionne. Bizarre.

Je vais chercher mon laptop sous windows. J'ouvre un client SSH. Le verdict est sans appel:

Cannot open session: PTY allocation failed.

Allons bon.. voila qui est mauvais. Je vais chercher mon eeePC sous gentoo, et lance la connexion ssh. Le shell se lance, effectivement sans PTY, donc sans prompt, sans couleurs, sans readline, rien. MAIS, il fonctionne. Qu'a-il donc pu se passer ? J'essaie d'attacher mon ancienne session.

open terminal failed: not a terminal

Evidemment. Un horrible pressentiment s'empare de moi...

uptime
21:14:32 up 1 min, 1 user, load average: 0.01, 0.01, 0.01

Mon monde s'écroule. Cette machine avait passé 1000 jours d'uptime. C'est déja un miracle qu'elle aie redémarré proprement. Elle tourne encore un kernel 2.6 openvz, un vieil openrc, un vieil udev...

su: must be run from a terminal

Voila autre chose. Impossible d'inspecter la situation sans un accès root. Je pars a la recherche d'une 3e machine, celle qui contient une clef ssh de secours, qui me permettra d'accéder directement au compte root.

La situation m'est familière; j'ai eu un ennui similaire sur mon eeePC, pendant une mise à jour d'openrc. Pour une raison bizarre, le script de boot responsable de monter le devpts sur /dev/pts se déclenche AVANT le montage d'un devtmpfs sur /dev, avec pour résultat un /dev/pts vide. En attendant, je remonte le /dev/pts à la main. Mon client SSH sur la tablette refonctionne, ouf.

Pour assainir le système, je procède enfin aux mises a jour que je procrastine depuis longtemps: udev (cauchemar), openrc, et le kernel. Dépendances et blocages se succèdent dans tous les sens.

jeudi 14 mai, 00h

Alors que mon nouveau kernel compile, je perds de nouveau la connexion. Petit instant d'angoisse; quelques minutes après, le serveur est de nouveau atteignable, mais la compilation a été interrompue. Je relance le processus, une fois, puis deux, puis trois, même problème. Quelque chose ne tourne pas rond.

uptime
00:02:14 up 1 min, 1 user, load average: 0.01, 0.01, 0.01

Saloperie. Qu'est-ce qui a bien pu se passer ? je fouille les logs. Rien de suspect. Puis, une intuition:

ipmitool sel list
10 | 05/14/2015 |  00:01:12 | Fan #0x54  | Upper Critical going high
10 | 05/13/2015 |  23:58:55 | Fan #0x54  | Upper Critical going high
10 | 05/13/2015 |  23:56:49 | Fan #0x54  | Upper Critical going high

D'accord. Un ventilateur mort, la machine surchauffe pendant les intensives compilations requises par les mises a jour.. Pas mal de choses commencent à s'expliquer.

Mon kernel est finalement compilé, mon udev est a jour. Je monte /boot, lance le make install, reconstruit mon initramfs avec genkernel, vérifie la configuration de grub, tout a l'air correct... Je me prépare a redémarrer. Mais un doute me taraude. Udev a été mis a jour, openrc également, j'ai un mauvais pressentiment. Et si l'interface réseau ne repart pas correctement ?

Cette machine n'a pas de KVM, on travaille sans filet. Si l'interface réseau ne repart pas, je suis bon pour attendre vendredi pour aller la dépanner physiquement. Pendant ce temps, tous les gens qui dépendent de cette machine (les amis d'IRC, et quelques associations) en pâtiront. Je décide de me préparer au pire.

Par chance, cette machine est cohébergée avec un deuxième serveur, lui-même parfaitement opérationnel. Un cable réseau croisé relie les deux systèmes, je décide de l'exploiter, et installe le script suivant dans /etc/rc.local/backdoor.start:

IFACE=`ip link |grep -B1 xx.xx.xx.xx.xx.xx |head -n1 |cut -d: -f2`
ip link set $IFACE up
ip addr add 10.10.10.10/24 dev $IFACE
nohup nc -l -p 1337 --continuous -e /bin/sh -s 10.10.10.10 &

Oui, c'est un risque sur le plan sécurité; mais l'interface est physiquement isolée du monde extérieur, personne n'est actuellement connecté sur l'autre machine, et nous n'utilisons pas ce subnet.

Je lance le reboot et croise les doigts.

jeudi 14 mai, 02h

Quelque chose n'a pas fonctionné. La machine ne répond ni sur son addresse publique, ni sur l'addresse interne habituelle. J'essaie le ping sur 10.10.10.10. ça répond.

J'ai été bien inspiré d'installer cette petite backdoor. Un nc plus tard, et je suis de nouveau aux commandes de la machine, encore une fois sans PTY, mais l'essentiel est la.

J'inspecte l'état de la machine. OpenSSH n'a pas démarré, et les deux bridges sont éteints. Sans cette backdoor improvisée, la machine était perdue. J'essaie de démarrer une des deux interfaces:

error: cannot create bridge

Je revérifie la configuration réseau, elle est correcte. Je met a jour bridge-utils. J'essaie de créer le bridge a la main, rien a faire... la commande échoue silencieusement. Mais enfin, que se passe-il ?

Soudain, l'inspiration me prend:

lsmod

Rien. Nada, zilch, zip, que d'alle. Pas un seul module chargé. Je réalise mon erreur avec horreur.

Je n'ai pas lancé le make modules_install du nouveau kernel.

Par chance, les drivers essentiels (disque et réseau) étaient en dur, tout comme le device-mapper. Un make modules_install plus loin, le module bridge se charge correctement; de tests en tests, tout a l'air de fonctionner. Je relance la machine, elle démarre complètement correctement... le plus gros incident est passé.

Il va maintenant falloir migrer tous les conteneurs d'openvz, qui n'est plus supporté...