توی این پست قراره در خصوص نحوه کرک اپلیکیشنهای اندرویدی، چالشهای Bypass مکانیزمهای امنیتی و در کل هر چیزی که به Pentest و Reverse engineering اپهای اندرویدی مربوط میشه صحبت کنم.
حدود 3 سال پیش یه دوره ویدیویی 10 قسمته با موضوع "کرک اپلیکیشنهای اندرویدی" گذاشته بودم یوتیوب، آپارات و کانال تلگرام، که البته اونقدری که انتظارشو داشتم دیده نشد. (الان که چک کردم تا این لحظه در مجموع 3259 بار دیده شده)
در کل بازخورد خوبی از اون دوره گرفتم (هر کس دیده راضی بوده 😁)، سوالاتی هم که پرسیده میشد معمولا یکی از اینا بود:
طبیعتا دلیل این پست که البته قراره حدود 11-10 پست بشه، دسته سوم میشن و در کنارش دلایل زیر:
دلیل اینکه اپهای بانکی و پرداخت رو زیاد مثال میزنم، چون بالاترین امنیت رو باید این دسته از اپها داشته باشن.
اگه برنامهنویس اندروید هستین و یا در زمینه امنیت فعالیت دارین، این رشته پستها احتمالا براتون جالب باشه. هدف از این رشته پستها اینه که تا حدودی اوضاع مواردی که بالا اشاره کردم رو بهتر کنه...
توی این رشته پستها (که این میشه پست اول)، فرض شده که شما با مقدمات برنامهنویسی و امنیت اندروید آشنا هستین، البته من بازم تا جایی که پست خیلی طولانی نشه توضیح میدم موضوعات رو، ولی در کل پیشنیازهایی که لازم داریم ایناست:
این نکته هم در نظر بگیرین که در حال حاضر معماری اکثر دیوایسهای اندروید و IOSی ARM هست که یکمی syntax متفاوتی با x86 داره (ARM راحتتره)، ولی اگه از روی علاقه شخصی تصمیم به یادگیری Assembly گرفتین، پیشنهادم اینه که بر اساس X86 برین جلو چون باعث میشه درک کاملتری از پردازندهها داشته باشین و بعد ARM براتون در حد بازی به حساب میاد.
همونطور که میدونین یکی از چالشهایی که در فرایند تحلیل اپلیکیشنهای اندرویدی، Reverse و Pentestشون داریم، وجود الگوریتمهای امنیتیایه که هدف از بکارگیریشون این بوده که کارو برای ما (شخصی که میخواد Reverse یا Pentest کنه) سخت کنن و یکی از اصلیترین چالشهای ما هم Bypass کردن همین الگوریتمهاست.
اگه با این الگوریتمها آشنا نیستین پست زیر میتونه براتون مفید باشه:
البته پست زیر برای 2-3 سال پیشه و یکم نیاز به ویرایش داره، ولی خب بازم برای یادگیری کلیات خوبه.
مثلا فرض کنین میخوایم ترافیک Network اپلیکیشن رو بگیریم ولی نتونیم، یا ترافیک رو گرفتیم ولی به صورت Encrypt شده ارسال/دریافت میشه و محدودیتهای این مدلی دیگه، خب چکار کنیم؟ دو تا روش کلی داره:
خیلی خلاصهش اینجوری میشه که در فرایند Hook، به جای اینکه مستقیم فایل اپلیکیشن را تغییر بدیم (کاری که در Patch یا کرک انجام میدادیم)، بخشی از کد که میخوایم تغییر کنه رو در لحظه اجرا و از روی Memory تغییر میدیم. مثلا فرض کنید یک فانکشن تعریف شده که بررسی میکنه کاربر کلمه عبور رو صحیح وارد کرده یا نه. در حالت پیشفرض این فانکشن باید فقط در صورتی اجازه لاگین بده که کلمه عبور صحیح وارد بشه، ما با Hook کردن میایم این فانکشن رو (در لحظه اجرا) تغییر میدیم که در هر صورت اجازه لاگین بده.
پس هوک کردن یه جورایی مثل همون Patch کردن میمونه، با این تفاوت که تغییر در لحظه اجرا انجام میشه و چیزی از روی فایل تغییر نمیکنه.
اگه براتون سواله که Patch کردن بهتره یا Hook کردن، بستگی به پروژه و رفتاری داره که میخوایم Bypassش کنیم، خیلی وقتا هست که بخشی از کار رو با Patch کردن انجام میدیم بخشی دیگه رو با Hook (ولی خب روی هم رفته در فرایند Reverse و Pentest از Hook معمولا بیشتر استفاده میشه)
برای آشنا شدن با Hook شاید پست زیر هم براتون جالب باشه:
راستی اینم بگم که برای Hook کردن اپهای اندرویدی، از ابزارهای دیگهای هم میشه استفاده کرد، مثلا:
-- استفاده از Objection (خودش از Frida استفاده میکنه، در واقع یکسری اسکریپت آماده داره که فرایند هوک رو خیلی سادهتر میکنه، ولی طبیعتا همیشه جواب نمیده)
-- استفاده از r2frida (اینم همونطور که مشخصه از Frida استفاده میکنه، در واقع میاد Frida و Radere2 رو ترکیب میکنه، ابزار خیلی خوبیه، با این کار میکنیم جلوتر)
-- استفاده از Xposed Framework (اینم ابزار بدی نیست، قدیما خیلی ازش استفاده میشد، ولی اصلا در حد Frida نیست، یکسری مشکلات داره مثلا با نسخههای جدید اندروید مشکل داره، برای کار با Native Code خیلی محدودیت داره، فقط با دیوایسهای روت شده کار میکنه و...)
پس تا اینجای کار رسیدیم به این که یکی از روشهای Bypass مکانیزمهای امنیتی اینه که اون بخشی از کد که باعث محدودیت شده رو hook کنیم و در حال حاضر Frida بهترین ابزاریه که برای این منظور استفاده میشه.
در واقع Frida اول میاد با استفاده از ptrace به Processی که میخوایم (اپلیکیشنی که میخوایم Hookش کنیم)، Attach میشه، بعدش Gadget خودش رو که برای اندروید یک فایل باینری به اسم libfrida-gadget.so هست رو بهش Inject میکنه، مرحله آخر هم میاد با V8 اسکریپتی که بهش دادیم رو اجرا میکنه.
معمولا تو این حالت الگوریتمی که استفاده شده همون الگوریتمهای Publicه، فقط اسم کلاس و توابع تغییر داده شده و بنابراین برای Bypass هم تغییری که نیازه روی اسکریپتهای عمومی انجام بدیم خیلی جزئیه.
شاید این سوال پیش بیاد که چرا همیشه از روش سوم استفاده نکنیم که خیالمون راحت باشه هر الگوریتمی رو میتونیم Bypass کنیم؟ جواب اینه که واقعا لازم نیست. وقتی اپلیکیشن اومده از کتابخونه و روشهای عمومی استفاده کرده و میشه راحت با اسکریپت عمومی Bypassش کرد، چرا بخوایم وقتمون رو هدر بدیم اسکریپت اختصاصی بنویسیم!؟ پس هر چیزی به جای خودش و در صورت نیاز.
زمانی که از Hook کردن اپلیکیشنهای اندرویدی صحبت میکنیم، معمولا با یکی از این دو مورد سر و کار داریم:
مورد اول که مشخصه، دومی هم یعنی کدی که میخوایم Hookش کنیم، با C یا ++C نوشته شده.
حالا که صحبت از Native Methods شد، اینم در نظر بگیرین که اکثر اسکریپتهای آمادهی Frida برای Java Methods نوشته شدن و اگه اپلیکیشن و یا اون بخشی که میخوایم Hookش کنیم با C یا ++C پیادهسازی شده باشه، دیگه جواب نمیدن.
شاید براتون سوال پیش بیاد الان دیگه کی میاد با C یا ++C اپلیکیشن اندرویدی بزنه!؟
سوال دیگهای که ممکنه براتون پیش بیاد اینه که چرا Reverse کردن C و ++C نسبت به زبانی مثل Java و Kotlin سختتره؟
همونطور که میدونید خیلی از زبانها مثل Java، Kotlin و #C، در زمان کامپایل به زبان میانی (Intermediate Language) تبدیل و بعد در زمان اجرا به زبان ماشین ترجمه میشن، ولی زبانهایی مثل C و ++C همون مرحلهی کامپایل به زبان ماشین (01010110011) تبدیل میشن. یکی از دلایلی که Performance در زبانهای سطح پایین بیشتره هم همینه، شما با اسمبلی کد بزنین سرعتتون خیلی بیشتره تا حتی با C بزنین، در همین خصوص شاید این لینک براتون جالب باشه:
افزایش 94 برابری عملکرد FFMpeg با استفاده از کدهای اسمبلی
حالا این تفاوت چه ربطی به فرآیند Reverse Engineering داره؟ وقتی اپلیکیشنی با زبانی مثل Java زده شده، خروجیای که داریم به زبان میانی هست که این باعث میشه Decompile کردن و برگردوندنش به کد سطح بالاتر خیلی ساده باشه. ولی برخلافش در زبانهایی مثل C و ++C، از اونجایی که همون اول کار بعد از کامپایل، خروجی به زبان ماشین (باینری) تبدیل میشه، اگه بخوایم برش گردونیم به زبان سطح بالا اول باید با استفاده از یک Disassembler به اسمبلی تبدیل بشه بعدش با یک Decompiler تا جای امکان به کد سطح بالاتر (شبیه به C)، که همین باعث میشه Reverse کردن فایلهای باینری پیچیدگیهای خاص خودش رو داشته باشه.
من برای این پست و احتمالا 3-4 پست بعدی، از Sampleهای MASTG استفاده میکنم. خوبی این Sampleها اینه که بهطور خاص به هدف آموزش و آشنایی با مفاهیم امنیت طراحی شدن، تو بیشتر کتاب و آموزشهای مرتبط با امنیت اندروید هم از همین نمونهها استفاده شده، ما هم فعلا با همین نمونهها میریم جلو...
1- اول فایل اپلیکیشن رو از لینک زیر دانلود میکنیم:
https://github.com/OWASP/owasp-mastg/blob/master/Crackmes/Android/Level_02/UnCrackable-Level2.apk
2- اپلیکیشن رو توی Emulator نصب و اجرا میکنیم. (بعدا یه پست در خصوص نحوه راهاندازی اصولی Emulator برای تحلیل بدافزارهای اندرویدی آماده میکنم و در خصوص موضوعاتی مثل انتخاب شبیهساز مناسب، ایزوله کردن شبیهساز، ابزارهای تحلیل، شبیهسازی اینترنت، Snapshot و .. صحبت میکنم. ولی فعلا اینجا تو این مثال چیزی که روش کار میکنیم بدافزار نیست و تنها چیزی که لازم داریم اینه که روی هر Emulatorی که باهاش راحتترین نصب و اجراش کنین)
3- همونطور که میبینین اپلیکیشن Root detection داره و اجازه بالا اومدن سر دیوایس روت شده رو نمیده، بریم فعلا این بخش رو Bypass کنیم:
4- اپلیکیشن رو با JADX دیکامپایل میکنیم و بخشی از متن Alert Dialog بالا رو توی کدهای استخراج شده سرچ میکنیم (با هدف پیدا کردن بخشی از کد که وظیفه بررسی Root بودن دستگاه رو بر عهده داره):
اپلیکیشنی که داریم کار میکنیم، سورسکدش خیلی کمه و خیلی راحت با نگاه کردن هم میشه هر چیزی رو پیدا کرد و در واقع نیازی به سرچ توی کد نیست. در کل خیلی از توضیحات و روشهایی که توی این رشته پستها گفته میشه ممکنه برای Sampleی که روش کار میکنیم اصلا نیازی نباشه، ولی هدف اینه با روش انجام کار در شرایط واقعی آشنا بشیم.
نسبت به اینکه هر اپلیکیشن با چه زبان و فریمورکی توسعه داده شده، باید از Decompiler مخصوص به خودش استفاده کرد، به فرض اگه اپلیکیشن با Flutter توسعه داده شده بود JADX نمیتونست بخش اصلی (فایل libapp.so) رو دیکامپایل کنه.
برای اینکه ببینین هر زبان و فریمورک رو با چه Decompilerهایی میشه دیکامپایل کرد، فقط کافیه یه سرچ کوچیک کنین. (البته تو این رشته پستها با 2-3 تا از بهتریناشون آشنا میشیم...)
خب همونطور که میبینید بخشی از متن Alert Dialog رو سرچ کردیم و رسیدیم به یک if که در صورت true بودن، مقدار Root detected رو به متد a ارسال میکنه:
اینم فانکشن a ، که build و show کردن Alert Dialog رو انجام میده. پس در صورتی که if بالا (کادر قرمز) برقرار باشه، مقدار Root Detected به این فانکشن ارسال میشه تا همراه با Message زیر به صورت یک Alert Dialog به کاربر نمایش داده بشه:
5- حالا باید ببینیم if ی که true شده و باعث شده این Alert Dialog فراخوانی بشه چی بوده.
همونطور که میدونین توی JADX اگه روی فراخوانی هر تابع کلیک کنیم به خود تابع میرسیم.
پس اینجوری میرسیم به توابعی که داخل if استفاده شدن:
3 متد بالا برای تشخیص روت بودن دستگاه استفاده شدن:
از اونجایی که توی شرطی که داریم (کادر قرمز بالا) بین این 3 متد OR شده، پس حتی اگه return یکی از این methodها true بشه، دستگاه روت شده تشخیص داده میشه.
6- خب پس تنها تغییری که نیازه انجام بدیم اینه که مقدار return هر 3 تا متد همیشه و در هر صورت برابر با false بشه. اگه دورهای که اول پست صحبتش بود رو دیده باشین، نمونههای این مدلی زیاد کار کردیم توی دوره، فقط فرقی که اونجا داشت این بود که اپلیکیشن رو Patch میکردیم (سورسکد رو تغییر میدادیم و از سورسکد تغییر داده شده Build میگرفتیم) ، ولی الان اینجا میخوایم بدون تغییر تو فایل اپلیکیشن، تغییراتی که نیاز داریم رو لحظه اجرا شدن (از روی Memory) تغییر بدیم.
حالا چجوری میتونیم برای Frida اسکریپتی بنویسیم که این تغییرات رو انجام بده، خیلی راحتتر از هر چیزی که فکرشو کنین:
قبلش بگم که مستندات کامل کار با Frida رو میتونین از دو لینک زیر مطالعه کنین: (پیشنهادم اینه که یک دور روزنامهای بخونینش تا با امکاناتی که داره آشنا بشین)
لینک GitHub سورسکدی که کار میکنیم رو آخرای پست براتون گذاشتم که بتونین راحتتر استفاده کنین.
7- اسکریپتی که برای هوک کردن و تغییر مقدار return نیاز داریم همین چند خط کد زیر میشه:
خط 1، طبق مستندات Frida اسکریپتمون رو داخل این فانکشن تعریف میکنیم تا در زمان و به صورت مناسب تحویل (Java virtual machine) JVM بشه. خط 3 کلاسی که قصد Hook کردنش رو داریم مشخص کردیم که از اینجا (کادر قرمز پایین) برش داشتیم:
PackageName.ClassName
در خط 6 هم متدی که میخوایم Hook کنیم رو مشخص میکنیم (کادر سبز شماره 1)، خط 10 مقداری که در حال حاضر (بدون هوک) متد a داره return میکنه رو log میگیریم (نیازی نیست، فقط میخوایم ببینیم مقدار return اصلی چی بوده). خط 14 و 15 هم مقدار false رو به عنوان return برمیگردونیم (یعنی میگیم return متد a تحت هر شرایطی false باشه)
خب حالا باید همین اسکریپت رو برای متد b و c (کادر سبز رنگ شماره 2 و 3) هم انجام بدیم دیگه، پس میشه به این صورت:
شاید براتون سوال پیش بیاد این اسکریپت رو میشه خیلی کوتاهتر و مرتبتر هم نوشت! درسته.
اسکریپت نهایی میتونه به صورت Refactor شدهی زیر باشه:
8- حالا بریم اسکریپت رو اجرا کنیم ببینیم نتیجه چی میشه:
اگه با روش اجرای اسکریپتهای Frida آشنا نیستین، تو این لینک توضیح داده شده.
خب همونطور که مشخصه، متد b و c بدون تغییر از سمت ما، مقدار false رو return کردن ولی از متد a مقدار true برگشته و همین باعث شده شرط بررسی روت بودن دستگاه true شه و اجازه اجرا شدن اپلیکیشن داده نشه (Alert Dialog نمایش داده بشه)، ولی ما اومدیم هوکش کردیم و مقدار return هر 3 متد رو false کردیم و بنابراین الگوریتم Root Detection الان Bypass شده:
قبل از اینکه بریم سراغ ادامه کار، برای تمرین بیشتر میتونین اپلیکیشن رو Debug Mode بیارین بالا و بعد Debug Mode Detection رو Bypass کنین، یعنی این قسمت:
یکی از روشهای Bypassش دقیقا به همین روشی میشه که Root Detectionش رو Bypass کردیم.
بعد از اینکه Root Detection رو Bypass کردیم و صفحه اول اپلیکیشن بالا اومد، یه Edit Text داریم با یک Button به اسم Verify، زمانی که روی Verify کلیک میکنیم پیغام زیر نمایش داده میشه که نشون میده مقداری که وارد کردیم اشتباهه (البته از روی UI هیچوقت نمیشه فانکشن رو پیشبینی کرد ولی اینجا با این فرض میریم جلو)، پس بریم مقدار صحیح رو پیدا کنیم.
1- مشابه با کاری که بخش اول انجام دادیم، اول باید اون بخشی از کد که باعث فراخوانی Alert Dialog شده رو پیدا کنیم، پس میایم بخشی از متنش رو توی سورسکد سرچ میکنیم و میرسیم به این قسمت:
همونطور که مشخصه، اگه شرط (کادر نارنجی) true باشه، کادر سبز و اگه false باشه، کادر قرمز بهصورت یک Alert Dialog به کاربر نمایش داده میشه. در واقع تا اینجای کار هنوز نمیدونیم این شرط دقیقا داره چه چیزی رو چک میکنه، ولی فرض میکنیم میاد مقدار وارد شده برای EditText رو با مقداری که به عنوان مقدار صحیح در نظر گرفته شده، مقایسه میکنه.
2- خب حالا باید شرط (کادر نارنجی بالا) که باعث فراخوانی این Alert Dialog شده رو بررسی کنیم. که میرسیم به:
3- اینجا قبل از متد bar از کلمهکلیدی native استفاده شده، زمانی که موقع تعریف یک متد از native استفاده میشه، یعنی این متد در زبانی غیر از Java (معمولاً C یا ++C) پیادهسازی شده، در واقع کلمهکلیدی native از طریق JNI (Java Native Interface) برای ارتباط بین Java و متدهای Native استفاده میشه.
پس الان مسیری که پیش رو داریم مشخص شد:
اینم در نظر داشته باشین که برخلاف روش قبلی، الان با فایل باینری سروکار داریم که روش دیکامپایل و هوک متفاوتی داره.
4- بریم فایل رو پیدا کنیم...
اپلیکیشنی که داریم کار میکنیم فقط یک فایل باینری داره، پس مشخصه که تابع bar هم توی همین فایل پیادهسازی شده، ولی خیلی وقتها اپلیکیشنها از چند فایل باینری استفاده کردن، تو همچین حالتی برای اینکه متوجه بشیم کدی که میخوایم هوکش کنیم تو کدوم فایل پیادهسازی شده، کافیه موارد زیر رو توی سورسکد سرچ کنیم (معمولا فایلهای باینری با یکی از این روشها تو Java بارگذاری میشن)
System.loadLibrary()
System.load()
ClassLoader
5- همونطور که مشخصه، این اپلیکیشن با روش اول فایل Nativeش رو بارگذاری کرده:
یک نکته رو دقت کنین، اینجا گفته شده foo رو بارگذاری کن، ولی ما باید دنبال فایل libfoo.so باشیم. چرا؟ دلیل اینجاست که JVM بر اساس نوع سیستم عامل، prefixes و extension اضافه میکنه. مثلا اگه ویندوز بود دنبال foo.dll میگشت، اگه macOS بود دنبال libfoo.dylib میگشت. الان ما داریم روی اندروید (که مبتنی بر لینوکسه) کار میکنیم، توی لینوکس JVM میاد یه lib به اول و یه so. به آخر مقدار وارد شده اضافه میکنه، پس میشه libfoo.so
6- خب حالا بریم فایل libfoo.so رو توی ساختار فایل پیدا کنیم و بعد دیکامپایلش کنیم:
همونطور که میبینید توی فولدر lib برای هر معماری، فایل باینری مخصوص اون معماری قرار داده شده. حالا ما روی کدوم کار کنیم؟ باید ببینیم دیوایس یا Emulatorی که روش کار میکنیم چه معماریای داره، الان اکثر دیوایسهای اندرویدی ARM64 هستن ولی اگه دارین روی Emulator کار میکنین اکثرا x86 یا x86_64 هستن (البته اگه با Frida کار میکنین پس معماری دیوایسی که روش Frida اجرا شده رو هم میدونین قطعا، چون باید بر اساس معماری Frida Server رو دانلود کرده باشین).
برای تشخیص معماری دیوایس اندرویدی، یکی از روشها اینه که کد زیر رو با adb اجرا کنید:
adb shell getprop ro.product.cpu.abi
من الان دارم از یک Emulator روی Nox Player که نسخه 7 اندروید و معماری x86 داره استفاده میکنم. ولی از اونجایی که از ARM translation پشتیبانی میکنه، میتونم از armeabi-v7a هم استفاده کنم. حالا کدوم بهتره؟ اگه اپلیکیشنی که دارین روش کار میکنین هم معماری arm رو داشت و هم x86، بهتره arm رو انتخاب کنین (با فرض اینکه از NEON و Thumb mode استفاده نشده باشه، arm نسبت به x86 راحت تره)
ولی ما اینجا در ادامه با x86 رفتیم جلو که اگه با اپلیکیشنی مواجه شدین که فقط x86 داشت فکر نکنین خیلی چیز عجیب و پیچیدهایه...
7- برای Decompile من از Radare2 استفاده کردم ولی شما میتونین از هر دیکامپایلری که باهاش راحتتر هستین استفاده کنین (Radare2، Ghidra و JEB رو پیشنهاد میکنم)
فانکشنهایی که داریم به این صورته:
همونطور که مشخصه یکی از functionهایی که داره همون تابع bar هست که دنبالش بودیم، که در 0x00000f60 قرار گرفته، بریم ببینیم چی داره داخلش:
8- خب حالا از کجا متوجه بشیم کدوم قسمت رو باید تغییر بدیم؟ لازمه از خط اول شروع کنیم به تحلیل کد؟ بیشتر وقتا همچین چیزی اصلا لازم نیست، کافیه نحوه پیادهسازی اون بخشی از کد که میخوایم hookش کنیم رو در نظر بگیریم و دنبال تابعی باشیم که به اون الگوریتم مربوط میشه. حالا این یعنی چی؟
ما یک Edit Text داریم که از کاربر ورودی میگیره و نسبت به اینکه ورودی صحیح باشه یا غلط، نتیجه رو با یک Alert Dialog نشون میده. خب تو همچین حالتی الگوریتم رو به چه صورت میشه فرض کرد؟ اینکه دو تا مقدار داریم، یکی مقداری که کاربر وارد میکنه، یکی هم مقدار صحیح، احتمالا میاد این 2 تا مقدار رو با هم مقایسه میکنه و در صورتی که برابر باشن مقدار مشخصی رو برمیگردونه. خب حالا با همین فرض، کافیه تابعی رو پیدا کنیم که میتونه این کارو انجام بده. همونطور که مشخصه (کادر نارنجی پایین)، اینجا از تابع strncmp استفاده شده که برای مقایسه دو string استفاده میشه.
یه نگاهی به مستندات strncmp بندازیم:
https://cplusplus.com/reference/cstring/strncmp/
int strncmp ( const char * str1, const char * str2, size_t num );
-- آرگومان اول (str1): رشته اول که میخوایم با رشته دوم مقایسه بشه.
-- آرگومان دوم (str2): رشته دوم که با رشته اول مقایسه میشه.
-- آرگومان سوم (num): تعداد کاراکترهایی که باید مقایسه بشن.
اگه مثلا num رو برابر 5 تعریف کنیم، میاد 5 کاراکتر اول از هر دو رشته رو مقایسه میکنه.
نتیجهی تابع هم به این صورت میشه:
-- اگه رشتهها یکسان باشن، مقدار 0 برگشت داده میشه. (بیشتر توابع مقایسهی دو string در زبان C، زمانی که مقادیر برابر باشن مقدار صفر return میشه.)
-- اگه رشته اول از دوم کوچکتر باشه، مقدار منفی برگشت داده میشه.
-- اگه رشته اول از دوم بزرگتر باشه، مقدار مثبت برگشت داده میشه.
* و اینکه طبق مستندات،مقایسه بر اساس ASCII انجام میشه، بنابراین Case-sensitive هست.
9- حالا باید مشخص کنیم کدوم بخش و چه چیزی از strncmp رو باید هوک کنیم؟
اگه بخشی که داریم کار میکنیم مثلا صفحه لاگین بود، معمولا فقط برامون مهمه که بتونیم لاگین کنیم. توی اون حالت میتونستیم مقدار برگشتی از strncmp رو برابر با 0 بذاریم. ولی اینجا هدف اینه که مقدار صحیح رو پیدا کنیم (نه اینکه فقط حالتی رو به وجود بیاریم که بگه مقدار صحیح وارد شده، خود مقدار صحیح رو میخوایم)
چکار کنیم؟ کافیه زمان فراخوانی strncmp ، آرگومانی که مربوط به مقدار صحیح (مقدار ثابت) میشه رو log بگیریم. حالا توی strncmp آرگومان اول اون مقداریه که داینامیک از کاربر گرفته میشه یا آرگومان دوم؟ طبق مستندات این تابع، در صورتی که هدف مقایسه دو رشته برابر (مقدار برگشتی 0) باشه، ترتیب آرگومانهای اول و دوم مهم نیست. ولی طبق کد، رجیستر eax قبل از اینکه به صورت پارامتر به تابع ارسال بشه، اول با دستور lea eax, [s2]
مقداردهی شده، یعنی با یک مقدار ثابت (آدرس متغیر s2) پر شده، پس به احتمال خیلی زیاد eax همون مقداری هست که به صورت ثابت (همون مقدار صحیح) به صورت hardcode در کد وجود داره.
البته اینجا ما میتونیم هر 3 آرگومانش رو log بگیریم چون بالاخره یکیش میشه همون مقدار ثابت، توضیحی که دادم برای تأکید بر اهمیت تحلیل اسمبلی بود.
اسکریپتی که نیاز داریم به این صورت میشه:
لینک Github کدهای که تو پروژه کار کردیم رو آخر پست گذاشتم.
قبل از اینکه اسکریپت رو توضیح بدم، نیازه یکم با تابع android_dlopen_ext آشنا بشیم. توی لینوکس از تابع dlopen برای بارگذاری کتابخونههای داینامیک (منظور کتابخونههایی که در لحظه اجرا اضافه میشن) استفاده میشه که همین تابع با یکسری تغییرات مثل بهینهسازی و افزایش امنیت بارگذاری کتابخونهها، توی اندروید به تابع android_dlopen_ext تغییر کرده، در واقع مدیریت کتابخونه و فایلهای باینریای که یک اپلیکیشن اندرویدی بارگذاری میکنه رو این تابع بر عهده داره.
اینجا خط 1 گفتیم تابع android_dlopen_ext رو پیدا کن. null ای که گذاشتیم یعنی داریم میگیم این تابع رو بین همهی کتابخونههای بارگذاری شده بگرد.
البته میتونیم به جای null اسم خود کتابخونهای که این تابع رو پیادهسازی کرده رو هم بذاریم ولی از اونجایی که تو نسخههای مختلف اندروید اسم این کتابخونه فرق میکنه، بهتره null بذاریم که بین کل کتابخونههای بارگذاری شده بگرده.
خط 3 یک boolean تعریف کردیم که هر موقع libfoo بارگذاری شد از این طریق متوجه بشیم.
خط 5 تا 21 به android_dlopen_ext اومدیم Attach شدیم، اینجا دو تا تابع داریم، یکی OnEnter و یکی OnLeave. به محض اینکه android_dlopen_ext اجرا میشه OnEnter فراخوانی میشه و بعد در زمانی که android_dlopen_ext مقداری رو Return کنه OnLeave فراخوانی میشه.
در واقع android_dlopen_ext هر بار که call میشه یک کتابخونه رو بارگذاری میکنه (تو این مرحله OnEnter فراخوانی میشه) و بعد از پایان تابع و Return مقدار برگشتی، OnLeave فراخوانی میشه.
حالا ما تو خط 7 اومدیم گفتیم آرگومان ورودی android_dlopen_ext رو بگیر و اگه برابر با libfoo بود، متغیر libfoo_loaded رو true کن. (در واقع این حلقه اونقدر تکرار میشه تا کل کتابخونهها بارگذاری بشن، که یکی از کتابخونهها میشه libfoo که دنبالش هستیم)
سوال دیگهای که ممکنه پیش بیاد اینه که دلیلی داشته آرگومان ورودی android_dlopen_ext رو توی OnEnter گرفتیم؟ بله، چون توابع توی OnEnter فراخوانی میشن و فقط توی OnEnter ه که میتونیم args رو بگیریم. اگه فرض کنیم توی OnLeave هم به args دسترسی داشتیم، دیگه نیازی به تعریف libfoo_loaded نبود.
خط 12 اومدیم گفتیم اگه libfoo فراخوانی شده بود Base Addressش رو بگیر. (تا فراخوانی نشه که نمیتونیم بهش دسترسی داشته باشیم، پس قبلش libfoo_loaded رو چک میکنیم که true باشه)
خط 23 یک تابع تعریف کردیم و گفتیم به Base Address ی که از libfoo پیدا کردی (مرحله قبل)، مقدار Offset مربوط به strncmp که میشه 0x00000ffb رو اضافه کن (که دقیقا به آدرس strncmp برسیم) و بهش Attach شو. آدرس offset مربوط به strncmp رو هم که از اینجا برداشتیم:
خط 25 اومدیم گفتیم زمانی که strncmp فراخوانی میشه مقدار eax (آرگومان ورودی تابع که مقدارش به صورت ثابت توی کد تعریف شده) رو بگیر و توی Log نشونش بده.
و در آخر، خط 18 تابعی که خط 23 تعریف کردیم رو فراخوانی میکنیم.
خب، حالا بریم اسکریپت رو اجرا کنیم.
دقت کنید که اسکریپت اول و دوم باید هم زمان (هر دو در یک فایل) اجرا بشن، که اسکریپت اول Root Detection رو Bypass کنه و اسکریپت دوم مقدار ورودی صحیح رو log کنه.
10- بعد از اجرای اسکریپت، به صورت پیشفرض هیچ لاگی نمایش داده نمیشه. چرا؟ چون تابع strncmp به طور پیشفرض فراخوانی نمیشه، فقط زمانی فراخوانی میشه که مقداری رو مقایسه کنه. بنابراین، لازمه مقداری رو وارد کنیم و بعد Verify رو کلیک کنیم تا strncmp فراخوانی بشه.
اما، بعد از وارد کردن مقدار و کلیک روی Verify بازم لاگی ثبت نمیشه. چرا !؟
بریم باز یه نگاهی به کد بندازیم...
همونطور که مشخصه (کادر نارنجی شماره 1) در صورتی که مقدار موجود در رجیستر eax یعنی همون مقداری که وارد میکنیم برابر با 0x17 (که برابر با 23 میشه) نباشه ZF (Zero Flag) برابر با 0 میشه و زمانی هم که ZF برابر با صفر بشه، EIP به آدرس جدید یعنی 0x1007 منتقل میشه (از روی strncmp رد میشه)، خب پس برای اینکه jump انجام نشه باید طول مقداری که وارد میکنیم برابر با 23 باشه.
توی کادر نارنجی شماره 2 در صورتی که مقادیر برابر باشن و strncmp مقدار صفر رو برگردوند.ه باشه، ZF برابر با 1 میشه و EIP به آدرس 0x101e تغییر میکنه (هدفی که ما داریم)
11- پس برای فراخوانی strncmp لازمه مقداری که وارد میکنیم دقیقا 23 کاراکتر باشه:
12- خروجی اسکریپت بعد از کلیک Verify به صورت زیر میشه:
13- در نهایت مقداری رو که به عنوان مقدار صحیح log گرفتیم تست میکنیم:
رسیدیم به پایان بخش اول...
این پست خیلی طولانیتر از چیزی شد که انتظارش رو داشتم، ولی از طرفی اگه دو بخشش میکردیم موضوع ناقص میموند.
برنامه اینه که این پست، حدود 11-12 بخش دیگه هم داشته باشه. هدف از بخش اول (همین پست)، آشنایی اولیه با مفاهیم هوک کردن بود.
در روند انجام مراحلی که کار کردیم، سعی کردم سوالاتی که ممکنه براتون پیش بیاد رو جواب بدم. چند موضوع دیگه که شاید گفتنش بد نباشه اینه که:
روشی که برای انجام مراحل این پست کار کردیم به هدف یادگیری مفاهیم بود، وگرنه میشد خیلی سریعتر هم به جواب رسید. مثلاً:
-- توی بخش دوم، رشتهای رو که با هوک کردن به دست آوردیم، میشد خیلی راحت با بررسی استاتیک هم پیدا کرد.
-- یا در بخش Root Detection، میشد با یک خط Bypass کرد.
و سوال دیگهای که ممکنه براتون پیش اومده باشه اینه که:
نوشتن اسکریپت برای Frida واقعاً اینقدر سادهست؟
اگه نگاهی به محبوبترین اسکریپتهای Frida CodeShare بندازید، بعد از این پست احتمالاً 70-80٪ از کدشون رو متوجه بشین، اما واقعیت اینه که نه، هوک کردن همیشه هم به این سادگی نیست. در واقع به نحوه پیادهسازی مکانیزم امنیتیای که میخوایم hookش کنیم بستگی داره، مثلا:
خبر خوب اینکه نگران نباشین، همونطور که گفتم قراره این پست تا 11-12 بخش دیگه ادامه داشته باشه و کلی از چالشهای مرتبط با Reverse و Hook (که فقط بخشی ازشون به هوک کردن مربوط میشه) رو بررسی میکنیم.
راستی بازم بگم که اگه در زمینه باگبانتی و Pentest اپلیکیشنهای داخلی کار میکنین، حداقل 80% اشون (شامل بانکها، مرتبط با پرداخت، سازمانها و ...) اکثر مکانیزمهای امنیتیای که پیادهسازی کردن کتابخونه و الگوریتمهای آمادست و بنابراین هیچ چالشی برای Bypass کردنشون ندارین. هدف این دوره در واقع حل چالشهای اون 20% باقی موندهست.
اگه در زمینه باگبانتی و Pentest اپلیکیشنهای خارجی کار میکنین، نسبتا اوضاع بهتری دارن. مثلا در زمینه اپلیکیشنهای بانکی و پرداخت بین المللی اکثرشون در کنار برنامه باگبانتی همیشگی، از راهکارهای تجاری زیادی مثل nowsecure ،zimperium ،guardsquare و... استفاده کردن که باعث شده Reverse اشون با چالشهای خیلی زیادی همراه باشه.
این پست رو دیگه بیشتر از این طولانی نکنم... 😅
سورسکد اسکریپتی که کار کردیم:
https://github.com/EmadAbedini/Android-Pentest-Challenges
اگه به امنیت علاقه دارین، این دو پست هم احتمالا براتون جالب باشه:
امیدوارم این پست براتون مفید بوده باشه. سعی میکنم بخشهای بعدی زودتری آماده بشه.
راستی اگه دوست داشتید میتونید از طریق لینکدین باهام در ارتباط باشید.
شاد و موفق باشید... ❤️