Andrdoid中对应用程序的行为拦截实现方式之----从底层C进行拦截
之前的一篇概要文章中主要说了我这次研究的一些具体情况,这里就不在多说了,但是这里还需要指出的是,感谢一下三位大神愿意分享的知识(在我看来,懂得分享和细致的人才算是大神,不一定是技术牛奥~~)
第一篇:http://blog.csdn.net/jinzhuojun/article/details/9900105
第二篇:http://bbs.pediy.com/showthread.php?t=186880
第三篇:http://bbs.pediy.com/showthread.php?t=157419
最重要的还是第一篇,所以这里我就不多介绍了,当然如果要看这篇blog的话,最好是先仔细阅读一下上面的三篇文章。
当然我对第一篇文章做了修改了详细的描述了:http://blog.csdn.net/jiangwei0910410003/article/details/39293635
这篇文章最好是看懂了,而且是必须真的懂了,同时将demo自己执行一边,流程走通了,不然下面也是会遇到问题的。
当然这种拦截方式的前提是:手机必须root,同时需要获取su权限
下面开始进入正题
当然在之前的文章中的摘要中我们可以看到我这次主要拦截的是可以获取系统信息的进程,所以我们要搞清楚拦截的对象,这里就不做介绍了,我们拦截的进程是system_server(关于这个进程可以google一下,当然我们可以使用adb shell以及ps命令查看这个进程的相关信息)
关于inject源码这里就不做太多的解释了,主要来看一下他的main函数:
int main(int argc, char** argv) { pid_t target_pid; target_pid = find_pid_of("system_server"); if (-1 == target_pid) { printf("Can't find the process\n"); return -1; } printf("target_id:%d\n",target_pid); inject_remote_process(target_pid, "/data/libsys.so", "hook_entry", "I'm parameter!", strlen("I'm parameter!")); return 0; }这里的一个主要的方法就是inject_remote_process(...)
第一个参数:需要注入的进程id
第二个参数:需要注入的动态库(其实这个库中就是包含我们需要替换的函数地址)
第三个参数:动态库入口的函数名称
第四个参数:动态库入口的函数所需要的参数
第五个参数:动态库入口的函数所需要的参数的长度
当然这里我们还有一个是通过进程名称获取到进程的id的函数find_pid_of(...)
既然现在我们需要注入system_server进程中,那么我们就要需要将我们的代码注入到libbiner.so文件中
在sys.c代码中修改几个地方:
第一个修改的地方就是so文件路径:
#define LIBSF_PATH "/system/lib/libbinder.so"
然后就是注入的函数:我们记得在注入surfaceflinger进程的时候,拦截的是eglSwapBuffers函数,我们注入到system_server的话,就是拦截ioctl函数,因为我们知道想使用一些系统服务都是调用这个方法的,下面就对这个函数进行替换:
int (*old_ioctl) (int __fd, unsigned long int __request, void * arg) = 0; // 欲接替ioctl的新函数地址,其中内部调用了老的ioctl int new_ioctl (int __fd, unsigned long int __request, void * arg) { if ( __request == BINDER_WRITE_READ ) { call_count++; LOGD("call_count:%d",call_count); } int res = (*old_ioctl)(__fd, __request, arg); return res; }
在这个函数中,我们会判断一下请求状态_request,如果是BINDER_WRITE_READ,说明上层有请求服务了,这里就是做一个简单的判断,通过一个int值,然后用log将其值打印出来。
int hook_eglSwapBuffers() { old_ioctl = ioctl; void * base_addr = get_module_base(getpid(), LIBSF_PATH); LOGD("libsurfaceflinger.so address = %p\n", base_addr); int fd; fd = open(LIBSF_PATH, O_RDONLY); if (-1 == fd) { LOGD("error\n"); return -1; } Elf32_Ehdr ehdr; read(fd, &ehdr, sizeof(Elf32_Ehdr)); unsigned long shdr_addr = ehdr.e_shoff; int shnum = ehdr.e_shnum; int shent_size = ehdr.e_shentsize; unsigned long stridx = ehdr.e_shstrndx; Elf32_Shdr shdr; lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET); read(fd, &shdr, shent_size); char * string_table = (char *)malloc(shdr.sh_size); lseek(fd, shdr.sh_offset, SEEK_SET); read(fd, string_table, shdr.sh_size); lseek(fd, shdr_addr, SEEK_SET); int i; uint32_t out_addr = 0; uint32_t out_size = 0; uint32_t got_item = 0; int32_t got_found = 0; for (i = 0; i < shnum; i++) { read(fd, &shdr, shent_size); if (shdr.sh_type == SHT_PROGBITS) { int name_idx = shdr.sh_name; if (strcmp(&(string_table[name_idx]), ".got.plt") == 0 || strcmp(&(string_table[name_idx]), ".got") == 0) { out_addr = base_addr + shdr.sh_addr; out_size = shdr.sh_size; LOGD("out_addr = %lx, out_size = %lx\n", out_addr, out_size); for (i = 0; i < out_size; i += 4) { got_item = *(uint32_t *)(out_addr + i); if (got_item == old_ioctl) { LOGD("Found eglSwapBuffers in got\n"); got_found = 1; uint32_t page_size = getpagesize(); uint32_t entry_page_start = (out_addr + i) & (~(page_size - 1)); mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_WRITE); *(uint32_t *)(out_addr + i) = new_ioctl; break; } else if (got_item == new_ioctl) { LOGD("Already hooked\n"); break; } } if (got_found) break; } } } free(string_table); close(fd); }
还有就是hook_eglSwapBuffers函数(函数名都难得改了~~)
好的,修改差不多了,下面我们来编译吧~~
编译会出错的,因为会提示找不到ioctl的定义以及一些常量值,所以我们得找到这个函数的定义,百度一下之后会发现这个函数的定义是在binder.h中,当然这个头文件我们是可以在Android源码中找到的
(关于源码下载和编译的问题:http://blog.csdn.net/jiangwei0910410003/article/details/37988637)。
然后将这个binder.h拷贝到我们编译的目录中,然后再代码中引用一下即可。
再次编译,擦,还是提示错误,说这个函数没有定义。
原因很简单,我们只是引用了头文件,并没有将函数的具体实现引用进来,所以还需要去找到这个函数的具体定义了。
这个过程就是有点麻烦了,纠结了很长时间呀~~,幸好最后搞定了
具体步骤:
从设备的system/lib/ 目录中找到libbinder.so文件,将其拷贝出来,这是一个动态库文件
然后将其放到我们之前的NDK配置目录中的:具体目录如下:
最后我们在Android.mk文件进行引用:
LOCAL_LDLIBS := -llog -lbinder -lutils -landroid_runtime
当然,这里我们会看到-lXXX是通用的格式,同样的,我们如果要用到JVM中的函数的话,会用到libandroid_runtime.so文件,头文件:android_runtime.h,也是可以在源码中找到的(后面会提及到)
(注:这里就介绍了我们如何在使用Android系统中底层的一些函数,同时编译的时候引用到了动态链接库文件)
扩展:
这里在扩展一下:还有另外的一种方式引用so文件:
操作步骤:
首先在我们编译目录中新建一个文件夹:prebuilt
在这个文件夹中存放两个文件:
1.Android.mk:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := binder LOCAL_SRC_FILES := libbinder.so include $(PREBUILT_SHARED_LIBRARY)
2. libbinder.so(这个动态链接库文件就是我们需要用到的)
然后在回到我们编译的目录中,修改一下我们的Android.mk文件:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_LDLIBS := -llog LOCAL_MODULE := sys LOCAL_SRC_FILES := sys.c include $(BUILD_SHARED_LIBRARY) include $(LOCAL_PATH)/prebuilt/Android.mk最重要的一行就是在最后。所以这也是一种方法。当然我们会看到这种方法有一个弊端,就是一次只能引用到一个so文件。所以使用范围比较窄~~
在回到主题上来,我们通过引用libbinder.so文件之后,编译可以通过了,然后将编译之后的libsys.so文件拷贝到/data/目录中,这个和之前的libsuf.so步骤差不多,以及将inject拷贝到/data/目录中。
然后进入到设备的data目录中,执行inject
=> adb shell
=> cd data
=> ./inject
同时检测一下log信息:
adb logcat -s PERMISSIONINTERCEPTER
随便动动手机,call_count就刷刷的变~~
至此,我们看到了,hook进程和拦截ioctl函数成功~~
Demo下载地址:
http://download.csdn.net/detail/jiangwei0910410003/7930603
下面再来深入看一下,如何拦截哪些应用在使用系统服务呢?
首先,我们还是需要对上面我们自定义的new_ioctl函数中的逻辑进行修改一下。
其实这里面就有一个很大的难度了(这个也正是底层C进行拦截的一个弊端),需要分析数据格式,然后才能正确的进行拦截。
下面来看一下具体的代码吧:
// 欲接替ioctl的新函数地址,其中内部调用了老的ioctl int new_ioctl (int __fd, unsigned long int __request, void * arg) { if ( __request == BINDER_WRITE_READ ) { int dir = _IOC_DIR(__request); //根据命令获取传输方向 int type = _IOC_TYPE(__request); //根据命令获取类型 int nr = _IOC_NR(__request); //根据命令获取类型命令 int size = _IOC_SIZE(__request); //根据命令获取传输数据大小 struct binder_write_read* tmp = (struct binder_write_read*) arg; signed long write_size = tmp->write_size; signed long read_size = tmp->read_size; if(write_size > 0)//该命令将write_buffer中的数据写入到binder { //LOGD("binder_write_read----->write size: %d,write_consumed :%d", tmp->write_size, tmp->write_consumed); int already_got_size = 0; unsigned long *pcmd = 0; //LOGD("=================write_buffer process start!"); while(already_got_size < write_size)//循环处理buffer中的每一个命令 { pcmd = (unsigned long *)(tmp->write_buffer + already_got_size); //指针后移 int code = pcmd[0]; //LOGD("pcmd: %x, already_got_size: %d", pcmd, already_got_size); int dir = _IOC_DIR(code); //根据命令获取传输方向 int type = _IOC_TYPE(code); //根据命令获取类型 int nr = _IOC_NR(code); //根据命令获取类型命令 int size = _IOC_SIZE(code); //根据命令获取传输数据大小 //LOGD("cmdcode:%d, dir:%d, type:%c, nr:%d, size:%d\n", code, dir, type, nr, size); struct binder_transaction_data* pdata = (struct binder_transaction_data*)(&pcmd[1]); switch (code) { case BC_TRANSACTION: if(pdata->sender_pid>5000){ //LOGD("code:%d",pdata->code); //LOGD("name:%x",pdata->data.ptr.buffer); //LOGD("pid: %d",pdata->sender_pid); //char *pname = (char*)malloc(50*sizeof(char)); //cfgmng_get_taskname(pdata->sender_pid,pname); //LOGD("pname: %s",pname); //free(pname); //hexdump(pdata->data.ptr.buffer, pdata->data_size); } parse_binder(pdata, 1); break; case BC_REPLY: //LOGD("pid: %d, BC_REPLY, dir:%d, type:%c, nr:%d, size:%d\n", pdata->sender_pid, dir, type, nr, size); parse_binder(pdata, 1); break; default: break; } already_got_size += (size+4); } //LOGD("=================write_buffer process end!"); } if(read_size > 0)//从binder中读取数据写入到read_buffer { //LOGD("binder_write_read----->read size: %d, read_consumed: %d", tmp->read_size, tmp->read_consumed); int already_got_size = 0; unsigned long *pret = 0; //LOGD("=================read_buffer process start!"); while(already_got_size < read_size)//循环处理buffer中的每一个命令 { pret = (unsigned long *)(tmp->read_buffer + already_got_size); //指针后移 int code = pret[0]; //LOGD("pret: %x, already_got_size: %d", pret, already_got_size); int dir = _IOC_DIR(code); //根据命令获取传输方向 int type = _IOC_TYPE(code); //根据命令获取类型 int nr = _IOC_NR(code); //根据命令获取类型命令 int size = _IOC_SIZE(code); //根据命令获取传输数据大小 //LOGD("retcode:%d, dir:%d, type:%c, nr:%d, size:%d\n", code, dir, type, nr, size); struct binder_transaction_data* pdata = (struct binder_transaction_data*)(&pret[1]); switch (code) { case BR_TRANSACTION: if(pdata->sender_pid>5000){ //LOGD("code:%d",pdata->code); //LOGD("name:%s",pdata->data.ptr.buffer->flag); //LOGD("pid: %d",pdata->sender_pid); //char *pname = (char*)malloc(50*sizeof(char)); //getNameByPid(pdata->sender_pid,pname); //LOGD("pname: %s",pname); //free(pname); char * pname = hexdump(pdata->data.ptr.buffer, pdata->data_size); if(isStub(pname,STUB) == 1 && pid!=pdata->sender_pid) { pid = pdata->sender_pid; //指定一下log的输出格式:服务的名称&&应用的进程id LOGD("%s&&%d",pname,pdata->sender_pid); //在一个线程中开启socket /*pthread_t tid; int status = pthread_create(&tid,NULL,&new_socket,(void*)&data); if(status != 0){ LOGD("can't create thread"); }else{ LOGD("create thread success"); }*/ //开一个socket //new_socket(pid,pname); //采用动态调用so文件中的函数f() /*void * dp = dlopen("libmiddle.so",RTLD_NOW); LOGD("dp is %p",dp); int (*f)() = dlsym(dp,"f"); LOGD("func pointer==%p",f); if(f == NULL) { LOGD("not find func"); } else { (*f)(); }*/ } free(pname); } parse_binder(pdata, 2); break; case BR_REPLY: //LOGD("pid: %d, BR_REPLY, dir:%d, type:%c, nr:%d, size:%d\n", pdata->sender_pid, dir, type, nr, size); parse_binder(pdata, 2); break; default: break; } already_got_size += (size+4);//数据内容加上命令码 } //LOGD("=================read_buffer process end!"); } } if (old_ioctl == -1) { //LOGD("error\n"); return; } int res = (*old_ioctl)(__fd, __request, arg); return res; }
这个函数修改的逻辑就比较多了~~
首先来了解一下Binder在传输数据中比较关键的一个数据结构:binder_transaction_data
了解了这个数据结构之后,我们就可以从这个数据结构中提取出我们想要的服务的名称(这一步真的很重要,也是网上很多人咨询的一个问题,关于这个问题,我是阅读了一本比较好的资料《Android框架揭秘》是棒子写的一本书,里面有一些错误,但是不影响大体的知识方向)、同时也是可以获取到进程id,Android中一个应用一般就是一个进程,所以我们可以粗略的通过进程id来来获取应用的一些详细信息(可以获取最近正在运行的应用信息列表中进行过滤)
获取服务的名称的主要操作:
char * pname = hexdump(pdata->data.ptr.buffer, pdata->data_size);
然后就是data.ptr.buffer的数据结构:
那么现在问题就是如何能将这个服务的名称正确的取出来呢?那么这个就要看hexdump函数了
从字面意义上看应该是将hex文本转化一下:
char* hexdump(void *_data, unsigned len) { unsigned char *data = _data; char *dataAry = (char*)malloc(len*(sizeof(char))); char *dataTmp = dataAry; unsigned count; for (count = 0; count < len; count++) { if ((count & 15) == 0) LOGD(stderr,"%04x:", count); //only show charset and '.' if(((*data >= 65) && (*data <= 90)) || ((*data >= 97) && (*data <= 122)) || (*data == 46)) { *dataAry = *data; dataAry++; } data++; if ((count & 15) == 15) LOGD(stderr,"\n"); } *dataAry = '\0'; return dataTmp; if ((count & 15) != 0) LOGD(stderr,"\n"); }
看到中间的一个核心的if判断,那个就是用来过滤服务的包名的:大小写字母+点号,这样就能从buffer中提取出服务的包名了,然后返回即可(说实话,这个问题也是纠结了我好长时间,解决了还是很开心的)
在函数new_ioctl中最主要的核心代码:
switch (code) { case BR_TRANSACTION: if(pdata->sender_pid>5000){ //LOGD("pid: %d, BR_TRANSACTION, dir:%d, type:%c, nr:%d, size:%d\n", pdata->sender_pid, dir, type, nr, size); //LOGD("code:%d",pdata->code); //LOGD("name:%s",pdata->data.ptr.buffer->flag); //LOGD("pid: %d",pdata->sender_pid); //char *pname = (char*)malloc(50*sizeof(char)); //getNameByPid(pdata->sender_pid,pname); //LOGD("pname: %s",pname); //free(pname); char * pname = hexdump(pdata->data.ptr.buffer, pdata->data_size); if(isStub(pname,STUB) == 1 && pid!=pdata->sender_pid) { pid = pdata->sender_pid; //指定一下log的输出格式:服务的名称&&应用的进程id LOGD("%s&&%d",pname,pdata->sender_pid); //在一个线程中开启socket /*pthread_t tid; int status = pthread_create(&tid,NULL,&new_socket,(void*)&data); if(status != 0){ LOGD("can't create thread"); }else{ LOGD("create thread success"); }*/ //开一个socket //new_socket(pid,pname); //采用动态调用so文件中的函数f() /*void * dp = dlopen("libmiddle.so",RTLD_NOW); LOGD("dp is %p",dp); int (*f)() = dlsym(dp,"f"); LOGD("func pointer==%p",f); if(f == NULL) { LOGD("not find func"); } else { (*f)(); }*/ } free(pname); } parse_binder(pdata, 2); break; case BR_REPLY: //LOGD("pid: %d, BR_REPLY, dir:%d, type:%c, nr:%d, size:%d\n", pdata->sender_pid, dir, type, nr, size); parse_binder(pdata, 2); break; default: break; }
这个函数中就是判断请求的方式
当code等于BR_TRANSACTION
表示开始读取数据,所以我们这时候就可以在这里进行数据的拦截分析,这里在获取进程id的时候,我做了一次判断就是判断进程id大于5000的,这个5000只是我随便取的一个值,因为如果这里不做判断的话,在后面打印log信息的时候很不方便,因为系统有很多应用都可能在获取服务,log信息有点多吧,这里就相当于做个过滤。同时这里还有一个过滤条件,就是服务名称中有ILocationManager的,因为开始的时候只是想验证一下这个不做能否成功,所以先用位置信息来做实验。
效果图:
Demo下载地址:http://download.csdn.net/detail/jiangwei0910410003/7932293
上面的拦截操作算是完成了,那么下面我们还得来做一件事(这件事也是网上好多同学纠结的一个问题)
就是如何将我们这里拦截到的信息包括是哪个应用获取服务的进程id以及获取服务的名称,如何将其这些信息传递到上层然后进行显示(比如360弹出的对话框,或者是在通知栏中:XXX应用正在获取你的XXX信息,拒绝还是允许),如下图:
我也花了一个礼拜的时间去解决的,找到三种方式:
第一种方式:通过上层(就是我们的app,建立一个SockeServer),然后在底层拦截到的详细信息通过socket传递到server中。
底层的代码如下:
void new_socket(int pid,char *pname){ //将进程id转化成字符串然后和服务名称进行拼接 pname = strcat(int_to_str(pid),pname); LOGD("start trasact data ...%s",pname); int sockfd,sendbytes; char buf[MAXDATASIZE]; struct hostent *host = gethostbyname(SERVIP); struct sockaddr_in serv_addr; if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){ LOGD("socket fail"); exit(1); } serv_addr.sin_family=AF_INET; serv_addr.sin_port=htons(SERVPORT); serv_addr.sin_addr.s_addr = inet_addr(SERVIP); bzero(&(serv_addr.sin_zero),8); if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1){ LOGD("connect fail"); //exit(1); } if((sendbytes=send(sockfd,pname,strlen(pname),0))==-1){ LOGD("send fail"); //exit(1); } free(pname); close(sockfd); }
这个是在Linux中建立一个Socket,具体的操作这里就不做解释了,自行可以去搜索一下Linux中如何建立socket。
下面在来看一下上层的app中建立一个SocketServer:
package com.sohu.intercepter.demo; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class MainActivity extends Activity { private static final String TAG = "PERMISSIONINTERCEPTER"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(){ @Override public void run(){ java.net.ServerSocket sc = null; try { sc = new java.net.ServerSocket(3333); Log.i(TAG,"socket create success"); while (true) { java.net.Socket client = sc.accept(); String content = streamToString(client.getInputStream()); Log.i(TAG, "accept ==> "+content); } } catch (Exception ef) { Log.i(TAG, "exception===>"+ef.getMessage()); }finally{ Log.i(TAG, "stop connection"); if (sc != null){ try { sc.close(); } catch (IOException e) { Log.i(TAG, "exception===>"+e.getMessage()); } sc = null; } } } }.start(); } public String streamToString(InputStream is){ if(is == null) return ""; ByteArrayOutputStream bos = null; try{ byte[] buffer = new byte[1024]; int len = 0; bos = new ByteArrayOutputStream(); while((len=is.read(buffer)) != -1){ bos.write(buffer, 0, len); } return new String(bos.toByteArray()); }catch(Exception e){ Log.i("TTT", "exception--->"+e.getMessage()); }finally{ try { is.close(); bos.close(); } catch (IOException e) { Log.i("TTT", "exception===>"+e.getMessage()); } } return ""; } }
这段代码一般是放在Service中的,因为要始终监听,同时这里开启的端口是:3333.为了防止和其他应用的端口冲突,一边最好起个比较有个性的端口,当然这里没有做更好的优化,比如当建立端口的时候发现这个端口已经被占用了怎么办?做的好的应该做一下端口的判断,然后找到没被占用的端口(可以用Socket探针技术)。
具体步骤:
1.修改一下sys.c文件中,使用socket进行传输数据(将对应的注释代码打开即可),编译之后,将libsys.so拷贝到/data/目录中
2.运行上层代码,安装到手机中,运行应用(打开SocketServer监听)
3.进入到/data/目录,运行inject
4.执行命令:
=> adb shell
=> cd data
=> su
=> ./inject
5.同时检测log信息:adb logcat -s PERMISSIONINTERCEPTER
结果图:
第二种方式:有点复杂了就是使用JNI技术了,我们需要在底层中调用上层的Java代码。这个技术就是可以在C++代码中使用类加载机制+反射技术调用Java中的指定方法。这里有一个问题就是如何获取JVM对象以及JNIEnv对象。
众所周知,Android的应用进程,都是由Zygote孵化的子进程,每个进程都运行在独立的JVM中(具体的知识可以查看老罗的blog)。
那么JVM对象可以获取到,那么JNIEnv对象呢?我们知道,在JVM进程中,JavaVM是全局唯一的,而JNIEnv则是按线程分配。另外,Dalvik的线程跟Linux线程是一一对应的,因此我们可以把自身所在的线程Attatch到JavaVM,JavaVM就会为我们分配JNIEnv对象了。通过阅读Dalvik源码,从AndroidRuntime类中我们可以得到JavaVm的地址,再通过JavaVm所提供的AttachCurrentThead和DetachCurrentThread两个函数,即可完成JNIEnv的获取。
具体代码如下:
importdex.cpp代码
#include <stdio.h> #include <stddef.h> #include <jni.h> #include <android_runtime/AndroidRuntime.h> #include "log.h" using namespace android; static const char JSTRING[] = "Ljava/lang/String;"; static const char JCLASS_LOADER[] = "Ljava/lang/ClassLoader;"; static const char JCLASS[] = "Ljava/lang/Class;"; static JNIEnv* jni_env; static char sig_buffer[512]; //ClassLoader.getSystemClassLoader() static jobject getSystemClassLoader(){ jclass class_loader_claxx = jni_env->FindClass("java/lang/ClassLoader"); snprintf(sig_buffer, 512, "()%s", JCLASS_LOADER); jmethodID getSystemClassLoader_method = jni_env->GetStaticMethodID(class_loader_claxx, "getSystemClassLoader", sig_buffer); return jni_env->CallStaticObjectMethod(class_loader_claxx, getSystemClassLoader_method); } void Main() { JavaVM* jvm = AndroidRuntime::getJavaVM(); jvm->AttachCurrentThread(&jni_env, NULL); //TODO 使用JNIEnv //jvm->DetachCurrentThread(); jstring apk_path = jni_env->NewStringUTF("/data/local/tmp/DemoInject2.apk"); jstring dex_out_path = jni_env->NewStringUTF("/data/data/"); jclass dexloader_claxx = jni_env->FindClass("dalvik/system/DexClassLoader"); snprintf(sig_buffer, 512, "(%s%s%s%s)V", JSTRING, JSTRING, JSTRING, JCLASS_LOADER); jmethodID dexloader_init_method = jni_env->GetMethodID(dexloader_claxx, "<init>", sig_buffer); snprintf(sig_buffer, 512, "(%s)%s", JSTRING, JCLASS); jmethodID loadClass_method = jni_env->GetMethodID(dexloader_claxx, "loadClass", sig_buffer); jobject class_loader = getSystemClassLoader(); check_value(class_loader); jobject dex_loader_obj = jni_env->NewObject( dexloader_claxx, dexloader_init_method, apk_path, dex_out_path, NULL, class_loader); jstring class_name = jni_env->NewStringUTF("com.demo.inject2.EntryClass"); jclass entry_class = static_cast<jclass>(jni_env->CallObjectMethod(dex_loader_obj, loadClass_method, class_name)); jmethodID invoke_method = jni_env->GetStaticMethodID(entry_class, "invoke", "(I)[Ljava/lang/Object;"); check_value(invoke_method); jobjectArray objectarray = (jobjectArray) jni_env->CallStaticObjectMethod(entry_class, invoke_method, 0); jvm->DetachCurrentThread(); }
这里会遇到一个问题就是AndroidRuntime类找不到的问题,可以google一下,他的定义在android_runtime.h头文件中,这个同样也是可以从Android源码中找到的,同时这个AndroidRuntime类的实现可以从设备的/system/lib/目录中找到动态库文件libandroid_runtime.so。和之前使用libbinder.so差不多的步骤,这里就不做说明了。
编译项目下载地址:
http://download.csdn.net/detail/jiangwei0910410003/7932355
编译之后得到importdex.so文件,这个在后面会用到。
下面来具体看一下代码。在看代码之前需要先了解一下Android中的类加载机制,请看下面的一篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/17679823
如果这篇文章看懂了,其实上面的代码就没有任何难度了,其实就是C++中实现这种方式,和Java中的反射机制很相似,
在代码中还可以看到DexClassLoader这个类的一个参数是dex文件的输出目录。这里直接放到了/data/data目录中。
同时我们需要将开发一个DemoInject2.apk,这里面要有一个类:com.demo.inject2.EntryClass,在这个类中需要定义一个静态的invoke方法:
package com.demo.inject2; import android.content.Context; import android.util.Log; import android.widget.Toast; public final class EntryClass { public static Object[] invoke(int i) { try { Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy 2!!!!<<<<<<<<<<<<<<"); Context context = ContexHunter.getContext(); Toast.makeText(context, "Success", Toast.LENGTH_LONG).show(); /*Class<?> MainActivity_class = context.getClassLoader().loadClass("com.demo.host.MainActivity"); Method setA_method = MainActivity_class.getDeclaredMethod("setA", int.class); setA_method.invoke(null, 1);*/ } catch (Exception e) { e.printStackTrace(); } return null; } }然后我们就可以通过底层调用这个方法,将进程id和服务的名称作为参数传递过去,但是在使用这种方法的时候遇到一个问题:
那就是我们看到sys.c是C程序,但是上面的那个程序是C++的,所以这里面会牵扯到一个问题就是怎么在C程序中调用C++的代码,这个可以看一下下面的一篇文章是怎么操作:
http://blog.csdn.net/jiangwei0910410003/article/details/39312947
下面看看我们这里是如何做到的
这里会用到一个libimportdex.so文件(上面编译之后的文件),还需要在弄一个编译项目middle.middle文件夹:
middle.c:
#include "importdex.h" extern "C"{ int f() { callback(); return 0; } }
Android.mk:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_LDLIBS := -llog -lbinder -lutils -limportdex LOCAL_MODULE := middle LOCAL_SRC_FILES := middle.cpp include $(BUILD_SHARED_LIBRARY)这里我们还需要将之前的编译得到的libimportdex.so文件拷贝到NDK的配置目录中
编译项目下载地址:
http://download.csdn.net/detail/jiangwei0910410003/7932369
底层的执行so文件中指定的函数的核心代码:
void * dp = dlopen("libmiddle.so",RTLD_NOW); LOGD("dp is %p",dp); int (*f)() = dlsym(dp,"f"); LOGD("func pointer==%p",f); if(f == NULL) { LOGD("not find func"); } else { (*f)(); }
最后如果想成功的话,还需要将importdex.so文件以及libmiddle.so文件拷贝到设备的/system/lib/目录下面,因为你需要引用。
第三种方式:使用Log日志来传递数据(这个是最终的解决方法)
这个实现原理很简单,就是在底层C中,我们拿到的了进程的id和服务的名称然后使用特定的规则将其进行拼接,然后在上层的app进行log的拦截,然后获取指定的值,这里会遇到一个问题就是如何能在本应用中获取其他进程中的log信息,如果我们在本应用中获取log信息很简单的,但是获取其他进程的log信息遇到点问题,但是最后靠着自己摸索,发现在su权限的情况下,可以获取到所有进程的log信息,所以我们要获取su权限。
在上层代码中取指定log信息代码:
package com.isoft.log; import java.io.DataInputStream; import android.annotation.SuppressLint; import android.app.Service; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.util.Log; import android.widget.Toast; public class LogObserverService extends Service implements Runnable{ private String TAG = "LOG"; private StringBuffer logContent = null; private final static String[] CMDS = new String[]{"/system/bin/su","-c","logcat -s PERMISSIONINTERCEPTER"}; @SuppressLint("HandlerLeak") private Handler handler = new Handler(){ @Override public void dispatchMessage(Message msg) { if(msg.what == 0){ String content = (String)msg.obj; Toast.makeText(getApplicationContext(), "conent:"+content, Toast.LENGTH_LONG).show(); } super.dispatchMessage(msg); } }; @Override public void onCreate() { super.onCreate(); Log.i(TAG,"onCreate"); logContent = new StringBuffer(); new Thread(this).start(); } @Override public void run() { Process pro = null; try { pro = Runtime.getRuntime().exec(CMDS); }catch (Exception e) { Log.i(TAG, "异常:"+e.getMessage()); e.printStackTrace(); } DataInputStream dis = new DataInputStream(pro.getInputStream()); String line = null; while (true) { try { while ((line = dis.readLine()) != null) { if(line != null){ String[] lineAry = line.split("&&"); if(lineAry.length == 2){ //检测一下获取有位置信息的log信息 if(lineAry[0].contains("ILocationManager")){ Message msg = new Message(); msg.what = 0; msg.obj = "进程id:"+lineAry[1] + "\n获取位置信息"; handler.sendMessage(msg); } } } String temp = logContent.toString(); logContent.delete(0, logContent.length()); logContent.append(line); logContent.append("\n"); logContent.append(temp); Thread.yield(); } } catch (Exception e) { e.printStackTrace(); } } } @Override public IBinder onBind(Intent intent) { return null; } }
主要的执行命令:
private final static String[] CMDS = new String[]{"/system/bin/su","-c","logcat -s PERMISSIONINTERCEPTER"};
这样就可以获取到指定的Tag的log信息,然后对其进行解析,获取进程id和服务的名称。
这三种方式的实现的代码的Demo下载地址(前两种方式我在代码中注释了,如果想用的话,可以打开注释)
总结一下,为什么会有这三种方式的产生呢?首先在有这种需求,想到的解决方式就是第一种。但是我在实现第一种方式的时候,出现了一个问题,就是运行一段时间总是会死机,总是需要reboot命令重启或者扣电池了~~,现在还没找到解决方案。所以就延伸出第二种方式了,但是第二种方式可以是可以,没先到也会死机和第一种方式的问题一样。现在还没有找到解决方案。所以就延伸出第三种方式了,因为第三种方式真的很简单。但是第三种方式有一个很大的问题,就是不能和底层进行交互,因为当我们在上层app中会点击拒绝的话,就是不给某个应用获取此服务,所以我们需要得到用户的选择状态值,这样才能在底层做具体的操作。回想一下,第一种和第二种方式都是可以实现的。但是会出现死机的情况。所以这个在后续是一定要解决的。
最后在来感受一下效果吧:
(ps:关于这个注入和拦截的问题,网上有很多同学都在研究,可能有的同学已经成功了,但是有的同学没有,所以我就将这篇文章分享了一下,因为在这个过程中,我懂得那种遇到问题无助的感触,所以如果你看完了这个文章,如果遇到任何问题,请给我留言,我能帮你解决的尽量解决一下)