ویرگول
ورودثبت نام
عماد عابدینی
عماد عابدینیSecurity Researcher | Full Stack Developer
عماد عابدینی
عماد عابدینی
خواندن ۱۱ دقیقه·۱ سال پیش

چالش‌های Pentest اپلیکیشن‌های اندرویدی (بخش دوم)

در بخش قبل با مفاهیم پایه‌ Hooking در اپلیکیشن‌های اندرویدی آشنا شدیم. قبل از شروع این بخش، اول مهم‌ترین نکات بخش قبل رو مرور کنیم:

  • بیشتر اپلیکیشن‌های اندرویدی (شاید چیزی حدود 80%) برای پیاده‌سازی مکانیزم‌های امنیتی از کتابخونه‌ و اسکریپت‌های عمومی استفاده کردن، بنابراین خیلی راحت و بدون هیچ چالشی میشه الگوریتم‌های امنیتی‌شون رو Bypass کرد.

  • برای انجام اصولی فرایند Pentest و Reverse اپلیکیشن‌های اندرویدی، در کنار دانش برنامه‌نویسی Native، نیازه که با اسمبلی هم آشنا باشیم. (دانش برنامه‌نویسی بیشتر = تحلیل و Reverse دقیق‌تر)

  • از مهم‌ترین روش‌هایی که برای Bypass مکانیزم‌های امنیتی استفاده میشه، Patch و Hook کردن اون بخش از Codeه که باعث ایجاد محدودیت شده.

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

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

  • در فرایند Pentest و Reverse اپ‌های اندرویدی، ممکنه با چالش‌های زیادی روبه‌رو بشیم که فقط بخشی از این چالش‌ها به Hook کردن مربوط میشه.

یکی از مشکلات بخش قبلی طولانی بودنش بود، حدود 6200 کلمه (تقریبا چیزی حدود 25 صفحه A4) که برای یک پست واقعاً زیاده. دلیل اصلی این طولانی شدن هم این بود که بخش زیادی به مباحث پایه و مقدماتی اختصاص داده شد.
همونطور که توی پست قبلی توضیح دادم در این رشته پست‌ها فرض شده که شما با اصول اولیه Reverse اپلیکیشن‌های اندرویدی آشنا هستین (پست قبلی رعایت نکردم این موضوعو 😁)
حالا برنامه اینه که از این بخش به بعد، مستقیم بریم سر اصل مطلب که تا جای ممکن هر بخش کوتاه و خلاصه باشه. اگه با مباحث پایه آشنایی ندارید نگران نباشید اصلا، محتوای خیلی زیادی (حتی فارسی) در این زمینه وجود داره که خیلی راحت می‌تونین با یه سرچ ساده یادشون بگیرین.


و اما عنوان این بخش:

وقتی که سورس‌کد خودش رو قایم کرده! 😕

خیلی وقت‌ها اپلیکیشن رو دیکامپایل می‌کنیم اما خبری از سورس‌کد نیست! تو این حالت معمولا سورس‌کد Encrypt ،Compress و Obfuscate شده و در زمان اجرا (Runtime)، رمزگشایی و فراخوانی میشه، اصطلاحا میگن سورس‌کد Pack شده.

هدف از پیاده‌سازی این تکنیک مخفی کردن کد در تحلیل استاتیک و در نتیجه پیچیده‌تر کردن فرایند Reverse Engineering هست.

حالا سوال اینه که کد کجاست؟ 🤔 معمولا با یکی از این دو حالت روبرو هستیم:

  1. دانلود در لحظه اجرا (Runtime): تو این حالت کد از اینترنت و در لحظه اجرا بارگذاری میشه (این روش بیشتر توی malwareها دیده میشه).

  2. جاسازی و رمزنگاری در فایل‌های اپلیکیشن: تو این حالت کد داخل فایل‌های اپلیکیشن وجود داره و در لحظه اجرا Decrypt و فراخوانی میشه. تو این روش، بسته به الگوریتم مورد استفاده، اون بخش از کد که قراره در زمان اجرا فراخوانی بشه، به صورت Encrypt شده ممکنه یه فایل dex باشه، ممکنه داخل فولدر Assets باشه، ممکنه یه فایل باینری باشه، ممکنه توی Resources باشه، شاید تو فولدر Raw باشه و حتی ممکنه به چند بخش تقسیم شده باشه و هر بخش در یک فایل مجزا قرار گرفته باشه.

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

