مبانی معماری امنیت اندروید – بخش اول
مقاله
  • ۲۴ شهریور ۱۴۰۱
  • Learning Road Map
  • ۱۲ دقیقه خواندن

مبانی معماری امنیت اندروید – بخش اول

آکادمی راوین

پیش‌گفتار

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

معماری اندروید - آکادمی راوین

در گام اول با هم نگاهی به اولین لایه‌ی سیستم عامل یعنی هسته لینوکسی اندروید خواهیم انداخت و در گام بعدی سطح کاربر یا Native Userspace که بر روی هسته لینوکسی قرار دارد را بررسی می کنیم. سپس به توضیح ماشین مجازی Dalvik می‌پردازیم و با این بخش حیاتی اندروید بیشتر آشنا خواهیم شد. در گام چهارم به تفصیل درباره کتابخانه های Runtime جاوا توضیحاتی را ارایه خواهیم داد. گام بعدی به توضیح سرویس‌های سیستمی اندروید می‌پردازد و در این مرحله می‌توان به شرح Inter-Process Communication پرداخت که سیستم عامل اندروید از مکانیزمی به نام Binder استفاده می‌کند و به تفصیل امنیت این بخش مهم را بررسی خواهیم کرد. در گام بعدی درباره‌ی کتاب‌خانه‌های Framework اندروید صحبت می‌کنیم. در نهایت در گام‌های هفتم و هشتم به بررسی اپلیکیشن‌هایی که بر روی این معماری قرار می‌گیرند خواهیم پرداخت که شامل اپلیکیشن‌های سیستمی و اپلیکیشن‌های سطح کاربر می‌شود.

۱- هسته‌ی لینوکس (Linux Kernel)

همان‌طور که در تصویر قبل مشاهده کردید، سیستم عامل اندروید بر روی یک هسته‌ی لینوکسی پیاده سازی شده است. همان‌گونه که در تمام سیستم‌های مبتنی بر UNIX نیز صدق می‌کند، هسته وظیفه‌ی ارایه‌ی درایور برای سخت افزار و شبکه، دسترسی به فایل سیستم و مدیریت سایر فرآیندها را بر عهده دارد. با قابلیتی که پروژه‌ی Mainlining اندروید به ما می‌دهد، می‌توان اندروید را بر روی هسته جدیدی به نام Vanilla در اینترنت قابل دسترس است، پیاده سازی کرد. با این حال هسته‌ی اندروید کمی با هسته‌ی لینوکس‌های متداول مثل Debian، Fedora و غیره تفاوت دارد.
دلیل اصلی این تفاوت‌ها به دلیل وجود یک سری قابلیت‌های اضافه است که در اندروید به وجود آمده و بعضی اوقات با نام Androidisms نیز نامیده می‌شود. این قابلیت‌ها به هسته اضافه شده تا از ویژگی‌های اندروید پشتیبانی کند. بعضی از مهم‌ترین این قابلیت‌ها عبارتند از Low Memory Killer، Annonymous،wakelocks ، paranoid networking، alarms، shared memory و binder.
مهم‌ترین نکته در خصوص Androidisms برای ما موارد Binder و Paranoid Networking است. Binder مکانیزمی است که IPC و همچنین یک مکانیزم امنیتی تعاملی را پیاده‌سازی می‌کند. که در بخش‌های بعدی بیشتر درمورد این مکانیزم شرح داده شده است. قابلیت Paranoid Networking نیز دسترسی به سوکت‌های شبکه به اپلیکیشن‌هایی که یک مجوز مشخص را دارند، محدود می‌کند.

۲- فضای کاربر (Native Userspace)

فضا یا لایه‌ی کاربر، لایه‌ای است که بر روی هسته قرار گرفته و شامل فایل باینری Init می‌شود. در واقع Init همان پروسه‌ی اولی است که هنگام بالا آمدن سیستم عامل ایجاد شده و تمام پروسه‌های دیگر، زیرمجموعه‌ی آن خواهند بود. هم‌چنین یک سری daemonهای Native و چند هزار کتابخانه Native دیگر که در سیستم‌عامل مورد استفاده قرار خواهند گرفت، در این بخش وجود دارند.
در حالی‌که باینری Init و همچنین daemonهای دیگر، یک بخش سنتی از سیستم‌عامل‌های مبتنی بر لینوکس به شمار می‌آید اما باید به این نکته دقت داشت که در اندروید، هم Init و هم اسکریپت‌های Startup دوباره از اول توسعه داده شده‌اند و مقدار کمی با آن چیزی که در لینوکس وجود دارد، متفاوت هستند.

۳- ماشین مجازی Dalvik 

