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

روش Decrypt ترافیک AES در اپلیکیشن‌های Flutter

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

در این پست فرض شده که شما با مفاهیم و فرایند کلی تست نفوذ اپلیکیشن‌های اندرویدی آشنا هستید. اگه آشنایی ندارید، پیشنهاد می‌کنم اول پست‌های قبلی رو مطالعه کنین.


اوایل تنها مکانیزم امنیتی، Obfuscation و استفاده از SSL بود، ولی به مرور مکانیزم‌های دیگه‌ای به اپلیکیشن‌ها اضافه شد، مثل:

  • تشخیص اجرا روی شبیه ساز (Emulator Detection)

  • تشخیص روت بودن دستگاه (Root Detection)

  • تشخیص استفاده از VPN/Proxy

  • پیاده‌سازی SSL Pinning

  • نادیده گرفتن System Proxy

  • رمزنگاری ترافیک ارسالی/دریافتی

  • پیاده‌سازی لایه‌های امنیتی به صورت native (با C و ++C)

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

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

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

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

اگه با مکانیزم‌های بالا و نحوه Bypassشون آشنایی ندارید، در پست‌های قبلی در موردشون توضیح دادم.

توی این پست می‌خوایم سراغ اپلیکیشن‌هایی بریم که با Flutter توسعه داده شدن و ترافیک ارسالی/دریافتی‌شون رو رمزنگاری (Encrypt) می‌کنن.

چرا Flutter ؟

همونطور که می‌دونید در حال حاضر زبان‌ها و فریمورک‌های زیادی برای توسعه اپلیکیشن‌های اندرویدی وجود داره. یکی از روش‌هایی که باعث میشه فرایند Reverse زمان‌بر بشه نیاز به تحلیل فایل‌های باینریه. به‌ همین دلیل خیلی از اپلیکیشن‌هایی که امنیت براشون اهمیت داره (مثل اکثر اپ‌های مالی و پرداخت)، بخش‌های حساس رو به‌ صورت native پیاده‌سازی می‌کنن تا Reverse و Bypass لایه‌های امنیتی‌شون نیاز به تحلیل باینری داشته باشه. حالا از بین این زبان و فریمورک‎‌ها، وقتی شما با مثلا Rust، Go یا Flutter اپ اندرویدی توسعه می‌دید، خروجی به کد native (فایل so.) تبدیل می‌شه.

درسته که می‌شه با استفاده از NDK تو اکثر زبان و فریمورک‌ها بخشی از پروژه رو native زد، اما موضوع اینجاست که بعضی از این زبان‌ و فریمورک‌ها، بدون اینکه نیاز باشه ++C/C بزنین، خودشون در زمان کامپایل خروجی رو به native code تبدیل می‌کنن.

پس ما این پست رو به Flutter اختصاص دادیم چون:

  • Flutter به‌صورت پیشفرض چندین مکانیزم امنیتی رو به خروجی اضافه می‌کنه.

  • از روز اول انتشارش، دور زدن لایه‌های امنیتی اپ‌هایی که با Flutter ایجاد شده بودن چالش‌های زیادی داشت (بخش زیادی از این چالش‌ها سر این بود که تا قبل از فلاتر، اکثر اپ‌ها با Java/Kotlin پیاده‌سازی می‌شدن ولی از اونجایی که فلاتر خروجی رو به باینری تبدیل می‌کرد، خیلی از روش‌های Bypass قبلی دیگه جوابگو نبودن)

  • در Flutter (در واقع Dart) خیلی از Objectها، Constها و Variableها به جای اینکه مستقیم در کد ذخیره شن، در یک جدول (Object Pool) قرار می‌گیرن و از طریق Index فراخوانی می‌شن. (بنابراین استخراج Stringها با ابزارهای رایجی که برای این منظور استفاده میشه کمکی نمی‌کنه)

  • این روزا Flutter خیلی محبوبه، تا جایی که حتی خیلی از اپ‌های مالی و پرداخت هم با فلاتر توسعه داده شدن.

روش Bypass لایه‌های امنیتی اپ‌های فلاتری رو در پست‌های قبلی توضیح دادم، تو این پست فقط روی روش Decrypt ترافیک تمرکز می‌کنیم.

پیش نیاز‌ها:

  • با مقدمات فرایند تست‌ نفوذ و Reverse اپلیکیشن‌های اندرویدی مثل کار با Frida و مفاهیم پایه معماری ARM آشنا باشید. (اگه آشنا نیستید نگران نباشید، با یه سرچ ساده یا استفاده از ChatBotها می‌تونید خیلی سریع یادشون بگیرین)

  • یک دیوایس فیزیکی یا Emulator با معماری ARM64 داشته باشید. در کل برای انجام تست‌ نفوذ و Reverse اپ‌های اندرویدی بهتره از ARM64 استفاده کنید، چون این روزا اکثر Device ها این معماری رو دارن و بیشتر ابزارها هم با این معماری سازگاری و عملکرد بهتری دارن.

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


