📜 ⬆️ ⬇️

Interception of calls of functions of native libraries in Android applications

What is it for


I often came across the need to debug Android applications using native code. Sometimes I needed to intercept calls to bionic (libc), sometimes to .so-shkam, to which I did not have the source code. Sometimes it was necessary to include in your applications others .so, to which there were no sources, and it was necessary to correct their behavior.

So, how to make LD_PRELOAD in Android?

As is widely known, this problem is easily solved in the usual Linux desktop-e using the LD_PRELOAD environment variable. This trick works as follows: the dynamic linker puts the library from this variable at the very beginning of the list of available libraries. As a result, when the code tries to make a library call for the first time (lazy binding), the linker will bind the function to the one we defined in our library.

This is all great, but on Android this trick will not work. Applications running from the UI are already linked to the moment when the code written by the application author is launched. Purely theoretically, applications can be run from the command line and set LD_PRELOAD. But this is a difficult task, and it works only for debag.
')

A bit about dynamic layout


In order to use dynamic libraries, you need the ability to call their code from other libraries - and vice versa. How can compiled code call code from another library? Ordinary jmp / bx transition operations require an address, but we cannot know it beforehand (at the time of assembling .so), since different .so in memory can fall into different (or even random) places. It is possible to patch the addresses of the required functions in the code, when all .so is already decomposed in memory. But it is not elegant, slow, it requires writing to the code area, plus each application had to receive its own copy of the code and there would be no memory saving.

The output is very simple: the jump occurs at the address recorded somewhere outside the section of executable code. And if this address is made not absolute, but relative (for example, by writing it as the offset of the command itself), then it turns out that the code itself can be placed anywhere in the memory. And already behind it is placed PLT table, procedure linkage table. It is usually mapped as (r, or rw), rather than eXecutable. “Real” addresses are placed in this table. The table can be filled both at the start and directly at run time, in lazy mode.

If we put everything together, in order to force the xxx.so module to jump into our interceptor when calling the yyy () function, we need:


Actually, interception


Android uses bionic and is slightly different from glibc, but there are no fundamental differences. Internal data is stored in the soinfo structure and this is a linked list of all .so loaded at the moment.

In glibc, dlopen() returns a spherical void* in vacuum to us:

 void *dlopen(const char *filename, int flag) 


But, looking at the bionic sources, we will see that the coveted soinfo coming back soinfo
 soinfo* do_dlopen(const char* name, int flags) 

If the library is already loaded, we will get back soinfo for it. Hooray, now we have in our hands all the information about .so.

In ELF, character strings are stored separately (strtab), separately structures with a description of characters (symtab). For the characters themselves (string constants), a hash is calculated, which allows you to quickly find the offset for the character you are interested in.

ELF character hash count
  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; } 


When the hash is calculated, it is necessary to find the symbol.
hash character search
 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; } 


Here is the procedure for replacing the desired value:
  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; } 


Now, to intercept connect () from libandroid_runtime.so, we need to call:

 hook_call("libandroid_runtime.so", "connect", &my_connect); 

Source: https://habr.com/ru/post/238619/


All Articles