| import gradio as gr |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import numpy as np |
| from PIL import Image |
| from torchvision import transforms |
| import pickle |
| import json |
| import math |
|
|
|
|
| |
| class MalwareClassifier(nn.Module): |
| def __init__(self, input_dim=2381): |
| super(MalwareClassifier, self).__init__() |
| self.network = nn.Sequential( |
| nn.Linear(input_dim, 512), |
| nn.BatchNorm1d(512), |
| nn.ReLU(), |
| nn.Dropout(0.3), |
| nn.Linear(512, 256), |
| nn.BatchNorm1d(256), |
| nn.ReLU(), |
| nn.Dropout(0.3), |
| nn.Linear(256, 128), |
| nn.ReLU(), |
| nn.Linear(128, 1), |
| nn.Sigmoid() |
| ) |
|
|
| def forward(self, x): |
| return self.network(x).squeeze(1) |
|
|
|
|
| |
| class PatchEmbedding(nn.Module): |
| def __init__(self, img_size, patch_size, in_channels, embed_dim): |
| super().__init__() |
| self.projection = nn.Conv2d( |
| in_channels, embed_dim, |
| kernel_size=patch_size, stride=patch_size |
| ) |
|
|
| def forward(self, x): |
| x = self.projection(x) |
| x = x.flatten(2) |
| x = x.transpose(1, 2) |
| return x |
|
|
|
|
| class MultiHeadSelfAttention(nn.Module): |
| def __init__(self, embed_dim, num_heads, dropout=0.0): |
| super().__init__() |
| self.num_heads = num_heads |
| self.head_dim = embed_dim // num_heads |
| self.scale = self.head_dim ** -0.5 |
| self.qkv = nn.Linear(embed_dim, embed_dim * 3) |
| self.proj = nn.Linear(embed_dim, embed_dim) |
| self.dropout = nn.Dropout(dropout) |
|
|
| def forward(self, x): |
| B, N, D = x.shape |
| qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim) |
| qkv = qkv.permute(2, 0, 3, 1, 4) |
| q, k, v = qkv[0], qkv[1], qkv[2] |
| attn = (q @ k.transpose(-2, -1)) * self.scale |
| attn = attn.softmax(dim=-1) |
| attn = self.dropout(attn) |
| x = (attn @ v).transpose(1, 2).reshape(B, N, D) |
| return self.proj(x) |
|
|
|
|
| class TransformerBlock(nn.Module): |
| def __init__(self, embed_dim, num_heads, mlp_dim, dropout=0.1): |
| super().__init__() |
| self.norm1 = nn.LayerNorm(embed_dim) |
| self.attn = MultiHeadSelfAttention(embed_dim, num_heads, dropout) |
| self.norm2 = nn.LayerNorm(embed_dim) |
| self.ffn = nn.Sequential( |
| nn.Linear(embed_dim, mlp_dim), |
| nn.GELU(), |
| nn.Dropout(dropout), |
| nn.Linear(mlp_dim, embed_dim), |
| nn.Dropout(dropout) |
| ) |
|
|
| def forward(self, x): |
| x = x + self.attn(self.norm1(x)) |
| x = x + self.ffn(self.norm2(x)) |
| return x |
|
|
|
|
| class VisionTransformer(nn.Module): |
| def __init__(self, img_size, patch_size, in_channels, num_classes, |
| embed_dim, depth, num_heads, mlp_dim, dropout=0.1): |
| super().__init__() |
| num_patches = (img_size // patch_size) ** 2 |
| self.patch_embed = PatchEmbedding(img_size, patch_size, in_channels, embed_dim) |
| self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) |
| self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim)) |
| self.dropout = nn.Dropout(dropout) |
| self.transformer = nn.Sequential(*[ |
| TransformerBlock(embed_dim, num_heads, mlp_dim, dropout) |
| for _ in range(depth) |
| ]) |
| self.norm = nn.LayerNorm(embed_dim) |
| self.head = nn.Linear(embed_dim, num_classes) |
|
|
| def forward(self, x): |
| B = x.shape[0] |
| x = self.patch_embed(x) |
| cls = self.cls_token.expand(B, -1, -1) |
| x = torch.cat([cls, x], dim=1) |
| x = x + self.pos_embed |
| x = self.dropout(x) |
| x = self.transformer(x) |
| x = self.norm(x[:, 0]) |
| return self.head(x) |
|
|
|
|
| |
| device = torch.device("cpu") |
|
|
| |
| mlp_model = MalwareClassifier(input_dim=2381) |
| mlp_model.load_state_dict(torch.load("malware_classifier.pth", map_location=device)) |
| mlp_model.eval() |
|
|
| with open("scaler.pkl", "rb") as f: |
| scaler = pickle.load(f) |
|
|
| |
| with open("vit_class_names.json", "r") as f: |
| class_names = json.load(f) |
|
|
| vit_model = VisionTransformer( |
| img_size=64, patch_size=8, in_channels=1, |
| num_classes=len(class_names), |
| embed_dim=256, depth=6, num_heads=8, mlp_dim=512, dropout=0.1 |
| ) |
| vit_model.load_state_dict(torch.load("vit_malware_final.pth", map_location=device)) |
| vit_model.eval() |
|
|
| |
| from ember.features import PEFeatureExtractor |
| extractor = PEFeatureExtractor(feature_version=2) |
|
|
| |
| vit_transforms = transforms.Compose([ |
| transforms.Grayscale(num_output_channels=1), |
| transforms.Resize((64, 64)), |
| transforms.ToTensor(), |
| transforms.Normalize([0.5], [0.5]) |
| ]) |
|
|
|
|
| |
| def bytes_to_image(file_bytes): |
| arr = np.frombuffer(file_bytes, dtype=np.uint8) |
| size = int(np.sqrt(len(arr))) |
| if size < 8: |
| return None |
| arr = arr[:size * size].reshape(size, size) |
| return Image.fromarray(arr, mode="L") |
|
|
|
|
| |
| def analyze_file(file): |
| if file is None: |
| return ( |
| "No file uploaded.", |
| "β", |
| "β", |
| "β", |
| None, |
| "" |
| ) |
|
|
| try: |
| with open(file, "rb") as f: |
| file_bytes = f.read() |
|
|
| |
| features = extractor.feature_vector(file_bytes) |
| features = np.array(features, dtype=np.float32).reshape(1, -1) |
| features = scaler.transform(features) |
|
|
| tensor = torch.tensor(features, dtype=torch.float32).to(device) |
| with torch.no_grad(): |
| mlp_prob = mlp_model(tensor).item() |
|
|
| mlp_verdict = "MALWARE" if mlp_prob >= 0.5 else "BENIGN" |
| mlp_confidence = f"{mlp_prob:.2%}" |
|
|
| |
| if mlp_prob < 0.5: |
| return ( |
| f"β
BENIGN", |
| mlp_confidence, |
| "Skipped β file classified as benign by Stage 1", |
| "β", |
| None, |
| ( |
| f"Stage 1 (MLP) malware probability: {mlp_prob:.2%}\n" |
| f"Below threshold of 50% β classified as benign.\n" |
| f"Stage 2 (ViT) was not run." |
| ) |
| ) |
|
|
| |
| img = bytes_to_image(file_bytes) |
| if img is None: |
| return ( |
| f"β οΈ MALWARE (Stage 2 failed)", |
| mlp_confidence, |
| "Could not convert file to image", |
| "β", |
| None, |
| f"Stage 1 probability: {mlp_prob:.2%}\nFile too small for ViT conversion." |
| ) |
|
|
| img_tensor = vit_transforms(img).unsqueeze(0).to(device) |
|
|
| with torch.no_grad(): |
| logits = vit_model(img_tensor) |
| probs = F.softmax(logits, dim=1).squeeze() |
| conf, idx = probs.max(0) |
|
|
| family = class_names[idx.item()] |
| vit_conf = f"{conf.item():.2%}" |
|
|
| |
| top5_vals, top5_idx = probs.topk(5) |
| top5_text = "\n".join([ |
| f" {i+1}. {class_names[i2.item()]:<22} {v.item():.2%}" |
| for i, (v, i2) in enumerate(zip(top5_vals, top5_idx)) |
| ]) |
|
|
| details = ( |
| f"Stage 1 β MLP (EMBER features)\n" |
| f" Malware probability : {mlp_prob:.2%}\n" |
| f" Verdict : MALWARE β passed to Stage 2\n\n" |
| f"Stage 2 β ViT (grayscale image)\n" |
| f" Image size : {img.size[0]}x{img.size[1]} px from {len(file_bytes):,} bytes\n" |
| f" Predicted family : {family}\n" |
| f" Confidence : {conf.item():.2%}\n\n" |
| f"Top 5 family predictions:\n{top5_text}" |
| ) |
|
|
| |
| img_display = img.resize((256, 256), Image.NEAREST) |
|
|
| return ( |
| f"π΄ MALWARE", |
| mlp_confidence, |
| family, |
| vit_conf, |
| img_display, |
| details |
| ) |
|
|
| except Exception as e: |
| return ( |
| "ERROR", |
| "β", |
| "β", |
| "β", |
| None, |
| f"Could not process file: {str(e)}" |
| ) |
|
|
|
|
| |
| with gr.Blocks(title="Malware Detection Pipeline") as demo: |
|
|
| gr.Markdown("# Two-Stage Malware Detection Pipeline") |
| gr.Markdown( |
| "Upload a Windows executable (`.exe` or `.dll`) and the pipeline will:\n\n" |
| "**Stage 1 β MLP:** Analyze 2381 static PE features to determine if the file is malware\n\n" |
| "**Stage 2 β ViT:** If malware is detected, convert the binary to a grayscale image " |
| "and classify it into one of 25 known malware families using a Vision Transformer\n\n" |
| "No file is executed at any point." |
| ) |
|
|
| with gr.Row(): |
| file_input = gr.File(label="Upload PE file (.exe or .dll)") |
|
|
| with gr.Row(): |
| analyze_btn = gr.Button("Analyze", variant="primary", size="lg") |
|
|
| gr.Markdown("### Stage 1 β MLP Malware Detection") |
| with gr.Row(): |
| verdict_out = gr.Textbox(label="Verdict", interactive=False) |
| mlp_conf_out = gr.Textbox(label="Malware probability", interactive=False) |
|
|
| gr.Markdown("### Stage 2 β ViT Family Classification") |
| with gr.Row(): |
| family_out = gr.Textbox(label="Malware family", interactive=False) |
| vit_conf_out = gr.Textbox(label="ViT confidence", interactive=False) |
|
|
| with gr.Row(): |
| image_out = gr.Image( |
| label="Grayscale image fed to ViT (what the transformer sees)", |
| type="pil" |
| ) |
|
|
| details_out = gr.Textbox(label="Full analysis details", interactive=False, lines=12) |
|
|
| analyze_btn.click( |
| fn=analyze_file, |
| inputs=file_input, |
| outputs=[verdict_out, mlp_conf_out, family_out, vit_conf_out, image_out, details_out] |
| ) |
|
|
| gr.Markdown( |
| "---\n" |
| "**Stage 1 β MLP:** Trained on EMBER 2018 Β· 800k samples Β· 95% accuracy Β· ROC-AUC 0.9878\n\n" |
| "**Stage 2 β ViT:** Trained on MalImg Β· 9,339 images Β· 25 families Β· 98% accuracy Β· Built from scratch" |
| ) |
|
|
|
|
| app = demo.app |
|
|