رمز کردن ترافیک در اپلیکیشن‌های اندرویدی

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

بدون در نظر گرفتن اپ‌های ایجاد شده با Flutter، به‌طور کلی وقتی به ترافیک Encrypt شده می‌رسیم معمولا به این صورت میریم جلو:

  • بررسی می‌کنیم از چه زبان و فریمورکی استفاده شده و بعد دنبال نشونه‌هایی از کتابخونه‌ و الگوریتم‌های رایج مرتبط با رمزنگاری می‌گردیم (بر اساس نام)

  • اگه نشونه‌ای از کتابخونه و الگوریتم‌های رایج پیدا کردیم، میریم سراغ Bypass با ابزار و Scriptهای عمومی مرتبط با اون کتابخونه یا الگوریتم (به احتمال خیلی زیاد هم به نتیجه می‌رسیم)

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

  • روش دیگه اینه که بیایم تمام فانکشن‌ها و کتابخونه‌های رایج مرتبط با رمزنگاری رو Hook کنیم (اسکریپت آماده‌ش هم هست)

  • یا می‌تونیم اگه توابع مرتبط با رمزنگاری رو پیدا کردیم، بخشی ازش رو گوگل و مخصوصا Github سرچ کنیم. (این روش بارها برای من نتیجه داده، حتی پارامترها رو هم تغییر نداده بودن و کلید هم همون چیزی بود که تو Github بود، باور کردنی نیست نه؟ ولی خیلی پیش میاد)

  • یک روش دیگه اینه که بعد از اجرای اپلیکیشن و ارسال اولین ترافیک، از فایل‌هایی که ایجاد کرده دنبال نشونه‌های از روش رمزنگاری و پارامترهای مرتبط با اون الگوریتم بگردیم. (بارها برام پیش اومده که با همین روش به نتیجه رسیدم، اومده بودن کلید و پارامترهای رمزنگاری رو گذاشته بودن تو Shared Preferences یا یک فایل دیگه اونم با اسم کاملا مشخص) 😶

  • یا می‌تونیم در صورتی که Server دیتا رو به صورت Plain قبول می‌کرد، اون قسمت از کد که مقادیر رو رمزنگاری می‌کنه حذف کنیم و مجدد Rebuild بگیریم. (البته خیلی وقت‌ها این روش ممکنه وقت خیلی بیشتری نسبت به سایر روش‌ها بگیره)

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

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

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

  • این روش به عنوان یکی از روش‌هایی هست که میشه برای Decrypt ترافیک ازش استفاده کرد. (تنها روش نیست و ممکنه نسبت به شرایط پروژه، روش‌‌های دیگه سریع‌تر به نتیجه برسن)


راستی تا یادم نرفته بگم که تو این پست که حدودا برای دو سال پیش میشه، گفته بودم برای Proxy کردن می‌تونین یا از ProxyDroid استفاده کنید یا از iptables، ولی تو نسخه‌های اخیر فلاتر ProxyDroid تو خیلی موارد دیگه جوابگو نیست و بهتره مستقیم از iptables استفاده کنین. روش کار رو هم که تو همون پست توضیح دادم.

مثلا اینجا گفتیم ترافیک 80 و 443 رو بفرست به 192.168.1.4:8080 که میشه همون Proxy Serverمون
مثلا اینجا گفتیم ترافیک 80 و 443 رو بفرست به 192.168.1.4:8080 که میشه همون Proxy Serverمون

مورد دوم هم اینکه، برای Bypass کردن SSL Pinning اپلیکیشن‌های فلاتری، دیگه لازم نیست روش وقت‌گیر قبلی که تو همون پست توضیح دادم رو انجام بدین، می‌تونین از اسکریپت زیر استفاده کنین که کار رو واقعا ساده کرده:

https://github.com/NVISOsecurity/disable-flutter-tls-verification

البته روشی که این Script استفاده میکنه چیز خاص و جدیدی نیست، میاد بر اساس Pattern، بخش مربوط به Pinning رو پیدا و Hook می‌کنه. (در واقع همون روشی که ما تو اون پست به صورت دستی انجام می‌دادیم رو به صورت automate انجام میده و همین باعث شده کار بسیار سریع‌تر بشه)


خب دیگه توضیح کافیه، بریم سر اصل کار 🤠

