Reversing random stealer
Intro
Ça faisait un moment que je n’avais pas vraiment pris le temps de reverse un “vrai” malware from scratch.
Pour éviter de rouiller (et parce que c’est quand même fun), je me suis lancé un petit défi simple : prendre un malware totalement au hasard et voir jusqu’où je pouvais aller dans l’analyse.
Direction MalShare, clic au hasard, téléchargement d’un binaire sans trop savoir ce qui m’attendait. Spoiler : je ne suis pas tombé sur un simple crack foireux, mais sur un stealer assez motivé, avec du vol de mots de passe, des tokens Discord, du chiffrement, et toute la panoplie classique.
Fingerprints
Premières données :
1 | Fichier: malware |
Au moment ou cet article est rédigé, ces premieres infos sont déjà référencés (VT, etc).
Cependant, les IOC (notamment les IP contactées), ne correspondent pas totalement à mes découvertes 👀
Premier contact
On lance son tool favori (le meilleur :binja_love: ). Et là :
.text → 200 KB (le code exécutable, normal)
.data → 50 KB (données, ok)
.rsrc → 539 KB WTF?!
539 KB de ressources… Dans un exe console de 1.5 MB… mouais.
Pour rappel, .rsrc c’est censé contenir des trucs légitimes : icônes, dialogues Windows, strings d’interface, ce genre de choses. Typiquement, cela représente quelques KB, rarement plus de 100 KB (hors usines à gaz :D).
Là, on a un demi-méga de données. Et quand on regarde de plus près :pas d’icône structurée, pas de dialogues, pas de strings multilingues… Juste un gros blob binaire avec une entropie bien elevé (7.8/8.0).
On soupsçone un blob “chiffré”.
Strings & Ciphers
Première étape classique : regarder les strings du binaire, on tombe rapidement sur des choses du type :
Jackpot !
1 | "XOR cipher reversed" |
Pas très discret …
On remarque également pas mal de chaînes faisant référence à de l’anti debug, ainsi que les classiques malware (AMSI patch, sandbox detection, etc). C’est assez verbeux, avec des notions de debug log, ils ont du oublier de les enlever…
Bref, avec les xref, on identifie assez facilement la méthode qui va “déchiffrer” quelquechose :
Cette fonction est assez conséquente, plusieurs appels de memcpy/malloc, pas mal de boucles, suggérant qu’il s’agit bien d’une fonction de “déchiffrement”.
En vérifiant où elle est appelée, on identifie une autre fonction assez lourde qui, hasard de dingue, va chercher des data dans les ressources avant d’appeler le déchiffrement…
En résumé et en nettoyant tous les appels pour les patchings AMSI, ETW et compagnie, ça donne un truc comme ça :
1 | ... |
Clairement, ça crawl les ressources pour en trouver une en particulier, avec des checks sur les tailles de certains champs etc, puis ça déchiffre et exécute, classique.
Déchiffrement
Maintenant, il s’agit de récupérer ce payload, et de le déchiffrer pour comprendre le comportement du malware.
D’après les strings identifiées, ça serait un algo en 3 étapes :
- Transposition cipher reversed
- Substitution cipher reversed
- XOR cipher reversed
Extraction du payload
Dans un premier temps, on tente d’extraire le paylaod. Le dropper va aller chercher dans les ressources du programme (0xa).
A l’aide de la chaine de debug et des checks effectués dans le code, on peut rapidement comprendre la structure :
1 | "Header parsed - headerSize: %d, blockSize: %d, keySize: %d, encryptedSize: %d" |
On a aussi un check fait sur les ressources fetchées :
1 | 004062fd if (_Size u<= 0x31) |
D’après la chaine de debug on en déduit donc qu’un header de la forme suivante est présent:
headerSize: entre 8 et 15 bytes (vu dans l’assembleur)blockSize: taille des blocs pour la transposition (probablement 16)keySize: taille de la clé (probablement 32)
La suite semble être clef + data.
On recherche dans la section ressource et on tombe directement sur un blob qui colle plutôt bien (et juste après les structs classiques de ressources) :
On observe par la suite qu’il créer des sorts de databuffer qu’on pourrait définir ainsi :
1 | struct DataBuffer |
On va ensuite suivre les 3 étapes de déchiffrement
Transposition
Ici c’est assez simple, on peut voir qu’il prends les données par blocs de 16 et qu’il les inverse dans un gros buffer, bloc par bloc.
En gros il rempli un nouveau buffer par groupe de 16bytes en inversant l’ordre, par exemple :
1 | Input: [00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F] [10 11 12 ...] |
Jusqu’ici tout va bien.
Substitution
Là c’est un peu plus vicieux. On a affaire à une sorte de substitution dynamique avec une table de correspondance crée à la volée via une clef.
Concrétement, pour chaque octet à l’index i :
- Calculer
base = (i + key[i % keySize]) % 256 - Construire une table de substitution :
table[j] = (base + j) % 256pour j=0..255 - Trouver l’inverse : chercher
jtel quetable[j] == byte_chiffré - Remplacer le byte par
j
C’est plus une obfuscation qu’un chiffrement. Un peu pénible à comprendre, en fait la table de substitution change à chaque itération. En vrai, les messages de débug présents m’ont grandement aidé à mieux comprendre les algos :D
As simple as a XOR
La denière étape consiste simplement en un xor classique en itérant sur le clef de 32bytes.
Let’s decrypt
Si on refait ces 3 étapes, avec le payload extrait, ça donne ça :
1 | def reverse_transposition(data, blockSize): |
On obtient bien un autre binaire !
1 | ghozt@maze:~/research/malware$ file output.exe |
.NET stealer
Avec ilspy on récupère le code C# :
1 | ghozt@maze:~/research/malware$ ilspycmd output.exe -o ./decompiled_dotnet/ |
On remarque vite que les strings sont obfusquées. Cependant, nous pouvons analyser le comportement général du stealer. Vol de token discord, déchiffrement du password manager de chrome via DPAPI, …
Pour les chaines, on découvre que tout est chiffré en AES, avec une clef un peu obfusquée :
1 | public static class Strings |
Il y a 190 strings chiffrées dans le malware. Toutes utilisent AES-CBC, mais avec un twist : le ciphertext est reversed avant le déchiffrement wouhou.
Les clés AES elles-mêmes sont encodées avec XOR + Base64.
En reproduisant l’algorithme, on récupére la clef et l’IV :
Les clés AES sont maintenant correctement déchiffrées :
1 | AES Key: OFhBENNkSFZaYyg2w6yQi5Mrqn7ypiPXDrim64w/FM0= |
Et on peut ensuite déchiffrer toutes les strings !
Cibles de Vol de Données
Navigateurs :
1 | Login Data → Credentials Chrome/Edge/Brave |
Messagerie :
1 | discord\Local Storage\leveldb\ → Tokens Discord |
Crypto Wallets :
1 | wallet.dat → Bitcoin Core, Litecoin |
VPN :
1 | NordVPN\user.config → Config NordVPN |
Gaming & FTP :
1 | Steam\loginusers.vdf → Comptes Steam |
Et le gros morceau :
1 | net.tcp://185.172.128.70:3808 |
Nous pouvons identifier le C2 sur lequel toutes les données sont transmises, le stealer envoie toutes les données volées à cette adresse en utilisant le protocole WCF (Windows Communication Foundation).
Reconnaissance Système
Le malware collecte aussi plein d’infos système :
1 | SELECT * FROM Win32_Processor -- CPU |
Et il détecte les produits de sécurité :
1 | ROOT\SecurityCenter\AntivirusProduct |
Vue d’Ensemble
Maintenant qu’on a toutes les strings, on comprend le workflow complet :
Phase 1 : Reconnaissance
- Collecte des infos système (CPU, GPU, RAM, disques, OS)
- Détection des produits de sécurité (AV, firewall)
- Détection VM/sandbox (QEMU, RDP)
- Liste des processus en cours
- Liste des logiciels installés
- Géolocalisation via
https://api.ip.sb/ip
Phase 2 : Vol de Données
- Navigateurs : credentials, cookies, autofill, cartes de crédit
- Discord : tokens, extraction via LevelDB
- Telegram : copie du dossier
tdata - Wallets crypto : recherche de
wallet.dat, extensions browser - VPN : configs NordVPN, ProtonVPN, OpenVPN
- FTP : FileZilla credentials
- Gaming : Steam credentials
Phase 3 : Exfiltration
- Compression des données volées
- Connexion au C2 :
net.tcp://185.172.128.70:3808 - Envoi via WCF (protocole binaire)
Persistance
On remarque qu’une chaine chiffrée est en base64, qui une fois décodée correspond à un autre exécutable .NET, qui est écrtit dans le dossier de startup :
1 | using System; |
Il s’agit probablement d’une backdoor dormante qui permettra dans un second temps de s’attacher à ce processus.
IoCs Summary
1 | Dropper: |