содержимое процедур/карт

Обнаружение динамической загрузки в приложениях Android с помощью /proc/maps

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

Динамическая загрузка и связывание:

Чтобы понять остальную часть блога, важно пройтись по основам связывания и загрузки, а также выделить различия между связыванием и загрузкой, статическим и динамическим.

Что связывает:

Процесс компиляции состоит из нескольких частей. Связывание — это последний шаг перед тем, как мы получим работоспособный выполнимый. Программа обычно представляет собой нечто большее, чем простой автономный файл, и для работы она полагается на другие библиотеки или файлы. Таким образом, вам недостаточно скомпилировать свой код в машинный код, чтобы иметь возможность его запускать, но также каким-то образом ссылке различные файлы в единый исполняемый файл.

Как это делается на практике? После компиляции и сборки ассемблер выдает объектные файлы, которые обычно соответствуют каждому модулю в вашей программе. Такие файлы могут быть перемещаемыми или исполняемыми. Объектные файлы первого типа должны пройти компоновку, прежде чем их можно будет запустить, а файлы второго типа могут выполняться немедленно. В экосистеме 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), который приложение будет загружать, а затем динамически загружать во время использования, и, таким образом, их приложение будет выглядеть безобидным при базовой статической проверке. АПК.

Как загружаются классы DEX:

Android предлагает возможность динамической загрузки файлов .dex с использованием класса с именем ДексКлассЗагрузчик. Чтобы загрузить класс, нам просто нужно написать:

// 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

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

  • Адрес – В первом столбце показаны начальный и конечный адреса сегмента.
  • Разрешения... – В этом столбце показано, какие разрешения есть у процесса для сегмента. р/ж/х обычные чтение/запись/выполнение, а последняя буква s or p, что означает общий или частный, соответственно.
  • Офсет – Это смещение от начала файла, чтобы можно было вычислить начальный адрес отображаемых данных. Иногда сегмент не отображается из файла (в этом случае столбец пути будет иметь идентификатор характера сегмента, как описано ниже), и в этом случае смещение просто остается равным 0.
  • Устройство – Если сегмент был отображен из файла, в этом столбце отображается шестнадцатеричный номер устройства, на котором хранится файл.
  • Инод (индексный узел) – Если сегмент получен из файла, это номер inode файла.
  • Path – Это путь к файлу, если он есть. Это может быть [куча], [стек] или [всдо] если сегмент является одноименной структурой.

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

В вредоносных приложениях для Android динамическая загрузка кода обычно выполняется из домашнего каталога приложения, поэтому файлы, загружаемые в память из этого словаря (или других общих мест), должны отображаться в файле карт. Чтобы автоматизировать задачу проверки этого файла, мы создали очень простой скрипт, который использует регулярное выражение для поиска строки «/data/data» в файле карт заданного PID на подключенном устройстве и возвращает совпадающие строки. /data/data — это домашний каталог приложений, в котором хранятся файлы и данные. Это единственное место, откуда приложение может загружать файлы DEX.

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

Вы можете увидеть полный сценарий, который мы используем здесь. Github суть.

перейти к содержанию