[Из песочницы] Перехват вызовов функций нативных библиотек в Android приложениях
Для чего это нужно Я часто сталкивался с необходимостью отлаживать Android приложения, использующие нативный код. Иногда мне было нужно перехватить вызовы к bionic (libc), иногда к .so-шкам, к которым исходного кода у меня не было. Иногда приходилось включать в свои приложения чужие .so, к которым не было исходников и надо было подкорректировать их поведение.Итак, как сделать LD_PRELOAD в Android? Как широко известно, в обычном desktop-е Linux эта проблема легко решается использованием переменной окружения LD_PRELOAD. Этот фокус работает следующим образом: динамический линковщик ставит библиотеку из этой переменной в самое начало списка доступных библиотек. В результате, когда код пытается сделать библиотечный вызов в первый раз (lazy binding), линковщик байндит функцию именно на ту, которую мы определили в нашей библиотеке.
Это все замечательно, но в Android этот фокус не пройдет. Приложения, запущенные из UI, уже слинкованы к тому моменту, когда запускается код, написаный автором приложения. Чисто теоритически приложения можно запускать из командной строки и выставить LD_PRELOAD. Но это сложное занятие, да и работает только для дебага.
Немного о динамической компоновке Для того, чтобы использовать динамические библиотеки, нужна возможность вызова их кода из других библиотек — и наоборот. Как уже скомпилированный код может вызвать код из другой библиотеки? Обычные операции перехода типа jmp/bx требуют адрес, но его мы заранее (в момент сборки .so) знать не можем, так как разные .so в памяти могут попасть в разные (или даже случайные) места. Можно банально в коде пропатчить адреса требуемых функций, когда все .so уже разложены в памяти. Но это не элегантно, медленно, требует записи в область кода, плюс каждому приложению пришлось получать свой собственный экземпляр кода и сбережение памяти не было бы.Выход очень простой: прыжок происходит по адресу, записанному где-то вне секции исполняемого кода. И если этот адрес сделать не абсолютным, а относительным (например, записав его как смещение самой команды), то получается, что сам код можно размещать где угодно в памяти. А уже за ним уже размещается таблица PLT, procedure linkage table. Она обычно мапится как (r, или rw), а не eXecutable. В эту таблицу помещаются как раз «настоящие» адреса. Таблица может заполнятся как на старте, так и непосредственно во время выполнения, в lazy режиме.
Если собрать всё вместе, то для того, чтобы заставить модуль xxx.so при вызове функции yyy () прыгать в наш перехватчик, нужно:
найти PLT секцию в xxx; посчитать/найти смещение для yyy () в PLT; записать адрес нашей функции. Собственно, перехват В Android используется bionic и он слегка отличается от glibc, но принципиальных отличий нет. Внутренние данные хранятся в структуре soinfo и это связанный список из всех загруженных в данных момент .so.В glibc dlopen () возвращает нам сферический void* в вакууме:
void *dlopen (const char *filename, int flag) Но, посмотрев в исходники bionic, мы увидим, что возвращается заветный soinfo
soinfo* do_dlopen (const char* name, int flags) Если библиотека уже загружена, нам вернут soinfo для нее. Ура, теперь у нас в руках вся информация об интересующей нас .so.В ELF строчки с символами хранятся отдельно (strtab), отдельно структуры с описанием символов (symtab). Для самих символов (строковых констант) вычислен хэш, что позволяет быстро найти offset для интересующего нас символа.
подсчета хэша символа ELF static unsigned elfhash (const char *_name) { const unsigned char *name = (const unsigned char *) _name; unsigned h = 0, g; while (*name) { h = (h << 4) + *name++; g = h & 0xf0000000; h ^= g; h ^= g >> 24; } return h; } Когда хэш посчитан, необходимо найти сивол.поиск символа по хэшу static Elf32_Sym *soinfo_elf_lookup (soinfo *si, unsigned hash, const char *name) { Elf32_Sym *s; Elf32_Sym *symtab = si→symtab; const char *strtab = si→strtab; unsigned n; n = hash % si→nbucket; for (n = si→bucket[hash % si→nbucket]; n!= 0; n = si→chain[n]){ s = symtab + n; if (strcmp (strtab + s→st_name, name)) continue; return s; } return NULL; } А вот и процедура замены нужного значения: int hook_call (char *soname, char *symbol, unsigned newval) { soinfo *si = NULL; Elf32_Rel *rel = NULL; Elf32_Sym *s = NULL; uint32_t sym_offset = 0; uint32_t page_size = 0;
if (! soname || ! symbol || ! newval) return 0; si = (soinfo*) dlopen (soname, 0); if (! si) return 0; s = soinfo_elf_lookup (si, elfhash (symbol), symbol); if (! s) return 0; page_size = getpagesize (); sym_offset = s — si→symtab; // индекс найденного символа rel = si→plt_rel; /* идем по таблице релокаций пока не попадется нужный нам индекс */ for (int i = 0; i < si->plt_rel_count; i++, rel++) { unsigned type = ELF32_R_TYPE (rel→r_info); unsigned sym = ELF32_R_SYM (rel→r_info); unsigned reloc = (unsigned)(rel→r_offset + si→base); unsigned oldval = 0; if (sym_offset == sym) { switch (type) { case R_ARM_JUMP_SLOT: // нужно пометить страницу как RW, и адрес должен быть page-aligned mprotect ((uint32_t *) reloc& (~(page_size — 1), page_size, PROT_READ | PROT_WRITE); oldval = *(unsigned*) reloc; *((unsigned*)reloc) = newval; return 1; default: return 0; } } } return 0; } Теперь, чтобы перехватить connect () из libandroid_runtime.so, нам надо вызвать:
hook_call («libandroid_runtime.so», «connect», &my_connect);