ابزار man in the middle، ماک، هک و تست نرم افزار

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

احتمالا در مورد حمله مرد میانی یا Man In The Middle (MITM) Attack می‌دونید. اینکه یکی بتونه اطلاعات رد و بدل شده بین دو نقطه مختلف رو بشنوه یا حتی در اونها تغییر ایجاد کنه. فکر می‌کنم نیازی به تاکید بر میزان خطرناک بودن و پیچیدگی حفاظت در مقابل این نوع حملات نیست:

عکس از اینجا
عکس از اینجا

روشهای زیادی برای جلوگیری از این حمله‌ها وجود دارن. احتمالا مهم‌ترین اونها رمزنگاری SSL/TLS هست که ما برای غیرقابل فهم کردن اطلاعات‌مون در اینترنت و بر روی پروتوکل HTTP استفاده می‌کنیم (همون HTTPS). خود SSL خیلی روش ساده و جالبی هست و این سادگی در روشی که امنیت بخش بزرگی از دنیای دیجیتال رو بر عهده داره نشان از نبوغ به کار رفته در طراحی اون داره. نوشتن در مورد استک HTTPS خیلی طولانیه و فعلا بهتره ازش بگذریم. امیدوارم یه روز فرصت بشه در موردش یک مطلب مناسب تولید کنم.

توی این پست به نوعی حمله MITM اشاره میشه که توسط یک پروکسی‌ قابل انجام هست و این نرم افزار مخصوص این کار (ولی نه لزوما برای خرابکاری) نوشته شده و به این مساله هم جلوتر اشاره میشه که چطوری حتی HTTPS رو دور می‌زنیم و اطلاعاتش رو می‌خونیم.

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

بزارید از اینجا شروع کنم: توی تست نرم‌افزار، یک مفهوم مهمی وجود داره به اسم Mocking. نرم افزار شما از کلی اجزای کوچیک (مثل تابع ها و API ها) تشکیل شده که با هم در ارتباط هستن و در این شرایط کافیه یکی از این توابع پایه درست کار نکنه تا همه اجزایی که کارایی شون به اون وابسته هست با مشکل مواجه بشن و نتایج کل مجموعه تست‌هاتون به یک چیز غیر قابل استفاده تبدیل بشه (چون فقط میگه که هیچ چیزی توی نرم‌افزار کار نمی‌کنه ولی نمیگه مشکل از کجاست). فرض کنید شما دو تا تابع A و B دارید که B با فراخوانی A میتونه وظیفه اش رو انجام بده، یعنی اگر A درست کار نکنه، B هم درست کار نخواهد کرد. خب الان شما یک تست برای تابع B نوشتید و تست درست اجرا نمیشه. این خطا نشون میده B درست کار نمیکنه؟ یا شاید مشکل از A هست؟

ماک کردن در این مواقع خیلی به درد میخوره. شما موقع نوشتن تست برای تابع B یک تابع ماک‌ می‌نویسید که رفتار A رو تقلید می‌کنه، ولی این تابع فقط یک تقلیدگر هست و واقعا اون کار رو انجام نمیده. مثلا تابع A قراره به یک جایی تو شبکه وصل بشه و یک تغییراتی روی سرور دوم ایجاد کنه. حالا اگر اون سرور مشکل داشته باشه، یا شبکه ارتباطی به مشکل بخوره، یا هر چیز دیگه، نه تنها تست‌های تابع A، که تست‌های تابع B هم مشکل پیدا می‌کنن.

یه راه حل اینه که وقتی می‌خوایم کارکرد تابع B رو چک کنیم، تابع A رو ماک کنیم و هر وقت B به تابع A تقلبی گفت به فلان دیتابیس وصل شو و این کار رو بکن، تابع A تقلبی هم بلافاصله بگه باشه انجامش دادم و تموم شد، این هم خروجی کار (یک خروجی تقلبی نشون بده). اینطوری حتی اگه تابع A واقعی هم درست کار نکنه، تابع B تست‌ها رو به سلامت می‌گذرونه.

