вміст proc/maps

Виявлення динамічного завантаження в програмах Android за допомогою /proc/maps

TL; DR: Завдяки динамічному завантаженню автори зловмисного програмного забезпечення можуть приховано завантажувати шкідливий код у свою програму, щоб уникнути виявлення. Ми можемо виявити таке завантаження через створений ядром файл /proc/[PID]/maps.
Нещодавно ми створили простий сценарій, який дозволяє нам виявляти динамічне завантаження в програмах Android. Це дало нам гарну можливість обговорити динамічне навантаження в цілому в цьому блозі. 

Динамічне завантаження та посилання:

Щоб зрозуміти решту блогу, важливо розглянути основи зв’язування та завантаження, а також підкреслити різницю між зв’язуванням і завантаженням, статичним і динамічним.

Що таке посилання:

Процес компіляції складається з кількох частин. Зв’язування — це останній крок перед тим, як ми отримаємо запуск виконуваний файл. Зазвичай програма — це щось більше, ніж простий самодостатній файл, і для роботи вона покладається на інші бібліотеки або файли. Таким чином, вам недостатньо скомпілювати свій код у машинний код, щоб мати можливість його запускати, але також якось link різні файли в єдиний виконуваний файл.

Як це робиться на практиці? Після компіляції та складання асемблер виводить об’єктні файли, які зазвичай відповідають кожному модулю вашої програми. Такі файли можуть бути переміщуваними або виконуваними. Об'єктні файли першого типу повинні пройти зв'язування перед тим, як їх можна буде запустити, але файли другого типу можуть бути виконані негайно. В екосистемі GNU/Linux звичайним розширенням об’єктного файлу є .o. Спільні об’єктні файли, відомі як бібліотеки, є переміщуваними об’єктними файлами, які призначені для використання багатьма різними програмами та не можуть запускатися окремо. У GNU/Linux вони мають розширення .so (Спільний об’єкт), тоді як у Windows вони мають розширення .dll (Бібліотека динамічного компонування).

Потім компонувальник бере файли, розв’язує символи (функції та змінні) і вказує їм правильні адреси пам’яті, записуючи все в таблицю символів виконуваного файлу. Це також заради ефективності переміщається ваш код, щоб пов’язані фрагменти коду в кінцевому підсумку відображалися на найближчі адреси пам’яті, незалежно від того, як ви спочатку організували свою програму, оскільки читабельність більше не є проблемою, а продуктивність є головним пріоритетом. 

Багато різних назв виконуваного файлу:

І в Linux, і в Windows ці, здавалося б, різні розширення мають однакові основні формати, що й виконувані файли. У Windows це PE (Portable Executable) і зазвичай мають розширення .exe та .dll, а в Linux це ELF (Executable and Linkable File) і мають розширення .bin, .so або .o (Так , об’єктні файли, про які ми згадували раніше, також є ELF).

Динамічне зв'язування:

Ми говоримо, що щось було динамічно зв’язано, коли замість виконання цього процесу під час компіляції, це виконується під час завантаження або виконання. Це підводить нас до завантаження: завантажувач просто копіює вміст виводу компонувальника в пам’ять і запускає програму. Коли використовується динамічне зв’язування, процес зв’язування відбувається безпосередньо перед завантаженням, під час запуску програми, що часто призводить до плутанини між ними.
Динамічне завантаження, з іншого боку, означає, що частини коду можуть бути завантажені в пам’ять у будь-який момент під час виконання. Ці два процеси можуть і часто виконуються разом.

Чому це проблема?

Динамічне завантаження, безумовно, корисно. Наприклад, замість того, щоб завантажувати всі бібліотеки, які ваша програма використовуватиме під час завантаження, ви можете завантажувати їх лише тоді, коли вам потрібно їх використати, таким чином використовуючи менше пам’яті, або лише умовно завантажувати їх у певних випадках.

Але це також пропонує простий спосіб для розробників зловмисного програмного забезпечення приховати свій шкідливий код. Вони можуть помістити весь свій легітимний код у APK і перемістити весь мерзенний код у DEX (виконуваний файл Dalvik), який програма завантажить, а потім динамічно завантажуватиме під час використання, і таким чином їх програма буде виглядати нешкідливою після основної статичної перевірки APK.

Як завантажуються класи DEX:

Android пропонує опцію динамічного завантаження файлів .dex за допомогою класу під назвою DexClassLoader. Щоб завантажити клас, нам просто потрібно написати:

// Init the loader
DexClassLoader dexClassLoader = new DexClassLoader(path_to_dex, null, null, parent_class);

// Load the class:
Class dynamic_class = dexClassLoader.loadClass("DynamicClass");