فرض کنین یک اپلیکیشن فلاتری داریم که در کنار پیاده‌سازی کل مکانیزم‌هایی که بالاتر اشاره شد، ترافیک‌ش رو هم به‌صورت Encrypt شده ارسال و دریافت می‌کنه، مثلا چیزی شبیه به این:

تا قبل از انتشار ابزاری که می‌خوام معرفی کنم، حل این چالش وقت زیادی می‌گرفت. یک روش این بود که با استفاده از Frida Stalker، بررسی Call stack بعد از ارسال ترافیک، Memory Mapping و یا Debug کردن، محدوده آدرس مشکوک رو پیدا می‌کردی، بعد با Disassembler بین اون محدوده Callersها رو بررسی می‌کردی و یکی یکی همه رو Hook می‌کردی تا در نهایت بخش اصلی پیدا بشه.

البته ناگفته نمونه از اونجایی که اکثر اپلیکیشن‌ها از الگوریتم‌های رایج (کپی از اینترنت) استفاده می‌کنن، کلی اسکریپت عمومی Frida برای این منظور وجود داره که بر اساس Pattern بخش مورد نظر رو Hook می‌کنه.

خودمون هم وقتی یک بار بخش مرتبط با Encryption رو پیدا می‌کنیم، می‌تونیم pattern‌ در بیاریم و برای پروژه‌های بعدی به امید اینکه شاید از همون الگوریتم استفاده کرده باشه، ازش استفاده کنیم. ولی خب یه تغییر کوچیک تو الگوریتم‌ رمزنگاری باعث میشه این Patternها دیگه match نشن و مجبور باشیم مراحلی که بالا اشاره کردم رو دستی بریم جلو.

(در خصوص استخراج Pattern و اسکریپت نوشتن برای Frida تو پست‌های قبلی توضیح دادم)

برای حل این چالش، Blutter همون ابزاریه که می‌تونه تا حد زیادی این فرایند رو برامون سریع‌تر کنه:

https://github.com/worawit/blutter

فقط در نظر داشته باشین که:

  • پیشنهاد می‌کنم روی لینوکس یا macOS ازش استفاده کنید، سر ویندوز آخرین باری که تست کردم مشکل زیاد داشت.

  • نسخه‌ و توزیعی از لینوکس که استفاده می‌کنید باید حداقل GCC 13 داشته باشه (من پیشنهادم استفاده از آخرین نسخه Kali هست، چون هم پیش‌نیازها و هم یکسری از ابزارهایی که نیاز داریم رو از قبل نصب داره)

  • این ابزار در واقع Object Pool و Snapshotها رو Parse می‌کنه و مقادیر رو به همراه Offset نمایش میده.

  • از اونجایی که فرمت Snapshot و Object Pool بین نسخه‌های مختلف Dart فرق می‌کنه، فایل رو که بهش میدین، اگه اون نسخه از Dart رو نداشته باشه اول دانلودش می‌کنه. (IPتون نباید ایران باشه)

  • فعلا فقط ARM64 پشتیبانی می‌شه.

استفاده‌ ازش خیلی ساده‌ست، فقط کافیه مسیر lib/arm64-v8a/libapp رو بهش بدین:

python3 blutter.py path/to/app/lib/arm64-v8a out_dir

به این صورت:

حالا اگه فولدری که به‌عنوان خروجی (out_dir) مشخص کردیم رو چک کنید، این چند فایل داخلش ایجاد شده:

  • asm/ : بخش‌های اسمبلی استخراج شده به همراه symbols برای تحیل در Disassemblerها (تو حالتی که میزان Obfuscation شدید باشه، خیلی کاربردی نیست)

  • blutter_frida.js : اسکریپت‌ Frida مورد نیاز برای ادامه کار

  • ida_script/ : اضافه کردنش به IDA باعث خواناتر شدن Disassembly میشه. بر اساس دیتایی که استخراج کرده، فانکشن‌ها رو نامگذاری می‌کنه، کامنت اضافه می‌کنه، label میذاره و...

  • objs.txt : ساختار کامل و درختی از Objectها به همراه referenceشون

  • pp.txt : لیست همه Objectهای موجود در Object Pool (مهم‌ترین فایلی که الان باهاش کار داریم)

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

ساختار کلی فایل pp.txt
ساختار کلی فایل pp.txt

