چرا به گنو/لینوکس عشق می ورزم: خاطرات یک هکر

می‌دونین که من سفر کاری خیلی زیاد می رم . در یکی از از سفرها قرار شد یک دستگاه رو با یک دستگاه دیگه عوض کنن ولی یک سوال مهم باقی موند:

جادی عزیز، ما یک دیتابیس از ۳۰۰ هزار کاربر داریم که هش شده‌اند. حالا قرار است این افراد به یک سیستم جدید منتقل شوند اما لازم است پسوردهای آنان را داشته باشیم. شرکت سازنده نرم افزار اول و شرکت نویسنده نرم افزار جدید گفته‌اند که امکان کشف اینکه یک هش چه چیزی بوده وجود ندارد. شما راه حلی می‌شناسید؟

راه حل ساده است و مجموعه ای از اسکریپت نویسی جذاب (اینبار پایتون) و ترکیب کردن دستورات خط فرمان گنو/لینوکس. جریان چیه؟‌ اینها سیصد هزار شماره تلفن و پسورد دارند که مال یک سیستم خاص است. پسوردها هش شده‌اند. یعنی چنین چیزی:

9370333**** ea9bf866d98db73eb0909fa9c1cc1b11
9370050**** fcbfab2f4cda26061ed9e3ee96a4fd61
9370750**** 6a130f1dc6f0c829f874e92e5458dced
9370001**** 9ad97add7f3d9f29cd262159d4540c96

هش یک الگوریتم یکطرفه است که می‌تونه یک استرینگ رو به یک استرینگ پیچیده تبدیل کنه. مثلا الان پسورد نفر اول بعد از هش شدن (با الگوریتم md5) تبدیل شده به ea9bf866d98db73eb0909fa9c1cc1b11. این هش غیرقابل برگشت به پسورد اصلی است اما وقتی طرف مثلا سعی می کنه لاگین کنه و پسوردش رو لاگین می‌کنه، سیستم می‌تونه پسورد جدیدا وارد شده رو هش کنه و ببینه بازهم به همون هش سابق که ذخیره کرده (یعنی ea9bf866d98db73eb0909fa9c1cc1b11) می‌رسه یا نه. این تکنیک اجازه می‌ده ما چک کنیم طرف پسورد صحیح رو داره یا نه بدون اینکه مجبور باشیم پسوردش رو جایی ذخیره کنیم. حالا قراره این سیستم با یک سیستم دیگه که با هش متفاوتی کار می کنه جایگزین بشه و لازمه بی دردسر بودن جریان اینه که بشه پسوردها رو از این هش‌ها استخراج کرد. اما هش ساخته شده تا معکوس نشه – یعنی از نظر علمی از هش نمی شه به هش‌شده رسید.

خب حالا راه حل ما چیه؟ یک راه که بهش «بروت فورث» می‌گن اینه که یکی یکی هر چیز ممکن رو هش کنیم. یعنی اول ۱ رو هش کنیم ببنیم به اون چیزی که اونجا هست می‌رسیم یا نه. بعد ۲ رو هش کنیم. بعد ۱۲ رو بعد ۱۱ رو و خلاصه هر چیز ممکن رو. اینکار یک نیروی کور است و بسیار وقت گیر. اما یک راه هوشمندانه تر هم داریم که بهش می‌گن رنگین کمان / rainbow. تکنیک رنگین کمان اینه که تمام کارهای بالا رو بکنیم (یعنی مثلا اگر اکثر افراد در موبایل از پسوردهای عددی استفاده می کنن از ۰ تا ۹۹۹۹۹ رو هش کنیم و رمز و هش اون رو بریزیم توی یک دیتابیس) و بعد یکی یکی هش ها رو توی دیتابیس سرچ کنیم و پسورد رو نشون بدیم… خب آماده‌اید؟

برای ایجاد همه پسوردهای ۰۰۰۰۰ تا ۹۹۹۹۹ و هش کردن اونها این برنامه پایتون رو نوشتم. مطمئنا می‌تونه بهتر هم نوشته بشه ولی این کار من رو راه انداخت:

