همانطور که می دانید Hashing یا هش کردن مقادیر مختلف یکی از راه های رمزنگاری یک طرفه است (آن را با encryption اشتباه نگیرید چرا که دو طرفه است). به طور مثال فرض کنید تابعی به نام H داشته باشیم (مخفف hash) و سپس داده ای به نام d (مخفف data) را به آن پاس بدهیم. با این حساب (d)H اجرا شده و داده ما هش می شود. مقدارِ هش شده d معمولا یک رشته عجیب و غریب است و اینجاست که مفهوم «یک طرفه» بودن آن مطرح می شود. یک طرفه بودن هش ها بدین معنی است که تقریبا هیچکس نمی تواند مقدار هش شده را گرفته و مقدار اصلی را از آن استخراج کند.
الگوریتم های مختلفی برای هش کردن داده ها وجود دارد. یک مثال ساده رشته roxo.ir/plus می باشد. اگر بخواهیم با استفاده از الگوریتم SHA-1 آن را هش کنیم، مقدار زیر را می گیریم:
26627ace586899f113ed6c88c6759b62776dad96
هیچکس نمی تواند با داشتن این هش، به رشته roxo.ir/plus برسد. حالا اگر بخواهیم رشته roxo.ir/plus را با الگوریتم ripemd320 هش کنیم، نتیجه زیر را می گیریم:
f48eb49a66d597efa93519ffa1b1fb7433f431d708833fd95004891e8bb7dc8d251dbe1419a4f6f1
همانطور که می بینید طول این رشته ها متفاوت است چرا که از الگوریتم های متفاوتی استفاده کرده ایم (هر کدام از این الگوریتم ها کاربرد مخصوص خود را دارد).
کاربرد های Hashing در علوم کامپیوتری بسیار زیاد است اما من دو نمونه رایج از آن ها را مثال می زنم: بررسی تمامیت فایل و محافظت از رمز های عبور.
فرض کنید فایلی را از جایی دانلود کرده باشیم. چطور مطمئن شویم که فایل ها بدون مشکل دانلود شده است؟ از کجا بدانیم که کسی این فایل را دستکاری نکرده است؟ ما می توانیم بر اساس داده های فایل یک هش برایش تولید کنیم و آن هش را به همه اعلام کنیم. حالا هر کسی که فایل را دانلود کرده باشد می تواند هش فایل دانلود شده را با هش اعلام شده توسط ما مقایسه کند. در صورتی که هش ها یکی بودند یعنی حتما دو فایل دانلود شده یکی هستند اما اگر هش ها متفاوت بودند یعنی یا فایل ما به اشتباه دانلود شده است (مثلا در هنگام دانلود قسمتی از فایل خراب شده است) یا اینکه کسی این فایل را دستکاری کرده است (به طور خلاصه «تمامیت» فایل خدشه دار شده است).
به طور مثال اگر به صفحه دانلود سیستم عامل 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 (ساخت هش از یک مقدار) وجود دارد که هر کدام مزایا و معایب خاص خودش را دارد. علاوه بر این موضوع سرعت هر کدام از این الگوریتم ها نیز متفاوت است اما سوالی که پیش می آید اینجاست که ما باید از چه نوع الگوریتمی برای هش کردن رمز عبور استفاده کنیم؟ شاید بگویید هر چه الگوریتم و تابع ما سریع تر باشد وب سایت ما نیز سریع تر است بنابراین الگوریتم های سریع تر گزینه های بهتری هستند اما اینطور نیست!
برای پاسخ به این سوال ابتدا باید دوباره مکانیسم عمل در وب سایت را برایتان توضیح بدهم:
حالا از شما سوالی دارم: آیا با حملات 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 اثر بخش است.
برای آشنایی من چند مورد از این الگوریتم ها را برای شما ذکر می کنم:
موسسه OWASP (پروژه امنیت نرمافزاری تحت وب) یک موسسه آنلاین است که مقالات و تحقیقات مختلفی را در حوزه امنیت سایبری منتشر می کند. توصیه های این موسسه از معتبر ترین توصیه های حوزه امنیت است بنابراین باید به سراغ توصیه OWASP برای هش کردن رمز عبور برویم:
احتمالا می گویید ما در مورد salt صحبت نکرده ایم. salt چیست؟
salt (به معنی «نمک») داده ای تصادفی است که قبل از عملیات هش به داده شما اضافه می شود. طبیعتا اضافه کردن یک مقدار تصادفی به داده اصلی قبل از hash کردن آن باعث می شود که نتیجه hash متفاوت با زمانی باشد که فقط از داده اصلی استفاده کرده اید.
ما می دانیم که Hashing یک فرآیند یک طرفه است بنابراین اگر در هنگام ثبت نام کاربر داده ای تصادفی را به رمز عبور او اضافه کنیم و سپس آن را هش کنیم چطور می توانیم در هنگام ثبت نام، این داده تصادفی را داشته باشیم؟ طبیعتا زمانی که کاربر قصد login داشته باشد باید رمز عبور وارد شده را از فرم login گرفته و با همان داده تصادفی ترکیب کنیم اما از کجا بدانیم این داده تصادفی چه بوده است؟ راه حل اینجاست که salt را در پایگاه داده ذخیره کنید، سپس رمز عبور را از فرم login گرفته و salt را به آن اضافه کنید. حالا این مقدار جدید را هش کرده و با هش ذخیره شده در پایگاه داده (به عنوان رمز عبور) مقایسه می کنیم.
سوال بعدی ما اینجاست که چرا از salt استفاده کنیم؟ برای پاسخ دادن به این سوال باید با سه نوع از کاربردی ترین حملات برای دور زدن رمز عبور آشنا شوید. من این سه نوع حمله را به صورت خلاصه برایتان توضیح می دهم:
اضافه کردن salt به هش ها باعث می شود که کرک کردن هش ها به صورت تصاعدی سخت تر بشود تا جایی که حملات rainbow تقریبا غیر ممکن می شوند. سازمان IETF (کارگروه مهندسی اینترنت) در یکی از یادداشت های خودشان توضیح می دهند که اندازه salt باید حداقل ۸ بیت باشد.
زبان پایتون ماژولی به نام 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
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.