اگه پست‌های قبلی و مخصوصا دوره کرک رو دیده باشید، می‌دونید که توی همچین حالتی میریم سراغ نشونه‌هایی که بتونه ما رو به بخشی که دنبالش هستیم نزدیک‌تر کنه. تو این وضعیت چی قابل‌اتکا و یونیک‌تره؟
ما یه اپ داریم که بخش Body در Request/Response به صورت Encrypt شده به وب‌سرور ارسال/دریافت میشه. پس می‌تونیم همچین Flow ای رو براش در نظر بگیریم:

  • بخش Request:

  1. بخش Body از Request، به صورت plaintext به عنوان آرگومان ورودی به یک فانکشن پاس داده میشه و مقدار Encrypt شده Return میشه.

  2. درخواست نهایی به همراه مقدار تولید شده از مرحله قبل (Body رمز شده) به وب سرور ارسال میشه.

  • بخش Response:

  1. بخش Body از Response، به فانکشن Decryption پاس داده میشه.

  2. خروجی مرحله قبل (دیتای Decrypt شده) به فرمتی مثل JSON تبدیل میشه.

از این Flow فرضی چه پارامترهایی رو داریم؟ در مرحله‌ی قبل تونستیم ترافیک رو با Proxy بگیریم، درسته که بخش Body رمز شده بود، اما این مقادیر رو داریم:

Scheme, Hostname, Port, Path, Params, Method, Headers

از بین این پارامتر‌ها، اونی که بیشتر از بقیه می‌تونیم روی یونیک بودنش حساب کنیم، مقدار Hostname هست. فرض کنید hostname برابر با crypto-feed.example.net باشه، پس می‌تونیم خروجی pp.txt رو با این مقدار فیلتر کنیم.

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

فیلتر کردن بر اساس مقدار hostname (با 3 خط قبل و بعدش)
فیلتر کردن بر اساس مقدار hostname (با 3 خط قبل و بعدش)

همون‌طور که مشخصه، تونستیم مقدار hostnameهمراه با offsetش که برابر با 0xc800 هست رو پیدا کنیم.

اگه دقت کنید مقدار hostnameهمراه با مقادیر مرتبط دیگه مثل apiKey و Content-Type کنار هم قرار گرفتن. البته ترتیب قرارگیری مقادیر در Object Pool همیشه دقیقا همون ترتیب استفاده‌شون در کد نیست، ولی معمولا نزدیک به هم ذخیره میشن و همین می‌تونه یه نشونه بده که مسیر رو درست اومدیم.


اگه Object Pool رو مثل یک جدول دو‌ ستونه در نظر بگیریم، ستون اول شماره ردیف (offset) و ستون دوم مقدار متناظر اون ردیف میشه. ما الان شماره ردیف و مقدارش رو داریم، قدم بعدی اینه که ببینیم در کد دقیقا کجا از این offset استفاده شده. پس نیاز داریم با نحوه دسترسی و استفاده از Object Pool، مفاهیم پایه ARM64 و قراردادهای داخلی Dart VM آشنا باشیم. ولی نگران نباشید، برای چیزی که ما اینجا لازم داریم نیاز به دانش عمیقی نیست و هر نکته‌ای نیازه رو تو همین پست توضیح میدم.

برای یادگیری سریع‌تر معماری ARM، پیشنهاد می‌کنم اول با معماری ساده‌تری مثل x86 شروع کنید و مفاهیم پایه‌ رو یاد بگیرید. بعد در خصوص تفاوت‌هایی که دارن مطالعه کنید، اینجوری درک ساختار ARM خیلی راحت‌تر و سریع‌تر میشه.

در معماری ARM64، خوندن مقادیر از حافظه می‌تونه به روش‌های مختلفی انجام بشه، اما در باینری‌های Flutter معمولا اکثر وقت‌ها با این دو دستور متوالی زیر انجام می‌شه.
مثلا فرض کنید می‌خوایم مقداری با offset = 0x95a7 رو بخونیم:

یا همون مقداری که بالاتر داشتیم (hostname) با offset = 0xc800 رو بخوایم تو کد استفاده کنیم:

در واقع، این دو خط کنار هم یک الگوی مشخص از دسترسی غیر مستقیم به یک مقدار در حافظه (از بخش Object Pool) رو تشکیل میدن که در باینری‌های Flutter و Dart AOT بسیار رایجه.

حالا بریم ببینیم این دو خط دقیقا چکار می‌کنن:

در معماری ARM64، دستور add به صورت پیشفرض فقط می‌تونه تا مقدار 12 بیت (0 تا 4095) رو به‌ صورت مستقیم نگه داره، مثلا اگه بگیم:

add x0, x1, #0xFFF

