Hashing در علوم کامپیوتر، رمز عبور و تمامیت فایل

Hashing

04 اسفند 1399
hashing در علوم کامپیوتر، رمز عبور و تمامیت فایل

Hashing چیست؟

همانطور که می دانید Hashing یا هش کردن مقادیر مختلف یکی از راه های رمزنگاری یک طرفه است (آن را با encryption اشتباه نگیرید چرا که دو طرفه است). به طور مثال فرض کنید تابعی به نام H داشته باشیم (مخفف hash) و سپس داده ای به نام d (مخفف data) را به آن پاس بدهیم. با این حساب (d)H اجرا شده و داده ما هش می شود. مقدارِ هش شده d معمولا یک رشته عجیب و غریب است و اینجاست که مفهوم «یک طرفه» بودن آن مطرح می شود. یک طرفه بودن هش ها بدین معنی است که تقریبا هیچکس نمی تواند مقدار هش شده را گرفته و مقدار اصلی را از آن استخراج کند.

الگوریتم های مختلفی برای هش کردن داده ها وجود دارد. یک مثال ساده رشته roxo.ir/plus می باشد. اگر بخواهیم با استفاده از الگوریتم SHA-1 آن را هش کنیم، مقدار زیر را می گیریم:

26627ace586899f113ed6c88c6759b62776dad96

هیچکس نمی تواند با داشتن این هش، به رشته roxo.ir/plus برسد. حالا اگر بخواهیم رشته roxo.ir/plus را با الگوریتم ripemd320 هش کنیم، نتیجه زیر را می گیریم:

f48eb49a66d597efa93519ffa1b1fb7433f431d708833fd95004891e8bb7dc8d251dbe1419a4f6f1

همانطور که می بینید طول این رشته ها متفاوت است چرا که از الگوریتم های متفاوتی استفاده کرده ایم (هر کدام از این الگوریتم ها کاربرد مخصوص خود را دارد).

کاربرد Hashing در علوم کامپیوتری

کاربرد های Hashing در علوم کامپیوتری بسیار زیاد است اما من دو نمونه رایج از آن ها را مثال می زنم: بررسی تمامیت فایل و محافظت از رمز های عبور.

بررسی تمامیت فایل (File Integrity)

فرض کنید فایلی را از جایی دانلود کرده باشیم. چطور مطمئن شویم که فایل ها بدون مشکل دانلود شده است؟ از کجا بدانیم که کسی این فایل را دستکاری نکرده است؟ ما می توانیم بر اساس داده های فایل یک هش برایش تولید کنیم و آن هش را به همه اعلام کنیم. حالا هر کسی که فایل را دانلود کرده باشد می تواند هش فایل دانلود شده را با هش اعلام شده توسط ما مقایسه کند. در صورتی که هش ها یکی بودند یعنی حتما دو فایل دانلود شده یکی هستند اما اگر هش ها متفاوت بودند یعنی یا فایل ما به اشتباه دانلود شده است (مثلا در هنگام دانلود قسمتی از فایل خراب شده است) یا اینکه کسی این فایل را دستکاری کرده است (به طور خلاصه «تمامیت» فایل خدشه دار شده است).

به طور مثال اگر به صفحه دانلود سیستم عامل Ubuntu بروید، لینکی به نام verify your download را خواهید دید. اگر روی آن کلیک کنید متن زیر برایتان نمایش داده می شود:

Run this command in your terminal in the directory the iso was downloaded to verify the SHA256 checksum:

echo "3ef833828009fb69d5c584f3701d6946f89fa304757b7947e792f9491caa270e *ubuntu-20.10-desktop-amd64.iso" | shasum -a 256 --check




You should get the following output:

ubuntu-20.10-desktop-amd64.iso: OK

رشته طولانی 3ef833828009fb69d5c584f3701d6946f89fa304757b7947e792f9491caa270e همان هش فایل iso است و با دستور shasum در ترمینال لینوکس می توانیم آن را بررسی کنیم اما بهتر است این تست را با یکی از فایل های سیستم خودتان انجام بدهید.

