In a malware galaxy far away: Malwareclusters visualiseren met ssdeep en plotly
This blog post is also available in English: In a malware galaxy far away: Visualizing malware clusters with ssdeep and plotly
tldr; Het groeperen van malware op basis van gelijkenis in gedrag of structuur is een bekende techniek vor threat intelligence- of malwareanalisten. In deze blogpost wordt beschreven hoe fuzzy hashing via ssdeep in combinatie met Multi-Dimensional Scaling (MDS) kan worden ingezet om malware gebruikt door (in dit geval) threat actors te clusteren en vooral ook visualiseren. En dat ziet er zo uit (klik op de afbeelding om naar het live voorbeeld te gaan):
Bij traditionele hashes zoals SHA256 of MD5 verandert de hele hash zodra je ook maar één byte in een bestand aanpast. Superhandig als je precies wilt weten of een bestand 1-op-1 overeenkomt, maar hartstikke onbruikbaar als je zoekt naar bestanden die nét iets van elkaar verschillen. Denk aan hercompilaties, wat extra obfuscatie of een kleine tweak in de code.
En precies daarom wordt in malware-analyse vaak fuzzy hashing ingezet. Één van de bekendste tools daarvoor is ssdeep. In plaats van een vaste hash geeft ssdeep een soort vingerafdruk gebaseerd op de context en structuur van het bestand. Daarmee kun je niet alleen zeggen is dit hetzelfde bestand?, maar ook hoeveel lijkt dit op iets wat we al eerder hebben gezien? — met een score tussen de 0 en 100. Veel nuttiger als je op zoek bent naar varianten in plaats van exacte kopieën.
Pak bijvoorbeeld de volgende file met de volgende hashes:
- MD5:
583ec7348c3817d757ee5844f665f293
- SHA256:
ff457cc4b90c5c28ce85107b386fddfb4a7dd42c9c401a9598a46ce36b43a5b5
- ssdeep:
12:wiVUXxCWEUbNI9kxwAUIySKxWHUWyFULPCUNoeVUb6UJxRI4qKAUKJeUwx7+cAUd:rmv5bNIqcCKxzeHNU3Z1xlmv5goHA
Op basis van deze fuzzy hash kun je berekenen wat de overeenkomsten in percentage is. Zowel deze file als deze file worden allebei getagd als een Mirai shell script dropper op MalwareBazaar. Of ze daadwerkelijk vergelijkbaar zijn, kun je berekenen met de volgende functie:
Voorbeeld:
# Gebruik: ssdeep.compare(hash1, hash2)
ssdeep.compare(
"12:wiVUXxCWEUbNI9kxwAUIySKxWHUWyFULPCUNoeVUb6UJxRI4qKAUKJeUwx7+cAUd:rmv5bNIqcCKxzeHNU3Z1xlmv5goHA",
"12:eiV+mxCWE+QNI9kxwA+5ySKxWH+exRI4qKA+Svq+UPC+2oeV+DJe+vx7+cA+xy9T:h7mNIqYKxS1x6wsZa8v"
)
De functie ssdeep.compare
geeft je een score tussen de 0 (totaal verschillend) en 100 (praktisch identiek). Volgens bovenstaand voorbeeld komen de files voor 55% overeen. Het zijn beiden shell scripts, dus dat is makkelijk te checken. Allebei de files doen flink wat wget
en chmod
; deze score lijkt dus prima. Hiermee kun je dus niet alleen exacte duplicates spotten, maar ook sterk verwante varianten van dezelfde malwarefamilie. Klinkt ideaal, maar niet helemaal op de volgende punten:
- het is gevoelig voor packers of encryptie van de payload
- de volgorde van de inhoud speelt mee in de score, net als verschillen in structuur
Toch kan het juist in dit soort gevallen — bijvoorbeeld als je mogelijke malwarevarianten van dezelfde threat actor probeert te groeperen — waardevolle inzichten opleveren. In de rest van deze post laat ik zien hoe je met getagde APT-samples een visualisatie maakt op basis van hun ssdeep-hash én MDS-clustering.
ssdeepinformatie van samples ophalen uit MalwareBazaar
De eerste stap is het verzamelen van relevante malware om te clusteren. In dit voorbeeld heb ik twee APT-groepen (ik zag twee beren..) gepakt en hiervan de relevante malware opgezocht op Malpedia en deze in apt_groups gedefinieerd. Vervolgens haal ik per familie alle samples binnen die op MalwareBazaar met die namen getagd zijn en trek ik daar de ssdeep-hashes uit:
apt_groups = {
"apt28": ["xagent", "komplex", ...],
"apt29": ["beatdrop", "boombox", ...]
}
for apt, tools in apt_groups.items():
for tool in tools:
data = get_malware_bazaar(apt, tool)
De get_malware_bazaar()
functie maakt gebruik van de MalwareBazaar API om bestanden op te halen op basis van een tag. De opgehaalde gegevens worden opgeslagen in een simpele sqlite database:
class Signature(db.Model):
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255))
filetype = db.Column(db.String(50))
signature_name = db.Column(db.String(255))
actor = db.Column(db.String(50))
sha256 = db.Column(db.String(64), unique=True)
ssdeep = db.Column(db.String(255))
date = db.Column(db.DateTime)
def get_malware_bazaar(actor, signature_name):
headers = {'API-KEY': [API_KEY_HERE]}
data = {'query': 'get_taginfo', 'tag': signature_name}
response = requests.post('https://mb-api.abuse.ch/api/v1/', headers=headers, data=data)
list_data = response.json()
if 'data' not in list_data:
return
with app.app_context():
for item in list_data['data']:
if not Signature.query.filter_by(sha256=item['sha256_hash']).first():
signature = Signature(
filename=item['file_name'],
filetype=item['file_type'],
actor=actor,
signature_name=signature_name,
sha256=item['sha256_hash'],
ssdeep=item['ssdeep'],
date=datetime.strptime(item['first_seen'], '%Y-%m-%d %H:%M:%S')
)
db.session.add(signature)
db.session.commit()
Dit script checkt iedere zoveel tijd of er nieuwe samples zijn die getagd zijn met 1 van deze gedefinieerde namen. Eindstand: een sqlite-database met daarin een rij met o.a. de SHA256-hash (om dubbelingen te checken), de ssdeep hash en door welke threat actor deze gebruikt is op basis van Malpedia.
Met deze ssdeep hashes kun je vervolgens de afstanden berekenen en in een afstandsmatrix zetten. Wanneer je alle hashes pakt, daar de ssdeep.compare-functie op loslaat en daarvan alles dicht bij elkaar clustert op basis van afstand óf bijvoorbeeld een bepaalde drempelwaarde (in dit geval, boven de 75%), dan krijg je de volge snippet om dat te doen. Hij ziet dus >75% overeenkomst als één cluster.
num_files = len(files)
distance_matrix = np.zeros((num_files, num_files))
clusters = []
for i in range(num_files):
cluster_found = False
for cluster in clusters:
similarity = ssdeep.compare(hashes[i], hashes[cluster[0]])
if similarity >= 75:
cluster.append(i)
cluster_found = True
break
if not cluster_found:
clusters.append([i])
Vervolgens wordt de afstandsmatrix opgebouwd op basis van deze compare-output, waarbij extra gewicht wordt gegeven aan een gelijke threat actor groepsnaam of de naam van de signature van de sample. Ik heb hierbij een beetje geëxperimenteerd met de afstanden. Je kunt in principe de afstand as-is pakken, maar in het voorbeeld hieronder heb ik ze enigszins aangepast naar een aantal categorieën:
- 0 (geen afstand bij similarity van >75%)
- 0.25 (files van dezelfde threat actor of met dezelfde signature)
- similarity zelf (als het niet 1 van de andere opties is, dan de berekende similarity)
- 1 (maximale afstand)
for i in range(num_files):
for j in range(num_files):
if i != j:
similarity = ssdeep.compare(hashes[i], hashes[j])
if similarity >= 75:
distance_matrix[i, j] = 0
elif similarity > 0:
distance_matrix[i, j] = similarity
elif files[i] == files[j] or apts[i] == apts[j]:
distance_matrix[i, j] = 0.25
else:
distance_matrix[i, j] = 1
else:
distance_matrix[i, j] = 0
Vertalen van de afstandsmatrix naar punten met MDS
Zodra voor elke sample een afstand is berekend ten opzichte van alle andere samples, ontstaat er een afstandsmatrix (ook wel distance matrix). Deze matrix is alleen moeilijk visueel interpreteerbaar in de vorm waarin deze nu is opgeslagen, zeker als het om tientallen of honderden samples gaat.
Daarom wordt Multi-Dimensional Scaling (MDS) ingezet: een techniek om de onderlinge afstanden tussen objecten in een lagere dimensie te projecteren (bijvoorbeeld 2D of 3D), waarbij de verhoudingen behouden blijven. Het resultaat is een visualisatie waarin samples die op elkaar lijken dicht bij elkaar worden weergegeven en niet-verwante samples verder uit elkaar liggen. Samples met gelijke ssdeep hashes worden weergegeven als 1 groter cluster: de diameter van het cluster is dan ook gelijk aan het aantal samples met diezelfde ssdeep hash.
from sklearn.manifold import MDS
mds = MDS(n_components=2, dissimilarity="precomputed", random_state=42)
mds_results = mds.fit_transform(distance_matrix)
# Prepare data for Plotly
plot_data = {
"x": [],
"y": [],
"text": [],
"size": []
}
for cluster in clusters:
if len(cluster) > 1:
x = mds_results[cluster, 0].mean()
y = mds_results[cluster, 1].mean()
text = files[cluster[0]].upper()
size = len(cluster) * 10 # Make the size larger for clusters
plot_data["x"].append(x)
plot_data["y"].append(y)
plot_data["text"].append(text)
plot_data["size"].append(size)
Visualisatie met Plotly
Nu het echt toffe gedeelte: het visualiseren van deze afstandsmatrix met Plotly. Plotly is een open-source visualisatielibrary in Python (en ook beschikbaar in andere talen zoals JavaScript en R), die interactieve grafieken en dashboards genereert. In tegenstelling tot statische visualisaties zoals bij matplotlib, maakt Plotly gebruik van webtechnologie (HTML, JavaScript, D3.js), waardoor de grafieken inzoom-, hover- en klikfunctionaliteit hebben. Ook kun je hiermee clusters visualiseren met eigen kleuren en labels, zoals met de code hieronder:
def cluster_plotly(plot_data, clusters):
fig = go.Figure()
space_colors = ['#015a46', '#034237', ...]
num_points = len(plot_data["x"])
for idx, cluster in enumerate(clusters):
if isinstance(cluster, list):
valid_indices = [i for i in cluster if 0 <= i < num_points]
x = [plot_data["x"][i] for i in valid_indices]
y = [plot_data["y"][i] for i in valid_indices]
text = [plot_data["text"][i] for i in valid_indices]
size = [plot_data["size"][i] for i in valid_indices]
else:
if 0 <= cluster < num_points:
x = [plot_data["x"][cluster]]
y = [plot_data["y"][cluster]]
text = [plot_data["text"][cluster]]
size = [plot_data["size"][cluster]]
else:
continue
print(f"Cluster {idx}: {cluster}")
Resultaat
Door ssdeep hashes op deze manier te gebruiken voor clustering en visualisatie, kun je best interessante patronen spotten — denk aan nieuwe malwarefamilies of trends in gedrag over tijd. Nu haalt de applicatie data op uit een vrij beperkte set aan sample en dankzij caching van de te plotten coördinaten laadt de visualisatie ook prima snel. Maar zodra je werkt met een megaset aan samples, is deze manier qua space & time (ja hoor, throwback naar Datastructuren) niet optimaal. Bovendien geeft enkel ssdeep geen compleet beeld, zoals eerder al benoemd. In zo’n geval kun je beter meerdere technieken combineren — denk aan YARA-hits of hergebruikte IoCs — om een vollediger plaatje te krijgen.