今天周日,昨天花了一天的时间总算是搞定了,问题还是想对应用程序的行为进行拦截的操作,就是像小米手机一样,哪些应用在获取你什么权限的信息。在之前写过对应用程序的行为进行拦截的方式(C层)实现的博客,在写完这篇之后,本来想是尽快的把Java层拦截的文章结束的,但是由于各种原因吧,所以一直没有时间去弄这些了。今天算是有空,就总结一下吧。下面进入正题:
一、摘要
我们知道现在一些安全软件都会有一个功能就是能够拦截应用的行为(比如地理位置信息,通讯录等),所以这里就来实现以下这样的功能,当然实现这样的功能有两种方式,一种是从底层进行拦截,这个我在之前的博客中已经讲解过了。
还有一种方式就是从上层进行拦截,也就是我们今天所要说的内容,这种方式都是可以的,当然很多人更多的偏向上层,因为底层拦截需要熟知Binder协议和Binder的数据格式的。上层拦截就简单点了。
二、知识点概要
首先我们需要了解一点知识就是不管是底层拦截还是上层拦截,都需要一个技术支持:进程注入,关于这个知识点,这里就不作解释了。
了解了进程注入之后,这篇文章主要讲解三点知识:
1、如何动态加载so,并且执行其中的函数
2、如何在C层执行Java方法(NDK一般是指Java中调用C层函数)
3、如何修改系统服务(Context.getSystemService(String...)其实返回来的就是Binder对象)对象
当然我们还需要一些预备知识:知道如何使用NDK进行编译项目。
这篇文章编译环境是Window下的,个人感觉还是不方便,还是在Ubuntu环境下操作比较方便
还有一点需要声明:就是拦截行为是需要root权限的
三、例子
第一个例子:简单的进程注入功能
目的:希望将我们自己的功能模块(so文件)注入到目标进程中,然后修改目标进程中的某个函数的执行过程
文件:注入功能可执行文件poison、目标进程可执行文件demo1、需要注入的模块libmyso.so
注入功能的可执行文件核心代码poison.c,这个功能模块在后面讲到的例子中也会用到,所以他是公用的
#include <unistd.h> #include <errno.h> #include <stdlib.h> #include <dlfcn.h> #include <sys/mman.h> #include <sys/ptrace.h> #include <sys/wait.h> #include "ptrace_utils.h" #include "elf_utils.h" #include "log.h" #include "tools.h" struct process_hook { pid_t pid; char *dso; } process_hook = {0, ""}; int main(int argc, char* argv[]) { LOGI("argv len:"+argc); if(argc < 2) exit(0); struct pt_regs regs; process_hook.dso = strdup(argv[1]); process_hook.pid = atoi(argv[2]); if (access(process_hook.dso, R_OK|X_OK) < 0) { LOGE("[-] so file must chmod rx\n"); return 1; } const char* process_name = get_process_name(process_hook.pid); ptrace_attach(process_hook.pid, strstr(process_name,"zygote")); LOGI("[+] ptrace attach to [%d] %s\n", process_hook.pid, get_process_name(process_hook.pid)); if (ptrace_getregs(process_hook.pid, ®s) < 0) { LOGE("[-] Can't get regs %d\n", errno); goto DETACH; } LOGI("[+] pc: %x, r7: %d", regs.ARM_pc, regs.ARM_r7); void* remote_dlsym_addr = get_remote_address(process_hook.pid, (void *)dlsym); void* remote_dlopen_addr = get_remote_address(process_hook.pid, (void *)dlopen); LOGI("[+] remote_dlopen address %p\n", remote_dlopen_addr); LOGI("[+] remote_dlsym address %p\n", remote_dlsym_addr); if(ptrace_dlopen(process_hook.pid, remote_dlopen_addr, process_hook.dso) == NULL){ LOGE("[-] Ptrace dlopen fail. %s\n", dlerror()); } if (regs.ARM_pc & 1 ) { regs.ARM_pc &= (~1u); regs.ARM_cpsr |= CPSR_T_MASK; } else { regs.ARM_cpsr &= ~CPSR_T_MASK; } if (ptrace_setregs(process_hook.pid, ®s) == -1) { LOGE("[-] Set regs fail. %s\n", strerror(errno)); goto DETACH; } LOGI("[+] Inject success!\n"); DETACH: ptrace_detach(process_hook.pid); LOGI("[+] Inject done!\n"); return 0; } |
我们看到,这个注入功能的代码和我们之前说的从底层进行拦截的那篇文章中的注入代码(inject.c)不太一样呀?这个是有人在网上从新改写了一下,其实功能上没什么区别的,我们从main函数可以看到,有两个入口参数:
第一个是:需要注入so文件的全路径
第二个是:需要注入进程的pid
也就是说,我们在执行poison程序的时候需要传递这两个值。在之前说道的注入代码(inject.c)中,其实这两个参数是在代码中写死的,如果忘记的同学可以回去看一下,就是前面提到的从底层进行拦截的那篇文章。
那么这样修改之后,貌似灵活性更高了。
当然注入功能的代码不止这一个,其实是一个工程,这里由于篇幅的原因就不做介绍了。
使用NDK编译一下,生成可执行文件就OK了。
第一部分:代码实现
1)目标进程依赖的so文件inso.h和inso.c
__attribute__ ((visibility ("default"))) void setA(int i); __attribute__ ((visibility ("default"))) int getA(); |
inso.c代码
#include <stdio.h> #include "inso.h" static int gA = 1; void setA(int i){ gA = i; } int getA(){ return gA; } |
编译成so文件即可,项目下载:http://download.csdn.net/detail/jiangwei0910410003/8138107
2)目标进程的可执行文件demo1.c
这个就简单了,就是非常简单的代码,起一个循环每个一段时间打印数值,这个项目需要引用上面编译的inso.so文件
头文件inso.h(和上面的头文件是一样的)
__attribute__ ((visibility ("default"))) void setA(int i); __attribute__ ((visibility ("default"))) int getA(); |
demo1.c文件
#include <stdio.h> #include <unistd.h> #include "inso.h" #include "log.h" int main(){ LOGI("DEMO1 start."); while(1){ LOGI("%d", getA()); setA(getA() + 1); sleep(2); } return 0; } |
代码简单吧,就是执行循环打印数值,这里使用的是底层的log方法,在log.h文件中定义了,篇幅原因。
3)注入的模块功能源文件myso.c
#include <stdio.h> #include <stddef.h> #include <dlfcn.h> #include <pthread.h> #include <stddef.h> #include "log.h" __attribute__ ((__constructor__)) void Main() { LOGI(">>>>>>>>>>>>>Inject Success!!!!<<<<<<<<<<<<<<"); void (*setA_func)(int); void* handle = dlopen("libinso.so", RTLD_NOW); LOGI("Handle:%p",handle); //void (*setA_func)(int) = (void (*)(int))dlsym(handle, "setA"); setA_func = (void (*)(int))dlsym(handle,"setA"); LOGI("Func:%p",setA_func); if (setA_func) { LOGI("setA is Executing!!!"); (*setA_func)(999); } dlclose(handle); } |
说明:
这段代码需要解释一下,首先来看一下:
__attribute__ ((__constructor__)) |
gcc为函数提供了几种类型的属性,其中包含:构造函数(constructors)和析构函数(destructors)。
程序员应当使用类似下面的方式来指定这些属性:
static void start(void) __attribute__ ((constructor)); static void stop(void) __attribute__ ((destructor)); |
带有"构造函数"属性的函数将在main()函数之前被执行,而声明为"析构函数"属性的函数则将在main()退出时执行。
用法举例:
#include <iostream> void breforemain() __attribute__((constructor)); void aftermain() __attribute__((destructor)); class AAA{ public: AAA(){std::cout << "before main function AAA" << std::endl;} ~AAA(){std::cout << "after main function AAA" << std::endl;} }; AAA aaa; void breforemain() { std::cout << "before main function" << std::endl; } void aftermain() { std::cout << "after main function" << std::endl; } int main(int argc,char** argv) { std::cout << "in main function" << std::endl; return 0; } |
输出结果:
before main function AAA before main function in main function after main function AAA after main function |
有点类似于Spring的AOP编程~~
还有一个就是我们开始说的,如何加载so文件,并且执行其中的函数,原理很简单,就是打开so文件,然后返回一个函数指针。
需要的头文件:#include <dlfcn.h>
核心代码:
打开so文件,返回一个句柄handle:
void* handle = dlopen("libinso.so", RTLD_NOW); |
得到指定的函数指针:
setA_func = (void (*)(int))dlsym(handle,"setA"); |
函数指针的定义:
就是这么简单,有点类似Java中动态加载jar包,然后执行其中的方法。
第二部分:拷贝文件
好了,到这里离成功不远了,我们保证上面的工程编译都能通过,得到以下文件:demo1、poison、libmyso.so、libinso.so然后我们就可以实践了
首先我将这些文件拷贝到手机中的/data/data/目录中
adb push demo1 /data/data/
adb push poison /data/data/
adb push libmyso.so /data/data/ |
拷贝完之后,还需要进入adb shell,修改他们的权限
chmod 777 demo1
chmod 777 poison
chmod 777 libmyso.so |
这里要注意的是,libinso.so文件要单独拷贝到/system/lib中,不然在执行demo1的时候,会报错(找不到libinso.so),当然在拷贝的时候会遇到一点问题
报错:"Failed to push selection: Read-only
file system"
这时候只要改变system目录的挂载读写属性就好了
mount -o remount rw /system/ |
然后进入adb shell在修改一下/system的属性
然后就可以拷贝了:
adb push libinso.so /system/lib/ |
然后进入到system/lib中,修改libinso.so的属性
第三部分:开始执行
然后进入到data/data目录中,开始执行文件,这时候我们需要开三个终端:一个是监听log信息,一个是执行demo1,一个是执行poison,如下图所示:
1、监听log信息
2、执行demo1
3、执行poison
./poison /data/data/libmyso.so 1440 |
这里我们看到,执行poison有两个入口参数:一个是so文件的路径,一个是目标进程(demo1)的pid就是log信息中的显示的pid
到这里我们就实现了我们的第一个例子了。
第二个例子:将目标进程改成Android应用
下面继续:将目标进程改变成一个Android应用
这里和上边的唯一区别就是我们需要将demo1变成一个Android应用
那么来看一下这个Android应用的代码:
package com.demo.host; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.util.Log; public final class MainActivity extends Activity { private static int sA = 1; public static void setA(int a) { sA = a; } public static int getA() { return sA; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new Thread() { public void run() { while (true) { Log.i("TTT", "" + getA()); setA(getA() + 1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }; }.start(); } } |
代码和demo1的功能一样的,写个循环,打印数值,工程下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138227
这个应用就变成了我们注入的目标进程,但是有一个问题,我们怎么才能修改setA()方法的行为呢?在第一个案例中,我们是动态加载libinso.so,然后获取到setA()函数,修改器返回值。这里我们需要做的就是怎么动态的去修改上面的MainActivity中的setA()方法的返回值,其实这里就到了我开始说的第二个知识点:如何在底层C中调用Java方法?
我们知道在使用NDK的时候,Java层调用底层C的时候连接的纽带就是那个JNIEnv变量,这个变量是作为函数参数传递过来的。那么如果我们在底层C中获取到这个变量的话,就可以调用Java方法了,但是这里我们又没有定义本地方法,怎么得到JNIEnv变量呢?
答案就是#include <android_runtime/AndroidRuntime.h>这个头文件,得到JVM变量之后,然后得到当前线程的JNIEnv变量:
JavaVM* jvm = AndroidRuntime::getJavaVM(); LOGI("jvm is %p",jvm); jvm->AttachCurrentThread(&jni_env, NULL); //TODO 使用JNIEnv jvm->DetachCurrentThread(); |
通过AndroidRuntime中的getJavaVM方法获取jvm变量,然后在获取当前线程的JNIEnv变量即可
关于AndroidRuntime这个类的定义和实现是在 AndroidRuntime源码目录/jni/AndroidRuntime.cpp中
好了,当我们拿到JNIEnv变量之后,我们就可以干很多事了,因为我们知道在弄NDK的时候,如果使用JNIEnv变量的时候都清楚,他好比Java中的反射机制,可以动态的加载Java中的类,然后获取其方法,字段等信息,进行操作。
但是现在还有一个问题,就是我们怎么去动态加载MainActivity这个类呢?
但是当我们尝试使用PathClassLoader去加载MainActivity时,会抛ClassNotFoundException
唯一可行的方案是找到host(目标应用)的PathClassLoader,然后通过这个ClassLoader寻找MainActivity
因为我们是注入到MainActivity这个应用的进程中,那么我们的注入代码和MainActivity是在一个进程中的,又因为Android中一个进程对应一个全局Context对象,所以我们只要得到这个进程Context对象的类加载器就可以了
(其实Android中多个应用是可以跑在一个进程中的,他们会拥有一共同的全局Context变量,当然这个Context不是特定的Activity的,而是Application对象持有的Context)
要想得到一个进程中的Context对象。通过阅读源码,发现可以通过下面的方式读取到Context对象:
如果是System_Process,可以通过如下方式获取
Context context = ActivityThread.mSystemContext |
如果是非System_Process(即普通的Android进程),可以通过如下方式获取
Context context = ((ApplicationThread)RuntimeInit.getApplicationObject()).app_obj.this$0 |
到这里,我们都知道该怎么办了,没错就是用反射机制,获取到全局的Context变量
上面的思路是有了,下面在来整理一下吧:
首先我们需要注入到MainActivity所在的进程,然后修改他的setA()方法。
但是我们注入的时候是把so文件注入到一个进程中,所以需要在底层修改setA()方法的执行
如果底层想修改/执行Java层方法的话,必须要得到JNIEnv变量
然后可以通过AndroidRuntime类先得到jvm变量,然后在通过jvm变量得到JNIEnv变量
得到JNIEnv变量之后,用JNIEnv的一些方法去动态加载MainActivity类,然后修改他的方法,但是会出现异常找不到MainActivity类
找不到这个类的原因是类加载器找的不对,我们需要找到全局的Context对象的类加载器,因为我们是注入到了MainActivity这个应用的进程中,一个进程有一个全局的Context对象,所以只要得到它的类加载器就可以了。
然后通过查看源码,我们可以在Java层通过反射获取到这个对象。
最后:通过上面的分析,我们还需要一些项目的支持:
1、底层获取JNIEnv对象的项目,也就是我们需要注入的so。
2、在上层还需要一个模块,去获取到全局的Context对象,然后动态的加载MainActivity类,修改他的方法。
|