ابتدا یک فایل جدید به نام text.txt را ایجاد کنید. حالا ترمینال لینوکس یا CMD ویندوز را در آن محل باز کنید. اگر از کاربران لینوکس هستید دستور زیر را اجرا کنید تا یک هش از نوع SHA-256 ایجاد کنیم:

sha256sum text.txt

با اجرای این دستور نتیجه زیر را می گیریم:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  text.txt

حالا یک بار فایل text.txt را باز کرده و چند کاراکتر در آن نوشته و آن را ذخیره کنید. حالا دوباره دستور زیر را اجرا کنید:

sha256sum text.txt

این بار نتیجه زیر را می گیرید:

06f961b802bc46ee168555f066d28f4f0e9afdf3f88174c1ee6f9de004fc30a0  text.txt

همانطور که می بینید نتیجه کاملا متفاوت است بنابراین می فهمیم این فایل text.txt همان فایل قبلی text.txt نیست. حتی اگر بدون نوشتن هیچ متنی درون فایل text.txt فقط یک بار آن را ذخیره کنید باز هم هش تولید شده متفاوت خواهد بود. حتی اگر یک بایت از این فایل تغییر کند باز هم هش متفاوتی را می گیریم.

کاربران ویندوز می توانند powershell خود را باز کرده و دستور زیر را اجرا کنند:

Get-FileHash C:\Users\user1\Downloads\text.txt -Algorithm SHA256 | Format-List

همانطور که می بینید در ویندوز باید الگوریتم را به صورت دستی مشخص کنیم. با اجرای این دستور نتیجه زیر را می گیریم:

Algorithm : SHA256

Hash      : 3CBCFDDEC145E3382D592266BE193E5BE53443138EE6AB6CA09FF20DF609E268

Path      : C:\Users\user1\Downloads\text.txt

اطلاعات بیشتر در وب سایت ماکروسافت.

محافظت از رمز عبور

یکی دیگر از استفاده های Hashing در محافظت از رمز های عبور است. زمانی که کاربری در وب سایت شما ثبت نام می کند، رمز عبور خود را برایتان ارسال می کند. اگر شما این رمز عبور را مستقیما در پایگاه داده ذخیره کنید، مشکلات امنیتی زیادی خواهیم داشت. به طور مثال اگر در آینده وب سایت شما هک شود، هکر ها به تمام رمز های عبور دسترسی پیدا می کنند. بدتر آن است که کاربران معمولا از یک رمز برای تمام حساب هایشان استفاده می کنند بنابراین رمز لو رفته در سایت شما می تواند باعث هک شدن حساب های دیگر آن کاربر نیز بشود. این مسئله فقط مختص به هک نیست بلکه ممکن است یکی از کارمندان خود شما که به پایگاه داده دسترسی دارد، قصد دزدیدن رمز های عبور را داشته باشد.

اگر رمز عبور را در همان ابتدا هش کرده و فقط مقدار هش شده را در پایگاه داده ذخیره کنیم دیگر هیچ کدام از این مشکلات را نخواهیم داشت. البته سوالی ایجاد می شود؛ زمانی که کاربر بخواهد login کند چطور؟ در این حالت ما رمز را از کاربر گرفته و آن را hash می کنیم. اگر این رمز هش شده با مقدار هش شده در پایگاه داده یکی بود یعنی رمز عبور کاربر درست است و او را login می کنیم (بدون اینکه بدانیم رمز عبور او چیست).

من در بلوک زیر کمی شبه کد پایتون نوشته ام تا متوجه کلیت کار بشوید:

def login(username, password):

    user = Users.get(username) # دریافت اطلاعات کاربر از پایگاه داده




    # اگر کاربر وجود نداشته باشد

    if not user: 

        return False




    # هش کردن رمز عبور پاس داده شده توسط کاربر

    supplied_hash = some_hash_function(password)




    # بررسی رمز عبور هش شده با هشی که درون پایگاه داده است

    if supplied_hash == user.password_hash:

        return True

    else: 

        return False