این مجازه و مشکلی نداره، ولی اگه بخوایم از مقدار بزرگتری مثل 0xC800 استفاده کنیم، چون از محدوده‌ی 12 بیتی خارج میشه دیگه به این صورت نمیشه انجامش داد. برای حل این محدودیت (البته محدودیت که نه، ساختارش به این صورته)، از شیفت 12 بیتی (lsl 12) استفاده می‌کنیم. اینجوری می‌تونیم مقدار اصلی رو در محدوده 12 بیت، مثل 0xC بدیم و بعد با lsl 12 اون رو 12 بیت (یعنی 2 به توان 12 که میشه 4096 برابر) شیفت بدیم تا در واقع مقدار 0xC000 ساخته بشه.
بعد با دستور ldr مقدار 0x800 بهش اضافه می‌کنیم تا به آدرس نهایی 0xC800 برسیم.

خلاصه و ساده‌ش اینجوری میشه که جای اینکه مستقیم از 0xC800 بخونیم، به دلیل ساختار ARM64، اول

0xC000 رو می‌سازیم و بعد 0x800 رو بهش اضافه می‌کنیم تا در نهایت بشه همون 0xC800

حالا این وسط x27 و x16 کارشون چیه؟

  • x27 طبق قرارداد، برای اشاره به Object Pool استفاده میشه و مقدارش در طول اجرای کد ثابت باقی می‌مونه.

  • x16 در ARM64 یک رجیستر موقت (Temporary Register) هست که برای محاسبات موقت استفاده میشه. البته کامپایلر می‌تونه برای این منظور از x17 یا رجیسترهای دیگه هم استفاده کنه.

در x86_64، دسترسی به Object Pool با استفاده از R15 انجام میشه و در ARM32 با استفاده از R5،

ولی ما این پست رو بر اساس ARM64 میریم جلو و به سایر معماری‌ها کاری نداریم.

پس اگه بخوایم این قسمت رو جمع‌بندی کنیم:

  • قسمتی که با کادر قرمز مشخص شده، نسبت به offset تغییر می‌کنه.

  • بخش‌هایی که با کادر سبز مشخص شدن، فعلا ثابت هستن.

  • مقدار x16 که با کادر زرد مشخص شده، معمولا ثابته ولی ممکنه از رجیستر دیگه‌ای مثل x17، x1 و... هم استفاده بشه.

چرا ممکنه تغییر کنه یا فعلا ثابته؟ چون در حال حاضر—طبق قرارداد و ساختار فعلی—مثلا x27 برای اشاره به Object Pool استفاده می‌شه، یا مثلا برای استفاده از مقادیر بزرگ‌تر از 12بیت از lsl 12 استفاده می‌کنیم، ولی ممکنه همین فردا نسخه جدیدی منتشر بشه و این ساختار کامل تغییر کنه، مثلا از یک رجیستر دیگه برای اشاره به Object Pool استفاده بشه یا حتی کل روش فراخوانی عوض بشه.

البته احتمال همچین تغییری کمه. ولی از اونجایی که فلاتر تا الان تغییرات کم نداشته، پس هر تغییری امکان‌پذیره. بریم جلوتر یک نمونه‌ از این تغییرات رو می‌بینیم.


حالا که با نحوه فراخوانی آشنا شدیم، برای ادامه‌ی کار باید این بخش از کد رو در فایل باینری پیدا کنیم.
برای این منظور، Radare2 یکی از بهترین Disassemblerهایی هست که می‌تونیم ازش استفاده کنیم، چون به ما امکان Pattern Search می‌ده و برای این نوع جست‌وجو نیازی به نصب هیچ پلاگینی نداره (حتی توی IDA Pro هم نیازه پلاگین جداگونه نصب کرد).

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

طبق مستندات radare2، برای جستجو در اسمبلی به روش Pattern Search می‌تونیم به این صورت عمل کنیم:

/ad <pattern>

به فرض اگه بگیم:

/ad/ add.*, x27, 0xc, lsl 12;0x800]

یعنی داریم میگیم برو هر جا add.*, x27, 0xc, lsl 12 دیدی که خط بعدش هم 0x800بود، بهمون نمایش بده. حالا اینو شما میتونین به هر مدل دیگه‌ای که نیازه تغییرش بدین، مثلا اگه جای x27 خواستین بگین کل رجیسترها رو بگرده، میشه به این صورت:

/ad/ add.*, x[0-9]+, 0xc, lsl 12;0x800]

پس بعد از باز کردن فایل lib/arm64-v8a/libapp.so در Radare2، مقدار مورد نظرمون رو سرچ می‌کنیم.
اینجوری آدرسی از باینری که مقدار hostname رو فراخوانی کرده پیدا می‌کنیم:

حالا اگه به محدوده این آدرس بریم، سایر مقادیر و پارامترهایی که در Object Pool کنار هم قرار داشتن رو هم می‌تونیم ببینیم:

در واقع هر وقت این الگو رو دیدین، کافیه مقدار offsetش رو داخل فایل pp.txt پیدا کنین تا مقدار اصلیش مشخص بشه (مطابق تصویر بالا و پایین)