معمولا RATهای درست حسابی Shell Code اشون رو اینجوری اضافه می‌کنن.

بنابراین نسبت به الگوریتم Packer، تحلیل و دسترسی به سورس‌کد به‌صورت استاتیک، یا امکان‌پذیر نیست یا بسیار زمان‌بره.


اگه بخوایم دقیق‌تر بهش نگاه کنیم:

پیاده‌سازی این مکانیزم به سه روش امکان‌پذیره:

  • توسعه اسکریپت اختصاصی

  • استفاده از اسکریپت‌ها و کتابخانه‌های عمومی

  • استفاده از راهکارهای تجاری مثل DexProtector، GuardSquare و موارد مشابه

اسکریپت‌ها، کتابخونه‌های عمومی و حتی راهکارهای تجاری‌ای که برای پیاده‌سازی این مکانیزم استفاده میشه خیلی‌هاشون Signature اشون درومده و همین باعث شده اسکریپت‌های Decrypt اشون هم در دسترس باشه. از طرفی توسعه اسکریپت اختصاصی‌ به دانش زیادی در زمینه‌های Android Internals، تکنیک‌های Reverse Engineering، الگوریتم‌های Encryption و Cryptography نیاز داره که خب این باعث میشه پیاده‌سازی یک اسکریپت اختصاصی امن، زمان‌بر و پیچیده باشه.
پس انتخاب روش مناسب به سطح امنیت مورد نیاز اپلیکیشن بستگی داره:
- اگه هدف صرفا افزایش نسبی امنیت باشه، می‌تونیم از کتابخونه‌های عمومی استفاده کنیم که حداقل باعث شه تا حدودی فرایند Reverse زمان بیشتری بگیره.
- اما در موارد حساس‌ مثل اپلیکیشن‌های مالی و بانکی، باید از ترکیب راهکارهای تجاری و اسکریپت اختصاصی استفاده کرد. (اکثر اپلیکیشن‌های پرداخت بین‌المللی برای پیاده‌سازی این تکنیک، از ترکیب راهکارهای تجاری و الگوریتم‌های اختصاصی استفاده کردن)


سوالی که ممکنه براتون پیش بیاد اینه که:
پس الان اکثر اپلیکیشن‌‌های حساس (بانکی، پرداخت و از این قبیل) حداقل با استفاده از الگوریتم‌های عمومی این مکانیزم رو پیاده‌سازی کردن؟
نه! 😐 طبق بررسی‌هایی که انجام دادم شاید بشه گفت کمتر از 20% اپلیکیشن‌های حساس داخلی این تکنیک رو پیاده‌سازی کردن. (البته که قطعا من همه اپلیکیشن‌ها رو تست نکردم، ولی در زمینه اپلیکیشن‌های بانکی و پرداخت فقط توی 3 اپلیکیشن به صورت صحیح پیاده‌سازی شده بود)

تعداد زیادی از بانک‌های داخلی Core Banking و اپلیکیشن‌اشون مشترکه (رنگ و آیکنش عوض شده)، در واقع اگه به فرض 25 نمونه اپلیکیشن بانکی داشته باشیم، در واقع چیزی حدود 14 نمونه برای بررسی داریم.

راستی اینم در نظر داشته باشین:

  • زمانی که سورس‌‍‌کد رو Pack می‌کنیم، نسبت به الگوریتمی که پیاده‌سازی کردیم معمولا حجم خروجی زیاد میشه (نه خیلی).

  • احتمال داره Google Play اپلیکیشن رو Reject کنه چون قبل از تایید انتشار، سورس‌کد بررسی میشه. (متاسفانه کافه بازار ظاهرا فقط بر اساس نتیجه Multi AV انتشار یا عدم انتشار اپلیکیشن‌ها رو بررسی می‌کنه!)

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