همانطور که گفتم این یک pseudo-code یا «شبه کد» است و کد واقعی پایتون نیست.

الگوریتم های مختلف Hashing

الگوریتم ها و توابع مختلفی برای Hashing (ساخت هش از یک مقدار) وجود دارد که هر کدام مزایا و معایب خاص خودش را دارد. علاوه بر این موضوع سرعت هر کدام از این الگوریتم ها نیز متفاوت است اما سوالی که پیش می آید اینجاست که ما باید از چه نوع الگوریتمی برای هش کردن رمز عبور استفاده کنیم؟ شاید بگویید هر چه الگوریتم و تابع ما سریع تر باشد وب سایت ما نیز سریع تر است بنابراین الگوریتم های سریع تر گزینه های بهتری هستند اما اینطور نیست!

برای پاسخ به این سوال ابتدا باید دوباره مکانیسم عمل در وب سایت را برایتان توضیح بدهم:

  • ثبت نام: کاربر رمز عبور خود را به ما می دهد و ما آن را هش کرده و سپس در پایگاه داده ذخیره می کنیم.
  • ورود به حساب: کاربر رمز حسابش را در فرم login وارد می کند. ما این رمز را گرفته و با همان الگوریتم قبلی هش می کنیم. حالا این هش را با هش ذخیره شده در پایگاه داده مقایسه می کنیم. اگر کاربر رمز صحیح را وارد کرده باشد هر دو هش یکسان خواهند بود.

حالا از شما سوالی دارم: آیا با حملات brute force آشنایی دارید؟ در این نوع حملات، هکر ها تعداد درخواست زیادی را به سرور شما ارسال کرده و سعی می کنند رمز عبور را حدس بزنند. اگر یادتان باشد در دوران کودکی کیف های سامسونت قدیمی را که رمز چهار رقمی داشتند به همین شکل باز می کردیم. آنقدر با اعداد مختلف بازی می کردیم تا بالاخره رمز عبور صحیح را به دست بیاوریم.

حالا به مبحث hash کردن رمز عبور برمی گردیم. اگر از الگوریتم هشی استفاده کنیم که بسیاری سریع است، یعنی هکر ها می توانند تعداد بیشتری رمز عبور را در بازه های کوتاه تری تست کنند و شانس بیشتری برای شکستن رمز عبور دارند. به طور مثال در پایگاه داده ای مانند redis که از مموری سیستم استفاده می کند سرعت پردازش کوئری ها بسیار بالا است تا حدی که اگر به فایل پیکربندی آن (/etc/redis/redis.conf) بروید اخطار زیر را مشاهده می کنید:

# Warning: since Redis is pretty fast an outside user can try up to

# 150k passwords per second against a good box. This means that you should

# use a very strong password otherwise it will be very easy to break.

این اخطار به ما می گوید که سرعت redis بسیار بالاتر از پایگاه داده های عادی است که داده ها را روی دیسک ذخیره می کنند. این تفاوت تا حدی است که هکر ها می توانند تا ۱۵۰ هزار رمز عبور در ثانیه را تست کنند بنابراین پیشنهاد می شود که رمز ادمین برای پایگاه داده redis حداقل ۶۰ رقم باشد! بنابراین اگر از الگوریتم های سریع استفاده کنید، به هکر ها شانس بیشتری برای هک حساب کاربرانتان می دهید. البته در نظر داشته باشید که تفاوت سریع و کُند برای انسان ها قابل تشخیص نیست (زیر یک ثانیه اتفاق می افتد) اما همین تغییر کوچک از نظر ممانعت از حملات brute force اثر بخش است.