تا الان محدوده آدرس مرتبط با Encryption رو پیدا کردیم، ولی هنوز محل دقیقش رو نمی‌دونیم. اینجا نیاز داریم با Calling Convention در Flutter آشنا بشیم:

تا قبل از نسخه 3.4.0، زبان Dart عملا هیچ Convention ثابت و استانداردی برای نحوه مقداردهی آرگومان‎‌های توابع نداشت. یکسری رو می‌ریخت داخل استک و یکسری رو توی رجیسترها ذخیره می‌کرد. همین موضوع باعث شده بود حتی IDA Pro گیج بشه و نتونه ساختار فانکشن‌ها رو به صورت صحیح نمایش بده. (اون زمان JEB عملکرد خیلی بهتری داشت)

اما از v3.4.0 به بعد، استانداردهای ARM64 رعایت شد و بر این اساس 8 آرگومان اول هر تابع در رجیستر‌های x0 تا x7 ذخیره میشن و اگه تعداد آرگومان‌ها بیشتر باشه در Stack قرار می‌گیرن. (البته رجیستر x0 علاوه بر نقش آرگومان اول، برای نگه‌داری مقدار return هم استفاده میشه)

حالا که با Calling Convention آشنا شدیم، برمی‌گردیم به کد بالا، همونطور که در تصویر زیر مشخص شده ما اینجا 3 فانکشن داریم:

یکی از این فانکشن‌ها، مقادیر قبلیش (hostname و سایر پارامترها) را به عنوان ورودی می‎‌گیره و به مرحله بعدی (رمزنگاری، ارسال درخواست و ...) ارسال می‌کنه. پس برای اینکه بتونیم به بخش Encryption نزدیک‎‌تر بشیم نیاز داریم این فانکشن رو پیدا کنیم. ولی چجوری؟

خبر خوب اینکه اکثر وقت‌ها برای پیاده‌سازی Encryption از روش‌های رایج استفاده میشه و معمولا با چیز عجیب غریبی روبرو نیستیم. تو همچین حالتی میایم آرگومان‌های ورودی یا مقدار return شده‌ی فانکشن‌هایی که بلافاصله بعد از فراخوانی پارامترهایی مثل hostname قرار دارند رو با Hook کردن Log می‌کنیم. (اکثر وقت‌ها این روش جواب میده)

ممکنه این سوالات براتون پیش بیاد:

  • تا چند فانکشن بعدی رو بررسی کنیم؟ معمولا حداکثر تا 10 فانکشن بعدی کافیه. (پیدا کردنش سخت نیست اصلا)

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

  • لازمه کد رو خط‌به‌خط تحلیل کنیم؟ ترکیب تحلیل استاتیک و داینامیک، قطعا همیشه بهترین انتخابه. ولی واقعیت اینجاست در مبحث Bypass مکانیزم‌های رایج امنیتی با فرض اینکه از الگوریتم و روش خیلی خاصی استفاده نشده باشه، اکثر وقت‌ها خیلی اونقدری نیاز به تحلیل عمیق کد پیدا نمی‎‌کنیم. مثلا اینجا خروجی فانکشن اول در [x29, -0x18] قرار گرفته (آدرس 0x0040997c) و بعد در خط 0x00409990، این مقدار به‌ عنوان آرگومان اول به فانکشن سوم پاس داده میشه. پس احتمالا فانکشنی که دنبالشیم همین فانکشن سوم باشه. (این فقط یک احتمال منطقیه و ممکنه درست نباشه، دقیق‌ترین روش اینه که مقدار x1 یا [x29-0x18] رو با یه Hook کردن ساده Log بگیریم.)

اسکریپتی که برای Hook کردن این فانکشن‌ها نیاز داریم رو از قبل Blutter با نام blutter_frida.js ایجاد کرده. کافیه فانکشن onLibappLoaded از این فایل رو Edit کنیم و آدرس فانکشن مد نظرمون رو به همراه شماره (index) آرگومانی که می‌خوایم Log کنه رو بهش بدیم، مطابق تصویر:

اگه با نوشتن اسکریپت برای Frida آشنایی ندارید، پیشنهاد میکنم پست‌های مرتبط قبلی رو مطالعه کنید.

همونطور که مشخصه آرگومان اول این فانکشن ظاهرا همون Body ای هست که قراره در مرحله بعد Encrypt بشه:

