عنوان:

‫اسکن بدافزارها در حین آپلود فایل‌ها در ASP.NET Core با استفاده از Windows AMSI: آخرین خط دفاعی


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۷/۱۷ ۱۳:۳۵
آدرس: www.dntips.ir
امنیت آپلود فایل‌ها یکی از چالش‌های اساسی در توسعه برنامه‌های وب، به‌ویژه در ASP.NET Core است. مهاجمان می‌توانند از ضعف‌های این بخش برای تزریق کدهای مخرب یا بارگذاری فایل‌های آلوده سوءاستفاده کنند. در حالی که اقداماتی نظیر بررسی پسوند، محتوا (MIME Type) و محدودیت اندازه فایل ضروری هستند، اما یک خط دفاعی نهایی برای مقابله با تهدیدات پیشرفته‌تر وجود دارد: اسکن بدافزار محتوا-محور، که در سیستم‌عامل ویندوز توسط رابط اسکن ضد بدافزار ویندوز (AMSI) به‌راحتی قابل پیاده‌سازی است.

چرا به اسکن بدافزار نیاز داریم؟ آخرین خط دفاعی

حتی پس از اجرای دقیق کلیه مراحل اعتبارسنجی اولیه (شامل بررسی نوع فایل، اندازه و غیره)، یک مهاجم مصمم همچنان می‌تواند محتوای مخرب را آپلود کند. این فایل‌ها از اعتبارسنجی‌های قبلی عبور می‌کنند زیرا در ظاهر قالب‌هایی قانونی دارند، اما در واقعیت مسلح شده‌اند:
  • فایل PDF قانونی حاوی سوءاستفاده‌های جاسازی‌شده جاوااسکریپت.
  • سند آفیس معتبر حاوی ماکروهای مخرب (Macro-based Malware).
  • فایل تصویری واقعی با بارگذاری‌های (Payloads) جاسازی‌شده از طریق نهان‌نگاری (Steganography).
  • بایگانی‌های (Archive) واقعی حاوی بدافزار.
  • حملات روز صفر (Zero-day) که کتابخانه‌های پردازش فایل را هدف قرار می‌دهند.
در این سناریوها، اسکن بدافزار به یک امر حیاتی تبدیل می‌شود، چرا که محتوای درونی فایل را قبل از پردازش یا ذخیره‌سازی تجزیه و تحلیل می‌کند.

AMSI چیست و چرا برای ASP.NET Core روی ویندوز عالی است؟

رابط اسکن ضد بدافزار ویندوز (Windows Antimalware Scan Interface - AMSI) یک ویژگی قدرتمند ویندوز است که به برنامه‌ها اجازه می‌دهد با موتور آنتی‌ویروس نصب شده روی سیستم (مانند Windows Defender یا هر آنتی‌ویروس سازگار دیگر) ادغام شوند.
AMSI مشکل رایجی را حل می‌کند: آنتی‌ویروس‌های سنتی عمدتاً فایل‌های روی دیسک را اسکن می‌کنند. اما در بسیاری از سناریوهای مدرن (مانند PowerShell، برنامه‌های .NET که اسمبلی‌ها را به‌صورت پویا بارگذاری می‌کنند، یا برنامه‌های Node.js)، محتوای مخرب ممکن است مستقیماً در حافظه (Memory Stream) دانلود و استفاده شود. AMSI به برنامه شما این توانایی را می‌دهد که یک جریان یا یک رشته داده را به‌صورت مستقیم برای تجزیه و تحلیل به آنتی‌ویروس ارسال کند.

مزایای استفاده از AMSI

مزیتتوضیح
استفاده از آنتی‌ویروس موجوداز Windows Defender یا هر آنتی‌ویروس سازگار با AMSI که در حال حاضر نصب شده، بهره می‌برد.
به‌روزرسانی خودکارامضاهای تشخیص بدافزار به‌طور خودکار توسط Windows Update به‌روز می‌شوند.
بدون هزینه اضافیاستفاده از آن در سیستم‌عامل‌های ویندوز رایگان است و نیازی به مجوز جداگانه ندارد.
ادغام سبک‌وزندارای API ساده با سربار (Overhead) حداقلی.
اسکن محتوا-محورمحتوای واقعی فایل را تجزیه و تحلیل می‌کند، نه صرفاً امضاهای سطح بالای فایل را.
استاندارد صنعتیاز هوش تهدیدات گسترده مایکروسافت بهره می‌برد.