#!/usr/bin/python

import MySQLdb

db = MySQLdb.connect("localhost","jadi","password","break" )

# prepare a cursor object using cursor() method
cursor = db.cursor()


for i in range(0, 10000):
    pre = '';
    if i < 10:
        pre += '0';
    if i < 100:
        pre += '0';
    if i < 1000:
        pre += '0';
    num = "%s%s" % (pre, i);
    print num;
    # execute SQL query using execute() method.
    cursor.execute("insert into rainbox values ('%s', md5('%s'));"%(num,num));

# Fetch a single row using fetchone() method.
#data = cursor.fetchone()

# disconnect from server
db.close()

همین برنامه رو برای پسوردهای ۰۰۰۰۰ تا ۹۹۹۹۹ و ۰۰۰ تا ۹۹۹ و ۰۰ تا ۹۹ و ۰ تا ۹ هم کم و زیاد کردم و رنگین کمانی شامل ۱۱۱۱۱۰ پسورد و هش اون ساختم:

mysql> select count(*) from rainbox;
+----------+
| count(*) |
+----------+
|   111110 |
+----------+
1 row in set (0.07 sec)

بعد کلیدی روی هش تعریف کردم که سرچ سریعتر بشه:

mysql> ALTER TABLE rainbox ADD primary index (hash);

حالا وقت شکستن رمزها است. فایلی دارم به اسم users.csv که این شکلی است (چهار رقم آخر تلفن ها رو ستاره کردم که پرایوسی آدم‌ها حفظ بشه):

9370333**** ea9bf866d98db73eb0909fa9c1cc1b11
9370050**** fcbfab2f4cda26061ed9e3ee96a4fd61
9370750**** 6a130f1dc6f0c829f874e92e5458dced
9370001**** 9ad97add7f3d9f29cd262159d4540c96

کافیه این رو با لایبری csv پایتون بخونم، و بعد هش هر خط رو از دیتابیسم کوئری بزنم و اگر جواب داشت توی خروجی تلفن و هش و پسورد شکسته شده رو بنویسم و اگر هم جواب این هش توی دیتابیس من نبود، جلوش بنویسم later تا بعدا از یک جای دیگه پیداش کنم.

#!/usr/bin/python

import MySQLdb
import csv

db = MySQLdb.connect("localhost","jadi","password","break" )

# prepare a cursor object using cursor() method
cursor = db.cursor()

spamReader = csv.reader(open('users.csv', 'rb'), delimiter=' ', quotechar='|')
for row in spamReader:
    tofind = row[1];
    try:
        cursor.execute("select pass from rainbox where hash = '%s'"%tofind);
        data = cursor.fetchone()[0];
        print row[0], tofind, data;
    except: #this hash was not in db
        print row[0], tofind, "later" 

# disconnect from server
db.close()

برنامه بالا رو اجرا می‌کنم و زمان می‌گیرم:

jadi@jubun:~/w$ wc -l users.csv && time python break.py > out.txt
316590 users.csv

real    0m57.226s
user    0m25.362s
sys 0m4.856s