اگه وارد این فانکشن (fcn.0029c15c) بشیم، همونطور که می‌بینید داخلش کلی فانکشن دیگه وجود داره. تا اینجا مشخص شده که این فانکشن، مقدار Body رو به عنوان ورودی می‌گیره، بنابراین به احتمال خیلی زیاد یکی از این فانکشن‌های داخلی، باید همون Encryption باشه. ولی کدوم یکی؟

  • یک روش می‌تونه این باشه که مثل قبل، با دنبال کردن الگوهای فراخوانی از Object Pool و پیدا کردن Offsetها در فایل pp.txt، فانکشن‌های مشکوک رو جدا کنیم و بعد با Hook کردن تک تک‌شون به فانکشنی که دنبالش هستیم برسیم.

  • یا میتونیم مقدار Return فانکشن‌ها رو Log کنیم. طبیعتا فانکشنی که Encryption رو انجام میده، باید مقدار Encrypt شده رو Return کنه.

  • ولی روش خیلی سریع‌تر اینه که برعکس بریم جلو، حالا این یعنی چی؟

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

afn encrypt_request_body

یه نگاه سریع به روش رمزنگاری AES بندازیم. برای کار با AES به چند پارامتر نیاز داریم:

  • Key — کلید رمزنگاری

  • Mode — مثل CBC, CTR, GCM و …

  • Padding — وقتی طول دیتا دقیقا مضربی از اندازه بلاک AES نباشه، بلاک آخر به کمک یک روش Padding باید کامل بشه تا به اندازه بلاک برسه.

  • IV / Nonce — مقداری که برای شروع Encryption استفاده میشه تا خروجی هر بار متفاوت باشه.

البته ما تو این پست کاری به جزئیات این پارامترها نداریم. توضیحات بالا فقط برای آشنایی کلی اورده شدن.

اگه به نمونه کد زیر که برای Decrypt/Encrypt با استفاده از AES هست دقت کنید، پارامترهایی که با کادر قرمز مشخص شدن از اونجایی که به صورت String هستن، احتمال داره بشه داخل pp.txt پیداشون کرد.

از بین این پارامتر‌ها، مقدار key و iv که هیچی، درسته که string هستن، ولی اگه مقدارشون رو می‌دونستیم که از همون اول کار تموم بود! می‎‌مونه mode و padding. مقادیری که این دوتا می‌تونن بگیرن زیاد نیست، پس با یه جستجوی ساده توی فایل pp.txt می‌شه پیداشون کرد.

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

اگه به تصویر زیر که مربوط به بخشی از فایل pp.txt هست دقت کنید، می‌بینید که پارامتر mode برابر با cbc و پارامتر padding برابر با PKCS7 هست. مقدار Offset هرکدوم هم که مشخصه. پس با همون روش pattern search که قبلا انجام دادیم، می‌تونیم اینجا هم محل فراخوانی این پارامترها رو پیدا کنیم. جایی که این پارامترها استفاده شدن، به احتمال زیاد همون تابع اصلی Decrypt/Encryptه.

سرچ رو بر اساس مقدار mode میریم جلو، یعنی 0xc8b0 ، که میشه به این صورت:

طبق خروجی بالا، این مقدار دوبار توی کد فراخوانی شده، که منطقیه — یک بار برای Decrypt و بار دیگه برای Encrypt.

حالا اگه به آدرس 0x0026fda4 بریم، به این فانکشن می‌رسیم:

همونطور که مشخصه (کادر زرد)، این فانکشن از دو جا call شده که یکی از اون‌ها همون encrypt_request_bodyه که مرحله‌ی قبل بهش رسیده بودیم. اینم کاملا منطقیه، چون توی encrypt_request_body باید یک فانکشن صدا زده بشه که مقادیر رو بگیره و Encrypt کنه. بنابراین مسیر رو درست اومدیم و فانکشنی که الان داخلش هستیم وظیفه Encrypt کردن رو داره. اون یکی فانکشن، یعنی fcn.0026fa18 هم به احتمال زیاد فانکشن مرتبط با Decrypt باشه.

خب حالا برای راحتی بیشتر میایم به این فانکشن هم یک اسم میدیم، مثلا :

afn AES_Encryptor

حالا اگه به encrypt_request_body برگردیم، فانکشن AES_Encryptor با اسمی که براش تعیین کردیم نمایش داده شده و به این روش تونستیم خیلی راحت و سریع، فانکشنی که وظیفه Encrypt کردن داره رو پیدا کنیم:

حالا چه کار کنیم؟ درست حدس زدید — آرگومان‌های ورودیش رو بررسی می‌کنیم. طبق Calling Convention اینجا دو آرگومان داریم: x1 و x2. کدومو بررسی کنیم؟ هر دو رو — البته با خوندن کد می‌شه فهمید کدوم حساس‌تره، ولی تو این پست در کل هدف اینه تا جای ممکن وقت سر تحلیل کد نذاریم.
در نهایت با Hexdump گرفتن از آرگومان دوم (x2) به اندازهٔ 500 بایت، می‌تونیم به key برسیم:

  • چرا 500 بایت؟

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

  • اگه با روش زیر آشنایی ندارید، در پست‌های قبلی توضیح دادم.

