عنوان:

‫بررسی تصویر امنیتی (Captcha) سایت - قسمت دوم


نویسنده: مهدی پایروند
تاریخ: ۱۳۹۱/۰۸/۳۰ ۲۱:۳۰
آدرس: www.dntips.ir

در ادامه بررسی تصویر امنیتی سایت مواردی که باید پیاده سازی شود برای مورد اول میتوان کلاس زیر را در نظر گرفت که متدهایی برای تولید اعداد بصورت تصادفی در بین بازه معرفی شده را بازگشت میدهد:


// RandomGenerator.cs
using System;
using System.Security.Cryptography;

namespace PersianCaptchaHandler
{
    public class RandomGenerator
    {
        private static readonly byte[] Randb = new byte[4];
        private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();

        public static int Next()
        {
            Rand.GetBytes(Randb);
            var value = BitConverter.ToInt32(Randb, 0);
            if (value < 0) value = -value;
            return value;
        }
        public static int Next(int max)
        {
            Rand.GetBytes(Randb);
            var value = BitConverter.ToInt32(Randb, 0);
            value = value%(max + 1);
            if (value < 0) value = -value;
            return value;
        }
        public static int Next(int min, int max)
        {
            var value = Next(max - min) + min;
            return value;
        }
    }
}
و برای تبدیل عدد تصادفی بدست آمده به متن نیز از این کلاس میتوان استفاده کرد که به طرز ساده ای این کار را انجام میدهد:
// NumberToString.cs
namespace PersianCaptchaHandler
{
    public class NumberToString
    {

        #region Fields
        private static readonly string[] Yakan = new[] { "صفر", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" };
        private static readonly string[] Dahgan = new[] { "", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" };
        private static readonly string[] Dahyek = new [] { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" };
        private static readonly string[] Sadgan = new [] { "", "یکصد", "دوصد", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد" };
        private static readonly string[] Basex = new [] { "", "هزار", "میلیون", "میلیارد", "تریلیون" };
        #endregion

        private static string Getnum3(int num3)
        {
            var s = "";
            var d12 = num3 % 100;
            var d3 = num3 / 100;
            if (d3 != 0)
                s = Sadgan[d3] + " و ";
            if ((d12 >= 10) && (d12 <= 19))
            {
                s = s + Dahyek[d12 - 10];
            }
            else
            {
                var d2 = d12 / 10;
                if (d2 != 0)
                    s = s + Dahgan[d2] + " و ";
                var d1 = d12 % 10;
                if (d1 != 0)
                    s = s + Yakan[d1] + " و ";
                s = s.Substring(0, s.Length - 3);
            }
            return s;
        }

        public static string ConvertIntNumberToFarsiAlphabatic(string snum)
        {
            var stotal = "";
            if (snum == "0") return Yakan[0];

            snum = snum.PadLeft(((snum.Length - 1) / 3 + 1) * 3, '0');
            var l = snum.Length / 3 - 1;
            for (var i = 0; i <= l; i++)
            {
                var b = int.Parse(snum.Substring(i * 3, 3));
                if (b != 0)
                    stotal = stotal + Getnum3(b) + " " + Basex[l - i] + " و ";
            }
            stotal = stotal.Substring(0, stotal.Length - 3);
            return stotal;
        }
    }
}
و برای کد کردن و دیکد کردن یعنی موارد سوم و چهارم مقاله قبلی، متن بدست آمده را که بعنوان قسمتی از آدرس تصویر در ادامه آدرس هندلر معرفی شده می‌آید تبدیل به یک string بی معنی برای بازدیدکننده میکند:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace PersianCaptchaHandler
{
    public class Encryptor
    {
        #region constraints
        private static string Password { get { return "Mehdi"; } }
        private static string Salt { get { return "Payervand"; } }
        #endregion

        public static string Encrypt(string clearText)
        {
            // Turn text to bytes
            var clearBytes = Encoding.Unicode.GetBytes(clearText);

            var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));

            var ms = new MemoryStream();
            var alg = Rijndael.Create();

            alg.Key = pdb.GetBytes(32);
            alg.IV = pdb.GetBytes(16);

            var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write);

            cs.Write(clearBytes, 0, clearBytes.Length);
            cs.Close();

            var encryptedData = ms.ToArray();

            return Convert.ToBase64String(encryptedData);
        }
        public static string Decrypt(string cipherText)
        {
            // Convert text to byte
            var cipherBytes = Convert.FromBase64String(cipherText);

            var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));

