两个层面上的hook: java层:需要了解虚拟机的特性与java上反射的使用; native层:难点在于理解elf文件。关键在于如何找到函数的入口点、替换函数。

对于进程附着,Android的内核中有一个函数叫ptrace,它能够动态地attach(跟踪一个目标进程)、detach(结束跟踪一个目标进程)、peektext(获取内存字节)、poketext(向内存写入地址)等,它能够满足我们的需求。而Android中的另一个内核函数dlopen,能够以指定模式打开指定的动态链接库文件。对于程序的指向流程,我们可以调用ptrace让PC指向LR堆栈。最后调用,对目标进程调用dlopen则能够将我们希望注入的动态库注入至目标进程中。

对于代码的注入,我们可以使用mmap函数分配一段临时的内存来完成代码的存放。对于目标进程中的mmap函数地址的寻找与Hook API函数地址的寻找都需要通过目标进程的虚拟地址空间解析与ELF文件解析来完成,具体算法如下:

(1)通过读取/proc/PID/maps文件找到链接库的基地址。

(2)读取动态库,解析elf文件,找到目标函数相对elf文件开始的偏移。

(3)计算目标函数的绝对地址 (= 函数在elf中的偏移+ 动态库基地址)

 总之,向目标进程中注入代码分为以下几步:

(1)用ptrace attach上目标进程 (2)发现装载共享库so函数。(3)装载指定的so (4)让目标进程的执行流程跳转到注入的代码执行 (5)使用ptrace函数的detach释放目标进程。

Android hook技术 安卓hook函数_动态库

ptrace提供了一种使父进程得以监视和控制其他进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。使用ptrace,你可以在用户层拦截和修改系统调用(这个和Hook所要达到的目的类似),父进程还可以使子进程继续执行,并选择是否忽略引起终止的信号。

ptrace函数定义如下所示:

int ptrace(int request, int pid, int addr, int data);
  • request是请求ptrace执行的操作。
  • pid是目标进程的ID。
  • addr是目标进程的地址值。
  • data是作用的数据。

ptrace允许的操作见 http://www.epubit.com.cn/book/onlinechapter/33620

常用的hook工具: XPosed框架,CydiaSubstrate框架,ADBI/DDI框架等。

使用这些框架可以hook 系统api, 用户自定义的函数,以及nativce code的执行,实现广告注入、登陆劫持、改变系统表现(如系统颜色)等功能。

hook检测:

首先得到该app的pid,之后用/proc/PID/maps查看在其进程空间中的信息,看其加载了哪些第三方的库文件,看是否包含有substrate等怀疑为hook框架的库。

hook修复:

我们希望除了自身包名(com.example.testndklib)下的其他动态链接全都给删除关闭,且关闭后的应用程序还能够正常地运行。因为所有的第三方库都是通过dlopen后注入的方式附加到应用程序进程中的,这里我们很容易想到我们直接使用 dlclose 将其中的第三方函数挨个卸载关闭即可。

这样一个程序思路就来了,首先扫描/proc/<pid>/maps目录下的所有so库文件,并将自身的动态库文件排除,对于非自身的动态链接库我们全都卸载关闭。对于Java我们无法使用dlclose,所以这里我们还是采用了JNI的方式来完成,具体的操作函数如下所示。

/**
   * 根据包名与进程pid,删除非包名下的动态库
   * @parampid 进程pid
   * @parampkg 包名
   * @return
   */
public List<String> removeHooks(intpid, String pkg) {
    List<String> hookLibFile = newArrayList<>();
    // 找到对应进程的虚拟地址空间文件
    File file = newFile("/proc/" + pid + "/maps");
if(!file.exists()) {
returnhookLibFile;
    }

try{
      BufferedReader bufferedReader = newBufferedReader(newInputStreamReader  
(newFileInputStream(file)));
      String lineString = null;
while((lineString = bufferedReader.readLine()) != null) {
        String tempString = lineString.trim();
        // 被hook注入的so动态库
if(tempString.contains("/data/data") && !tempString.contains("/data/data/" + pkg)) {
intindex = tempString.indexOf("/data/data");
          String soPath = tempString.substring(index);
          hookLibFile.add(soPath);
          // 调用native方法删除so动态库
          removeHookSo(soPath);
        }
      }
      bufferedReader.close();
    } catch(FileNotFoundException e) {
      e.printStackTrace();
    } catch(IOException e) {
      e.printStackTrace();
    }
return hookLibFile;
  }

  /**
   * 卸载加载的so库
   * @paramsoPath so库地址路径
   */
public native void removeHookSo(String soPath);

// JNI中的removeHookSo卸载一个so的加载
void Java_com_example_testndklib_MainActivity_removeHookSo(JNIEnv* env,
    jobject thiz, jstring path) {
  constchar

之后看了一下linux下用gdb实现的so代码注入http://blog.chinaunix.net/uid-29482215-id-4135833.html ,实验了下做到最后一步并没有像作者说的打印出时间信息,或许某一步出错,稍后重新试一下。

(目的是用injection.o中的injection函数地址 替换目标进程 app 中的print函数地址。

在gdb已经attach到app 进程上的条件下,通过gdb调用open("injection.o",2)来打开要注入的文件并返回文件描述符;之后call mmap(0, 2504, 1|2|4, 1, 3, 0) 其中3为文件描述符 来将要注入的函数加载到内存中。

通过readelf 得到injection函数中调用的第一个函数print相对于文件开头的偏移,用 cat /proc/PID/maps (PID为app的进程id)找到injection.o加载到内存中的地址空间。用此地址空间的起始地址加上print函数的偏移得到print函数应该映射到内存中哪个地址。

在要注入的目标进程app中,readelf -s app 得到重定向表中要被hook的函数print的offset (相对于plt的)

Android hook技术 安卓hook函数_包名_02

这里的逻辑是,在加载之后执行之前print真正的地址会填到0x0804a010处。若我们将injection函数的入口地址写在这里,则可以实现app中print方法的hook

在完成这一步替换之后还要做一下函数重定位来解决injection函数内部调用的问题。

linux x86架构 重定位类型:

R_386_32表示绝对地址的重定位,可以直接使用符号的地址;
R_386_PC32表示对相对地址的重定位,要用“符号地址-重定位地址 + 附加数”得出相对地址。
说明:所谓重定位类型,就是规定了使用何种方式,去计算这个值,具体有哪些变量参与计算如同如何进行计算一样也是不固定的,各种重定位类型有自己的规定。据规范里面的规定,重定位类型R_386_PC32的计算需要有三个变量参与:S,A和P。其计算方式是 S+A-P。根据规范,当R_386_PC32类型的重定位发生在link editor链接若干个.o对象文件从而形成可执行文件的过程中的时候,变量S指代的是被重定位的符号的实际运行时地址,而变量P是重定位所影响到的地址单元的实际运行时地址。在运行于x86架构上的Linux系统中,这两个地址都是虚拟地址。变量A最简单,就是重定位所需要的附加数,它是一个常数。x86架构所使用的重定位条目结构体类型Elf32_Rela,所以附加数就存在于受重定位影响的地址单元中。重定位最后将计算得到的值patch到这个地址单元中。
R_386_32 类型规定只是将附加数加上符号的值作为所需要的值,即.rodata的重定位需要在地址0x00bee000的基础上加上一个附加数。

====================

然后又看了下看雪上有大神实现的android so injection的代码。应该是跟CydiaSubstrate框架中的原理类似,框架中大约封装的是类似的代码吧。

http://bbs.pediy.com/showthread.php?t=141355