// Load a method we could call it
Method method = dynamic_class.getMethod("method1");

І тоді ми можемо використовувати метод invoke() для використання методу.

Файл /proc/[PID]/maps:

В Unix, все є файлом, і навіть якщо він насправді не один, він обробляється та отримується як один. Це включає структури даних ядра, і Linux не є винятком із правил. Ядро Linux дозволяє нам отримувати доступ і читати його структури даних через /proc/ псевдофайлова система. Тоді кожен процес має власну папку в /proc/[PID]. Файли та вкладені папки містять багато корисної та важливої ​​інформації про процес, але сьогодні ми зосередимося лише на одному файлі: /proc/[PID]/maps.

/proc/[PID]/maps відображає діаграму відображеної пам’яті процесу. Коли ми говоримо про відображену пам’ять, ми маємо на увазі сегмент віртуальної пам’яті, який має однозначну відповідність файлу. Це відображення дозволяє програмі змінювати та отримувати доступ до файлів шляхом читання та запису безпосередньо в пам’ять. Це означає, що коли програма звертається до файлу, це буде записано у її файлі /proc/[PID]/maps.

/proc/[PID]/maps також показує нам, які дозволи процес має для кожного сегмента. Це може допомогти нам визначити, які файли процес редагував, а які він прочитав.

Ось як виглядає короткий фрагмент звичайного файлу /proc/PID/maps:

7f9cefbf7000-7f9cefbf8000 r--p 00000000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf8000-7f9cefbf9000 r-xp 00001000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf9000-7f9cefbfa000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfa000-7f9cefbfb000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfb000-7f9cefbfc000 rw-p 00003000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfc000-7f9cefc08000 r--p 00000000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc08000-7f9cefc1b000 r-xp 0000c000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc1b000-7f9cefc24000 r--p 0001f000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc24000-7f9cefc25000 r--p 00027000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc25000-7f9cefc26000 rw-p 00028000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc26000-7f9cefc27000 r--p 00000000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc27000-7f9cefc28000 r-xp 00001000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc28000-7f9cefc29000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc29000-7f9cefc2a000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2a000-7f9cefc2b000 rw-p 00003000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2b000-7f9cefc47000 r--p 00000000 103:03 1584005            /usr/lib/libX11.so.6.3.0
7f9cefc47000-7f9cefcd1000 r-xp 0001c000 103:03 1584005            /usr/lib/libX11.so.6.3.0

Кожен рядок у файлі записує один сегмент пам’яті в безперервному адресному просторі віртуальної пам’яті, виділеному для процесу.

  • адреса – Перший стовпець показує початкову та кінцеву адресу сегмента.
  • Дозволи – У цьому стовпці показано, які дозволи має процес для сегмента. r/w/x є звичайним читанням/записом/виконанням, тоді як остання літера є s or p, що означає спільний або приватний відповідно.
  • Зсув – Це зміщення від початку файлу, щоб можна було обчислити початкову адресу зіставлених даних. Іноді сегмент не зіставляється з файлу (у цьому випадку стовпець шляху матиме ідентифікатор природи сегмента, як пояснюється нижче), у такому випадку зміщення просто залишається рівним 0.
  • Пристрій – Коли сегмент було зіставлено з файлу, у цьому стовпці відображається шістнадцятковий номер пристрою, на якому зберігається файл.
  • Inode (індексний вузол) – Якщо сегмент походить із файлу, це номер inode файлу.
  • Шлях – Це шлях до файлу, якщо він є. Це може бути [heap], [stack] або [vsdo] якщо сегмент є однойменною структурою.

Наш скромний сценарій:

У шкідливих програмах Android динамічне завантаження коду зазвичай виконується з домашнього каталогу програми, тому файли, завантажені в пам’ять із цього словника (або інших поширених місць), мають відображатися у файлі карт. Щоб автоматизувати завдання перевірки цього файлу, ми створили дуже простий сценарій, який використовує регулярний вираз для пошуку рядка '/data/data' у файлі карт певного PID на підключеному пристрої та повертає відповідні рядки. /data/data — це, звичайно, домашній каталог програми, де зберігаються файли та дані. Це єдине місце, з якого програма може завантажувати файли DEX.

Взаємодія з ADB через Python іноді була складною, оскільки використання оболонки може бути незручним, але ми знайшли корисний набір інструментів під назвою pwntools які забезпечують саме цю функціональність. Вони пропонують широкий набір функціональних можливостей, призначених для допомоги у зломі та прототипуванні. Обов'язково перевірте це.

Ви можете переглянути повний сценарій, який ми використовуємо, тут Суть Github.

Перейти до вмісту