محدودیت‌ها:
  • انحصاری برای ویندوز: AMSI فقط در سیستم‌عامل ویندوز قابل استفاده است. (برای نیازهای کراس‌پلتفرم، می‌توان ClamAV یا سرویس‌های ابری را در نظر گرفت).
  • نیاز به آنتی‌ویروس سازگار: نصب بودن یک موتور آنتی‌ویروس سازگار با AMSI ضروری است.
  • غیرقابل نفوذ نبودن: مانند هر راهکار آنتی‌ویروس، توانایی تشخیص تهدیدات روز صفر کاملاً ناشناخته را ندارد.

پیاده‌سازی اسکن بدافزار در ASP.NET Core با AMSI

برای پیاده‌سازی این قابلیت در ASP.NET Core، می‌توانیم یک مرحله اعتبارسنجی (Validation Step) مجزا تعریف کنیم که از کتابخانه‌ای برای تعامل با AMSI استفاده می‌کند.
AMSI دو متد اصلی برای اسکن ارائه می‌دهد: یکی برای اسکن یک رشته (String) و دیگری برای اسکن یک بلوک داده باینری (Binary Blob)، که ما از مورد دوم برای فایل‌ها استفاده می‌کنیم.

۱. کلاس پیاده‌سازی منطق اسکن

کلاس زیر، منطق اصلی اعتبارسنجی را نشان می‌دهد که داده‌های فایل را دریافت کرده و آن‌ها را به موتور AMSI ارسال می‌کند.
public sealed class MalwareScanValidationStep
{
    private readonly ILogger _logger;

    public MalwareScanValidationStep(ILogger logger)
    {
        _logger = logger;
    }

    public async Task<ValidationResult> ValidateAsync(IFormFile file, CancellationToken token)
    {
        // فرض کنید متد GetData() یک متد Extension برای خواندن محتوای IFormFile به صورت آرایه بایت است.
        var payload = await file.GetData(); 
        
        // 1. ایجاد یک Context جدید AMSI
        using var application = AmsiContext.Create("DocumentStorage"); 
        
        // 2. ایجاد یک Session جدید (برای مرتبط کردن درخواست‌های اسکن)
        using var session = application.CreateSession(); 
        
        // 3. اسکن محتوای فایل
        var isMalware = await session.IsPotentialMalwareAsync(payload, file.Name, token);
        
        if (isMalware)
        {
            _logger.LogWarning("MalwareScan: File '{FileName}' potentially contains malware.", file.FileName);
            // در صورت تشخیص بدافزار، عملیات را با خطا متوقف می‌کنیم.
            return ValidationResult.Fail(new BadRequestObjectResult(new { message = "Document potentially contains malware." }));
        }

        return ValidationResult.Success();
    }
}


برای تعامل با AMSI، از قابلیت Platform Invoke (P/Invoke) در .NET برای فراخوانی توابع غیرمدیریت‌شده (Unmanaged Functions) موجود در amsi.dll استفاده می‌کنیم.
این کلاس وظیفه مقداردهی اولیه (AmsiInitialize) و اتمام (AmsiUninitialize) رابط AMSI برای برنامه ما را بر عهده دارد و یک دسته (Handle) اصلی برای کل برنامه ایجاد می‌کند.
public sealed class AmsiContext : IDisposable
{
    private IntPtr _handle;

    private AmsiContext(IntPtr handle)
    {
        _handle = handle;
    }

    public static AmsiContext Create(string appName)
    {
        var result = AmsiInitialize(appName, out var handle);
        if (result != 0)
        {
            throw new InvalidOperationException($"Failed to initialize AMSI. Error code: {result}");
        }
        return new AmsiContext(handle);
    }