            var ms = new MemoryStream();
            var alg = Rijndael.Create();

            alg.Key = pdb.GetBytes(32);
            alg.IV = pdb.GetBytes(16);

            var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write);

            cs.Write(cipherBytes, 0, cipherBytes.Length);
            cs.Close();

            var decryptedData = ms.ToArray();

            return Encoding.Unicode.GetString(decryptedData);
        }
    }
}


و نیز برای اعتبار سنجی عدد دریافتی از کاربر میتوان از عبارت با قاعده زیر استفاده کرد:

// Utils.cs
using System.Text.RegularExpressions;

namespace PersianCaptchaHandler
{
    public class Utils
    {
        private static readonly Regex NumberMatch = new Regex(@"^([0-9]*|\d*\.\d{1}?\d*)$", RegexOptions.Compiled);
        public static bool IsNumber(string number2Match)
        {
            return NumberMatch.IsMatch(number2Match);
        }
    }
}
برای استفاده نیز کافیست که هندلر مربوط به این کتابخانه را بطریق زیر در وب کانفیگ رجیستر کرد:

<add verb="GET" path="/captcha/" type="PersianCaptchaHandler.CaptchaHandler, PersianCaptchaHandler, Version=1.0.0.0, Culture=neutral"  />
و در صفحه مورد نظرتان بطریق زیر میتوان از یک تگ تصویر برای نمایش تصویر تولیدی و از یک فیلد مخفی برای نگهداری مقدار کد شده معادل عدد تولیدی که در هنگام مقایسه با عدد ورودی کاربر مورد نیاز است استفاده شود:

<!-- ASPX -->
<dl>
    <dt>تصویر امنیتی</dt>
    <dd>
        <asp:Image ID="imgCaptchaText" runat="server" AlternateText="CaptchaImage" />
        <asp:HiddenField ID="hfCaptchaText" runat="server" />
        <asp:ImageButton ID="btnRefreshCaptcha" runat="server" ImageUrl="/img/refresh.png"
            OnClick="btnRefreshCaptcha_Click" />
        <br />
        <asp:TextBox ID="txtCaptcha" runat="server" AutoCompleteType="Disabled"></asp:TextBox>
    </dd>
    <dd>
        <asp:Button ID="btnSubmit" runat="server" Text="ثبت" OnClick="btnSubmit_Click" />
    </dd>
</dl>
<asp:Label ID="lblMessage" runat="server"></asp:Label>
در زمان لود صفحه، تصویر امنیتی مقدار دهی میشود و در زمان ورود عدد توسط کاربر با توجه به اینکه کاربر حتما باید عدد وارد کند با عبارت با قاعده این اعتبار سنجی انجام میشود:

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
                SetCaptcha();
        }

        private void SetCaptcha()
        {
            lblMessage.Text =
            txtCaptcha.Text = string.Empty;

            var newNumber =
                RandomGenerator.Next(100, 999)
                ;

            var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(newNumber.ToString());

            hfCaptchaText.Value =
                HttpUtility
                .UrlEncode(
                    Encryptor.Encrypt(
                        farsiAlphabatic
                    )
                );

            txtCaptcha.Text = string.Empty;
            imgCaptchaText.ImageUrl =
                "/captcha/?text=" + hfCaptchaText.Value;
        }
و بعد از ورود عدد از سمت کاربر از متد تبدیل به حروف استفاده کرده و این مقدار تولیدی با مقدار فیلد مخفی مقایسه میشود:
        private string GetCaptcha()
        {
            var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(txtCaptcha.Text);

            var encryptedString =
                HttpUtility
                .UrlEncode(
                    Encryptor.Encrypt(
                        farsiAlphabatic
                    )
                );

            return encryptedString;
        }

        private bool ValidateUserInputForLogin()
        {
            if (!Utils.IsNumber(txtCaptcha.Text))
            {
                lblMessage.Text = "تصویر امنیتی را بطور صحیح وارد نکرده اید";
                return false;
            }

            var strGetCaptcha =
                GetCaptcha();

            var strDecodedVAlue =
                hfCaptchaText.Value;

            if (strDecodedVAlue != strGetCaptcha)
            {
                lblMessage.Text = "کلمه امنیتی اشتباه است";
                SetCaptcha();
                return false;
            }
            return true;
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            if (!ValidateUserInputForLogin()) return;
            lblMessage.Text = "کلمه امنیتی درست است";
        }

        protected void btnRefreshCaptcha_Click(object sender, ImageClickEventArgs e)
        {
            SetCaptcha();
        }
 در آخر این پروژه در کدپلکس قرار داده شده، و مشتاق نظرات و پیشنهادات شما هستم و نیز نمونه مثال بالا ضمیمه شده است