ماک کردن در unit test ها خیلی معمول هست و اکثر فریم‌ورک‌هاش ابزارهایی برای ماک کردن توابع و بخش‌های مختلف نرم‌افزار دارن. در تست‌های End-to-End ما معمولا نمی‌خوایم سیستم رو ماک کنیم (به همین خاطر بهش میگن End-to-End) ولی گاهی نیاز میشه که یه چیزهایی رو از سناریو تست‌مون کنار بزاریم (اگه این اصطلاح ها براتون جدیدن می‌تونید به پست‌های اول من نگاهی بندازید، اینها رو اونجا هم توضیح دادم). مثلا شما در نرم افزارتون تابعی دارید که زمان طولانی برای اجراش لازمه، ولی می‌خواید کنترل‌های مربوط به عملکرد اون در رابط گرافیکی رو (فقط رابط گرافیکی) چک کنید که درست کار می‌کنن و فرامین رو به درستی فعال می‌کنن یا نه.

یه مثال دیگه: توی تست فرانت‌اند شما نمی‌خواید کل سیستم برای هر کلیک که در رابط گرافیکی انجام می‌دید، بره و همه کارهای مرتبط با اون کلیک رو روی سرور انجام بده. اگر از ابزارهایی مثل Cypress استفاده می‌کنید که برای تست فرانت‌اند به کار میرن و می‌تونن درخواستهای HTTP رو توی مرورگر ماک کنن، این مشکل براتون قابل حل هست، ولی اگر از چیزهایی مثل Selenium استفاده می‌کنید، راه حل های موجود برای این کار یه کم پیچیده‌تر خواهند بود.

یک راه حل برای این کار استفاده از ابزار‌های حمله مرد میانی هست. من از mitm-proxy برای این کار استفاده می‌کنم. توی این روش mitmproxy یه تونل برای شما باز می‌کنه که با تنظیم مرورگر برای استفاده از اون تونل، همه اطلاعات بین سرور و مرورگر از زیر دست این نرم افزار رد میشن. این نرم افزار چند محیط برای کار داره، شما می‌تونید توی محیط گرافیکی به شکل دستی به request ها نگاه کنید و اگر نیاز بود تغییرشون بدید، از طریق خط فرمان نحوه دستکاری رو کنترل کنید و از افزونه‌هایی که نرم‌افزار داره استفاده کنید، یا اینکه یک کد ساده پایتون بنویسید که اتوماتیک اجرا بشه.

رابط وب نرم افزار  (mimtweb)
رابط وب نرم افزار (mimtweb)

کدهای mitm-proxy دو تابع مهم داره: یکی که وقتی request فرستاده شده به پروکسی رسید فعال میشه و چک میکنه ببینه آیا می‌خواد دخالت کنه و جواب تقلبی رو خودش رو برای این درخواست برگردونه؟ (دقت کنید که سرور اصلا از ارسال این درخواست باخبر نمیشه)، و یکی هم وقتی که درخواست به سرور رسیده، سرور response رو تولید کرده و حالا نرم‌افزار می‌تونه جواب در حال برگشتن رو بخونه و قبل از رسیدن به دست کلاینت در اون تغییر ایجاد کنه. به همین سادگی :)

