بعد از یک مدت طولانی دوباره برگشتم تا پست جدید بزارم و تکونی به اینجا بدم. اول از همه این رو بگم که هدف من نوشتن وبلاگ فقط در مورد تست نرم افزار نیست، ولی این مطلب جدید هم که در پیشنویسهای وبلاگ مونده بود، مرتبط با تست خودکار نرمافزار هست که داره منتشر میشه.
احتمالا در مورد حمله مرد میانی یا 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 ها نگاه کنید و اگر نیاز بود تغییرشون بدید، از طریق خط فرمان نحوه دستکاری رو کنترل کنید و از افزونههایی که نرمافزار داره استفاده کنید، یا اینکه یک کد ساده پایتون بنویسید که اتوماتیک اجرا بشه.
کدهای mitm-proxy دو تابع مهم داره: یکی که وقتی request فرستاده شده به پروکسی رسید فعال میشه و چک میکنه ببینه آیا میخواد دخالت کنه و جواب تقلبی رو خودش رو برای این درخواست برگردونه؟ (دقت کنید که سرور اصلا از ارسال این درخواست باخبر نمیشه)، و یکی هم وقتی که درخواست به سرور رسیده، سرور response رو تولید کرده و حالا نرمافزار میتونه جواب در حال برگشتن رو بخونه و قبل از رسیدن به دست کلاینت در اون تغییر ایجاد کنه. به همین سادگی :)
from mitmproxy import ctx, http
def request(flow: http.HTTPFlow) -> None:
if flow.request.pretty_url == "http://example.com/path/to/api":
flow.response = http.Response.make(
200, # status code
b"OK, sure! ψ(`∇´)ψ", # content
{"Content-Type": "text/html"}, # headers
)
def response(flow: http.HTTPFlow) -> None: if flow.request.pretty_url == "http://example.com/path/to/the/api":
response_body = flow.response.get_text() # get server response
flow.response.text = b"? ΔΜØŇǤ ỮŞ" # modify response content
حالا شما میتونید با کمی خلاقیت و با استفاده از انعطاف پذیری بالایی که این فریمورک در اختیارتون قرار داده، کدی بنویسید که به mitm-proxy بگید که در حین انجام هر سناریو، در مقابل چه دسته از درخواستهایی که ارسال میشن چه رفتاری رو نشون بده. مثلا اگه یه REST API دارید که میگه برو یک کار محاسباتی سنگین رو انجام بده و برگرد، و شما در چندین سناریو تست مختلف قراره به این API درخواستهای مشابه بفرستید، حالا لزوما نیاز نیست تو همه این دفعات، API واقعی رو فعال کنید و کلی وقت برای هر بار تست صرف کنید.
برای اینکه این پست طولانی نشه، چند تا نکته رو هم به طور خلاصه اشاره میکنم و میرم به بخش بعد:
فرض میشه که 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 که:
با این روش شما میتونید اطلاعات رد و بدل شده در زمان توسعه یا تست نرم افزار رو حتی اگر اون نرمافزار از HTTPS استفاده میکنه ببینید یا دستکاری کنید.
فقط یک نکته باقی میمونه، ما که تا اینجا با حملات مرد میانی راه اومدیم، باید متوجه باشیم که چون اغلب نرمافزارها این ایده رو دارن که HTTPS امنه و بهش اعتماد میکنن، ما میتونیم با استفاده از نرم افزار و مانیتور کردن اطلاعات در شبکه و استفاده از CA تقلبی خودمون، کارهای زیادی روی نرمافزارها انجام بدیم و اونا هم واقعا فکر کنن با سرور خود نرم افزار در ارتباط هستن! مثلا هک کردن اپهای اندروید و دستکاری اطلاعاتی که بین سرور و اپلیکیشن رد و بدل میشه، میتونه اپلیکیشن و سرور رو به خطا بندازه، چون سرور مطمئنه اون چیزی که کلاینت میگه اطلاعات واقعیه و کلاینت هم همین ایده رو در مورد سرور داره. یک ایده ساده برای هک بعضی نرمافزارها ;)