نظرات

  • احمد احمدی در ۱۳۹۱/۰۹/۰۱ ۱۳:۲۸
    تشکر از مطلب مفیدتون.
    جناب نصیری کلاسی برای تبدیل عدد به حروف نوشته اند که استفاده از اون کلاس در این پروژه ، زیبایی کار را دو چندان می‌کند!
    • مهدی پایروند در ۱۳۹۱/۰۹/۰۱ ۱۳:۴۰
      بسیار ممنون، با اجازه مهندس نصیری این کار انجام میشه.
      لطفا درخواست رو در قسمت مربوط به پروژه در سایت مطرح کنید تا یک تاریخچه مطلوبی برای پروژه ایجاد بشه.
  • مهدی پایروند در ۱۳۹۱/۰۹/۰۱ ۱۳:۳۸
    پروژه در کدپلکس
    پروژه در سایت
  • مهمان در ۱۳۹۱/۰۹/۰۶ ۱۱:۲۹

    مشکلی که این روش دارد این است که با یک بار بارگذاری صفحه، خواندن عکس توسط انسان و یادداشت متن رمز گذاری شده می‌توان مقدار اولیه عکس را هر بار با ویرایش متن رمزگذاری شده به سرور ارسال کرد و تایید گرفت! مشکلی که سایت فعلی هم دارد.

    برای حل این مشکل چه پیشنهادی دارید؟




    • مهدی پایروند در ۱۳۹۱/۰۹/۰۶ ۱۴:۱۹
      ممنون از نظرتون،
      موضوع  مورد نطر، قسمتی است که متن تصویر برای این کپچا در آن نگهداری میشود،
      1.  در صورتی که بخواهیم از توانایی‌های سمت سرور کمک گرفت: بنظرم برای اعتبار سنجی میتوان برای کابران در بانک یک کد یونیک به ازای اولین هیت کاربر در سایت ایجاد کرد و این کد را بصورت رمزنگاری شده در کلاینت(کوکی) نگهداشت و مواردی از این دست را با این کد اعتبارسنجی کرد.
      2. در صورتی که بخواهیم در سمت کلاینت همه چیز هندل شود : مطمئنا بهتر است که این فیلد مخفی در کنار باکس ورودی قرار داده نشود و برای نگهداری این فیلد از جاوا اسکریپت یا کوکی و از هردو کمک گرفت به این صورت که این مقدار کد شده بصورت مقداری غیر از این و با نامی بدون ربط در متغیری که از سمت سرور مقدار دهی میشود و فقط در سرور خوانده میشود تعویض کرد.
  • امیرحسین مرجانی در ۱۳۹۱/۰۹/۰۷ ۱۷:۵
    لطفا راهنمایی کنید چطور پروژه رو دانلود کنم؟
    • مهدی پایروند در ۱۳۹۱/۰۹/۰۷ ۱۷:۵۶
      برای دمو از این لینک میتونید استفاده کنید +
      و همچنین در کدپلکس persiacaptchahandler.codeplex.com
      و در همین سایت هم PersianCaptchaHandler
      • کیارش سلیمان زاده در ۱۳۹۱/۰۹/۰۸ ۱۲:۴۳
        بهترین راه استفاده از Expiration date هستش،به این صورت که به متن رمزنگاری شده یک تاریخ انقضای 3دقیقه ای داد،بدون استفاده از دیتابیس و کوکی.

        • مهدی پایروند در ۱۳۹۱/۰۹/۰۸ ۱۲:۵۷
          البته اگر از بازه عددی کوچکی استفاده بشه که ممکنه این تاریخ هم بلا استفاده بمونه، همچنین این تاریخ قرار هست کجا نگهداری بشه 
          • وحید نصیری در ۱۳۹۱/۰۹/۰۸ ۱۳:۴
            در تمام قسمت‌های رمزنگاری شده سایت جاری از یک salt با طول عمر یک روز استفاده می‌شود. این salt به کلید رمزنگاری اطلاعات اضافه می‌شود. بنابراین اطلاعات رمزنگاری شده توسط آن کلید، فقط در طی روز جاری قابل رمزگشایی خواهند بود.
            برای مثال لینک یک پیغام خصوصی سایت را در جایی ذخیره کنید. تا پایان روز کار می‌کند. روز بعد به صورت خودکار منقضی شده (چون salt فردا چیز دیگری است) و اطلاعات روز قبل توسط کلید جدید قابل رمزگشایی نیست. به این ترتیب شخص نیاز خواهد داشت تا لینک جدیدی را در صفحه مداخل مرتبط دریافت کند.
            • کیارش سلیمان زاده در ۱۳۹۱/۰۹/۰۸ ۱۳:۷
              یک روز برای یک captcha زمان زیادی نیست؟
              • وحید نصیری در ۱۳۹۱/۰۹/۰۸ ۱۳:۱۰
                این لایه اول کلی هست (در تمام قسمت‌های سایت). تست کنید ببینید مطابق تصویر بالایی که ارسال شده می‌تونید در مورد کپچای سایت جواب بگیرید یا نه ...
                • کیارش سلیمان زاده در ۱۳۹۱/۰۹/۰۸ ۱۳:۱۸
                  تو لایه‌های بعدی چطور تست میکنید؟
                  طریقه ایجاد salt رو میشه بیشتر توضیح بدید؟
                  چیزی رو در دیتابیس هم ذخیره می‌کنید؟
                  • وحید نصیری در ۱۳۹۱/۰۹/۰۸ ۱۳:۲۱
                    salt مورد استفاده تاریخ ساده روز سیستم است؛ در بانک اطلاعاتی هم ذخیره نمی‌شود.
                    • کیارش سلیمان زاده در ۱۳۹۱/۰۹/۰۸ ۱۳:۴۴
                      ممنون،
                      "تست کنید ببینید مطابق تصویر بالایی که ارسال شده می‌تونید در مورد کپچای سایت جواب بگیرید یا نه ..."
                      بله تست کردم.
                      بعد از اینکه مشخص شد salt متعلق به امروز هستش،چطور تست میکنید که آیا مطابق با تصویر بالا،متن کپی شده یا اصلی هستش؟
                      • وحید نصیری در ۱۳۹۱/۰۹/۰۸ ۱۳:۴۷
                        در مرحله بعد از یک کش 5 دقیقه‌ای با کلیدی بر اساس IP شخص استفاده می‌کنم.
                        • کیارش سلیمان زاده در ۱۳۹۱/۰۹/۰۸ ۱۳:۵۴
                          مگه IP نسبت به ISP برای بعضی‌ها یکسان نیست؟
                          در این صورت مشکلی پیش نمیاد؟
                          • وحید نصیری در ۱۳۹۱/۰۹/۰۸ ۱۳:۵۸
                            تا الان که کسی مشکلی گزارش نکرده.
                             همچنین مقدار ذخیره شده در این کش با طول عمر کوتاه 5 دقیقه‌ای به ازای هر بار ریفرش صفحه متفاوت خواهد بود.
                • رحمت اله رضایی در ۱۳۹۱/۰۹/۰۸ ۱۴:۱۱
                  هفته پیش کپچای سایت این مشکل را داشت ولی الان برطرف شده.
                  • وحید نصیری در ۱۳۹۱/۰۹/۰۸ ۱۶:۱۲
                    گیرم اصلا این کپچا نباشد. با anti forgery-token می‌خواهید چکار کنید؟ با ماژول آنتی داس چطور و خیلی مسایل دیگر.
                    خلاصه صرف اینکه در داخل یک مرورگر به کمک یک افزونه برای مدت کوتاهی توانستید اطلاعاتی را شبیه سازی کنید به این معنا نیست که قابلیت استفاده وسیع و خارج از مرورگر را هم به سادگی دارا است (مثلا بروت فورس پسورد کاربران). تا حد شبیه سازی کامل رفتار یک مرورگر باید پیش برید (و ... اصلا کار ساده‌ای نیست).
                    • رحمت اله رضایی در ۱۳۹۱/۰۹/۰۸ ۲۱:۵۴
                      هدف من از طرح این مساله این بود که نمایش تصویر امنیتی بدون در نظر گرفتن این مورد هیچ فایده ای برای سایت نخواهد داشت و کار به ماژول آنتی داس و مشکلات مربوط به آن کشیده می‌شود (یک لایه عقب تر).
                      در واقع کپچا یک تصویر تزیینی می‌شود.

                      دستکاری اطلاعات ارسالی به سرور، فقط در اینجا مطرح نیست. عمده مورد استفاده آن در ویرایش id‌ها (مثلا در DropDownList ها) ست. (ثبت نام مسکن برای شهری که هنوز ثبت نام آن فعال نشده، خرید بلیت جایگاه ویژه برای یک کنسرت، پرداخت بیمه با دستکاری هزینه و ...)
            • مهدی پایروند در ۱۳۹۱/۰۹/۰۸ ۱۳:۸
              البته بنظرم برای نگهداری نکردن تاریخ در سرور میشه salt رو همین تاریخ امروز در نظر گرفت
          • کیارش سلیمان زاده در ۱۳۹۱/۰۹/۰۸ ۱۳:۵
            در واقع باید متن رو مثلا شامل دو بخش،بخش اول،و بخش دوم هم تاریخ و زمان فعلی هستش که با یه splitter از هم جدا شدن و بعد رمزنگاری کرد،و در سمت سرور ابتدا بخش دوم رو با تاریخ و زمان فعلی(submit شدن فرم) مقایسه کرد و درصورتیکه اختلاف اونها بیشتر از 3 دقیفه(یا 2 یا ...) بود captcha غیر معتبر هستش. 
  • داود حنیفی در ۱۳۹۱/۰۹/۲۱ ۹:۵
    با سلام
    ببخشید این مورد تو vs2012 چطور کار میکنه؟
    تست من تو vs2012 جواب نداد.
    با تشکر از زحماتتون.
    • مهدی پایروند در ۱۳۹۱/۰۹/۲۱ ۹:۴۶
      من این مثال رو با VS2012 باز کردم مشکلی نداشت
      • داود حنیفی در ۱۳۹۱/۰۹/۲۱ ۱۰:۳۴
        ببخشید من اشتباه کردم.منظورم با (net 4.5.) بود.
        • مهدی پایروند در ۱۳۹۱/۰۹/۲۱ ۱۰:۵۰
          با اون دات نت 4.5 هم مشکلی نداشت
          اگه کارتون با نمونه پروژه تبدیل شده راه میوفته من اونو تو این لینک گذاشتم
          PersianCaptcha_2012-12-11_10-51-20.rar
  • مهمان در ۱۳۹۱/۱۱/۲۶ ۱۶:۱۹
    سلام
    لازم است در ابتدا بابت این پروژه تشکر کنم.
    من مشکلی با این کپچا دارم و آن این است که روی محیط تست خودم کار می‌کند اما زمانی که آن را پابلیش می‌کنم و روی سرور اصلی قرار می‌دهم، تصویر کپچا نمایش داده نمی‌شود.
    لازم به ذکر است که از پروتوکل https استفاده می‌کنم.

    با تشکر
    • وحید نصیری در ۱۳۹۱/۱۱/۲۶ ۱۶:۵۴
      یک حدس از راه دور:
      تنظیم پیش فرض این پروژه در وب کانفیگ مربوط به IIS6 است (system.web/httpHandlers). برای IIS7 تنظیم جداگانه‌ای (system.webServer/handlers) نیاز دارد.
      • مهمان در ۱۳۹۱/۱۱/۲۶ ۱۷:۲۳
        سلام
        ممنون از پاسختون.
        اما IIS من ۷ است و موردی رو هم که فرمودید پیش از این چک کردم ولی تغییری حاصل نشده بود.
      • مهمان در ۱۳۹۱/۱۱/۲۶ ۱۷:۳۲
        با راه اندازی مجدد IIS مشکل رفع شد.

        ممنون
  • مهمان در ۱۳۹۲/۱۰/۲۲ ۱۶:۱۶
    سلام؛ بنده آخر متوجه نشدم که از کپچا به این صورت استفاده کنیم یا نه؟
    استفاده از expiration time به نظر من جواب نمیده چون توسط ربات تویه عرض یک دقیقه میشه هزار تا comment ثبت کرد.فکر کنم استفاده از captcha به همین روش و استفاده از anti forgery-token هیچ مشکل امنیتی نداشته باشه. شما چطوری مشکل این روش رو برطرف کردید؟ با تشکر