from mitmproxy import ctx, http
def request(flow: http.HTTPFlow) -> None:
    if flow.request.pretty_url == &quothttp://example.com/path/to/api&quot:
        flow.response = http.Response.make(
            200,  # status code
            b&quotOK, sure! ψ(`∇´)ψ&quot,  # content
            {&quotContent-Type&quot: &quottext/html&quot},  # headers
        )
def response(flow: http.HTTPFlow) -> None:
    if flow.request.pretty_url == &quothttp://example.com/path/to/the/api&quot:
        response_body = flow.response.get_text() # get server response
        flow.response.text =  b&quot😈 ΔΜØŇǤ ỮŞ&quot # modify response content

حالا شما می‌تونید با کمی خلاقیت و با استفاده از انعطاف پذیری بالایی که این فریم‌ورک در اختیارتون قرار داده، کدی بنویسید که به mitm-proxy بگید که در حین انجام هر سناریو، در مقابل چه دسته از درخواستهایی که ارسال میشن چه رفتاری رو نشون بده. مثلا اگه یه REST API دارید که میگه برو یک کار محاسباتی سنگین رو انجام بده و برگرد، و شما در چندین سناریو تست مختلف قراره به این API درخواست‌های مشابه بفرستید، حالا لزوما نیاز نیست تو همه این دفعات، API واقعی رو فعال کنید و کلی وقت برای هر بار تست صرف کنید.

برای اینکه این پست طولانی‌ نشه، چند تا نکته رو هم به طور خلاصه اشاره می‌کنم و می‌رم به بخش بعد:

  • تنها راه فرستادن درخواستها به mitm-proxy فقط تنظیم پروکسی روی نرم افزار نیست، شما میتونید از این فریم‌ورک به عنوان پروکسی transparent هم استفاده کنید (اگر نرم افزارتون می‌تونه به جای فرستادن درخواست‌ها به آدرس سرور پیش‌فرض‌اش، از یک آدرس ثانویه استفاده کنه و درخواست‌ها رو خودش به آدرس پروکسی بفرسته).
  • پروکسی transparent توی یک حالت دیگه هم کاربرد داره: نرم افزار شما قابلیتی که بالا اشاره شد رو ساپورت نمی‌کنه و شما می‌تونید این کار رو تو سطح سیستم عامل انجام بدید و مسیردهی iptable رو تغییر بدید و یک port forward روی سیستم‌تون تعریف کنید (این کار توی ویندوز و یونیکس ممکن هست ولی من تابحال روی ویندوز امتحانش نکردم).
  • این نرم‌افزار mode های دیگه هم داره ولی برای موارد خیلی خاص‌تر استفاده میشن. دو مورد بالا هم یه کم استفاده‌های پیچیده‌ای هستن و شاید خیلی برای شروع مناسب نباشن. توصیه می‌کنم همون روش پروکسی عادی توضیح داده شده رو همیشه امتحان کنید، چون اون روش به کمترین تغییرات در خارج از مجموعه کدهای تست شما نیاز داره و از این لحاظ خیلی نسبت به روش‌های دیگه اولویت داره و اگر با این روش تونستید کار رو پیش ببرید، بهترین انتخاب برای انجام تست خواهد بود.
  • با نصب این نرم‌افزار، یک دستور دیگه برای شما در دسترس خواهد بود به اسم mitmdump که میتونید برای ذخیره کردن اطلاعات همه درخواست‌های HTTP رد و بدل شده استفاده کنید که امکانات خوبی هم داره (ولی من به شخصه روش‌های قدیمی مثل tcpdump و wireshark رو ترجیح میدم، هر چند که این دستور امکانات جالب زیادی در اختیار قرار میده).
  • یک مورد هم اینکه من چند جا دیدم در مورد کند بودن این نرم‌افزار صحبت شده و وقتی حجم ترافیک دیتایی که ازش میگذره زیاد باشه، راه حل خیلی مناسبی برای استفاده نیست. اما این کند بودن در انجام تست‌ها خودش رو نشون نمیده چون حجم دیتا معمولا کم هست. ولی اگر خواستید از mitm-proxy برای جاهای دیگه استفاده کنید، این نکته رو هم به یاد داشته باشید.

حالا می‌رسیم به بخش HTTPS و امنیت

فرض میشه که HTTPS میتونه امنیت اطلاعات ما رو برامون برقرار کنه و جلوی این رو که کسی اطلاعات ما رو تو میانه راه بخونه یا حتی عوض‌شون کنه رو میگیره. SSL\TLS از الگوریتم RSA استفاده میکنه که یک الگوریتم رمزنگاری نامتقارن بوده و یک جفت کلید (کلید عمومی و خصوصی) داره و برای رمزگشایی از اطلاعات کلید عمومی به کلید خصوصی نیاز هست و برعکس.

مثلا وقتی داریم با google.com ارتباط برقرار می‌کنیم، الگوریتم RSA به ما اجازه میده با داشتن یک کلید عمومی که همه بهش دسترسی دارن با گوگل ارتباطی برقرار کنیم و هیچ کسی به جز گوگل نتونه اطلاعات رمزنگاری شده ما رو بخونه.

ولی این روش هم همچنان با حمله مرد میانی آسیب پذیره. مرد میانی‌ای که خودش رو گوگل معرفی می‌کنه، وسط راه میشینه و کلید عمومی خودش رو برای من می‌فرسته و با داشتن کلید خصوصی‌ پیش خودش، می‌تونه اطلاعات من رو رمزگشایی کنه و تغییر بده و بفرسته برای گوگل. راه حلی که برای این مشکل استفاده میشه اینه که ما بتونیم یه جوری تشخیص بدیم این کلید متعلق به google.com هست و آیا میشه به کلید برای رمزنگاری اعتماد کرد؟ یا کلید خصوصی اش ممکنه دست یکی دیگه به غیر از گوگل باشه؟

طبیعتا یه راه حل اینه که یه نشانه‌ای توی کلید باشه که بتونیم از روش بفهمیم کلید متعلق به گوگل هست. ولی این کار رو که نمیشه برای تک‌ تک سایت‌های روی اینترنت انجام داد. پس چیکار کنیم؟ یه دسته کلید از مرجع صلاحیت‌های اصلی قابل اعتماد توسط همه توی اینترنت رو توی نرم‌افزارمون میزاریم و هر کلیدی که نشانه اون مرجع صلاحیت یا (CA)Certificate Authority رو با خودش داشته باشه برای ما قابل اعتماد خواهد بود. این روش هم مختص رابطه مستقیم یک CA اصلی با اون سایت نیست و این کار میتونه شامل یک زنجیره اعتماد از CA های مختلف بشه که نهایتا CA آخری، کلید گوگل رو تایید میکنه (مثلا اینجا رو نگاه کنید) و فقط کافیه ما به سر اون زنجیره اعتماد داشته باشیم تا به کل زنجیره اعتماد کنیم.
تا اینجای کار به چی رسیدیم؟ HTTPS می‌تونه به ما اطمینان بده که اگه به سرور گوگل وصل شدیم و داریم باهاش حرف میزنیم، محتوای صفحات رو کسی نمیتونه وسط راه ببینه یا دستکاری کنه. ولی به شرط اینکه مطمئن باشیم از اول با گوگل در ارتباط بودیم و کلید رمزنگاری رو از اون گرفتیم. برای اطمینان از این قضیه هم CA ها بهمون کمک می‌کنن تا مطمئن باشیم کلید امضا شده توسط اونا متعلق به سایت گوگل هست.

خب تا اینجا همه چی امنه و ما نمیتونیم اطلاعات بین سرور و کلاینت رو بخونیم. پس توی این حالت چطوری می‌تونیم حمله مرد میانی رو با پروکسی‌مون روی HTTPS انجام بدیم؟ تقریبا تمام سیستم‌عامل‌ها و مرورگرها این امکان رو دارن که کلید CA جدیدی بهشون اضافه کنید و بگید که من به این CA اعتماد دارم (کنترل در سطح Secure Socket یا SSL معمولا با سیستم عامل هست، ولی بعضی نرم‌افزارها مثل مرورگرها این کا رو خودشون انجام میدن تا از ارتباط امن‌شون مطمئن باشن). این CA جدید کدوم هست؟ تو مثال ما، این CA چیزی نیست به جز mitm-proxy که:

  • نقش مرد میانی رو برای خوندن اطلاعات رو بازی می‌کنه
  • کلید تقلبی خودش رو استفاده می‌کنه تا برنامه‌ها و مرورگر‌ها رو به اشتباه بندازه
  • ما اون رو به عنوان CA قابل اعتماد به نرم افزار معرفی کردیم و چون خودش کلید خودش رو امضا می‌کنه، نرم‌‌افزارها هم به کلیدش اعتماد می‌کنن

با این روش شما می‌تونید اطلاعات رد و بدل شده در زمان توسعه یا تست نرم افزار رو حتی اگر اون نرم‌افزار از HTTPS استفاده می‌کنه ببینید یا دستکاری کنید.

فقط یک نکته باقی می‌مونه، ما که تا اینجا با حملات مرد میانی راه اومدیم، باید متوجه باشیم که چون اغلب نرم‌افزارها این ایده رو دارن که HTTPS امنه و بهش اعتماد می‌کنن، ما می‌تونیم با استفاده از نرم افزار و مانیتور کردن اطلاعات در شبکه و استفاده از CA تقلبی خودمون، کارهای زیادی روی نرم‌افزارها انجام بدیم و اونا هم واقعا فکر کنن با سرور خود نرم افزار در ارتباط هستن! مثلا هک کردن اپ‌های اندروید و دستکاری اطلاعاتی که بین سرور و اپلیکیشن رد و بدل میشه، می‌تونه اپلیکیشن و سرور رو به خطا بندازه، چون سرور مطمئنه اون چیزی که کلاینت میگه اطلاعات واقعیه و کلاینت هم همین ایده رو در مورد سرور داره. یک ایده ساده برای هک بعضی نرم‌افزارها ;)