واو! سیصد و شونزده هزار پسورد رو توی کمتر از یک دقیقه شکستیم (: یک نگاه به فایل آوت.تکست می‌گه:

jadi@jubun:~/w$ head out.txt 
93703334*** ea9bf866d98db73eb0909fa9c1cc1b11 7523
93700508*** fcbfab2f4cda26061ed9e3ee96a4fd61 8510
93707500*** 6a130f1dc6f0c829f874e92e5458dced 7496
93700013*** 9ad97add7f3d9f29cd262159d4540c96 9538
93700177*** c902514ac30b6e23dbb0c3dc80ec7d4a later
93700858*** ee676ed9ce5bd51b4452ddfbdf962ef7 later
93707848*** 8c249675aea6c3cbd91661bbae767ff1 1986

ظاهرا پسوردهای چهار رقمی مد هستن (: ظاهرا تعداد later ها هم کم نیست. بذارین یک نگاه هم به اونها بندازیم:

jadi@jubun:~/w$ grep later out.txt | wc -l 
1558

خب... شش هزار نفر هستن که پسوردشون شکسته نشده. اما من و شمای هکر می دونیم که شش هزار نفر به معنی شش هزار پسورد نیست. معمولا آدم‌ها پسوردهای تکراری می ذارن. بذارین ببینیم اگر پسوردهای تکراری رو حذف کنیم، چند تا پسورد منحصر به فرد توی این گروه می مونه. دستور cut می تونه یکسری از فیلدهای اضافی رو حذف کنه. مثلا الان کات میکنم بر اساس فیلد ۲ و با جدا کننده اسپیس (بعد از دش دی، اسپیس می‌ذارم). بعد نتیجه قسمت اول که هش ها هستن رو می دم به دستور یونیک (که با سوییچ یو، فقط خط های غیرتکراری رو نشون می ده)‌ و بعد تعداد خطوط رو می شمرم:

jadi@jubun:~/w$ grep later out.txt  > later.txt && \
                                cut later.txt -f2 -d' ' | uniq -u | wc -l 
5536

نتیجه طبیعی نیست... یعنی از شش هزار نفر فقط پونصد ششصد نفر پسورد تکراری داشتن؟ هر کسی که یک مقاله در مورد رفتار پسوردی آدم‌ها خونده باشه می دونه که این عدد غیر واقعی است.. مشکل! قبل از استفاده از دستور یونیک، باید داده‌ها رو مرتب کرد. پس یک سورت بین دستور اضافه می کنم:

jadi@jubun:~/w$ cut later.txt -f2 -d' ' | sort | uniq -u | wc -l 
767

هها! نگفته بودم؟ کل این شش هزار نفر باقیمونده، فقط از ۷۶۷ پسورد استفاده کردن. این ۷۶۷ پسورد رو می‌ریزیم توی یک فایل مجزا برای کشف کردنشون در پست بعدی.

توجه مهم! کمی از این مقاله گم شده! بعله درست خوندین ! به شکل عجیبی بخشی از مقاله در این قسمت غیب شده و نیست (: من این مقاله رو مدت های مدید قبل نوشتم ولی به خاطر احتمال لو دادن اطلاعات مشتری ها و هکربازی بچه اسکریپتی ها ارسالش نکردم. حالا چون آرمین به دردش می خورد گفتم بفرستمش ولی ظاهرا یک بخش کوچیکی اش نیست! مهم هم نیست. فدای سرمون. روش کشف بقیه پسوردها این بوده که بقیه رو از منابع اینترنت صدا بزنیم. یک سایت داریم که می شه براش یک هش فرستاد و پرسید که آیا این جواب رو داره یا نه. من اول یک تست با اون می زنم و بعد یک برنامه می نویسم که همه پسوردهای باقی مونده رو از اون صدا بزنه

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

Found: md5("zaka") = 00a3b206c4ad2cae515e28745423093a

و zaka رو یک جایی ذخیره کنه. کافیه این کار رو برای همه هش‌هایی که هنوز کشف نشدن ادامه بدیم. مشخصه که هنوز یکسری کشف نشده خواهند موند ولی بذارین ببینیم به چی می رسیم. این مرحله به خاطر فرستادن درخواست به اینترنت کندتر است و یک راه حل خوبه اینه که فایل hard.txt و برنامه رو روی یک سرور وی پی اس آپلود کنیم و اونجا اجراش کنیم. همین کار رو می کنم و برنامه پایتون رو هم اینجوری می‌نویسم که یکی یکی هش ها رو برداره، به اون سایت بفرسته و توی خروجی اچ تی ام ال که بر می گرده، خطی که پسورد رو نشون می ده رو جدا کنه. اگر این هش اونجا هم نبود می نویسیم too difficult و از خیرش می گذریم (: بریم ببینیم چی می شه:

#!/usr/bin/python

import urllib
import csv
import re

spamReader = csv.reader(open('hard.txt', 'rb'), delimiter=' ', quotechar='|')
for row in spamReader:
    try:
        # This is here for copy/pasters....
        # Originally by Jadi at jadi.net
        params = urllib.urlencode({'term':row[0], 'crackbtn': 'Crack that hash baby!'})
        f = urllib.urlopen("http://md5crack.com/crackmd5.php", params)
        page = f.read()
        password = re.search('Found: md5\("(.*)"\) = %s' % row[0], page)
        print row[0], password.group(1)
    except: 
        print "too difficult"

و برای اجرا می‌زنیم:

user@remotehost:/tmp$nohup python onlinebreak.py > easy.txt & 

که خروجی پسوردهای شکسته شده رو بریزه توی easy.txt و حتی اگر من هم قطع شدم کار رو در پشت زمینه ادامه بده. تقریبا بعد از یازده دقیقه کار، همه پسوردها با این بانک اطلاعاتی هم چک می‌شن. بعد از اضافه کردن این‌ها به موارد شکسته شده قبلی توی دیتابیس خودمون، یک نگاه می‌ندازیم ببینیم چند تا از پسوردها هنوز باقی موندن.

jadi@jubun:~/w$ time python break.py > out.txt &&  grep later out.txt | wc -l 

real    0m58.863s
user    0m27.706s
sys 0m5.756s
1558

عالی (: حالا دیگه فقط ۱۵۵۸ نفر داریم که پسوردشون کشف نشده. از سیصد هزار نفری که اول داشتیم، پیشرفت عالیی است (: این هزار و پونصد نفر که پسوردهای غیرمعمول گذاشتن هم می‌تونن زنگ بزنن به پشتیبانی و بگن از امروز پسوردشون کار نمی کنه و اونها براشون ریست می کنن (:

نتیجه: از سیصد و خورده‌ای هزار نفر، فقط ۱۵۵۸ نفر پسوردهایی دارن که جزو پسوردهای استانداردی که هر هکری چک می کنه نیست. در پسوردهاتون عدد و حروف کوچیک و بزرگ بذارین و پسوردتون از هشت حرف هم بیشتر باشه. اصلا بذارن این یک دستور دیگه هم بدیم: فایل خروجی رو نشون بده، ستون سومش (پسورد) رو جدا کن (ستون‌ها رو اسپیس از هم جدا می کنه) بعد خروجی رو مرتب کن (پسوردها مرتب می شن) بعد بشمر هر پسوردی چند بار تکرار شده و خروجی و مرتب کن به شکل عددی معکوس و اسپیس های اول خط رو هم بیخیال شو و بعد ده خط اول رو نشون بده.

jadi@jubun:~/w$ cut out.txt -f3 -d' ' | sort | uniq -c | sort -b -n -r | head
 192019 1234
  11780 2222
  10532 123
   9734 1111
   7547 5555
   5293 12345
   4512 4444
   4473 0000
   2568 3333
   2192 444

و این شما و این پر استفاده‌ترین پسوردهای این سیصد هزار نفر. جالبه که ۶۰٪ از کاربران پسوردشون رو گذاشتن ۱۲۳۴۵. فوق العاده نیست؟

  • ۶۰٪ از کاربران پسوردشون رو گذاشتن ۱۲۳۴ البته! D:
    جالب بود

  • sadra

    باید بگم گوه گیجه گرفتم و مقداری افسردگی و کمی هم اعتماد بنفسمو از دست دادم!
    ینی خیلی ضایس من اینارو نمی فهمم؟!

    • جادی

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

  • سلام،

    خیلی عالی و کامل بود جادی، دستت درد نکنه :)

  • داوود قرشی

    جالب بود جادی. مرسی :)

  • ایلیا

    جادی تمام این کارا رو می‌تونستی با ویندوز هم بکنی :) برای عشقت دلایل بهتر پیدا کن!

    • جادی

      ایلیا: توی بخش های دیگه این سری های «چرا گنو/لینوکس رو دوست دارم» بارها اینو گفتم. اینجا دیگه نگفتم چون خودش طولانی بود. لینوکس یک سیستم عامل است و ویندوز و مک و غیره هم سیستم عامل. توی همه می شه اکثر برنامه ها رو نصب کرد و تقریبا هر کاری که با یکی می شه کرد رو با بقیه هم می شه. اما گنو/لینوکس نقش فرهنگی پر رنگی داره و می بینه که اینجور مقالات برای اون نوشته می شن. بحث پرفرمنس و سرعت و غیره هم هست و در نهایت راحتی. من یک دستور تایپ می کنم و همه برنامه های مورد نیاز نصب می شن و سیستم آماده کاره.

      من جزو کسانی هستم که می گم اصلا نباید فکر کنین این چیزها به سیستم عامل ربط داره. یا مثلا اگر فلان سیستم عامل رو استفاده می کنین معنی اش اینه. سیستم عامل ابزاره. استفاده ای که می کنیم مهمه

  • Jadi, the great

  • Narbeh Arakil Jahangiri

    سلام
    خیلی ممنونم جادی جان. مهم اینه که راه حلو پیدا میکنی.

    ایلیا جان میگم مگه توی ویندوز دستور های
    grep later out.txt > later.txt && cut later.txt -f2 -d’ ‘ | uniq -u | wc -l
    یا مثلا
    cut later.txt -f2 -d’ ‘ | sort | uniq -u | wc -l
    وجود داره؟ یا باید بشینیم واسه ۱ خیل کد یه برنامه بنویسیم؟

  • من یه راه دیگه به نظرم اومد
    چرا از خود کاربرها استفاده نکنیم؟
    کافیه صبر کرد تا هر کابر لاگین کنه ، اگر رمز فرستاده شده بعد از MD5 شدن با MD5 پایگاه‌داده ما یکی بود D: خوب رمز رو هم پیدا کردیم!
    مگر اینکه رمزها را یکجا لازم داشته باشیم.

    از نظر تئوری این کار میتونه مشکل داشته باشه ، لزوما تابع MD5 برای هر ورودی ، یک خروجی یکتا تولید نمی‌کند.
    یعنی وجود دارن دو جمله‌ای که MD5 آنها یکی باشد و به این دلیله که MD5 قول داده برای یک جمله همیشه یک عبارت یکسان برگرداند مگرنه مجبور بود ورودی‌هاش رو به تعداد (۱۶به‌توان۳۲) حالت محدود کنه.
    یعنی ممکنه یک رمزی پیدا کنید که رمز کاربر نباشه و هم‌زاد MD5!! رمز کاربر باشد.

    گفتما بحث تئوریه ، فوقش طرف زنگ میزد می‌گفت ریست کنی :دی

    • جادی

      مهرداد: خودت گفتی دیگه توی تئوری (: توی زندگی واقعی تقریبا غیر ممکنه دو تا چیز ام دی فایو مشابه تولید کنند. اما در مورد بخش اولت خیلی ایده خوب و هوشمندانه ای است (: کیف کردم. در کاربرد عملی نمی شد استفاده کرد چون یک جا همه رمزها رو می خواستن که ایمپورت کنن توی بانک جدید (که با ام دی فایو کار نمی کرد) ولی در سطح ایده واقعا عالیه فکری که کردی. کلی لذت بخش بود.

      • ممنون جادی عزیز
        خوبه که کیف کردی ، تلافی کیفی که من از بلاگت کردم :)

  • احسان دیاری

    بسیار عالی :-)

  • voltan

    کار خیلی جالبی بود :دی

    یادمه چند ماه پیش داشتیم روی اقال یه سایت نیمه دولتی که حدود ۵۰۰۰۰ تا کاربر داشت کار میکردیم و قرار بود پسورد ها رو یه جوری به دست بیاریم. قبل از اینکه دیتابیس رو به ما بدن ۳ تا جلسه داشتیم در مورد اینکه توجیح بشیم امنیت اطلاعاتشون و پسورد هاشون و … چقدر مهمه و از این حرف ها.

    نتیجه اینکه وقتی دیتابیس رو گرفتیم دیدیم برنامه نویس محترم قبلی یه فیلد تو جدول یوزر ها گذاشته بود و پسورد های MD5 نشده رو اونجا ذخیره کرده بود. :دی

  • واسه همینه که اینقدر اصرار میشه پسورد رو بدون salt هش نکنید!

  • این مقاله درسته …ولی ماهمینجوری که به یه دیتابیس دسترسی نداریم که بشینیم و کرک کنیم. این مقاله به مامیگه،افرادی هستندکه پسوردهای خیلی ضعیف انتخاب می کنن! پس اونایی که پسورد ضعیف به کاربردن فکرنکنن که کسی اراده کنه تو اون زمانهایی که تو مقاله اومده می تونه پسورد اونارو کرک کنه! اول باید به دیتابیس دسترسی داشته باشه.

  • کامیار آینانلو

    خوب نوشتی آورین

  • مسعود

    ممنونم ازت جادی عزیز :)

  • مرسی بابت اطلاعات مفیدی که ارائه کردی. البته دو تا نکته هست که خواننده باید در این زمینه دقت کنه:

    Hash یه الگوریتم یه طرفه است و لزوما یه رابطه یک به یک نیست، پس ممکنه بعضی از این یافته ها درست نباشه. ممکنه یه کاربر به صورت کاملا تصادفی رمزی داشته باشه که با رشته ۱۲۳۴۵ یه hash واحد درست میکنن. در این مورد بخونین:
    en.wikipedia.org/wiki/Birthday_attack
    en.wikipedia.org/wiki/Collision_attack
    پس قاعدتا این راه برای انتقال رمز کاربران به یه سیستم جدید توصیه نمیشه.

    حالت ایده آل اینه که سیستم بتونه از طریق یه Custom PAM Module (یا مکانیزم مشابه) از طریق رمز قبلی تایید هویت کنه و رمز رو دوباره با مکانیزم جدید hash کنه و توی سیستم ذخیره کنه.

    هومن

  • جالبه اتفاقا همین دیروز داشتم یه مطلب می‌خوندم در مورد این که یه رمز به صورت یه عبارت طولانی (بدون این که خیلی بخوای توش کاراکترهای عجیب غریب و عدد و … استفاده کنی) که یادآوریش راحته از یه رمز کوچیک و مشکل خیلی بهتره! در همین راستا:
    http://xkcd.com/936/

  • khikho

    راه حل جالبیه اما من اگه بودم md5 کاربرا رو با هر الگوریتم دیگه ای که میخواستم Hash میکردم (دو بار hash میکردم) بعد تو login برنامه جدید میگفتم اول رمز رو به MD5 و نتیجه رو دوباره به الگویی که مد نظرم هست Hash کن.
    (میدونم با این کار هیچ کدوم از این کارهای قشنگ رو نمیشد انجام داد و تمام بار رو مینداختیم رو سیستم)
    ******البته اگر سوال رو درست متوجه شده باشم*****

    • جادی

      خیخو(؟): راه حل خیلی هوشمندانه ای است راهت. در این مورد خاص نمی شد چون برنامه دوم از قدیم نوشته شده. در کاربری روزمره هم به نظرم اون باعث پیچیدگی ای می شه که دو سال بعد هیچکس دلیلش رو نمی دونه و ناراحته از پیچیدگی ولی از نظر هوش و ابتکار خیلی عالیه (: کلی خوشم اومد.

  • امین

    واقعا عالی بود. تنها چیزی که میتونم بگم اینه که واقعا از خوندن این پست لذت بردم.. ممنون جادی عزیز! :x

  • فرزاد

    باحال نوشتی ولی اصولن باید از «نمک» استفاده کرد تا نشه اینطوری با بروت فورس پسوردها رو کشید بیرون :>

  • siavash

    ما نیز بسی لذت بردیم.
    سپاس جادی

  • ناشناس

    من نفهمیدم چرا باید خود پسورد ها رو به دست آورد؟
    چون سیستم اول و دوم از الگوریتم های رمزنگاری متفاوتی استفاده کردن؟

    • جادی

      کاملا درسته ناشناس. به همین دلیل

  • Pingback: چرا راه سخت‌تر را انتخاب می‌کنیم‌؟ | GilAsus()

  • Pingback: چرا راه سخت‌تر را انتخاب می‌کنیم‌؟()