برای آشنایی من چند مورد از این الگوریتم ها را برای شما ذکر می کنم:

  • MD5: در دهه های قبل گزینه بسیار رایجی برای هش کردن رمز عبور بود اما امروزه نباید از آن استفاده کنید چرا که نقاط ضعف خاصی را دارد و عملا منسوخ است (البته برای Hashing رمز عبور).
  • SHA-1: این الگوریتم توسط NSA یا آژانس امنیت ملی آمریکا طراحی شده بود اما در دنیای امروزی منسوخ و ناامن محسوب می شود.
  • SHA-3: نسخه ارتقاء یافته SHA-1 است و نسبتا امن و انعطاف پذیر می باشد.
  • NTLM: معمولا در Windows active directory استفاده می شود اما به راحتی شکسته می شود و دیگر یک طرفه نیست. در صورت نیاز می توانید از NTLMv2 به جای آن استفاده کنید.
  • Bcrypt: الگوریتمی کُند است که مخصوص هش کردن رمز های عبور طراحی شده است و در مقابله با حملات brute-force مقاوم می باشد. این الگوریتم در برخی از distro های سیستم عامل لینوکس استفاده شده و بسیار امن است.
  • Argon2: الگوریتمی بسیار بسیار امن است که در مقابل حملات brute-force مقاوم است. پیاده سازی این الگوریتم به دلیل پیچیدگی آن دشوار است.

پیشنهاد OWASP چیست؟

موسسه OWASP (پروژه امنیت نرم‌افزاری تحت وب) یک موسسه آنلاین است که مقالات و تحقیقات مختلفی را در حوزه امنیت سایبری منتشر می کند. توصیه های این موسسه از معتبر ترین توصیه های حوزه امنیت است بنابراین باید به سراغ توصیه OWASP برای هش کردن رمز عبور برویم:

  • همیشه از Bcrypt استفاده کنید مگر آنکه دلیل محکم و خاصی داشته باشید.
  • از salt استفاده کنید.

احتمالا می گویید ما در مورد salt صحبت نکرده ایم. salt چیست؟

salt در Hashing چیست؟

salt (به معنی «نمک») داده ای تصادفی است که قبل از عملیات هش به داده شما اضافه می شود. طبیعتا اضافه کردن یک مقدار تصادفی به داده اصلی قبل از hash کردن آن باعث می شود که نتیجه hash متفاوت با زمانی باشد که فقط از داده اصلی استفاده کرده اید.

ما می دانیم که Hashing یک فرآیند یک طرفه است بنابراین اگر در هنگام ثبت نام کاربر داده ای تصادفی را به رمز عبور او اضافه کنیم و سپس آن را هش کنیم چطور می توانیم در هنگام ثبت نام، این داده تصادفی را داشته باشیم؟ طبیعتا زمانی که کاربر قصد login داشته باشد باید رمز عبور وارد شده را از فرم login گرفته و با همان داده تصادفی ترکیب کنیم اما از کجا بدانیم این داده تصادفی چه بوده است؟ راه حل اینجاست که salt را در پایگاه داده ذخیره کنید، سپس رمز عبور را از فرم login گرفته و salt را به آن اضافه کنید. حالا این مقدار جدید را هش کرده و با هش ذخیره شده در پایگاه داده (به عنوان رمز عبور) مقایسه می کنیم.

سوال بعدی ما اینجاست که چرا از salt استفاده کنیم؟ برای پاسخ دادن به این سوال باید با سه نوع از کاربردی ترین حملات برای دور زدن رمز عبور آشنا شوید. من این سه نوع حمله را به صورت خلاصه برایتان توضیح می دهم:

  • rainbow table attacks: هر rainbow table پایگاه داده ای از مقادیر از پیش محاسبه شده است. چه نوع مقادیری؟ یک دیکشنری بزرگ از رمز های عبور معمولی (هش نشده) و مقادیر هش شده آن ها. باید بدانید که دو رشته ممکن است دقیقا یک هش را تولید کنند بنابراین اصلا نیازی به دانستن رمز عبور اصلی (متنی و هش نشده) نیست بلکه فقط باید هش درست را به دست بیاوریم. این نوع حملات پیچیده هستند و باید در مقاله اختصاصی خودشان توضیح داده شوند.
  • brute-force attacks: این دسته از حملات را در بخش های قبلی توضیح دادم. مثال ساده آن باز کردن رمز کیف سامسونت است که در آن تمام حالات ممکن را یکی پس از دیگری امتحان می کنیم تا بالاخره به جواب برسیم.
  • dictionary attack: این نوع حملات دقیقا مانند حملات brute-force هستند با این تفاوت که در dictionary attack ها به جای امتحان کردن تمام حالت های ممکن، لیستی از رمز عبور های ممکن را داریم و فقط آن ها را امتحان می کنیم. این لیست رمز عبور معمولا از رمز های عبور نشت شده در حملات قبلی یا رمز های عبور فروخته شده در dark web است.