خروجی به صورت زیر خواهد بود:

key: 123456789012345678901234

تا اینجا تونستیم مقدار پارامتر key و mode رو پیدا کنیم، ولی برای Decrypt کردن نیاز داریم مقدار یک پارامتر دیگه رو هم بدونیم که اون iv هست.

دقت کنید که پارامتر iv نباید ثابت باشه، ولی معمولا برای راحتی کار ثابت در نظر گرفته می‌شه. پس وقتی به iv رسیدین، حتما چک کنین ببینین توی هر درخواست مقدارش تغییر می‌کنه یا ثابته.

اگه فرض کنیم توسعه‌دهنده از کتابخونه و الگوریتم‌های رایج استفاده کرده باشه، پارامتر iv معمولا بعد از ایجاد Object از کلاس AES (بعد از Constructor) مقدار دهی می‌شه. طبق نمونه کد زیر:

بنابراین، اگه به encrypt_request_body برگردیم، پارامتر iv باید در یکی از این فانکشن‌ها مقداردهی بشه:

حالا کدوم فانکشن؟ یک روش اینه که آرگومان‌های ورودی چند فانکشن‌ بعدی رو دونه‌دونه چک کنیم. این راه در نهایت جواب میده ولی اگه 20 تا فانکشن داشتیم چطور؟ یکی یکی همه رو Hook کنیم!؟ پس بهتره یکم دقیق‌تر بریم جلو.
اگه به نمونه کد بالا دقت کنید، در پکیج encrypt در Dart، فانکشنی که پارامتر iv رو مقداردهی می‌کنه، مقدار plaintext رو هم به عنوان ورودی گرفته. پس به جای هوک کردن همه فانکشن‌ها، فقط سراغ اونایی میریم که در آرگومان‌های ورودی‌شون plaintext رو هم دارن، این باعث میشه تعداد فانکشن‌هایی که باید بررسی کنیم خیلی کمتر بشه. حالا سؤال: plaintext رو از کجا پیدا کنیم؟

اگه یادتون باشه موقعی که encrypt_request_body فراخوانی می‌شد، x1 همون مقداری بود که به‌ عنوان plaintext به عنوان آرگومان ورودی پاس داده می‌شد. این پارامتر طبق Calling Convention میشه آرگومان دوم و اگه محل فراخوانی هم بررسی کنیم، radare2 هم این موضوع رو برامون مشخص کرده:

پس کافیه بریم داخل فانکشن و بررسی کنیم که آرگومان دوم به چه صورت و کجا استفاده شده:

همونطور که مشخصه آرگومان دوم در آفست 8 از Stack Pointer قرار گرفته که به صورت [8- , x29] نمایش داده میشه. پس باید بگردیم فانکشن‌هایی رو پیدا کنیم که [8- , x29] رو به عنوان آرگومان ورودی دارن. می‌رسیم به fcn.0029c118 و fcn.0029c254 :

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

در نهایت، با Hook کردن آرگومان x3 در فانکشن دوم (fcn.0029c254)، به مقدار iv می‌رسیم:

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

الان تموم پارامترهای مورد نیاز برای Decrypt رو داریم: 😍

algorithm: AES mode: CBC key: 123456789012345678901234 iv: abcdefghijklmnop

حالا کافیه با هر ابزار یا روشی که براتون راحت‌تره مثل CyberChef مقدار مورد نظرتون رو Decrypt کنین:

البته در فرایند تست نفوذ، ما به Intercept ترافیک نیاز داریم و انجام دستی Encrypt/Decrypt روش مناسبی نیست. تو این حالت اگه با Burp Suite کار می‌کنین، خیلی راحت می‌تونین یه Extension براش آماده کنین، الان با کمک AI این موضوع به سادگی و خیلی سریع قابل انجامه. یا اگه Bug Bounty کار می‌کنین، پیشنهاد می‌کنم یه ابزار GUI ساده برای این فرایند آماده کنین. آماده کردن همچین چیزایی خیلی وقت‌ها می‌تونه باعث Bonus بشه.

خلاصه اینکه وقتی پارامترهای Encrypt/Decrypt رو پیدا کردین، از اینجا به بعد با توجه به پروژه و هدف‌تون بهترین روش رو برای ادامه کار انتخاب کنین.

خب بالاخره این پست هم تموم شد 😍

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

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

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