بخش زیادی از اندروید با استفاده از زبان برنامه نویسی جاوا پیاده سازی شده است و در نتیجه به صورت طبیعی در Java Virtual Machine اجرا می شوند. ماشین مجازی که اندروید از آن استفاده می‌کند، Dalvik نام دارد و یک لایه دیگر در stack ما به حساب می‌آید. دقت داشته باشید که دالویک برای دستگاه‌های موبایل طراحی شده و به صورت مستقیم نمی‌تواند Byte Codeهای جاوا را اجرا کند. فرمت پیش‌فرض آن، به نام Dalvik Executable یا DEX شناخته شده و به صورت فایل هایی با پسوند .dex در فایل سیستم ذخیره می‌شوند. در نتیجه، فایل های dex هم می توانند در کتاب‌خانه‌های جاوا یا jar مشاهده شوند و هم می‌توانند در برنامه‌های اندرویدی یا apk وجود داشته باشند.
نکته‌ای که باید در این‌جا به آن اشاره کنیم این است که Dalvik و Oracle از دیدگاه معماری با هم تفاوت دارند. اگر بخواهیم اشاره‌ی کوچکی به معماری آن‌ها داشته باشیم باید بگوییم که معماری Dalvik به صورت Register-Base و معماری Oracle به صورت Stack-Base است. بنابراین طبیعی است که از دستورات مختلفی نیز برای اجرای کد استفاده می‌کنند. همان‌طور که می‌دانید، بارگذاری کد بر روی registerها هزینه و سربار کمتری نسبت به ارسال و اعمال دستورات مختلف، به سیستم تحمیل می‌کند. در نتیجه معماری Register-Base سریع‌تر از معماری stack-base است اما محدودیت‌هایی را نیز به همراه خواهد داشت.
دقت داشته باشید که امروزه در اغلب دستگاه‌های تولید شده، کتاب‌خانه‌های سیستمی و اپلیکیشن‌های از پیش نصب شده، کدهای dex وجود ندارند. در حقیقت با پیشرفت معماری اندروید، کدهای dex با فرمت دیگری که بهبود یافته‌تر از dex هستند، ذخیره می‌شود و آن فرمت odex است. این پسوند به صورت معمول در همان دایرکتوری قرار دارد که فایل‌های jar یا apk مربوط به آن نیز وجود دارند. رویکرد مشابهی نیز برای بهبود فرآیند نصب اپلیکیشن‌هایی که کاربر اقدام به نصب آن می‌کند، تعبیه شده است که شرح آن در این بخش نمی‌گنجد.

۴- کتابخانه های زمان اجرای جاوا (Java Runtime Libraries)

پیاده‌سازی زبان جاوا به کتابخانه‌های زمان اجرا یا همان Java Runtime Libraries نیاز دارد که اغلب به صورت فایل‌های java.something یا javax.something شناخته می‌شوند. کتابخانه‌های اصلی جاوا که برای اندروید ایجاد شده‌اند، مربوط به پروژه‌ای به نام Apache Harmony هستند. همان‌طور که اندروید در طول زمان، رفته رفته تکامل پیدا کرده است، کدهای Harmony نیز تغییر و تکامل پیدا کرده‌اند. در این فرآیند بعضی از قابلیت‌ها نیز به صورت کامل تغییر پیدا کرده‌اند. به عنوان مثال می‌توان به Internationalization Support یا Cryptographic Provider و کلاس‌هایی که به این دو بخش مرتبط هستند اشاره کرد در حالی‌که قابلیت‌های دیگر توسعه داده شده و پیشرفت کرده‌اند. کتابخانه‌های اصلی اغلب به زبان جاوا تولید شده‌اند اما یک سری کدهای Native نیز در این بین وجود دارند. این کد های Native به وسیله Java Native Interface، به کتابخانه‌های جاوایی که در اندروید وجود دارند، لینک می‌شود و این امکان را به وجود می‌آورد تا کدهای جاوا بتوانند کدهای Native را Call کنند و برعکس. کتاب‌خانه‌های Runtime جاوا به صورت مستقیم هم از طریق سرویس‌های سیستمی و هم از طریق اپلیکیشن‌ها قابل دسترس هستند.

۵- سرویس‌های سیستمی (System Services) در اندروید