اگه از راهکارهای تجاری استفاده کنین که خب نگرانی‌ای بابت موضوعات بالا نیست، ولی اگه خواستین الگوریتم اختصاصی خودتون رو بنویسین یا از کتابخونه‌های عمومی استفاده کنین به این موضوعات هم دقت داشته باشین، در واقع باید Decryption و Compression در سطح مناسبی باشه (نه خیلی شدید و نه خیلی ضعیف)


خب بگذریم، برگردیم سر ادامه موضوع...

چجوری می‌تونیم به سورس‌کد برسیم؟

اول باید بررسی کنیم اپلیکیشن از کتابخونه‌ و روش‌های عمومی استفاده کرده یا الگوریتم اختصاصی. برای این کار می‌تونیم از ابزارهایی مثل APKiD استفاده کنیم، یا به صورت دستی (بدون ابزار) نوع Packer رو تشخیص بدیم و برای استفاده‌های بعدی Yara Ruleش کنیم.

حالا اگه از کتابخونه‌ و روش‌های عمومی استفاده شده باشه و اسکریپت Decryptش هم موجود باشه که خیلی راحت به سورس‌کد می‌رسیم و چالشی هم نیست. پس میایم فرض می‌کنیم از روش اختصاصی استفاده شده یا به هر دلیل دسترسی به اسکریپت Decrypt نداریم.

  • روش اول: درصد بالایی از Packerها نحوه کارشون به این صورته که بخش Encrypt و Compress شده در زمان اجرا (Runtime)، رمزگشایی و با روش ‌هایی مثل استفاده از کلاس DexClassLoader در حافظه Load میشه و بعد فایل unpack شده فورا از روی دیسک حذف میشه. تو این حالت می‌تونیم فانکشن های مرتبط با حذف فایل رو hook کنیم که Remove شدن اتفاق نیفته و بعد میریم فایل unpack شده رو از روی دیسک برمی‎‌داریم 😍

  • روش دوم: حالت دوم زمانی استفاده میشه که روش اول جواب نمیده. بعضی Packerها ممکنه به‌جای استفاده از توابع رایج، از روش‌هایی استفاده کنن که ما به هر دلیل نتونیم جلوی حذف شدن فایل از دیسک رو بگیریم. یه مورد که تازگیا دیدم این بود که چندین تابع مرتبط با remove تعریف کرده بود و شرط گذاشته بود که اگه تعدادی مشخص از این توابع به طور کامل اجرا شدن (فایلی مشخص رو حذف کردن) بعد میرفت سراغ Decrypt بخش Pack شده، همین باعث میشد روش اول جواب نده چون ما معمولا میایم تموم توابع مرتبط با remove رو هوک می‌کنیم (البته که تو این حالت هم میشه با تحلیل استاتیک کد به همون روش اول به نتیجه رسید ولی زمان‌بر میشه). در روش دوم، ما میایم به جای اینکه جلوی حذف فایل unpack شده رو بگیریم، از Memory Mapping برای Pull کردنش استفاده می‌کنیم.

روش دوم معمولا همیشه جواب میده، مگه اینکه اون بخش از Memory که فایل Pack شده اونجا قرار گرفته Encrypt شده باشه. تو این حالت نیاز به Tracing و Debuging داریم.


نحوه انجام روش اول: (جلوگیری از حذف فایل unpack شده)

همونطور که توضیح دادم باید یک اسکریپت بنویسیم که جلوی حذف فایل رو بگیره. اینجا بستگی داره حذف فایل با استفاده از Java Methods انجام شده باشه یا Native Methods.

توی جاوا یکسری Method داریم که به طور مشخص برای حذف فایل استفاده میشن، مثل delete و deleteIfExists از کلاس‌های java.io.File و java.nio.file.Files ، ولی یکسری Method هم داریم که کاربرد اصلی‌شون حذف فایل نیست ولی به نوعی میشه باهاشون فایل رو غیر قابل دسترس کرد، مثلا با java.nio.file.attribute.FileAttributeView میشه با دستکاری مجوزهای فایل بدون اینکه فایل حذف بشه به نوعی غیرقابل استفادش کرد. ولی ما خیلی پیچیدش نمی‌کنیم، میایم فقط همون delete و deleteIfExists رو در نظر می‌گیریم (فوقش اینه که روش اول جواب نمیده، میریم روش دوم)