    public AmsiSession CreateSession()
    {
        var result = AmsiOpenSession(_handle, out var sessionHandle);
        if (result != 0)
        {
            throw new InvalidOperationException($"Failed to open AMSI session. Error code: {result}");
        }
        return new AmsiSession(_handle, sessionHandle);
    }

    public void Dispose()
    {
        if (_handle != IntPtr.Zero)
        {
            AmsiUninitialize(_handle);
            _handle = IntPtr.Zero;
        }
    }

    [DllImport("amsi.dll", CharSet = CharSet.Unicode)]
    private static extern int AmsiInitialize(string appName, out IntPtr amsiContext);

    [DllImport("amsi.dll")]
    private static extern int AmsiOpenSession(IntPtr amsiContext, out IntPtr session);

    [DllImport("amsi.dll")]
    private static extern void AmsiUninitialize(IntPtr amsiContext);
}

AmsiSession

این کلاس یک جلسه اسکن (AmsiOpenSession/AmsiCloseSession) ایجاد می‌کند و مهم‌ترین متد، یعنی AmsiScanBuffer را فراخوانی می‌کند که داده‌های باینری را برای اسکن به آنتی‌ویروس ارسال می‌کند.
public sealed class AmsiSession : IDisposable
{
    private readonly IntPtr _amsiContext;
    private IntPtr _sessionHandle;

    internal AmsiSession(IntPtr amsiContext, IntPtr sessionHandle)
    {
        _amsiContext = amsiContext;
        _sessionHandle = sessionHandle;
    }

    public async Task<bool> IsPotentialMalwareAsync(byte[] data, string filename, CancellationToken cancellationToken = default)
    {
        return await Task.Run(() => IsPotentialMalware(data, filename), cancellationToken);
    }

    private bool IsPotentialMalware(byte[] data, string filename)
    {
        var result = AmsiScanBuffer(_amsiContext, data, (uint)data.Length, filename, _sessionHandle, out var scanResult);
        if (result != 0)
        {
            throw new InvalidOperationException($"Failed to scan buffer. Error code: {result}");
        }

        // AMSI_RESULT values:
        // 0 = Clean
        // 1-32767 = Detected (higher = more confident detection)
        // 32768+ = Blocked
        return scanResult >= 1;
    }

    public void Dispose()
    {
        if (_sessionHandle != IntPtr.Zero)
        {
            AmsiCloseSession(_amsiContext, _sessionHandle);
            _sessionHandle = IntPtr.Zero;
        }
    }

    [DllImport("amsi.dll")]
    private static extern int AmsiScanBuffer(IntPtr amsiContext, byte[] buffer, uint length, string contentName, IntPtr session, out int result);

    [DllImport("amsi.dll")]
    private static extern void AmsiCloseSession(IntPtr amsiContext, IntPtr session);
}

راهبرد تست (Testing Strategy)

تست اسکن بدافزار می‌تواند پیچیده باشد. بهترین رویکرد، استفاده از فایل تست EICAR است. فایل تست EICAR یک فایل استاندارد ضد بدافزار است که توسط همه موتورهای آنتی‌ویروس بزرگ به‌عنوان بدافزار شناسایی می‌شود، اما کاملاً بی‌ضرر است و صرفاً برای تست استفاده می‌شود. کد زیر یک نمونه تست واحد (Unit Test) برای بررسی صحت عملکرد پیاده‌سازی AMSI با استفاده از رشته EICAR را نشان می‌دهد:
[Test]
public async Task ValidateAsync_WithEICARTestFile_ReturnsFail()
{
    // EICAR test string - universally detected as malware
    const string eicarString = @"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
    var eicarBytes = Encoding.UTF8.GetBytes(eicarString);
    
    var mockFile = new Mock<IFormFile>();
    mockFile.Setup(f => f.FileName).Returns("eicar.txt");
    
    // Mock GetData() extension method
    var memoryStream = new MemoryStream(eicarBytes);
    mockFile.Setup(f => f.OpenReadStream()).Returns(memoryStream);
    mockFile.Setup(f => f.Length).Returns(eicarBytes.Length);
    
    var validator = new MalwareScanValidationStep(logger);
    
    // Act
    var result = await validator.ValidateAsync(mockFile.Object, CancellationToken.None);
    
    // Assert
    Assert.That(result.IsValid, Is.False);
    Assert.That(result.ErrorResult, Is.InstanceOf<BadRequestObjectResult>());
}