لایه‌هایی که تا اینجا به بررسی اجمالی آن‌ها پرداختیم، در واقع بخش‌های پایه جهت پیاده‌سازی بخش اصلی اندروید که سرویس‌های سیستمی هستند، محسوب می‌شوند. تعداد سرویس‌های سیستمی در اندروید نسخه‌ی 4.4 برابر با 79 سرویس بوده است در حالی‌که این تعداد در اندروید نسخه‌ی 10 به 108 سرویس رسیده است. این سرویس‌های سیستمی اغلب قابلیت‌های زیرساختی در اندروید را فراهم می‌کنند که شامل Display، Touch Screen، Support، Telephony و Network Connectivity می‌شود. همان‌طور که می‌توان انتظار داشت، بیشتر سرویس‌های سیستمی با استفاده از جاوا پیاده‌سازی شده‌اند، اما بعضی از  آن‌ها نیز به زبان Native هستند. بدون در نظر گرفتن موارد استثنا، هر سرویس سیستمی در اندروید، به صورت پیش‌فرض شامل یک Interface است تا به صورت Rremote توسط سرویس‌های دیگر و یا اپلیکیشن‌ها قابل فراخوانی باشد. به همراه Service Discovery، Mediation و IPC که توسط Binder فراهم شده است، سرویس‌های سیستمی قابلیت Object Oriented را بر روی لینوکس به خوبی فراهم کرده‌آند. یکی از بخش های اصلی مدل امنیتی اندروید که در این قسمت می‌خواهیم درباره آن صحبت کنیم، IPC می‌باشد که به وسیله‌ی Binder پیاده‌سازی شده است.

۵-۱- ارتباطات بین پروسه‌ای (Inter-Process Communication) در اندروید

همان‌طور که در بخش قبل نیز به آن اشاره شد، Binder یک مکانیزم ارتباط بین پروسه ای یا همان IPC است. اما قبل از آن که به سراغ جزییات Binder برویم، بهتر است تا نگاه مختصری به IPC بیندازیم.
مطابق تمام سیستم عامل‌های مبتنی بر  UNIX، پروسه‌ها در اندروید آدرس‌های جدا از  یکدیگر دارند و یک پروسه نمی‌تواند به صورت مستقیم به حافظه‌ی پروسه‌ی دیگر دسترسی داشته باشد. به این فرآیند Process Isolation هم گفته می‌شود. چنین مکانیزمی هم از نظر پایایی یا Stability و هم از نظر افزایش امنیت اغلب بسیار مفید واقع می‌شود. اگر چند پروسه بتوانند به یک فضای حافظه مشترک دسترسی داشته یا آن را تغییر دهند، این امکان وجود دارد که فاجعه‌ای رخ دهد. به عنوان مثال احتمالا شما علاقه‌ای ندارید تا یک پروسه‌ی هرز که توسط کاربر دیگری ایجاد شده است، اطلاعات موجود در ایمیل شما را با دسترسی داشتن به حافظه Mail Client، استخراج کند. با این حال، اگر پروسه ای بخواهد یک سرویس کاربردی را به پروسه‌ی دیگری ارایه دهد، نیاز دارد تا یک سری فرآیند را برای پیدا شدن توسط پروسه های دیگر طی کند و به اصطلاح با آن‌ها Interact داشته باشد. به زبان ساده به این فرآیندها IPC می‌گویند.
امروزه نیاز به یک IPC که استاندارد هم باشد، مقوله‌ی جدیدی نیست. بعضی از قابلیت‌های اندروید به این مکانیزم نیاز دارند مثل فایل‌ها، سیگنال‌ها، سوکت‌ها، pipe، smephore و حافظ‌های اشتراکی، message queues و بسیاری از موارد دیگر. لازم به ذکر است اندروید به بعضی از این قابلیت‌ها نیاز دارد و از آن‌ها استفاده می‌کند مانند Local Sockets، اما بعضی‌ها را نیز پشتیبانی نمی‌کند، به عنوان مثال می‌توان به System IPCهایی مانند semaphore یا shared memory segments و message queues اشاره کرد. حال که به صورت مختصر با IPC آشنا شدیم، نوبت آن است تا به بررسی Binder بپردازیم.

۵-۲- Binder

به دلیل عدم انعطاف کافی IPC استاندارد و این که به اندازه کافی نیز قابل اطمینان نبود، مکانیزم IPC جدیدی به نام Binder برای اندروید توسعه داده شد. این مکانیزم از روی معماری و ایده‌ی پروژه‌ای به نام OpenBinder تولید شده است.
این مکانیزم معماری اجزای توزیع شده را بر اساس یک Abstract Interface پیاده‌سازی می‌کند. Binder شباهت‌های زیادی به Windows Common Object Model یا COM و همچنین Common Object Broker Request یا COBRA در UNIX دارد. اما بر خلاف این فریم‌ورک‌ها، بر روی فقط یک دستگاه اجرا می‌شود و RPC یا Remote Procedure Call ها را بر روی شبکه پشتیبانی نمی‌کند. اما با این حال می‌توان RPC را بر روی Binder پیاده‌سازی کرد. توضیح کامل این که Binder چیست و چه کاری را انجام می‌دهد، خارج از بحث ماست ولی ماژول‌های اصلی آن را به صورت کلی در ادامه شرح می‌دهیم.