اضافه کردن salt به هش ها باعث می شود که کرک کردن هش ها به صورت تصاعدی سخت تر بشود تا جایی که حملات rainbow تقریبا غیر ممکن می شوند. سازمان IETF (کارگروه مهندسی اینترنت) در یکی از یادداشت های خودشان توضیح می دهند که اندازه salt باید حداقل ۸ بیت باشد.

پیاده سازی تابعی برای Hashing با زبان پایتون

زبان پایتون ماژولی به نام bcrypt دارد که به ما اجازه می دهد از bcrypt استفاده کنیم. برای استفاده از این ماژول باید ابتدا آن را نصب کنیم:

pip install bcrypt

پس از نصب این ماژول می توانیم شروع به کار کنیم. تابع Hashing ما به شکل زیر است:

import bcrypt




# این تابع مسئولیت ساخت هشی را دارد که در پایگاه داده ذخیره می شود

def create_bcrypt_hash(password):

    # رمز عبور را از رشته به بایت تبدیل می کنیم

    password_bytes = password.encode()     

    # یک سالت یا نمک تولید می کنیم

    salt = bcrypt.gensalt(14)              

    # هش را به صورت بایت تولید می کنیم

    password_hash_bytes = bcrypt.hashpw(password_bytes, salt)  

    # هش به صورت بایت تولید شده است بنابراین آن را به رشته تبدیل می کنیم

    password_hash_str = password_hash_bytes.decode()           







    # هش تولید شده توسط این تابع چیزی شبیه به رشته زیر خواهد بود

    # $2b$10$//DXiVVE59p7G5k/4Klx/ezF7BI42QZKmoOD0NDvUuqxRE5bFFBLy

    return password_hash_str       




# این تابع وظیفه مقایسه رمز وارد شده توسط کاربر و هش موجود در پایگاه داده را دارد

def verify_password(password, hash_from_database):

    password_bytes = password.encode()

    hash_bytes = hash_from_database.encode()




    # این تابع، نمک را به صورت خودکار از هش می گیرد

    # در مرحله بعدی این نمک را با رمز عبور (پارامتر اول) ترکیب می کند

    # در نهایت آن را هش کرده و با مقدار ذخیره شده در پایگاه داده مقایسه می کند

    does_match = bcrypt.checkpw(password_bytes, hash_bytes)




    return does_match

طبیعتا من نمی دانم شما از چه پایگاه داده ای استفاده می کنید بنابراین کد های مربوط به پایگاه داده را ننوشته ام اما خط به خط کد بالا را کامنت نویسی کرده ام تا بدانید چه اتفاقی افتاده است.

کلام آخر

کد ارائه شده در این جلسه صرفا برای اهداف آموزشی است. پیشنهاد می کنم در وب سایت های واقعی خودتان از هش کردن رمز عبور به صورت دستی پرهیز کنید چرا که احتمال ایجاد خطا و آسیب پذیری زیاد است (مخصوصا اگر توسعه دهنده ای تازه کار هستید). راه حل بهتر استفاده از فریم ورک های معتبر یا سیستم های Identity Management service مانند OpenID یا Google Auth است.


منبع: وب‌سایت github

نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.