Thursday, January 31, 2013

Android hacking: hooking system functions used by Dalvik

Let's say you need to alter behavior of some Android app you do not have source code for.
For example, you want to redirect all socket connect()s made to badhost.com to myhost.com or perhaps even 127.0.0.1.
How this can be achieved (without having to root the device and recompile system libs) ?

On normal Linux systems one can easily use dynamic linker LD_PRELOAD environment variable to let ld change symbol resolution order and thus inject any code you want. However, on Android it is not possible for following reasons:
  • LD_PRELOAD is not useful because zygote already forked JVM
  • Once an app is running, JVM is already started
  • It not possible to (easily) modify environment variables for JVM invokation 
So, how do we do it? By patching PLT (.rel.plt)  (procedure linkage tables) of a running process in-flight!

I will not be going into describing how PLT works and details on ELF binary format. It is suffice to say that compiled code which imports symbols from other .so-s needs to figure out where these functions are located in memory. Since libraries can be loaded at a different place in memory for different processes, it is impossible to know imported functions addresses at compile time. Detailed description can be found here.

However, Android's Bionic library is different, and code specific to glib/gcc dynamic linker won't work. Fortunately, it is easily possible to access ld's internal structures and fish out relevant data. Consider signature of regular POSIX dlopen call:

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

Interestingly, this gives us void* abstract 'handle'. In practice, this handle is pointer to a struct soinfo which contains all the information we ever need to override PLT tables.

From linker/linker.cpp:

 soinfo* do_dlopen(const char* name, int flags)  

Now, all we need to is to simply re-dlopen() shared library we want (in my case it libandroid_runtime.so) and walk through plt table, patch connect() method to our own.

Here is the complete hooking code:
/*
Copyright (C) 2013 Andrey Petrov

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software 
without restriction, including without limitation the rights to use, copy, modify, 
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 
persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or 
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
DEALINGS IN THE SOFTWARE.
*/

#define ANDROID_ARM_LINKER  
 #include <dlfcn.h>  
 #include <errno.h>  
 #include <fcntl.h>  
 #include <pthread.h>  
 #include <stdio.h>  
 #include <stdlib.h>  
 #include <string.h>  
 #include <sys/atomics.h>  
 #include <sys/mman.h>  
 #include <sys/stat.h>  
 #include <unistd.h>  
 #include "linker.h"  // get it from bionic
 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;   
  unsigned int sym_offset = 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;  
  sym_offset = s - si->symtab;  
  rel = si->plt_rel;  
  /* walk through reloc table, find symbol index matching one we've got */  
  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:  
         /* we do not have to read original value, but it would be good   
           idea to make sure it contains what we are looking for */  
         oldval = *(unsigned*) reloc;  
         *((unsigned*)reloc) = newval;  
         return 1;  
      default:  
         return 0;  
    }  
   }  
  }  
  return 0;  
 }  

So, in order to hook connect call you would need to call:

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

All you now need is to write appropriate my_connect function which will inspect then modify input parameters and then delegate call to real connect()

Next thing to worry about is how to get this code to be executed on target app start, but perhaps it is a topic for a separate post.

Stay tuned ;-)

update: some people told me this interception does not work anymore. This works fine for 2.3.x and 4.0.x. For >4.1.x you would want to intercept libjavacore.so. Thanks for Madhavi Rao for figuring this out