۵-۳- پیاده‌سازی Binder

همان‌طوری که در بخش قبل نیز شرح داده شد، در سیستم‌های شبه UNIX، یک پروسه نمی‌تواند به حافظه‌ی پروسه‌ی دیگر دسترسی داشته باشد. با این حال، هسته روی همه‌ی پروسه‌ها کنترل دارد و در نتیجه امکان افشای آن اینترفیسی که IPC را فعال می‌کند، همواره وجود دارد. در Binder، این اینترفیس در دایرکتوری /dev/binder می‌باشد که توسط Binder Kernel Driver پیاده‌سازی شده است. در نتیجه Binder یک شی اصلی فریم‌ورک است و همه‌ی IPC Call ها از طریق آن انجام می‌شود. ارتباطات درون پروسه‌ای نیز به وسیله‌ی ioctl() پیاده سازی شده است که هم ارسال و هم دریافت اطلاعات از طریق ساختار Binder_Write_Read انجام می‌شود که شامل 2 بخش است. بخش اول، Write_Buffer است که شامل دستوراتی برای درایور می‌باشد و بخش دوم، Read_Buffer که شامل دستوراتی است که در Userspace مورد نیاز هستند.
اما سوالی که در اینجا مطرح می‌گردد این است که چگونه اطلاعات یا داده بین پروسه‌ها انتقال داده می‌شود؟ جواب ساده است. Binder Driver وظیفه مدیریت بعضی از فضای آدرس هر پروسه را به عهده دارد. Binder Driver بخشی از حافظه که برای پروسه‌ی مربوطه Read Only هست را مدیریت می‌کند. اما همه‌ی Writingها توسط ماژول هسته اتفاق می‌افتد. زمانی که یک پروسه یک پیامی را به یک پروسه دیگر ارسال می‌کند. هسته مقداری از حافظه را به پروسه مقصد اختصاص می دهد و به صورت مستقیم، پیامی که از پروسه ارسال کننده به سمت پروسه مقصد ارسال می‌شود را در آن فضا کپی می‌کند. در این صورت، یک Queue شکل پیدا می‌کند که در آن پیام کوچکی که به سمت گیرنده ارسال شده، وجود دارد و به پروسه مقصد می‌گوید که در کدام آدرس حافظه می‌تواند پیام را مشاهده کند. در این حالت، گیرنده می‌تواند به صورت مستقیم به پیام دسترسی پیدا کند، چرا که در محدوده‌ی فضای حافظه خودش قرار دارد. هرگاه پروسه‌ای که پیام در فضای حافظه‌ی آن قرار دارد خاتمه پیدا کند، به Binder Driver می‌گوید که آن قسمت از حافظه را آزاد کند. در شکل زیر معماری Binder IPC نمایش داده شده است.

معماری Binder IPC - آکادمی راوین

Abstractionهای لایه‌های بالاتر از IPC مانند Intent، Messengers و ContentProviders بر روی binder ساخته می‌شوند. به‌علاوه، اینترفیس‌های سرویسی که نیاز به افشای آن‌ها وجود دارد، می‌توانند با استفاده از Android Interface Definition Language یا AIDL ساخته شوند که این امکان را به Clientها می‌دهد تا درصورتی که آن Clientها، Objectهای محلی جاوا باشند، سرویس‌های Remote را Call کنند. ابزار اشتراکی AIDL به صورت خودکار 2 بخش را تولید می‌کند. Stubs که در واقع تصویری از Remote Objectهایی از سمت کاربر هستند و proxies که اولا متودهای اینترفیس مربوطه را به متود لایه پایین transact() نگاشت می‌کند و ثانیا پارامترها را به قالبی که binder توان انتقال آن را داشته باشد، تبدیل می‌کند (که به آن parameter marshalling/unmarshalling نیز گفته می شود). از آن‌جایی که Binder به صورت اجدادی Typeless است، AIDL کار تولید Stubs و Proxies  را انجام می‌دهد و همچنین یک مکانیزم امن را به وسیله‌ی قرار دادن اینترفیس مقصد برای هر Transaction در Binder (در Proxy) که در نهایت در Stubs تایید می گردند، فراهم می‌کند.
 ‌ 

ویدیوی شرح مقاله توسط «محمدرضا تیموری»

ادامه‌‌ی این مطلب را می‌توانید در بخش دوم مقاله که به زودی منتشر می‌شود، مطالعه کنید. همچنین با گذارندن دوره‌ی «مبانی هک وب و موبایل» این مطلب را به همراه بسیاری از مطالب دیگر به صورت عملی بیاموزید.