اسکریپت‌ها رو می‌تونین از اینجا داشته باشین:
Android-Pentest-Challenges

در اسکریپت زیر، متد delete از کلاس java.io.File رو هوک کردیم. در خط 5، از this که میشه همون شیء ایجاد شده از File، مسیر فایل (path) که قراره حذف بشه رو گرفتیم. در خط ۹ مقدار true رو برگردوندیم تا متد اصلی delete اجرا نشه.

حالا چرا true رو برگردوندیم؟ چون وقتی متد delete مقدار true رو برمی‌گردونه، یعنی فایل با موفقیت حذف شده. اگه فایل به هر دلیلی حذف نشه (مثلاً فایل توی اون مسیر وجود نداشته باشه) مقدار false برمی‌گرده، اما اینجا عمدا مقدار true برگشت داده شده که فکر کنه فایل حذف شده، در حالی که واقعا حذف نشده.

قبل از اینکه خروجی بالا رو ببینم، اگه پیاده‌سازی الگوریتم Packing در Native Methods باشه باید چکار کرد؟ روش کار دقیقا همونه، باید بیایم فانکشن‌های مرتبط با حذف فایل که در ++C/C استفاده میشن رو هوک کنیم، فانکشن‌هایی مثل:

remove, unlink, rmdir

مثلاً اینجا فانکشن remove رو هوک کردیم:
مقدار 0 رو return کردیم، چون وقتی این فانکشن فایل رو با موفقیت حذف کنه، مقدار 0 رو برمی‌گردونه. در خط 9، نوع داده رو int گذاشتیم چون مقدار برگشتی از نوع int هست و از pointer استفاده کردیم که به NativeCallback بگیم آرگومان ورودی رو باید به عنوان pointer مدیریت کنی. (البته اگه مستندات Frida رو در کنار مستندات فانکشنی که می‌خواین هوک کنید مطالعه کنید، تمام موارد به‌طور کامل توضیح داده شده.)

در نهایت، اسکریپت رو اجرا می‌کنیم و خروجی به صورت زیر میشه:

الان خیلی راحت می‌تونیم فایل unpack شده رو از مسیر زیر pull کنیم، بعد unzip ش کنیم و به سورس‌کد برسیم:

/data/user/0/ir.emad-abedini.app.pico/code_cache/secondary-dexes/base.apk.classes1.zip



نحوه انجام روش دوم: (Memory Mapping)

خب همونطور که توضیح دادم ممکنه حالتی پیش بیاد که به هر دلیل روش اول جواب نده، تو این حالت می‌تونیم از Memory Mapping استفاده کنیم. در واقع فایل Pack شده اول روی دیسک ایجاد میشه، بعد روی حافظه بارگذاری میشه و بعد از روی دیسک فورا حذف میشه. (اما همچنان روی حافظه هست و میشه مستقیم از روی حافظه dumpش کرد)

پس اول Memory Mapping رو به این صورت انجام میدیم: (طبیعتا اپلیکیشن باید قبلش اجرا باشه)

[ADB_SHELL] : /proc/[pid]/maps

توی خروجی با یکم اسکرول کردن میشه راحت فایل unpack شده رو پیدا کرد. اگه پیدا نکردین (که البته بعیده)، معمولا اون بازه‌ای که طولش بیشتره میشه همون فایل unpack شده، الان اینجا طول بین 81e5026000 و 81e500d000 میشه 102,400 بایت (اگه دقت کنید بقیه region ها طول کمتری دارن)

الان رسیدیم به آدرسی از دیسک که فایل از اون آدرس Extract شده و روی Memory بارگذاری شده. حالا همونطور که توضیح دادم درسته به مسیر فایل رسیدیم، ولی نمی‌تونیم pullش کنیم، چون فایل بعد از بارگذاری روی Memory، سریع از روی دیسک حذف شده (فایل روی دیسک نیست که بشه pullش کرد)، حالا چکار کنیم؟