نتیجه‌گیری

ادغام Windows AMSI در برنامه‌های ASP.NET Core که روی سرورهای ویندوز اجرا می‌شوند، یک لایه امنیتی حیاتی و با کیفیت سازمانی را بدون هیچ هزینه زیرساختی یا مجوزی اضافه می‌کند. این روش تضمین می‌کند که حتی پیشرفته‌ترین حملات بدافزاری که از اعتبارسنجی‌های مرسوم نوع فایل عبور می‌کنند، در همان لحظه آپلود و قبل از آسیب‌رسانی، توسط موتور آنتی‌ویروس قوی و به‌روز ویندوز شناسایی و مسدود می‌شوند. این یک گام اساسی برای ساختن یک سیستم آپلود فایل قوی و ایمن است.

نظرات

  • هادی مزارعی در ۱۴۰۴/۰۷/۱۷ ۱۶:۲۱
    آیا آماری از ارسال فایل‌های متعدد و همزمان توسط کاربران و ارزیابی زمان انجام کار با استفاده از آنتی‌ویروس با روش مرسوم بررسی مثلا امضای فایل موجود هست؟ بطور خلاصه: چقدر وقفه ایجاد خواهد کرد؟
    • وحید نصیری در ۱۴۰۴/۰۷/۱۷ ۱۹:۴۸
      - خیلی سریعه: 100-500 میلی ثانیه برای اکثر فایل‌ها؛ هرچند برای فایل‌های حجیم ممکنه به چند ثانیه هم برسد.
      - از همین فناوری در SharePoint هم استفاده می‌شود.
  • وحید نصیری در ۱۴۰۴/۰۷/۱۸ ۱۰:۲۲
    یک نکته‌ی تکمیلی

    متد IsPotentialMalwareAsync معرفی شده‌:
    public async Task<bool> IsPotentialMalwareAsync(byte[] data, string filename, 
            CancellationToken cancellationToken = default)
    {
        return await Task.Run(() => IsPotentialMalware(data, filename), cancellationToken);
    }
    در رده‌ی «متدهای async تقلبی» قرار می‌گیرد! زمانیکه اصل API ویندوز در این حالت به صورت async ارائه نشده، جازدن آن به صورت async، با فراخوانی دستی Task.Run، کمکی به async شدن آن نمی‌کند و فقط سربار پردازشی برنامه‌های وب را بیشتر می‌کند:
    «... در اینجا Task.Run فراخوانی شده صرفا سبب خواهد شد عملیات مدنظر، بر روی یک ترد دیگر، نسبت به ترد اصلی اختصاص داده شده توسط ASP.NET برای فراخوانی و پردازش CalculateXYZAsync، صورت گیرد. این عملیات بهینه نیست. تمام پردازش‌های درخواست‌های ASP.NET در تردهای خاص خود انجام می‌شوند. وجود ترد دوم ایجاد شده توسط Task.Run در اینجا چه حاصلی را بجز سوئیچ بی‌جهت بین تردها و همچنین بالا بردن میزان کار Garbage collector دارد؟ در این حالت نه تنها سبب بالا بردن مقیاس پذیری سیستم نشده‌ایم، بلکه میزان کار Garbage collector و همچنین سوئیچ بین تردهای مختلف را در Thread pool برنامه به شدت افزایش داده‌ایم. همچنین یک چنین سیستمی برای تدارک تردهای بیشتر و مدیریت آن‌ها، مصرف حافظه‌ی بیشتری نیز خواهد داشت ...»