میایم اون Region از حافظه رو dump می‌گیریم و خروجی رو در یک فایل ذخیره ‌می‌کنیم:
تنها نکته اینجاست آدرسی که قراره خروجی dump ذخیره شه باید دایرکتوری خود اپلیکیشن باشه، یعنی:

/data/data/<package_name>


نکته بعدی که وجود داره ASLRه، در نسخه‌های قبل از 4 اندروید زمانی که اپلیکیشن اجرا میشد همه بخش‌ها (کد، کتابخونه‌ها، Stack و Heap) همیشه توی آدرس مشخصی از Memory قرار میگرفتن که این ثابت بودن مشکلات امنیتی زیادی مثل buffer overflow و Return-oriented programming رو به وجود میاورد، از اندروید 4 به بعد ASLR اضافه شد که هر بار اپلیکیشن اجرا میشه این آدرس‌ها تغییر کنه.
پس اگه بیایم اسکریپتی که نوشتیم رو Spawn کنیم، اپلیکیشن دوباره اجرا میشه و زمانی هم که بیاد بالا فایل unpack شده بر اساس ASLR جای دیگه‌ای از حافظه قرار گرفته و اینجوری اسکریپت ما کار نمی‌کنه.

پس جای Spawn اسکریپت رو به این صورت Attach می‌کنیم:

frida -U [APP_NAME] -l [SCRIPT]

که در نهایت نتیجه به این صورت میشه:

الان راحت میتونیم فایل unpack شده رو از این مسیر pull کنیم.



خب رسیدم به پایان بخش دوم 😍 (از احتمالا 11 ام)

نکته خاص نگفته‌ای به نظرم نمیرسه، فقط اینکه:

برای رسیدن به فایل unapack شده، یکسری ابزار آماده هم وجود داره، مثلا Objection هم این امکان رو داره. درحال حاضر ابزارهایی که برای Bypass این مکانیزیم (Packing) منتشر شدن محدودیت‌های خیلی زیادی دارن (فقط روی الگوریتم‌های ساده جواب میدن)، ولی خب برای صرفه‌جویی در زمان، پیشنهادم اینه که بهتره اول از ابزارهای آماده استفاده کنیم و اگه جواب نگرفتیم بعدش بیایم سراغ اسکریپت نوشتن.

یکی دیگه از روش‌های رسیدن به فایل unpack شده (در صورتی که فایلی که قراره Decrypt شه DEX یا Jar باشه) هوک کردن متدهایی مثل ClassLoader و PathClassLoader هست.

دسته‌ خیلی کمی از اپلیکیشن‌ها Memory رو Encrypt می‌کنن، تو این حالت dump گرفتن از Memory عملا هیچ کاربردی نداره. روش کار به این صورته که تموم stringهایی که داره رو بگیرین، فانکشن و متد‌ها رو جدا کنین و بر اساس فانکشن یا متدی که مرتبط با مدیریت حافظه‌ست debug رو شروع کنین ( به نتیجه میرسین)

امیدوارم که این پست براتون مفید بوده باشه...
اگه سوالی داشتید می‌تونیم لینکدین در ارتباط باشیم.

شاد و موفق باشید... ❤️


ویرایش: (1404/05/17)
با توجه به اینکه این پست و پست قبلی بازخورد زیادی نداشتن، فعلا این موضوع رو تا همین‌جا نگه می‌داریم. برنامه‌م اینه که از پست‌های بعدی سراغ موضوعات دیگه (در همین حوزه امنیت) برم.
برای دنبال‌کردن مطالب بعدی خوشحال می‌شم در لینکدین در ارتباط باشیم ❤️

تست نفوذامنیتمهندسی معکوسفارنزیکاندروید
۳
۰
عماد عابدینی
عماد عابدینی
Security Researcher | Full Stack Developer
شاید از این پست‌ها خوشتان بیاید